/** * API Library * * @author Javad Rahmatzadeh * @copyright 2020-2024 * @license GPL-3.0-only */ const XY_POSITION = { TOP_START: 0, TOP_CENTER: 1, TOP_END: 2, BOTTOM_START: 3, BOTTOM_CENTER: 4, BOTTOM_END: 5, CENTER_START: 6, CENTER_CENTER: 7, CENTER_END: 8, }; const PANEL_POSITION = { TOP: 0, BOTTOM: 1, }; const PANEL_BOX_POSITION = { CENTER: 0, RIGHT: 1, LEFT: 2, }; const PANEL_HIDE_MODE = { ALL: 0, DESKTOP: 1, }; const SHELL_STATUS = { NONE: 0, OVERVIEW: 1, }; const DASH_ICON_SIZES = [16, 22, 24, 32, 40, 48, 56, 64]; /** * API to avoid calling GNOME Shell directly * and make all parts compatible with different GNOME Shell versions */ export class API { /** * Current shell version * * @type {number|null} */ #shellVersion = null; /** * Originals holder * * @type {object} */ #originals = {}; /** * Timeout ids * * @type {object} */ #timeoutIds = {}; /** * Class Constructor * * @param {Object} dependencies * 'Main' reference to ui::main * 'BackgroundMenu' reference to ui::backgroundMenu * 'OverviewControls' reference to ui::overviewControls * 'WorkspaceSwitcherPopup' reference to ui::workspaceSwitcherPopup * 'SwitcherPopup' reference to ui::switcherPopup * 'InterfaceSettings' reference to Gio::Settings for 'org.gnome.desktop.interface' * 'Search' reference to ui::search * 'SearchController' reference to ui::searchController * 'WorkspaceThumbnail' reference to ui::workspaceThumbnail * 'WorkspacesView' reference to ui::workspacesView * 'Panel' reference to ui::panel * 'PanelMenu' reference to ui::panelMenu * 'WindowPreview' reference to ui::windowPreview * 'Workspace' reference to ui::workspace * 'LookingGlass' reference to ui::lookingGlass * 'MessageTray' reference to ui::messageTray * 'OSDWindow' reference to ui::osdTray * 'WindowMenu' reference to ui::windowMenu * 'AltTab' reference to ui::altTab * 'St' reference to St * 'GLib' reference to GLib * 'Clutter' reference to Clutter * 'Util' reference to misc::util * 'Meta' reference to Meta * 'GObject' reference to GObject * @param {number} shellVersion float in major.minor format */ constructor(dependencies, shellVersion) { this._main = dependencies['Main'] || null; this._backgroundMenu = dependencies['BackgroundMenu'] || null; this._overviewControls = dependencies['OverviewControls'] || null; this._workspaceSwitcherPopup = dependencies['WorkspaceSwitcherPopup'] || null; this._switcherPopup = dependencies['SwitcherPopup'] || null; this._interfaceSettings = dependencies['InterfaceSettings'] || null; this._search = dependencies['Search'] || null; this._searchController = dependencies['SearchController'] || null; this._workspaceThumbnail = dependencies['WorkspaceThumbnail'] || null; this._workspacesView = dependencies['WorkspacesView'] || null; this._panel = dependencies['Panel'] || null; this._panelMenu = dependencies['PanelMenu'] || null; this._windowPreview = dependencies['WindowPreview'] || null; this._workspace = dependencies['Workspace'] || null; this._lookingGlass = dependencies['LookingGlass'] || null; this._messageTray = dependencies['MessageTray'] || null; this._osdWindow = dependencies['OSDWindow'] || null; this._windowMenu = dependencies['WindowMenu'] || null; this._altTab = dependencies['AltTab'] || null; this._st = dependencies['St'] || null; this._glib = dependencies['GLib'] || null; this._clutter = dependencies['Clutter'] || null; this._util = dependencies['Util'] || null; this._meta = dependencies['Meta'] || null; this._gobject = dependencies['GObject'] || null; this.#shellVersion = shellVersion; /** * whether search entry is visible * * @member {boolean} */ this._searchEntryVisibility = true; } /** * prepare everything needed for API * * @returns {void} */ open() { this.UIStyleClassAdd(this.#getAPIClassname('shell-version')); } /** * remove everything from GNOME Shell been added by this class * * @returns {void} */ close() { this.UIStyleClassRemove(this.#getAPIClassname('shell-version')); this.#startSearchSignal(false); this.#computeWorkspacesBoxForStateSetDefault(); this.#altTabSizesSetDefault(); for (let [name, id] of Object.entries(this.#timeoutIds)) { this._glib.source_remove(id); delete(this.#timeoutIds[name]); } } /** * get x and y align for position * * @param int pos position * see XY_POSITION * * @returns {array} * - 0 Clutter.ActorAlign * - 1 Clutter.ActorAlign */ #xyAlignGet(pos) { if (XY_POSITION.TOP_START === pos) { return [this._clutter.ActorAlign.START, this._clutter.ActorAlign.START]; } if (XY_POSITION.TOP_CENTER === pos) { return [this._clutter.ActorAlign.CENTER, this._clutter.ActorAlign.START]; } if (XY_POSITION.TOP_END === pos) { return [this._clutter.ActorAlign.END, this._clutter.ActorAlign.START]; } if (XY_POSITION.CENTER_START === pos) { return [this._clutter.ActorAlign.START, this._clutter.ActorAlign.CENTER]; } if (XY_POSITION.CENTER_CENTER === pos) { return [this._clutter.ActorAlign.CENTER, this._clutter.ActorAlign.CENTER]; } if (XY_POSITION.CENTER_END === pos) { return [this._clutter.ActorAlign.END, this._clutter.ActorAlign.CENTER]; } if (XY_POSITION.BOTTOM_START === pos) { return [this._clutter.ActorAlign.START, this._clutter.ActorAlign.END]; } if (XY_POSITION.BOTTOM_CENTER === pos) { return [this._clutter.ActorAlign.CENTER, this._clutter.ActorAlign.END]; } if (XY_POSITION.BOTTOM_END === pos) { return [this._clutter.ActorAlign.END, this._clutter.ActorAlign.END]; } } /** * add to animation duration * * @param {number} duration in milliseconds * * @returns {number} */ #addToAnimationDuration(duration) { let settings = this._st.Settings.get(); return (settings.enable_animations) ? settings.slow_down_factor * duration : 1; } /** * get signal id of the event * * @param {Gtk.Widget} widget to find signal in * @param {string} signalName signal name * * @returns {number} */ #getSignalId(widget, signalName) { return this._gobject.signal_handler_find(widget, {signalId: signalName}); } /** * get the css class name for API * * @param {string} type * * @returns {string} */ #getAPIClassname(type) { let starter = 'just-perfection-api-'; if (type === 'shell-version') { let shellVerMajor = Math.trunc(this.#shellVersion); return `${starter}gnome${shellVerMajor}`; } return `${starter}${type}`; } /** * set panel size to default * * @returns {void} */ panelSetDefaultSize() { if (!this.#originals['panelHeight']) { return; } this.panelSetSize(this.#originals['panelHeight'], false); } /** * change panel size * * @param {number} size 0 to 100 * @param {boolean} fake true means it shouldn't change the last size, * false otherwise * * @returns {void} */ panelSetSize(size, fake) { if (!this.#originals['panelHeight']) { this.#originals['panelHeight'] = this._main.panel.height; } if (size > 100 || size < 0) { return; } this._main.panel.height = size; if (!fake) { this._panelSize = size; } } /** * get the last size of the panel * * @returns {number} */ panelGetSize() { if (this._panelSize !== undefined) { return this._panelSize; } if (this.#originals['panelHeight']) { return this.#originals['panelHeight']; } return this._main.panel.height; } /** * emit refresh styles * this is useful when changed style doesn't emit change because doesn't have * standard styles. for example, style with only `-natural-hpadding` * won't notify any change. so you need to call this function * to refresh that * * @returns {void} */ #emitRefreshStyles() { let classname = this.#getAPIClassname('refresh-styles'); this.UIStyleClassAdd(classname); this.UIStyleClassRemove(classname); } /** * show panel * * @returns {void} */ panelShow() { this._panelVisibility = true; let classname = this.#getAPIClassname('no-panel'); if (!this.UIStyleClassContain(classname)) { return; } // The class name should be removed before addChrome the panelBox // removing after can cause `st_theme_node_lookup_shadow` crash this.UIStyleClassRemove(classname); let overview = this._main.overview; let searchEntryParent = overview.searchEntry.get_parent(); let panelBox = this._main.layoutManager.panelBox; panelBox.translation_y = 0; this._main.layoutManager.overviewGroup.remove_child(panelBox); this._main.layoutManager.addChrome(panelBox, { affectsStruts: true, trackFullscreen: true, }); if (this._hidePanelWorkareasChangedSignal) { global.display.disconnect(this._hidePanelWorkareasChangedSignal); delete(this._hidePanelWorkareasChangedSignal); } if (this._hidePanelHeightSignal) { panelBox.disconnect(this._hidePanelHeightSignal); delete(this._hidePanelHeightSignal); } searchEntryParent.set_style(`margin-top: 0;`); // hide and show can fix windows going under panel panelBox.hide(); panelBox.show(); this.#fixLookingGlassPosition(); if (this.#timeoutIds.panelHide) { this._glib.source_remove(this.#timeoutIds.panelHide); delete(this.#timeoutIds.panelHide); } } /** * hide panel * * @param {mode} hide mode see PANEL_HIDE_MODE. defaults to hide all * @param {boolean} force apply hide even if it is hidden * * @returns {void} */ panelHide(mode) { this._panelVisibility = false; this._panelHideMode = mode; let overview = this._main.overview; let searchEntryParent = overview.searchEntry.get_parent(); let panelBox = this._main.layoutManager.panelBox; let panelHeight = this._main.panel.height; let panelPosition = this.panelGetPosition(); let direction = (panelPosition === PANEL_POSITION.BOTTOM) ? 1 : -1; if (panelBox.get_parent() === this._main.layoutManager.uiGroup) { this._main.layoutManager.removeChrome(panelBox); this._main.layoutManager.overviewGroup.insert_child_at_index(panelBox, 0); } panelBox.translation_y = (mode === PANEL_HIDE_MODE.DESKTOP) ? 0 : panelHeight * direction; if (panelPosition === PANEL_POSITION.TOP) { // when panel is hidden the first element gets too close to the top, // so we fix it with top margin in search entry let marginTop = (mode === PANEL_HIDE_MODE.ALL) ? 15 : panelHeight; searchEntryParent.set_style(`margin-top: ${marginTop}px;`); } else { searchEntryParent.set_style(`margin-top: 0;`); } // hide and show can fix windows going under panel panelBox.hide(); panelBox.show(); this.#fixLookingGlassPosition(); if (this._hidePanelWorkareasChangedSignal) { global.display.disconnect(this._hidePanelWorkareasChangedSignal); delete(this._hidePanelWorkareasChangedSignal); } this._hidePanelWorkareasChangedSignal = global.display.connect( 'workareas-changed', () => { this.panelHide(this._panelHideMode); } ); if (!this._hidePanelHeightSignal) { this._hidePanelHeightSignal = panelBox.connect( 'notify::height', () => { this.panelHide(this._panelHideMode); } ); } let classname = this.#getAPIClassname('no-panel'); this.UIStyleClassAdd(classname); // update hot corners since we need to make them available // outside overview this._main.layoutManager._updateHotCorners(); // Maximized windows will have bad maximized gap after unlock in Wayland // This is a Mutter issue, // See https://gitlab.gnome.org/GNOME/mutter/-/issues/1627 // TODO remove after the issue is fixed on Mutter if (this._meta.is_wayland_compositor()) { let duration = this.#addToAnimationDuration(180); this.#timeoutIds.panelHide = this._glib.timeout_add( this._glib.PRIORITY_DEFAULT, duration, () => { panelBox.hide(); panelBox.show(); return this._glib.SOURCE_REMOVE; } ); } } /** * check whether panel is visible * * @returns {boolean} */ isPanelVisible() { if (this._panelVisibility === undefined) { return true; } return this._panelVisibility; } /** * check whether dash is visible * * @returns {boolean} */ isDashVisible() { return this._dashVisibility === undefined || this._dashVisibility; } /** * show dash * * @returns {void} */ dashShow() { if (!this._main.overview.dash || this.isDashVisible()) { return; } this._dashVisibility = true; this._main.overview.dash.show(); this._main.overview.dash.height = -1; this._main.overview.dash.setMaxSize(-1, -1); this.#updateWindowPreviewOverlap(); } /** * hide dash * * @returns {void} */ dashHide() { if (!this._main.overview.dash || !this.isDashVisible()) { return; } this._dashVisibility = false; this._main.overview.dash.hide(); this._main.overview.dash.height = 0; this.#updateWindowPreviewOverlap(); } /** * update window preview overlap * * @returns {void} */ #updateWindowPreviewOverlap() { let wpp = this._windowPreview.WindowPreview.prototype; if (this.isDashVisible() && wpp.overlapHeightsOld) { wpp.overlapHeights = wpp.overlapHeightsOld; delete(wpp.overlapHeightsOld); return; } if (!this.isDashVisible()) { wpp.overlapHeightsOld = wpp.overlapHeights; wpp.overlapHeights = function () { let [top, bottom] = this.overlapHeightsOld(); return [top + 24, bottom + 24]; }; } } /** * add class name to the UI group * * @param {string} classname class name * * @returns {void} */ UIStyleClassAdd(classname) { this._main.layoutManager.uiGroup.add_style_class_name(classname); } /** * remove class name from UI group * * @param {string} classname class name * * @returns {void} */ UIStyleClassRemove(classname) { this._main.layoutManager.uiGroup.remove_style_class_name(classname); } /** * check whether UI group has class name * * @param {string} classname class name * * @returns {boolean} */ UIStyleClassContain(classname) { return this._main.layoutManager.uiGroup.has_style_class_name(classname); } /** * enable background menu * * @returns {void} */ backgroundMenuEnable() { if (!this.#originals['backgroundMenuOpen']) { return; } this._backgroundMenu.BackgroundMenu.prototype.open = this.#originals['backgroundMenuOpen']; } /** * disable background menu * * @returns {void} */ backgroundMenuDisable() { let backgroundMenuProto = this._backgroundMenu.BackgroundMenu.prototype; if (!this.#originals['backgroundMenuOpen']) { this.#originals['backgroundMenuOpen'] = backgroundMenuProto.open; } backgroundMenuProto.open = () => {}; } /** * show search * * @param {boolean} fake true means it just needs to do the job but * don't need to change the search visibility status * * @returns {void} */ searchEntryShow(fake) { let classname = this.#getAPIClassname('no-search'); if (!this.UIStyleClassContain(classname)) { return; } this.UIStyleClassRemove(classname); let searchEntry = this._main.overview.searchEntry; let searchEntryParent = searchEntry.get_parent(); searchEntryParent.ease({ height: searchEntry.height, opacity: 255, mode: this._clutter.AnimationMode.EASE, duration: 110, onComplete: () => { searchEntryParent.height = -1; searchEntry.ease({ opacity: 255, mode: this._clutter.AnimationMode.EASE, duration: 700, }); }, }); if (!fake) { this._searchEntryVisibility = true; } this.#computeWorkspacesBoxForStateChanged(); } /** * hide search * * @param {boolean} fake true means it just needs to do the job * but don't need to change the search visibility status * * @returns {void} */ searchEntryHide(fake) { this.UIStyleClassAdd(this.#getAPIClassname('no-search')); let searchEntry = this._main.overview.searchEntry; let searchEntryParent = searchEntry.get_parent(); searchEntry.ease({ opacity: 0, mode: this._clutter.AnimationMode.EASE, duration: 50, }); searchEntryParent.ease({ height: 0, opacity: 0, mode: this._clutter.AnimationMode.EASE, duration: 120, }); if (!fake) { this._searchEntryVisibility = false; } this.#computeWorkspacesBoxForStateChanged(); } /** * enable start search * * @returns {void} */ startSearchEnable() { this.#startSearchSignal(true); if (!this.#originals['startSearch']) { return; } this._searchController.SearchController.prototype.startSearch = this.#originals['startSearch']; } /** * disable start search * * @returns {void} */ startSearchDisable() { this.#startSearchSignal(false); if (!this.#originals['startSearch']) { this.#originals['startSearch'] = this._searchController.SearchController.prototype.startSearch } this._searchController.SearchController.prototype.startSearch = () => {}; } /** * add search signals that needs to be show search entry when the * search entry is hidden * * @param {boolean} add true means add the signal, false means remove * the signal * * @returns {void} */ #startSearchSignal(add) { let controller = this._main.overview.viewSelector || this._main.overview._overview.viewSelector || this._main.overview._overview.controls._searchController; // remove if (!add) { if (this._searchActiveSignal) { controller.disconnect(this._searchActiveSignal); this._searchActiveSignal = null; } return; } // add if (this._searchActiveSignal) { return; } this._searchActiveSignal = controller.connect('notify::search-active', () => { if (this._searchEntryVisibility) { return; } let inSearch = controller.searchActive; if (inSearch) { this.UIStyleClassAdd(this.#getAPIClassname('type-to-search')); this.searchEntryShow(true); } else { this.UIStyleClassRemove(this.#getAPIClassname('type-to-search')); this.searchEntryHide(true); } }); } /** * Set maximum displayed search result to default value * * @returns {void} */ setMaxDisplayedSearchResultToDefault() { if (!this.#originals['searchGetMaxDisplayedResults']) { return; } let ListSearchResultsProto = this._search.ListSearchResults.prototype; ListSearchResultsProto._getMaxDisplayedResults = this.#originals['searchGetMaxDisplayedResults']; } /** * Set maximum displayed search result * * @param {number} items max items * * @returns {void} */ setMaxDisplayedSearchResult(items) { let ListSearchResultsProto = this._search.ListSearchResults.prototype; if (!this.#originals['searchGetMaxDisplayedResults']) { this.#originals['searchGetMaxDisplayedResults'] = ListSearchResultsProto._getMaxDisplayedResults; } ListSearchResultsProto._getMaxDisplayedResults = () => { return items; } } /** * enable OSD * * @returns {void} */ OSDEnable() { if (!this.#originals['osdWindowManagerShow']) { return; } this._main.osdWindowManager.show = this.#originals['osdWindowManagerShow']; } /** * disable OSD * * @returns {void} */ OSDDisable() { if (!this.#originals['osdWindowManagerShow']) { this.#originals['osdWindowManagerShow'] = this._main.osdWindowManager.show; } this._main.osdWindowManager.show = () => {}; } /** * enable workspace popup * * @returns {void} */ workspacePopupEnable() { if (!this.#originals['workspaceSwitcherPopupDisplay']) { return; } this._workspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype.display = this.#originals['workspaceSwitcherPopupDisplay'] } /** * disable workspace popup * * @returns {void} */ workspacePopupDisable() { if (!this.#originals['workspaceSwitcherPopupDisplay']) { this.#originals['workspaceSwitcherPopupDisplay'] = this._workspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype.display; } this._workspaceSwitcherPopup.WorkspaceSwitcherPopup.prototype.display = function (index) { this.destroy(); }; } /** * show workspace switcher * * @returns {void} */ workspaceSwitcherShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-workspace')); this.#workspaceSwitcherShouldShowSetToLast(); } /** * hide workspace switcher * * @returns {void} */ workspaceSwitcherHide() { this.workspaceSwitcherShouldShow(false, true); // should be after `this.workspaceSwitcherShouldShow()` // since it checks whether it's visible or not this.UIStyleClassAdd(this.#getAPIClassname('no-workspace')); } /** * check whether workspace switcher is visible * * @returns {boolean} */ isWorkspaceSwitcherVisible() { return !this.UIStyleClassContain(this.#getAPIClassname('no-workspace')); } /** * set workspace switcher to its default size * * @returns {void} */ workspaceSwitcherSetDefaultSize() { let thumbnailsBox = this._main.overview._overview._controls._thumbnailsBox; let ThumbnailsBoxProto = this._workspaceThumbnail.ThumbnailsBox.prototype; if (!ThumbnailsBoxProto._initOld) { return; } ThumbnailsBoxProto._init = ThumbnailsBoxProto._initOld; delete(ThumbnailsBoxProto._initOld); thumbnailsBox._maxThumbnailScale = this._workspaceThumbnail.MAX_THUMBNAIL_SCALE; } /** * set workspace switcher size * * @param {number} size in float * * @returns {void} */ workspaceSwitcherSetSize(size) { let thumbnailsBox = this._main.overview._overview._controls._thumbnailsBox; let ThumbnailsBoxProto = this._workspaceThumbnail.ThumbnailsBox.prototype; thumbnailsBox._maxThumbnailScale = size; if (!ThumbnailsBoxProto._initOld) { ThumbnailsBoxProto._initOld = ThumbnailsBoxProto._init; } ThumbnailsBoxProto._init = function(...params) { this._maxThumbnailScale = size; this._initOld(...params); }; } /** * add element to stage * * @param {St.Widget} element widget * * @returns {void} */ chromeAdd(element) { this._main.layoutManager.addChrome(element, { affectsInputRegion : true, affectsStruts : false, trackFullscreen : true, }); } /** * remove element from stage * * @param {St.Widget} element widget * * @returns {void} */ chromeRemove(element) { this._main.layoutManager.removeChrome(element); } /** * show activities button * * @returns {void} */ activitiesButtonShow() { let activities = this._main.panel.statusArea.activities; if (!this.isLocked() && activities) { activities.container.show(); } } /** * hide activities button * * @returns {void} */ activitiesButtonHide() { let activities = this._main.panel.statusArea.activities; if (activities) { activities.container.hide(); } } /** * show date menu * * @returns {void} */ dateMenuShow() { if (!this.isLocked()) { this._main.panel.statusArea.dateMenu.container.show(); } } /** * hide date menu * * @returns {void} */ dateMenuHide() { this._main.panel.statusArea.dateMenu.container.hide(); } /** * show keyboard layout * * @returns {void} */ keyboardLayoutShow() { this._main.panel.statusArea.keyboard.container.show(); } /** * hide keyboard layout * * @returns {void} */ keyboardLayoutHide() { this._main.panel.statusArea.keyboard.container.hide(); } /** * show accessibility menu * * @returns {void} */ accessibilityMenuShow() { this._main.panel.statusArea.a11y?.container.show(); } /** * hide accessibility menu * * @returns {void} */ accessibilityMenuHide() { this._main.panel.statusArea.a11y?.container.hide(); } /** * show quick settings menu * * @returns {void} */ quickSettingsMenuShow() { this._main.panel.statusArea.quickSettings.container.show(); } /** * hide quick settings menu * * @returns {void} */ quickSettingsMenuHide() { this._main.panel.statusArea.quickSettings.container.hide(); } /** * check whether lock dialog is currently showing * * @returns {boolean} */ isLocked() { return this._main.sessionMode.isLocked; } /** * enable window picker icon * * @returns {void} */ windowPickerIconEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-window-picker-icon')); } /** * disable window picker icon * * @returns {void} */ windowPickerIconDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-window-picker-icon')); } /** * show power icon * * @returns {void} */ powerIconShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-power-icon')); } /** * hide power icon * * @returns {void} */ powerIconHide() { this.UIStyleClassAdd(this.#getAPIClassname('no-power-icon')); } /** * get primary monitor information * * @returns {false|Object} false when monitor does not exist | object * x: int * y: int * width: int * height: int * geometryScale: float */ monitorGetInfo() { let pMonitor = this._main.layoutManager.primaryMonitor; if (!pMonitor) { return false; } return { 'x': pMonitor.x, 'y': pMonitor.y, 'width': pMonitor.width, 'height': pMonitor.height, 'geometryScale': pMonitor.geometry_scale, }; } /** * get panel position * * @returns {number} see PANEL_POSITION */ panelGetPosition() { if (this._panelPosition === undefined) { return PANEL_POSITION.TOP; } return this._panelPosition; } /** * move panel position * * @param {number} position see PANEL_POSITION * @param {boolean} force allow to set even when the current position * is the same * * @returns {void} */ panelSetPosition(position, force = false) { let monitorInfo = this.monitorGetInfo(); let panelBox = this._main.layoutManager.panelBox; if (!force && position === this.panelGetPosition()) { return; } if (position === PANEL_POSITION.TOP) { this._panelPosition = PANEL_POSITION.TOP; if (this._workareasChangedSignal) { global.display.disconnect(this._workareasChangedSignal); this._workareasChangedSignal = null; } if (this._panelHeightSignal) { panelBox.disconnect(this._panelHeightSignal); this._panelHeightSignal = null; } let topX = (monitorInfo) ? monitorInfo.x : 0; let topY = (monitorInfo) ? monitorInfo.y : 0; panelBox.set_position(topX, topY); this.UIStyleClassRemove(this.#getAPIClassname('bottom-panel')); this.#fixPanelMenuSide(this._st.Side.TOP); this.#fixLookingGlassPosition(); return; } this._panelPosition = PANEL_POSITION.BOTTOM; // only change it when a monitor detected // 'workareas-changed' signal will do the job on next monitor detection if (monitorInfo) { let BottomX = monitorInfo.x; let BottomY = monitorInfo.y + monitorInfo.height - this.panelGetSize(); panelBox.set_position(BottomX, BottomY); this.UIStyleClassAdd(this.#getAPIClassname('bottom-panel')); } if (!this._workareasChangedSignal) { this._workareasChangedSignal = global.display.connect('workareas-changed', () => { this.panelSetPosition(PANEL_POSITION.BOTTOM, true); }); } if (!this._panelHeightSignal) { this._panelHeightSignal = panelBox.connect('notify::height', () => { this.panelSetPosition(PANEL_POSITION.BOTTOM, true); }); } this.#fixPanelMenuSide(this._st.Side.BOTTOM); this.#fixLookingGlassPosition(); } /** * fix panel menu opening side based on panel position * * @param {number} position St.Side value * is the same * * @returns {void} */ #fixPanelMenuSide(position) { let PanelMenuButton = this._panelMenu.Button; let PanelMenuButtonProto = PanelMenuButton.prototype; // Set Instances let findPanelMenus = (widget) => { if (widget instanceof PanelMenuButton && widget.menu?._boxPointer) { widget.menu._boxPointer._userArrowSide = position; } widget.get_children().forEach(subWidget => { findPanelMenus(subWidget) }); } let panelBoxes = [ this._main.panel._centerBox, this._main.panel._rightBox, this._main.panel._leftBox, ]; panelBoxes.forEach(panelBox => findPanelMenus(panelBox)); // Set Prototypes if (position === this._st.Side.TOP) { // reset to default since GNOME Shell panel is top by default if (PanelMenuButtonProto._setMenuOld) { PanelMenuButtonProto.setMenu = PanelMenuButtonProto._setMenuOld; } return; } if (!PanelMenuButtonProto._setMenuOld) { PanelMenuButtonProto._setMenuOld = PanelMenuButtonProto.setMenu; } PanelMenuButtonProto.setMenu = function (menu) { this._setMenuOld(menu); if (menu) { menu._boxPointer._userArrowSide = position; } } } /** * fix looking glass position * * @returns {void} */ #fixLookingGlassPosition() { let lookingGlassProto = this._lookingGlass.LookingGlass.prototype; if (this.#originals['lookingGlassResize'] === undefined) { this.#originals['lookingGlassResize'] = lookingGlassProto._resize; } if (this.panelGetPosition() === PANEL_POSITION.TOP && this.isPanelVisible()) { lookingGlassProto._resize = this.#originals['lookingGlassResize']; delete(lookingGlassProto._oldResizeMethod); delete(this.#originals['lookingGlassResize']); return; } if (lookingGlassProto._oldResizeMethod === undefined) { lookingGlassProto._oldResizeMethod = this.#originals['lookingGlassResize']; const Main = this._main; lookingGlassProto._resize = function () { let panelHeight = Main.layoutManager.panelBox.height; this._oldResizeMethod(); this._targetY -= panelHeight; this._hiddenY -= panelHeight; }; } } /** * enable panel notification icon * * @returns {void} */ panelNotificationIconEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-panel-notification-icon')); } /** * disable panel notification icon * * @returns {void} */ panelNotificationIconDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-panel-notification-icon')); } /** * disconnect all clock menu position signals * * @returns {void} */ #disconnectClockMenuPositionSignals() { let panelBoxes = [ this._main.panel._centerBox, this._main.panel._rightBox, this._main.panel._leftBox, ]; if (this._clockMenuPositionSignals) { for (let i = 0; i <= 2; i++) { panelBoxes[i].disconnect(this._clockMenuPositionSignals[i]); } delete(this._clockMenuPositionSignals); } } /** * set the clock menu position to default * * @returns {void} */ clockMenuPositionSetDefault() { this.clockMenuPositionSet(0, 0); this.#disconnectClockMenuPositionSignals(); } /** * set the clock menu position * * @param {number} pos see PANEL_BOX_POSITION * @param {number} offset starts from 0 * * @returns {void} */ clockMenuPositionSet(pos, offset) { let dateMenu = this._main.panel.statusArea.dateMenu; let panelBoxes = [ this._main.panel._centerBox, this._main.panel._rightBox, this._main.panel._leftBox, ]; this.#disconnectClockMenuPositionSignals(); let fromPos = -1; let fromIndex = -1; let toIndex = -1; let childLength = 0; for (let i = 0; i <= 2; i++) { let child = panelBoxes[i].get_children(); let childIndex = child.indexOf(dateMenu.container); if (childIndex !== -1) { fromPos = i; fromIndex = childIndex; childLength = panelBoxes[pos].get_children().length; toIndex = (offset > childLength) ? childLength : offset; break; } } // couldn't find the from and to position because it has been removed if (fromPos === -1 || fromIndex === -1 || toIndex === -1) { return; } if (pos === fromPos && toIndex === fromIndex) { return; } panelBoxes[fromPos].remove_child(dateMenu.container); panelBoxes[pos].insert_child_at_index(dateMenu.container, toIndex); if (this.isLocked()) { this.dateMenuHide(); } if (!this._clockMenuPositionSignals) { this._clockMenuPositionSignals = [null, null, null]; for (let i = 0; i <= 2; i++) { this._clockMenuPositionSignals[i] = panelBoxes[i].connect( (this.#shellVersion >= 46) ? 'child-added' : 'actor-added', () => { this.clockMenuPositionSet(pos, offset); } ); } } } /** * enable show apps button * * @returns {void} */ showAppsButtonEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-show-apps-button')); } /** * disable show apps button * * @returns {void} */ showAppsButtonDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-show-apps-button')); } /** * set animation speed as default * * @returns {void} */ animationSpeedSetDefault() { if (this.#originals['StSlowDownFactor'] === undefined) { return; } this._st.Settings.get().slow_down_factor = this.#originals['StSlowDownFactor']; } /** * change animation speed * * @param {number} factor in float. bigger number means slower * * @returns {void} */ animationSpeedSet(factor) { if (this.#originals['StSlowDownFactor'] === undefined) { this.#originals['StSlowDownFactor'] = this._st.Settings.get().slow_down_factor; } this._st.Settings.get().slow_down_factor = factor; } /** * set the enable animation as default * * @returns {void} */ enableAnimationsSetDefault() { if (this.#originals['enableAnimations'] === undefined) { return; } let status = this.#originals['enableAnimations']; this._interfaceSettings.set_boolean('enable-animations', status); } /** * set the enable animation status * * @param {boolean} status true to enable, false otherwise * * @returns {void} */ enableAnimationsSet(status) { if (this.#originals['enableAnimations'] === undefined) { this.#originals['enableAnimations'] = this._interfaceSettings.get_boolean('enable-animations'); } this._interfaceSettings.set_boolean('enable-animations', status); } /** * enable focus when window demands attention happens * * @returns {void} */ windowDemandsAttentionFocusEnable() { if ( this._displayWindowDemandsAttentionSignal || this._displayWindowMarkedUrgentSignal ) { return; } let display = global.display; let demandFunction = (display, window) => { if (!window || window.has_focus() || window.is_skip_taskbar()) { return; } this._main.activateWindow(window); }; this._displayWindowDemandsAttentionSignal = display.connect('window-demands-attention', demandFunction); this._displayWindowMarkedUrgentSignal = display.connect('window-marked-urgent', demandFunction); // since removing '_windowDemandsAttentionId' doesn't have any effect // we remove the original signal and re-connect it on disable let signalId = this.#getSignalId(global.display, 'window-demands-attention'); let signalId2 = this.#getSignalId(global.display, 'window-marked-urgent'); display.disconnect(signalId); display.disconnect(signalId2); } /** * disable focus when window demands attention happens * * @returns {void} */ windowDemandsAttentionFocusDisable() { if ( !this._displayWindowDemandsAttentionSignal || !this._displayWindowMarkedUrgentSignal ) { return; } let display = global.display; display.disconnect(this._displayWindowDemandsAttentionSignal); display.disconnect(this._displayWindowMarkedUrgentSignal); this._displayWindowDemandsAttentionSignal = null; this._displayWindowMarkedUrgentSignal = null; let wah = this._main.windowAttentionHandler; wah._windowDemandsAttentionId = display.connect( 'window-demands-attention', wah._onWindowDemandsAttention.bind(wah) ); wah._windowDemandsAttentionId = display.connect( 'window-marked-urgent', wah._onWindowDemandsAttention.bind(wah) ); } /** * enable maximizing windows on creation * * @returns {void} */ windowMaximizedOnCreateEnable() { if (this._displayWindowCreatedSignal) { return; } let display = global.display; let createdFunction = (display, window) => { if (window.can_maximize()) { window.maximize(this._meta.MaximizeFlags.HORIZONTAL | this._meta.MaximizeFlags.VERTICAL); } }; this._displayWindowCreatedSignal = display.connect('window-created', createdFunction); } /** * disable maximizing windows on creation * * @returns {void} */ windowMaximizedOnCreateDisable() { if (!this._displayWindowCreatedSignal) { return; } let display = global.display; display.disconnect(this._displayWindowCreatedSignal); delete(this._displayWindowCreatedSignal); } /** * set startup status * * @param {number} status see SHELL_STATUS for available status * * @returns {void} */ startupStatusSet(status) { let sessionMode = this._main.sessionMode; let layoutManager = this._main.layoutManager; if (!layoutManager._startingUp) { return; } if (this.#originals['sessionModeHasOverview'] === undefined) { this.#originals['sessionModeHasOverview'] = sessionMode.hasOverview; } let ControlsState = this._overviewControls.ControlsState; let Controls = this._main.overview._overview.controls; switch (status) { case SHELL_STATUS.NONE: sessionMode.hasOverview = false; layoutManager.startInOverview = false; Controls._stateAdjustment.value = ControlsState.HIDDEN; break; case SHELL_STATUS.OVERVIEW: default: sessionMode.hasOverview = true; layoutManager.startInOverview = true; break; } if (!this._startupCompleteSignal) { this._startupCompleteSignal = layoutManager.connect('startup-complete', () => { sessionMode.hasOverview = this.#originals['sessionModeHasOverview']; }); } } /** * set startup status to default * * @returns {void} */ startupStatusSetDefault() { if (this.#originals['sessionModeHasOverview'] === undefined) { return; } if (this._startupCompleteSignal) { this._main.layoutManager.disconnect(this._startupCompleteSignal); } } /** * set dash icon size to default * * @returns {void} */ dashIconSizeSetDefault() { let classnameStarter = this.#getAPIClassname('dash-icon-size'); DASH_ICON_SIZES.forEach(size => { this.UIStyleClassRemove(classnameStarter + size); }); } /** * set dash icon size * * @param {number} size in pixels * see DASH_ICON_SIZES for available sizes * * @returns {void} */ dashIconSizeSet(size) { this.dashIconSizeSetDefault(); if (!DASH_ICON_SIZES.includes(size)) { return; } let classnameStarter = this.#getAPIClassname('dash-icon-size'); this.UIStyleClassAdd(classnameStarter + size); } /** * change ControlsManagerLayout._computeWorkspacesBoxForState * base on the current state * * @returns {void} */ #computeWorkspacesBoxForStateChanged() { let controlsLayout = this._main.overview._overview._controls.layout_manager; if (!this.#originals['computeWorkspacesBoxForState']) { this.#originals['computeWorkspacesBoxForState'] = controlsLayout._computeWorkspacesBoxForState; } controlsLayout._computeWorkspacesBoxForState = (state, box, searchHeight, ...args) => { let inAppGrid = state === this._overviewControls.ControlsState.APP_GRID; if (inAppGrid && !this._searchEntryVisibility) { // We need some spacing on top of workspace box in app grid // when the search entry is not visible. searchHeight = 40; } box = this.#originals['computeWorkspacesBoxForState'].call( controlsLayout, state, box, searchHeight, ...args); if (inAppGrid && this._workspacesInAppGridHeight !== undefined) { box.set_size( box.get_width(), this._workspacesInAppGridHeight ); } return box; }; // Since workspace background has shadow around it, it can cause // unwanted shadows in app grid when the workspace height is 0. // so we are removing the shadow when we are in app grid if (!this._appButtonForComputeWorkspacesSignal) { this._appButtonForComputeWorkspacesSignal = this._main.overview.dash.showAppsButton.connect( 'notify::checked', () => { let checked = this._main.overview.dash.showAppsButton.checked; let classname = this.#getAPIClassname('no-workspaces-in-app-grid'); if (checked) { this.UIStyleClassAdd(classname); } else { this.UIStyleClassRemove(classname); } } ); } } /** * change ControlsManagerLayout._computeWorkspacesBoxForState to its default * * @returns {void} */ #computeWorkspacesBoxForStateSetDefault() { if (!this.#originals['computeWorkspacesBoxForState']) { return; } let controlsLayout = this._main.overview._overview._controls.layout_manager; controlsLayout._computeWorkspacesBoxForState = this.#originals['computeWorkspacesBoxForState']; if (this._appButtonForComputeWorkspacesSignal) { let showAppsButton = this._main.overview.dash.showAppsButton; showAppsButton.disconnect(this._appButtonForComputeWorkspacesSignal); delete(this._appButtonForComputeWorkspacesSignal); this.UIStyleClassRemove(this.#getAPIClassname('no-workspaces-in-app-grid')); } } /** * disable workspaces in app grid * * @returns {void} */ workspacesInAppGridDisable() { this._workspacesInAppGridHeight = 0; this.#computeWorkspacesBoxForStateChanged(); } /** * enable workspaces in app grid * * @returns {void} */ workspacesInAppGridEnable() { if (this._workspacesInAppGridHeight === undefined) { return; } delete(this._workspacesInAppGridHeight); this.#computeWorkspacesBoxForStateChanged(); } /** * change notification banner position * * @param {number} pos * see XY_POSITION for available positions * * @returns {void} */ notificationBannerPositionSet(pos) { let messageTray = this._main.messageTray; let bannerBin = messageTray._bannerBin; if (this.#originals['bannerAlignmentX'] === undefined) { this.#originals['bannerAlignmentX'] = messageTray.bannerAlignment; } if (this.#originals['bannerAlignmentY'] === undefined) { this.#originals['bannerAlignmentY'] = bannerBin.get_y_align(); } if (this.#originals['hideNotification'] === undefined) { this.#originals['hideNotification'] = messageTray._hideNotification; } // TOP messageTray._hideNotification = this.#originals['hideNotification']; bannerBin.set_y_align(this._clutter.ActorAlign.START); if (pos === XY_POSITION.TOP_START) { messageTray.bannerAlignment = this._clutter.ActorAlign.START; return; } if (pos === XY_POSITION.TOP_END) { messageTray.bannerAlignment = this._clutter.ActorAlign.END; return; } if (pos === XY_POSITION.TOP_CENTER) { messageTray.bannerAlignment = this._clutter.ActorAlign.CENTER; return; } // BOTTOM // >> // This block is going to fix the animation when the notification is // in bottom area // this is the same function from (ui.messageTray.messageTray._hideNotification) // with clutter animation mode set to EASE. // because the EASE_OUT_BACK (original code) causes glitch when // the tray is on bottom const State = this._messageTray.State; const ANIMATION_TIME = this._messageTray.ANIMATION_TIME; const Clutter = this._clutter; messageTray._hideNotification = function (animate) { this._notificationFocusGrabber.ungrabFocus(); this._banner.disconnectObject(this); this._resetNotificationLeftTimeout(); this._bannerBin.remove_all_transitions(); if (animate) { this._notificationState = State.HIDING; this._bannerBin.ease({ opacity: 0, duration: ANIMATION_TIME, mode: Clutter.AnimationMode.EASE, }); this._bannerBin.ease({ opacity: 0, y: this._bannerBin.height, duration: ANIMATION_TIME, mode: Clutter.AnimationMode.EASE, onComplete: () => { this._notificationState = State.HIDDEN; this._hideNotificationCompleted(); this._updateState(); }, }); } else { this._bannerBin.y = this._bannerBin.height; this._bannerBin.opacity = 0; this._notificationState = State.HIDDEN; this._hideNotificationCompleted(); } } // << bannerBin.set_y_align(this._clutter.ActorAlign.END); if (pos === XY_POSITION.BOTTOM_START) { messageTray.bannerAlignment = this._clutter.ActorAlign.START; return; } if (pos === XY_POSITION.BOTTOM_END) { messageTray.bannerAlignment = this._clutter.ActorAlign.END; return; } if (pos === XY_POSITION.BOTTOM_CENTER) { messageTray.bannerAlignment = this._clutter.ActorAlign.CENTER; return; } } /** * set notification banner position to default position * * @returns {void} */ notificationBannerPositionSetDefault() { if (this.#originals['bannerAlignmentX'] === undefined || this.#originals['bannerAlignmentY'] === undefined || this.#originals['hideNotification'] === undefined ) { return; } let messageTray = this._main.messageTray; let bannerBin = messageTray._bannerBin; messageTray.bannerAlignment = this.#originals['bannerAlignmentX']; bannerBin.set_y_align(this.#originals['bannerAlignmentY']); messageTray._hideNotification = this.#originals['hideNotification']; } /** * set the workspace switcher to always/never show * * @param {boolean} show true for always show, false for never show * @param {boolean} fake true means set the current should show status * * @returns {void} */ workspaceSwitcherShouldShow(shouldShow = true, fake = false) { if (!fake) { this._shouldShow = shouldShow; } if (!this.isWorkspaceSwitcherVisible()) { return; } let ThumbnailsBoxProto = this._workspaceThumbnail.ThumbnailsBox.prototype; if (!this.#originals['updateShouldShow']) { this.#originals['updateShouldShow'] = ThumbnailsBoxProto._updateShouldShow; } ThumbnailsBoxProto._updateShouldShow = function () { if (this._shouldShow === shouldShow) { return; } this._shouldShow = shouldShow; this.notify('should-show'); }; } /** * set the always show workspace switcher status to last real status * * @returns {void} */ #workspaceSwitcherShouldShowSetToLast() { if (this._shouldShow === undefined) { this.workspaceSwitcherShouldShowSetDefault(); return; } this.workspaceSwitcherShouldShow(this._shouldShow); } /** * set the always show workspace switcher status to default * * @returns {void} */ workspaceSwitcherShouldShowSetDefault() { if (!this.#originals['updateShouldShow'] || !this.isWorkspaceSwitcherVisible()) { return; } let ThumbnailsBoxProto = this._workspaceThumbnail.ThumbnailsBox.prototype; ThumbnailsBoxProto._updateShouldShow = this.#originals['updateShouldShow']; delete(this.#originals['updateShouldShow']); delete(this._shouldShow); } /** * set panel button hpadding to default * * @returns {void} */ panelButtonHpaddingSetDefault() { if (this._panelButtonHpaddingSize === undefined) { return; } let classnameStarter = this.#getAPIClassname('panel-button-padding-size'); this.UIStyleClassRemove(classnameStarter + this._panelButtonHpaddingSize); this.#emitRefreshStyles(); delete this._panelButtonHpaddingSize; } /** * set panel button hpadding size * * @param {number} size in pixels (0 - 60) * * @returns {void} */ panelButtonHpaddingSizeSet(size) { this.panelButtonHpaddingSetDefault(); if (size < 0 || size > 60) { return; } this._panelButtonHpaddingSize = size; let classnameStarter = this.#getAPIClassname('panel-button-padding-size'); this.UIStyleClassAdd(classnameStarter + size); this.#emitRefreshStyles(); } /** * set panel indicator padding to default * * @returns {void} */ panelIndicatorPaddingSetDefault() { if (this._panelIndicatorPaddingSize === undefined) { return; } let classnameStarter = this.#getAPIClassname('panel-indicator-padding-size'); this.UIStyleClassRemove(classnameStarter + this._panelIndicatorPaddingSize); this.#emitRefreshStyles(); delete this._panelIndicatorPaddingSize; } /** * set panel indicator padding size * * @param {number} size in pixels (0 - 60) * * @returns {void} */ panelIndicatorPaddingSizeSet(size) { this.panelIndicatorPaddingSetDefault(); if (size < 0 || size > 60) { return; } this._panelIndicatorPaddingSize = size; let classnameStarter = this.#getAPIClassname('panel-indicator-padding-size'); this.UIStyleClassAdd(classnameStarter + size); this.#emitRefreshStyles(); } /** * get window preview prototype * * @returns {Object} */ #windowPreviewGetPrototype() { return this._windowPreview.WindowPreview.prototype; } /** * enable window preview caption * * @returns {void} */ windowPreviewCaptionEnable() { if (!this.#originals['windowPreviewGetCaption']) { return; } let windowPreviewProto = this.#windowPreviewGetPrototype(); windowPreviewProto._getCaption = this.#originals['windowPreviewGetCaption']; this.UIStyleClassRemove(this.#getAPIClassname('no-window-caption')); } /** * disable window preview caption * * @returns {void} */ windowPreviewCaptionDisable() { let windowPreviewProto = this.#windowPreviewGetPrototype(); if (!this.#originals['windowPreviewGetCaption']) { this.#originals['windowPreviewGetCaption'] = windowPreviewProto._getCaption; } windowPreviewProto._getCaption = () => { return ''; }; this.UIStyleClassAdd(this.#getAPIClassname('no-window-caption')); } /** * set workspace background border radius to default size * * @returns {void} */ workspaceBackgroundRadiusSetDefault() { if (this._workspaceBackgroundRadiusSize === undefined) { return; } let workspaceBackgroundProto = this._workspace.WorkspaceBackground.prototype; workspaceBackgroundProto._updateBorderRadius = this.#originals['workspaceBackgroundUpdateBorderRadius']; let classnameStarter = this.#getAPIClassname('workspace-background-radius-size'); this.UIStyleClassRemove(classnameStarter + this._workspaceBackgroundRadiusSize); delete this._workspaceBackgroundRadiusSize; } /** * set workspace background border radius size * * @param {number} size in pixels (0 - 60) * * @returns {void} */ workspaceBackgroundRadiusSet(size) { if (size < 0 || size > 60) { return; } this.workspaceBackgroundRadiusSetDefault(); let workspaceBackgroundProto = this._workspace.WorkspaceBackground.prototype; if (!this.#originals['workspaceBackgroundUpdateBorderRadius']) { this.#originals['workspaceBackgroundUpdateBorderRadius'] = workspaceBackgroundProto._updateBorderRadius; } const Util = this._util; const St = this._st; workspaceBackgroundProto._updateBorderRadius = function () { const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); const cornerRadius = scaleFactor * size; const backgroundContent = this._bgManager.backgroundActor.content; backgroundContent.rounded_clip_radius = Util.lerp(0, cornerRadius, this._stateAdjustment.value); } this._workspaceBackgroundRadiusSize = size; let classnameStarter = this.#getAPIClassname('workspace-background-radius-size'); this.UIStyleClassAdd(classnameStarter + size); } /** * enable workspace wraparound * * @returns {void} */ workspaceWraparoundEnable() { let metaWorkspaceProto = this._meta.Workspace.prototype; if (!this.#originals['metaWorkspaceGetNeighbor']) { this.#originals['metaWorkspaceGetNeighbor'] = metaWorkspaceProto.get_neighbor; } const Meta = this._meta; metaWorkspaceProto.get_neighbor = function (dir) { let index = this.index(); let lastIndex = global.workspace_manager.n_workspaces - 1; let neighborIndex; if (dir === Meta.MotionDirection.UP || dir === Meta.MotionDirection.LEFT) { // prev neighborIndex = (index > 0) ? index - 1 : lastIndex; } else { // next neighborIndex = (index < lastIndex) ? index + 1 : 0; } return global.workspace_manager.get_workspace_by_index(neighborIndex); }; } /** * disable workspace wraparound * * @returns {void} */ workspaceWraparoundDisable() { if (!this.#originals['metaWorkspaceGetNeighbor']) { return; } let metaWorkspaceProto = this._meta.Workspace.prototype; metaWorkspaceProto.get_neighbor = this.#originals['metaWorkspaceGetNeighbor']; } /** * enable window preview close button * * @returns {void} */ windowPreviewCloseButtonEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-window-close')); } /** * disable window preview close button * * @returns {void} */ windowPreviewCloseButtonDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-window-close')); } /** * enable ripple box * * @returns {void} */ rippleBoxEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-ripple-box')); } /** * disable ripple box * * @returns {void} */ rippleBoxDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-ripple-box')); } /** * unblock overlay key * * @returns {void} */ unblockOverlayKey() { if (!this._overlayKeyOldSignalId) { return; } this._gobject.signal_handler_unblock( global.display, this._overlayKeyOldSignalId ); delete(this._overlayKeyOldSignalId); } /** * block overlay key * * @returns {void} */ blockOverlayKey() { this._overlayKeyOldSignalId = this.#getSignalId(global.display, 'overlay-key'); if (!this._overlayKeyOldSignalId) { return; } this._gobject.signal_handler_block(global.display, this._overlayKeyOldSignalId); } /** * enable double super press to toggle app grid * * @returns {void} */ doubleSuperToAppGridEnable() { if (this._isDoubleSuperToAppGrid === true) { return; } if (!this._overlayKeyNewSignalId) { return; } global.display.disconnect(this._overlayKeyNewSignalId); delete(this._overlayKeyNewSignalId); this.unblockOverlayKey(); this._isDoubleSuperToAppGrid = true; } /** * disable double super press to toggle app grid * * @returns {void} */ doubleSuperToAppGridDisable() { if (this._isDoubleSuperToAppGrid === false) { return; } this.blockOverlayKey(); this._overlayKeyNewSignalId = global.display.connect('overlay-key', () => { this._main.overview.toggle(); }); this._isDoubleSuperToAppGrid = false; } /** * disable the removal of switcher popup delay * * @returns {void} */ switcherPopupDelaySetDefault() { let SwitcherPopupProto = this._switcherPopup.SwitcherPopup.prototype; if (!SwitcherPopupProto.showOld) { return; } SwitcherPopupProto.show = SwitcherPopupProto.showOld; delete(SwitcherPopupProto.showOld); } /** * enable the removal of switcher popup delay * * @returns {void} */ removeSwitcherPopupDelay() { let SwitcherPopupProto = this._switcherPopup.SwitcherPopup.prototype; SwitcherPopupProto.showOld = SwitcherPopupProto.show; SwitcherPopupProto.show = function (...args) { let res = this.showOld(...args); if (res) { this._showImmediately(); } return res; }; } /** * set default OSD position * * @returns {void} */ osdPositionSetDefault() { if (!this.#originals['osdWindowShow']) { return; } let osdWindowProto = this._osdWindow.OsdWindow.prototype; osdWindowProto.show = this.#originals['osdWindowShow']; delete(osdWindowProto._oldShow); delete(this.#originals['osdWindowShow']); if ( this.#originals['osdWindowXAlign'] !== undefined && this.#originals['osdWindowYAlign'] !== undefined ) { let osdWindows = this._main.osdWindowManager._osdWindows; osdWindows.forEach(osdWindow => { osdWindow.x_align = this.#originals['osdWindowXAlign']; osdWindow.y_align = this.#originals['osdWindowYAlign']; }); delete(this.#originals['osdWindowXAlign']); delete(this.#originals['osdWindowYAlign']); } this.UIStyleClassRemove(this.#getAPIClassname('osd-position-top')); this.UIStyleClassRemove(this.#getAPIClassname('osd-position-bottom')); this.UIStyleClassRemove(this.#getAPIClassname('osd-position-center')); } /** * set OSD position * * @param int pos position XY_POSITION * * @returns {void} */ osdPositionSet(pos) { let osdWindowProto = this._osdWindow.OsdWindow.prototype; if (!this.#originals['osdWindowShow']) { this.#originals['osdWindowShow'] = osdWindowProto.show; } if ( this.#originals['osdWindowXAlign'] === undefined || this.#originals['osdWindowYAlign'] === undefined ) { let osdWindows = this._main.osdWindowManager._osdWindows; this.#originals['osdWindowXAlign'] = osdWindows[0].x_align; this.#originals['osdWindowYAlign'] = osdWindows[0].y_align; } if (osdWindowProto._oldShow === undefined) { osdWindowProto._oldShow = this.#originals['osdWindowShow']; } let [xAlign, yAlign] = this.#xyAlignGet(pos); osdWindowProto.show = function () { this.x_align = xAlign; this.y_align = yAlign; this._oldShow(); }; if ( pos === XY_POSITION.TOP_START || pos === XY_POSITION.TOP_CENTER || pos === XY_POSITION.TOP_END ) { this.UIStyleClassAdd(this.#getAPIClassname('osd-position-top')); } if ( pos === XY_POSITION.BOTTOM_START || pos === XY_POSITION.BOTTOM_CENTER || pos === XY_POSITION.BOTTOM_END ) { this.UIStyleClassAdd(this.#getAPIClassname('osd-position-bottom')); } if ( pos === XY_POSITION.CENTER_START || pos === XY_POSITION.CENTER_CENTER || pos === XY_POSITION.CENTER_END ) { this.UIStyleClassAdd(this.#getAPIClassname('osd-position-center')); } } /** * show weather in date menu * * @returns {void} */ weatherShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-weather')); } /** * hide weather in date menu * * @returns {void} */ weatherHide() { this.UIStyleClassAdd(this.#getAPIClassname('no-weather')); } /** * show world clocks in date menu * * @returns {void} */ worldClocksShow() { if (!this.#originals['clocksItemSync']) { return; } let clocksItem = this._main.panel.statusArea.dateMenu._clocksItem; clocksItem._sync = this.#originals['clocksItemSync']; delete(this.#originals['clocksItemSync']); if (this._clocksItemShowSignal) { clocksItem.disconnect(this._clocksItemShowSignal); delete(this._clocksItemShowSignal); } clocksItem._sync(); } /** * hide world clocks in date menu * * @returns {void} */ worldClocksHide() { let clocksItem = this._main.panel.statusArea.dateMenu._clocksItem; if (!this.#originals['clocksItemSync']) { this.#originals['clocksItemSync'] = clocksItem._sync; } clocksItem._sync = function () { this.visible = false; }; if (!this._clocksItemShowSignal) { this._clocksItemShowSignal = clocksItem.connect('show', () => { clocksItem._sync(); }); } clocksItem._sync(); } /** * show events button in date menu * * @returns {void} */ eventsButtonShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-events-button')); } /** * hide events button in date menu * * @returns {void} */ eventsButtonHide() { this.UIStyleClassAdd(this.#getAPIClassname('no-events-button')); } /** * show calendar in date menu * * @returns {void} */ calendarShow() { this._main.panel.statusArea.dateMenu._calendar.show(); } /** * hide calendar in date menu * * @returns {void} */ calendarHide() { this._main.panel.statusArea.dateMenu._calendar.hide(); } /** * set default panel icon size * * @returns {void} */ panelIconSetDefaultSize() { if (this._panelIconSize === undefined || !this.#originals['panelIconSize']) { return; } let classnameStarter = this.#getAPIClassname('panel-icon-size'); this.UIStyleClassRemove(classnameStarter + this._panelIconSize); this.#emitRefreshStyles(); let defaultSize = this.#originals['panelIconSize']; this.#changeDateMenuIndicatorIconSize(defaultSize); delete(this._panelIconSize); } /** * set panel icon size * * @param {number} size 1-60 * * @returns {void} */ panelIconSetSize(size) { if (size < 1 || size > 60) { return; } if (!this.#originals['panelIconSize']) { this.#originals['panelIconSize'] = this._panel.PANEL_ICON_SIZE; } let classnameStarter = this.#getAPIClassname('panel-icon-size'); this.UIStyleClassRemove(classnameStarter + this.panelIconGetSize()); this.UIStyleClassAdd(classnameStarter + size); this.#emitRefreshStyles(); this.#changeDateMenuIndicatorIconSize(size); this._panelIconSize = size; } /** * change date menu indicator icon size * * @param {number} size * * @returns {void} */ #changeDateMenuIndicatorIconSize(size) { let dateMenu = this._main.panel.statusArea.dateMenu; // we get set_icon_size is not a function in some setups // in case the date menu has been removed or not created if ( dateMenu && dateMenu._indicator && dateMenu._indicator.set_icon_size ) { dateMenu._indicator.set_icon_size(size); } } /** * get panel icon size * * @returns {void} */ panelIconGetSize() { if (this._panelIconSize !== undefined) { return this._panelIconSize; } return this._panel.PANEL_ICON_SIZE; } /** * show dash separator * * @returns {void} */ dashSeparatorShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-dash-separator')); } /** * hide dash separator * * @returns {void} */ dashSeparatorHide() { this.UIStyleClassAdd(this.#getAPIClassname('no-dash-separator')); } /** * get looking glass size * * @returns {array} * width: int * height: int */ #lookingGlassGetSize() { let lookingGlass = this._main.createLookingGlass(); return [lookingGlass.width, lookingGlass.height]; } /** * set default looking glass size * * @returns {void} */ lookingGlassSetDefaultSize() { if (!this._lookingGlassShowSignal) { return; } this._main.lookingGlass.disconnect(this._lookingGlassShowSignal); delete(this._lookingGlassShowSignal); delete(this._lookingGlassOriginalSize); delete(this._monitorsChangedSignal); } /** * set looking glass size * * @param {number} width in float * @param {number} height in float * * @returns {void} */ lookingGlassSetSize(width, height) { let lookingGlass = this._main.createLookingGlass(); if (!this._lookingGlassOriginalSize) { this._lookingGlassOriginalSize = this.#lookingGlassGetSize(); } if (this._lookingGlassShowSignal) { lookingGlass.disconnect(this._lookingGlassShowSignal); delete(this._lookingGlassShowSignal); } this._lookingGlassShowSignal = lookingGlass.connect('show', () => { let [, currentHeight] = this.#lookingGlassGetSize(); let [originalWidth, originalHeight] = this._lookingGlassOriginalSize; let monitorInfo = this.monitorGetInfo(); let dialogWidth = (width !== null) ? monitorInfo.width * width : originalWidth; let x = monitorInfo.x + (monitorInfo.width - dialogWidth) / 2; lookingGlass.set_x(x); let keyboardHeight = this._main.layoutManager.keyboardBox.height; let availableHeight = monitorInfo.height - keyboardHeight; let dialogHeight = (height !== null) ? Math.min(monitorInfo.height * height, availableHeight * 0.9) : originalHeight; let hiddenY = lookingGlass._hiddenY + currentHeight - dialogHeight; lookingGlass.set_y(hiddenY); lookingGlass._hiddenY = hiddenY; lookingGlass.set_size(dialogWidth, dialogHeight); }); if (!this._monitorsChangedSignal) { this._monitorsChangedSignal = this._main.layoutManager.connect('monitors-changed', () => { this.lookingGlassSetSize(width, height); }); } } /** * show screenshot in window menu * * @returns {void} */ screenshotInWindowMenuShow() { let windowMenuProto = this._windowMenu.WindowMenu.prototype; if (windowMenuProto._oldBuildMenu === undefined) { return; } windowMenuProto._buildMenu = this.#originals['WindowMenubuildMenu']; delete(windowMenuProto._oldBuildMenu); } /** * hide screenshot in window menu * * @returns {void} */ screenshotInWindowMenuHide() { let windowMenuProto = this._windowMenu.WindowMenu.prototype; if (!this.#originals['WindowMenubuildMenu']) { this.#originals['WindowMenubuildMenu'] = windowMenuProto._buildMenu; } if (windowMenuProto._oldBuildMenu === undefined) { windowMenuProto._oldBuildMenu = this.#originals['WindowMenubuildMenu']; } windowMenuProto._buildMenu = function (window) { this._oldBuildMenu(window); this.firstMenuItem.hide(); }; } /** * set all alt tab sizes to default * * @returns {void} */ #altTabSizesSetDefault() { let WindowIconProto = this._altTab.WindowIcon.prototype; if (WindowIconProto._initOld) { WindowIconProto._init = WindowIconProto._initOld; delete(WindowIconProto._initOld); } delete(this._altTabAPP_ICON_SIZE); delete(this._altTabAPP_ICON_SIZE_SMALL); delete(this._altTabWINDOW_PREVIEW_SIZE); } /** * set alt tab sizes * * @param {number|null} appIconSize * @param {number|null} appIconSizeSmall * @param {number|null} windowPreviewSize * * @returns {void} */ #altTabSizesSet(appIconSize, appIconSizeSmall, windowPreviewSize) { let WindowIconProto = this._altTab.WindowIcon.prototype; if (!WindowIconProto._initOld) { WindowIconProto._initOld = WindowIconProto._init; } this._altTabAPP_ICON_SIZE ||= this._altTab.APP_ICON_SIZE; this._altTabAPP_ICON_SIZE_SMALL ||= this._altTab.APP_ICON_SIZE_SMALL; this._altTabWINDOW_PREVIEW_SIZE ||= this._altTab.WINDOW_PREVIEW_SIZE; const APP_ICON_SIZE = appIconSize || this._altTabAPP_ICON_SIZE; const APP_ICON_SIZE_SMALL = appIconSizeSmall || this._altTabAPP_ICON_SIZE_SMALL; const WINDOW_PREVIEW_SIZE = windowPreviewSize || this._altTabWINDOW_PREVIEW_SIZE; WindowIconProto._init = function(window, mode) { this._initOld(window, mode); } } /** * set default alt tab window preview size * * @returns {void} */ altTabWindowPreviewSetDefaultSize() { if (!this.#originals['altTabWindowPreviewSize']) { return; } this.#altTabSizesSet(null, null, this.#originals['altTabWindowPreviewSize']); } /** * set alt tab window preview size * * @param {number} size 1-512 * * @returns {void} */ altTabWindowPreviewSetSize(size) { if (size < 1 || size > 512) { return; } if (!this.#originals['altTabWindowPreviewSize']) { this.#originals['altTabWindowPreviewSize'] = this._altTab.WINDOW_PREVIEW_SIZE; } this.#altTabSizesSet(null, null, size); } /** * set default alt tab small icon size * * @returns {void} */ altTabSmallIconSetDefaultSize() { if (!this.#originals['altTabAppIconSizeSmall']) { return; } this.#altTabSizesSet(null, this.#originals['altTabAppIconSizeSmall'], null); } /** * set alt tab small icon size * * @param {number} size 1-512 * * @returns {void} */ altTabSmallIconSetSize(size) { if (size < 1 || size > 512) { return; } if (!this.#originals['altTabAppIconSizeSmall']) { this.#originals['altTabAppIconSizeSmall'] = this._altTab.APP_ICON_SIZE_SMALL; } this.#altTabSizesSet(null, size, null); } /** * set default alt tab icon size * * @returns {void} */ altTabIconSetDefaultSize() { if (!this.#originals['altTabAppIconSize']) { return; } this.#altTabSizesSet(this.#originals['altTabAppIconSize'], null, null); } /** * set alt tab icon size * * @param {number} size 1-512 * * @returns {void} */ altTabIconSetSize(size) { if (size < 1 || size > 512) { return; } if (!this.#originals['altTabAppIconSize']) { this.#originals['altTabAppIconSize'] = this._altTab.APP_ICON_SIZE; } this.#altTabSizesSet(size, null, null); } /** * enable screen sharing indicator * * @returns {void} */ screenSharingIndicatorEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-screen-sharing-indicator')); } /** * disable screen sharing indicator * * @returns {void} */ screenSharingIndicatorDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-screen-sharing-indicator')); } /** * enable screen recording indicator * * @returns {void} */ screenRecordingIndicatorEnable() { this.UIStyleClassRemove(this.#getAPIClassname('no-screen-recording-indicator')); } /** * disable screen recording indicator * * @returns {void} */ screenRecordingIndicatorDisable() { this.UIStyleClassAdd(this.#getAPIClassname('no-screen-recording-indicator')); } /** * set controls manager spacing to default * * @returns {void} */ controlsManagerSpacingSetDefault() { if (this._controlsManagerSpacingSize === undefined) { return; } let classnameStarter = this.#getAPIClassname('controls-manager-spacing-size'); this.UIStyleClassRemove(classnameStarter + this._controlsManagerSpacingSize); delete this._controlsManagerSpacingSize; } /** * set controls manager spacing size * * @param {number} size in pixels (0 - 150) * * @returns {void} */ controlsManagerSpacingSizeSet(size) { this.controlsManagerSpacingSetDefault(); if (size < 0 || size > 150) { return; } this._controlsManagerSpacingSize = size; let classnameStarter = this.#getAPIClassname('controls-manager-spacing-size'); this.UIStyleClassAdd(classnameStarter + size); } /** * set workspaces view spacing to default * * @returns {void} */ workspacesViewSpacingSetDefault() { let wsvp = this._workspacesView.WorkspacesView.prototype; if (wsvp._getSpacingOld === undefined) { return; } wsvp._getSpacing = wsvp._getSpacingOld; delete wsvp._getSpacingOld; } /** * set workspaces view spacing size * * @param {number} size in pixels (0 - 500) * * @returns {void} */ workspacesViewSpacingSizeSet(size) { if (size < 0 || size > 500) { return; } let wsvp = this._workspacesView.WorkspacesView.prototype; if (wsvp._getSpacingOld === undefined) { wsvp._getSpacingOld = wsvp._getSpacing; } wsvp._getSpacing = function (box, fitMode, vertical) { if (fitMode === 0) { return size; } return this._getSpacingOld(box, fitMode, vertical); }; } /** * show dash app running dot * * @returns {void} */ dashAppRunningDotShow() { this.UIStyleClassRemove(this.#getAPIClassname('no-dash-app-running-dot')); } /** * hide dash app running dot * * @returns {void} */ dashAppRunningDotHide() { this.UIStyleClassAdd(this.#getAPIClassname('no-dash-app-running-dot')); } /** * show dark style toggle button in quick settings * * @returns {void} */ quickSettingsDarkStyleToggleShow() { this.#onQuickSettingsPropertyCall('_darkMode', (darkMode) => { darkMode.quickSettingsItems[0].show(); }); } /** * hide dark style toggle button in quick settings * * @returns {void} */ quickSettingsDarkStyleToggleHide() { this.#onQuickSettingsPropertyCall('_darkMode', (darkMode) => { darkMode.quickSettingsItems[0].hide(); }); } /** * set workspaces view spacing size * * @param {string} propertyName * @param {Function} func function to call when the property is available * * @returns {void} */ #onQuickSettingsPropertyCall(propertyName, func) { const quickSettings = this._main.panel.statusArea.quickSettings; this._glib.idle_add(this._glib.PRIORITY_DEFAULT_IDLE, () => { if (!quickSettings[propertyName]) { return this._glib.SOURCE_CONTINUE; } func(quickSettings[propertyName]); return this._glib.SOURCE_REMOVE; }); } }