1
0
Fork 0
gnome-shell-extensions-extra/extensions/47/vertical-workspaces/lib/windowPreview.js

630 lines
23 KiB
JavaScript
Raw Normal View History

/**
* V-Shell (Vertical Workspaces)
* windowPreview.js
*
* @author GdH <G-dH@github.com>
* @copyright 2022 - 2024
* @license GPL-3.0
*
*/
'use strict';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import St from 'gi://St';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import Pango from 'gi://Pango';
import Graphene from 'gi://Graphene';
import Atk from 'gi://Atk';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as OverviewControls from 'resource:///org/gnome/shell/ui/overviewControls.js';
import * as WindowPreview from 'resource:///org/gnome/shell/ui/windowPreview.js';
let Me;
let opt;
const WINDOW_SCALE_TIME = 200;
const WINDOW_ACTIVE_SIZE_INC = 5;
const WINDOW_OVERLAY_FADE_TIME = 200;
const WINDOW_DND_SIZE = 256;
const DRAGGING_WINDOW_OPACITY = 100;
const ICON_OVERLAP = 0.7;
const ICON_TITLE_SPACING = 6;
const ControlsState = OverviewControls.ControlsState;
export const WindowPreviewModule = class {
constructor(me) {
Me = me;
opt = Me.opt;
this._firstActivation = true;
this.moduleEnabled = false;
this._overrides = null;
}
cleanGlobals() {
Me = null;
opt = null;
}
update(reset) {
this.moduleEnabled = opt.get('windowPreviewModule');
const conflict = false;
reset = reset || !this.moduleEnabled || conflict;
// don't touch the original code if module disabled
if (reset && !this._firstActivation) {
this._disableModule();
} else if (!reset) {
this._firstActivation = false;
this._activateModule();
}
if (reset && this._firstActivation)
console.debug(' WindowPreviewModule - Keeping untouched');
}
_activateModule() {
if (!this._overrides)
this._overrides = new Me.Util.Overrides();
this._overrides.addOverride('WindowPreview', WindowPreview.WindowPreview.prototype, WindowPreviewCommon);
// 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; // incompatible
console.debug(' WindowPreviewModule - Activated');
}
_disableModule() {
// If WindowPreview._init was injected by another extension (like Burn My Windows)
// which enables/disables before V-Shell
// don't restore the original if it's not injected,
// because it would restore injected _init and recursion would freeze GS when extensions are enabled again.
// This can happen when all extension re-enabled, not only when screen is locked/unlocked
// If _init doesn't include "fn.apply(this, args)" when reset === true, some extension already restored the original
const skipReset = WindowPreview.WindowPreview.prototype._init.toString().includes('fn.apply(this, args)');
if (this._overrides && skipReset) {
// skip restoring original _init()
this._overrides['_init'] = null;
}
if (this._overrides)
this._overrides.removeAll();
this._overrides = null;
console.debug(' WindowPreviewModule - Disabled');
}
};
const WindowPreviewCommon = {
_init(metaWindow, workspace, overviewAdjustment) {
this.metaWindow = metaWindow;
this.metaWindow._delegate = this;
this._windowActor = metaWindow.get_compositor_private();
this._workspace = workspace;
this._overviewAdjustment = overviewAdjustment;
const ICON_SIZE = opt.WIN_PREVIEW_ICON_SIZE;
Shell.WindowPreview.prototype._init.bind(this)({
reactive: true,
can_focus: true,
accessible_role: Atk.Role.PUSH_BUTTON,
offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
});
const windowContainer = new Clutter.Actor({
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
});
this.window_container = windowContainer;
windowContainer.connect('notify::scale-x',
() => this._adjustOverlayOffsets());
// gjs currently can't handle setting an actors layout manager during
// the initialization of the actor if that layout manager keeps track
// of its container, so set the layout manager after creating the
// container
windowContainer.layout_manager = new Shell.WindowPreviewLayout();
this.add_child(windowContainer);
this._addWindow(metaWindow);
this._delegate = this;
this._stackAbove = null;
this._cachedBoundingBox = {
x: windowContainer.layout_manager.bounding_box.x1,
y: windowContainer.layout_manager.bounding_box.y1,
width: windowContainer.layout_manager.bounding_box.get_width(),
height: windowContainer.layout_manager.bounding_box.get_height(),
};
windowContainer.layout_manager.connect(
'notify::bounding-box', layout => {
this._cachedBoundingBox = {
x: layout.bounding_box.x1,
y: layout.bounding_box.y1,
width: layout.bounding_box.get_width(),
height: layout.bounding_box.get_height(),
};
// A bounding box of 0x0 means all windows were removed
if (layout.bounding_box.get_area() > 0)
this.emit('size-changed');
});
this._windowActor.connectObject('destroy', () => this.destroy(), this);
this._updateAttachedDialogs();
let clickAction = new Clutter.ClickAction();
clickAction.connect('clicked', act => {
const button = act.get_button();
if (button === Clutter.BUTTON_SECONDARY) {
if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 1) {
this._closeWinAction();
return Clutter.EVENT_STOP;
} else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 2) {
this._searchAppWindowsAction();
return Clutter.EVENT_STOP;
} else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && global.windowThumbnails) {
this._removeLaters();
global.windowThumbnails?.createThumbnail(metaWindow);
return Clutter.EVENT_STOP;
}
} else if (button === Clutter.BUTTON_MIDDLE) {
if (opt.WIN_PREVIEW_MID_BTN_ACTION === 1) {
this._closeWinAction();
return Clutter.EVENT_STOP;
} else if (opt.WIN_PREVIEW_MID_BTN_ACTION === 2) {
this._searchAppWindowsAction();
return Clutter.EVENT_STOP;
} else if (opt.WIN_PREVIEW_SEC_BTN_ACTION === 3 && global.windowThumbnails) {
this._removeLaters();
global.windowThumbnails?.createThumbnail(metaWindow);
return Clutter.EVENT_STOP;
}
}
return this._activate();
});
if (this._onLongPress) {
clickAction.connect('long-press', this._onLongPress.bind(this));
} else {
clickAction.connect('long-press', (action, actor, state) => {
if (state === Clutter.LongPressState.ACTIVATE)
this.showOverlay(true);
return true;
});
}
this.connect('destroy', this._onDestroy.bind(this));
this._draggable = DND.makeDraggable(this, {
restoreOnSuccess: true,
manualMode: !!this._onLongPress,
dragActorMaxSize: WINDOW_DND_SIZE,
dragActorOpacity: DRAGGING_WINDOW_OPACITY,
});
// _draggable.addClickAction is new in GS45
if (this._draggable.addClickAction)
this._draggable.addClickAction(clickAction);
else
this.add_action(clickAction);
this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
this._draggable.connect('drag-end', this._onDragEnd.bind(this));
this.inDrag = false;
this._selected = false;
this._overlayEnabled = true;
this._overlayShown = false;
this._closeRequested = false;
this._idleHideOverlayId = 0;
const tracker = Shell.WindowTracker.get_default();
const app = tracker.get_window_app(this.metaWindow);
this._icon = app.create_icon_texture(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: windowContainer,
coordinate: Clutter.BindCoordinate.POSITION,
}));
this._icon.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.X_AXIS,
factor: 0.5,
}));
this._icon.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }),
factor: 1,
}));
if (opt.WINDOW_ICON_CLICK_ACTION) {
const iconClickAction = new Clutter.ClickAction();
iconClickAction.connect('clicked', act => {
if (act.get_button() === Clutter.BUTTON_PRIMARY) {
if (opt.WINDOW_ICON_CLICK_ACTION === 1) {
this._searchAppWindowsAction();
return Clutter.EVENT_STOP;
} else if (opt.WINDOW_ICON_CLICK_ACTION === 2 && global.windowThumbnails) {
this._removeLaters();
global.windowThumbnails?.createThumbnail(metaWindow);
return Clutter.EVENT_STOP;
}
} /* else if (act.get_button() === Clutter.BUTTON_SECONDARY) {
return Clutter.EVENT_STOP;
}*/
return Clutter.EVENT_PROPAGATE;
});
this._icon.add_action(iconClickAction);
}
const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
this._title = new St.Label({
visible: false,
style_class: 'window-caption',
text: this._getCaption(),
reactive: true,
});
this._title.clutter_text.single_line_mode = true;
this._title.add_constraint(new Clutter.BindConstraint({
source: windowContainer,
coordinate: Clutter.BindCoordinate.X,
}));
let offset;
if (opt.WIN_TITLES_POSITION < 2) {
// we cannot get proper title height before it gets to the stage, so 35 is estimated height + spacing
offset = -scaleFactor * (ICON_SIZE * ICON_OVERLAP + 35);
} else {
offset = scaleFactor * (ICON_SIZE * (1 - ICON_OVERLAP) + 4);
}
this._title.add_constraint(new Clutter.BindConstraint({
source: windowContainer,
coordinate: Clutter.BindCoordinate.Y,
offset,
}));
this._title.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.X_AXIS,
factor: 0.5,
}));
this._title.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: 0 }),
factor: 1,
}));
this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
this.label_actor = this._title;
this.metaWindow.connectObject(
'notify::title', () => (this._title.text = this._getCaption()),
this);
const layout = Meta.prefs_get_button_layout();
this._closeButtonSide =
layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
? St.Side.LEFT : St.Side.RIGHT;
this._closeButton = new St.Button({
visible: false,
style_class: 'window-close',
icon_name: 'preview-close-symbolic',
});
this._closeButton.add_constraint(new Clutter.BindConstraint({
source: windowContainer,
coordinate: Clutter.BindCoordinate.POSITION,
}));
this._closeButton.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.X_AXIS,
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
}));
this._closeButton.add_constraint(new Clutter.AlignConstraint({
source: windowContainer,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
factor: 0,
}));
this._closeButton.connect('clicked', () => this._deleteAll());
this.add_child(this._title);
this.add_child(this._icon);
this.add_child(this._closeButton);
this._overviewAdjustment.connectObject(
'notify::value', () => this._updateIconScale(), this);
this._updateIconScale();
this.connect('notify::realized', () => {
if (!this.realized)
return;
this._title.ensure_style();
this._icon.ensure_style();
});
if (ICON_SIZE < 22) {
// disable app icon
this._icon.hide();
} else {
this._updateIconScale();
}
// 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;
}
if (opt.ALWAYS_SHOW_WIN_TITLES)
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
if (global.get_pointer()[0] === opt.showingPointerX || Main.overview._overview._controls._stateAdjustment.value < 1)
return;
opt.WORKSPACE_MODE = 1;
const view = this._workspace.get_parent();
view.exposeWindows(this._workspace.metaWorkspace.index());
this.disconnect(this._wsStateConId);
});
}
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));
}
const metaWin = this.metaWindow;
if (opt.DASH_ISOLATE_WS && !metaWin._wsChangedConId) {
metaWin._wsChangedConId = metaWin.connect('workspace-changed',
() => Main.overview.dash._queueRedisplay());
} else if (!opt.DASH_ISOLATE_WS && metaWin._wsChangedConId) {
metaWin.disconnect(metaWin._wsChangedConId);
}
},
_closeWinAction() {
this.hide();
this._deleteAll();
},
_removeLaters() {
if (this._longPressLater) {
const laters = global.compositor.get_laters();
laters.remove(this._longPressLater);
delete this._longPressLater;
}
},
_searchAppWindowsAction() {
// 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
this._removeLaters();
const tracker = Shell.WindowTracker.get_default();
const appName = tracker.get_window_app(this.metaWindow).get_name();
Me.Util.activateSearchProvider(`${Me.WSP_PREFIX} ${appName}`);
},
_updateIconScale() {
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 =
(initialState > ControlsState.HIDDEN || finalState > ControlsState.HIDDEN) &&
!(finalState === ControlsState.APP_GRID && opt.WS_ANIMATION && primaryMonitor);
let scale = 0;
if (visible)
scale = currentState >= 1 ? 1 : currentState % 1;
if (!primaryMonitor && opt.WORKSPACE_MODE &&
((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
(initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER))
)
scale = 1;
else if (!primaryMonitor && opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)
scale = 0;
/* } else if (primaryMonitor && ((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
initialState === ControlsState.APP_GRID && finalState === ControlsState.HIDDEN)) {*/
else if (primaryMonitor && currentState > ControlsState.WINDOW_PICKER)
scale = 0;
// in static workspace mode show icon and title on windows expose
if (opt.OVERVIEW_MODE) {
if (currentState === 1)
scale = this._workspace._background._stateAdjustment.value;
else if ((finalState === 1 && !opt.WORKSPACE_MODE) || (finalState === 0 && !opt.WORKSPACE_MODE))
return;
}
if (!opt.WS_ANIMATION && (Main.overview.searchController.searchActive ||
((initialState === ControlsState.WINDOW_PICKER && finalState === ControlsState.APP_GRID) ||
(initialState === ControlsState.APP_GRID && finalState === ControlsState.WINDOW_PICKER)))
)
return;
// 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
if (scale === 1) {
this._icon.set({
scale_x: 1,
scale_y: 1,
});
this._title.ease({
duration: 100,
opacity: 255,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
} else {
this._title.opacity = 0;
this._icon.set({
scale_x: scale,
scale_y: scale,
});
}
},
showOverlay(animate) {
if (!this._overlayEnabled)
return;
if (this._overlayShown)
return;
this._overlayShown = true;
if (opt.WIN_TITLES_POSITION === 2)
this._restack();
// 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() && opt.SHOW_CLOSE_BUTTON
? [this._closeButton]
: [];
if (!opt.ALWAYS_SHOW_WIN_TITLES)
toShow.push(this._title);
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');
},
hideOverlay(animate) {
if (!this._overlayShown)
return;
this._overlayShown = false;
if (opt.ALWAYS_ACTIVATE_SELECTED_WINDOW && Main.overview._overview.controls._stateAdjustment.value < 1)
this._activateSelected = true;
if (opt.WIN_TITLES_POSITION === 2)
this._restack();
// 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];
if (!opt.ALWAYS_SHOW_WIN_TITLES)
toHide.push(this._title);
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,
});
}
},
overlapHeights() {
const [, titleHeight] = this._title.get_preferred_height(-1);
const topOverlap = 0;
const bottomOverlap = opt.WIN_TITLES_POSITION === 2 ? titleHeight + ICON_TITLE_SPACING : 0;
return [topOverlap, bottomOverlap];
},
_onDestroy() {
if (this._activateSelected)
this._activate();
this.metaWindow._delegate = null;
this._delegate = null;
this._destroyed = true;
if (this._longPressLater) {
const laters = global.compositor.get_laters();
laters.remove(this._longPressLater);
delete this._longPressLater;
}
if (this._idleHideOverlayId > 0) {
GLib.source_remove(this._idleHideOverlayId);
this._idleHideOverlayId = 0;
}
if (this.inDrag) {
this.emit('drag-end');
this.inDrag = false;
}
if (this._stateAdjustmentSigId)
this._workspace.stateAdjustment.disconnect(this._stateAdjustmentSigId);
},
};