gconf-settings/skel/.local/share/gnome-shell/extensions/dash-to-dockmicxgx.gmail.com/dash.js

1203 lines
42 KiB
JavaScript
Raw Normal View History

2019-10-26 20:50:28 +00:00
// -*- 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 Gtk = imports.gi.Gtk;
const Signals = imports.signals;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
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);
}
});
/**
* This class is a fork of the upstream DashActor class (ui.dash.js)
*
* Summary of changes:
* - modified chldBox calculations for when 'show-apps-at-top' option is checked
* - handle horizontal dash
*/
var MyDashActor = GObject.registerClass(
class DashToDock_MyDashActor extends St.Widget {
_init() {
// a prefix is required to avoid conflicting with the parent class variable
this._rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL);
this._position = Utils.getPosition();
this._isHorizontal = ((this._position == St.Side.TOP) ||
(this._position == St.Side.BOTTOM));
let layout = new Clutter.BoxLayout({
orientation: this._isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL
});
super._init({
name: 'dash',
layout_manager: layout,
clip_to_allocation: true
});
// Since we are usually visible but not usually changing, make sure
// most repaint requests don't actually require us to repaint anything.
// This saves significant CPU when repainting the screen.
this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
}
vfunc_allocate(box, flags) {
this.set_allocation(box, flags);
let contentBox = box;
let availWidth = contentBox.x2 - contentBox.x1;
let availHeight = contentBox.y2 - contentBox.y1;
let [appIcons, showAppsButton] = this.get_children();
let [showAppsMinHeight, showAppsNatHeight] = showAppsButton.get_preferred_height(availWidth);
let [showAppsMinWidth, showAppsNatWidth] = showAppsButton.get_preferred_width(availHeight);
let offset_x = this._isHorizontal?showAppsNatWidth:0;
let offset_y = this._isHorizontal?0:showAppsNatHeight;
let childBox = new Clutter.ActorBox();
let settings = Docking.DockManager.settings;
if ((settings.get_boolean('show-apps-at-top') && !this._isHorizontal)
|| (settings.get_boolean('show-apps-at-top') && !this._rtl)
|| (!settings.get_boolean('show-apps-at-top') && this._isHorizontal && this._rtl)) {
childBox.x1 = contentBox.x1 + offset_x;
childBox.y1 = contentBox.y1 + offset_y;
childBox.x2 = contentBox.x2;
childBox.y2 = contentBox.y2;
appIcons.allocate(childBox, flags);
childBox.y1 = contentBox.y1;
childBox.x1 = contentBox.x1;
childBox.x2 = contentBox.x1 + showAppsNatWidth;
childBox.y2 = contentBox.y1 + showAppsNatHeight;
showAppsButton.allocate(childBox, flags);
}
else {
childBox.x1 = contentBox.x1;
childBox.y1 = contentBox.y1;
childBox.x2 = contentBox.x2 - offset_x;
childBox.y2 = contentBox.y2 - offset_y;
appIcons.allocate(childBox, flags);
childBox.x2 = contentBox.x2;
childBox.y2 = contentBox.y2;
childBox.x1 = contentBox.x2 - showAppsNatWidth;
childBox.y1 = contentBox.y2 - showAppsNatHeight;
showAppsButton.allocate(childBox, flags);
}
}
vfunc_get_preferred_width(forHeight) {
// We want to request the natural height of all our children
// as our natural height, so we chain up to StWidget (which
// then calls BoxLayout), but we only request the showApps
// button as the minimum size
let [, natWidth] = this.layout_manager.get_preferred_width(this, forHeight);
let themeNode = this.get_theme_node();
let [, showAppsButton] = this.get_children();
let [minWidth, ] = showAppsButton.get_preferred_height(forHeight);
return [minWidth, natWidth];
}
vfunc_get_preferred_height(forWidth) {
// We want to request the natural height of all our children
// as our natural height, so we chain up to StWidget (which
// then calls BoxLayout), but we only request the showApps
// button as the minimum size
let [, natHeight] = this.layout_manager.get_preferred_height(this, forWidth);
let themeNode = this.get_theme_node();
let [, showAppsButton] = this.get_children();
let [minHeight, ] = showAppsButton.get_preferred_height(forWidth);
return [minHeight, natHeight];
}
});
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.Bin {
_init(remoteModel, monitorIndex) {
// Initialize icon variables and size
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._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._ensureAppIconVisibilityTimeoutId = 0;
this._labelShowing = false;
this._container = new MyDashActor();
this._scrollView = new St.ScrollView({
name: 'dashtodockDashScrollview',
hscrollbar_policy: Gtk.PolicyType.NEVER,
vscrollbar_policy: Gtk.PolicyType.NEVER,
enable_mouse_scrolling: false
});
this._scrollView.connect('scroll-event', this._onScrollEvent.bind(this));
this._box = new St.BoxLayout({
vertical: !this._isHorizontal,
clip_to_allocation: false,
x_align: Clutter.ActorAlign.START,
y_align: Clutter.ActorAlign.START
});
this._box._delegate = this;
this._container.add_actor(this._scrollView);
this._scrollView.add_actor(this._box);
// Create a wrapper around the real showAppsIcon in order to add a popupMenu.
this._showAppsIcon = new AppIcons.MyShowAppsIcon();
this._showAppsIcon.show();
this._showAppsIcon.icon.setIconSize(this.iconSize);
this._hookUpLabel(this._showAppsIcon);
this._showAppsIcon.connect('menu-state-changed', (_icon, opened) => {
this._itemMenuStateChanged(this._showAppsIcon, opened);
});
this._container.add_actor(this._showAppsIcon);
let rtl = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL;
super._init({
child: this._container,
y_align: St.Align.START,
x_align: rtl ? St.Align.END : St.Align.START
});
if (this._isHorizontal) {
this.connect('notify::width', () => {
if (this._maxHeight != this.width)
this._queueRedisplay();
this._maxHeight = this.width;
});
}
else {
this.connect('notify::height', () => {
if (this._maxHeight != this.height)
this._queueRedisplay();
this._maxHeight = this.height;
});
}
// Update minimization animation target position on allocation of the
// container and on scrollview change.
this._box.connect('notify::allocation', this._updateAppsIconGeometry.bind(this));
let scrollViewAdjustment = this._isHorizontal ? this._scrollView.hscroll.adjustment : this._scrollView.vscroll.adjustment;
scrollViewAdjustment.connect('notify::value', this._updateAppsIconGeometry.bind(this));
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._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._onDragBegin.bind(this)
], [
Main.overview,
'item-drag-end',
this._onDragEnd.bind(this)
], [
Main.overview,
'item-drag-cancelled',
this._onDragCancelled.bind(this)
]);
this.connect('destroy', this._onDestroy.bind(this));
}
_onDestroy() {
this._signalsHandler.destroy();
}
_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) {
Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId);
this._ensureAppIconVisibilityTimeoutId = 0;
}
// Skip to avoid double events mouse
if (event.is_pointer_emulated())
return Clutter.EVENT_STOP;
let adjustment, delta;
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;
switch (event.get_scroll_direction()) {
case Clutter.ScrollDirection.UP:
delta = -increment;
break;
case Clutter.ScrollDirection.DOWN:
delta = +increment;
break;
case Clutter.ScrollDirection.SMOOTH:
let [dx, dy] = event.get_scroll_delta();
delta = dy * increment;
// Also consider horizontal component, for instance touchpad
if (this._isHorizontal)
delta += dx * increment;
break;
}
adjustment.set_value(adjustment.get_value() + delta);
return Clutter.EVENT_STOP;
}
_onDragBegin() {
this._dragCancelled = false;
this._dragMonitor = {
dragMotion: this._onDragMotion.bind(this)
};
DND.addDragMonitor(this._dragMonitor);
if (this._box.get_n_children() == 0) {
this._emptyDropTarget = new Dash.EmptyDropTargetItem();
this._box.insert_child_at_index(this._emptyDropTarget, 0);
this._emptyDropTarget.show(true);
}
}
_onDragCancelled() {
this._dragCancelled = true;
this._endDrag();
}
_onDragEnd() {
if (this._dragCancelled)
return;
this._endDrag();
}
_endDrag() {
this._clearDragPlaceholder();
this._clearEmptyDropTarget();
this._showAppsIcon.setDragApp(null);
DND.removeDragMonitor(this._dragMonitor);
}
_onDragMotion(dragEvent) {
let app = Dash.getAppFromSource(dragEvent.source);
if (app == null)
return DND.DragMotionResult.CONTINUE;
let showAppsHovered = this._showAppsIcon.contains(dragEvent.targetActor);
if (!this._box.contains(dragEvent.targetActor) || showAppsHovered)
this._clearDragPlaceholder();
if (showAppsHovered)
this._showAppsIcon.setDragApp(app);
else
this._showAppsIcon.setDragApp(null);
return DND.DragMotionResult.CONTINUE;
}
_appIdListToHash(apps) {
let ids = {};
for (let i = 0; i < apps.length; i++)
ids[apps[i].get_id()] = apps[i];
return ids;
}
_queueRedisplay() {
Main.queueDeferredWork(this._workId);
}
_hookUpLabel(item, appIcon) {
item.child.connect('notify::hover', () => {
this._syncLabel(item, appIcon);
});
let id = Main.overview.connect('hiding', () => {
this._labelShowing = false;
item.hideLabel();
});
item.child.connect('destroy', function() {
Main.overview.disconnect(id);
});
if (appIcon) {
appIcon.connect('sync-tooltip', () => {
this._syncLabel(item, appIcon);
});
}
}
_createAppItem(app) {
let appIcon = new AppIcons.MyAppIcon(this._remoteModel, app,
this._monitorIndex,
{ setSizeManually: true,
showLabel: false });
if (appIcon._draggable) {
appIcon._draggable.connect('drag-begin', () => {
appIcon.actor.opacity = 50;
});
appIcon._draggable.connect('drag-end', () => {
appIcon.actor.opacity = 255;
});
}
appIcon.connect('menu-state-changed', (appIcon, opened) => {
this._itemMenuStateChanged(item, opened);
});
let item = new MyDashItemContainer();
item.setChild(appIcon.actor);
appIcon.actor.connect('notify::hover', () => {
if (appIcon.actor.hover) {
this._ensureAppIconVisibilityTimeoutId = Mainloop.timeout_add(100, () => {
ensureActorVisibleInScrollView(this._scrollView, appIcon.actor);
this._ensureAppIconVisibilityTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
}
else {
if (this._ensureAppIconVisibilityTimeoutId > 0) {
Mainloop.source_remove(this._ensureAppIconVisibilityTimeoutId);
this._ensureAppIconVisibilityTimeoutId = 0;
}
}
});
appIcon.actor.connect('clicked', (actor) => {
ensureActorVisibleInScrollView(this._scrollView, actor);
});
appIcon.actor.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.actor.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._delegate &&
actor.child._delegate.icon &&
!actor.animatingOut;
});
let appIcons = iconChildren.map(function(actor) {
return actor.child._delegate;
});
return appIcons;
}
_updateAppsIconGeometry() {
let appIcons = this.getAppIcons();
appIcons.forEach(function(icon) {
icon.updateIconGeometry();
});
}
_itemMenuStateChanged(item, opened) {
// When the menu closes, it calls sync_hover, which means
// that the notify::hover handler does everything we need to.
if (opened) {
if (this._showLabelTimeoutId > 0) {
Mainloop.source_remove(this._showLabelTimeoutId);
this._showLabelTimeoutId = 0;
}
item.label.opacity = 0;
item.label.hide();
}
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');
}
}
_syncLabel(item, appIcon) {
let shouldShow = appIcon ? appIcon.shouldShowTooltip() : item.child.get_hover();
if (shouldShow) {
if (this._showLabelTimeoutId == 0) {
let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT;
this._showLabelTimeoutId = Mainloop.timeout_add(timeout, () => {
this._labelShowing = true;
item.showLabel();
this._showLabelTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._showLabelTimeoutId, '[gnome-shell] item.showLabel');
if (this._resetHoverTimeoutId > 0) {
Mainloop.source_remove(this._resetHoverTimeoutId);
this._resetHoverTimeoutId = 0;
}
}
}
else {
if (this._showLabelTimeoutId > 0)
Mainloop.source_remove(this._showLabelTimeoutId);
this._showLabelTimeoutId = 0;
item.hideLabel();
if (this._labelShowing) {
this._resetHoverTimeoutId = Mainloop.timeout_add(DASH_ITEM_HOVER_TIMEOUT, () => {
this._labelShowing = false;
this._resetHoverTimeoutId = 0;
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing');
}
}
}
_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(function(actor) {
return actor.child &&
actor.child._delegate &&
actor.child._delegate.icon &&
!actor.animatingOut;
});
iconChildren.push(this._showAppsIcon);
if (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;
let themeNode = this._container.get_theme_node();
let maxAllocation = new Clutter.ActorBox({
x1: 0,
y1: 0,
x2: this._isHorizontal ? this._maxHeight : 42 /* whatever */,
y2: this._isHorizontal ? 42 : this._maxHeight
});
let maxContent = themeNode.get_content_box(maxAllocation);
let availHeight;
if (this._isHorizontal)
availHeight = maxContent.x2 - maxContent.x1;
else
availHeight = maxContent.y2 - maxContent.y1;
let spacing = themeNode.get_length('spacing');
let firstButton = iconChildren[0].child;
let firstIcon = firstButton._delegate.icon;
let minHeight, natHeight, minWidth, natWidth;
// Enforce the current icon size during the size request
firstIcon.setIconSize(this.iconSize);
[minHeight, natHeight] = firstButton.get_preferred_height(-1);
[minWidth, natWidth] = firstButton.get_preferred_width(-1);
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
let iconSizes = this._availableIconSizes.map(function(s) {
return s * scaleFactor;
});
// Subtract icon padding and box spacing from the available height
if (this._isHorizontal)
availHeight -= iconChildren.length * (natWidth - this.iconSize * scaleFactor) +
(iconChildren.length - 1) * spacing;
else
availHeight -= iconChildren.length * (natHeight - this.iconSize * scaleFactor) +
(iconChildren.length - 1) * spacing;
let availSize = availHeight / iconChildren.length;
let newIconSize = this._availableIconSizes[0];
for (let i = 0; i < iconSizes.length; i++) {
if (iconSizes[i] < availSize)
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, or when initially filling
// the dash
if (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.remove_all_transitions();
icon.icon.ease({
width: targetWidth,
height: targetHeight,
time: 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(function(actor) {
return actor.child &&
actor.child._delegate &&
actor.child._delegate.app;
});
// Apps currently in the dash
let oldApps = children.map(function(actor) {
return 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]);
}
// We reorder the running apps so that they don't change position on the
// dash with every redisplay() call
if (settings.get_boolean('show-running')) {
// First: add the apps from the oldApps list that are still running
for (let i = 0; i < oldApps.length; i++) {
let index = running.indexOf(oldApps[i]);
if (index > -1) {
let app = running.splice(index, 1)[0];
if (settings.get_boolean('show-favorites') && (app.get_id() in favorites))
continue;
newApps.push(app);
}
}
// Second: add the new apps
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)) {
// No change at oldIndex/newIndex
if (oldApps[oldIndex] && oldApps[oldIndex] == newApps[newIndex]) {
oldIndex++;
newIndex++;
continue;
}
// App removed at oldIndex
if (oldApps[oldIndex] && (newApps.indexOf(oldApps[oldIndex]) == -1)) {
removedActors.push(children[oldIndex]);
oldIndex++;
continue;
}
// App added at newIndex
if (newApps[newIndex] && (oldApps.indexOf(newApps[newIndex]) == -1)) {
let newItem = this._createAppItem(newApps[newIndex]);
addedItems.push({ app: newApps[newIndex],
item: newItem,
pos: newIndex });
newIndex++;
continue;
}
// App moved
let insertHere = newApps[newIndex + 1] && (newApps[newIndex + 1] == oldApps[oldIndex]);
let alreadyRemoved = removedActors.reduce(function(result, actor) {
let removedApp = actor.child._delegate.app;
return result || removedApp == newApps[newIndex];
}, false);
if (insertHere || alreadyRemoved) {
let newItem = this._createAppItem(newApps[newIndex]);
addedItems.push({
app: newApps[newIndex],
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
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);
// 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 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 mantain 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._delegate &&
actor.child._delegate.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();
}
_clearDragPlaceholder() {
if (this._dragPlaceholder) {
this._animatingPlaceholdersCount++;
this._dragPlaceholder.animateOutAndDestroy();
this._dragPlaceholder.connect('destroy', () => {
this._animatingPlaceholdersCount--;
});
this._dragPlaceholder = null;
}
this._dragPlaceholderPos = -1;
}
_clearEmptyDropTarget() {
if (this._emptyDropTarget) {
this._emptyDropTarget.animateOutAndDestroy();
this._emptyDropTarget = null;
}
}
handleDragOver(source, actor, x, y, time) {
let app = Dash.getAppFromSource(source);
// Don't allow favoriting of transient apps
if (app == null || app.is_window_backed())
return DND.DragMotionResult.NO_DROP;
if (!this._shellSettings.is_writable('favorite-apps') ||
!Docking.DockManager.settings.get_boolean('show-favorites'))
return DND.DragMotionResult.NO_DROP;
let favorites = AppFavorites.getAppFavorites().getFavorites();
let numFavorites = favorites.length;
let favPos = favorites.indexOf(app);
let children = this._box.get_children();
let numChildren = children.length;
let boxHeight = 0;
for (let i = 0; i < numChildren; i++)
boxHeight += this._isHorizontal?children[i].width:children[i].height;
// Keep the placeholder out of the index calculation; assuming that
// the remove target has the same size as "normal" items, we don't
// need to do the same adjustment there.
if (this._dragPlaceholder) {
boxHeight -= this._isHorizontal?this._dragPlaceholder.width:this._dragPlaceholder.height;
numChildren--;
}
let pos;
if (!this._emptyDropTarget) {
pos = Math.floor((this._isHorizontal?x:y) * numChildren / boxHeight);
if (pos > numChildren)
pos = numChildren;
}
else
pos = 0; // always insert at the top when dash is empty
// Take into account childredn position in rtl
if (this._isHorizontal && (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL))
pos = numChildren - pos;
if ((pos != this._dragPlaceholderPos) && (pos <= numFavorites) && (this._animatingPlaceholdersCount == 0)) {
this._dragPlaceholderPos = pos;
// Don't allow positioning before or after self
if ((favPos != -1) && (pos == favPos || pos == favPos + 1)) {
this._clearDragPlaceholder();
return DND.DragMotionResult.CONTINUE;
}
// If the placeholder already exists, we just move
// it, but if we are adding it, expand its size in
// an animation
let fadeIn;
if (this._dragPlaceholder) {
this._dragPlaceholder.destroy();
fadeIn = false;
}
else
fadeIn = true;
this._dragPlaceholder = new Dash.DragPlaceholderItem();
this._dragPlaceholder.child.set_width (this.iconSize);
this._dragPlaceholder.child.set_height (this.iconSize / 2);
this._box.insert_child_at_index(this._dragPlaceholder,
this._dragPlaceholderPos);
this._dragPlaceholder.show(fadeIn);
// Ensure the next and previous icon are visible when moving the placeholder
// (I assume there's room for both of them)
if (this._dragPlaceholderPos > 1)
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]);
}
// Remove the drag placeholder if we are not in the
// "favorites zone"
if (pos > numFavorites)
this._clearDragPlaceholder();
if (!this._dragPlaceholder)
return DND.DragMotionResult.NO_DROP;
let srcIsFavorite = (favPos != -1);
if (srcIsFavorite)
return DND.DragMotionResult.MOVE_DROP;
return DND.DragMotionResult.COPY_DROP;
}
/**
* Draggable target interface
*/
acceptDrop(source, actor, x, y, time) {
let app = Dash.getAppFromSource(source);
// Don't allow favoriting of transient apps
if (app == null || app.is_window_backed())
return false;
if (!this._shellSettings.is_writable('favorite-apps') ||
!Docking.DockManager.settings.get_boolean('show-favorites'))
return false;
let id = app.get_id();
let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
let srcIsFavorite = (id in favorites);
let favPos = 0;
let children = this._box.get_children();
for (let i = 0; i < this._dragPlaceholderPos; i++) {
if (this._dragPlaceholder && (children[i] == this._dragPlaceholder))
continue;
let childId = children[i].child._delegate.app.get_id();
if (childId == id)
continue;
if (childId in favorites)
favPos++;
}
// No drag placeholder means we don't wan't to favorite the app
// and we are dragging it to its original position
if (!this._dragPlaceholder)
return true;
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
let appFavorites = AppFavorites.getAppFavorites();
if (srcIsFavorite)
appFavorites.moveFavoriteToPos(id, favPos);
else
appFavorites.addFavoriteAtPos(id, favPos);
return false;
});
return true;
}
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()
this.showAppsButton.set_width(0)
this.showAppsButton.set_height(0)
}
});
/**
* 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];
}