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(); }