1307 lines
50 KiB
JavaScript
1307 lines
50 KiB
JavaScript
/**
|
|
* V-Shell (Vertical Workspaces)
|
|
* dash.js
|
|
*
|
|
* @author GdH <G-dH@github.com>
|
|
* @copyright 2022-2024
|
|
* @license GPL-3.0
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import GLib from 'gi://GLib';
|
|
import Meta from 'gi://Meta';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
|
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
import * as Dash from 'resource:///org/gnome/shell/ui/dash.js';
|
|
import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js';
|
|
import * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js';
|
|
import * as AppMenu from 'resource:///org/gnome/shell/ui/appMenu.js';
|
|
import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
|
|
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
|
|
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
|
|
|
let Me;
|
|
let opt;
|
|
// gettext
|
|
let _;
|
|
|
|
let _moduleEnabled;
|
|
let _timeouts;
|
|
|
|
// added values to achieve a better ability to scale down according to available space
|
|
export const BaseIconSizes = [16, 24, 32, 40, 44, 48, 56, 64, 72, 80, 96, 112, 128];
|
|
|
|
const DASH_ITEM_LABEL_SHOW_TIME = 150;
|
|
|
|
const shellVersion46 = !Clutter.Container; // Container has been removed in 46
|
|
|
|
export const DashModule = class {
|
|
constructor(me) {
|
|
Me = me;
|
|
opt = Me.opt;
|
|
_ = Me.gettext;
|
|
|
|
this._firstActivation = true;
|
|
this.moduleEnabled = false;
|
|
this._overrides = null;
|
|
this._horizontalWorkId = null;
|
|
this._verticalWorkId = null;
|
|
this._showAppsIconBtnPressId = 0;
|
|
}
|
|
|
|
cleanGlobals() {
|
|
Me = null;
|
|
opt = null;
|
|
_ = null;
|
|
}
|
|
|
|
update(reset) {
|
|
this._removeTimeouts();
|
|
|
|
this.moduleEnabled = opt.get('dashModule');
|
|
const conflict = !!(Me.Util.getEnabledExtensions('dash-to-dock').length ||
|
|
Me.Util.getEnabledExtensions('dash2dock').length ||
|
|
Me.Util.getEnabledExtensions('ubuntu-dock').length ||
|
|
Me.Util.getEnabledExtensions('dash-to-panel').length);
|
|
|
|
if (conflict && !reset)
|
|
console.warn(`[${Me.metadata.name}] Warning: "Dash" module disabled due to potential conflict with another extension`);
|
|
|
|
reset = reset || !this.moduleEnabled || conflict;
|
|
this._conflict = conflict;
|
|
|
|
// don't touch the original code if module disabled
|
|
if (reset && !this._firstActivation) {
|
|
this._disableModule();
|
|
} else if (!reset) {
|
|
this._firstActivation = false;
|
|
this._activateModule();
|
|
}
|
|
if (reset && this._firstActivation)
|
|
console.debug(' DashModule - Keeping untouched');
|
|
}
|
|
|
|
updateStyle(dash) {
|
|
if (opt.DASH_BG_LIGHT)
|
|
dash._background.add_style_class_name('dash-background-light');
|
|
else
|
|
dash._background.remove_style_class_name('dash-background-light');
|
|
|
|
dash._background.opacity = opt.DASH_BG_OPACITY;
|
|
let radius = opt.DASH_BG_RADIUS;
|
|
if (radius) {
|
|
let style;
|
|
switch (opt.DASH_POSITION) {
|
|
case 1:
|
|
style = opt.DASH_BG_GS3_STYLE ? `border-radius: ${radius}px 0 0 ${radius}px;` : `border-radius: ${radius}px;`;
|
|
break;
|
|
case 3:
|
|
style = opt.DASH_BG_GS3_STYLE ? `border-radius: 0 ${radius}px ${radius}px 0;` : `border-radius: ${radius}px;`;
|
|
break;
|
|
default:
|
|
style = `border-radius: ${radius}px;`;
|
|
}
|
|
dash._background.set_style(style);
|
|
} else {
|
|
dash._background.set_style('');
|
|
}
|
|
}
|
|
|
|
_activateModule() {
|
|
_moduleEnabled = true;
|
|
_timeouts = {};
|
|
const dash = Main.overview._overview._controls.layoutManager._dash;
|
|
|
|
if (!this._originalWorkId)
|
|
this._originalWorkId = dash._workId;
|
|
|
|
if (!this._overrides)
|
|
this._overrides = new Me.Util.Overrides();
|
|
|
|
this._resetStyle(dash);
|
|
this.updateStyle(dash);
|
|
|
|
this._overrides.addOverride('DashItemContainer', Dash.DashItemContainer.prototype, DashItemContainerCommon);
|
|
this._overrides.addOverride('DashCommon', Dash.Dash.prototype, DashCommon);
|
|
this._overrides.addOverride('AppIcon', AppDisplay.AppIcon.prototype, AppIconCommon);
|
|
this._overrides.addOverride('DashIcon', Dash.DashIcon.prototype, DashIconCommon);
|
|
this._overrides.addOverride('AppMenu', AppMenu.AppMenu.prototype, AppMenuCommon);
|
|
|
|
if (shellVersion46)
|
|
dash.add_style_class_name('dash-46');
|
|
|
|
if (opt.DASH_VERTICAL) {
|
|
// this._overrides.addOverride('Dash', Dash.Dash.prototype, DashVerticalOverride);
|
|
dash.add_style_class_name(shellVersion46
|
|
? 'vertical-46'
|
|
: 'vertical'
|
|
);
|
|
|
|
this._setOrientation(Clutter.Orientation.VERTICAL);
|
|
} else {
|
|
this._setOrientation(Clutter.Orientation.HORIZONTAL);
|
|
}
|
|
|
|
if (opt.DASH_VERTICAL && opt.DASH_BG_GS3_STYLE) {
|
|
if (opt.DASH_LEFT) {
|
|
dash.add_style_class_name(shellVersion46
|
|
? 'vertical-46-gs3-left'
|
|
: 'vertical-gs3-left');
|
|
} else if (opt.DASH_RIGHT) {
|
|
dash.add_style_class_name(shellVersion46
|
|
? 'vertical-46-gs3-right'
|
|
: 'vertical-gs3-right');
|
|
}
|
|
} else {
|
|
dash.remove_style_class_name('vertical-gs3-left');
|
|
dash.remove_style_class_name('vertical-gs3-right');
|
|
dash.remove_style_class_name('vertical-46-gs3-left');
|
|
dash.remove_style_class_name('vertical-46-gs3-right');
|
|
}
|
|
|
|
if (!this._customWorkId)
|
|
this._customWorkId = Main.initializeDeferredWork(dash._box, dash._redisplay.bind(dash));
|
|
dash._workId = this._customWorkId;
|
|
|
|
this._moveDashAppGridIcon();
|
|
this._connectShowAppsIcon();
|
|
|
|
dash.visible = opt.DASH_VISIBLE;
|
|
// dash._background.add_style_class_name('dash-background-reduced');
|
|
dash._queueRedisplay();
|
|
|
|
if (opt.DASH_ISOLATE_WS && !this._wmSwitchWsConId) {
|
|
this._wmSwitchWsConId = global.windowManager.connect('switch-workspace', () => dash._queueRedisplay());
|
|
this._newWindowConId = global.display.connect_after('window-created', () => dash._queueRedisplay());
|
|
}
|
|
|
|
console.debug(' DashModule - Activated');
|
|
}
|
|
|
|
_disableModule() {
|
|
const dash = Main.overview._overview._controls.layoutManager._dash;
|
|
|
|
if (this._overrides)
|
|
this._overrides.removeAll();
|
|
this._overrides = null;
|
|
|
|
dash._workId = this._originalWorkId;
|
|
|
|
if (this._wmSwitchWsConId) {
|
|
global.windowManager.disconnect(this._wmSwitchWsConId);
|
|
this._wmSwitchWsConId = 0;
|
|
}
|
|
if (this._newWindowConId) {
|
|
global.windowManager.disconnect(this._newWindowConId);
|
|
this._newWindowConId = 0;
|
|
}
|
|
|
|
const reset = true;
|
|
this._setOrientation(Clutter.Orientation.HORIZONTAL);
|
|
this._moveDashAppGridIcon(reset);
|
|
this._connectShowAppsIcon(reset);
|
|
|
|
this._resetStyle(dash);
|
|
dash.visible = !this._conflict;
|
|
dash._background.opacity = 255;
|
|
|
|
_moduleEnabled = false;
|
|
console.debug(' DashModule - Disabled');
|
|
}
|
|
|
|
_resetStyle(dash) {
|
|
dash.remove_style_class_name('dash-46');
|
|
dash.remove_style_class_name('vertical');
|
|
dash.remove_style_class_name('vertical-46');
|
|
dash.remove_style_class_name('vertical-gs3-left');
|
|
dash.remove_style_class_name('vertical-gs3-right');
|
|
dash.remove_style_class_name('vertical-46-gs3-left');
|
|
dash.remove_style_class_name('vertical-46-gs3-right');
|
|
dash.remove_style_class_name('vertical-left');
|
|
dash.remove_style_class_name('vertical-right');
|
|
dash._background.remove_style_class_name('dash-background-light');
|
|
dash._background.remove_style_class_name('dash-background-reduced');
|
|
dash._background.set_style('');
|
|
}
|
|
|
|
_removeTimeouts() {
|
|
if (_timeouts) {
|
|
Object.values(_timeouts).forEach(t => {
|
|
if (t)
|
|
GLib.source_remove(t);
|
|
});
|
|
_timeouts = null;
|
|
}
|
|
}
|
|
|
|
_setOrientation(orientation, dash) {
|
|
dash = dash ?? Main.overview._overview._controls.layoutManager._dash;
|
|
|
|
dash._box.layout_manager.orientation = orientation;
|
|
dash._dashContainer.layout_manager.orientation = orientation;
|
|
dash._dashContainer.y_expand = !orientation;
|
|
dash._dashContainer.x_expand = !!orientation;
|
|
dash.x_align = orientation ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER;
|
|
dash.y_align = orientation ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL;
|
|
|
|
let sizerBox = dash._background.get_children()[0];
|
|
sizerBox.clear_constraints();
|
|
sizerBox.add_constraint(new Clutter.BindConstraint({
|
|
source: dash._showAppsIcon.icon,
|
|
coordinate: orientation ? Clutter.BindCoordinate.WIDTH : Clutter.BindCoordinate.HEIGHT,
|
|
}));
|
|
sizerBox.add_constraint(new Clutter.BindConstraint({
|
|
source: dash._dashContainer,
|
|
coordinate: orientation ? Clutter.BindCoordinate.HEIGHT : Clutter.BindCoordinate.WIDTH,
|
|
}));
|
|
dash._box.remove_all_children();
|
|
dash._separator = null;
|
|
dash._queueRedisplay();
|
|
dash._adjustIconSize();
|
|
}
|
|
|
|
_moveDashAppGridIcon(reset = false) {
|
|
// move dash app grid icon to the front
|
|
const dash = Main.overview._overview._controls.layoutManager._dash;
|
|
|
|
const appIconPosition = opt.get('showAppsIconPosition');
|
|
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;
|
|
}
|
|
}
|
|
|
|
_connectShowAppsIcon(reset = false, dash) {
|
|
dash = dash ?? Main.overview._overview._controls.layoutManager._dash;
|
|
if (!reset) {
|
|
if (this._showAppsIconBtnPressId || Me.Util.dashIsDashToDock()) {
|
|
// button is already connected || dash is Dash to Dock
|
|
return;
|
|
}
|
|
dash._showAppsIcon.reactive = true;
|
|
this._showAppsIconBtnPressId = dash._showAppsIcon.connect('button-press-event', (actor, event) => {
|
|
const button = event.get_button();
|
|
if (button === Clutter.BUTTON_MIDDLE)
|
|
Me.Util.openPreferences();
|
|
else if (button === Clutter.BUTTON_SECONDARY)
|
|
Me.Util.activateSearchProvider(Me.WSP_PREFIX);
|
|
else
|
|
return Clutter.EVENT_PROPAGATE;
|
|
return Clutter.EVENT_STOP;
|
|
});
|
|
} else if (this._showAppsIconBtnPressId) {
|
|
dash._showAppsIcon.disconnect(this._showAppsIconBtnPressId);
|
|
this._showAppsIconBtnPressId = 0;
|
|
dash._showAppsIcon.reactive = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
function getAppFromSource(source) {
|
|
if (source instanceof AppDisplay.AppIcon)
|
|
return source.app;
|
|
else
|
|
return null;
|
|
}
|
|
|
|
const DashItemContainerCommon = {
|
|
// move labels according dash position
|
|
showLabel() {
|
|
if (!this._labelText)
|
|
return;
|
|
|
|
const windows = this.child.app?.get_windows();
|
|
const recentWindowTitle = windows && windows.length ? windows[0].get_title() : '';
|
|
const windowCount = this.child.app?.get_windows().length;
|
|
let labelSuffix = '';
|
|
if (windowCount > 1)
|
|
labelSuffix = ` (${windowCount})`;
|
|
if (recentWindowTitle && recentWindowTitle !== this._labelText)
|
|
labelSuffix += `\n ${recentWindowTitle}`;
|
|
|
|
this.label.set_text(this._labelText + labelSuffix);
|
|
|
|
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);
|
|
const primaryMonitor = global.display.get_monitor_geometry(global.display.get_primary_monitor());
|
|
x = Math.clamp(x, primaryMonitor.x, primaryMonitor.x + primaryMonitor.width - labelWidth);
|
|
|
|
let node = this.label.get_theme_node();
|
|
let y;
|
|
|
|
if (opt.DASH_TOP) {
|
|
const yOffset = itemHeight + (shellVersion46 ? 0 : -3);
|
|
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 = shellVersion46 ? 8 : 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 = shellVersion46 ? 8 : 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 = {
|
|
_redisplay() {
|
|
// After disabling V-Shell queueRedisplay() may call this function
|
|
// In that case redirect the call to the current _redisplay()
|
|
if (!_moduleEnabled) {
|
|
this._redisplay();
|
|
return;
|
|
}
|
|
|
|
let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
|
|
|
|
let running = this._appSystem.get_running();
|
|
|
|
if (opt.DASH_ISOLATE_WS) {
|
|
const currentWs = global.workspace_manager.get_active_workspace();
|
|
running = running.filter(app => {
|
|
return app.get_windows().filter(w => w.get_workspace() === currentWs).length;
|
|
});
|
|
this._box.get_children().forEach(a => a.child?._updateRunningStyle());
|
|
}
|
|
|
|
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: opt.DASH_VERTICAL ? this.iconSize : 1,
|
|
height: opt.DASH_VERTICAL ? 1 : this.iconSize,
|
|
});
|
|
this._box.add_child(this._separator);
|
|
}
|
|
|
|
// FIXME: separator placement is broken (also in original dash)
|
|
let pos = nFavorites + this._animatingPlaceholdersCount;
|
|
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;
|
|
if (opt.DASH_VERTICAL) {
|
|
indicator.x_align = opt.DASH_LEFT ? Clutter.ActorAlign.START : Clutter.ActorAlign.END;
|
|
indicator.y_align = Clutter.ActorAlign.CENTER;
|
|
} else {
|
|
indicator.x_align = Clutter.ActorAlign.CENTER;
|
|
indicator.y_align = Clutter.ActorAlign.END;
|
|
}
|
|
|
|
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;
|
|
},
|
|
|
|
// 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);
|
|
|
|
|
|
// showWindowsIcon and extensionsIcon can be provided by the WSP and ESP extensions
|
|
if (this._showWindowsIcon)
|
|
iconChildren.push(this._showWindowsIcon);
|
|
|
|
if (this._extensionsIcon)
|
|
iconChildren.push(this._extensionsIcon);
|
|
|
|
|
|
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 maxIconSize = opt.MAX_ICON_SIZE;
|
|
if (!maxIconSize) {
|
|
maxIconSize = Me.Util.monitorHasLowResolution()
|
|
? 48
|
|
: 64;
|
|
}
|
|
|
|
let availWidth, availHeight;
|
|
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, maxIconSize * 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, maxIconSize * 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;
|
|
},
|
|
|
|
handleDragOver(source, actor, x, y, _time) {
|
|
let app = 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 boxSize = opt.DASH_VERTICAL ? this._box.height : this._box.width;
|
|
|
|
// 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) {
|
|
boxSize -= opt.DASH_VERTICAL ? this._dragPlaceholder.height : this._dragPlaceholder.width;
|
|
numChildren--;
|
|
}
|
|
|
|
// Same with the separator
|
|
if (this._separator) {
|
|
boxSize -= opt.DASH_VERTICAL ? this._separator.height : this._separator.width;
|
|
numChildren--;
|
|
}
|
|
|
|
let pos;
|
|
if (this._emptyDropTarget)
|
|
pos = 0; // always insert at the start when dash is empty
|
|
else if (this.text_direction === Clutter.TextDirection.RTL)
|
|
pos = numChildren - Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize);
|
|
else
|
|
pos = Math.floor((opt.DASH_VERTICAL ? y : x) * numChildren / boxSize);
|
|
|
|
// 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(); // not exported in 45
|
|
this._dragPlaceholder = new Dash.DashItemContainer();
|
|
this._dragPlaceholder.setChild(new St.Bin({ style_class: 'placeholder' }));
|
|
this._dragPlaceholder.child.set_width(this.iconSize / (opt.DASH_VERTICAL ? 2 : 1));
|
|
this._dragPlaceholder.child.set_height(this.iconSize / (opt.DASH_VERTICAL ? 1 : 2));
|
|
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;
|
|
},
|
|
};
|
|
|
|
const AppIconCommon = {
|
|
after__init() {
|
|
if (this._updateRunningDotStyle)
|
|
this._updateRunningDotStyle();
|
|
},
|
|
|
|
_updateRunningDotStyle() {
|
|
if (opt.RUNNING_DOT_STYLE)
|
|
this._dot.add_style_class_name('app-grid-running-dot-custom');
|
|
else
|
|
this._dot.remove_style_class_name('app-grid-running-dot-custom');
|
|
},
|
|
|
|
activate(button) {
|
|
const event = Clutter.get_current_event();
|
|
const state = event ? event.get_state() : 0;
|
|
const isMiddleButton = button && button === Clutter.BUTTON_MIDDLE;
|
|
const isCtrlPressed = Me.Util.isCtrlPressed(state);
|
|
const isShiftPressed = Me.Util.isShiftPressed(state);
|
|
|
|
const currentWS = global.workspace_manager.get_active_workspace();
|
|
const appRecentWorkspace = this._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);
|
|
}
|
|
);
|
|
}
|
|
|
|
const openNewWindow = this.app.can_open_new_window() &&
|
|
this.app.state === Shell.AppState.RUNNING &&
|
|
(((isCtrlPressed || isMiddleButton) && !opt.DASH_CLICK_OPEN_NEW_WIN) ||
|
|
(opt.DASH_CLICK_OPEN_NEW_WIN && !this._selectedMetaWin && !isMiddleButton) ||
|
|
((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !targetWindowOnCurrentWs));
|
|
|
|
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)*/) {
|
|
|
|
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_MODE2 && !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) || ((opt.DASH_CLICK_PREFER_WORKSPACE || opt.DASH_ISOLATE_WS) && !openNewWindow)) && this.app.get_windows().length) {
|
|
this._moveAppToCurrentWorkspace();
|
|
if (opt.DASH_ISOLATE_WS) {
|
|
this.app.activate();
|
|
// hide the overview after the window is re-created
|
|
GLib.idle_add(GLib.PRIORITY_LOW, () => Main.overview.hide());
|
|
}
|
|
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) {
|
|
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.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_child(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++);
|
|
}
|
|
}]);
|
|
}
|
|
|
|
popupItems.push([_('Move App to Current Workspace ( Shift + Click )'), this._moveAppToCurrentWorkspace]);
|
|
// WTMB (Windows Thumbnails) extension required
|
|
if (global.windowThumbnails) {
|
|
popupItems.push([_('Create Window Thumbnail/PiP'), () => {
|
|
global.windowThumbnails?.createThumbnail(this.app.get_windows()[0]);
|
|
}]);
|
|
}
|
|
}
|
|
|
|
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));
|
|
if (i[1] === this._moveAppToCurrentWorkspace && !this._windowsOnOtherWs())
|
|
item.setSensitive(false);
|
|
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;
|
|
},
|
|
|
|
_getWindowApp(metaWin) {
|
|
const tracker = Shell.WindowTracker.get_default();
|
|
return tracker.get_window_app(metaWin);
|
|
},
|
|
|
|
_getAppLastUsedWindow(app) {
|
|
let recentWin;
|
|
global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null).forEach(metaWin => {
|
|
const winApp = this._getWindowApp(metaWin);
|
|
if (!recentWin && winApp === app)
|
|
recentWin = metaWin;
|
|
});
|
|
return recentWin;
|
|
},
|
|
|
|
_getAppRecentWorkspace(app) {
|
|
const recentWin = this._getAppLastUsedWindow(app);
|
|
if (recentWin)
|
|
return recentWin.get_workspace();
|
|
|
|
return null;
|
|
},
|
|
};
|
|
|
|
const DashIconCommon = {
|
|
after__init() {
|
|
if (opt.DASH_ICON_SCROLL && !Me.Util.dashNotDefault()) {
|
|
this._scrollConId = this.connect('scroll-event', DashExtensions.onScrollEvent.bind(this));
|
|
this._leaveConId = this.connect('leave-event', DashExtensions.onLeaveEvent.bind(this));
|
|
}
|
|
},
|
|
|
|
popupMenu() {
|
|
const side = opt.DASH_VERTICAL ? St.Side.LEFT : St.Side.BOTTOM;
|
|
AppIconCommon.popupMenu.bind(this)(side);
|
|
},
|
|
|
|
_updateRunningDotStyle() {
|
|
if (opt.RUNNING_DOT_STYLE)
|
|
this._dot.add_style_class_name('app-grid-running-dot-custom');
|
|
else
|
|
this._dot.remove_style_class_name('app-grid-running-dot-custom');
|
|
|
|
this._dot.translation_x = 0;
|
|
// _updateDotStyle() has been added in GS 46.2 to apply translation_y value from the CSS on style change
|
|
if (shellVersion46 && !this._updateDotStyle && !opt.DASH_VERTICAL)
|
|
this._dot.translation_y = 8;
|
|
|
|
// GS 46.0 (Ubuntu) only
|
|
if (opt.DASH_VERTICAL)
|
|
this._dot.translationY = 0;
|
|
},
|
|
|
|
_updateRunningStyle() {
|
|
const currentWs = global.workspace_manager.get_active_workspace();
|
|
const show = opt.DASH_ISOLATE_WS
|
|
? this.app.get_windows().filter(w => w.get_workspace() === currentWs).length
|
|
: this.app.state !== Shell.AppState.STOPPED;
|
|
|
|
if (show)
|
|
this._dot.show();
|
|
else
|
|
this._dot.hide();
|
|
},
|
|
};
|
|
|
|
const DashExtensions = {
|
|
onScrollEvent(source, event) {
|
|
if ((this.app && !opt.DASH_ICON_SCROLL) || (this._isSearchWindowsIcon && !opt.SEARCH_WINDOWS_ICON_SCROLL)) {
|
|
if (this._scrollConId) {
|
|
this.disconnect(this._scrollConId);
|
|
this._scrollConId = 0;
|
|
}
|
|
if (this._leaveConId) {
|
|
this.disconnect(this._leaveConId);
|
|
this._leaveConId = 0;
|
|
}
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
if (Main.overview._overview.controls._stateAdjustment.value > 1)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
let direction = Me.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();
|
|
|
|
DashExtensions.switchWindow.bind(this)(direction);
|
|
return Clutter.EVENT_STOP;
|
|
},
|
|
|
|
onLeaveEvent() {
|
|
if (!this._selectedMetaWin || this.has_pointer || this.toggleButton?.has_pointer)
|
|
return;
|
|
|
|
this._selectedPreview._activateSelected = false;
|
|
this._selectedMetaWin = null;
|
|
this._scrolledWindows = null;
|
|
DashExtensions.showWindowPreview.bind(this)(null);
|
|
},
|
|
|
|
|
|
switchWindow(direction) {
|
|
if (!this._scrolledWindows) {
|
|
this._initialSelection = true;
|
|
// source is app icon
|
|
if (this.app) {
|
|
this._scrolledWindows = this.app.get_windows();
|
|
if (opt.DASH_ISOLATE_WS) {
|
|
const currentWs = global.workspaceManager.get_active_workspace();
|
|
this._scrolledWindows = this._scrolledWindows.filter(w => w.get_workspace() === currentWs);
|
|
}
|
|
|
|
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 = Me.Util.getWindows(null);
|
|
else
|
|
this._scrolledWindows = Me.Util.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;
|
|
// const focusWindow = Me.Util.getWindows(null)[0]; // incompatible 45
|
|
const focusWindow = Me.Util.getWindows(null)[0];
|
|
const appFocused = this._scrolledWindows[0] === focusWindow && this._scrolledWindows[0].get_workspace() === global.workspace_manager.get_active_workspace();
|
|
// only if the app has focus, immediately switch to the previous window
|
|
// otherwise just set the current window above others
|
|
if (!this._initialSelection || appFocused)
|
|
targetIdx += direction;
|
|
else
|
|
this._initialSelection = false;
|
|
|
|
if (targetIdx > windows.length - 1)
|
|
targetIdx = 0;
|
|
else if (targetIdx < 0)
|
|
targetIdx = windows.length - 1;
|
|
|
|
const metaWin = windows[targetIdx];
|
|
DashExtensions.showWindowPreview.bind(this)(metaWin);
|
|
this._selectedMetaWin = metaWin;
|
|
},
|
|
|
|
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());
|
|
if (_timeouts.wsSwitcherAnimation)
|
|
GLib.source_remove(_timeouts.wsSwitcherAnimation);
|
|
// setting window preview above siblings before workspace switcher animation has no effect
|
|
// we need to set the window above after the ws preview become visible on the screen
|
|
// the default switcher animation time is 250, 200 ms delay should be enough
|
|
_timeouts.wsSwitcherAnimation = GLib.timeout_add(0, 200 * St.Settings.get().slow_down_factor, () => {
|
|
windowPreview.get_parent().set_child_above_sibling(windowPreview, null);
|
|
_timeouts.wsSwitcherAnimation = 0;
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
} else {
|
|
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 AppMenuCommon = {
|
|
_updateWindowsSection() {
|
|
if (global.compositor) {
|
|
if (this._updateWindowsLaterId) {
|
|
const laters = global.compositor.get_laters();
|
|
laters.remove(this._updateWindowsLaterId);
|
|
}
|
|
} else if (this._updateWindowsLaterId) {
|
|
Meta.later_remove(this._updateWindowsLaterId);
|
|
}
|
|
|
|
this._updateWindowsLaterId = 0;
|
|
|
|
this._windowSection.removeAll();
|
|
this._openWindowsHeader.hide();
|
|
|
|
if (!this._app)
|
|
return;
|
|
|
|
const minWindows = this._showSingleWindows ? 1 : 2;
|
|
const currentWs = global.workspaceManager.get_active_workspace();
|
|
const isolateWs = opt.DASH_ISOLATE_WS && !Main.overview.dash.showAppsButton.checked;
|
|
const windows = this._app.get_windows().filter(w => !w.skip_taskbar && (isolateWs ? w.get_workspace() === currentWs : true));
|
|
if (windows.length < minWindows)
|
|
return;
|
|
|
|
this._openWindowsHeader.show();
|
|
|
|
windows.forEach(window => {
|
|
const title = window.title || this._app.get_name();
|
|
const item = this._windowSection.addAction(title, event => {
|
|
Main.activateWindow(window, event.get_time());
|
|
});
|
|
window.connectObject('notify::title', () => {
|
|
item.label.text = window.title || this._app.get_name();
|
|
}, item);
|
|
});
|
|
},
|
|
};
|