6487cd8c6f
Having the tooltip change when it's visible looks strange and glitchy. This also makes sure that "Remove from Favorites" doesn't change, even when the user removes their mouse cursor from it. https://bugzilla.gnome.org/show_bug.cgi?id=685313
907 lines
31 KiB
JavaScript
907 lines
31 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Signals = imports.signals;
|
|
const Lang = imports.lang;
|
|
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 DND = imports.ui.dnd;
|
|
const IconGrid = imports.ui.iconGrid;
|
|
const Main = imports.ui.main;
|
|
const Tweener = imports.ui.tweener;
|
|
const Workspace = imports.ui.workspace;
|
|
|
|
const DASH_ANIMATION_TIME = 0.2;
|
|
const DASH_ITEM_LABEL_SHOW_TIME = 0.15;
|
|
const DASH_ITEM_LABEL_HIDE_TIME = 0.1;
|
|
const DASH_ITEM_HOVER_TIMEOUT = 300;
|
|
|
|
function getAppFromSource(source) {
|
|
if (source instanceof AppDisplay.AppWellIcon) {
|
|
let appSystem = Shell.AppSystem.get_default();
|
|
return appSystem.lookup_app(source.getId());
|
|
} else if (source.metaWindow) {
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
return tracker.get_window_app(source.metaWindow);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// A container like StBin, but taking the child's scale into account
|
|
// when requesting a size
|
|
const DashItemContainer = new Lang.Class({
|
|
Name: 'DashItemContainer',
|
|
|
|
_init: function() {
|
|
this.actor = new Shell.GenericContainer({ style_class: 'dash-item-container' });
|
|
this.actor.connect('get-preferred-width',
|
|
Lang.bind(this, this._getPreferredWidth));
|
|
this.actor.connect('get-preferred-height',
|
|
Lang.bind(this, this._getPreferredHeight));
|
|
this.actor.connect('allocate',
|
|
Lang.bind(this, this._allocate));
|
|
this.actor._delegate = this;
|
|
|
|
this._labelText = "";
|
|
this.label = new St.Label({ style_class: 'dash-label'});
|
|
this.label.hide();
|
|
Main.layoutManager.addChrome(this.label);
|
|
this.actor.label_actor = this.label;
|
|
|
|
this.child = null;
|
|
this._childScale = 1;
|
|
this._childOpacity = 255;
|
|
this.animatingOut = false;
|
|
},
|
|
|
|
_allocate: function(actor, box, flags) {
|
|
if (this.child == null)
|
|
return;
|
|
|
|
let availWidth = box.x2 - box.x1;
|
|
let availHeight = box.y2 - box.y1;
|
|
let [minChildWidth, minChildHeight, natChildWidth, natChildHeight] =
|
|
this.child.get_preferred_size();
|
|
let [childScaleX, childScaleY] = this.child.get_scale();
|
|
|
|
let childWidth = Math.min(natChildWidth * childScaleX, availWidth);
|
|
let childHeight = Math.min(natChildHeight * childScaleY, availHeight);
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
childBox.x1 = (availWidth - childWidth) / 2;
|
|
childBox.y1 = (availHeight - childHeight) / 2;
|
|
childBox.x2 = childBox.x1 + childWidth;
|
|
childBox.y2 = childBox.y1 + childHeight;
|
|
|
|
this.child.allocate(childBox, flags);
|
|
},
|
|
|
|
_getPreferredHeight: function(actor, forWidth, alloc) {
|
|
alloc.min_size = 0;
|
|
alloc.natural_size = 0;
|
|
|
|
if (this.child == null)
|
|
return;
|
|
|
|
let [minHeight, natHeight] = this.child.get_preferred_height(forWidth);
|
|
alloc.min_size += minHeight * this.child.scale_y;
|
|
alloc.natural_size += natHeight * this.child.scale_y;
|
|
},
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
alloc.min_size = 0;
|
|
alloc.natural_size = 0;
|
|
|
|
if (this.child == null)
|
|
return;
|
|
|
|
let [minWidth, natWidth] = this.child.get_preferred_width(forHeight);
|
|
alloc.min_size = minWidth * this.child.scale_y;
|
|
alloc.natural_size = natWidth * this.child.scale_y;
|
|
},
|
|
|
|
showLabel: function() {
|
|
if (!this._labelText)
|
|
return;
|
|
|
|
this.label.set_text(this._labelText);
|
|
this.label.opacity = 0;
|
|
this.label.show();
|
|
|
|
let [stageX, stageY] = this.actor.get_transformed_position();
|
|
|
|
let itemHeight = this.actor.allocation.y2 - this.actor.allocation.y1;
|
|
|
|
let labelHeight = this.label.get_height();
|
|
let yOffset = Math.floor((itemHeight - labelHeight) / 2)
|
|
|
|
let y = stageY + yOffset;
|
|
|
|
let node = this.label.get_theme_node();
|
|
let xOffset = node.get_length('-x-offset');
|
|
|
|
let x;
|
|
if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
|
|
x = stageX - this.label.get_width() - xOffset;
|
|
else
|
|
x = stageX + this.actor.get_width() + xOffset;
|
|
|
|
this.label.set_position(x, y);
|
|
Tweener.addTween(this.label,
|
|
{ opacity: 255,
|
|
time: DASH_ITEM_LABEL_SHOW_TIME,
|
|
transition: 'easeOutQuad',
|
|
});
|
|
},
|
|
|
|
setLabelText: function(text) {
|
|
this._labelText = text;
|
|
},
|
|
|
|
hideLabel: function () {
|
|
Tweener.addTween(this.label,
|
|
{ opacity: 0,
|
|
time: DASH_ITEM_LABEL_HIDE_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this, function() {
|
|
this.label.hide();
|
|
})
|
|
});
|
|
},
|
|
|
|
setChild: function(actor) {
|
|
if (this.child == actor)
|
|
return;
|
|
|
|
this.actor.destroy_all_children();
|
|
|
|
this.child = actor;
|
|
this.actor.add_actor(this.child);
|
|
},
|
|
|
|
animateIn: function() {
|
|
if (this.child == null)
|
|
return;
|
|
|
|
this.childScale = 0;
|
|
this.childOpacity = 0;
|
|
Tweener.addTween(this,
|
|
{ childScale: 1.0,
|
|
childOpacity: 255,
|
|
time: DASH_ANIMATION_TIME,
|
|
transition: 'easeOutQuad'
|
|
});
|
|
},
|
|
|
|
destroy: function() {
|
|
if (this.label)
|
|
this.label.destroy();
|
|
|
|
this.actor.destroy();
|
|
},
|
|
|
|
animateOutAndDestroy: function() {
|
|
if (this.label)
|
|
this.label.destroy();
|
|
|
|
if (this.child == null) {
|
|
this.actor.destroy();
|
|
return;
|
|
}
|
|
|
|
this.animatingOut = true;
|
|
this.childScale = 1.0;
|
|
Tweener.addTween(this,
|
|
{ childScale: 0.0,
|
|
childOpacity: 0,
|
|
time: DASH_ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this, function() {
|
|
this.actor.destroy();
|
|
})
|
|
});
|
|
},
|
|
|
|
set childScale(scale) {
|
|
this._childScale = scale;
|
|
|
|
if (this.child == null)
|
|
return;
|
|
|
|
this.child.set_scale_with_gravity(scale, scale,
|
|
Clutter.Gravity.CENTER);
|
|
this.actor.queue_relayout();
|
|
},
|
|
|
|
get childScale() {
|
|
return this._childScale;
|
|
},
|
|
|
|
set childOpacity(opacity) {
|
|
this._childOpacity = opacity;
|
|
|
|
if (this.child == null)
|
|
return;
|
|
|
|
this.child.set_opacity(opacity);
|
|
this.actor.queue_redraw();
|
|
},
|
|
|
|
get childOpacity() {
|
|
return this._childOpacity;
|
|
}
|
|
});
|
|
|
|
const ShowAppsIcon = new Lang.Class({
|
|
Name: 'ShowAppsIcon',
|
|
Extends: DashItemContainer,
|
|
|
|
_init: function() {
|
|
this.parent();
|
|
|
|
this.toggleButton = new St.Button({ style_class: 'show-apps',
|
|
track_hover: true,
|
|
can_focus: true,
|
|
toggle_mode: true });
|
|
this._iconActor = null;
|
|
this.icon = new IconGrid.BaseIcon(_("Show Applications"),
|
|
{ setSizeManually: true,
|
|
showLabel: false,
|
|
createIcon: Lang.bind(this, this._createIcon) });
|
|
this.toggleButton.add_actor(this.icon.actor);
|
|
this.toggleButton._delegate = this;
|
|
|
|
this.setChild(this.toggleButton);
|
|
this.setDragApp(null);
|
|
this.toggleButton.label_actor = this.label;
|
|
},
|
|
|
|
_createIcon: function(size) {
|
|
this._iconActor = new St.Icon({ icon_name: 'view-grid-symbolic',
|
|
icon_size: size,
|
|
style_class: 'show-apps-icon',
|
|
track_hover: true });
|
|
return this._iconActor;
|
|
},
|
|
|
|
_canRemoveApp: function(app) {
|
|
if (app == null)
|
|
return false;
|
|
|
|
let id = app.get_id();
|
|
let isFavorite = AppFavorites.getAppFavorites().isFavorite(id);
|
|
return isFavorite;
|
|
},
|
|
|
|
setDragApp: function(app) {
|
|
let canRemove = this._canRemoveApp(app);
|
|
|
|
this.toggleButton.set_hover(canRemove);
|
|
if (this._iconActor)
|
|
this._iconActor.set_hover(canRemove);
|
|
|
|
if (canRemove)
|
|
this.setLabelText(_("Remove from Favorites"));
|
|
else
|
|
this.setLabelText(_("Show Applications"));
|
|
},
|
|
|
|
handleDragOver: function(source, actor, x, y, time) {
|
|
let app = getAppFromSource(source);
|
|
if (app == null)
|
|
return DND.DragMotionResult.NO_DROP;
|
|
|
|
let id = app.get_id();
|
|
let isFavorite = AppFavorites.getAppFavorites().isFavorite(id);
|
|
if (!isFavorite)
|
|
return DND.DragMotionResult.NO_DROP;
|
|
|
|
return DND.DragMotionResult.MOVE_DROP;
|
|
},
|
|
|
|
acceptDrop: function(source, actor, x, y, time) {
|
|
let app = getAppFromSource(source);
|
|
if (app == null)
|
|
return false;
|
|
|
|
let id = app.get_id();
|
|
|
|
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this,
|
|
function () {
|
|
AppFavorites.getAppFavorites().removeFavorite(id);
|
|
return false;
|
|
}));
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
const DragPlaceholderItem = new Lang.Class({
|
|
Name: 'DragPlaceholderItem',
|
|
Extends: DashItemContainer,
|
|
|
|
_init: function() {
|
|
this.parent();
|
|
this.setChild(new St.Bin({ style_class: 'placeholder' }));
|
|
}
|
|
});
|
|
|
|
const DashActor = new Lang.Class({
|
|
Name: 'DashActor',
|
|
Extends: St.Widget,
|
|
|
|
_init: function() {
|
|
let layout = new Clutter.BoxLayout({ orientation: Clutter.Orientation.VERTICAL });
|
|
this.parent({ name: 'dash',
|
|
layout_manager: layout,
|
|
clip_to_allocation: true });
|
|
},
|
|
|
|
vfunc_allocate: function(box, flags) {
|
|
let contentBox = this.get_theme_node().get_content_box(box);
|
|
let availWidth = contentBox.x2 - contentBox.x1;
|
|
|
|
this.set_allocation(box, flags);
|
|
|
|
let [appIcons, showAppsButton] = this.get_children();
|
|
let [showAppsMinHeight, showAppsNatHeight] = showAppsButton.get_preferred_height(availWidth);
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
childBox.x1 = contentBox.x1;
|
|
childBox.y1 = contentBox.y1;
|
|
childBox.x2 = contentBox.x2;
|
|
childBox.y2 = contentBox.y2 - showAppsNatHeight;
|
|
appIcons.allocate(childBox, flags);
|
|
|
|
childBox.y1 = contentBox.y2 - showAppsNatHeight;
|
|
childBox.y2 = contentBox.y2;
|
|
showAppsButton.allocate(childBox, flags);
|
|
}
|
|
});
|
|
|
|
const Dash = new Lang.Class({
|
|
Name: 'Dash',
|
|
|
|
_init : function() {
|
|
this._maxHeight = -1;
|
|
this.iconSize = 64;
|
|
this._shownInitially = false;
|
|
|
|
this._dragPlaceholder = null;
|
|
this._dragPlaceholderPos = -1;
|
|
this._animatingPlaceholdersCount = 0;
|
|
this._showLabelTimeoutId = 0;
|
|
this._resetHoverTimeoutId = 0;
|
|
this._labelShowing = false;
|
|
|
|
this._container = new DashActor();
|
|
this._box = new St.BoxLayout({ vertical: true,
|
|
clip_to_allocation: true });
|
|
this._box._delegate = this;
|
|
this._container.add_actor(this._box);
|
|
|
|
this._showAppsIcon = new ShowAppsIcon();
|
|
this._showAppsIcon.icon.setIconSize(this.iconSize);
|
|
this._hookUpLabel(this._showAppsIcon);
|
|
|
|
this.showAppsButton = this._showAppsIcon.toggleButton;
|
|
|
|
this._container.add_actor(this._showAppsIcon.actor);
|
|
|
|
this.actor = new St.Bin({ child: this._container });
|
|
this.actor.connect('notify::height', Lang.bind(this,
|
|
function() {
|
|
if (this._maxHeight != this.actor.height)
|
|
this._queueRedisplay();
|
|
this._maxHeight = this.actor.height;
|
|
}));
|
|
|
|
this._workId = Main.initializeDeferredWork(this._box, Lang.bind(this, this._redisplay));
|
|
|
|
this._appSystem = Shell.AppSystem.get_default();
|
|
|
|
this._appSystem.connect('installed-changed', Lang.bind(this, this._queueRedisplay));
|
|
AppFavorites.getAppFavorites().connect('changed', Lang.bind(this, this._queueRedisplay));
|
|
this._appSystem.connect('app-state-changed', Lang.bind(this, this._queueRedisplay));
|
|
|
|
Main.overview.connect('item-drag-begin',
|
|
Lang.bind(this, this._onDragBegin));
|
|
Main.overview.connect('item-drag-end',
|
|
Lang.bind(this, this._onDragEnd));
|
|
Main.overview.connect('item-drag-cancelled',
|
|
Lang.bind(this, this._onDragCancelled));
|
|
Main.overview.connect('window-drag-begin',
|
|
Lang.bind(this, this._onDragBegin));
|
|
Main.overview.connect('window-drag-cancelled',
|
|
Lang.bind(this, this._onDragCancelled));
|
|
Main.overview.connect('window-drag-end',
|
|
Lang.bind(this, this._onDragEnd));
|
|
},
|
|
|
|
_onDragBegin: function() {
|
|
this._dragCancelled = false;
|
|
this._dragMonitor = {
|
|
dragMotion: Lang.bind(this, this._onDragMotion)
|
|
};
|
|
DND.addDragMonitor(this._dragMonitor);
|
|
},
|
|
|
|
_onDragCancelled: function() {
|
|
this._dragCancelled = true;
|
|
this._endDrag();
|
|
},
|
|
|
|
_onDragEnd: function() {
|
|
if (this._dragCancelled)
|
|
return;
|
|
|
|
this._endDrag();
|
|
},
|
|
|
|
_endDrag: function() {
|
|
this._clearDragPlaceholder();
|
|
DND.removeDragMonitor(this._dragMonitor);
|
|
},
|
|
|
|
_onDragMotion: function(dragEvent) {
|
|
let app = getAppFromSource(dragEvent.source);
|
|
if (app == null)
|
|
return DND.DragMotionResult.CONTINUE;
|
|
|
|
let showAppsHovered =
|
|
this._showAppsIcon.actor.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: function(apps) {
|
|
let ids = {};
|
|
for (let i = 0; i < apps.length; i++)
|
|
ids[apps[i].get_id()] = apps[i];
|
|
return ids;
|
|
},
|
|
|
|
_queueRedisplay: function () {
|
|
Main.queueDeferredWork(this._workId);
|
|
},
|
|
|
|
_hookUpLabel: function(item) {
|
|
item.child.connect('notify::hover', Lang.bind(this, function() {
|
|
this._onHover(item);
|
|
}));
|
|
|
|
Main.overview.connect('hiding', Lang.bind(this, function() {
|
|
this._labelShowing = false;
|
|
item.hideLabel();
|
|
}));
|
|
},
|
|
|
|
_createAppItem: function(app) {
|
|
let display = new AppDisplay.AppWellIcon(app,
|
|
{ setSizeManually: true,
|
|
showLabel: false });
|
|
display._draggable.connect('drag-begin',
|
|
Lang.bind(this, function() {
|
|
display.actor.opacity = 50;
|
|
}));
|
|
display._draggable.connect('drag-end',
|
|
Lang.bind(this, function() {
|
|
display.actor.opacity = 255;
|
|
}));
|
|
|
|
let item = new DashItemContainer();
|
|
item.setChild(display.actor);
|
|
|
|
item.setLabelText(app.get_name());
|
|
// Override default AppWellIcon label_actor
|
|
display.actor.label_actor = item.label;
|
|
display.icon.setIconSize(this.iconSize);
|
|
this._hookUpLabel(item);
|
|
|
|
return item;
|
|
},
|
|
|
|
_onHover: function (item) {
|
|
if (item.child.get_hover() && !item.child._delegate.isMenuUp) {
|
|
if (this._showLabelTimeoutId == 0) {
|
|
let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT;
|
|
this._showLabelTimeoutId = Mainloop.timeout_add(timeout,
|
|
Lang.bind(this, function() {
|
|
this._labelShowing = true;
|
|
item.showLabel();
|
|
return false;
|
|
}));
|
|
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,
|
|
Lang.bind(this, function() {
|
|
this._labelShowing = false;
|
|
return false;
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
_adjustIconSize: function() {
|
|
// 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._delegate.child &&
|
|
actor._delegate.child._delegate &&
|
|
actor._delegate.child._delegate.icon &&
|
|
!actor._delegate.animatingOut;
|
|
});
|
|
|
|
iconChildren.push(this._showAppsIcon.actor);
|
|
|
|
if (this._maxHeight == -1)
|
|
return;
|
|
|
|
let themeNode = this._container.get_theme_node();
|
|
let maxAllocation = new Clutter.ActorBox({ x1: 0, y1: 0,
|
|
x2: 42 /* whatever */,
|
|
y2: this._maxHeight });
|
|
let maxContent = themeNode.get_content_box(maxAllocation);
|
|
let availHeight = maxContent.y2 - maxContent.y1;
|
|
let spacing = themeNode.get_length('spacing');
|
|
|
|
|
|
let firstIcon = iconChildren[0]._delegate.child._delegate.icon;
|
|
|
|
let minHeight, natHeight;
|
|
|
|
// Enforce the current icon size during the size request if
|
|
// the icon is animating
|
|
if (firstIcon._animating) {
|
|
let [currentWidth, currentHeight] = firstIcon.icon.get_size();
|
|
|
|
firstIcon.icon.set_size(this.iconSize, this.iconSize);
|
|
[minHeight, natHeight] = iconChildren[0].get_preferred_height(-1);
|
|
|
|
firstIcon.icon.set_size(currentWidth, currentHeight);
|
|
} else {
|
|
[minHeight, natHeight] = iconChildren[0].get_preferred_height(-1);
|
|
}
|
|
|
|
// Subtract icon padding and box spacing from the available height
|
|
availHeight -= iconChildren.length * (natHeight - this.iconSize) +
|
|
(iconChildren.length - 1) * spacing;
|
|
|
|
let availSize = availHeight / iconChildren.length;
|
|
|
|
let iconSizes = [ 16, 22, 24, 32, 48, 64 ];
|
|
|
|
let newIconSize = 16;
|
|
for (let i = 0; i < iconSizes.length; i++) {
|
|
if (iconSizes[i] < availSize)
|
|
newIconSize = iconSizes[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]._delegate.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 not visible or when initially filling the dash
|
|
if (!Main.overview.visible || !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._animating = true;
|
|
Tweener.addTween(icon.icon,
|
|
{ width: targetWidth,
|
|
height: targetHeight,
|
|
time: DASH_ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: function() {
|
|
icon._animating = false;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
_redisplay: function () {
|
|
let favorites = AppFavorites.getAppFavorites().getFavoriteMap();
|
|
|
|
let running = this._appSystem.get_running();
|
|
|
|
let children = this._box.get_children().filter(function(actor) {
|
|
return actor._delegate.child &&
|
|
actor._delegate.child._delegate &&
|
|
actor._delegate.child._delegate.app;
|
|
});
|
|
// Apps currently in the dash
|
|
let oldApps = children.map(function(actor) {
|
|
return actor._delegate.child._delegate.app;
|
|
});
|
|
// Apps supposed to be in the dash
|
|
let newApps = [];
|
|
|
|
for (let id in favorites)
|
|
newApps.push(favorites[id]);
|
|
|
|
for (let i = 0; i < running.length; i++) {
|
|
let app = running[i];
|
|
if (app.get_id() in favorites)
|
|
continue;
|
|
newApps.push(app);
|
|
}
|
|
|
|
// Figure out the actual changes to the list of items; we iterate
|
|
// over both the list of items currently in the dash and the list
|
|
// of items expected there, and collect additions and removals.
|
|
// Moves are both an addition and a removal, where the order of
|
|
// the operations depends on whether we encounter the position
|
|
// where the item has been added first or the one from where it
|
|
// was removed.
|
|
// There is an assumption that only one item is moved at a given
|
|
// time; when moving several items at once, everything will still
|
|
// end up at the right position, but there might be additional
|
|
// additions/removals (e.g. it might remove all the launchers
|
|
// and add them back in the new order even if a smaller set of
|
|
// additions and removals is possible).
|
|
// If above assumptions turns out to be a problem, we might need
|
|
// to use a more sophisticated algorithm, e.g. Longest Common
|
|
// Subsequence as used by diff.
|
|
let addedItems = [];
|
|
let removedActors = [];
|
|
|
|
let newIndex = 0;
|
|
let oldIndex = 0;
|
|
while (newIndex < newApps.length || oldIndex < oldApps.length) {
|
|
// No change at oldIndex/newIndex
|
|
if (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) {
|
|
addedItems.push({ app: newApps[newIndex],
|
|
item: this._createAppItem(newApps[newIndex]),
|
|
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._delegate.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.actor,
|
|
addedItems[i].pos);
|
|
|
|
for (let i = 0; i < removedActors.length; i++) {
|
|
let item = removedActors[i]._delegate;
|
|
|
|
// Don't animate item removal when the overview is hidden
|
|
if (Main.overview.visible)
|
|
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
|
|
if (!this._shownInitially) {
|
|
this._shownInitially = true;
|
|
return;
|
|
}
|
|
|
|
// Don't animate item addition when the overview is hidden
|
|
if (!Main.overview.visible)
|
|
return;
|
|
|
|
for (let i = 0; i < addedItems.length; i++)
|
|
addedItems[i].item.animateIn();
|
|
},
|
|
|
|
_clearDragPlaceholder: function() {
|
|
if (this._dragPlaceholder) {
|
|
this._dragPlaceholder.animateOutAndDestroy();
|
|
this._dragPlaceholder = null;
|
|
this._dragPlaceholderPos = -1;
|
|
}
|
|
},
|
|
|
|
handleDragOver : function(source, actor, x, y, time) {
|
|
let app = getAppFromSource(source);
|
|
|
|
// Don't allow favoriting of transient apps
|
|
if (app == null || app.is_window_backed())
|
|
return DND.DragMotionResult.NO_DROP;
|
|
|
|
let favorites = AppFavorites.getAppFavorites().getFavorites();
|
|
let numFavorites = favorites.length;
|
|
|
|
let favPos = favorites.indexOf(app);
|
|
|
|
let children = this._box.get_children();
|
|
let numChildren = children.length;
|
|
let boxHeight = this._box.height;
|
|
|
|
// Keep the placeholder out of the index calculation; assuming that
|
|
// the remove target has the same size as "normal" items, we don't
|
|
// need to do the same adjustment there.
|
|
if (this._dragPlaceholder) {
|
|
boxHeight -= this._dragPlaceholder.actor.height;
|
|
numChildren--;
|
|
}
|
|
|
|
let pos = Math.floor(y * numChildren / boxHeight);
|
|
|
|
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)) {
|
|
if (this._dragPlaceholder) {
|
|
this._dragPlaceholder.animateOutAndDestroy();
|
|
this._animatingPlaceholdersCount++;
|
|
this._dragPlaceholder.actor.connect('destroy',
|
|
Lang.bind(this, function() {
|
|
this._animatingPlaceholdersCount--;
|
|
}));
|
|
}
|
|
this._dragPlaceholder = null;
|
|
|
|
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.actor.destroy();
|
|
fadeIn = false;
|
|
} else {
|
|
fadeIn = true;
|
|
}
|
|
|
|
this._dragPlaceholder = new DragPlaceholderItem();
|
|
this._dragPlaceholder.child.set_width (this.iconSize);
|
|
this._dragPlaceholder.child.set_height (this.iconSize / 2);
|
|
this._box.insert_child_at_index(this._dragPlaceholder.actor,
|
|
this._dragPlaceholderPos);
|
|
if (fadeIn)
|
|
this._dragPlaceholder.animateIn();
|
|
}
|
|
|
|
// Remove the drag placeholder if we are not in the
|
|
// "favorites zone"
|
|
if (pos > numFavorites && this._dragPlaceholder) {
|
|
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 : function(source, actor, x, y, time) {
|
|
let app = getAppFromSource(source);
|
|
|
|
// Don't allow favoriting of transient apps
|
|
if (app == null || app.is_window_backed()) {
|
|
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.actor)
|
|
continue;
|
|
|
|
let childId = children[i]._delegate.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, Lang.bind(this,
|
|
function () {
|
|
let appFavorites = AppFavorites.getAppFavorites();
|
|
if (srcIsFavorite)
|
|
appFavorites.moveFavoriteToPos(id, favPos);
|
|
else
|
|
appFavorites.addFavoriteAtPos(id, favPos);
|
|
return false;
|
|
}));
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
Signals.addSignalMethods(Dash.prototype);
|