diff --git a/data/Makefile.am b/data/Makefile.am index e17b0e7f7..8c0419811 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -25,6 +25,7 @@ dist_theme_DATA = \ theme/close-window.svg \ theme/close.svg \ theme/corner-ripple.png \ + theme/dash-placeholder.svg \ theme/dialog-error.svg \ theme/gnome-shell.css \ theme/mosaic-view-active.svg \ diff --git a/data/theme/dash-placeholder.svg b/data/theme/dash-placeholder.svg new file mode 100644 index 000000000..cbae148a2 --- /dev/null +++ b/data/theme/dash-placeholder.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 750ccb878..2dd59aca2 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -383,6 +383,11 @@ StTooltip StLabel { border-radius: 0px 9px 9px 0px; } +.dash-placeholder { + background-image: url("dash-placeholder.svg"); + height: 27px; +} + #viewSelector { spacing: 16px; } @@ -544,7 +549,16 @@ StTooltip StLabel { padding: 6px 12px; } +.remove-favorite-icon { + color: #a0a0a0; +} + +.remove-favorite-icon:hover { + color: white; +} + .app-well-app > .overview-icon, +.remove-favorite > .overview-icon, .search-result-content > .overview-icon { border-radius: 4px; padding: 4px; @@ -565,6 +579,7 @@ StTooltip StLabel { } .app-well-app:hover > .overview-icon, +.remove-favorite:hover > .overview-icon, .search-result-content:hover > .overview-icon { background: rgba(255,255,255,0.33); text-shadow: black 0px 2px 2px; diff --git a/js/ui/appFavorites.js b/js/ui/appFavorites.js index 9c050ee4a..a3e83c4fd 100644 --- a/js/ui/appFavorites.js +++ b/js/ui/appFavorites.js @@ -63,7 +63,7 @@ AppFavorites.prototype = { return appId in this._favorites; }, - _addFavorite: function(appId) { + _addFavorite: function(appId, pos) { if (appId in this._favorites) return false; @@ -73,14 +73,17 @@ AppFavorites.prototype = { return false; let ids = this._getIds(); - ids.push(appId); + if (pos == -1) + ids.push(appId); + else + ids.splice(pos, 0, appId); global.settings.set_strv(this.FAVORITE_APPS_KEY, ids); this._favorites[appId] = app; return true; }, - addFavorite: function(appId) { - if (!this._addFavorite(appId)) + addFavoriteAtPos: function(appId, pos) { + if (!this._addFavorite(appId, pos)) return; let app = Shell.AppSystem.get_default().get_app(appId); @@ -90,6 +93,15 @@ AppFavorites.prototype = { })); }, + addFavorite: function(appId) { + this.addFavoriteAtPos(appId, -1); + }, + + moveFavoriteToPos: function(appId, pos) { + this._removeFavorite(appId); + this._addFavorite(appId, pos); + }, + _removeFavorite: function(appId) { if (!appId in this._favorites) return false; @@ -100,13 +112,16 @@ AppFavorites.prototype = { }, removeFavorite: function(appId) { + let ids = this._getIds(); + let pos = ids.indexOf(appId); + let app = this._favorites[appId]; if (!this._removeFavorite(appId)) return; Main.overview.shellInfo.setMessage(_("%s has been removed from your favorites.").format(app.get_name()), Lang.bind(this, function () { - this._addFavorite(appId); + this._addFavorite(appId, pos); })); } }; diff --git a/js/ui/dash.js b/js/ui/dash.js index 40bf445b7..8d722e0d8 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -1,8 +1,8 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ -const Mainloop = imports.mainloop; const Signals = imports.signals; const Lang = imports.lang; +const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; const Gettext = imports.gettext.domain('gnome-shell'); @@ -11,10 +11,67 @@ const _ = Gettext.gettext; 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 Workspace = imports.ui.workspace; +function RemoveFavoriteIcon() { + this._init(); +} + +RemoveFavoriteIcon.prototype = { + _init: function() { + this.actor = new St.Bin({ style_class: 'remove-favorite' }); + this._iconActor = null; + this.icon = new IconGrid.BaseIcon(_("Remove"), + { setSizeManually: true, + createIcon: Lang.bind(this, this._createIcon) }); + this.actor.set_child(this.icon.actor); + this.actor._delegate = this; + }, + + _createIcon: function(size) { + this._iconActor = new St.Icon({ icon_name: 'user-trash', + style_class: 'remove-favorite-icon', + icon_size: size }); + return this._iconActor; + }, + + setHover: function(hovered) { + this.actor.set_hover(hovered); + if (this._iconActor) + this._iconActor.set_hover(hovered); + }, + + // Rely on the dragged item being a favorite + handleDragOver: function(source, actor, x, y, time) { + return DND.DragMotionResult.MOVE_DROP; + }, + + acceptDrop: function(source, actor, x, y, time) { + let app = null; + if (source instanceof AppDisplay.AppWellIcon) { + let appSystem = Shell.AppSystem.get_default(); + app = appSystem.get_app(source.getId()); + } else if (source instanceof Workspace.WindowClone) { + let tracker = Shell.WindowTracker.get_default(); + app = tracker.get_window_app(source.metaWindow); + } + + let id = app.get_id(); + + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, + function () { + AppFavorites.getAppFavorites().removeFavorite(id); + return false; + })); + + return true; + } +}; + + function Dash() { this._init(); } @@ -25,6 +82,11 @@ Dash.prototype = { this._menus = []; this._menuDisplays = []; this._maxHeight = -1; + this._iconSize = 48; + + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + this._favRemoveTarget = null; this._favorites = []; @@ -51,6 +113,75 @@ Dash.prototype = { this._tracker.connect('app-state-changed', Lang.bind(this, this._queueRedisplay)); }, + show: function() { + this._itemDragBeginId = Main.overview.connect('item-drag-begin', + Lang.bind(this, this._onDragBegin)); + this._itemDragEndId = Main.overview.connect('item-drag-end', + Lang.bind(this, this._onDragEnd)); + this._windowDragBeginId = Main.overview.connect('window-drag-begin', + Lang.bind(this, this._onDragBegin)); + this._windowDragEndId = Main.overview.connect('window-drag-end', + Lang.bind(this, this._onDragEnd)); + }, + + hide: function() { + Main.overview.disconnect(this._itemDragBeginId); + Main.overview.disconnect(this._itemDragEndId); + Main.overview.disconnect(this._windowDragBeginId); + Main.overview.disconnect(this._windowDragEndId); + }, + + _onDragBegin: function() { + this._dragMonitor = { + dragMotion: Lang.bind(this, this._onDragMotion) + }; + DND.addDragMonitor(this._dragMonitor); + }, + + _onDragEnd: function() { + this._clearDragPlaceholder(); + if (this._favRemoveTarget) { + this._favRemoveTarget.actor.destroy(); + this._favRemoveTarget = null; + } + DND.removeMonitor(this._dragMonitor); + }, + + _onDragMotion: function(dragEvent) { + let app = null; + if (dragEvent.source instanceof AppDisplay.AppWellIcon) + app = this._appSystem.get_app(dragEvent.source.getId()); + else if (dragEvent.source instanceof Workspace.WindowClone) + app = this._tracker.get_window_app(dragEvent.source.metaWindow); + else + return DND.DragMotionResult.CONTINUE; + + let id = app.get_id(); + + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + let srcIsFavorite = (id in favorites); + + if (srcIsFavorite && this._favRemoveTarget == null) { + this._favRemoveTarget = new RemoveFavoriteIcon(); + this._favRemoveTarget.icon.setIconSize(this._iconSize); + this._box.add(this._favRemoveTarget.actor); + } + + let favRemoveHovered = false; + if (this._favRemoveTarget) + favRemoveHovered = + this._favRemoveTarget.actor.contains(dragEvent.targetActor); + + if (!this._box.contains(dragEvent.targetActor) || favRemoveHovered) + this._clearDragPlaceholder(); + + if (this._favRemoveTarget) + this._favRemoveTarget.setHover(favRemoveHovered); + + return DND.DragMotionResult.CONTINUE; + }, + _appIdListToHash: function(apps) { let ids = {}; for (let i = 0; i < apps.length; i++) @@ -62,6 +193,19 @@ Dash.prototype = { Main.queueDeferredWork(this._workId); }, + _addApp: function(app) { + let display = new AppDisplay.AppWellIcon(app); + 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; + })); + this._box.add(display.actor); + }, + _redisplay: function () { this._box.hide(); this._box.remove_all(); @@ -76,16 +220,14 @@ Dash.prototype = { for (let id in favorites) { let app = favorites[id]; - let display = new AppDisplay.AppWellIcon(app); - this._box.add(display.actor); + this._addApp(app); } for (let i = 0; i < running.length; i++) { let app = running[i]; if (app.get_id() in favorites) continue; - let display = new AppDisplay.AppWellIcon(app); - this._box.add(display.actor); + this._addApp(app); } if (this._placeholderText) { this._placeholderText.destroy(); @@ -102,8 +244,9 @@ Dash.prototype = { for (let i = 0; i < iconSizes.length; i++) { let minHeight, natHeight; + this._iconSize = iconSizes[i]; for (let j = 0; j < children.length; j++) - children[j]._delegate.icon.setIconSize(iconSizes[i]); + children[j]._delegate.icon.setIconSize(this._iconSize); [minHeight, natHeight] = this.actor.get_preferred_height(-1); @@ -114,6 +257,14 @@ Dash.prototype = { this._box.show(); }, + _clearDragPlaceholder: function() { + if (this._dragPlaceholder) { + this._dragPlaceholder.destroy(); + this._dragPlaceholder = null; + this._dragPlaceholderPos = -1; + } + }, + handleDragOver : function(source, actor, x, y, time) { let app = null; if (source instanceof AppDisplay.AppWellIcon) @@ -125,6 +276,28 @@ Dash.prototype = { if (app == null || app.is_transient()) return DND.DragMotionResult.NO_DROP; + let numFavorites = AppFavorites.getAppFavorites().getFavorites().length; + let numChildren = this._box.get_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.height; + numChildren--; + } + + let pos = Math.round(y * numChildren / boxHeight); + + if (pos != this._dragPlaceholderPos && pos <= numFavorites) { + this._dragPlaceholderPos = pos; + if (this._dragPlaceholder) + this._dragPlaceholder.destroy(); + this._dragPlaceholder = new St.Bin({ style_class: 'dash-placeholder' }); + this._box.insert_actor(this._dragPlaceholder, pos); + } + let id = app.get_id(); let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); @@ -132,7 +305,7 @@ Dash.prototype = { let srcIsFavorite = (id in favorites); if (srcIsFavorite) - return DND.DragMotionResult.NO_DROP; + return DND.DragMotionResult.MOVE_DROP; return DND.DragMotionResult.COPY_DROP; }, @@ -157,14 +330,25 @@ Dash.prototype = { let srcIsFavorite = (id in favorites); - if (srcIsFavorite) { - return false; - } else { - Mainloop.idle_add(Lang.bind(this, function () { - AppFavorites.getAppFavorites().addFavorite(id); + let favPos = 0; + let children = this._box.get_children(); + for (let i = 0; i < this._dragPlaceholderPos; i++) { + let childId = children[i]._delegate.app.get_id(); + if (childId == id) + continue; + if (childId in favorites) + favPos++; + } + + 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; } diff --git a/js/ui/overview.js b/js/ui/overview.js index 403e83ffd..92c3c9a66 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -302,6 +302,7 @@ Overview.prototype = { this.viewSelector.show(); this._workspacesDisplay.show(); + this._dash.show(); this.workspaces = this._workspacesDisplay.workspacesView; this._group.add_actor(this.workspaces.actor); @@ -434,6 +435,7 @@ Overview.prototype = { this._workspacesDisplay.hide(); this.viewSelector.hide(); + this._dash.hide(); this._desktopFade.hide(); this._background.hide();