1
0
Fork 0
gnome-shell-extensions-extra/extensions/vertical-workspaces/lib/dash.js
Daniel Baumann f8854b5650
Adding upstream version 20230618.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 23:10:52 +01:00

1186 lines
43 KiB
JavaScript

/**
* V-Shell (Vertical Workspaces)
* dash.js
*
* @author GdH <G-dH@github.com>
* @copyright 2022-2023
* @license GPL-3.0
* modified dash module of https://github.com/RensAlthuis/vertical-overview extension
*/
const { Clutter, GObject, St, Shell, Meta } = imports.gi;
const AppDisplay = imports.ui.appDisplay;
const AppFavorites = imports.ui.appFavorites;
const DND = imports.ui.dnd;
const IconGrid = imports.ui.iconGrid;
const Main = imports.ui.main;
const Overview = imports.ui.overview;
const Dash = imports.ui.dash;
const PopupMenu = imports.ui.popupMenu;
const { AppMenu } = imports.ui.appMenu;
const BoxPointer = imports.ui.boxpointer;
const AltTab = imports.ui.altTab;
const Me = imports.misc.extensionUtils.getCurrentExtension();
const Util = Me.imports.lib.util;
const _ = Me.imports.lib.settings._;
const shellVersion = Util.shellVersion;
let _origWorkId;
let _newWorkId;
let _showAppsIconBtnPressId;
// added values to achieve a better ability to scale down according to available space
var BaseIconSizes = [16, 24, 32, 40, 44, 48, 56, 64, 72, 80, 96, 112, 128];
const RecentFilesSearchProviderPrefix = Me.imports.lib.recentFilesSearchProvider.prefix;
const WindowSearchProviderPrefix = Me.imports.lib.windowSearchProvider.prefix;
let _overrides;
let opt;
let _firstRun = true;
const DASH_ITEM_LABEL_SHOW_TIME = 150;
function update(reset = false) {
opt = Me.imports.lib.settings.opt;
const moduleEnabled = opt.get('dashModule', true);
reset = reset || !moduleEnabled;
// don't even touch this module if disabled
if (_firstRun && reset)
return;
_firstRun = false;
if (_overrides)
_overrides.removeAll();
const dash = Main.overview._overview._controls.layoutManager._dash;
setToHorizontal();
dash.remove_style_class_name('vertical');
dash.remove_style_class_name('vertical-left');
dash.remove_style_class_name('vertical-right');
if (reset) {
_moveDashAppGridIcon(reset);
_connectShowAppsIcon(reset);
_updateSearchWindowsIcon(false);
_updateRecentFilesIcon(false);
dash.visible = true;
dash._background.opacity = 255;
dash._background.remove_style_class_name('v-shell-dash-background');
_overrides = null;
opt = null;
return;
}
_overrides = new Util.Overrides();
_overrides.addOverride('DashItemContainer', Dash.DashItemContainer.prototype, DashItemContainerCommon);
_overrides.addOverride('DashCommon', Dash.Dash.prototype, DashCommon);
_overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIconCommon);
_overrides.addOverride('DashIcon', Dash.DashIcon.prototype, DashIconCommon);
if (opt.DASH_VERTICAL) {
_overrides.addOverride('Dash', Dash.Dash.prototype, DashOverride);
setToVertical();
dash.add_style_class_name('vertical');
if (!_newWorkId) {
_origWorkId = dash._workId;
dash._workId = Main.initializeDeferredWork(dash._box, dash._redisplay.bind(dash));
_newWorkId = dash._workId;
} else {
dash._workId = _newWorkId;
}
} else {
setToHorizontal();
if (_origWorkId)
dash._workId = _origWorkId;
}
_updateSearchWindowsIcon();
_updateRecentFilesIcon();
_moveDashAppGridIcon();
_connectShowAppsIcon();
if (dash._showWindowsIcon && !dash._showWindowsIconClickedId)
dash._showWindowsIconClickedId = dash._showWindowsIcon.toggleButton.connect('clicked', (a, c) => c && Util.activateSearchProvider(WindowSearchProviderPrefix));
if (dash._recentFilesIcon && !dash._recentFilesIconClickedId)
dash._recentFilesIconClickedId = dash._recentFilesIcon.toggleButton.connect('clicked', (a, c) => c && Util.activateSearchProvider(RecentFilesSearchProviderPrefix));
dash.visible = opt.DASH_VISIBLE;
dash._background.add_style_class_name('v-shell-dash-background');
dash._redisplay();
}
function setToVertical() {
let dash = Main.overview._overview._controls.layoutManager._dash;
dash._box.layout_manager.orientation = Clutter.Orientation.VERTICAL;
dash._dashContainer.layout_manager.orientation = Clutter.Orientation.VERTICAL;
dash._dashContainer.y_expand = false;
dash._dashContainer.x_expand = true;
dash.x_align = Clutter.ActorAlign.START;
dash.y_align = Clutter.ActorAlign.CENTER;
let sizerBox = dash._background.get_children()[0];
sizerBox.clear_constraints();
sizerBox.add_constraint(new Clutter.BindConstraint({
source: dash._showAppsIcon.icon,
coordinate: Clutter.BindCoordinate.WIDTH,
}));
sizerBox.add_constraint(new Clutter.BindConstraint({
source: dash._dashContainer,
coordinate: Clutter.BindCoordinate.HEIGHT,
}));
dash._box.remove_all_children();
dash._separator = null;
dash._queueRedisplay();
dash._adjustIconSize();
dash.add_style_class_name(opt.DASH_LEFT ? 'vertical-left' : 'vertical-right');
}
function setToHorizontal() {
let dash = Main.overview._overview._controls.layoutManager._dash;
if (_origWorkId)
dash._workId = _origWorkId; // pretty sure this is a leak, but there no provided way to disconnect these...
dash._box.layout_manager.orientation = Clutter.Orientation.HORIZONTAL;
dash._dashContainer.layout_manager.orientation = Clutter.Orientation.HORIZONTAL;
dash._dashContainer.y_expand = true;
dash._dashContainer.x_expand = false;
dash.x_align = Clutter.ActorAlign.CENTER;
dash.y_align = 0;
let sizerBox = dash._background.get_children()[0];
sizerBox.clear_constraints();
sizerBox.add_constraint(new Clutter.BindConstraint({
source: dash._showAppsIcon.icon,
coordinate: Clutter.BindCoordinate.HEIGHT,
}));
sizerBox.add_constraint(new Clutter.BindConstraint({
source: dash._dashContainer,
coordinate: Clutter.BindCoordinate.WIDTH,
}));
dash._box.remove_all_children();
dash._separator = null;
dash._queueRedisplay();
dash._adjustIconSize();
}
function _moveDashAppGridIcon(reset = false) {
// move dash app grid icon to the front
const dash = Main.overview._overview._controls.layoutManager._dash;
const appIconPosition = opt.get('showAppsIconPosition', true);
dash._showAppsIcon.remove_style_class_name('show-apps-icon-vertical-hide');
dash._showAppsIcon.remove_style_class_name('show-apps-icon-horizontal-hide');
dash._showAppsIcon.opacity = 255;
if (!reset && appIconPosition === 0) // 0 - start
dash._dashContainer.set_child_at_index(dash._showAppsIcon, 0);
if (reset || appIconPosition === 1) { // 1 - end
const index = dash._dashContainer.get_children().length - 1;
dash._dashContainer.set_child_at_index(dash._showAppsIcon, index);
}
if (!reset && appIconPosition === 2) { // 2 - hide
const style = opt.DASH_VERTICAL ? 'show-apps-icon-vertical-hide' : 'show-apps-icon-horizontal-hide';
dash._showAppsIcon.add_style_class_name(style);
// for some reason even if the icon height in vertical mode should be set to 0 by the style, it stays visible in full size returning height 1px
dash._showAppsIcon.opacity = 0;
}
}
function _connectShowAppsIcon(reset = false) {
if (!reset) {
if (_showAppsIconBtnPressId || Util.dashIsDashToDock()) {
// button is already connected || dash is Dash to Dock
return;
}
Main.overview.dash._showAppsIcon.reactive = true;
_showAppsIconBtnPressId = Main.overview.dash._showAppsIcon.connect('button-press-event', (actor, event) => {
const button = event.get_button();
if (button === Clutter.BUTTON_MIDDLE)
Util.openPreferences();
else if (button === Clutter.BUTTON_SECONDARY)
Util.activateSearchProvider(WindowSearchProviderPrefix);
else
return Clutter.EVENT_PROPAGATE;
return Clutter.EVENT_STOP;
});
} else if (_showAppsIconBtnPressId) {
Main.overview.dash._showAppsIcon.disconnect(_showAppsIconBtnPressId);
_showAppsIconBtnPressId = 0;
Main.overview.dash._showAppsIcon.reactive = false;
}
}
const DashOverride = {
handleDragOver(source, actor, _x, y, _time) {
let app = Dash.getAppFromSource(source);
// Don't allow favoriting of transient apps
if (app === null || app.is_window_backed())
return DND.DragMotionResult.NO_DROP;
if (!global.settings.is_writable('favorite-apps'))
return DND.DragMotionResult.NO_DROP;
let favorites = AppFavorites.getAppFavorites().getFavorites();
let numFavorites = favorites.length;
let favPos = favorites.indexOf(app);
let children = this._box.get_children();
let numChildren = children.length;
let boxHeight = this._box.height;
// Keep the placeholder out of the index calculation; assuming that
// the remove target has the same size as "normal" items, we don't
// need to do the same adjustment there.
if (this._dragPlaceholder) {
boxHeight -= this._dragPlaceholder.height;
numChildren--;
}
// Same with the separator
if (this._separator) {
boxHeight -= this._separator.height;
numChildren--;
}
let pos;
if (!this._emptyDropTarget)
pos = Math.floor(y * numChildren / boxHeight);
else
pos = 0; // always insert at the top when dash is empty
// Put the placeholder after the last favorite if we are not
// in the favorites zone
if (pos > numFavorites)
pos = numFavorites;
if (pos !== this._dragPlaceholderPos && this._animatingPlaceholdersCount === 0) {
this._dragPlaceholderPos = pos;
// Don't allow positioning before or after self
if (favPos !== -1 && (pos === favPos || pos === favPos + 1)) {
this._clearDragPlaceholder();
return DND.DragMotionResult.CONTINUE;
}
// If the placeholder already exists, we just move
// it, but if we are adding it, expand its size in
// an animation
let fadeIn;
if (this._dragPlaceholder) {
this._dragPlaceholder.destroy();
fadeIn = false;
} else {
fadeIn = true;
}
this._dragPlaceholder = new Dash.DragPlaceholderItem();
this._dragPlaceholder.child.set_width(this.iconSize / 2);
this._dragPlaceholder.child.set_height(this.iconSize);
this._box.insert_child_at_index(this._dragPlaceholder,
this._dragPlaceholderPos);
this._dragPlaceholder.show(fadeIn);
}
if (!this._dragPlaceholder)
return DND.DragMotionResult.NO_DROP;
let srcIsFavorite = favPos !== -1;
if (srcIsFavorite)
return DND.DragMotionResult.MOVE_DROP;
return DND.DragMotionResult.COPY_DROP;
},
_redisplay() {
let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
let running = this._appSystem.get_running();
let children = this._box.get_children().filter(actor => {
return actor.child &&
actor.child._delegate &&
actor.child._delegate.app;
});
// Apps currently in the dash
let oldApps = children.map(actor => actor.child._delegate.app);
// Apps supposed to be in the dash
let newApps = [];
for (let id in favorites)
newApps.push(favorites[id]);
for (let i = 0; i < running.length; i++) {
let app = running[i];
if (app.get_id() in favorites)
continue;
newApps.push(app);
}
// Figure out the actual changes to the list of items; we iterate
// over both the list of items currently in the dash and the list
// of items expected there, and collect additions and removals.
// Moves are both an addition and a removal, where the order of
// the operations depends on whether we encounter the position
// where the item has been added first or the one from where it
// was removed.
// There is an assumption that only one item is moved at a given
// time; when moving several items at once, everything will still
// end up at the right position, but there might be additional
// additions/removals (e.g. it might remove all the launchers
// and add them back in the new order even if a smaller set of
// additions and removals is possible).
// If above assumptions turns out to be a problem, we might need
// to use a more sophisticated algorithm, e.g. Longest Common
// Subsequence as used by diff.
let addedItems = [];
let removedActors = [];
let newIndex = 0;
let oldIndex = 0;
while (newIndex < newApps.length || oldIndex < oldApps.length) {
let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null;
let newApp = newApps.length > newIndex ? newApps[newIndex] : null;
// No change at oldIndex/newIndex
if (oldApp === newApp) {
oldIndex++;
newIndex++;
continue;
}
// App removed at oldIndex
if (oldApp && !newApps.includes(oldApp)) {
removedActors.push(children[oldIndex]);
oldIndex++;
continue;
}
// App added at newIndex
if (newApp && !oldApps.includes(newApp)) {
addedItems.push({
app: newApp,
item: this._createAppItem(newApp),
pos: newIndex,
});
newIndex++;
continue;
}
// App moved
let nextApp = newApps.length > newIndex + 1
? newApps[newIndex + 1] : null;
let insertHere = nextApp && nextApp === oldApp;
let alreadyRemoved = removedActors.reduce((result, actor) => {
let removedApp = actor.child._delegate.app;
return result || removedApp === newApp;
}, false);
if (insertHere || alreadyRemoved) {
let newItem = this._createAppItem(newApp);
addedItems.push({
app: newApp,
item: newItem,
pos: newIndex + removedActors.length,
});
newIndex++;
} else {
removedActors.push(children[oldIndex]);
oldIndex++;
}
}
for (let i = 0; i < addedItems.length; i++) {
this._box.insert_child_at_index(addedItems[i].item,
addedItems[i].pos);
}
for (let i = 0; i < removedActors.length; i++) {
let item = removedActors[i];
// Don't animate item removal when the overview is transitioning
// or hidden
if (Main.overview.visible && !Main.overview.animationInProgress)
item.animateOutAndDestroy();
else
item.destroy();
}
this._adjustIconSize();
// Skip animations on first run when adding the initial set
// of items, to avoid all items zooming in at once
let animate = this._shownInitially && Main.overview.visible &&
!Main.overview.animationInProgress;
if (!this._shownInitially)
this._shownInitially = true;
for (let i = 0; i < addedItems.length; i++)
addedItems[i].item.show(animate);
// Update separator
const nFavorites = Object.keys(favorites).length;
const nIcons = children.length + addedItems.length - removedActors.length;
if (nFavorites > 0 && nFavorites < nIcons) {
// destroy the horizontal separator if it exists.
// this is incredibly janky, but I can't think of a better way atm.
if (this._separator && this._separator.height !== 1) {
this._separator.destroy();
this._separator = null;
}
if (!this._separator) {
this._separator = new St.Widget({
style_class: 'dash-separator',
x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.CENTER,
width: this.iconSize,
height: 1,
});
this._box.add_child(this._separator);
}
// FIXME: separator placement is broken (also in original dash)
let pos = nFavorites;
if (this._dragPlaceholder)
pos++;
this._box.set_child_at_index(this._separator, pos);
} else if (this._separator) {
this._separator.destroy();
this._separator = null;
}
// Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744
// Without it, StBoxLayout may use a stale size cache
this._box.queue_relayout();
},
_createAppItem(app) {
let appIcon = new Dash.DashIcon(app);
let indicator = appIcon._dot;
indicator.x_align = opt.DASH_LEFT ? Clutter.ActorAlign.START : Clutter.ActorAlign.END;
indicator.y_align = Clutter.ActorAlign.CENTER;
appIcon.connect('menu-state-changed',
(o, opened) => {
this._itemMenuStateChanged(item, opened);
});
let item = new Dash.DashItemContainer();
item.setChild(appIcon);
// Override default AppIcon label_actor, now the
// accessible_name is set at DashItemContainer.setLabelText
appIcon.label_actor = null;
item.setLabelText(app.get_name());
appIcon.icon.setIconSize(this.iconSize);
this._hookUpLabel(item, appIcon);
return item;
},
};
const DashItemContainerCommon = {
// move labels according dash position
showLabel() {
if (!this._labelText)
return;
this.label.set_text(this._labelText);
this.label.opacity = 0;
this.label.show();
let [stageX, stageY] = this.get_transformed_position();
const itemWidth = this.allocation.get_width();
const itemHeight = this.allocation.get_height();
const labelWidth = this.label.get_width();
const labelHeight = this.label.get_height();
let xOffset = Math.floor((itemWidth - labelWidth) / 2);
let x = Math.clamp(stageX + xOffset, 0, global.stage.width - labelWidth);
let node = this.label.get_theme_node();
let y;
if (opt.DASH_TOP) {
const yOffset = itemHeight - labelHeight + 3 * node.get_length('-y-offset');
y = stageY + yOffset;
} else if (opt.DASH_BOTTOM) {
const yOffset = node.get_length('-y-offset');
y = stageY - this.label.height - yOffset;
} else if (opt.DASH_RIGHT) {
const yOffset = Math.floor((itemHeight - labelHeight) / 2);
xOffset = 4;
x = stageX - xOffset - this.label.width;
y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight);
} else if (opt.DASH_LEFT) {
const yOffset = Math.floor((itemHeight - labelHeight) / 2);
xOffset = 4;
x = stageX + this.width + xOffset;
y = Math.clamp(stageY + yOffset, 0, global.stage.height - labelHeight);
}
this.label.set_position(x, y);
this.label.ease({
opacity: 255,
duration: DASH_ITEM_LABEL_SHOW_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
this.label.set_position(x, y);
this.label.ease({
opacity: 255,
duration: DASH_ITEM_LABEL_SHOW_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
},
};
const DashCommon = {
// use custom BaseIconSizes and add support for custom icons
_adjustIconSize() {
// if a user launches multiple apps at once, this function may be called again before the previous call has finished
// as a result, new icons will not reach their full size, or will be missing, if adding a new icon and changing the dash size due to lack of space at the same time
if (this._adjustingInProgress)
return;
// For the icon size, we only consider children which are "proper"
// icons (i.e. ignoring drag placeholders) and which are not
// animating out (which means they will be destroyed at the end of
// the animation)
let iconChildren = this._box.get_children().filter(actor => {
return actor.child &&
actor.child._delegate &&
actor.child._delegate.icon &&
!actor.animatingOut;
});
// add new custom icons to the list
if (this._showAppsIcon.visible)
iconChildren.push(this._showAppsIcon);
if (this._showWindowsIcon)
iconChildren.push(this._showWindowsIcon);
if (this._recentFilesIcon)
iconChildren.push(this._recentFilesIcon);
if (!iconChildren.length)
return;
if (this._maxWidth === -1 || this._maxHeight === -1)
return;
const dashHorizontal = !opt.DASH_VERTICAL;
const themeNode = this.get_theme_node();
const maxAllocation = new Clutter.ActorBox({
x1: 0,
y1: 0,
x2: dashHorizontal ? this._maxWidth : 42, // not whatever
y2: dashHorizontal ? 42 : this._maxHeight,
});
let maxContent = themeNode.get_content_box(maxAllocation);
let spacing = themeNode.get_length('spacing');
let firstButton = iconChildren[0].child;
let firstIcon = firstButton._delegate.icon;
if (!firstIcon.icon)
return;
// Enforce valid spacings during the size request
firstIcon.icon.ensure_style();
const [, , iconWidth, iconHeight] = firstIcon.icon.get_preferred_size();
const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size();
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
let availWidth, availHeight, maxIconSize;
if (dashHorizontal) {
availWidth = maxContent.x2 - maxContent.x1;
// Subtract icon padding and box spacing from the available width
availWidth -= iconChildren.length * (buttonWidth - iconWidth) +
(iconChildren.length - 1) * spacing +
2 * this._background.get_theme_node().get_horizontal_padding();
availHeight = this._maxHeight;
availHeight -= this.margin_top + this.margin_bottom;
availHeight -= this._background.get_theme_node().get_vertical_padding();
availHeight -= themeNode.get_vertical_padding();
availHeight -= buttonHeight - iconHeight;
maxIconSize = Math.min(availWidth / iconChildren.length, availHeight, opt.MAX_ICON_SIZE * scaleFactor);
} else {
availWidth = this._maxWidth;
availWidth -= this._background.get_theme_node().get_horizontal_padding();
availWidth -= themeNode.get_horizontal_padding();
availWidth -= buttonWidth - iconWidth;
availHeight = maxContent.y2 - maxContent.y1;
availHeight -= iconChildren.length * (buttonHeight - iconHeight) +
(iconChildren.length - 1) * spacing +
2 * this._background.get_theme_node().get_vertical_padding();
maxIconSize = Math.min(availWidth, availHeight / iconChildren.length, opt.MAX_ICON_SIZE * scaleFactor);
}
let iconSizes = BaseIconSizes.map(s => s * scaleFactor);
let newIconSize = BaseIconSizes[0];
for (let i = 0; i < iconSizes.length; i++) {
if (iconSizes[i] <= maxIconSize)
newIconSize = BaseIconSizes[i];
}
if (newIconSize === this.iconSize)
return;
// set the in-progress state here after all the possible cancels
this._adjustingInProgress = true;
let oldIconSize = this.iconSize;
this.iconSize = newIconSize;
this.emit('icon-size-changed');
let scale = oldIconSize / newIconSize;
for (let i = 0; i < iconChildren.length; i++) {
let icon = iconChildren[i].child._delegate.icon;
// Set the new size immediately, to keep the icons' sizes
// in sync with this.iconSize
icon.setIconSize(this.iconSize);
// Don't animate the icon size change when the overview
// is transitioning, not visible or when initially filling
// the dash
if (!Main.overview.visible || Main.overview.animationInProgress ||
!this._shownInitially)
continue;
let [targetWidth, targetHeight] = icon.icon.get_size();
// Scale the icon's texture to the previous size and
// tween to the new size
icon.icon.set_size(icon.icon.width * scale,
icon.icon.height * scale);
icon.icon.ease({
width: targetWidth,
height: targetHeight,
duration: Dash.DASH_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
if (this._separator) {
this._separator.ease({
width: dashHorizontal ? 1 : this.iconSize,
height: dashHorizontal ? this.iconSize : 1,
duration: Dash.DASH_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
this._adjustingInProgress = false;
},
};
const DashIconCommon = {
after__init() {
if (opt.DASH_ICON_SCROLL) {
this._scrollConId = this.connect('scroll-event', _onScrollEvent.bind(this));
this._leaveConId = this.connect('leave-event', _onLeaveEvent.bind(this));
}
},
};
function _onScrollEvent(source, event) {
if ((this.app && !opt.DASH_ICON_SCROLL) || (this._isSearchWindowsIcon && !opt.SEARCH_WINDOWS_ICON_SCROLL)) {
if (this._scrollConId)
this.disconnect(this._scrollConId);
if (this._leaveConId)
this.disconnect(this._leaveConId);
return Clutter.EVENT_PROPAGATE;
}
let direction = Util.getScrollDirection(event);
if (direction === Clutter.ScrollDirection.UP)
direction = 1;
else if (direction === Clutter.ScrollDirection.DOWN)
direction = -1;
else
return Clutter.EVENT_STOP;
// avoid uncontrollable switching if smooth scroll wheel or trackpad is used
if (this._lastScroll && Date.now() - this._lastScroll < 160)
return Clutter.EVENT_STOP;
this._lastScroll = Date.now();
_switchWindow.bind(this)(direction);
return Clutter.EVENT_STOP;
}
function _onLeaveEvent() {
if (!this._selectedMetaWin || this.has_pointer || this.toggleButton?.has_pointer)
return;
this._selectedPreview._activateSelected = false;
this._selectedMetaWin = null;
this._scrolledWindows = null;
_showWindowPreview.bind(this)(null);
}
function _switchWindow(direction) {
if (!this._scrolledWindows) {
// source is app icon
if (this.app) {
this._scrolledWindows = this.app.get_windows();
const wsList = [];
this._scrolledWindows.forEach(w => {
const ws = w.get_workspace();
if (!wsList.includes(ws))
wsList.push(ws);
});
// sort windows by workspaces in MRU order
this._scrolledWindows.sort((a, b) => wsList.indexOf(a.get_workspace()) > wsList.indexOf(b.get_workspace()));
// source is Search Windows icon
} else if (this._isSearchWindowsIcon) {
if (opt.SEARCH_WINDOWS_ICON_SCROLL === 1) // all windows
this._scrolledWindows = AltTab.getWindows(null);
else
this._scrolledWindows = AltTab.getWindows(global.workspace_manager.get_active_workspace());
}
}
let windows = this._scrolledWindows;
if (!windows.length)
return;
// if window selection is in the process, the previewed window must be the current one
let currentWin = this._selectedMetaWin ? this._selectedMetaWin : windows[0];
const currentIdx = windows.indexOf(currentWin);
let targetIdx = currentIdx + direction;
if (targetIdx > windows.length - 1)
targetIdx = 0;
else if (targetIdx < 0)
targetIdx = windows.length - 1;
const metaWin = windows[targetIdx];
_showWindowPreview.bind(this)(metaWin);
this._selectedMetaWin = metaWin;
}
function _showWindowPreview(metaWin) {
const views = Main.overview._overview.controls._workspacesDisplay._workspacesViews;
const viewsIter = [views[0]];
// secondary monitors use different structure
views.forEach(v => {
if (v._workspacesView)
viewsIter.push(v._workspacesView);
});
viewsIter.forEach(view => {
// if workspaces are on primary monitor only
if (!view || !view._workspaces)
return;
view._workspaces.forEach(ws => {
ws._windows.forEach(windowPreview => {
// metaWin === null resets opacity
let opacity = metaWin ? 50 : 255;
windowPreview._activateSelected = false;
// minimized windows are invisible if windows are not exposed (WORKSPACE_MODE === 0)
if (!windowPreview.opacity)
windowPreview.opacity = 255;
// app windows set to lower opacity, so they can be recognized
if (this._scrolledWindows && this._scrolledWindows.includes(windowPreview.metaWindow)) {
if (opt.DASH_ICON_SCROLL === 2)
opacity = 254;
}
if (windowPreview.metaWindow === metaWin) {
if (metaWin && metaWin.get_workspace() !== global.workspace_manager.get_active_workspace())
Main.wm.actionMoveWorkspace(metaWin.get_workspace());
windowPreview.get_parent().set_child_above_sibling(windowPreview, null);
opacity = 255;
this._selectedPreview = windowPreview;
windowPreview._activateSelected = true;
}
// if windows are exposed, highlight selected using opacity
if ((opt.OVERVIEW_MODE && opt.WORKSPACE_MODE) || !opt.OVERVIEW_MODE) {
if (metaWin && opacity === 255)
windowPreview.showOverlay(true);
else
windowPreview.hideOverlay(true);
windowPreview.ease({
duration: 200,
opacity,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
});
});
});
}
const AppIconCommon = {
activate(button) {
const event = Clutter.get_current_event();
const state = event ? event.get_state() : 0;
const isMiddleButton = button && button === Clutter.BUTTON_MIDDLE;
const isCtrlPressed = Util.isCtrlPressed(state);
const isShiftPressed = Util.isShiftPressed(state);
const openNewWindow = (this.app.can_open_new_window() &&
this.app.state === Shell.AppState.RUNNING &&
(isCtrlPressed || isMiddleButton) && !opt.DASH_CLICK_ACTION === 2) ||
(opt.DASH_CLICK_ACTION === 2 && !this._selectedMetaWin && !isMiddleButton);
const currentWS = global.workspace_manager.get_active_workspace();
const appRecentWorkspace = _getAppRecentWorkspace(this.app);
// this feature shouldn't affect search results, dash icons don't have labels, so we use them as a condition
const showWidowsBeforeActivation = opt.DASH_CLICK_ACTION === 1 && !this.icon.label;
let targetWindowOnCurrentWs = false;
if (opt.DASH_FOLLOW_RECENT_WIN) {
targetWindowOnCurrentWs = appRecentWorkspace === currentWS;
} else {
this.app.get_windows().forEach(
w => {
targetWindowOnCurrentWs = targetWindowOnCurrentWs || (w.get_workspace() === currentWS);
}
);
}
if ((this.app.state === Shell.AppState.STOPPED || openNewWindow) && !isShiftPressed)
this.animateLaunch();
if (openNewWindow) {
this.app.open_new_window(-1);
// if DASH_CLICK_ACTION == "SHOW_WINS_BEFORE", the app has more than one window and has no window on the current workspace,
// don't activate the app immediately, only move the overview to the workspace with the app's recent window
} else if (showWidowsBeforeActivation && !isShiftPressed && this.app.get_n_windows() > 1 && !targetWindowOnCurrentWs/* && !(opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE)*/) {
// this._scroll = true;
// this._scrollTime = Date.now();
Main.wm.actionMoveWorkspace(appRecentWorkspace);
Main.overview.dash.showAppsButton.checked = false;
return;
} else if (this._selectedMetaWin) {
this._selectedMetaWin.activate(global.get_current_time());
} else if (showWidowsBeforeActivation && opt.OVERVIEW_MODE && !opt.WORKSPACE_MODE && !isShiftPressed && this.app.get_n_windows() > 1) {
// expose windows
Main.overview._overview._controls._thumbnailsBox._activateThumbnailAtPoint(0, 0, global.get_current_time(), true);
return;
} else if (opt.DASH_SHIFT_CLICK_MV && isShiftPressed && this.app.get_windows().length) {
this._moveAppToCurrentWorkspace();
return;
} else if (isShiftPressed) {
return;
} else {
this.app.activate();
}
Main.overview.hide();
},
_moveAppToCurrentWorkspace() {
this.app.get_windows().forEach(w => w.change_workspace(global.workspace_manager.get_active_workspace()));
},
popupMenu(side = St.Side.LEFT) {
if (shellVersion >= 42)
this.setForcedHighlight(true);
this._removeMenuTimeout();
this.fake_release();
if (!this._getWindowsOnCurrentWs) {
this._getWindowsOnCurrentWs = function () {
const winList = [];
this.app.get_windows().forEach(w => {
if (w.get_workspace() === global.workspace_manager.get_active_workspace())
winList.push(w);
});
return winList;
};
this._windowsOnOtherWs = function () {
return (this.app.get_windows().length - this._getWindowsOnCurrentWs().length) > 0;
};
}
if (!this._menu) {
this._menu = new AppMenu(this, side, {
favoritesSection: true,
showSingleWindows: true,
});
this._menu.setApp(this.app);
this._openSigId = this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
if (!isPoppedUp)
this._onMenuPoppedDown();
});
// Main.overview.connectObject('hiding',
this._hidingSigId = Main.overview.connect('hiding',
() => this._menu.close(), this);
Main.uiGroup.add_actor(this._menu.actor);
this._menuManager.addMenu(this._menu);
}
// once the menu is created, it stays unchanged and we need to modify our items based on current situation
if (this._addedMenuItems && this._addedMenuItems.length)
this._addedMenuItems.forEach(i => i.destroy());
const popupItems = [];
const separator = new PopupMenu.PopupSeparatorMenuItem();
this._menu.addMenuItem(separator);
if (this.app.get_n_windows()) {
// if (/* opt.APP_MENU_FORCE_QUIT*/true) {}
popupItems.push([_('Force Quit'), () => {
this.app.get_windows()[0].kill();
}]);
// if (opt.APP_MENU_CLOSE_WS) {}
const nWin = this._getWindowsOnCurrentWs().length;
if (nWin) {
popupItems.push([_(`Close ${nWin} Windows on Current Workspace`), () => {
const windows = this._getWindowsOnCurrentWs();
let time = global.get_current_time();
for (let win of windows) {
// increase time by 1 ms for each window to avoid errors from GS
win.delete(time++);
}
}]);
}
if (/* opt.APP_MENU_MV_TO_WS && */this._windowsOnOtherWs())
popupItems.push([_('Move App to Current Workspace ( Shift + Click )'), this._moveAppToCurrentWorkspace]);
}
this._addedMenuItems = [];
this._addedMenuItems.push(separator);
popupItems.forEach(i => {
let item = new PopupMenu.PopupMenuItem(i[0]);
this._menu.addMenuItem(item);
item.connect('activate', i[1].bind(this));
this._addedMenuItems.push(item);
});
this.emit('menu-state-changed', true);
this._menu.open(BoxPointer.PopupAnimation.FULL);
this._menuManager.ignoreRelease();
this.emit('sync-tooltip');
return false;
},
};
function _getWindowApp(metaWin) {
const tracker = Shell.WindowTracker.get_default();
return tracker.get_window_app(metaWin);
}
function _getAppLastUsedWindow(app) {
let recentWin;
global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => {
const winApp = _getWindowApp(metaWin);
if (!recentWin && winApp === app)
recentWin = metaWin;
});
return recentWin;
}
function _getAppRecentWorkspace(app) {
const recentWin = _getAppLastUsedWindow(app);
if (recentWin)
return recentWin.get_workspace();
return null;
}
function _updateSearchWindowsIcon(show = opt.SHOW_WINDOWS_ICON) {
const dash = Main.overview._overview._controls.layoutManager._dash;
const dashContainer = dash._dashContainer;
if (dash._showWindowsIcon) {
dashContainer.remove_child(dash._showWindowsIcon);
if (dash._showWindowsIconClickedId)
dash._showWindowsIcon.toggleButton.disconnect(dash._showWindowsIconClickedId);
dash._showWindowsIconClickedId = undefined;
if (dash._showWindowsIcon)
dash._showWindowsIcon.destroy();
dash._showWindowsIcon = undefined;
}
if (!show || !opt.WINDOW_SEARCH_PROVIDER_ENABLED)
return;
if (!dash._showWindowsIcon) {
dash._showWindowsIcon = new ShowWindowsIcon();
dash._showWindowsIcon.show(false);
dashContainer.add_child(dash._showWindowsIcon);
dash._hookUpLabel(dash._showWindowsIcon);
}
dash._showWindowsIcon.icon.setIconSize(dash.iconSize);
if (opt.SHOW_WINDOWS_ICON === 1) {
dashContainer.set_child_at_index(dash._showWindowsIcon, 0);
} else if (opt.SHOW_WINDOWS_ICON === 2) {
const index = dashContainer.get_children().length - 1;
dashContainer.set_child_at_index(dash._showWindowsIcon, index);
}
Main.overview._overview._controls.layoutManager._dash._adjustIconSize();
}
const ShowWindowsIcon = GObject.registerClass(
class ShowWindowsIcon extends Dash.DashItemContainer {
_init() {
super._init();
this._isSearchWindowsIcon = true;
this._labelText = _('Search Open Windows (Hotkey: Space)');
this.toggleButton = new St.Button({
style_class: 'show-apps',
track_hover: true,
can_focus: true,
toggle_mode: false,
});
this._iconActor = null;
this.icon = new IconGrid.BaseIcon(this.labelText, {
setSizeManually: true,
showLabel: false,
createIcon: this._createIcon.bind(this),
});
this.icon.y_align = Clutter.ActorAlign.CENTER;
this.toggleButton.add_actor(this.icon);
this.toggleButton._delegate = this;
this.setChild(this.toggleButton);
if (opt.SEARCH_WINDOWS_ICON_SCROLL) {
this.reactive = true;
this._scrollConId = this.connect('scroll-event', _onScrollEvent.bind(this));
this._leaveConId = this.connect('leave-event', _onLeaveEvent.bind(this));
}
}
_createIcon(size) {
this._iconActor = new St.Icon({
icon_name: 'focus-windows-symbolic',
icon_size: size,
style_class: 'show-apps-icon',
track_hover: true,
});
return this._iconActor;
}
});
function _updateRecentFilesIcon(show = opt.SHOW_RECENT_FILES_ICON) {
const dash = Main.overview._overview._controls.layoutManager._dash;
const dashContainer = dash._dashContainer;
if (dash._recentFilesIcon) {
dashContainer.remove_child(dash._recentFilesIcon);
if (dash._recentFilesIconClickedId)
dash._recentFilesIcon.toggleButton.disconnect(dash._recentFilesIconClickedId);
dash._recentFilesIconClickedId = undefined;
if (dash._recentFilesIcon)
dash._recentFilesIcon.destroy();
dash._recentFilesIcon = undefined;
}
if (!show || !opt.RECENT_FILES_SEARCH_PROVIDER_ENABLED)
return;
if (!dash._recentFilesIcon) {
dash._recentFilesIcon = new ShowRecentFilesIcon();
dash._recentFilesIcon.show(false);
dashContainer.add_child(dash._recentFilesIcon);
dash._hookUpLabel(dash._recentFilesIcon);
}
dash._recentFilesIcon.icon.setIconSize(dash.iconSize);
if (opt.SHOW_RECENT_FILES_ICON === 1) {
dashContainer.set_child_at_index(dash._recentFilesIcon, 0);
} else if (opt.SHOW_RECENT_FILES_ICON === 2) {
const index = dashContainer.get_children().length - 1;
dashContainer.set_child_at_index(dash._recentFilesIcon, index);
}
Main.overview._overview._controls.layoutManager._dash._adjustIconSize();
}
const ShowRecentFilesIcon = GObject.registerClass(
class ShowRecentFilesIcon extends Dash.DashItemContainer {
_init() {
super._init();
this._labelText = _('Search Recent Files (Hotkey: Ctrl + Space)');
this.toggleButton = new St.Button({
style_class: 'show-apps',
track_hover: true,
can_focus: true,
toggle_mode: false,
});
this._iconActor = null;
this.icon = new IconGrid.BaseIcon(this.labelText, {
setSizeManually: true,
showLabel: false,
createIcon: this._createIcon.bind(this),
});
this.icon.y_align = Clutter.ActorAlign.CENTER;
this.toggleButton.add_actor(this.icon);
this.toggleButton._delegate = this;
this.setChild(this.toggleButton);
}
_createIcon(size) {
this._iconActor = new St.Icon({
icon_name: 'document-open-recent-symbolic',
icon_size: size,
style_class: 'show-apps-icon',
track_hover: true,
});
return this._iconActor;
}
});