2021-08-21 22:39:24 +08:00

1073 lines
37 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const AppDisplay = imports.ui.appDisplay;
const AppFavorites = imports.ui.appFavorites;
const Dash = imports.ui.dash;
const DND = imports.ui.dnd;
const IconGrid = imports.ui.iconGrid;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const Util = imports.misc.util;
const Workspace = imports.ui.workspace;
const Me = imports.misc.extensionUtils.getCurrentExtension();
const Docking = Me.imports.docking;
const Utils = Me.imports.utils;
const AppIcons = Me.imports.appIcons;
const Locations = Me.imports.locations;
const DASH_ANIMATION_TIME = Dash.DASH_ANIMATION_TIME;
const DASH_ITEM_LABEL_HIDE_TIME = Dash.DASH_ITEM_LABEL_HIDE_TIME;
const DASH_ITEM_HOVER_TIMEOUT = Dash.DASH_ITEM_HOVER_TIMEOUT;
/**
* Extend DashItemContainer
*
* - set label position based on dash orientation
*
*/
let MyDashItemContainer = GObject.registerClass(
class DashToDock_MyDashItemContainer extends Dash.DashItemContainer {
showLabel() {
return AppIcons.itemShowLabel.call(this);
}
});
const MyDashIconsVerticalLayout = GObject.registerClass(
class DashToDock_MyDashIconsVerticalLayout 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 usefull for hidpi display
* - sync minimization application target position.
* - keep running apps ordered.
*/
var MyDash = GObject.registerClass({
Signals: {
'menu-closed': {},
'icon-size-changed': {},
}
}, class DashToDock_MyDash extends St.Widget {
_init(remoteModel, monitorIndex) {
// Initialize icon variables and size
this._maxWidth = -1;
this._maxHeight = -1;
this.iconSize = Docking.DockManager.settings.get_int('dash-max-icon-size');
this._availableIconSizes = baseIconSizes;
this._shownInitially = false;
this._initializeIconSize(this.iconSize);
this._separator = null;
this._remoteModel = remoteModel;
this._monitorIndex = monitorIndex;
this._position = Utils.getPosition();
this._isHorizontal = ((this._position == St.Side.TOP) ||
(this._position == St.Side.BOTTOM));
this._signalsHandler = new Utils.GlobalSignalsHandler();
this._dragPlaceholder = null;
this._dragPlaceholderPos = -1;
this._animatingPlaceholdersCount = 0;
this._showLabelTimeoutId = 0;
this._resetHoverTimeoutId = 0;
this._labelShowing = false;
super._init({
name: 'dash',
offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS,
layout_manager: new Clutter.BinLayout()
});
this._dashContainer = new St.BoxLayout({
x_align: Clutter.ActorAlign.CENTER,
y_align: this._isHorizontal ? Clutter.ActorAlign.CENTER: Clutter.ActorAlign.START,
vertical: !this._isHorizontal,
y_expand: this._isHorizontal,
x_expand: !this._isHorizontal,
pack_start: Docking.DockManager.settings.get_boolean('show-apps-at-top')
});
this._scrollView = new St.ScrollView({
name: 'dashtodockDashScrollview',
// TODO: Fix scrolling
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
});
if (Docking.DockManager.settings.get_boolean('extend-height')) {
if (!this._isHorizontal) {
this._scrollView.y_align = Clutter.ActorAlign.START;
} else {
this._scrollView.x_align = Clutter.ActorAlign.START;
}
}
this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this));
let 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 MyDashIconsVerticalLayout() } : {}),
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._dashContainer.add_actor(this._scrollView);
this._scrollView.add_actor(this._box);
this._showAppsIcon = new AppIcons.MyShowAppsIcon();
this._showAppsIcon.show(false);
this._showAppsIcon.icon.setIconSize(this.iconSize);
this._showAppsIcon.x_expand = false;
this._showAppsIcon.y_expand = false;
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._dashContainer.add_child(this._showAppsIcon);
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) {
let [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) {
let [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();
this._signalsHandler.destroy();
}
_onItemDragBegin() {
return Dash.Dash.prototype._onItemDragBegin.call(this, ...arguments);
}
_onItemDragCancelled() {
return Dash.Dash.prototype._onItemDragCancelled.call(this, ...arguments);
}
_onItemDragEnd() {
return Dash.Dash.prototype._onItemDragEnd.call(this, ...arguments);
}
_endItemDrag() {
return Dash.Dash.prototype._endItemDrag.call(this, ...arguments);
}
_onItemDragMotion() {
return Dash.Dash.prototype._onItemDragMotion.call(this, ...arguments);
}
_appIdListToHash() {
return Dash.Dash.prototype._appIdListToHash.call(this, ...arguments);
}
_queueRedisplay() {
return Dash.Dash.prototype._queueRedisplay.call(this, ...arguments);
}
_hookUpLabel() {
return Dash.Dash.prototype._hookUpLabel.call(this, ...arguments);
}
_syncLabel() {
return Dash.Dash.prototype._syncLabel.call(this, ...arguments);
}
_clearDragPlaceholder() {
return Dash.Dash.prototype._clearDragPlaceholder.call(this, ...arguments);
}
_clearEmptyDropTarget() {
return Dash.Dash.prototype._clearEmptyDropTarget.call(this, ...arguments);
}
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 {
Object.defineProperty(this._box, 'height', {
configurable: true,
get: () => this._box.get_children().reduce((a, c) => a + c.height, 0),
});
let replacedPlaceholderWidth = false;
if (this._dragPlaceholder) {
replacedPlaceholderWidth = true;
Object.defineProperty(this._dragPlaceholder, 'width', {
configurable: true,
get: () => this._dragPlaceholder.height,
});
}
ret = Dash.Dash.prototype.handleDragOver.call(this, source, actor, y, x, time);
delete this._box.height;
if (replacedPlaceholderWidth && this._dragPlaceholder)
delete this._dragPlaceholder.width;
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
// (I assume there's room for both of them)
if (this._dragPlaceholderPos > 0)
ensureActorVisibleInScrollView(this._scrollView,
this._box.get_children()[this._dragPlaceholderPos - 1]);
if (this._dragPlaceholderPos < this._box.get_children().length - 1)
ensureActorVisibleInScrollView(this._scrollView,
this._box.get_children()[this._dragPlaceholderPos + 1]);
}
return ret;
}
acceptDrop() {
return Dash.Dash.prototype.acceptDrop.call(this, ...arguments);
}
_onWindowDragBegin() {
return Dash.Dash.prototype._onWindowDragBegin.call(this, ...arguments);
}
_onWindowDragEnd() {
return Dash.Dash.prototype._onWindowDragEnd.call(this, ...arguments);
}
_onScrollEvent(actor, event) {
// If scroll is not used because the icon is resized, let the scroll event propagate.
if (!Docking.DockManager.settings.get_boolean('icon-size-fixed'))
return Clutter.EVENT_PROPAGATE;
// reset timeout to avid conflicts with the mousehover event
if (this._ensureAppIconVisibilityTimeoutId > 0) {
GLib.source_remove(this._ensureAppIconVisibilityTimeoutId);
this._ensureAppIconVisibilityTimeoutId = 0;
}
// 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();
let increment = adjustment.step_increment;
if (this._isHorizontal) {
switch (event.get_scroll_direction()) {
case Clutter.ScrollDirection.LEFT:
delta = -increment;
break;
case Clutter.ScrollDirection.RIGHT:
delta = +increment;
break;
case Clutter.ScrollDirection.SMOOTH:
let [dx, dy] = event.get_scroll_delta();
// TODO: Handle y
//delta = dy * increment;
// Also consider horizontal component, for instance touchpad
delta = dx * increment;
break;
}
} else {
switch (event.get_scroll_direction()) {
case Clutter.ScrollDirection.UP:
delta = -increment;
break;
case Clutter.ScrollDirection.DOWN:
delta = +increment;
break;
case Clutter.ScrollDirection.SMOOTH:
let [, 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;
}
_createAppItem(app) {
let appIcon = new AppIcons.MyAppIcon(this._remoteModel, 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', (appIcon, opened) => {
this._itemMenuStateChanged(item, opened);
});
let item = new MyDashItemContainer();
item.setChild(appIcon);
appIcon.connect('notify::hover', () => {
if (appIcon.hover) {
this._ensureAppIconVisibilityTimeoutId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT, 100, () => {
ensureActorVisibleInScrollView(this._scrollView, appIcon);
this._ensureAppIconVisibilityTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
}
else {
if (this._ensureAppIconVisibilityTimeoutId > 0) {
GLib.source_remove(this._ensureAppIconVisibilityTimeoutId);
this._ensureAppIconVisibilityTimeoutId = 0;
}
}
});
appIcon.connect('clicked', (actor) => {
ensureActorVisibleInScrollView(this._scrollView, actor);
});
appIcon.connect('key-focus-in', (actor) => {
let [x_shift, y_shift] = 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 = -x_shift;
appIcon._menu._boxPointer.yOffset = -y_shift;
}
});
// 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;
}
/**
* 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)
let iconChildren = this._box.get_children().filter(function(actor) {
return actor.child &&
!!actor.child.icon &&
!actor.animatingOut;
});
let appIcons = iconChildren.map(function(actor) {
return actor.child;
});
return appIcons;
}
_updateAppsIconGeometry() {
let appIcons = this.getAppIcons();
appIcons.forEach(function(icon) {
icon.updateIconGeometry();
});
}
_itemMenuStateChanged(item, opened) {
Dash.Dash.prototype._itemMenuStateChanged.call(this, item, opened);
if (!opened) {
// 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)
let 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.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
});
let maxContent = themeNode.get_content_box(maxAllocation);
let availWidth;
if (this._isHorizontal)
availWidth = maxContent.x2 - maxContent.x1;
else
availWidth = maxContent.y2 - maxContent.y1;
let spacing = themeNode.get_length('spacing');
let firstButton = iconChildren[0].child;
let firstIcon = firstButton._delegate.icon;
// 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();
// Subtract icon padding and box spacing from the available height
if (this._isHorizontal)
// Subtract icon padding and box spacing from the available width
availWidth -= iconChildren.length * (buttonWidth - iconWidth) +
(iconChildren.length - 1) * spacing;
else
availWidth -= iconChildren.length * (buttonHeight - iconHeight) +
(iconChildren.length - 1) * spacing;
// let availHeight = this._maxHeight;
// availHeight -= this._background.get_theme_node().get_vertical_padding();
// availHeight -= themeNode.get_vertical_padding();
// availHeight -= buttonHeight - iconHeight;
const maxIconSize = // TODO: Math.min(
availWidth / iconChildren.length // ); , availHeight);
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
let iconSizes = this._availableIconSizes.map(s => s * scaleFactor);
let newIconSize = this._availableIconSizes[0];
for (let i = 0; i < iconSizes.length; i++) {
if (iconSizes[i] <= maxIconSize)
newIconSize = this._availableIconSizes[i];
}
if (newIconSize == this.iconSize)
return;
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_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
if (this._separator) {
if (this._isHorizontal) {
this._separator.ease({
height: this.iconSize,
duration: DASH_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
} else {
this._separator.ease({
width: this.iconSize,
duration: DASH_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
}
}
_redisplay() {
let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
let running = this._appSystem.get_running();
let settings = Docking.DockManager.settings;
if (settings.get_boolean('isolate-workspaces') ||
settings.get_boolean('isolate-monitors')) {
// When using isolation, we filter out apps that have no windows in
// the current workspace
let monitorIndex = this._monitorIndex;
running = running.filter(function(_app) {
return AppIcons.getInterestingWindows(_app, monitorIndex).length != 0;
});
}
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 = [];
if (settings.get_boolean('show-favorites')) {
for (let id in favorites)
newApps.push(favorites[id]);
}
if (settings.get_boolean('show-running')) {
for (let i = 0; i < running.length; i++) {
let app = running[i];
if (settings.get_boolean('show-favorites') && app.get_id() in favorites)
continue;
newApps.push(app);
}
}
if (settings.get_boolean('show-mounts')) {
if (!this._removables) {
this._removables = new Locations.Removables();
this._signalsHandler.addWithLabel('show-mounts',
[ this._removables,
'changed',
this._queueRedisplay.bind(this) ]);
}
Array.prototype.push.apply(newApps, this._removables.getApps());
} else if (this._removables) {
this._signalsHandler.removeWithLabel('show-mounts');
this._removables.destroy();
this._removables = null;
}
if (settings.get_boolean('show-trash')) {
if (!this._trash) {
this._trash = new Locations.Trash();
this._signalsHandler.addWithLabel('show-trash',
[ this._trash,
'changed',
this._queueRedisplay.bind(this) ]);
}
newApps.push(this._trash.getApp());
} else if (this._trash) {
this._signalsHandler.removeWithLabel('show-trash');
this._trash.destroy();
this._trash = null;
}
// 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.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.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) {
if (!this._separator) {
if (!this._isHorizontal) {
this._separator = new St.Widget({
style_class: 'vertical-dash-separator',
x_align: Clutter.ActorAlign.CENTER,
width: this.iconSize,
});
} else {
this._separator = new St.Widget({
style_class: 'dash-separator',
y_align: Clutter.ActorAlign.CENTER,
height: this.iconSize,
});
}
this._box.add_child(this._separator);
}
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();
// TODO
// This is required for icon reordering when the scrollview is used.
this._updateAppsIconGeometry();
// This will update the size, and the corresponding number for each icon
this._updateNumberOverlay();
}
_updateNumberOverlay() {
let appIcons = this.getAppIcons();
let counter = 1;
appIcons.forEach(function(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) {
let appIcons = this.getAppIcons();
appIcons.forEach(function(icon) {
icon.toggleNumberOverlay(activate);
});
}
_initializeIconSize(max_size) {
let max_allowed = baseIconSizes[baseIconSizes.length-1];
max_size = Math.min(max_size, max_allowed);
if (Docking.DockManager.settings.get_boolean('icon-size-fixed'))
this._availableIconSizes = [max_size];
else {
this._availableIconSizes = baseIconSizes.filter(function(val) {
return (val<max_size);
});
this._availableIconSizes.push(max_size);
}
}
setIconSize(max_size, doNotAnimate) {
this._initializeIconSize(max_size);
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() {
let children = this._box.get_children().filter(function(actor) {
return actor.child &&
!!actor.child.icon;
});
for (let i = 0; i < children.length; i++) {
let 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.showAppsButton.visible = true
this.showAppsButton.set_width(-1)
this.showAppsButton.set_height(-1)
}
hideShowAppsButton() {
//this.showAppsButton.hide()
// The height and width of the button is bound to the background.
if (this._isHorizontal) {
this.showAppsButton.set_width(0)
} else {
this.showAppsButton.set_height(0)
}
}
setMaxSize(maxWidth, maxHeight) {
if (this._maxWidth === maxWidth &&
this._maxHeight === maxHeight)
return;
this._maxWidth = maxWidth;
this._maxHeight = maxHeight;
this._queueRedisplay();
}
updateShowAppsButton() {
if (Docking.DockManager.settings.get_boolean('show-apps-at-top')) {
this._dashContainer.pack_start = true;
} else {
this._dashContainer.pack_start = false;
}
}
});
/**
* This is a copy of the same function in utils.js, but also adjust horizontal scrolling
* and perform few further cheks 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
*/
function ensureActorVisibleInScrollView(scrollView, actor) {
let adjust_v = true;
let adjust_h = true;
let vadjustment = scrollView.get_vscroll_bar().get_adjustment();
let hadjustment = scrollView.get_hscroll_bar().get_adjustment();
let [vvalue, vlower, vupper, vstepIncrement, vpageIncrement, vpageSize] = vadjustment.get_values();
let [hvalue, hlower, hupper, hstepIncrement, hpageIncrement, hpageSize] = hadjustment.get_values();
let [hvalue0, vvalue0] = [hvalue, vvalue];
let voffset = 0;
let hoffset = 0;
let fade = scrollView.get_effect('fade');
if (fade) {
voffset = fade.vfade_offset;
hoffset = fade.hfade_offset;
}
let box = actor.get_allocation_box();
let y1 = box.y1, y2 = box.y2, x1 = box.x1, x2 = box.x2;
let parent = actor.get_parent();
while (parent != scrollView) {
if (!parent)
throw new Error('Actor not in scroll view');
let box = parent.get_allocation_box();
y1 += box.y1;
y2 += box.y1;
x1 += box.x1;
x2 += box.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];
}