gnome-shell/js/ui/dash.js
Georges Basile Stavracas Neto c0a4d90847 overviewControls: Limit Dash height to 15% of the overview's
Back when the Dash was vertical, the size of each item was calculated
solely based on the available height. After making the Dash horizontal,
this was swapped by the available width. However, when the height of the
Dash decreases, the current code results in never scaling them up ever
again.

Fix that by making ControlsManagerLayout explicitly pass the maximum Dash
sizes. Remove the 'notify::width' handler that served the same purpose.

Fixes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/3651

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1638>
2021-02-11 22:31:50 +00:00

984 lines
32 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Dash */
const { Clutter, GLib, GObject,
Graphene, Meta, Shell, St } = imports.gi;
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 Overview = imports.ui.overview;
var DASH_ANIMATION_TIME = 200;
var DASH_ITEM_LABEL_SHOW_TIME = 150;
var DASH_ITEM_LABEL_HIDE_TIME = 100;
var DASH_ITEM_HOVER_TIMEOUT = 300;
function getAppFromSource(source) {
if (source instanceof AppDisplay.AppIcon)
return source.app;
else
return null;
}
var DashIcon = GObject.registerClass(
class DashIcon extends AppDisplay.AppIcon {
_init(app) {
super._init(app, {
setSizeManually: true,
showLabel: false,
});
}
popupMenu() {
super.popupMenu(St.Side.BOTTOM);
}
// Disable scale-n-fade methods used during DND by parent
scaleAndFade() {
}
undoScaleAndFade() {
}
handleDragOver() {
return DND.DragMotionResult.CONTINUE;
}
acceptDrop() {
return false;
}
});
// A container like StBin, but taking the child's scale into account
// when requesting a size
var DashItemContainer = GObject.registerClass(
class DashItemContainer extends St.Widget {
_init() {
super._init({
style_class: 'dash-item-container',
pivot_point: new Graphene.Point({ x: .5, y: .5 }),
layout_manager: new Clutter.BinLayout(),
scale_x: 0,
scale_y: 0,
opacity: 0,
x_expand: true,
x_align: Clutter.ActorAlign.CENTER,
});
this._labelText = "";
this.label = new St.Label({ style_class: 'dash-label' });
this.label.hide();
Main.layoutManager.addChrome(this.label);
this.label_actor = this.label;
this.child = null;
this.animatingOut = false;
this.connect('notify::scale-x', () => this.queue_relayout());
this.connect('notify::scale-y', () => this.queue_relayout());
this.connect('destroy', () => {
if (this.child != null)
this.child.destroy();
this.label.destroy();
});
}
vfunc_get_preferred_height(forWidth) {
let themeNode = this.get_theme_node();
forWidth = themeNode.adjust_for_width(forWidth);
let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth);
return themeNode.adjust_preferred_height(minHeight * this.scale_y,
natHeight * this.scale_y);
}
vfunc_get_preferred_width(forHeight) {
let themeNode = this.get_theme_node();
forHeight = themeNode.adjust_for_height(forHeight);
let [minWidth, natWidth] = super.vfunc_get_preferred_width(forHeight);
return themeNode.adjust_preferred_width(minWidth * this.scale_x,
natWidth * this.scale_x);
}
showLabel() {
if (!this._labelText)
return;
this.label.set_text(this._labelText);
this.label.opacity = 0;
this.label.show();
let [stageX, stageY] = this.get_transformed_position();
const itemWidth = this.allocation.get_width();
const labelWidth = this.label.get_width();
const xOffset = Math.floor((itemWidth - labelWidth) / 2);
const x = Math.clamp(stageX + xOffset, 0, global.stage.width - labelWidth);
let node = this.label.get_theme_node();
const yOffset = node.get_length('-y-offset');
const y = stageY - this.label.height - yOffset;
this.label.set_position(x, y);
this.label.ease({
opacity: 255,
duration: DASH_ITEM_LABEL_SHOW_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
setLabelText(text) {
this._labelText = text;
this.child.accessible_name = text;
}
hideLabel() {
this.label.ease({
opacity: 0,
duration: DASH_ITEM_LABEL_HIDE_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this.label.hide(),
});
}
setChild(actor) {
if (this.child == actor)
return;
this.destroy_all_children();
this.child = actor;
this.child.y_expand = true;
this.add_actor(this.child);
}
show(animate) {
if (this.child == null)
return;
let time = animate ? DASH_ANIMATION_TIME : 0;
this.ease({
scale_x: 1,
scale_y: 1,
opacity: 255,
duration: time,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
animateOutAndDestroy() {
this.label.hide();
if (this.child == null) {
this.destroy();
return;
}
this.animatingOut = true;
this.ease({
scale_x: 0,
scale_y: 0,
opacity: 0,
duration: DASH_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this.destroy(),
});
}
});
var ShowAppsIcon = GObject.registerClass(
class ShowAppsIcon extends DashItemContainer {
_init() {
super._init();
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: this._createIcon.bind(this) });
this.icon.y_align = Clutter.ActorAlign.CENTER;
this.toggleButton.add_actor(this.icon);
this.toggleButton._delegate = this;
this.setChild(this.toggleButton);
this.setDragApp(null);
}
_createIcon(size) {
this._iconActor = new St.Icon({ icon_name: 'view-app-grid-symbolic',
icon_size: size,
style_class: 'show-apps-icon',
track_hover: true });
return this._iconActor;
}
_canRemoveApp(app) {
if (app == null)
return false;
if (!global.settings.is_writable('favorite-apps'))
return false;
let id = app.get_id();
let isFavorite = AppFavorites.getAppFavorites().isFavorite(id);
return isFavorite;
}
setDragApp(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(source, _actor, _x, _y, _time) {
if (!this._canRemoveApp(getAppFromSource(source)))
return DND.DragMotionResult.NO_DROP;
return DND.DragMotionResult.MOVE_DROP;
}
acceptDrop(source, _actor, _x, _y, _time) {
let app = getAppFromSource(source);
if (!this._canRemoveApp(app))
return false;
let id = app.get_id();
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
AppFavorites.getAppFavorites().removeFavorite(id);
return false;
});
return true;
}
});
var DragPlaceholderItem = GObject.registerClass(
class DragPlaceholderItem extends DashItemContainer {
_init() {
super._init();
this.setChild(new St.Bin({ style_class: 'placeholder' }));
}
});
var EmptyDropTargetItem = GObject.registerClass(
class EmptyDropTargetItem extends DashItemContainer {
_init() {
super._init();
this.setChild(new St.Bin({ style_class: 'empty-dash-drop-target' }));
}
});
const DashIconsLayout = GObject.registerClass(
class DashIconsLayout extends Clutter.BoxLayout {
_init() {
super._init({
orientation: Clutter.Orientation.HORIZONTAL,
});
}
vfunc_get_preferred_width(container, forHeight) {
const [, natWidth] = super.vfunc_get_preferred_width(container, forHeight);
return [0, natWidth];
}
});
const baseIconSizes = [16, 22, 24, 32, 48, 64];
var Dash = GObject.registerClass({
Signals: { 'icon-size-changed': {} },
}, class Dash extends St.Widget {
_init() {
this._maxWidth = -1;
this._maxHeight = -1;
this.iconSize = 64;
this._shownInitially = false;
this._separator = null;
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_expand: true,
});
this._box = new St.Widget({
clip_to_allocation: true,
layout_manager: new DashIconsLayout(),
y_expand: true,
});
this._box._delegate = this;
this._dashContainer.add_child(this._box);
this._showAppsIcon = new ShowAppsIcon();
this._showAppsIcon.show(false);
this._showAppsIcon.icon.setIconSize(this.iconSize);
this._hookUpLabel(this._showAppsIcon);
this._dashContainer.add_child(this._showAppsIcon);
this.showAppsButton = this._showAppsIcon.toggleButton;
this._background = new St.Widget({
style_class: 'dash-background',
});
const sizerBox = new Clutter.Actor();
sizerBox.add_constraint(new Clutter.BindConstraint({
source: this._showAppsIcon.icon,
coordinate: Clutter.BindCoordinate.HEIGHT,
}));
sizerBox.add_constraint(new Clutter.BindConstraint({
source: this._dashContainer,
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._appSystem = Shell.AppSystem.get_default();
this._appSystem.connect('installed-changed', () => {
AppFavorites.getAppFavorites().reload();
this._queueRedisplay();
});
AppFavorites.getAppFavorites().connect('changed', this._queueRedisplay.bind(this));
this._appSystem.connect('app-state-changed', this._queueRedisplay.bind(this));
Main.overview.connect('item-drag-begin',
this._onItemDragBegin.bind(this));
Main.overview.connect('item-drag-end',
this._onItemDragEnd.bind(this));
Main.overview.connect('item-drag-cancelled',
this._onItemDragCancelled.bind(this));
Main.overview.connect('window-drag-begin',
this._onWindowDragBegin.bind(this));
Main.overview.connect('window-drag-cancelled',
this._onWindowDragEnd.bind(this));
Main.overview.connect('window-drag-end',
this._onWindowDragEnd.bind(this));
// Translators: this is the name of the dock/favorites area on
// the left of the overview
Main.ctrlAltTabManager.addGroup(this, _("Dash"), 'user-bookmarks-symbolic');
}
_onItemDragBegin() {
this._dragCancelled = false;
this._dragMonitor = {
dragMotion: this._onItemDragMotion.bind(this),
};
DND.addDragMonitor(this._dragMonitor);
if (this._box.get_n_children() == 0) {
this._emptyDropTarget = new EmptyDropTargetItem();
this._box.insert_child_at_index(this._emptyDropTarget, 0);
this._emptyDropTarget.show(true);
}
}
_onItemDragCancelled() {
this._dragCancelled = true;
this._endItemDrag();
}
_onItemDragEnd() {
if (this._dragCancelled)
return;
this._endItemDrag();
}
_endItemDrag() {
this._clearDragPlaceholder();
this._clearEmptyDropTarget();
this._showAppsIcon.setDragApp(null);
DND.removeDragMonitor(this._dragMonitor);
}
_onItemDragMotion(dragEvent) {
let app = 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;
}
_onWindowDragBegin() {
this.ease({
opacity: 128,
duration: Overview.ANIMATION_TIME / 2,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
_onWindowDragEnd() {
this.ease({
opacity: 255,
duration: Overview.ANIMATION_TIME / 2,
mode: Clutter.AnimationMode.EASE_IN_QUAD,
});
}
_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);
});
item.child.connect('clicked', () => {
this._labelShowing = false;
item.hideLabel();
});
let id = Main.overview.connect('hiding', () => {
this._labelShowing = false;
item.hideLabel();
});
item.child.connect('destroy', () => {
Main.overview.disconnect(id);
});
if (appIcon) {
appIcon.connect('sync-tooltip', () => {
this._syncLabel(item, appIcon);
});
}
}
_createAppItem(app) {
let appIcon = new DashIcon(app);
appIcon.connect('menu-state-changed',
(o, opened) => {
this._itemMenuStateChanged(item, opened);
});
let item = new DashItemContainer();
item.setChild(appIcon);
// Override default AppIcon label_actor, now the
// accessible_name is set at DashItemContainer.setLabelText
appIcon.label_actor = null;
item.setLabelText(app.get_name());
appIcon.icon.setIconSize(this.iconSize);
this._hookUpLabel(item, appIcon);
return item;
}
_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) {
GLib.source_remove(this._showLabelTimeoutId);
this._showLabelTimeoutId = 0;
}
item.hideLabel();
}
}
_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 = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 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) {
GLib.source_remove(this._resetHoverTimeoutId);
this._resetHoverTimeoutId = 0;
}
}
} else {
if (this._showLabelTimeoutId > 0)
GLib.source_remove(this._showLabelTimeoutId);
this._showLabelTimeoutId = 0;
item.hideLabel();
if (this._labelShowing) {
this._resetHoverTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 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(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;
const themeNode = this.get_theme_node();
const maxAllocation = new Clutter.ActorBox({
x1: 0,
y1: 0,
x2: this._maxWidth,
y2: 42, /* whatever */
});
let maxContent = themeNode.get_content_box(maxAllocation);
let availWidth = maxContent.x2 - maxContent.x1;
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 width
availWidth -= iconChildren.length * (buttonWidth - iconWidth) +
(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 = Math.min(availWidth / iconChildren.length, availHeight);
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
let iconSizes = baseIconSizes.map(s => s * scaleFactor);
let newIconSize = baseIconSizes[0];
for (let i = 0; i < iconSizes.length; i++) {
if (iconSizes[i] <= maxIconSize)
newIconSize = baseIconSizes[i];
}
if (newIconSize == this.iconSize)
return;
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) {
this._separator.ease({
height: 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 children = this._box.get_children().filter(actor => {
return actor.child &&
actor.child._delegate &&
actor.child._delegate.app;
});
// Apps currently in the dash
let oldApps = children.map(actor => actor.child._delegate.app);
// Apps supposed to be in the dash
let newApps = [];
for (let id in favorites)
newApps.push(favorites[id]);
for (let i = 0; i < running.length; i++) {
let app = running[i];
if (app.get_id() in favorites)
continue;
newApps.push(app);
}
// Figure out the actual changes to the list of items; we iterate
// over both the list of items currently in the dash and the list
// of items expected there, and collect additions and removals.
// Moves are both an addition and a removal, where the order of
// the operations depends on whether we encounter the position
// where the item has been added first or the one from where it
// was removed.
// There is an assumption that only one item is moved at a given
// time; when moving several items at once, everything will still
// end up at the right position, but there might be additional
// additions/removals (e.g. it might remove all the launchers
// and add them back in the new order even if a smaller set of
// additions and removals is possible).
// If above assumptions turns out to be a problem, we might need
// to use a more sophisticated algorithm, e.g. Longest Common
// Subsequence as used by diff.
let addedItems = [];
let removedActors = [];
let newIndex = 0;
let oldIndex = 0;
while (newIndex < newApps.length || oldIndex < oldApps.length) {
let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null;
let newApp = newApps.length > newIndex ? newApps[newIndex] : null;
// No change at oldIndex/newIndex
if (oldApp == newApp) {
oldIndex++;
newIndex++;
continue;
}
// App removed at oldIndex
if (oldApp && !newApps.includes(oldApp)) {
removedActors.push(children[oldIndex]);
oldIndex++;
continue;
}
// App added at newIndex
if (newApp && !oldApps.includes(newApp)) {
addedItems.push({ app: newApp,
item: this._createAppItem(newApp),
pos: newIndex });
newIndex++;
continue;
}
// App moved
let nextApp = newApps.length > newIndex + 1
? newApps[newIndex + 1] : null;
let insertHere = nextApp && nextApp == oldApp;
let alreadyRemoved = removedActors.reduce((result, actor) => {
let removedApp = actor.child._delegate.app;
return result || removedApp == newApp;
}, false);
if (insertHere || alreadyRemoved) {
let newItem = this._createAppItem(newApp);
addedItems.push({ app: newApp,
item: newItem,
pos: newIndex + removedActors.length });
newIndex++;
} else {
removedActors.push(children[oldIndex]);
oldIndex++;
}
}
for (let i = 0; i < addedItems.length; i++) {
this._box.insert_child_at_index(addedItems[i].item,
addedItems[i].pos);
}
for (let i = 0; i < removedActors.length; i++) {
let item = removedActors[i];
// Don't animate item removal when the overview is transitioning
// or hidden
if (Main.overview.visible && !Main.overview.animationInProgress)
item.animateOutAndDestroy();
else
item.destroy();
}
this._adjustIconSize();
// Skip animations on first run when adding the initial set
// of items, to avoid all items zooming in at once
let animate = this._shownInitially && Main.overview.visible &&
!Main.overview.animationInProgress;
if (!this._shownInitially)
this._shownInitially = true;
for (let i = 0; i < addedItems.length; i++)
addedItems[i].item.show(animate);
// Update separator
const nFavorites = Object.keys(favorites).length;
const nIcons = children.length + addedItems.length - removedActors.length;
if (nFavorites > 0 && nFavorites < nIcons) {
if (!this._separator) {
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();
}
_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 = getAppFromSource(source);
// Don't allow favoriting of transient apps
if (app == null || app.is_window_backed())
return DND.DragMotionResult.NO_DROP;
if (!global.settings.is_writable('favorite-apps'))
return DND.DragMotionResult.NO_DROP;
let favorites = AppFavorites.getAppFavorites().getFavorites();
let numFavorites = favorites.length;
let favPos = favorites.indexOf(app);
let children = this._box.get_children();
let numChildren = children.length;
let boxWidth = this._box.width;
// Keep the placeholder out of the index calculation; assuming that
// the remove target has the same size as "normal" items, we don't
// need to do the same adjustment there.
if (this._dragPlaceholder) {
boxWidth -= this._dragPlaceholder.width;
numChildren--;
}
// Same with the separator
if (this._separator) {
boxWidth -= this._separator.width;
numChildren--;
}
let pos;
if (!this._emptyDropTarget)
pos = Math.floor(x * numChildren / boxWidth);
else
pos = 0; // always insert at the top when dash is empty
// Put the placeholder after the last favorite if we are not
// in the favorites zone
if (pos > numFavorites)
pos = numFavorites;
if (pos !== this._dragPlaceholderPos && this._animatingPlaceholdersCount === 0) {
this._dragPlaceholderPos = pos;
// Don't allow positioning before or after self
if (favPos != -1 && (pos == favPos || pos == favPos + 1)) {
this._clearDragPlaceholder();
return DND.DragMotionResult.CONTINUE;
}
// If the placeholder already exists, we just move
// it, but if we are adding it, expand its size in
// an animation
let fadeIn;
if (this._dragPlaceholder) {
this._dragPlaceholder.destroy();
fadeIn = false;
} else {
fadeIn = true;
}
this._dragPlaceholder = new 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);
}
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 = getAppFromSource(source);
// Don't allow favoriting of transient apps
if (app == null || app.is_window_backed())
return false;
if (!global.settings.is_writable('favorite-apps'))
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 want 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;
}
setMaxSize(maxWidth, maxHeight) {
if (this._maxWidth === maxWidth &&
this._maxHeight === maxHeight)
return;
this._maxWidth = maxWidth;
this._maxHeight = maxHeight;
this._queueRedisplay();
}
});