2025-02-09 23:08:39 +01:00
|
|
|
/**
|
2025-02-09 23:10:52 +01:00
|
|
|
* V-Shell (Vertical Workspaces)
|
2025-02-09 23:08:39 +01:00
|
|
|
* windowPreview.js
|
2025-02-09 23:10:52 +01:00
|
|
|
*
|
2025-02-09 23:08:39 +01:00
|
|
|
* @author GdH <G-dH@github.com>
|
|
|
|
* @copyright 2022 - 2023
|
|
|
|
* @license GPL-3.0
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
const { Clutter, GLib, GObject, Graphene, Meta, Shell, St } = imports.gi;
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
const WindowPreview = imports.ui.windowPreview;
|
|
|
|
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
|
|
const Me = ExtensionUtils.getCurrentExtension();
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
const _Util = Me.imports.lib.util;
|
|
|
|
const shellVersion = _Util.shellVersion;
|
|
|
|
|
2025-02-09 23:08:39 +01:00
|
|
|
let _overrides;
|
|
|
|
|
|
|
|
const WINDOW_SCALE_TIME = imports.ui.windowPreview.WINDOW_SCALE_TIME;
|
|
|
|
const WINDOW_ACTIVE_SIZE_INC = imports.ui.windowPreview.WINDOW_ACTIVE_SIZE_INC;
|
|
|
|
const WINDOW_OVERLAY_FADE_TIME = imports.ui.windowPreview.WINDOW_OVERLAY_FADE_TIME;
|
2025-02-09 23:10:52 +01:00
|
|
|
const SEARCH_WINDOWS_PREFIX = Me.imports.lib.windowSearchProvider.prefix;
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
const ControlsState = imports.ui.overviewControls.ControlsState;
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
let opt;
|
|
|
|
let _firstRun = true;
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
function update(reset = false) {
|
2025-02-09 23:10:52 +01:00
|
|
|
opt = Me.imports.lib.settings.opt;
|
|
|
|
const moduleEnabled = opt.get('windowPreviewModule', true);
|
|
|
|
reset = reset || !moduleEnabled;
|
|
|
|
|
|
|
|
// don't even touch this module if disabled
|
|
|
|
if (_firstRun && reset)
|
|
|
|
return;
|
|
|
|
|
|
|
|
_firstRun = false;
|
|
|
|
|
|
|
|
if (_overrides)
|
2025-02-09 23:08:39 +01:00
|
|
|
_overrides.removeAll();
|
2025-02-09 23:10:52 +01:00
|
|
|
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
if (reset) {
|
|
|
|
_overrides = null;
|
|
|
|
opt = null;
|
2025-02-09 23:10:52 +01:00
|
|
|
WindowPreview.WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
|
2025-02-09 23:08:39 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_overrides = new _Util.Overrides();
|
|
|
|
|
|
|
|
_overrides.addOverride('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewCommon);
|
2025-02-09 23:10:52 +01:00
|
|
|
// A shorter timeout allows user to quickly cancel the selection by leaving the preview with the mouse pointer
|
|
|
|
if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW)
|
|
|
|
WindowPreview.WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 150;
|
2025-02-09 23:08:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
const WindowPreviewCommon = {
|
|
|
|
// injection to _init()
|
|
|
|
after__init() {
|
2025-02-09 23:08:39 +01:00
|
|
|
const ICON_OVERLAP = 0.7;
|
|
|
|
|
|
|
|
if (opt.WIN_PREVIEW_ICON_SIZE < 64) {
|
|
|
|
this.remove_child(this._icon);
|
|
|
|
this._icon.destroy();
|
|
|
|
const tracker = Shell.WindowTracker.get_default();
|
|
|
|
const app = tracker.get_window_app(this.metaWindow);
|
|
|
|
this._icon = app.create_icon_texture(opt.WIN_PREVIEW_ICON_SIZE);
|
|
|
|
this._icon.add_style_class_name('icon-dropshadow');
|
|
|
|
this._icon.set({
|
|
|
|
reactive: true,
|
|
|
|
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
|
|
|
|
});
|
|
|
|
this._icon.add_constraint(new Clutter.BindConstraint({
|
|
|
|
source: this.windowContainer,
|
|
|
|
coordinate: Clutter.BindCoordinate.POSITION,
|
|
|
|
}));
|
|
|
|
this._icon.add_constraint(new Clutter.AlignConstraint({
|
|
|
|
source: this.windowContainer,
|
|
|
|
align_axis: Clutter.AlignAxis.X_AXIS,
|
|
|
|
factor: 0.5,
|
|
|
|
}));
|
|
|
|
this._icon.add_constraint(new Clutter.AlignConstraint({
|
|
|
|
source: this.windowContainer,
|
|
|
|
align_axis: Clutter.AlignAxis.Y_AXIS,
|
|
|
|
pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }),
|
|
|
|
factor: 1,
|
|
|
|
}));
|
|
|
|
this.add_child(this._icon);
|
|
|
|
if (opt.WIN_PREVIEW_ICON_SIZE < 22) {
|
|
|
|
// disable app icon
|
|
|
|
this._icon.hide();
|
|
|
|
}
|
|
|
|
this._iconSize = opt.WIN_PREVIEW_ICON_SIZE;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
|
|
|
|
const iconOverlap = opt.WIN_PREVIEW_ICON_SIZE * ICON_OVERLAP;
|
|
|
|
// we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing
|
2025-02-09 23:10:52 +01:00
|
|
|
this._title.get_constraints()[1].offset = scaleFactor * (-iconOverlap - 35);
|
2025-02-09 23:08:39 +01:00
|
|
|
this.set_child_above_sibling(this._title, null);
|
|
|
|
// if window is created while the overview is shown, icon and title should be visible immediately
|
|
|
|
if (Main.overview._overview._controls._stateAdjustment.value < 1) {
|
|
|
|
this._icon.scale_x = 0;
|
|
|
|
this._icon.scale_y = 0;
|
|
|
|
this._title.opacity = 0;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (opt.ALWAYS_SHOW_WIN_TITLES)
|
2025-02-09 23:08:39 +01:00
|
|
|
this._title.show();
|
|
|
|
|
|
|
|
if (opt.OVERVIEW_MODE === 1) {
|
|
|
|
// spread windows on hover
|
|
|
|
this._wsStateConId = this.connect('enter-event', () => {
|
|
|
|
// don't spread windows if user don't use pointer device at this moment
|
2025-02-09 23:10:52 +01:00
|
|
|
if (global.get_pointer()[0] === opt.showingPointerX || Main.overview._overview._controls._stateAdjustment.value < 1)
|
2025-02-09 23:08:39 +01:00
|
|
|
return;
|
|
|
|
|
|
|
|
const adjustment = this._workspace._background._stateAdjustment;
|
2025-02-09 23:10:52 +01:00
|
|
|
opt.WORKSPACE_MODE = 1;
|
|
|
|
_Util.exposeWindows(adjustment, false);
|
|
|
|
this.disconnect(this._wsStateConId);
|
2025-02-09 23:08:39 +01:00
|
|
|
});
|
|
|
|
}
|
2025-02-09 23:10:52 +01:00
|
|
|
|
2025-02-09 23:08:39 +01:00
|
|
|
if (opt.OVERVIEW_MODE) {
|
|
|
|
// show window icon and title on ws windows spread
|
|
|
|
this._stateAdjustmentSigId = this._workspace.stateAdjustment.connect('notify::value', this._updateIconScale.bind(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
// replace click action with custom one
|
|
|
|
const action = this.get_actions()[0];
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
const handlerId = GObject.signal_handler_find(action, { signalId: 'clicked' });
|
|
|
|
if (handlerId)
|
|
|
|
action.disconnect(handlerId);
|
|
|
|
|
|
|
|
action.connect('clicked', act => {
|
|
|
|
const button = act.get_button();
|
2025-02-09 23:08:39 +01:00
|
|
|
if (button === Clutter.BUTTON_PRIMARY) {
|
|
|
|
this._activate();
|
2025-02-09 23:10:52 +01:00
|
|
|
return Clutter.EVENT_STOP;
|
2025-02-09 23:08:39 +01:00
|
|
|
} else if (button === Clutter.BUTTON_SECONDARY) {
|
|
|
|
// this action cancels long-press event and the 'long-press-cancel' event is used by the Shell to actually initiate DnD
|
|
|
|
// so the dnd initiation needs to be removed
|
|
|
|
if (this._longPressLater) {
|
2025-02-09 23:10:52 +01:00
|
|
|
if (shellVersion >= 44) {
|
|
|
|
const laters = global.compositor.get_laters();
|
|
|
|
laters.remove(this._longPressLater);
|
|
|
|
} else {
|
|
|
|
Meta.later_remove(this._longPressLater);
|
|
|
|
delete this._longPressLater;
|
|
|
|
}
|
2025-02-09 23:08:39 +01:00
|
|
|
}
|
|
|
|
const tracker = Shell.WindowTracker.get_default();
|
|
|
|
const appName = tracker.get_window_app(this.metaWindow).get_name();
|
|
|
|
_Util.activateSearchProvider(`${SEARCH_WINDOWS_PREFIX} ${appName}`);
|
|
|
|
return Clutter.EVENT_STOP;
|
|
|
|
}
|
2025-02-09 23:10:52 +01:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2025-02-09 23:08:39 +01:00
|
|
|
});
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (opt.WINDOW_ICON_CLICK_SEARCH) {
|
|
|
|
const iconClickAction = new Clutter.ClickAction();
|
|
|
|
iconClickAction.connect('clicked', act => {
|
|
|
|
if (act.get_button() === Clutter.BUTTON_PRIMARY) {
|
|
|
|
const tracker = Shell.WindowTracker.get_default();
|
|
|
|
const appName = tracker.get_window_app(this.metaWindow).get_name();
|
|
|
|
_Util.activateSearchProvider(`${SEARCH_WINDOWS_PREFIX} ${appName}`);
|
|
|
|
return Clutter.EVENT_STOP;
|
|
|
|
}
|
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
});
|
|
|
|
this._icon.add_action(iconClickAction);
|
|
|
|
}
|
|
|
|
},
|
2025-02-09 23:08:39 +01:00
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
_updateIconScale() {
|
2025-02-09 23:08:39 +01:00
|
|
|
let { currentState, initialState, finalState } =
|
|
|
|
this._overviewAdjustment.getStateTransitionParams();
|
|
|
|
|
|
|
|
// Current state - 0 - HIDDEN, 1 - WINDOW_PICKER, 2 - APP_GRID
|
|
|
|
const primaryMonitor = this.metaWindow.get_monitor() === global.display.get_primary_monitor();
|
|
|
|
|
|
|
|
const visible =
|
2025-02-09 23:10:52 +01:00
|
|
|
(initialState > ControlsState.HIDDEN || finalState > ControlsState.HIDDEN) &&
|
|
|
|
!(finalState === ControlsState.APP_GRID && primaryMonitor);
|
2025-02-09 23:08:39 +01:00
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
let scale = 0;
|
|
|
|
if (visible)
|
|
|
|
scale = currentState >= 1 ? 1 : currentState % 1;
|
|
|
|
|
|
|
|
if (!primaryMonitor && opt.WORKSPACE_MODE &&
|
2025-02-09 23:08:39 +01:00
|
|
|
((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
|
|
|
|
(initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER))
|
2025-02-09 23:10:52 +01:00
|
|
|
)
|
2025-02-09 23:08:39 +01:00
|
|
|
scale = 1;
|
2025-02-09 23:10:52 +01:00
|
|
|
else if (!primaryMonitor && opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)
|
|
|
|
scale = 0;
|
|
|
|
/* } else if (primaryMonitor && ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
|
2025-02-09 23:08:39 +01:00
|
|
|
initialState === ControlsState.APP_GRID && finalState === ControlsState.HIDDEN)) {*/
|
2025-02-09 23:10:52 +01:00
|
|
|
else if (primaryMonitor && currentState > ControlsState.WINDOW_PICKER)
|
2025-02-09 23:08:39 +01:00
|
|
|
scale = 0;
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
|
|
|
|
// in static workspace mode show icon and title on windows expose
|
2025-02-09 23:08:39 +01:00
|
|
|
if (opt.OVERVIEW_MODE) {
|
2025-02-09 23:10:52 +01:00
|
|
|
if (currentState === 1)
|
|
|
|
scale = opt.WORKSPACE_MODE;
|
|
|
|
else if (finalState === 1 || (finalState === 0 && !opt.WORKSPACE_MODE))
|
2025-02-09 23:08:39 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (scale === 1) {
|
|
|
|
this._icon.ease({
|
|
|
|
duration: 50,
|
|
|
|
scale_x: scale,
|
|
|
|
scale_y: scale,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
});
|
|
|
|
this._title.ease({
|
|
|
|
duration: 100,
|
|
|
|
opacity: 255,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
});
|
|
|
|
} else if (this._icon.scale_x !== 0) {
|
|
|
|
this._icon.set({
|
|
|
|
scale_x: 0,
|
|
|
|
scale_y: 0,
|
|
|
|
});
|
|
|
|
this._title.opacity = 0;
|
|
|
|
}
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
// if titles are in 'always show' mode, we need to add transition between visible/invisible state
|
|
|
|
// but the transition is quite expensive,
|
|
|
|
// showing the titles at the end of the transition is good enough and workspace preview transition is much smoother
|
|
|
|
},
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
showOverlay(animate) {
|
2025-02-09 23:08:39 +01:00
|
|
|
if (!this._overlayEnabled)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this._overlayShown)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._overlayShown = true;
|
2025-02-09 23:10:52 +01:00
|
|
|
if (!opt.ALWAYS_ACTIVATE_SELECTED_WINDOW)
|
|
|
|
this._restack();
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
// If we're supposed to animate and an animation in our direction
|
|
|
|
// is already happening, let that one continue
|
|
|
|
const ongoingTransition = this._title.get_transition('opacity');
|
|
|
|
if (animate &&
|
|
|
|
ongoingTransition &&
|
|
|
|
ongoingTransition.get_interval().peek_final_value() === 255)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const toShow = this._windowCanClose()
|
|
|
|
? [this._closeButton]
|
|
|
|
: [];
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (!opt.ALWAYS_SHOW_WIN_TITLES)
|
2025-02-09 23:08:39 +01:00
|
|
|
toShow.push(this._title);
|
2025-02-09 23:10:52 +01:00
|
|
|
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
toShow.forEach(a => {
|
|
|
|
a.opacity = 0;
|
|
|
|
a.show();
|
|
|
|
a.ease({
|
|
|
|
opacity: 255,
|
|
|
|
duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const [width, height] = this.window_container.get_size();
|
|
|
|
const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
|
|
|
|
const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor;
|
|
|
|
const origSize = Math.max(width, height);
|
|
|
|
const scale = (origSize + activeExtraSize) / origSize;
|
|
|
|
|
|
|
|
this.window_container.ease({
|
|
|
|
scale_x: scale,
|
|
|
|
scale_y: scale,
|
|
|
|
duration: animate ? WINDOW_SCALE_TIME : 0,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.emit('show-chrome');
|
|
|
|
},
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
hideOverlay(animate) {
|
2025-02-09 23:08:39 +01:00
|
|
|
if (!this._overlayShown)
|
|
|
|
return;
|
|
|
|
this._overlayShown = false;
|
2025-02-09 23:10:52 +01:00
|
|
|
if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW && Main.overview._overview.controls._stateAdjustment.value < 1) {
|
|
|
|
this.get_parent()?.set_child_above_sibling(this, null);
|
|
|
|
this._activateSelected = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!opt.ALWAYS_ACTIVATE_SELECTED_WINDOW)
|
|
|
|
this._restack();
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
// If we're supposed to animate and an animation in our direction
|
|
|
|
// is already happening, let that one continue
|
|
|
|
const ongoingTransition = this._title.get_transition('opacity');
|
|
|
|
if (animate &&
|
|
|
|
ongoingTransition &&
|
|
|
|
ongoingTransition.get_interval().peek_final_value() === 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const toHide = [this._closeButton];
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (!opt.ALWAYS_SHOW_WIN_TITLES)
|
2025-02-09 23:08:39 +01:00
|
|
|
toHide.push(this._title);
|
2025-02-09 23:10:52 +01:00
|
|
|
|
2025-02-09 23:08:39 +01:00
|
|
|
toHide.forEach(a => {
|
|
|
|
a.opacity = 255;
|
|
|
|
a.ease({
|
|
|
|
opacity: 0,
|
|
|
|
duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
onComplete: () => a.hide(),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this.window_container) {
|
|
|
|
this.window_container.ease({
|
|
|
|
scale_x: 1,
|
|
|
|
scale_y: 1,
|
|
|
|
duration: animate ? WINDOW_SCALE_TIME : 0,
|
|
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_onDestroy() {
|
2025-02-09 23:10:52 +01:00
|
|
|
// workaround for upstream bug - hideOverlay is called after windowPreview is destroyed, from the leave event callback
|
|
|
|
// hiding the preview now avoids firing the post-mortem leave event
|
|
|
|
this.hide();
|
|
|
|
if (this._activateSelected)
|
|
|
|
this._activate();
|
2025-02-09 23:08:39 +01:00
|
|
|
|
|
|
|
this.metaWindow._delegate = null;
|
|
|
|
this._delegate = null;
|
|
|
|
|
|
|
|
if (this._longPressLater) {
|
2025-02-09 23:10:52 +01:00
|
|
|
if (shellVersion >= 44) {
|
|
|
|
const laters = global.compositor.get_laters();
|
|
|
|
laters.remove(this._longPressLater);
|
|
|
|
delete this._longPressLater;
|
|
|
|
} else {
|
|
|
|
Meta.later_remove(this._longPressLater);
|
|
|
|
delete this._longPressLater;
|
|
|
|
}
|
2025-02-09 23:08:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this._idleHideOverlayId > 0) {
|
|
|
|
GLib.source_remove(this._idleHideOverlayId);
|
|
|
|
this._idleHideOverlayId = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.inDrag) {
|
|
|
|
this.emit('drag-end');
|
|
|
|
this.inDrag = false;
|
|
|
|
}
|
|
|
|
|
2025-02-09 23:10:52 +01:00
|
|
|
if (this._stateAdjustmentSigId)
|
2025-02-09 23:08:39 +01:00
|
|
|
this._workspace.stateAdjustment.disconnect(this._stateAdjustmentSigId);
|
2025-02-09 23:10:52 +01:00
|
|
|
},
|
|
|
|
};
|