diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 93e401c776..96f4ff6974 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -107,6 +107,58 @@ &-notifications-badge-label { font-size: 12px; } + + &-scrollbox { + &-scrollview { + &.vfade { + -st-vfade-offset: 68px; + -st-hfade-offset: 0; + } + &.hfade { + -st-hfade-offset: 68px; + -st-vfade-offset: 0; + } + } + + &-button { + transition-duration: 100ms; + + &-icon { + icon-size: $scalable_icon_size; + color: inherit; + } + + &.vertical { } + + &-start { + margin-right: 1px; + + &.vertical { + margin-right: 0; + margin-bottom: 1px; + } + } + + &-end { + margin-left: 1px; + + &.vertical { + margin-left: 0; + margin-top: 1px; + } + } + + &:hover { + background-color: $lighter_bg_color; + color: $fg_color; + } + + &:active { + background-color: $lightest_bg_color; + color: $fg_color; + } + } + } } // classic window list diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index 1a12a801bc..ad1bbc1dee 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -375,10 +375,6 @@ var AppGroup = class AppGroup { getPreferredWidth(actor, forHeight, alloc) { const [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_width(forHeight); const [labelMinSize, labelNaturalSize] = this.label.get_preferred_width(forHeight); - // The label text starts in the center of the icon, so we should allocate the space - // needed for the icon plus the space needed for(label - icon/2) - alloc.min_size = 1 * global.ui_scale; - const {appId} = this.groupState; const allocateForLabel = this.labelVisiblePref || @@ -393,16 +389,18 @@ var AppGroup = class AppGroup { } else { alloc.natural_size = iconNaturalSize + 6 * global.ui_scale; } + alloc.min_size = alloc.natural_size; } else { alloc.natural_size = this.state.trigger('getPanelHeight'); + alloc.min_size = 1 * global.ui_scale; } } getPreferredHeight(actor, forWidth, alloc) { let [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_height(forWidth); let [labelMinSize, labelNaturalSize] = this.label.get_preferred_height(forWidth); - alloc.min_size = Math.min(iconMinSize, labelMinSize); alloc.natural_size = Math.max(iconNaturalSize, labelNaturalSize); + alloc.min_size = alloc.natural_size; } allocate(actor, box, flags) { @@ -555,7 +553,7 @@ var AppGroup = class AppGroup { } onEnter() { - if (this.state.panelEditMode) return false; + if (this.state.panelEditMode || this.state.scrollActive) return false; this.actor.add_style_pseudo_class('hover'); @@ -642,7 +640,7 @@ var AppGroup = class AppGroup { const {appId, metaWindows, lastFocused} = this.groupState; if (hasFocus === undefined) { - hasFocus = this.workspaceState.lastFocusedApp === appId; + hasFocus = this.workspaceState.lastFocusedApp === appId && getFocusState(lastFocused); } // If any of the windows associated with our app have focus, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 6a110acf12..22303a1360 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -110,7 +110,7 @@ class PinnedFavs { const currentWorkspace = this.params.state.trigger('getCurrentWorkspace'); const newFavorites = []; let refActorFound = false; - currentWorkspace.actor.get_children().forEach( (actor, i) => { + currentWorkspace.container.get_children().forEach( (actor, i) => { const appGroup = currentWorkspace.appGroups.find( appGroup => appGroup.actor === actor ); if (!appGroup) return; const {app, appId, isFavoriteApp} = appGroup.groupState; @@ -254,6 +254,7 @@ class GroupedWindowListApplet extends Applet.Applet { removeFavorite: (id) => this.pinnedFavorites.removeFavorite(id), getFavorites: () => this.pinnedFavorites._favorites, cycleWindows: (e, source) => this.handleScroll(e, source), + handleScroll: (e) => this.handleScroll(e), openAbout: () => this.openAbout(), configureApplet: () => this.configureApplet(), removeApplet: (event) => this.confirmRemoveApplet(event) @@ -300,7 +301,7 @@ class GroupedWindowListApplet extends Applet.Applet { bindSettings() { const settingsProps = [ {key: 'group-apps', value: 'groupApps', cb: this.refreshAllWorkspaces}, - {key: 'scroll-behavior', value: 'scrollBehavior', cb: null}, + {key: 'list-scroll-behavior', value: 'scrollBehavior', cb: null}, {key: 'left-click-action', value: 'leftClickAction', cb: null}, {key: 'middle-click-action', value: 'middleClickAction', cb: null}, {key: 'show-all-workspaces', value: 'showAllWorkspaces', cb: this.refreshAllWorkspaces}, @@ -314,6 +315,7 @@ class GroupedWindowListApplet extends Applet.Applet { {key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange}, {key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange}, {key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged}, + {key: 'enable-click-to-slide', value: 'enableClickToSlide', cb: null}, {key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null}, {key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState}, {key: 'animate-thumbnails', value: 'animateThumbs', cb: null}, @@ -794,6 +796,18 @@ class GroupedWindowListApplet extends Applet.Applet { && St.Widget.get_default_direction () === St.TextDirection.RTL; const axis = this.state.isHorizontal ? [x, 'x2'] : [y, 'y2']; + + let adjustmentValue = 0; + if (currentWorkspace.scrollBox) { + const adjustment = this.state.isHorizontal ? + currentWorkspace.scrollBox.scrollView.get_hscroll_bar().get_adjustment() : + currentWorkspace.scrollBox.scrollView.get_vscroll_bar().get_adjustment(); + adjustmentValue = adjustment.get_value(); + } + + // Add scroll position to current coordinate + axis[0] += adjustmentValue; + if(rtl_horizontal) axis[0] = this.actor.width - axis[0]; @@ -801,7 +815,7 @@ class GroupedWindowListApplet extends Applet.Applet { if(this.state.dragging.posList === null){ this.state.dragging.isForeign = !(source instanceof AppGroup); this.state.dragging.posList = []; - currentWorkspace.actor.get_children().forEach( child => { + currentWorkspace.container.get_children().forEach( child => { let childPos; if(rtl_horizontal) childPos = this.actor.width - child.get_allocation_box()['x1']; @@ -815,7 +829,7 @@ class GroupedWindowListApplet extends Applet.Applet { let pos = 0; while(pos < this.state.dragging.posList.length && axis[0] > this.state.dragging.posList[pos]) pos++; - + let favLength = 0; for (const appGroup of currentWorkspace.appGroups) { if(appGroup.groupState.isFavoriteApp) @@ -835,13 +849,13 @@ class GroupedWindowListApplet extends Applet.Applet { if(this.state.dragging.isForeign) { if (this.state.dragging.dragPlaceholder) - currentWorkspace.actor.set_child_at_index(this.state.dragging.dragPlaceholder, pos); + currentWorkspace.container.set_child_at_index(this.state.dragging.dragPlaceholder, pos); else { const iconSize = this.getPanelIconSize() * global.ui_scale; this.state.dragging.dragPlaceholder = new DND.GenericDragPlaceholderItem(); this.state.dragging.dragPlaceholder.width = iconSize; this.state.dragging.dragPlaceholder.height = iconSize; - currentWorkspace.actor.insert_child_at_index( + currentWorkspace.container.insert_child_at_index( this.state.dragging.dragPlaceholder, this.state.dragging.pos ); @@ -849,7 +863,7 @@ class GroupedWindowListApplet extends Applet.Applet { } } else - currentWorkspace.actor.set_child_at_index(source.actor, pos); + currentWorkspace.container.set_child_at_index(source.actor, pos); } if(this.state.dragging.isForeign) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js new file mode 100644 index 0000000000..1d96512fb3 --- /dev/null +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -0,0 +1,409 @@ +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const St = imports.gi.St; +const { SignalManager } = imports.misc.signalManager; + +const EDGE_SCROLL_ZONE_SIZE = 48; +const SLIDE_SPEED = 8; +const SLIDE_INTERVAL = 16; + +var ScrollBox = class ScrollBox { + constructor(state) { + this.state = state; + this.signals = new SignalManager(null); + + this.scrollView = new St.ScrollView({ + style_class: 'grouped-window-list-scrollbox-scrollview', + x_expand: true, + y_expand: true, + reactive: true, + }); + + this.scrollView.set_auto_scrolling(false); + this.scrollView.set_mouse_scrolling(true); + this.scrollView.set_clip_to_allocation(true); + this.scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); + + this.container = new St.BoxLayout({ + vertical: !this.state.isHorizontal, + style_class: 'grouped-window-list-scrollbox-container', + }); + + this.scrollView.add_actor(this.container); + + // Slider buttons + this.startButton = new St.Bin({ + style_class: 'grouped-window-list-scrollbox-button grouped-window-list-scrollbox-button-start', + visible: false, + reactive: true, + can_focus: true, + track_hover: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + this.endButton = new St.Bin({ + style_class: 'grouped-window-list-scrollbox-button grouped-window-list-scrollbox-button-end', + visible: false, + reactive: true, + can_focus: true, + track_hover: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + this.startIcon = new St.Icon({ + icon_name: 'xsi-pan-start-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' + }); + this.endIcon = new St.Icon({ + icon_name: 'xsi-pan-end-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' + }); + this.startButton.set_child(this.startIcon); + this.endButton.set_child(this.endIcon); + + // Wrapper actor: [startButton] [scrollView] [endButton] + const managerOrientation = this.state.isHorizontal + ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + this.mainLayout = new Clutter.BoxLayout({ orientation: managerOrientation }); + this.actor = new Clutter.Actor({ + layout_manager: this.mainLayout, + reactive: true, + x_expand: true, + y_expand: true + }); + + this.actor.add_child(this.startButton); + this.actor.add_child(this.scrollView); + this.actor.add_child(this.endButton); + + this.slideTimerSourceId = 0; + this.slideDirection = 0; + this.scrollActiveTimeoutId = 0; + + // Slider button signals + this.signals.connect(this.startButton, 'enter-event', () => { + if (!this.state.settings.enableClickToSlide) + this._startSlide(-1); + }); + this.signals.connect(this.startButton, 'leave-event', () => { + this._stopSlide(); + this.startButton.remove_style_pseudo_class('active'); + }); + this.signals.connect(this.endButton, 'enter-event', () => { + if (!this.state.settings.enableClickToSlide) + this._startSlide(1); + }); + this.signals.connect(this.endButton, 'leave-event', () => { + this._stopSlide(); + this.endButton.remove_style_pseudo_class('active'); + }); + this.signals.connect(this.startButton, 'button-press-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + if (this.state.settings.enableClickToSlide) { + this.startButton.add_style_pseudo_class('active'); + this._startSlide(-1); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.startButton, 'button-release-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + this.startButton.remove_style_pseudo_class('active'); + if (this.state.settings.enableClickToSlide) { + this._stopSlide(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.endButton, 'button-press-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + if (this.state.settings.enableClickToSlide) { + this.endButton.add_style_pseudo_class('active'); + this._startSlide(1); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.endButton, 'button-release-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + this.endButton.remove_style_pseudo_class('active'); + if (this.state.settings.enableClickToSlide) { + this._stopSlide(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + + // Scroll view signals + this.signals.connect(this.scrollView, 'scroll-event', (actor, event) => this._onScroll(actor, event)); + + // Track content size changes + this.signals.connect(this.container, 'allocation-changed', () => this.updateScrollButtonVisibility()); + + this.stateConnectionID = this.state.connect({ + orientation: (state) => this.on_orientation_changed() + }); + + this.on_orientation_changed(); + this._connectAdjustmentSignals(); + } + + destroy() { + this._stopSlide(); + this._disconnectAdjustmentSignals(); + if (this.stateConnectionID) { + this.state.disconnect(this.stateConnectionID); + } + this.signals.disconnectAllSignals(); + this.actor.destroy(); + } + + _connectAdjustmentSignals() { + this._disconnectAdjustmentSignals(); + + const adjustment = this._getScrollAdjustment(); + + if (!adjustment) return; + + this._currentAdjustment = adjustment; + this._adjustmentValueSigId = adjustment.connect('notify::value', () => this.updateScrollButtonVisibility()); + this._adjustmentChangedSigId = adjustment.connect('changed', () => this.updateScrollButtonVisibility()); + } + + _disconnectAdjustmentSignals() { + if (this._adjustmentValueSigId && this._currentAdjustment) { + this._currentAdjustment.disconnect(this._adjustmentValueSigId); + this._adjustmentValueSigId = 0; + } + if (this._adjustmentChangedSigId && this._currentAdjustment) { + this._currentAdjustment.disconnect(this._adjustmentChangedSigId); + this._adjustmentChangedSigId = 0; + } + this._currentAdjustment = null; + } + + on_orientation_changed() { + this.container.vertical = !this.state.isHorizontal; + + const managerOrientation = this.state.isHorizontal + ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + this.mainLayout.set_orientation(managerOrientation); + + if (this.state.isHorizontal) { + this.scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); + this.scrollView.remove_style_class_name('vfade'); + this.scrollView.add_style_class_name('hfade'); + + this.startIcon.set_icon_name('xsi-pan-start-symbolic'); + this.endIcon.set_icon_name('xsi-pan-end-symbolic'); + + this.startButton.remove_style_class_name('vertical'); + this.startButton.set_x_expand(false); + this.startButton.set_y_expand(true); + this.startButton.set_y_align(Clutter.ActorAlign.FILL); + + this.endButton.remove_style_class_name('vertical'); + this.endButton.set_x_expand(false); + this.endButton.set_y_expand(true); + this.endButton.set_y_align(Clutter.ActorAlign.FILL); + } else { + this.scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); + this.scrollView.remove_style_class_name('hfade'); + this.scrollView.add_style_class_name('vfade'); + + this.startIcon.set_icon_name('xsi-pan-up-symbolic'); + this.endIcon.set_icon_name('xsi-pan-down-symbolic'); + + this.startButton.add_style_class_name('vertical'); + this.startButton.set_x_expand(true); + this.startButton.set_y_expand(false); + this.startButton.set_x_align(Clutter.ActorAlign.FILL); + + this.endButton.add_style_class_name('vertical'); + this.endButton.set_x_expand(true); + this.endButton.set_y_expand(false); + this.endButton.set_x_align(Clutter.ActorAlign.FILL); + } + + this._connectAdjustmentSignals(); + this.updateScrollButtonVisibility(); + } + + _getFadeOffset() { + const fade_eff = this.scrollView.get_effect('fade'); + + let fade_offset = 0; + if (fade_eff) { + fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset; + } + + return fade_offset > 0 ? fade_offset : EDGE_SCROLL_ZONE_SIZE * global.ui_scale; + } + + scrollToChild(childActor) { + if (!childActor || !childActor.has_allocation()) return; + + // Get allocation of child relative to container + const allocation = childActor.get_allocation_box(); + + // Child coordinates + let c1, c2; + + const isHorizontal = this.state.isHorizontal; + let adjustment; + + if (isHorizontal) { + c1 = allocation.x1; + c2 = allocation.x2; + const hBar = this.scrollView.get_hscroll_bar(); + if (hBar) adjustment = hBar.get_adjustment(); + } else { + c1 = allocation.y1; + c2 = allocation.y2; + const vBar = this.scrollView.get_vscroll_bar(); + if (vBar) adjustment = vBar.get_adjustment(); + } + + if (adjustment) { + const current = adjustment.value; + const page_size = adjustment.page_size; + + let fade_offset = this._getFadeOffset(); + + if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) { + const newValue = (c1 + c2) / 2 - page_size / 2; + adjustment.value = Math.max(adjustment.lower, Math.min(newValue, adjustment.upper - page_size)); + } + } + } + + updateScrollButtonVisibility() { + const adjustment = this._getScrollAdjustment(); + if (!adjustment) { + this.startButton.visible = false; + this.endButton.visible = false; + return; + } + + const canScroll = adjustment.upper > adjustment.page_size; + if (canScroll) { + // Tolerance of 1 pixel to avoid flickering + this.startButton.visible = adjustment.value > adjustment.lower + 1; + this.endButton.visible = adjustment.value < adjustment.upper - adjustment.page_size - 1; + } else { + this.startButton.visible = false; + this.endButton.visible = false; + } + } + + _startSlide(direction) { + if (direction === this.slideDirection || this.state.panelEditMode) return; + + this._stopSlide(); + this.slideDirection = direction; + this.state.scrollActive = true; + + this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SLIDE_INTERVAL, () => { + const adjustment = this._getScrollAdjustment(); + if (!adjustment) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + const newValue = adjustment.value + (SLIDE_SPEED * this.slideDirection * global.ui_scale); + adjustment.value = Math.max(adjustment.lower, + Math.min(newValue, adjustment.upper - adjustment.page_size)); + + // Stop if we've reached the bounds + if (this.slideDirection < 0 && adjustment.value <= adjustment.lower) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + if (this.slideDirection > 0 && adjustment.value >= adjustment.upper - adjustment.page_size) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + return GLib.SOURCE_CONTINUE; + }); + } + + _stopSlide() { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + this.slideDirection = 0; + + if (this.scrollActiveTimeoutId) { + GLib.source_remove(this.scrollActiveTimeoutId); + } + + this.scrollActiveTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => { + if (this.slideDirection === 0) { + this.state.scrollActive = false; + } + this.scrollActiveTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _getScrollAdjustment() { + if (this.state.isHorizontal) { + const hBar = this.scrollView.get_hscroll_bar(); + return hBar ? hBar.get_adjustment() : null; + } else { + const vBar = this.scrollView.get_vscroll_bar(); + return vBar ? vBar.get_adjustment() : null; + } + } + + _onScroll(actor, event) { + if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; + + if (this.state.settings.scrollBehavior !== 4) { + this.state.trigger('handleScroll', event); + return Clutter.EVENT_STOP; + } + + // Handle horizontal scrolling with vertical wheel + if (this.state.isHorizontal) { + const direction = event.get_scroll_direction(); + let delta = 0; + + const hBar = this.scrollView.get_hscroll_bar(); + if (!hBar) return Clutter.EVENT_PROPAGATE; + + const adjustment = hBar.get_adjustment(); + if (!adjustment) return Clutter.EVENT_PROPAGATE; + + if (direction === Clutter.ScrollDirection.UP) { + delta = -10 * global.ui_scale; + } else if (direction === Clutter.ScrollDirection.DOWN) { + delta = 10 * global.ui_scale; + } else if (direction === Clutter.ScrollDirection.SMOOTH) { + const [dx, dy] = event.get_scroll_delta(); + // If pure vertical scroll, map to horizontal + if (Math.abs(dy) > Math.abs(dx)) { + delta = dy * (16 * global.ui_scale); // Scale factor + } else { + // Let StScrollView handle horizontal smooth scroll naturally if it exists + return Clutter.EVENT_PROPAGATE; + } + } else { + return Clutter.EVENT_PROPAGATE; + } + + if (delta !== 0) { + // Manually updating adjustment value using property + adjustment.value = adjustment.value + delta; + return Clutter.EVENT_STOP; + } else { + this.updateScrollButtonVisibility(); + } + } + return Clutter.EVENT_PROPAGATE; + } +} diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index c4f1b49cc2..a3470b5a52 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -27,7 +27,7 @@ "title" : "Behavior", "keys": [ "group-apps", - "scroll-behavior", + "list-scroll-behavior", "left-click-action", "middle-click-action", "show-all-workspaces", @@ -52,7 +52,8 @@ "launcher-animation-effect", "enable-window-count-badges", "enable-notification-badges", - "enable-app-button-dragging" + "enable-app-button-dragging", + "enable-click-to-slide" ] }, "thumbnailsSection": { @@ -96,14 +97,15 @@ "default": true, "description": "Group windows by application" }, - "scroll-behavior": { + "list-scroll-behavior": { "type": "combobox", - "default": 1, + "default": 4, "description": "Mouse wheel scroll action", "options": { "None": 1, "Cycle apps": 2, - "Cycle windows": 3 + "Cycle windows": 3, + "Slide app list": 4 } }, "left-click-action": { @@ -202,6 +204,12 @@ "default": true, "description": "Enable app button dragging" }, + "enable-click-to-slide": { + "type": "checkbox", + "default": false, + "description": "Enable click to slide when slide buttons are visible", + "tooltip": "When enabled, clicking the slide buttons at either end of the app list will scroll the list in that direction. When disabled, the list will scroll when the mouse pointer hovers over the slide buttons." + }, "thumbnail-scroll-behavior": { "type": "checkbox", "default": false, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index ff25863aa3..98e4c9a9b7 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -1,4 +1,5 @@ const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; @@ -6,13 +7,21 @@ const {unref} = imports.misc.util; const Me = imports.ui.extension.getCurrentExtension(); const {createStore} = Me.imports.state; const {AppGroup} = Me.imports.appGroup; +const {ScrollBox} = Me.imports.scrollBox; const {RESERVE_KEYS} = Me.imports.constants; var Workspace = class Workspace { constructor(params) { this.state = params.state; this.state.connect({ - orientation: () => this.on_orientation_changed(false) + orientation: (state) => { + this.on_orientation_changed(state.orientation); + }, + currentWs: (state) => { + if (this.metaWorkspace && (state?.currentWs === this.metaWorkspace.index())) { + this.scrollToLastFocusedApp(); + } + } }); this.workspaceState = createStore({ workspaceIndex: params.index, @@ -29,11 +38,16 @@ var Workspace = class Workspace { if (this.state.willUnmount) { return; } - this.actor.remove_child(actor); + this.container.remove_child(actor); }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { - if (focusedAppId === appGroup.groupState.appId) return; + if ((focusedAppId === appGroup.groupState.appId) && + (!appGroup.groupState.lastFocused || appGroup.groupState.lastFocused.has_focus())) { + // -- + this.scrollToAppGroup(appGroup); + return; + } appGroup.onFocusChange(false); }); } @@ -42,33 +56,41 @@ var Workspace = class Workspace { this.signals = new SignalManager(null); this.metaWorkspace = params.metaWorkspace; - const managerOrientation = this.state.isHorizontal ? 'HORIZONTAL' : 'VERTICAL'; - this.manager = new Clutter.BoxLayout({orientation: Clutter.Orientation[managerOrientation]}); - this.actor = new Clutter.Actor({layout_manager: this.manager}); + this.scrollBox = new ScrollBox(this.state); + this.actor = this.scrollBox.actor; + this.container = this.scrollBox.container; this.appGroups = []; this.lastFocusedApp = null; + this.scrollToAppDebounceTimeoutId = 0; // Connect all the signals this.signals.connect(global.display, 'window-workspace-changed', (...args) => this.windowWorkspaceChanged(...args)); // Ugly change: refresh the removed app instances from all workspaces this.signals.connect(this.metaWorkspace, 'window-removed', (...args) => this.windowRemoved(...args)); this.signals.connect(global.window_manager, 'switch-workspace' , (...args) => this.reloadList(...args)); - this.on_orientation_changed(null, true); - } - on_orientation_changed() { - if (!this.manager) return; + this.on_orientation_changed(this.state.orientation); + } - if (!this.state.isHorizontal) { - this.manager.set_orientation(Clutter.Orientation.VERTICAL); - this.actor.set_x_align(Clutter.ActorAlign.CENTER); - } else { - this.manager.set_orientation(Clutter.Orientation.HORIZONTAL); - } + on_orientation_changed(orientation) { this.refreshList(); } + scrollToAppGroup(appGroup) { + if (this.scrollToAppDebounceTimeoutId > 0) GLib.source_remove(this.scrollToAppDebounceTimeoutId); + this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this._scrollToAppGroup(appGroup); + this.scrollToAppDebounceTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _scrollToAppGroup(appGroup) { + if (!appGroup || !appGroup.actor) return; + this.scrollBox.scrollToChild(appGroup.actor); + } + getWindowCount(appId) { let windowCount = 0; this.appGroups.forEach( appGroup => { @@ -178,6 +200,7 @@ var Workspace = class Workspace { this.appGroups = []; this.loadFavorites(); this.refreshApps(); + this.scrollToLastFocusedApp(); } loadFavorites() { @@ -215,6 +238,26 @@ var Workspace = class Workspace { } } + scrollToLastFocusedApp() { + let lastFocusedAppInWorkspace = null; + for (let appGroup of this.appGroups) { + let lastFocusedInAppGroup = appGroup.groupState.lastFocused; + if (lastFocusedInAppGroup && lastFocusedInAppGroup.has_focus()) { + lastFocusedAppInWorkspace = appGroup; + break; + } else if (this.workspaceState.lastFocusedApp === appGroup.groupState.appId) { + lastFocusedAppInWorkspace = appGroup; + } else if ((!lastFocusedAppInWorkspace || lastFocusedAppInWorkspace.groupState.metaWindows.length === 0) && + appGroup.groupState.metaWindows.length > 0 + ) { + lastFocusedAppInWorkspace = appGroup; + } + } + if (lastFocusedAppInWorkspace && lastFocusedAppInWorkspace.groupState.metaWindows.length > 0) { + this.scrollToAppGroup(lastFocusedAppInWorkspace); + } + } + updateAttentionState(display, window) { this.appGroups.forEach( appGroup => { if (appGroup.groupState.metaWindows) { @@ -309,11 +352,11 @@ var Workspace = class Workspace { }); if(idx > -1) { - this.actor.insert_child_at_index(appGroup.actor, idx); + this.container.insert_child_at_index(appGroup.actor, idx); this.appGroups.splice(idx, 0, appGroup); } else { - this.actor.add_child(appGroup.actor); + this.container.add_child(appGroup.actor); this.appGroups.push(appGroup); } appGroup.windowAdded(metaWindow); @@ -340,7 +383,7 @@ var Workspace = class Workspace { updateAppGroupIndexes() { const newAppGroups = []; - this.actor.get_children().forEach( child => { + this.container.get_children().forEach( child => { const appGroup = this.appGroups.find( appGroup => appGroup.actor === child); if (appGroup) { newAppGroups.push(appGroup); @@ -404,7 +447,7 @@ var Workspace = class Workspace { // in edge case when multiple apps of the same program are favorited, do not move other app if(!otherAppObject.groupState.isFavoriteApp) { this.appGroups.splice(otherApp, 1); - this.actor.set_child_at_index(otherAppObject.actor, refApp); + this.container.set_child_at_index(otherAppObject.actor, refApp); this.appGroups.splice(refApp, 0, otherAppObject); // change previously unpinned app status to pinned @@ -421,11 +464,14 @@ var Workspace = class Workspace { } destroy() { + if (this.scrollToAppDebounceTimeoutId > 0) { + GLib.source_remove(this.scrollToAppDebounceTimeoutId); + this.scrollToAppDebounceTimeoutId = 0; + } this.signals.disconnectAllSignals(); this.appGroups.forEach( appGroup => appGroup.destroy() ); + this.scrollBox.destroy(); this.workspaceState.destroy(); - this.manager = null; - this.actor.destroy(); unref(this, RESERVE_KEYS); } }