Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions schemas/org.gnome.shell.extensions.dash-to-panel.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,16 @@
<summary>Hide overview on startup</summary>
<description>Hide overview on startup, which would be shown by default at gnome startup.</description>
</key>
<key type="i" name="drag-to-overview-delay">
<default>-1</default>
<summary>Delay before showing overview or previews when dragging over panel</summary>
<description>Number of milliseconds the pointer must hover over the panel while dragging before the overview or window previews are shown. Set to -1 to disable this behaviour entirely.</description>
</key>
<key type="b" name="drag-to-overview-preview">
<default>false</default>
<summary>Show window previews when dragging over app icons</summary>
<description>When dragging over an app icon, show the window preview popup instead of opening the overview.</description>
</key>
<key type="b" name="group-apps">
<default>true</default>
<summary>Group applications</summary>
Expand Down
104 changes: 94 additions & 10 deletions src/appIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const T3 = 'showDotsTimeout'
const T4 = 'overviewWindowDragEndTimeout'
const T5 = 'switchWorkspaceTimeout'
const T6 = 'displayProperIndicatorTimeout'
const T7 = 'xdndDragOverTimeout'

//right padding defined for .overview-label in stylesheet.css
const TITLE_RIGHT_PADDING = 8
Expand Down Expand Up @@ -432,7 +433,8 @@ export const TaskbarAppIcon = GObject.registerClass(
_onAppIconHoverChanged() {
if (
!SETTINGS.get_boolean('show-window-previews') ||
(!this.window && !this._nWindows)
(!this.window && !this._nWindows) ||
this._inXdndDrag
) {
return
}
Expand All @@ -445,6 +447,7 @@ export const TaskbarAppIcon = GObject.registerClass(
}

_onDestroy() {
this._disposed = true
super._onDestroy()

if (this._updateIconIdleId) {
Expand Down Expand Up @@ -1710,26 +1713,107 @@ export const TaskbarAppIcon = GObject.registerClass(
this._maybeUpdateNumberOverlay()
}

cancelXdndDragTimeout() {
if (this._disposed) return

this._timeoutsHandler.remove(T7)
this._xdndActionFired = false
this._inXdndDrag = false
this.set_hover(false)
}

handleDragOver(source) {
if (source == Main.xdndHandler) {
this._previewMenu.close(true)
let delay = SETTINGS.get_int('drag-to-overview-delay')

if (!this._nWindows && !this.window)
return DND.DragMotionResult.MOVE_DROP
if (delay < 0) {
// Feature disabled: do nothing on drag, just let the drag pass through
return DND.DragMotionResult.CONTINUE
}

if (this._nWindows == 1 || this.window) {
this.window
? Main.activateWindow(this.window)
: activateFirstWindow(this.app, this.monitor)
} else
this.dtpPanel.panelManager.showFocusedAppInOverview(this.app, true)
// Simulate hover highlight as if the cursor were normally over the icon
// (XDnD grabs the pointer so Clutter never sets hover automatically).
// Set _inXdndDrag before set_hover so that _onAppIconHoverChanged
// doesn't call requestOpen — preview opening is managed by T7 below.
if (!this.hover) {
// Cancel any pending panel-level xdnd timeout since the cursor is now
// over this icon
this.dtpPanel.cancelXdndOverviewTimeout()

// Clear state on all other icons (hover highlight, timers, fired flags).
// The xdnd walk stops at the first MOVE_DROP result so the panel's
// cancelXdndDragTimeouts() is never reached when moving between icons.
// Pass `this` so our own fired flag and timer are not disturbed.
this.dtpPanel.taskbar.cancelXdndDragTimeouts(this)

this._inXdndDrag = true
this.set_hover(true)
}

// Guard against re-firing when delay=0: once the action has run for
// this icon hover, don't restart it on every subsequent mouse-move event
if (!this._timeoutsHandler.getId(T7) && !this._xdndActionFired) {
this._timeoutsHandler.add([
T7,
delay,
() => {
this._xdndActionFired = true

if (!this._nWindows && !this.window) return

const usePreview = SETTINGS.get_boolean('drag-to-overview-preview')

if (usePreview) {
// If the preview is already open for a different icon, close it
// first. If it's already showing this icon, just enable drag
// mode, no need to close and re-open (which would cause a
// flicker animation).
if (this._previewMenu.currentAppIcon === this) {
this._previewMenu._setDragMode(true)
} else {
this._previewMenu.close(true)
this._previewMenu.open(this, true)
}
} else {
// Not using preview: close any open preview popup first
this._previewMenu.close(true)

if (this._nWindows == 1 || this.window) {
// No preview: activate the single window directly
this.window
? Main.activateWindow(this.window)
: activateFirstWindow(this.app, this.monitor)
} else {
// No preview, multi-window: fall back to overview
this.dtpPanel.panelManager.showFocusedAppInOverview(
this.app,
true,
)
}
}
},
])
}

return DND.DragMotionResult.MOVE_DROP
}

this._timeoutsHandler.remove(T7)
this._xdndActionFired = false
this._inXdndDrag = false
this.set_hover(false)
return DND.DragMotionResult.CONTINUE
}

acceptDrop() {
// Cancel any pending xdnd drag timeout when a drop occurs on the icon
this._timeoutsHandler.remove(T7)
this._xdndActionFired = false
this._inXdndDrag = false
this.set_hover(false)
return false
}

getAppIconInterestingWindows(isolateMonitors) {
return getInterestingWindows(
this.app,
Expand Down
125 changes: 123 additions & 2 deletions src/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const T4 = 'showDesktopTimeout'
const T5 = 'trackerFocusAppTimeout'
const T6 = 'scrollPanelDelayTimeout'
const T7 = 'waitPanelBoxAllocation'
const T8 = 'dragToOverviewDelay'

const MIN_PANEL_SIZE = 22

Expand Down Expand Up @@ -372,6 +373,39 @@ export const Panel = GObject.registerClass(
],
[this.panel, 'scroll-event', this._onPanelMouseScroll.bind(this)],
[Main.layoutManager, 'startup-complete', () => this._resetGeometry()],
[
Main.xdndHandler,
'drag-begin',
() => {
if (!this._xdndDragMonitor) {
this._xdndDragMonitor = {
dragMotion: (dragEvent) => {
this._onXdndDragMotion(dragEvent)
return DND.DragMotionResult.CONTINUE
},
}
DND.addDragMonitor(this._xdndDragMonitor)
}
},
],
[
Main.xdndHandler,
'drag-end',
() => {
// Fires on both cursor-leave and button-release.
// Cancel any pending timeout and close any drag-opened preview,
// but do NOT reset _xdndPanelActionFired. Once the overview has
// been triggered for this drag it should not trigger again on
// re-entry (drag-end/drag-begin fire on every leave/enter, not
// just on a genuine new drag starting).
this._removeXdndDragMonitor()
this._timeoutsHandler.remove(T8)
// Clear simulated hover on all icons when the drag leaves/ends
this.taskbar.cancelXdndDragTimeouts()
let previewMenu = this.taskbar.previewMenu
if (previewMenu._dragMode) previewMenu.close(true)
},
],
)

this._bindSettingsChanges()
Expand Down Expand Up @@ -417,6 +451,7 @@ export const Panel = GObject.registerClass(
disable() {
this.panelStyle.disable()

this._removeXdndDragMonitor()
this._timeoutsHandler.destroy()
this._signalsHandler.destroy()

Expand Down Expand Up @@ -523,13 +558,99 @@ export const Panel = GObject.registerClass(
source == Main.xdndHandler &&
Main.overview.shouldToggleByCornerOrButton()
) {
this.panelManager.showFocusedAppInOverview(null, true)
Main.overview.show()
let delay = SETTINGS.get_int('drag-to-overview-delay')

if (delay < 0) {
// Feature disabled: never open overview on drag
return DND.DragMotionResult.CONTINUE
}

// Only react when the cursor is over the taskbar area itself.
// Dragging over other panel elements (clock, system tray, activities
// button, padding, etc.) should not trigger the overview or previews.
const [x, y] = global.get_pointer()
const pickedActor = global.stage.get_actor_at_pos(
Clutter.PickMode.REACTIVE,
x,
y,
)
if (!this.taskbar.actor.contains(pickedActor)) {
// We're outside the taskbar (clock, system tray, etc.); cancel any
// pending T8 and icon-level timeouts but keep _xdndPanelActionFired
// so re-entering the taskbar does not re-trigger the overview.
this._timeoutsHandler.remove(T8)
this.taskbar.cancelXdndDragTimeouts()
return DND.DragMotionResult.CONTINUE
}

// Cancel any pending icon-level xdnd timeouts since the cursor is now
// over empty panel space
this.taskbar.cancelXdndDragTimeouts()

if (!this._timeoutsHandler.getId(T8) && !this._xdndPanelActionFired) {
this._timeoutsHandler.add([
T8,
delay,
() => {
this._xdndPanelActionFired = true
this.panelManager.showFocusedAppInOverview(null, true)
Main.overview.show()
},
])
}
} else {
this._timeoutsHandler.remove(T8)
this._xdndPanelActionFired = false
}

return DND.DragMotionResult.CONTINUE
}

acceptDrop() {
// Cancel any pending drag-to-overview timeout when a drop occurs on the panel
this._timeoutsHandler.remove(T8)
this._xdndPanelActionFired = false
return false
}

_onXdndDragMotion(dragEvent) {
let previewMenu = this.taskbar.previewMenu
let pickedActor = global.stage.get_actor_at_pos(
Clutter.PickMode.REACTIVE,
dragEvent.x,
dragEvent.y,
)

// Check if the cursor is over any of our managed areas:
// the taskbar (which contains app icons), or the preview popup menu.
let isOverTaskbar = this.taskbar.actor.contains(pickedActor)
let isOverPreview =
previewMenu._dragMode && previewMenu.menu.contains(pickedActor)

if (!isOverTaskbar && !isOverPreview) {
// Cursor has moved away from both the taskbar and any drag-opened
// preview popup. Clear simulated hover on all icons.
this.taskbar.cancelXdndDragTimeouts()

// Also cancel any pending panel-level overview timeout
this._timeoutsHandler.remove(T8)

// Close any drag-opened preview popup
if (previewMenu._dragMode) previewMenu.close(true)
}
}

cancelXdndOverviewTimeout() {
this._timeoutsHandler.remove(T8)
}

_removeXdndDragMonitor() {
if (this._xdndDragMonitor) {
DND.removeDragMonitor(this._xdndDragMonitor)
this._xdndDragMonitor = null
}
}

getPosition() {
let position = PanelSettings.getPanelPosition(
SETTINGS,
Expand Down
51 changes: 51 additions & 0 deletions src/prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,57 @@ const Preferences = class {
Gio.SettingsBindFlags.DEFAULT,
)

let dragToOverviewSwitch = this._builder.get_object('drag_to_overview_switch')
let dragToOverviewDelayRow = this._builder.get_object('drag_to_overview_delay_row')
let dragToOverviewDelaySpin = this._builder.get_object(
'drag_to_overview_delay_spinbutton',
)
let dragToOverviewPreviewRow = this._builder.get_object(
'drag_to_overview_preview_row',
)

let updateDragToOverviewUi = () => {
let delay = this._settings.get_int('drag-to-overview-delay')
let enabled = delay >= 0

dragToOverviewSwitch.set_active(enabled)
dragToOverviewDelayRow.set_sensitive(enabled)
dragToOverviewPreviewRow.set_sensitive(enabled)

if (enabled) dragToOverviewDelaySpin.set_value(delay)
}

updateDragToOverviewUi()

dragToOverviewSwitch.connect('notify::active', () => {
if (dragToOverviewSwitch.get_active()) {
// Restore last positive delay (or default 300ms)
let current = this._settings.get_int('drag-to-overview-delay')
this._settings.set_int(
'drag-to-overview-delay',
current >= 0 ? current : 300,
)
} else {
this._settings.set_int('drag-to-overview-delay', -1)
}
updateDragToOverviewUi()
})

dragToOverviewDelaySpin.connect('value-changed', () => {
if (dragToOverviewSwitch.get_active())
this._settings.set_int(
'drag-to-overview-delay',
dragToOverviewDelaySpin.get_value_as_int(),
)
})

this._settings.bind(
'drag-to-overview-preview',
this._builder.get_object('drag_to_overview_preview_switch'),
'active',
Gio.SettingsBindFlags.DEFAULT,
)

this._settings.bind(
'group-apps',
this._builder.get_object('group_apps_switch'),
Expand Down
8 changes: 8 additions & 0 deletions src/taskbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,14 @@ export const Taskbar = class extends EventEmitter {
.forEach((fav) => fav._container[cssFuncName]('favorite'))
}

cancelXdndDragTimeouts(exceptIcon) {
this._getAppIcons().forEach((icon) => {
if (icon && icon !== exceptIcon && !icon._disposed) {
icon.cancelXdndDragTimeout()
}
})
}

handleIsolatedWorkspaceSwitch() {
this._shownInitially = this.isGroupApps
this._queueRedisplay()
Expand Down
Loading