1199 lines
42 KiB
JavaScript
Executable File
1199 lines
42 KiB
JavaScript
Executable File
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import {
|
|
Clutter,
|
|
Gio,
|
|
GLib,
|
|
GObject,
|
|
Shell,
|
|
St,
|
|
} from './dependencies/gi.js';
|
|
|
|
import {
|
|
AppFavorites,
|
|
Dash,
|
|
DND,
|
|
Main,
|
|
} from './dependencies/shell/ui.js';
|
|
|
|
import {
|
|
Util,
|
|
} from './dependencies/shell/misc.js';
|
|
|
|
import {
|
|
AppIcons,
|
|
Docking,
|
|
Theming,
|
|
Utils,
|
|
} from './imports.js';
|
|
|
|
// module "Dash" does not export DASH_ANIMATION_TIME
|
|
// so we just define it like it is defined in Dash;
|
|
// taken from https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dash.js
|
|
const DASH_ANIMATION_TIME = 200;
|
|
const DASH_VISIBILITY_TIMEOUT = 3;
|
|
|
|
const Labels = Object.freeze({
|
|
SHOW_MOUNTS: Symbol('show-mounts'),
|
|
FIRST_LAST_CHILD_WORKAROUND: Symbol('first-last-child-workaround'),
|
|
});
|
|
|
|
/**
|
|
* Extend DashItemContainer
|
|
*
|
|
* - set label position based on dash orientation
|
|
*
|
|
*/
|
|
const DockDashItemContainer = GObject.registerClass(
|
|
class DockDashItemContainer extends Dash.DashItemContainer {
|
|
_init(position) {
|
|
super._init();
|
|
|
|
this.label?.add_style_class_name(Theming.PositionStyleClass[position]);
|
|
if (Docking.DockManager.settings.customThemeShrink)
|
|
this.label?.add_style_class_name('shrink');
|
|
}
|
|
|
|
showLabel() {
|
|
return AppIcons.itemShowLabel.call(this);
|
|
}
|
|
|
|
// we override the method show taken from:
|
|
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dash.js
|
|
// in order to apply a little modification at the end of the animation
|
|
// which makes sure that the icon background is not blurry
|
|
show(animate) {
|
|
if (this.child == null)
|
|
return;
|
|
|
|
this.ease({
|
|
scale_x: 1,
|
|
scale_y: 1,
|
|
opacity: 255,
|
|
duration: animate ? DASH_ANIMATION_TIME : 0,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => {
|
|
// when the animation is ended, we simulate
|
|
// a hover to gain back focus and unblur the
|
|
// background
|
|
this.set_hover(true);
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
const DockDashIconsVerticalLayout = GObject.registerClass(
|
|
class DockDashIconsVerticalLayout extends Clutter.BoxLayout {
|
|
_init() {
|
|
super._init({
|
|
orientation: Clutter.Orientation.VERTICAL,
|
|
});
|
|
}
|
|
|
|
vfunc_get_preferred_height(container, forWidth) {
|
|
const [natHeight] = super.vfunc_get_preferred_height(container, forWidth);
|
|
return [natHeight, 0];
|
|
}
|
|
});
|
|
|
|
|
|
const baseIconSizes = [16, 22, 24, 32, 48, 64, 96, 128];
|
|
|
|
/**
|
|
* This class is a fork of the upstream dash class (ui.dash.js)
|
|
*
|
|
* Summary of changes:
|
|
* - disconnect global signals adding a destroy method;
|
|
* - play animations even when not in overview mode
|
|
* - set a maximum icon size
|
|
* - show running and/or favorite applications
|
|
* - hide showApps label when the custom menu is shown.
|
|
* - add scrollview
|
|
* ensure actor is visible on keyfocus inseid the scrollview
|
|
* - add 128px icon size, might be useful for hidpi display
|
|
* - sync minimization application target position.
|
|
* - keep running apps ordered.
|
|
*/
|
|
export const DockDash = GObject.registerClass({
|
|
Properties: {
|
|
'requires-visibility': GObject.ParamSpec.boolean(
|
|
'requires-visibility', 'requires-visibility', 'requires-visibility',
|
|
GObject.ParamFlags.READWRITE,
|
|
false),
|
|
},
|
|
Signals: {
|
|
'menu-opened': {},
|
|
'menu-closed': {},
|
|
'icon-size-changed': {},
|
|
},
|
|
}, class DockDash extends St.Widget {
|
|
_init(monitorIndex) {
|
|
// Initialize icon variables and size
|
|
super._init({
|
|
name: 'dash',
|
|
offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS,
|
|
layout_manager: new Clutter.BinLayout(),
|
|
});
|
|
|
|
this._maxWidth = -1;
|
|
this._maxHeight = -1;
|
|
this.iconSize = Docking.DockManager.settings.dashMaxIconSize;
|
|
this._availableIconSizes = baseIconSizes;
|
|
this._shownInitially = false;
|
|
this._initializeIconSize(this.iconSize);
|
|
this._signalsHandler = new Utils.GlobalSignalsHandler(this);
|
|
|
|
this._separator = null;
|
|
|
|
this._monitorIndex = monitorIndex;
|
|
this._position = Utils.getPosition();
|
|
this._isHorizontal = (this._position === St.Side.TOP) ||
|
|
(this._position === St.Side.BOTTOM);
|
|
|
|
this._dragPlaceholder = null;
|
|
this._dragPlaceholderPos = -1;
|
|
this._animatingPlaceholdersCount = 0;
|
|
this._showLabelTimeoutId = 0;
|
|
this._resetHoverTimeoutId = 0;
|
|
this._labelShowing = false;
|
|
|
|
this._dashContainer = new St.BoxLayout({
|
|
name: 'dashtodockDashContainer',
|
|
x_align: Clutter.ActorAlign.CENTER,
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
vertical: !this._isHorizontal,
|
|
y_expand: this._isHorizontal,
|
|
x_expand: !this._isHorizontal,
|
|
});
|
|
|
|
this._scrollView = new St.ScrollView({
|
|
name: 'dashtodockDashScrollview',
|
|
hscrollbar_policy: this._isHorizontal ? St.PolicyType.EXTERNAL : St.PolicyType.NEVER,
|
|
vscrollbar_policy: this._isHorizontal ? St.PolicyType.NEVER : St.PolicyType.EXTERNAL,
|
|
x_expand: this._isHorizontal,
|
|
y_expand: !this._isHorizontal,
|
|
enable_mouse_scrolling: false,
|
|
});
|
|
|
|
this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this));
|
|
|
|
this._boxContainer = new St.BoxLayout({
|
|
name: 'dashtodockBoxContainer',
|
|
x_align: Clutter.ActorAlign.FILL,
|
|
y_align: Clutter.ActorAlign.FILL,
|
|
vertical: !this._isHorizontal,
|
|
});
|
|
this._boxContainer.add_style_class_name(Theming.PositionStyleClass[this._position]);
|
|
|
|
const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
|
|
this._box = new St.BoxLayout({
|
|
vertical: !this._isHorizontal,
|
|
clip_to_allocation: false,
|
|
...!this._isHorizontal ? {layout_manager: new DockDashIconsVerticalLayout()} : {},
|
|
x_align: rtl ? Clutter.ActorAlign.END : Clutter.ActorAlign.START,
|
|
y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START,
|
|
y_expand: !this._isHorizontal,
|
|
x_expand: this._isHorizontal,
|
|
});
|
|
this._box._delegate = this;
|
|
this._boxContainer.add_child(this._box);
|
|
Utils.addActor(this._scrollView, this._boxContainer);
|
|
this._dashContainer.add_child(this._scrollView);
|
|
|
|
this._showAppsIcon = new AppIcons.DockShowAppsIcon(this._position);
|
|
this._showAppsIcon.show(false);
|
|
this._showAppsIcon.icon.setIconSize(this.iconSize);
|
|
this._showAppsIcon.x_expand = false;
|
|
this._showAppsIcon.y_expand = false;
|
|
this.showAppsButton.connect('notify::hover', a => {
|
|
if (this._showAppsIcon.get_parent() === this._boxContainer)
|
|
this._ensureItemVisibility(a);
|
|
});
|
|
if (!this._isHorizontal)
|
|
this._showAppsIcon.y_align = Clutter.ActorAlign.START;
|
|
this._hookUpLabel(this._showAppsIcon);
|
|
this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => {
|
|
this._itemMenuStateChanged(this._showAppsIcon, opened);
|
|
});
|
|
this.updateShowAppsButton();
|
|
|
|
this._background = new St.Widget({
|
|
style_class: 'dash-background',
|
|
y_expand: this._isHorizontal,
|
|
x_expand: !this._isHorizontal,
|
|
});
|
|
|
|
const sizerBox = new Clutter.Actor();
|
|
sizerBox.add_constraint(new Clutter.BindConstraint({
|
|
source: this._isHorizontal ? this._showAppsIcon.icon : this._dashContainer,
|
|
coordinate: Clutter.BindCoordinate.HEIGHT,
|
|
}));
|
|
sizerBox.add_constraint(new Clutter.BindConstraint({
|
|
source: this._isHorizontal ? this._dashContainer : this._showAppsIcon.icon,
|
|
coordinate: Clutter.BindCoordinate.WIDTH,
|
|
}));
|
|
this._background.add_child(sizerBox);
|
|
|
|
this.add_child(this._background);
|
|
this.add_child(this._dashContainer);
|
|
|
|
this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this));
|
|
|
|
this._shellSettings = new Gio.Settings({
|
|
schema_id: 'org.gnome.shell',
|
|
});
|
|
|
|
this._appSystem = Shell.AppSystem.get_default();
|
|
|
|
this.iconAnimator = new Docking.IconAnimator(this);
|
|
|
|
this._signalsHandler.add([
|
|
this._appSystem,
|
|
'installed-changed',
|
|
() => {
|
|
AppFavorites.getAppFavorites().reload();
|
|
this._queueRedisplay();
|
|
},
|
|
], [
|
|
AppFavorites.getAppFavorites(),
|
|
'changed',
|
|
this._queueRedisplay.bind(this),
|
|
], [
|
|
this._appSystem,
|
|
'app-state-changed',
|
|
this._queueRedisplay.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'item-drag-begin',
|
|
this._onItemDragBegin.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'item-drag-end',
|
|
this._onItemDragEnd.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'item-drag-cancelled',
|
|
this._onItemDragCancelled.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'window-drag-begin',
|
|
this._onWindowDragBegin.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'window-drag-cancelled',
|
|
this._onWindowDragEnd.bind(this),
|
|
], [
|
|
Main.overview,
|
|
'window-drag-end',
|
|
this._onWindowDragEnd.bind(this),
|
|
]);
|
|
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
}
|
|
|
|
vfunc_get_preferred_height(forWidth) {
|
|
const [minHeight, natHeight] = super.vfunc_get_preferred_height.call(this, forWidth);
|
|
if (!this._isHorizontal && this._maxHeight !== -1 && natHeight > this._maxHeight)
|
|
return [minHeight, this._maxHeight];
|
|
else
|
|
return [minHeight, natHeight];
|
|
}
|
|
|
|
vfunc_get_preferred_width(forHeight) {
|
|
const [minWidth, natWidth] = super.vfunc_get_preferred_width.call(this, forHeight);
|
|
if (this._isHorizontal && this._maxWidth !== -1 && natWidth > this._maxWidth)
|
|
return [minWidth, this._maxWidth];
|
|
else
|
|
return [minWidth, natWidth];
|
|
}
|
|
|
|
get _container() {
|
|
return this._dashContainer;
|
|
}
|
|
|
|
_onDestroy() {
|
|
this.iconAnimator.destroy();
|
|
|
|
if (this._requiresVisibilityTimeout) {
|
|
GLib.source_remove(this._requiresVisibilityTimeout);
|
|
delete this._requiresVisibilityTimeout;
|
|
}
|
|
|
|
if (this._ensureActorVisibilityTimeoutId) {
|
|
GLib.source_remove(this._ensureActorVisibilityTimeoutId);
|
|
delete this._ensureActorVisibilityTimeoutId;
|
|
}
|
|
}
|
|
|
|
|
|
_onItemDragBegin(...args) {
|
|
return Dash.Dash.prototype._onItemDragBegin.call(this, ...args);
|
|
}
|
|
|
|
_onItemDragCancelled(...args) {
|
|
return Dash.Dash.prototype._onItemDragCancelled.call(this, ...args);
|
|
}
|
|
|
|
_onItemDragEnd(...args) {
|
|
return Dash.Dash.prototype._onItemDragEnd.call(this, ...args);
|
|
}
|
|
|
|
_endItemDrag(...args) {
|
|
return Dash.Dash.prototype._endItemDrag.call(this, ...args);
|
|
}
|
|
|
|
_onItemDragMotion(...args) {
|
|
return Dash.Dash.prototype._onItemDragMotion.call(this, ...args);
|
|
}
|
|
|
|
_appIdListToHash(...args) {
|
|
return Dash.Dash.prototype._appIdListToHash.call(this, ...args);
|
|
}
|
|
|
|
_queueRedisplay(...args) {
|
|
return Dash.Dash.prototype._queueRedisplay.call(this, ...args);
|
|
}
|
|
|
|
_hookUpLabel(...args) {
|
|
return Dash.Dash.prototype._hookUpLabel.call(this, ...args);
|
|
}
|
|
|
|
_syncLabel(...args) {
|
|
return Dash.Dash.prototype._syncLabel.call(this, ...args);
|
|
}
|
|
|
|
_clearDragPlaceholder(...args) {
|
|
return Dash.Dash.prototype._clearDragPlaceholder.call(this, ...args);
|
|
}
|
|
|
|
_clearEmptyDropTarget(...args) {
|
|
return Dash.Dash.prototype._clearEmptyDropTarget.call(this, ...args);
|
|
}
|
|
|
|
handleDragOver(source, actor, x, y, time) {
|
|
let ret;
|
|
if (this._isHorizontal) {
|
|
ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, x, y, time);
|
|
|
|
if (ret === DND.DragMotionResult.CONTINUE)
|
|
return ret;
|
|
} else {
|
|
const propertyInjections = new Utils.PropertyInjectionsHandler();
|
|
propertyInjections.add(this._box, 'width', {
|
|
get: () => this._box.get_children().reduce((a, c) => a + c.height, 0),
|
|
});
|
|
|
|
if (this._dragPlaceholder) {
|
|
propertyInjections.add(this._dragPlaceholder, 'width', {
|
|
get: () => this._dragPlaceholder.height,
|
|
});
|
|
}
|
|
|
|
ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, y, x, time);
|
|
propertyInjections.destroy();
|
|
|
|
if (ret === DND.DragMotionResult.CONTINUE)
|
|
return ret;
|
|
|
|
if (this._dragPlaceholder) {
|
|
this._dragPlaceholder.child.set_width(this.iconSize / 2);
|
|
this._dragPlaceholder.child.set_height(this.iconSize);
|
|
|
|
let pos = this._dragPlaceholderPos;
|
|
if (this._isHorizontal &&
|
|
Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
|
|
pos = this._box.get_children() - 1 - pos;
|
|
|
|
if (pos !== this._dragPlaceholderPos) {
|
|
this._dragPlaceholderPos = pos;
|
|
this._box.set_child_at_index(this._dragPlaceholder,
|
|
this._dragPlaceholderPos);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._dragPlaceholder) {
|
|
// Ensure the next and previous icon are visible when moving the
|
|
// placeholder (we're assuming there's room for both of them)
|
|
const children = this._box.get_children();
|
|
if (this._dragPlaceholderPos > 0) {
|
|
ensureActorVisibleInScrollView(this._scrollView,
|
|
children[this._dragPlaceholderPos - 1]);
|
|
}
|
|
|
|
if (this._dragPlaceholderPos >= -1 &&
|
|
this._dragPlaceholderPos < children.length - 1) {
|
|
ensureActorVisibleInScrollView(this._scrollView,
|
|
children[this._dragPlaceholderPos + 1]);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
acceptDrop(...args) {
|
|
return Dash.Dash.prototype.acceptDrop.call(this, ...args);
|
|
}
|
|
|
|
_onWindowDragBegin(...args) {
|
|
return Dash.Dash.prototype._onWindowDragBegin.call(this, ...args);
|
|
}
|
|
|
|
_onWindowDragEnd(...args) {
|
|
return Dash.Dash.prototype._onWindowDragEnd.call(this, ...args);
|
|
}
|
|
|
|
_onScrollEvent(actor, event) {
|
|
// If scroll is not used because the icon is resized, let the scroll event propagate.
|
|
if (!Docking.DockManager.settings.iconSizeFixed)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
// reset timeout to avid conflicts with the mousehover event
|
|
this._ensureItemVisibility(null);
|
|
|
|
// Skip to avoid double events mouse
|
|
// TODO: Horizontal events are emulated, potentially due to a conflict
|
|
// with the workspace switching gesture.
|
|
if (!this._isHorizontal && event.is_pointer_emulated())
|
|
return Clutter.EVENT_STOP;
|
|
|
|
|
|
let adjustment, delta = 0;
|
|
|
|
if (this._isHorizontal)
|
|
adjustment = this._scrollView.get_hscroll_bar().get_adjustment();
|
|
else
|
|
adjustment = this._scrollView.get_vscroll_bar().get_adjustment();
|
|
|
|
const increment = adjustment.step_increment;
|
|
|
|
if (this._isHorizontal) {
|
|
switch (event.get_scroll_direction()) {
|
|
case Clutter.ScrollDirection.LEFT:
|
|
delta = -increment;
|
|
break;
|
|
case Clutter.ScrollDirection.RIGHT:
|
|
delta = Number(increment);
|
|
break;
|
|
case Clutter.ScrollDirection.SMOOTH: {
|
|
const [dx] = event.get_scroll_delta();
|
|
delta = dx * increment;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
switch (event.get_scroll_direction()) {
|
|
case Clutter.ScrollDirection.UP:
|
|
delta = -increment;
|
|
break;
|
|
case Clutter.ScrollDirection.DOWN:
|
|
delta = Number(increment);
|
|
break;
|
|
case Clutter.ScrollDirection.SMOOTH: {
|
|
const [, dy] = event.get_scroll_delta();
|
|
delta = dy * increment;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const value = adjustment.get_value();
|
|
|
|
// TODO: Remove this if possible.
|
|
if (Number.isNaN(value))
|
|
adjustment.set_value(delta);
|
|
else
|
|
adjustment.set_value(value + delta);
|
|
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
_ensureItemVisibility(actor) {
|
|
if (actor?.hover) {
|
|
const destroyId =
|
|
actor.connect('destroy', () => this._ensureItemVisibility(null));
|
|
this._ensureActorVisibilityTimeoutId = GLib.timeout_add(
|
|
GLib.PRIORITY_DEFAULT, 100, () => {
|
|
actor.disconnect(destroyId);
|
|
ensureActorVisibleInScrollView(this._scrollView, actor);
|
|
this._ensureActorVisibilityTimeoutId = 0;
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
} else if (this._ensureActorVisibilityTimeoutId) {
|
|
GLib.source_remove(this._ensureActorVisibilityTimeoutId);
|
|
this._ensureActorVisibilityTimeoutId = 0;
|
|
}
|
|
}
|
|
|
|
_createAppItem(app) {
|
|
const appIcon = new AppIcons.makeAppIcon(app, this._monitorIndex, this.iconAnimator);
|
|
|
|
if (appIcon._draggable) {
|
|
appIcon._draggable.connect('drag-begin', () => {
|
|
appIcon.opacity = 50;
|
|
});
|
|
appIcon._draggable.connect('drag-end', () => {
|
|
appIcon.opacity = 255;
|
|
});
|
|
}
|
|
|
|
appIcon.connect('menu-state-changed', (_, opened) => {
|
|
this._itemMenuStateChanged(item, opened);
|
|
});
|
|
|
|
const item = new DockDashItemContainer(this._position);
|
|
item.setChild(appIcon);
|
|
|
|
appIcon.connect('notify::hover', a => this._ensureItemVisibility(a));
|
|
appIcon.connect('clicked', actor => {
|
|
ensureActorVisibleInScrollView(this._scrollView, actor);
|
|
});
|
|
|
|
appIcon.connect('key-focus-in', actor => {
|
|
const [xShift, yShift] = ensureActorVisibleInScrollView(this._scrollView, actor);
|
|
|
|
// This signal is triggered also by mouse click. The popup menu is opened at the original
|
|
// coordinates. Thus correct for the shift which is going to be applied to the scrollview.
|
|
if (appIcon._menu) {
|
|
appIcon._menu._boxPointer.xOffset = -xShift;
|
|
appIcon._menu._boxPointer.yOffset = -yShift;
|
|
}
|
|
});
|
|
|
|
appIcon.connect('notify::focused', () => {
|
|
const {settings} = Docking.DockManager;
|
|
if (appIcon.focused && settings.scrollToFocusedApplication)
|
|
ensureActorVisibleInScrollView(this._scrollView, item);
|
|
});
|
|
|
|
appIcon.connect('notify::urgent', () => {
|
|
if (appIcon.urgent) {
|
|
ensureActorVisibleInScrollView(this._scrollView, item);
|
|
if (Docking.DockManager.settings.showDockUrgentNotify)
|
|
this._requireVisibility();
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
|
|
item.connect('notify::position', () => appIcon.updateIconGeometry());
|
|
item.connect('notify::size', () => appIcon.updateIconGeometry());
|
|
|
|
return item;
|
|
}
|
|
|
|
_requireVisibility() {
|
|
this.requiresVisibility = true;
|
|
|
|
if (this._requiresVisibilityTimeout)
|
|
GLib.source_remove(this._requiresVisibilityTimeout);
|
|
|
|
this._requiresVisibilityTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
|
DASH_VISIBILITY_TIMEOUT, () => {
|
|
this._requiresVisibilityTimeout = 0;
|
|
this.requiresVisibility = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return an array with the "proper" appIcons currently in the dash
|
|
*/
|
|
getAppIcons() {
|
|
// 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)
|
|
const iconChildren = this._box.get_children().filter(actor => {
|
|
return actor.child &&
|
|
!!actor.child.icon &&
|
|
!actor.animatingOut;
|
|
});
|
|
|
|
const appIcons = iconChildren.map(actor => {
|
|
return actor.child;
|
|
});
|
|
|
|
return appIcons;
|
|
}
|
|
|
|
_itemMenuStateChanged(item, opened) {
|
|
Dash.Dash.prototype._itemMenuStateChanged.call(this, item, opened);
|
|
|
|
if (opened) {
|
|
this.emit('menu-opened');
|
|
} else {
|
|
// I want to listen from outside when a menu is closed. I used to
|
|
// add a custom signal to the appIcon, since gnome 3.8 the signal
|
|
// calling this callback was added upstream.
|
|
this.emit('menu-closed');
|
|
}
|
|
}
|
|
|
|
_adjustIconSize() {
|
|
// 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)
|
|
const iconChildren = this._box.get_children().filter(actor => {
|
|
return actor.child &&
|
|
actor.child._delegate &&
|
|
actor.child._delegate.icon &&
|
|
!actor.animatingOut;
|
|
});
|
|
|
|
iconChildren.push(this._showAppsIcon);
|
|
|
|
if (this._maxWidth === -1 && this._maxHeight === -1)
|
|
return;
|
|
|
|
// Check if the container is present in the stage. This avoids critical
|
|
// errors when unlocking the screen
|
|
if (!this._container.get_stage())
|
|
return;
|
|
|
|
const themeNode = this._dashContainer.get_theme_node();
|
|
const maxAllocation = new Clutter.ActorBox({
|
|
x1: 0,
|
|
y1: 0,
|
|
x2: this._isHorizontal ? this._maxWidth : 42 /* whatever */,
|
|
y2: this._isHorizontal ? 42 : this._maxHeight,
|
|
});
|
|
const maxContent = themeNode.get_content_box(maxAllocation);
|
|
let availSpace;
|
|
if (this._isHorizontal)
|
|
availSpace = maxContent.get_width();
|
|
else
|
|
availSpace = maxContent.get_height();
|
|
|
|
const spacing = themeNode.get_length('spacing');
|
|
|
|
const [{child: firstButton}] = iconChildren;
|
|
const {child: firstIcon} = firstButton?.icon ?? {child: null};
|
|
|
|
// if no icons there's nothing to adjust
|
|
if (!firstIcon)
|
|
return;
|
|
|
|
// Enforce valid spacings during the size request
|
|
firstIcon.ensure_style();
|
|
const [, , iconWidth, iconHeight] = firstIcon.get_preferred_size();
|
|
const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size();
|
|
|
|
if (this._isHorizontal) {
|
|
// Subtract icon padding and box spacing from the available width
|
|
availSpace -= iconChildren.length * (buttonWidth - iconWidth) +
|
|
(iconChildren.length - 1) * spacing;
|
|
|
|
if (this._separator) {
|
|
const [, , separatorWidth] = this._separator.get_preferred_size();
|
|
availSpace -= separatorWidth + spacing;
|
|
}
|
|
} else {
|
|
// Subtract icon padding and box spacing from the available height
|
|
availSpace -= iconChildren.length * (buttonHeight - iconHeight) +
|
|
(iconChildren.length - 1) * spacing;
|
|
|
|
if (this._separator) {
|
|
const [, , , separatorHeight] = this._separator.get_preferred_size();
|
|
availSpace -= separatorHeight + spacing;
|
|
}
|
|
}
|
|
|
|
const maxIconSize = availSpace / iconChildren.length;
|
|
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
|
|
const iconSizes = this._availableIconSizes.map(s => s * scaleFactor);
|
|
|
|
let [newIconSize] = this._availableIconSizes;
|
|
for (let i = 0; i < iconSizes.length; i++) {
|
|
if (iconSizes[i] <= maxIconSize)
|
|
newIconSize = this._availableIconSizes[i];
|
|
}
|
|
|
|
if (newIconSize === this.iconSize)
|
|
return;
|
|
|
|
const oldIconSize = this.iconSize;
|
|
this.iconSize = newIconSize;
|
|
this.emit('icon-size-changed');
|
|
|
|
const scale = oldIconSize / newIconSize;
|
|
for (let i = 0; i < iconChildren.length; i++) {
|
|
const {icon} = iconChildren[i].child._delegate;
|
|
|
|
// 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;
|
|
|
|
const [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_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
}
|
|
|
|
if (this._separator) {
|
|
const animateProperties = this._isHorizontal
|
|
? {height: this.iconSize} : {width: this.iconSize};
|
|
|
|
this._separator.ease({
|
|
...animateProperties,
|
|
duration: DASH_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
}
|
|
}
|
|
|
|
_redisplay() {
|
|
const favorites = AppFavorites.getAppFavorites().getFavoriteMap();
|
|
|
|
let running = this._appSystem.get_running();
|
|
const dockManager = Docking.DockManager.getDefault();
|
|
const {settings} = dockManager;
|
|
|
|
this._scrollView.set({
|
|
xAlign: Clutter.ActorAlign.FILL,
|
|
yAlign: Clutter.ActorAlign.FILL,
|
|
});
|
|
if (dockManager.settings.dockExtended) {
|
|
if (!this._isHorizontal) {
|
|
this._scrollView.yAlign = dockManager.settings.alwaysCenterIcons
|
|
? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START;
|
|
} else {
|
|
this._scrollView.xAlign = dockManager.settings.alwaysCenterIcons
|
|
? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START;
|
|
}
|
|
}
|
|
|
|
if (settings.isolateWorkspaces ||
|
|
settings.isolateMonitors) {
|
|
// When using isolation, we filter out apps that have no windows in
|
|
// the current workspace
|
|
const monitorIndex = this._monitorIndex;
|
|
running = running.filter(app =>
|
|
AppIcons.getInterestingWindows(app.get_windows(), monitorIndex).length);
|
|
}
|
|
|
|
const 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
|
|
const newApps = [];
|
|
|
|
const {showFavorites} = settings;
|
|
if (showFavorites)
|
|
newApps.push(...Object.values(favorites));
|
|
|
|
if (settings.showRunning) {
|
|
// We reorder the running apps so that they don't change position on the
|
|
// dash with every redisplay() call
|
|
|
|
// First: add the apps from the oldApps list that are still running
|
|
oldApps.forEach(oldApp => {
|
|
const index = running.indexOf(oldApp);
|
|
if (index > -1) {
|
|
const [app] = running.splice(index, 1);
|
|
if (!showFavorites || !(app.get_id() in favorites))
|
|
newApps.push(app);
|
|
}
|
|
});
|
|
|
|
// Second: add the new apps
|
|
running.forEach(app => {
|
|
if (!showFavorites || !(app.get_id() in favorites))
|
|
newApps.push(app);
|
|
});
|
|
}
|
|
|
|
this._signalsHandler.removeWithLabel(Labels.SHOW_MOUNTS);
|
|
if (dockManager.removables) {
|
|
this._signalsHandler.addWithLabel(Labels.SHOW_MOUNTS,
|
|
dockManager.removables, 'changed', this._queueRedisplay.bind(this));
|
|
dockManager.removables.getApps().forEach(removable => {
|
|
if (!newApps.includes(removable))
|
|
newApps.push(removable);
|
|
});
|
|
} else {
|
|
oldApps = oldApps.filter(app => !app.location || app.isTrash);
|
|
}
|
|
|
|
if (dockManager.trash) {
|
|
const trashApp = dockManager.trash.getApp();
|
|
if (!newApps.includes(trashApp))
|
|
newApps.push(trashApp);
|
|
} else {
|
|
oldApps = oldApps.filter(app => !app.isTrash);
|
|
}
|
|
|
|
// Temporary remove the separator so that we don't compute to position icons
|
|
const oldSeparatorPos = this._box.get_children().indexOf(this._separator);
|
|
if (this._separator)
|
|
this._box.remove_child(this._separator);
|
|
|
|
// 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.
|
|
|
|
const addedItems = [];
|
|
const removedActors = [];
|
|
|
|
let newIndex = 0;
|
|
let oldIndex = 0;
|
|
while (newIndex < newApps.length || oldIndex < oldApps.length) {
|
|
const oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null;
|
|
const 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
|
|
const nextApp = newApps.length > newIndex + 1
|
|
? newApps[newIndex + 1] : null;
|
|
const insertHere = nextApp && nextApp === oldApp;
|
|
const alreadyRemoved = removedActors.reduce((result, actor) => {
|
|
const removedApp = actor.child._delegate.app;
|
|
return result || removedApp === newApp;
|
|
}, false);
|
|
|
|
if (insertHere || alreadyRemoved) {
|
|
const 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++) {
|
|
const item = removedActors[i];
|
|
|
|
// Don't animate item removal when the overview is transitioning
|
|
// or hidden
|
|
if (!Main.overview.animationInProgress)
|
|
item.animateOutAndDestroy();
|
|
else
|
|
item.destroy();
|
|
}
|
|
|
|
// Update separator
|
|
const nFavorites = Object.keys(favorites).length;
|
|
const nIcons = children.length + addedItems.length - removedActors.length;
|
|
if (nFavorites > 0 && nFavorites < nIcons) {
|
|
if (!this._separator) {
|
|
this._separator = new St.Widget({
|
|
style_class: 'dash-separator',
|
|
x_align: this._isHorizontal
|
|
? Clutter.ActorAlign.FILL : Clutter.ActorAlign.CENTER,
|
|
y_align: this._isHorizontal
|
|
? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.FILL,
|
|
width: this._isHorizontal ? -1 : this.iconSize,
|
|
height: this._isHorizontal ? this.iconSize : -1,
|
|
reactive: true,
|
|
track_hover: true,
|
|
});
|
|
this._separator.connect('notify::hover', a => this._ensureItemVisibility(a));
|
|
}
|
|
let pos = nFavorites + this._animatingPlaceholdersCount;
|
|
if (this._dragPlaceholder)
|
|
pos++;
|
|
const removedFavorites = removedActors.filter(a =>
|
|
children.indexOf(a) < oldSeparatorPos);
|
|
pos += removedFavorites.length;
|
|
this._box.insert_child_at_index(this._separator, pos);
|
|
} else if (this._separator) {
|
|
this._separator.destroy();
|
|
this._separator = null;
|
|
}
|
|
|
|
this._adjustIconSize();
|
|
|
|
// Skip animations on first run when adding the initial set
|
|
// of items, to avoid all items zooming in at once
|
|
const animate = this._shownInitially &&
|
|
!Main.layoutManager._startingUp;
|
|
|
|
if (!this._shownInitially)
|
|
this._shownInitially = true;
|
|
|
|
addedItems.forEach(({item}) => item.show(animate));
|
|
|
|
// Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744
|
|
// Without it, StBoxLayout may use a stale size cache
|
|
this._box.queue_relayout();
|
|
|
|
// This will update the size, and the corresponding number for each icon
|
|
this._updateNumberOverlay();
|
|
|
|
this.updateShowAppsButton();
|
|
}
|
|
|
|
_updateNumberOverlay() {
|
|
const appIcons = this.getAppIcons();
|
|
let counter = 1;
|
|
appIcons.forEach(icon => {
|
|
if (counter < 10) {
|
|
icon.setNumberOverlay(counter);
|
|
counter++;
|
|
} else if (counter === 10) {
|
|
icon.setNumberOverlay(0);
|
|
counter++;
|
|
} else {
|
|
// No overlay after 10
|
|
icon.setNumberOverlay(-1);
|
|
}
|
|
icon.updateNumberOverlay();
|
|
});
|
|
}
|
|
|
|
toggleNumberOverlay(activate) {
|
|
const appIcons = this.getAppIcons();
|
|
appIcons.forEach(icon => {
|
|
icon.toggleNumberOverlay(activate);
|
|
});
|
|
}
|
|
|
|
_initializeIconSize(maxSize) {
|
|
const maxAllowed = baseIconSizes[baseIconSizes.length - 1];
|
|
maxSize = Math.min(maxSize, maxAllowed);
|
|
|
|
if (Docking.DockManager.settings.iconSizeFixed) {
|
|
this._availableIconSizes = [maxSize];
|
|
} else {
|
|
this._availableIconSizes = baseIconSizes.filter(val => {
|
|
return val < maxSize;
|
|
});
|
|
this._availableIconSizes.push(maxSize);
|
|
}
|
|
}
|
|
|
|
setIconSize(maxSize, doNotAnimate) {
|
|
this._initializeIconSize(maxSize);
|
|
|
|
if (doNotAnimate)
|
|
this._shownInitially = false;
|
|
|
|
this._queueRedisplay();
|
|
}
|
|
|
|
/**
|
|
* Reset the displayed apps icon to maintain the correct order when changing
|
|
* show favorites/show running settings
|
|
*/
|
|
resetAppIcons() {
|
|
const children = this._box.get_children().filter(actor => {
|
|
return actor.child &&
|
|
!!actor.child.icon;
|
|
});
|
|
for (let i = 0; i < children.length; i++) {
|
|
const item = children[i];
|
|
item.destroy();
|
|
}
|
|
|
|
// to avoid ugly animations, just suppress them like when dash is first loaded.
|
|
this._shownInitially = false;
|
|
this._redisplay();
|
|
}
|
|
|
|
get showAppsButton() {
|
|
return this._showAppsIcon.toggleButton;
|
|
}
|
|
|
|
showShowAppsButton() {
|
|
this._showAppsIcon.visible = true;
|
|
this._showAppsIcon.show(true);
|
|
this.updateShowAppsButton();
|
|
}
|
|
|
|
hideShowAppsButton() {
|
|
this._showAppsIcon.visible = false;
|
|
}
|
|
|
|
setMaxSize(maxWidth, maxHeight) {
|
|
if (this._maxWidth === maxWidth &&
|
|
this._maxHeight === maxHeight)
|
|
return;
|
|
|
|
this._maxWidth = maxWidth;
|
|
this._maxHeight = maxHeight;
|
|
this._queueRedisplay();
|
|
}
|
|
|
|
updateShowAppsButton() {
|
|
if (this._showAppsIcon.get_parent() && !this._showAppsIcon.visible)
|
|
return;
|
|
|
|
const {settings} = Docking.DockManager;
|
|
const notifiedProperties = [];
|
|
const showAppsContainer = settings.showAppsAlwaysInTheEdge || !settings.dockExtended
|
|
? this._dashContainer : this._boxContainer;
|
|
|
|
this._signalsHandler.addWithLabel(Labels.FIRST_LAST_CHILD_WORKAROUND,
|
|
showAppsContainer, 'notify',
|
|
(_obj, pspec) => notifiedProperties.push(pspec.name));
|
|
|
|
if (this._showAppsIcon.get_parent() !== showAppsContainer) {
|
|
this._showAppsIcon.get_parent()?.remove_child(this._showAppsIcon);
|
|
|
|
if (Docking.DockManager.settings.showAppsAtTop)
|
|
showAppsContainer.insert_child_below(this._showAppsIcon, null);
|
|
else
|
|
showAppsContainer.insert_child_above(this._showAppsIcon, null);
|
|
} else if (settings.showAppsAtTop) {
|
|
showAppsContainer.set_child_below_sibling(this._showAppsIcon, null);
|
|
} else {
|
|
showAppsContainer.set_child_above_sibling(this._showAppsIcon, null);
|
|
}
|
|
|
|
this._signalsHandler.removeWithLabel(Labels.FIRST_LAST_CHILD_WORKAROUND);
|
|
|
|
// This is indeed ugly, but we need to ensure that the last and first
|
|
// visible widgets are re-computed by St, that is buggy because of a
|
|
// mutter issue that is being fixed:
|
|
// https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/2047
|
|
if (!notifiedProperties.includes('first-child'))
|
|
showAppsContainer.notify('first-child');
|
|
if (!notifiedProperties.includes('last-child'))
|
|
showAppsContainer.notify('last-child');
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* This is a copy of the same function in utils.js, but also adjust horizontal scrolling
|
|
* and perform few further checks on the current value to avoid changing the values when
|
|
* it would be clamp to the current one in any case.
|
|
* Return the amount of shift applied
|
|
*
|
|
* @param scrollView
|
|
* @param actor
|
|
*/
|
|
function ensureActorVisibleInScrollView(scrollView, actor) {
|
|
// access to scrollView.[hv]scroll was deprecated in gnome 46
|
|
// instead, adjustment can be accessed directly
|
|
// keep old way for backwards compatibility (gnome <= 45)
|
|
const vAdjustment = scrollView.vadjustment ?? scrollView.vscroll.adjustment;
|
|
const hAdjustment = scrollView.hadjustment ?? scrollView.hscroll.adjustment;
|
|
const {value: vValue0, pageSize: vPageSize, upper: vUpper} = vAdjustment;
|
|
const {value: hValue0, pageSize: hPageSize, upper: hUpper} = hAdjustment;
|
|
let [hValue, vValue] = [hValue0, vValue0];
|
|
let vOffset = 0;
|
|
let hOffset = 0;
|
|
|
|
const fade = scrollView.get_effect('fade');
|
|
if (fade) {
|
|
vOffset = fade.fade_margins.top;
|
|
hOffset = fade.fade_margins.left;
|
|
}
|
|
|
|
const box = actor.get_allocation_box();
|
|
let {y1} = box, {y2} = box, {x1} = box, {x2} = box;
|
|
|
|
let parent = actor.get_parent();
|
|
while (parent !== scrollView) {
|
|
if (!parent)
|
|
throw new Error('Actor not in scroll view');
|
|
|
|
const parentBox = parent.get_allocation_box();
|
|
y1 += parentBox.y1;
|
|
y2 += parentBox.y1;
|
|
x1 += parentBox.x1;
|
|
x2 += parentBox.x1;
|
|
parent = parent.get_parent();
|
|
}
|
|
|
|
if (y1 < vValue + vOffset)
|
|
vValue = Math.max(0, y1 - vOffset);
|
|
else if (vValue < vUpper - vPageSize && y2 > vValue + vPageSize - vOffset)
|
|
vValue = Math.min(vUpper - vPageSize, y2 + vOffset - vPageSize);
|
|
|
|
if (x1 < hValue + hOffset)
|
|
hValue = Math.max(0, x1 - hOffset);
|
|
else if (hValue < hUpper - hPageSize && x2 > hValue + hPageSize - hOffset)
|
|
hValue = Math.min(hUpper - hPageSize, x2 + hOffset - hPageSize);
|
|
|
|
if (vValue !== vValue0) {
|
|
vAdjustment.ease(vValue, {
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: Util.SCROLL_TIME,
|
|
});
|
|
}
|
|
|
|
if (hValue !== hValue0) {
|
|
hAdjustment.ease(hValue, {
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
duration: Util.SCROLL_TIME,
|
|
});
|
|
}
|
|
|
|
return [hValue - hValue0, vValue - vValue0];
|
|
}
|