From 701b14ecbfbe49e743b265894da0810bab08eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 2 Jun 2023 13:12:54 +0200 Subject: [PATCH] extensions: Use extension class for all extensions This will be the only supported entry point when extension loading switches to dynamic imports, so prepare for that by wrapping the remaining standalone enable()/disable() methods in Extension classes. Part-of: --- extensions/apps-menu/extension.js | 28 +- extensions/auto-move-windows/extension.js | 79 +++--- extensions/drive-menu/extension.js | 31 ++- extensions/launch-new-instance/extension.js | 28 +- .../native-window-placement/extension.js | 155 ++++++----- extensions/places-menu/extension.js | 39 +-- .../screenshot-window-sizer/extension.js | 259 +++++++++--------- extensions/workspace-indicator/extension.js | 31 ++- 8 files changed, 345 insertions(+), 305 deletions(-) diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js index 23da6c08..67ae6e97 100644 --- a/extensions/apps-menu/extension.js +++ b/extensions/apps-menu/extension.js @@ -677,22 +677,26 @@ class ApplicationsButton extends PanelMenu.Button { } } -let appsMenuButton; +class Extension { + constructor() { + ExtensionUtils.initTranslations(); + } -/** */ -function enable() { - appsMenuButton = new ApplicationsButton(); - let index = Main.sessionMode.panel.left.indexOf('activities') + 1; - Main.panel.addToStatusArea('apps-menu', appsMenuButton, index, 'left'); -} + enable() { + this._appsMenuButton = new ApplicationsButton(); + const index = Main.sessionMode.panel.left.indexOf('activities') + 1; + Main.panel.addToStatusArea( + 'apps-menu', this._appsMenuButton, index, 'left'); + } -/** */ -function disable() { - Main.panel.menuManager.removeMenu(appsMenuButton.menu); - appsMenuButton.destroy(); + disable() { + Main.panel.menuManager.removeMenu(this._appsMenuButton.menu); + this._appsMenuButton.destroy(); + delete this._appsMenuButton; + } } /** */ function init() { - ExtensionUtils.initTranslations(); + return new Extension(); } diff --git a/extensions/auto-move-windows/extension.js b/extensions/auto-move-windows/extension.js index 298de536..9362a52e 100644 --- a/extensions/auto-move-windows/extension.js +++ b/extensions/auto-move-windows/extension.js @@ -105,47 +105,46 @@ class WindowMover { } } -let prevCheckWorkspaces; -let winMover; +class Extension { + enable() { + this._prevCheckWorkspaces = Main.wm._workspaceTracker._checkWorkspaces; + Main.wm._workspaceTracker._checkWorkspaces = + this._getCheckWorkspaceOverride(this._prevCheckWorkspaces); + this._windowMover = new WindowMover(); + } + + disable() { + Main.wm._workspaceTracker._checkWorkspaces = this._prevCheckWorkspaces; + this._windowMover.destroy(); + delete this._windowMover; + } + + _getCheckWorkspaceOverride(originalMethod) { + /* eslint-disable no-invalid-this */ + return function () { + const keepAliveWorkspaces = []; + let foundNonEmpty = false; + for (let i = this._workspaces.length - 1; i >= 0; i--) { + if (!foundNonEmpty) { + foundNonEmpty = this._workspaces[i].list_windows().some( + w => !w.is_on_all_workspaces()); + } else if (!this._workspaces[i]._keepAliveId) { + keepAliveWorkspaces.push(this._workspaces[i]); + } + } + + // make sure the original method only removes empty workspaces at the end + keepAliveWorkspaces.forEach(ws => (ws._keepAliveId = 1)); + originalMethod.call(this); + keepAliveWorkspaces.forEach(ws => delete ws._keepAliveId); + + return false; + }; + /* eslint-enable no-invalid-this */ + } +} /** */ function init() { - ExtensionUtils.initTranslations(); -} - -/** - * @returns {bool} - false (used as MetaLater handler) - */ -function myCheckWorkspaces() { - let keepAliveWorkspaces = []; - let foundNonEmpty = false; - for (let i = this._workspaces.length - 1; i >= 0; i--) { - if (!foundNonEmpty) { - foundNonEmpty = this._workspaces[i].list_windows().some( - w => !w.is_on_all_workspaces()); - } else if (!this._workspaces[i]._keepAliveId) { - keepAliveWorkspaces.push(this._workspaces[i]); - } - } - - // make sure the original method only removes empty workspaces at the end - keepAliveWorkspaces.forEach(ws => (ws._keepAliveId = 1)); - prevCheckWorkspaces.call(this); - keepAliveWorkspaces.forEach(ws => delete ws._keepAliveId); - - return false; -} - -/** */ -function enable() { - prevCheckWorkspaces = Main.wm._workspaceTracker._checkWorkspaces; - Main.wm._workspaceTracker._checkWorkspaces = myCheckWorkspaces; - - winMover = new WindowMover(); -} - -/** */ -function disable() { - Main.wm._workspaceTracker._checkWorkspaces = prevCheckWorkspaces; - winMover.destroy(); + return new Extension(); } diff --git a/extensions/drive-menu/extension.js b/extensions/drive-menu/extension.js index 45c854f4..fd98d989 100644 --- a/extensions/drive-menu/extension.js +++ b/extensions/drive-menu/extension.js @@ -212,20 +212,23 @@ class DriveMenu extends PanelMenu.Button { } } +class Extension { + constructor() { + ExtensionUtils.initTranslations(); + } + + enable() { + this._indicator = new DriveMenu(); + Main.panel.addToStatusArea('drive-menu', this._indicator); + } + + disable() { + this._indicator.destroy(); + delete this._indicator; + } +} + /** */ function init() { - ExtensionUtils.initTranslations(); -} - -let _indicator; - -/** */ -function enable() { - _indicator = new DriveMenu(); - Main.panel.addToStatusArea('drive-menu', _indicator); -} - -/** */ -function disable() { - _indicator.destroy(); + return new Extension(); } diff --git a/extensions/launch-new-instance/extension.js b/extensions/launch-new-instance/extension.js index a249cd48..53f2420c 100644 --- a/extensions/launch-new-instance/extension.js +++ b/extensions/launch-new-instance/extension.js @@ -1,17 +1,25 @@ -/* exported enable disable */ +/* exported init */ const AppDisplay = imports.ui.appDisplay; -let _activateOriginal = null; +class Extension { + constructor() { + this._appIconProto = AppDisplay.AppIcon.prototype; + this._activateOriginal = this._appIconProto.activate; + } -/** */ -function enable() { - _activateOriginal = AppDisplay.AppIcon.prototype.activate; - AppDisplay.AppIcon.prototype.activate = function () { - _activateOriginal.call(this, 2); - }; + enable() { + const {_activateOriginal} = this; + this._appIconProto.activate = function () { + _activateOriginal.call(this, 2); + }; + } + + disable() { + this._appIconProto.activate = this._activateOriginal; + } } /** */ -function disable() { - AppDisplay.AppIcon.prototype.activate = _activateOriginal; +function init() { + return new Extension(); } diff --git a/extensions/native-window-placement/extension.js b/extensions/native-window-placement/extension.js index b6b662a8..839464e2 100644 --- a/extensions/native-window-placement/extension.js +++ b/extensions/native-window-placement/extension.js @@ -236,75 +236,96 @@ class NaturalLayoutStrategy extends Workspace.LayoutStrategy { } } -let winInjections, workspaceInjections; +class Extension { + constructor() { + this._savedMethods = new Map(); + } -/** */ -function resetState() { - winInjections = { }; - workspaceInjections = { }; + enable() { + const settings = ExtensionUtils.getSettings(); + + const layoutProto = Workspace.WorkspaceLayout.prototype; + const previewProto = WindowPreview.prototype; + + this._overrideMethod(layoutProto, '_createBestLayout', () => { + /* eslint-disable no-invalid-this */ + return function () { + this._layoutStrategy = new NaturalLayoutStrategy({ + monitor: Main.layoutManager.monitors[this._monitorIndex], + }, settings); + return this._layoutStrategy.computeLayout(this._sortedWindows); + }; + /* eslint-enable no-invalid-this */ + }); + + // position window titles on top of windows in overlay + this._overrideMethod(previewProto, '_init', originalMethod => { + /* eslint-disable no-invalid-this */ + return function (...args) { + originalMethod.call(this, ...args); + + if (!settings.get_boolean('window-captions-on-top')) + return; + + const alignConstraint = this._title.get_constraints().find( + c => c.align_axis && c.align_axis === Clutter.AlignAxis.Y_AXIS); + alignConstraint.factor = 0; + + const bindConstraint = this._title.get_constraints().find( + c => c.coordinate && c.coordinate === Clutter.BindCoordinate.Y); + bindConstraint.offset = 0; + }; + /* eslint-enable no-invalid-this */ + }); + + this._overrideMethod(previewProto, '_adjustOverlayOffsets', originalMethod => { + /* eslint-disable no-invalid-this */ + return function (...args) { + originalMethod.call(this, ...args); + + if (settings.get_boolean('window-captions-on-top')) + this._title.translation_y = -this._title.translation_y; + }; + /* eslint-enable no-invalid-this */ + }); + } + + disable() { + this._restoreMethods(); + global.stage.queue_relayout(); + } + + _saveMethod(prototype, methodName) { + let map = this._savedMethods.get(prototype); + if (!map) { + map = new Map(); + this._savedMethods.set(prototype, map); + } + + const originalMethod = prototype[methodName]; + map.set(methodName, originalMethod); + return originalMethod; + } + + _overrideMethod(prototype, methodName, createOverrideFunc) { + const originalMethod = this._saveMethod(prototype, methodName); + prototype[methodName] = createOverrideFunc(originalMethod); + } + + _restoreMethods() { + for (const [proto, map] of this._savedMethods) { + for (const [methodName, originalMethod] of map) { + if (originalMethod === undefined) + delete proto[methodName]; + else + proto[methodName] = originalMethod; + } + } + this._savedMethods.clear(); + } } /** */ -function enable() { - resetState(); - - let settings = ExtensionUtils.getSettings(); - - workspaceInjections['_createBestLayout'] = Workspace.WorkspaceLayout.prototype._createBestLayout; - Workspace.WorkspaceLayout.prototype._createBestLayout = function (_area) { - this._layoutStrategy = new NaturalLayoutStrategy({ - monitor: Main.layoutManager.monitors[this._monitorIndex], - }, settings); - return this._layoutStrategy.computeLayout(this._sortedWindows); - }; - - // position window titles on top of windows in overlay - winInjections['_init'] = WindowPreview.prototype._init; - WindowPreview.prototype._init = function (...args) { - winInjections['_init'].call(this, ...args); - - if (!settings.get_boolean('window-captions-on-top')) - return; - - const alignConstraint = this._title.get_constraints().find( - c => c.align_axis && c.align_axis === Clutter.AlignAxis.Y_AXIS); - alignConstraint.factor = 0; - - const bindConstraint = this._title.get_constraints().find( - c => c.coordinate && c.coordinate === Clutter.BindCoordinate.Y); - bindConstraint.offset = 0; - }; - winInjections['_adjustOverlayOffsets'] = - WindowPreview.prototype._adjustOverlayOffsets; - WindowPreview.prototype._adjustOverlayOffsets = function (...args) { - winInjections['_adjustOverlayOffsets'].call(this, ...args); - - if (settings.get_boolean('window-captions-on-top')) - this._title.translation_y = -this._title.translation_y; - }; -} - -/** - * @param {object} object - object that was modified - * @param {object} injection - the map of previous injections - * @param {string} name - the @injection key that should be removed - */ -function removeInjection(object, injection, name) { - if (injection[name] === undefined) - delete object[name]; - else - object[name] = injection[name]; -} - -/** */ -function disable() { - var i; - - for (i in workspaceInjections) - removeInjection(Workspace.WorkspaceLayout.prototype, workspaceInjections, i); - for (i in winInjections) - removeInjection(WindowPreview.prototype, winInjections, i); - - global.stage.queue_relayout(); - resetState(); +function init() { + return new Extension(); } diff --git a/extensions/places-menu/extension.js b/extensions/places-menu/extension.js index 7024ee30..d7fdab37 100644 --- a/extensions/places-menu/extension.js +++ b/extensions/places-menu/extension.js @@ -138,24 +138,27 @@ class PlacesMenu extends PanelMenu.Button { } } +class Extension { + constructor() { + ExtensionUtils.initTranslations(); + } + + enable() { + this._indicator = new PlacesMenu(); + + let pos = Main.sessionMode.panel.left.length; + if ('apps-menu' in Main.panel.statusArea) + pos++; + Main.panel.addToStatusArea('places-menu', this._indicator, pos, 'left'); + } + + disable() { + this._indicator.destroy(); + delete this._indicator; + } +} + /** */ function init() { - ExtensionUtils.initTranslations(); -} - -let _indicator; - -/** */ -function enable() { - _indicator = new PlacesMenu(); - - let pos = Main.sessionMode.panel.left.length; - if ('apps-menu' in Main.panel.statusArea) - pos++; - Main.panel.addToStatusArea('places-menu', _indicator, pos, 'left'); -} - -/** */ -function disable() { - _indicator.destroy(); + return new Extension(); } diff --git a/extensions/screenshot-window-sizer/extension.js b/extensions/screenshot-window-sizer/extension.js index d59ec3f3..97354474 100644 --- a/extensions/screenshot-window-sizer/extension.js +++ b/extensions/screenshot-window-sizer/extension.js @@ -26,146 +26,145 @@ const Main = imports.ui.main; const MESSAGE_FADE_TIME = 2000; -let text; +class Extension { + SIZES = [ + [624, 351], + [800, 450], + [1024, 576], + [1200, 675], + [1600, 900], + [360, 654], // Phone portrait maximized + [720, 360], // Phone landscape fullscreen + ]; -/** */ -function hideMessage() { - text.destroy(); - text = null; -} - -/** - * @param {string} message - the message to flash - */ -function flashMessage(message) { - if (!text) { - text = new St.Label({style_class: 'screenshot-sizer-message'}); - Main.uiGroup.add_actor(text); - } - - text.remove_all_transitions(); - text.text = message; - - text.opacity = 255; - - let monitor = Main.layoutManager.primaryMonitor; - text.set_position( - monitor.x + Math.floor(monitor.width / 2 - text.width / 2), - monitor.y + Math.floor(monitor.height / 2 - text.height / 2)); - - text.ease({ - opacity: 0, - duration: MESSAGE_FADE_TIME, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: hideMessage, - }); -} - -let SIZES = [ - [624, 351], - [800, 450], - [1024, 576], - [1200, 675], - [1600, 900], - [360, 654], // Phone portrait maximized - [720, 360], // Phone landscape fullscreen -]; - -/** - * @param {Meta.Display} display - the display - * @param {Meta.Window=} window - for per-window bindings, the window - * @param {Meta.KeyBinding} binding - the key binding - */ -function cycleScreenshotSizes(display, window, binding) { - // Probably this isn't useful with 5 sizes, but you can decrease instead - // of increase by holding down shift. - let modifiers = binding.get_modifiers(); - let backwards = (modifiers & Meta.VirtualModifier.SHIFT_MASK) !== 0; - - // Unmaximize first - if (window.get_maximized() !== 0) - window.unmaximize(Meta.MaximizeFlags.BOTH); - - let workArea = window.get_work_area_current_monitor(); - let outerRect = window.get_frame_rect(); - - // Double both axes if on a hidpi display - let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; - let scaledSizes = SIZES.map(size => size.map(wh => wh * scaleFactor)) - .filter(([w, h]) => w <= workArea.width && h <= workArea.height); - - // Find the nearest 16:9 size for the current window size - let nearestIndex; - let nearestError; - - for (let i = 0; i < scaledSizes.length; i++) { - let [width, height] = scaledSizes[i]; - - // get the best initial window size - let error = Math.abs(width - outerRect.width) + Math.abs(height - outerRect.height); - if (nearestIndex === undefined || error < nearestError) { - nearestIndex = i; - nearestError = error; + _flashMessage(message) { + if (!this._text) { + this._text = new St.Label({style_class: 'screenshot-sizer-message'}); + Main.uiGroup.add_actor(this._text); } + + this._text.remove_all_transitions(); + this._text.text = message; + + this._text.opacity = 255; + + const monitor = Main.layoutManager.primaryMonitor; + this._text.set_position( + monitor.x + Math.floor(monitor.width / 2 - this._text.width / 2), + monitor.y + Math.floor(monitor.height / 2 - this._text.height / 2)); + + this._text.ease({ + opacity: 0, + duration: MESSAGE_FADE_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this._hideMessage(), + }); } - // get the next size up or down from ideal - let newIndex = (nearestIndex + (backwards ? -1 : 1)) % scaledSizes.length; - let [newWidth, newHeight] = scaledSizes[newIndex]; + _hideMessage() { + this._text.destroy(); + delete this._text; + } - // Push the window onscreen if it would be resized offscreen - let newX = outerRect.x; - let newY = outerRect.y; - if (newX + newWidth > workArea.x + workArea.width) - newX = Math.max(workArea.x + workArea.width - newWidth); - if (newY + newHeight > workArea.y + workArea.height) - newY = Math.max(workArea.y + workArea.height - newHeight); + /** + * @param {Meta.Display} display - the display + * @param {Meta.Window=} window - for per-window bindings, the window + * @param {Meta.KeyBinding} binding - the key binding + */ + _cycleScreenshotSizes(display, window, binding) { + // Probably this isn't useful with 5 sizes, but you can decrease instead + // of increase by holding down shift. + let modifiers = binding.get_modifiers(); + let backwards = (modifiers & Meta.VirtualModifier.SHIFT_MASK) !== 0; - const id = window.connect('size-changed', () => { - window.disconnect(id); - _notifySizeChange(window); - }); - window.move_resize_frame(true, newX, newY, newWidth, newHeight); -} + // Unmaximize first + if (window.get_maximized() !== 0) + window.unmaximize(Meta.MaximizeFlags.BOTH); -/** - * @param {Meta.Window} window - the window whose size changed - */ -function _notifySizeChange(window) { - const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); - let newOuterRect = window.get_frame_rect(); - let message = '%d×%d'.format( - newOuterRect.width / scaleFactor, - newOuterRect.height / scaleFactor); + let workArea = window.get_work_area_current_monitor(); + let outerRect = window.get_frame_rect(); - // The new size might have been constrained by geometry hints (e.g. for - // a terminal) - in that case, include the actual ratio to the message - // we flash - let actualNumerator = 9 * newOuterRect.width / newOuterRect.height; - if (Math.abs(actualNumerator - 16) > 0.01) - message += ' (%.2f:9)'.format(actualNumerator); + // Double both axes if on a hidpi display + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let scaledSizes = this.SIZES.map(size => size.map(wh => wh * scaleFactor)) + .filter(([w, h]) => w <= workArea.width && h <= workArea.height); - flashMessage(message); + // Find the nearest 16:9 size for the current window size + let nearestIndex; + let nearestError; + + for (let i = 0; i < scaledSizes.length; i++) { + let [width, height] = scaledSizes[i]; + + // get the best initial window size + let error = Math.abs(width - outerRect.width) + Math.abs(height - outerRect.height); + if (nearestIndex === undefined || error < nearestError) { + nearestIndex = i; + nearestError = error; + } + } + + // get the next size up or down from ideal + let newIndex = (nearestIndex + (backwards ? -1 : 1)) % scaledSizes.length; + let [newWidth, newHeight] = scaledSizes[newIndex]; + + // Push the window onscreen if it would be resized offscreen + let newX = outerRect.x; + let newY = outerRect.y; + if (newX + newWidth > workArea.x + workArea.width) + newX = Math.max(workArea.x + workArea.width - newWidth); + if (newY + newHeight > workArea.y + workArea.height) + newY = Math.max(workArea.y + workArea.height - newHeight); + + const id = window.connect('size-changed', () => { + window.disconnect(id); + this._notifySizeChange(window); + }); + window.move_resize_frame(true, newX, newY, newWidth, newHeight); + } + + /** + * @param {Meta.Window} window - the window whose size changed + */ + _notifySizeChange(window) { + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + let newOuterRect = window.get_frame_rect(); + let message = '%d×%d'.format( + newOuterRect.width / scaleFactor, + newOuterRect.height / scaleFactor); + + // The new size might have been constrained by geometry hints (e.g. for + // a terminal) - in that case, include the actual ratio to the message + // we flash + let actualNumerator = 9 * newOuterRect.width / newOuterRect.height; + if (Math.abs(actualNumerator - 16) > 0.01) + message += ' (%.2f:9)'.format(actualNumerator); + + this._flashMessage(message); + } + + enable() { + Main.wm.addKeybinding( + 'cycle-screenshot-sizes', + ExtensionUtils.getSettings(), + Meta.KeyBindingFlags.PER_WINDOW, + Shell.ActionMode.NORMAL, + this._cycleScreenshotSizes.bind(this)); + Main.wm.addKeybinding( + 'cycle-screenshot-sizes-backward', + ExtensionUtils.getSettings(), + Meta.KeyBindingFlags.PER_WINDOW | Meta.KeyBindingFlags.IS_REVERSED, + Shell.ActionMode.NORMAL, + this._cycleScreenshotSizes.bind(this)); + } + + disable() { + Main.wm.removeKeybinding('cycle-screenshot-sizes'); + Main.wm.removeKeybinding('cycle-screenshot-sizes-backward'); + } } /** */ -function enable() { - Main.wm.addKeybinding( - 'cycle-screenshot-sizes', - ExtensionUtils.getSettings(), - Meta.KeyBindingFlags.PER_WINDOW, - Shell.ActionMode.NORMAL, - cycleScreenshotSizes); - Main.wm.addKeybinding( - 'cycle-screenshot-sizes-backward', - ExtensionUtils.getSettings(), - Meta.KeyBindingFlags.PER_WINDOW | Meta.KeyBindingFlags.IS_REVERSED, - Shell.ActionMode.NORMAL, - cycleScreenshotSizes); -} - -/** */ -function disable() { - Main.wm.removeKeybinding('cycle-screenshot-sizes'); - Main.wm.removeKeybinding('cycle-screenshot-sizes-backward'); +function init() { + return new Extension(); } diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js index a9b3fd46..f3f1bb4d 100644 --- a/extensions/workspace-indicator/extension.js +++ b/extensions/workspace-indicator/extension.js @@ -454,20 +454,23 @@ class WorkspaceIndicator extends PanelMenu.Button { } } +class Extension { + constructor() { + ExtensionUtils.initTranslations(); + } + + enable() { + this._indicator = new WorkspaceIndicator(); + Main.panel.addToStatusArea('workspace-indicator', this._indicator); + } + + disable() { + this._indicator.destroy(); + delete this._indicator; + } +} + /** */ function init() { - ExtensionUtils.initTranslations(); -} - -let _indicator; - -/** */ -function enable() { - _indicator = new WorkspaceIndicator(); - Main.panel.addToStatusArea('workspace-indicator', _indicator); -} - -/** */ -function disable() { - _indicator.destroy(); + return new Extension(); }