diff --git a/data/org.gnome.shell.gschema.xml.in b/data/org.gnome.shell.gschema.xml.in
index 9c3e42c944dc70da5ec1a63183283d3c64ea5cf7..b8ff914fcb06d438a328b319363c8b3a81065c52 100644
--- a/data/org.gnome.shell.gschema.xml.in
+++ b/data/org.gnome.shell.gschema.xml.in
@@ -109,6 +109,10 @@
the shell.
+
+ []
+ Data about the icons in the icon grid
+
diff --git a/data/theme/gnome-shell-sass/_common.scss b/data/theme/gnome-shell-sass/_common.scss
index 5e377df1536dfaf35a1a73058dfef2c549b56145..8731ba48d973e1c4281f2b6a8c77b3efcffcfdb8 100644
--- a/data/theme/gnome-shell-sass/_common.scss
+++ b/data/theme/gnome-shell-sass/_common.scss
@@ -1514,6 +1514,9 @@ StScrollBar {
border-image: none;
background-image: none;
}
+ &:drop .overview-icon {
+ background-color: transparentize($selected_bg_color,.15);
+ }
&:active .overview-icon,
&:checked .overview-icon {
background-color: transparentize(darken($osd_bg_color,10%), 0.5);
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index a027dc86b1eaef5af62402bdf5a6df4f2d426294..4bf87d004149bc6d7777ce6d4afed6978df34090 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -39,6 +39,9 @@ var VIEWS_SWITCH_ANIMATION_DELAY = 0.1;
var PAGE_SWITCH_TIME = 0.3;
+var APP_ICON_SCALE_IN_TIME = 0.5;
+var APP_ICON_SCALE_IN_DELAY = 0.7;
+
const SWITCHEROO_BUS_NAME = 'net.hadess.SwitcherooControl';
const SWITCHEROO_OBJECT_PATH = '/net/hadess/SwitcherooControl';
@@ -82,6 +85,44 @@ function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
+function _findBestFolderName(apps) {
+ let appInfos = apps.map(app => app.get_app_info());
+
+ let categoryCounter = {};
+ let commonCategories = [];
+
+ appInfos.reduce((categories, appInfo) => {
+ for (let category of appInfo.get_categories().split(';')) {
+ if (!(category in categoryCounter))
+ categoryCounter[category] = 0;
+
+ categoryCounter[category] += 1;
+
+ // If a category is present in all apps, its counter will
+ // reach appInfos.length
+ if (category.length > 0 &&
+ categoryCounter[category] == appInfos.length) {
+ categories.push(category);
+ }
+ }
+ return categories;
+ }, commonCategories);
+
+ for (let category of commonCategories) {
+ let keyfile = new GLib.KeyFile();
+ let path = 'desktop-directories/%s.directory'.format(category);
+
+ try {
+ keyfile.load_from_data_dirs(path, GLib.KeyFileFlags.NONE);
+ return keyfile.get_locale_string('Desktop Entry', 'Name', null);
+ } catch (e) {
+ continue;
+ }
+ }
+
+ return null;
+}
+
class BaseAppView {
constructor(params, gridParams) {
if (this.constructor === BaseAppView)
@@ -114,42 +155,76 @@ class BaseAppView {
// Nothing by default
}
- removeAll() {
- this._grid.destroyAll();
- this._items = {};
- this._allItems = [];
- }
-
_redisplay() {
- this.removeAll();
- this._loadApps();
+ let oldApps = this._allItems.slice();
+ let oldAppIds = oldApps.map(icon => icon.id);
+
+ let newApps = this._loadApps();
+ let newAppIds = newApps.map(icon => icon.id);
+
+ let addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id));
+ let removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id));
+
+ // Remove old app icons
+ removedApps.forEach(icon => {
+ let iconIndex = this._allItems.indexOf(icon);
+
+ this._allItems.splice(iconIndex, 1);
+ this._grid.removeItem(icon);
+ delete this._items[icon.id];
+ });
+
+ // Add new app icons
+ addedApps.forEach(icon => {
+ let iconIndex = newApps.indexOf(icon);
+
+ this._allItems.splice(iconIndex, 0, icon);
+ this._items[icon.id] = icon;
+ });
+
+ this._loadGrid();
}
getAllItems() {
return this._allItems;
}
- hasItem(id) {
- return this._items[id] !== undefined;
- }
+ _loadGrid() {
+ this._allItems.forEach((item, index) => {
+ // Don't readd already added items
+ if (item.actor.get_parent())
+ return;
+
+ this._grid.addItem(item, index);
+ });
- addItem(icon) {
- let id = icon.id;
- if (this.hasItem(id))
- throw new Error(`icon with id ${id} already added to view`);
+ this._allItems.forEach((item, index) => {
+ this._grid.set_child_at_index(item.actor, index);
+ });
- this._allItems.push(icon);
- this._items[id] = icon;
+ this.emit('view-loaded');
}
- _compareItems(a, b) {
- return a.name.localeCompare(b.name);
- }
+ moveItem(item, newPosition) {
+ let itemIndex = this._allItems.indexOf(item);
- loadGrid() {
- this._allItems.sort(this._compareItems);
- this._allItems.forEach(item => this._grid.addItem(item));
- this.emit('view-loaded');
+ if (itemIndex == -1) {
+ log('Trying to move item %s that is not in this app view'.format(item.id));
+ return;
+ }
+
+ let visibleItems = this._allItems.filter(item => item.actor.visible);
+ let visibleIndex = visibleItems.indexOf(item);
+ if (newPosition > visibleIndex)
+ newPosition -= 1;
+
+ // Remove from the old position
+ this._allItems.splice(itemIndex, 1);
+
+ let realPosition = this._grid.moveItem(item, newPosition);
+ this._allItems.splice(realPosition, 0, item);
+
+ return realPosition;
}
_selectAppInternal(id) {
@@ -159,6 +234,14 @@ class BaseAppView {
log(`No such application ${id}`);
}
+ handleDragOver(source, actor, x, y, time) {
+ return DND.DragMotionResult.NO_DROP;
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ return false;
+ }
+
selectApp(id) {
if (this._items[id] && this._items[id].actor.mapped) {
this._selectAppInternal(id);
@@ -222,6 +305,18 @@ class BaseAppView {
Tweener.addTween(this._grid, params);
}
+
+ canDropAt(x, y) {
+ return this._grid.canDropAt(x, y);
+ }
+
+ nudgeItemsAtIndex(index, dragLocation) {
+ this._grid.nudgeItemsAtIndex(index, dragLocation);
+ }
+
+ removeNudges() {
+ this._grid.removeNudges();
+ }
}
Signals.addSignalMethods(BaseAppView.prototype);
@@ -238,6 +333,7 @@ var AllView = class AllView extends BaseAppView {
this.actor = new St.Widget({ layout_manager: new Clutter.BinLayout(),
x_expand: true, y_expand: true });
this.actor.add_actor(this._scrollView);
+ this._grid._delegate = this;
this._scrollView.set_policy(St.PolicyType.NEVER,
St.PolicyType.EXTERNAL);
@@ -324,40 +420,23 @@ var AllView = class AllView extends BaseAppView {
this._folderSettings.connect('changed::folder-children', () => {
Main.queueDeferredWork(this._redisplayWorkId);
});
- }
-
- removeAll() {
- this.folderIcons = [];
- super.removeAll();
- }
-
- _redisplay() {
- let openFolderId = null;
- if (this._displayingPopup && this._currentPopup)
- openFolderId = this._currentPopup._source.id;
-
- super._redisplay();
- if (openFolderId) {
- let [folderToReopen] = this.folderIcons.filter(folder => folder.id == openFolderId);
-
- if (folderToReopen)
- folderToReopen.open();
- }
- }
+ this._gridSettings = new Gio.Settings({ schema_id: 'org.gnome.shell' });
+ this._gridChangedId = this._gridSettings.connect('changed::icons-data', () => {
+ if (!this._blockGridSettings)
+ Main.queueDeferredWork(this._redisplayWorkId);
+ });
- _itemNameChanged(item) {
- // If an item's name changed, we can pluck it out of where it's
- // supposed to be and reinsert it where it's sorted.
- let oldIdx = this._allItems.indexOf(item);
- this._allItems.splice(oldIdx, 1);
- let newIdx = Util.insertSorted(this._allItems, item, this._compareItems);
+ Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this));
+ Main.overview.connect('item-drag-end', this._onDragEnd.bind(this));
- this._grid.removeItem(item);
- this._grid.addItem(item, newIdx);
+ this._nEventBlockerInhibits = 0;
+ this._popdownId = 0;
}
_refilterApps() {
+ let filteredApps = this._allItems.filter(icon => !icon.actor.visible);
+
this._allItems.forEach(icon => {
if (icon instanceof AppIcon)
icon.actor.visible = true;
@@ -370,6 +449,14 @@ var AllView = class AllView extends BaseAppView {
appIcon.actor.visible = false;
});
});
+
+ // Scale in app icons that weren't visible, but now are
+ this._allItems.filter(icon => {
+ return icon.actor.visible && filteredApps.includes(icon);
+ }).forEach(icon => {
+ if (icon instanceof AppIcon)
+ icon.scheduleScaleIn();
+ });
}
getAppInfos() {
@@ -377,6 +464,7 @@ var AllView = class AllView extends BaseAppView {
}
_loadApps() {
+ let newApps = [];
this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => {
try {
(appInfo.get_id()); // catch invalid file encodings
@@ -390,16 +478,28 @@ var AllView = class AllView extends BaseAppView {
let appSys = Shell.AppSystem.get_default();
+ this.folderIcons = [];
+ let appsInFolder = [];
+
+ let iconsData = this._gridSettings.get_value('icons-data').deep_unpack();
+ let customPositionedIcons = [];
+
let folders = this._folderSettings.get_strv('folder-children');
folders.forEach(id => {
- if (this.hasItem(id))
- return;
let path = this._folderSettings.path + 'folders/' + id + '/';
- let icon = new FolderIcon(id, path, this);
- icon.connect('name-changed', this._itemNameChanged.bind(this));
- icon.connect('apps-changed', this._refilterApps.bind(this));
- this.addItem(icon);
+ let icon = this._items[id];
+ if (!icon) {
+ icon = new FolderIcon(id, path, this);
+ icon.connect('apps-changed', this._redisplay.bind(this));
+ }
this.folderIcons.push(icon);
+
+ if (iconsData[id])
+ customPositionedIcons.push(icon);
+ else
+ newApps.push(icon);
+
+ icon.getAppIds().forEach(appId => appsInFolder.push(appId));
});
// Allow dragging of the icon only if the Dash would accept a drop to
@@ -410,15 +510,83 @@ var AllView = class AllView extends BaseAppView {
// but we hope that is not used much.
let favoritesWritable = global.settings.is_writable('favorite-apps');
+ // First, add only the app icons that do not have a custom position
+ // set. These icons will be sorted alphabetically.
apps.forEach(appId => {
let app = appSys.lookup_app(appId);
- let icon = new AppIcon(app,
+ let icon = new AppIcon(app, this,
{ isDraggable: favoritesWritable });
- this.addItem(icon);
+
+ if (iconsData[appId])
+ customPositionedIcons.push(icon);
+ else
+ newApps.push(icon);
+ });
+ newApps.sort((a, b) => a.name.localeCompare(b.name));
+
+ // The stored position is final. That means we need to add the custom
+ // icons in order (first to last) otherwise they end up with in the
+ // wrong position
+ customPositionedIcons.sort((a, b) => {
+ let indexA = iconsData[a.id].deep_unpack()['position'].deep_unpack();
+ let indexB = iconsData[b.id].deep_unpack()['position'].deep_unpack();
+
+ return indexA - indexB;
+ });
+
+ // Now add the icons with a custom position set. Because 'newApps' has
+ // literally all apps -- including the ones that will be hidden -- we
+ // need to translate from visible position to the real position.
+ let visibleApps = newApps.filter(app => !appsInFolder.includes(app.id));
+
+ customPositionedIcons.forEach((icon, index) => {
+ let iconData = iconsData[icon.id].deep_unpack();
+ let position = iconData['position'].deep_unpack();
+
+ // Because we are modifying 'newApps' here, compensate the number
+ // of added items by subtracting 'index'
+ let visibleAppAtPosition = visibleApps[position - index];
+ let realPosition = newApps.indexOf(visibleAppAtPosition);
+ newApps.splice(realPosition, 0, icon);
+ });
+
+ return newApps;
+ }
+
+ moveItem(item, position) {
+ let visibleApps = this._allItems.filter(icon => icon.actor.visible);
+ let oldPosition = visibleApps.indexOf(item);
+
+ if (oldPosition == position)
+ return;
+
+ super.moveItem(item, position);
+
+ if (position > oldPosition)
+ position -= 1;
+
+ // Update all custom icon positions to match what's visible
+ visibleApps = this._allItems.filter(icon => icon.actor.visible);
+ let iconsData = this._gridSettings.get_value('icons-data').deep_unpack();
+ visibleApps.forEach((icon, index) => {
+ if (!iconsData[icon.id] || icon.id == item.id)
+ return;
+
+ iconsData[icon.id] = new GLib.Variant('a{sv}', {
+ 'position': GLib.Variant.new_uint32(index),
+ });
});
- this.loadGrid();
+ iconsData[item.id] = new GLib.Variant('a{sv}', {
+ 'position': GLib.Variant.new_uint32(position),
+ });
+ this._gridSettings.set_value('icons-data',
+ new GLib.Variant('a{sv}', iconsData));
+ }
+
+ _loadGrid() {
+ super._loadGrid();
this._refilterApps();
}
@@ -670,6 +838,188 @@ var AllView = class AllView extends BaseAppView {
for (let i = 0; i < this.folderIcons.length; i++)
this.folderIcons[i].adaptToSize(availWidth, availHeight);
}
+
+ _handleDragOvershoot(dragEvent) {
+ let [gridX, gridY] = this.actor.get_transformed_position();
+ let [gridWidth, gridHeight] = this.actor.get_transformed_size();
+ let gridBottom = gridY + gridHeight;
+
+ // Within the grid boundaries, or already animating
+ if (dragEvent.y > gridY && dragEvent.y < gridBottom ||
+ Tweener.isTweening(this._adjustment)) {
+ return;
+ }
+
+ // Moving above the grid
+ let currentY = this._adjustment.value;
+ if (dragEvent.y <= gridY && currentY > 0) {
+ this.goToPage(this._grid.currentPage - 1);
+ return;
+ }
+
+ // Moving below the grid
+ let maxY = this._adjustment.upper - this._adjustment.page_size;
+ if (dragEvent.y >= gridBottom && currentY < maxY) {
+ this.goToPage(this._grid.currentPage + 1);
+ return;
+ }
+ }
+
+ _schedulePopdown() {
+ if (this._popdownId > 0)
+ return;
+
+ this._popdownId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
+ if (this._currentPopup)
+ this._currentPopup.popdown();
+ this._popdownId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _unschedulePopdown() {
+ if (this._popdownId > 0) {
+ GLib.source_remove(this._popdownId);
+ this._popdownId = 0;
+ }
+ }
+
+ _onDragBegin() {
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this)
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ }
+
+ _onDragMotion(dragEvent) {
+ let appIcon = dragEvent.source;
+
+ // When dragging from a folder, don't nudge items; instead,
+ // prevent DND entirely by returning NO_DROP
+ if (this._currentPopup) {
+ if (dragEvent.targetActor == this._grid ||
+ this._grid.contains(dragEvent.targetActor)) {
+ this._schedulePopdown();
+ return DND.DragMotionResult.NO_DROP;
+ } else {
+ this._unschedulePopdown();
+ }
+ } else {
+ // Handle the drag overshoot. When dragging to above the
+ // icon grid, move to the page above; when dragging below,
+ // move to the page below.
+ this._handleDragOvershoot(dragEvent);
+ }
+
+ if (dragEvent.targetActor != this._grid)
+ this.removeNudges();
+
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ _onDragEnd() {
+ this.removeNudges();
+
+ if (this._dragMonitor) {
+ DND.removeDragMonitor(this._dragMonitor);
+ this._dragMonitor = null;
+ }
+ }
+
+ handleDragOver(source, actor, x, y, time) {
+ let sourceIndex = -1;
+ if (source.view == this) {
+ let visibleItems = this._allItems.filter(item => item.actor.visible);
+ sourceIndex = visibleItems.indexOf(source);
+ }
+
+ let [index, dragLocation] = this.canDropAt(x, y);
+
+ this.removeNudges();
+ if (source.view && source.view != this)
+ source.view.removeNudges();
+
+ if (index != -1) {
+ if (sourceIndex == -1 || (index != sourceIndex && index != sourceIndex + 1))
+ this.nudgeItemsAtIndex(index, dragLocation);
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ return DND.DragMotionResult.NO_DROP;
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ let [index, dragLocation] = this.canDropAt(x, y);
+
+ if (index == -1)
+ return false;
+
+ if ((source instanceof AppIcon) &&
+ (source.view instanceof FolderView)) {
+ source.view.removeApp(source.app);
+ source = this._items[source.id];
+
+ if (this._currentPopup)
+ this._currentPopup.popdown();
+ }
+
+ source.undoScaleAndFade();
+
+ this.moveItem(source, index);
+ this.removeNudges();
+ return true;
+ }
+
+ inhibitEventBlocker() {
+ this._nEventBlockerInhibits++;
+ this._eventBlocker.visible = this._nEventBlockerInhibits == 0;
+ }
+
+ uninhibitEventBlocker() {
+ this._nEventBlockerInhibits--;
+ this._eventBlocker.visible = this._nEventBlockerInhibits == 0;
+ }
+
+ createFolder(apps, position=-1) {
+ let newFolderId = GLib.uuid_string_random();
+
+ let folders = this._folderSettings.get_strv('folder-children');
+ folders.push(newFolderId);
+ this._folderSettings.set_strv('folder-children', folders);
+
+ // Position the new folder before creating it
+ if (position >= 0) {
+ let iconsData = this._gridSettings.get_value('icons-data').deep_unpack();
+ iconsData[newFolderId] = new GLib.Variant('a{sv}', {
+ 'position': GLib.Variant.new_uint32(position),
+ });
+ this._gridSettings.set_value('icons-data',
+ new GLib.Variant('a{sv}', iconsData));
+ }
+
+ // Create the new folder. We are cannot use but Gio.Settings.new_with_path()
+ // for that.
+ let newFolderPath = this._folderSettings.path.concat('folders/', newFolderId, '/');
+ let newFolderSettings = Gio.Settings.new_with_path('org.gnome.desktop.app-folders.folder',
+ newFolderPath);
+ if (!newFolderSettings) {
+ log('Error creating new folder');
+ return false;
+ }
+
+ let appItems = apps.map(id => this._items[id].app);
+ let folderName = _findBestFolderName(appItems);
+ if (!folderName)
+ folderName = _("Unnamed Folder");
+
+ newFolderSettings.delay();
+ newFolderSettings.set_string('name', folderName);
+ newFolderSettings.set_strv('apps', apps);
+ newFolderSettings.apply();
+
+ return true;
+ }
};
Signals.addSignalMethods(AllView.prototype);
@@ -707,11 +1057,12 @@ var FrequentView = class FrequentView extends BaseAppView {
}
_loadApps() {
+ let apps = [];
let mostUsed = this._usage.get_most_used();
let hasUsefulData = this.hasUsefulData();
this._noFrequentAppsLabel.visible = !hasUsefulData;
if (!hasUsefulData)
- return;
+ return [];
// Allow dragging of the icon only if the Dash would accept a drop to
// change favorite-apps. There are no other possible drop targets from
@@ -724,10 +1075,12 @@ var FrequentView = class FrequentView extends BaseAppView {
for (let i = 0; i < mostUsed.length; i++) {
if (!mostUsed[i].get_app_info().should_show())
continue;
- let appIcon = new AppIcon(mostUsed[i],
+ let appIcon = new AppIcon(mostUsed[i], this,
{ isDraggable: favoritesWritable });
- this._grid.addItem(appIcon, -1);
+ apps.push(appIcon);
}
+
+ return apps;
}
// Called before allocation to calculate dynamic spacing
@@ -1013,28 +1366,35 @@ var AppSearchProvider = class AppSearchProvider {
createResultObject(resultMeta) {
if (resultMeta.id.endsWith('.desktop'))
- return new AppIcon(this._appSys.lookup_app(resultMeta['id']));
+ return new AppIcon(this._appSys.lookup_app(resultMeta['id']), null);
else
return new SystemActionIcon(this, resultMeta);
}
};
var FolderView = class FolderView extends BaseAppView {
- constructor() {
+ constructor(folder, id, parentView) {
super(null, null);
// If it not expand, the parent doesn't take into account its preferred_width when allocating
// the second time it allocates, so we apply the "Standard hack for ClutterBinLayout"
this._grid.x_expand = true;
+ this._id = id;
+ this._folder = folder;
+ this._parentView = parentView;
this.actor = new St.ScrollView({ overlay_scrollbars: true });
this.actor.set_policy(St.PolicyType.NEVER, St.PolicyType.AUTOMATIC);
let scrollableContainer = new St.BoxLayout({ vertical: true, reactive: true });
scrollableContainer.add_actor(this._grid);
this.actor.add_actor(scrollableContainer);
+ this._grid._delegate = this;
let action = new Clutter.PanAction({ interpolate: true });
action.connect('pan', this._onPan.bind(this));
this.actor.add_action(action);
+
+ this._folder.connect('changed', this._redisplay.bind(this));
+ this._redisplay();
}
_childFocused(actor) {
@@ -1097,6 +1457,47 @@ var FolderView = class FolderView extends BaseAppView {
this.actor.set_height(this.usedHeight());
}
+ handleDragOver(source, actor, x, y, time) {
+ let [index, dragLocation] = this.canDropAt(x, y);
+ let sourceIndex = this._allItems.indexOf(source);
+
+ this._parentView.removeNudges();
+ this.removeNudges();
+ if (index != -1 && index != sourceIndex && index != sourceIndex + 1)
+ this.nudgeItemsAtIndex(index, dragLocation);
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ let [index, dragLocation] = this.canDropAt(x, y);
+ let success = index != -1;
+
+ source.undoScaleAndFade();
+
+ if (success) {
+ // If we're dragging from another folder, remove from the old folder
+ if (source.view != this &&
+ (source instanceof AppIcon) &&
+ (source.view instanceof FolderView)) {
+ source.view.removeApp(source.app);
+ }
+
+ // If the new app icon is not in this folder yet, add it; otherwise,
+ // just move the icon to the new position
+ let folderApps = this._folder.get_strv('apps');
+ if (!folderApps.includes(source.id)) {
+ folderApps.splice(index, 0, source.id);
+ this._folder.set_strv('apps', folderApps);
+ } else {
+ this.moveItem(source, index);
+ }
+ }
+
+ this.removeNudges();
+ return success;
+ }
+
_getPageAvailableSize() {
let pageBox = new Clutter.ActorBox();
pageBox.x1 = pageBox.y1 = 0;
@@ -1127,34 +1528,326 @@ var FolderView = class FolderView extends BaseAppView {
setPaddingOffsets(offset) {
this._offsetForEachSide = offset;
}
+
+ _loadApps() {
+ let apps = [];
+ let excludedApps = this._folder.get_strv('excluded-apps');
+ let appSys = Shell.AppSystem.get_default();
+ let addAppId = appId => {
+ if (excludedApps.includes(appId))
+ return;
+
+ let app = appSys.lookup_app(appId);
+ if (!app)
+ return;
+
+ if (!app.get_app_info().should_show())
+ return;
+
+ if (apps.some(appIcon => appIcon.id == appId))
+ return;
+
+ let icon = new AppIcon(app, this);
+ apps.push(icon);
+ };
+
+ let folderApps = this._folder.get_strv('apps');
+ folderApps.forEach(addAppId);
+
+ let folderCategories = this._folder.get_strv('categories');
+ let appInfos = this._parentView.getAppInfos();
+ appInfos.forEach(appInfo => {
+ let appCategories = _getCategories(appInfo);
+ if (!_listsIntersect(folderCategories, appCategories))
+ return;
+
+ addAppId(appInfo.get_id());
+ });
+
+ return apps;
+ }
+
+ removeApp(app) {
+ let folderApps = this._folder.get_strv('apps');
+ let index = folderApps.indexOf(app.id);
+ if (index < 0)
+ return false;
+
+ folderApps.splice(index, 1);
+
+ // Remove the folder if this is the last app icon; otherwise,
+ // just remove the icon
+ if (folderApps.length == 0) {
+ let settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders' });
+ let folders = settings.get_strv('folder-children');
+ folders.splice(folders.indexOf(this._id), 1);
+ settings.set_strv('folder-children', folders);
+
+ // Resetting all keys deletes the relocatable schema
+ let keys = this._folder.settings_schema.list_keys();
+ for (let key of keys)
+ this._folder.reset(key);
+
+ // Remove the folder from the custom position list too
+ settings = new Gio.Settings({ schema_id: 'org.gnome.shell' });
+ let iconsData = settings.get_value('icons-data').deep_unpack();
+ if (iconsData[this._id]) {
+ delete iconsData[this._id];
+ settings.set_value('icons-data',
+ new GLib.Variant('a{sv}', iconsData));
+ }
+ } else {
+ this._folder.set_strv('apps', folderApps);
+ }
+
+ return true;
+ }
+
+ moveItem(item, newPosition) {
+ super.moveItem(item, newPosition);
+
+ let appIds = this._allItems.map(icon => icon.id);
+ this._folder.set_strv('apps', appIds);
+ }
};
-var FolderIcon = class FolderIcon {
+var BaseViewIcon = class BaseViewIcon {
+ constructor(params, buttonParams) {
+ buttonParams = Params.parse(buttonParams, {
+ pivot_point: new Clutter.Point({x: 0.5, y: 0.5}),
+ reactive: true,
+ can_focus: true,
+ x_fill: true,
+ y_fill: true
+ }, true);
+
+ this.actor = new St.Button(buttonParams);
+ this.actor._delegate = this;
+
+ // Get the isDraggable property without passing it on to the BaseIcon:
+ params = Params.parse(params, {
+ isDraggable: true,
+ hideWhileDragging: false
+ }, true);
+ let isDraggable = params['isDraggable'];
+ delete params['isDraggable'];
+
+ this._hasDndHover = false;
+
+ if (isDraggable) {
+ this._draggable = DND.makeDraggable(this.actor);
+ this._draggable.connect('drag-begin', () => {
+ this._dragging = true;
+ this.scaleAndFade();
+ Main.overview.beginItemDrag(this);
+ });
+ this._draggable.connect('drag-cancelled', () => {
+ this._dragging = false;
+ Main.overview.cancelledItemDrag(this);
+ });
+ this._draggable.connect('drag-end', () => {
+ this._dragging = false;
+ this.undoScaleAndFade();
+ Main.overview.endItemDrag(this);
+ });
+ }
+
+ Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this));
+ Main.overview.connect('item-drag-end', this._onDragEnd.bind(this));
+
+ this.actor.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ if (this._draggable && this._dragging) {
+ Main.overview.endItemDrag(this);
+ this.draggable = null;
+ }
+ }
+
+ _createIcon(iconSize) {
+ throw new GObject.NotImplementedError(`_createIcon in ${this.constructor.name}`);
+ }
+
+ _canDropAt(source) {
+ return false;
+ }
+
+ // Should be overriden by subclasses
+ _setHoveringByDnd(isHovering) {
+ if (isHovering)
+ this.actor.add_style_pseudo_class('drop');
+ else
+ this.actor.remove_style_pseudo_class('drop');
+ }
+
+ _onDragBegin() {
+ this._dragMonitor = {
+ dragMotion: this._onDragMotion.bind(this),
+ };
+ DND.addDragMonitor(this._dragMonitor);
+ }
+
+ _onDragMotion(dragEvent) {
+ let target = dragEvent.targetActor;
+ let hoveringActor = target == this.actor || this.actor.contains(target);
+ let canDrop = this._canDropAt(dragEvent.source);
+ let hasDndHover = hoveringActor && canDrop;
+
+ if (this._hasDndHover != hasDndHover) {
+ this._setHoveringByDnd(hasDndHover);
+ this._hasDndHover = hasDndHover;
+ }
+
+ return DND.DragMotionResult.CONTINUE;
+ }
+
+ _onDragEnd() {
+ this.actor.remove_style_pseudo_class('drop');
+ DND.removeDragMonitor(this._dragMonitor);
+ }
+
+ handleDragOver(source, actor, x, y, time) {
+ if (source == this)
+ return DND.DragMotionResult.NO_DROP;
+
+ if (!this._canDropAt(source))
+ return DND.DragMotionResult.CONTINUE;
+
+ return DND.DragMotionResult.MOVE_DROP;
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ source.undoScaleAndFade();
+
+ this._setHoveringByDnd(false);
+
+ if (!this._canDropAt(source))
+ return false;
+
+ return true;
+ }
+
+ getDragActor() {
+ let iconParams = {
+ createIcon: this._createIcon.bind(this),
+ showLabel: (this._icon.label != null),
+ setSizeManually: true
+ };
+
+ let icon = new IconGrid.BaseIcon(this.name, iconParams);
+ icon.setIconSize(this.icon.iconSize);
+
+ let bin = new St.Bin({ style_class: this.actor.style_class });
+ bin.set_child(icon);
+
+ return bin;
+ }
+
+ getDragActorSource() {
+ return this._icon.icon;
+ }
+
+ _scaleIn() {
+ this.actor.scale_x = 0;
+ this.actor.scale_y = 0;
+ this.actor.pivot_point = new Clutter.Point({ x: 0.5, y: 0.5 });
+
+ Tweener.addTween(this.actor, {
+ scale_x: 1,
+ scale_y: 1,
+ time: APP_ICON_SCALE_IN_TIME,
+ delay: APP_ICON_SCALE_IN_DELAY,
+ transition: (t, b, c, d) => {
+ // Similar to easeOutElastic, but less aggressive.
+ t /= d;
+ let p = 0.5;
+ return b + c * (Math.pow(2, -11 * t) * Math.sin(2 * Math.PI * (t - p / 4) / p) + 1);
+ }
+ });
+ }
+
+ _unscheduleScaleIn() {
+ if (this._scaleInId != 0) {
+ this.actor.disconnect(this._scaleInId);
+ this._scaleInId = 0;
+ }
+ }
+
+ scheduleScaleIn() {
+ if (this._scaleInId != 0)
+ return;
+
+ if (this.actor.mapped) {
+ this._scaleIn();
+ } else {
+ this._scaleInId = this.actor.connect('notify::mapped', () => {
+ this._unscheduleScaleIn();
+ this._scaleIn();
+ })
+ }
+ }
+
+ scaleAndFade() {
+ this.actor.save_easing_state();
+ this.actor.reactive = false;
+ this.actor.scale_x = 0.75;
+ this.actor.scale_y = 0.75;
+ this.actor.opacity = 128;
+ this.actor.restore_easing_state();
+ }
+
+ undoScaleAndFade() {
+ this.actor.save_easing_state();
+ this.actor.reactive = true;
+ this.actor.scale_x = 1.0;
+ this.actor.scale_y = 1.0;
+ this.actor.opacity = 255;
+ this.actor.restore_easing_state();
+ }
+
+ get icon() {
+ return this._icon;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get name() {
+ return this._name;
+ }
+
+ get view() {
+ return this._view;
+ }
+}
+
+var FolderIcon = class FolderIcon extends BaseViewIcon {
constructor(id, path, parentView) {
- this.id = id;
- this.name = '';
- this._parentView = parentView;
+ super({ hideWhileDragging: true }, {
+ style_class: 'app-well-app app-folder',
+ toggle_mode: true
+ });
+
+ this._id = id;
+ this._name = '';
+ this._view = parentView;
this._folder = new Gio.Settings({ schema_id: 'org.gnome.desktop.app-folders.folder',
path: path });
- this.actor = new St.Button({ style_class: 'app-well-app app-folder',
- button_mask: St.ButtonMask.ONE,
- toggle_mode: true,
- can_focus: true,
- x_fill: true,
- y_fill: true });
- this.actor._delegate = this;
+
// whether we need to update arrow side, position etc.
this._popupInvalidated = false;
- this.icon = new IconGrid.BaseIcon('', {
+ this._icon = new IconGrid.BaseIcon('', {
createIcon: this._createIcon.bind(this),
setSizeManually: true
});
this.actor.set_child(this.icon);
this.actor.label_actor = this.icon.label;
- this.view = new FolderView();
+ this._folderView = new FolderView(this._folder, id, parentView);
this.actor.connect('clicked', this.open.bind(this));
this.actor.connect('destroy', this.onDestroy.bind(this));
@@ -1168,10 +1861,10 @@ var FolderIcon = class FolderIcon {
}
onDestroy() {
- this.view.actor.destroy();
+ this._folderView.actor.destroy();
if (this._spaceReadySignalId) {
- this._parentView.disconnect(this._spaceReadySignalId);
+ this.view.disconnect(this._spaceReadySignalId);
this._spaceReadySignalId = 0;
}
@@ -1181,103 +1874,112 @@ var FolderIcon = class FolderIcon {
open() {
this._ensurePopup();
- this.view.actor.vscroll.adjustment.value = 0;
+ this._folderView.actor.vscroll.adjustment.value = 0;
this._openSpaceForPopup();
}
getAppIds() {
- return this.view.getAllItems().map(item => item.id);
+ return this._folderView.getAllItems().map(item => item.id);
}
- _updateName() {
- let name = _getFolderName(this._folder);
- if (this.name == name)
- return;
+ _onDragBegin() {
+ super._onDragBegin();
+ this.view.inhibitEventBlocker();
+ }
- this.name = name;
- this.icon.label.text = this.name;
- this.emit('name-changed');
+ _onDragEnd() {
+ super._onDragEnd();
+ this.view.uninhibitEventBlocker();
}
- _redisplay() {
- this._updateName();
+ _canDropAt(source) {
+ if (!(source instanceof AppIcon))
+ return false;
- this.view.removeAll();
+ if (!global.settings.is_writable('favorite-apps'))
+ return false;
- let excludedApps = this._folder.get_strv('excluded-apps');
- let appSys = Shell.AppSystem.get_default();
- let addAppId = appId => {
- if (this.view.hasItem(appId))
- return;
+ if (this._folder.get_strv('apps').includes(source.id))
+ return false
- if (excludedApps.includes(appId))
- return;
+ return true;
+ }
- let app = appSys.lookup_app(appId);
- if (!app)
- return;
+ handleDragOver(source, actor, x, y, time) {
+ if (!this._canDropAt(source))
+ return DND.DragMotionResult.NO_DROP;
- if (!app.get_app_info().should_show())
- return;
+ return DND.DragMotionResult.MOVE_DROP;
+ }
- let icon = new AppIcon(app);
- this.view.addItem(icon);
- };
+ acceptDrop(source, actor, x, y, time) {
+ if (!this._canDropAt(source)) {
+ source.undoScaleAndFade();
+ return true;
+ }
+ let app = source.app;
let folderApps = this._folder.get_strv('apps');
- folderApps.forEach(addAppId);
+ folderApps.push(app.id);
- let folderCategories = this._folder.get_strv('categories');
- let appInfos = this._parentView.getAppInfos();
- appInfos.forEach(appInfo => {
- let appCategories = _getCategories(appInfo);
- if (!_listsIntersect(folderCategories, appCategories))
- return;
+ this._folder.set_strv('apps', folderApps);
- addAppId(appInfo.get_id());
- });
+ return true;
+ }
+
+ _updateName() {
+ let name = _getFolderName(this._folder);
+ if (this._name == name)
+ return;
+
+ this._name = name;
+ this.icon.label.text = name;
+ this.emit('name-changed');
+ }
- this.actor.visible = this.view.getAllItems().length > 0;
- this.view.loadGrid();
+ _redisplay() {
+ this._updateName();
+ this.actor.visible = this._folderView.getAllItems().length > 0;
+ this.icon.update();
this.emit('apps-changed');
}
_createIcon(iconSize) {
- return this.view.createFolderIcon(iconSize, this);
+ return this._folderView.createFolderIcon(iconSize, this);
}
_popupHeight() {
- let usedHeight = this.view.usedHeight() + this._popup.getOffset(St.Side.TOP) + this._popup.getOffset(St.Side.BOTTOM);
+ let usedHeight = this._folderView.usedHeight() + this._popup.getOffset(St.Side.TOP) + this._popup.getOffset(St.Side.BOTTOM);
return usedHeight;
}
_openSpaceForPopup() {
- this._spaceReadySignalId = this._parentView.connect('space-ready', () => {
- this._parentView.disconnect(this._spaceReadySignalId);
+ this._spaceReadySignalId = this.view.connect('space-ready', () => {
+ this.view.disconnect(this._spaceReadySignalId);
this._spaceReadySignalId = 0;
this._popup.popup();
this._updatePopupPosition();
});
- this._parentView.openSpaceForPopup(this, this._boxPointerArrowside, this.view.nRowsDisplayedAtOnce());
+ this.view.openSpaceForPopup(this, this._boxPointerArrowside, this._folderView.nRowsDisplayedAtOnce());
}
_calculateBoxPointerArrowSide() {
- let spaceTop = this.actor.y - this._parentView.getCurrentPageY();
- let spaceBottom = this._parentView.actor.height - (spaceTop + this.actor.height);
+ let spaceTop = this.actor.y - this.view.getCurrentPageY();
+ let spaceBottom = this.view.actor.height - (spaceTop + this.actor.height);
return spaceTop > spaceBottom ? St.Side.BOTTOM : St.Side.TOP;
}
_updatePopupSize() {
// StWidget delays style calculation until needed, make sure we use the correct values
- this.view._grid.ensure_style();
+ this._folderView._grid.ensure_style();
let offsetForEachSide = Math.ceil((this._popup.getOffset(St.Side.TOP) +
this._popup.getOffset(St.Side.BOTTOM) -
this._popup.getCloseButtonOverlap()) / 2);
// Add extra padding to prevent boxpointer decorations and close button being cut off
- this.view.setPaddingOffsets(offsetForEachSide);
- this.view.adaptToSize(this._parentAvailableWidth, this._parentAvailableHeight);
+ this._folderView.setPaddingOffsets(offsetForEachSide);
+ this._folderView.adaptToSize(this._parentAvailableWidth, this._parentAvailableHeight);
}
_updatePopupPosition() {
@@ -1296,7 +1998,7 @@ var FolderIcon = class FolderIcon {
this._boxPointerArrowside = this._calculateBoxPointerArrowSide();
if (!this._popup) {
this._popup = new AppFolderPopup(this, this._boxPointerArrowside);
- this._parentView.addFolderPopup(this._popup);
+ this.view.addFolderPopup(this._popup);
this._popup.connect('open-state-changed', (popup, isOpen) => {
if (!isOpen)
this.actor.checked = false;
@@ -1313,7 +2015,7 @@ var FolderIcon = class FolderIcon {
this._parentAvailableWidth = width;
this._parentAvailableHeight = height;
if (this._popup)
- this.view.adaptToSize(width, height);
+ this._folderView.adaptToSize(width, height);
this._popupInvalidated = true;
}
};
@@ -1322,7 +2024,7 @@ Signals.addSignalMethods(FolderIcon.prototype);
var AppFolderPopup = class AppFolderPopup {
constructor(source, side) {
this._source = source;
- this._view = source.view;
+ this._view = source._folderView;
this._arrowSide = side;
this._isOpen = false;
@@ -1484,18 +2186,16 @@ var AppFolderPopup = class AppFolderPopup {
};
Signals.addSignalMethods(AppFolderPopup.prototype);
-var AppIcon = class AppIcon {
- constructor(app, iconParams = {}) {
+var AppIcon = class AppIcon extends BaseViewIcon {
+ constructor(app, parentView, iconParams = {}) {
+ super(iconParams, {
+ button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
+ style_class: 'app-well-app'
+ });
this.app = app;
- this.id = app.get_id();
- this.name = app.get_name();
-
- this.actor = new St.Button({ style_class: 'app-well-app',
- reactive: true,
- button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
- can_focus: true,
- x_fill: true,
- y_fill: true });
+ this._id = app.get_id();
+ this._name = app.get_name();
+ this._view = parentView;
this._dot = new St.Widget({ style_class: 'app-well-app-running-dot',
layout_manager: new Clutter.BinLayout(),
@@ -1510,15 +2210,14 @@ var AppIcon = class AppIcon {
this._iconContainer.add_child(this._dot);
this.actor._delegate = this;
+ this._scaleInId = 0;
+ this._folderPreviewId = 0;
- // Get the isDraggable property without passing it on to the BaseIcon:
- let appIconParams = Params.parse(iconParams, { isDraggable: true }, true);
- let isDraggable = appIconParams['isDraggable'];
delete iconParams['isDraggable'];
iconParams['createIcon'] = this._createIcon.bind(this);
iconParams['setSizeManually'] = true;
- this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
+ this._icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
this._iconContainer.add_child(this.icon);
this.actor.label_actor = this.icon.label;
@@ -1532,24 +2231,8 @@ var AppIcon = class AppIcon {
this._menu = null;
this._menuManager = new PopupMenu.PopupMenuManager(this.actor);
- if (isDraggable) {
- this._draggable = DND.makeDraggable(this.actor);
- this._draggable.connect('drag-begin', () => {
- this._dragging = true;
- this._removeMenuTimeout();
- Main.overview.beginItemDrag(this);
- });
- this._draggable.connect('drag-cancelled', () => {
- this._dragging = false;
- Main.overview.cancelledItemDrag(this);
- });
- this._draggable.connect('drag-end', () => {
- this._dragging = false;
- Main.overview.endItemDrag(this);
- });
- }
-
- this.actor.connect('destroy', this._onDestroy.bind(this));
+ if (this._draggable)
+ this._draggable.connect('drag-begin', this._removeMenuTimeout.bind(this));
this._menuTimeoutId = 0;
this._stateChangedId = this.app.connect('notify::state', () => {
@@ -1559,12 +2242,15 @@ var AppIcon = class AppIcon {
}
_onDestroy() {
+ super._onDestroy();
+
+ if (this._folderPreviewId > 0) {
+ GLib.source_remove(this._folderPreviewId);
+ this._folderPreviewId = 0;
+ }
+
if (this._stateChangedId > 0)
this.app.disconnect(this._stateChangedId);
- if (this._draggable && this._dragging) {
- Main.overview.endItemDrag(this);
- this.draggable = null;
- }
this._stateChangedId = 0;
this._removeMenuTimeout();
}
@@ -1714,18 +2400,73 @@ var AppIcon = class AppIcon {
this.app.open_new_window(params.workspace);
}
- getDragActor() {
- return this.app.create_icon_texture(Main.overview.dashIconSize);
+ shouldShowTooltip() {
+ return this.actor.hover && (!this._menu || !this._menu.isOpen);
}
- // Returns the original actor that should align with the actor
- // we show as the item is being dragged.
- getDragActorSource() {
- return this.icon.icon;
+ _showFolderPreview() {
+ this.icon.label.opacity = 0;
+
+ // HACK!!!
+ this.icon._iconBin.save_easing_state();
+ this.icon._iconBin.scale_x = FOLDER_SUBICON_FRACTION;
+ this.icon._iconBin.scale_y = FOLDER_SUBICON_FRACTION;
+ this.icon._iconBin.restore_easing_state();
}
- shouldShowTooltip() {
- return this.actor.hover && (!this._menu || !this._menu.isOpen);
+ _hideFolderPreview() {
+ this.icon.label.opacity = 255;
+
+ // HACK!!!
+ this.icon._iconBin.save_easing_state();
+ this.icon._iconBin.scale_x = 1.0;
+ this.icon._iconBin.scale_y = 1.0;
+ this.icon._iconBin.restore_easing_state();
+ }
+
+ _setHoveringByDnd(hovering) {
+ if (hovering) {
+ if (this._folderPreviewId > 0)
+ return;
+
+ this._folderPreviewId =
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
+ this._folderPreviewId = 0;
+
+ super._setHoveringByDnd(true);
+ this._showFolderPreview();
+
+ return GLib.SOURCE_REMOVE;
+ });
+ } else {
+ if (this._folderPreviewId > 0) {
+ GLib.source_remove(this._folderPreviewId);
+ this._folderPreviewId = 0;
+ }
+
+ super._setHoveringByDnd(false);
+ this._hideFolderPreview();
+ }
+ }
+
+ _canDropAt(source) {
+ return source != this &&
+ (source instanceof AppIcon) &&
+ (this.view instanceof AllView);
+ }
+
+ acceptDrop(source, actor, x, y, time) {
+ if (!super.acceptDrop(source, actor, x, y, time))
+ return false;
+
+ let apps = [this.id, source.id];
+ let visibleItems = this.view.getAllItems().filter(item => item.actor.visible);
+ let position = visibleItems.indexOf(this);
+
+ if (visibleItems.indexOf(source) < position)
+ position -= 1;
+
+ return this.view.createFolder(apps, position);
}
};
Signals.addSignalMethods(AppIcon.prototype);
diff --git a/js/ui/dash.js b/js/ui/dash.js
index 7da335bfb87a776a0ae8ceb1a7ea0137e1043d91..136a9837efd06666c1c5ea6b033bb02982812354 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -25,6 +25,32 @@ function getAppFromSource(source) {
}
}
+var DashIcon = class DashIcon extends AppDisplay.AppIcon {
+ constructor(app) {
+ super(app, null, {
+ setSizeManually: true,
+ showLabel: false
+ });
+
+
+ }
+
+ // Disable all DnD methods
+ _onDragBegin() {
+ }
+
+ _onDragEnd() {
+ }
+
+ 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(
@@ -475,19 +501,7 @@ var Dash = class Dash {
}
_createAppItem(app) {
- let appIcon = new AppDisplay.AppIcon(app,
- { 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;
- });
- }
+ let appIcon = new DashIcon(app);
appIcon.connect('menu-state-changed',
(appIcon, opened) => {
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index c74ff4361a01da7c616cdab74b862b11a1f377d5..435cdf697dfc8751b963f22d205a6a3f66c47c93 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -29,6 +29,27 @@ var AnimationDirection = {
var APPICON_ANIMATION_OUT_SCALE = 3;
var APPICON_ANIMATION_OUT_TIME = 0.25;
+const LEFT_DIVIDER_LEEWAY = 30;
+const RIGHT_DIVIDER_LEEWAY = 30;
+
+const NUDGE_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_ELASTIC;
+const NUDGE_DURATION = 800;
+
+const NUDGE_RETURN_ANIMATION_TYPE = Clutter.AnimationMode.EASE_OUT_QUINT;
+const NUDGE_RETURN_DURATION = 300;
+
+const NUDGE_FACTOR = 0.33;
+
+const ICON_POSITION_DELAY = 25;
+
+var DragLocation = {
+ DEFAULT: 0,
+ ON_ICON: 1,
+ START_EDGE: 2,
+ END_EDGE: 3,
+ EMPTY_AREA: 4,
+}
+
var BaseIcon = GObject.registerClass(
class BaseIcon extends St.Bin {
_init(label, params) {
@@ -142,6 +163,10 @@ class BaseIcon extends St.Bin {
// animating.
zoomOutActor(this.child);
}
+
+ update() {
+ this._createIconTexture(this.iconSize);
+ }
});
function clamp(value, min, max) {
@@ -342,6 +367,7 @@ var IconGrid = GObject.registerClass({
let y = box.y1 + this.topPadding;
let columnIndex = 0;
let rowIndex = 0;
+ let nChanged = 0;
for (let i = 0; i < children.length; i++) {
let childBox = this._calculateChildBox(children[i], x, y, box);
@@ -351,7 +377,16 @@ var IconGrid = GObject.registerClass({
} else {
if (!animating)
children[i].opacity = 255;
+
+ // Figure out how much delay to apply
+ if (!childBox.equal(children[i].get_allocation_box()))
+ nChanged++;
+
+ children[i].save_easing_state();
+ children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
+ children[i].set_easing_delay(ICON_POSITION_DELAY * nChanged);
children[i].allocate(childBox, flags);
+ children[i].restore_easing_state();
}
columnIndex++;
@@ -691,6 +726,22 @@ var IconGrid = GObject.registerClass({
this.add_actor(item.actor);
}
+ moveItem(item, newPosition) {
+ if (!this.contains(item.actor)) {
+ log('Cannot move item not contained by the IconGrid');
+ return;
+ }
+
+ let children = this.get_children();
+ let visibleChildren = children.filter(c => c.is_visible());
+ let visibleChildAtPosition = visibleChildren[newPosition];
+ let realPosition = children.indexOf(visibleChildAtPosition);
+
+ this.set_child_at_index(item.actor, realPosition);
+
+ return realPosition;
+ }
+
removeItem(item) {
this.remove_child(item.actor);
}
@@ -787,6 +838,223 @@ var IconGrid = GObject.registerClass({
}
return GLib.SOURCE_REMOVE;
}
+
+ // Drag n' Drop methods
+
+ nudgeItemsAtIndex(index, dragLocation) {
+ // No nudging when the cursor is in an empty area
+ if (dragLocation == DragLocation.EMPTY_AREA || dragLocation == DragLocation.ON_ICON)
+ return;
+
+ let children = this.get_children().filter(c => c.is_visible());
+ let nudgeIndex = index;
+ let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL);
+
+ if (dragLocation != DragLocation.START_EDGE) {
+ let leftItem = children[nudgeIndex - 1];
+ let offset = rtl ? Math.floor(this._hItemSize * NUDGE_FACTOR) : Math.floor(-this._hItemSize * NUDGE_FACTOR);
+ this._animateNudge(leftItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+ }
+
+ // Nudge the icon to the right if we are the first item or not at the
+ // end of row
+ if (dragLocation != DragLocation.END_EDGE) {
+ let rightItem = children[nudgeIndex];
+ let offset = rtl ? Math.floor(-this._hItemSize * NUDGE_FACTOR) : Math.floor(this._hItemSize * NUDGE_FACTOR);
+ this._animateNudge(rightItem, NUDGE_ANIMATION_TYPE, NUDGE_DURATION, offset);
+ }
+ }
+
+ removeNudges() {
+ let children = this.get_children().filter(c => c.is_visible());
+ for (let index = 0; index < children.length; index++) {
+ this._animateNudge(children[index],
+ NUDGE_RETURN_ANIMATION_TYPE,
+ NUDGE_RETURN_DURATION,
+ 0);
+ }
+ }
+
+ _animateNudge(item, animationType, duration, offset) {
+ if (!item)
+ return;
+
+ item.save_easing_state();
+ item.set_easing_mode(animationType);
+ item.set_easing_duration(duration);
+ item.translation_x = offset;
+ item.restore_easing_state();
+ }
+
+ // This function is overriden by the PaginatedIconGrid subclass so we can
+ // take into account the extra space when dragging from a folder
+ _calculateDndRow(y) {
+ let rowHeight = this._getVItemSize() + this._getSpacing();
+ return Math.floor(y / rowHeight);
+ }
+
+ // Returns the drop point index or -1 if we can't drop there
+ canDropAt(x, y) {
+ // This is an complex calculation, but in essence, we divide the grid
+ // as:
+ //
+ // left empty space
+ // | left padding right padding
+ // | | width without padding |
+ // +--------+---+---------------------------------------+-----+
+ // | | | | | | | |
+ // | | | | | | | |
+ // | | |--------+-----------+----------+-------| |
+ // | | | | | | | |
+ // | | | | | | | |
+ // | | |--------+-----------+----------+-------| |
+ // | | | | | | | |
+ // | | | | | | | |
+ // | | |--------+-----------+----------+-------| |
+ // | | | | | | | |
+ // | | | | | | | |
+ // +--------+---+---------------------------------------+-----+
+ //
+ // The left empty space is immediately discarded, and ignored in all
+ // calculations.
+ //
+ // The width (with paddings) is used to determine if we're dragging
+ // over the left or right padding, and which column is being dragged
+ // on.
+ //
+ // Finally, the width without padding is used to figure out where in
+ // the icon (start edge, end edge, on it, etc) the cursor is.
+
+ let [nColumns, usedWidth] = this._computeLayout(this.width);
+
+ let leftEmptySpace;
+ switch (this._xAlign) {
+ case St.Align.START:
+ leftEmptySpace = 0;
+ break;
+ case St.Align.MIDDLE:
+ leftEmptySpace = Math.floor((this.width - usedWidth) / 2);
+ break;
+ case St.Align.END:
+ leftEmptySpace = availWidth - usedWidth;
+ }
+
+ x -= leftEmptySpace;
+ y -= this.topPadding;
+
+ let row = this._calculateDndRow(y);
+
+ // Correct sx to handle the left padding to correctly calculate
+ // the column
+ let rtl = (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL);
+ let gridX = x - this.leftPadding;
+
+ let widthWithoutPadding = usedWidth - this.leftPadding - this.rightPadding;
+ let columnWidth = widthWithoutPadding / nColumns;
+
+ let column;
+ if (x < this.leftPadding)
+ column = 0;
+ else if (x > usedWidth - this.rightPadding)
+ column = nColumns - 1;
+ else
+ column = Math.floor(gridX / columnWidth);
+
+ let isFirstIcon = column == 0;
+ let isLastIcon = column == nColumns - 1;
+
+ // If we're outside of the grid, we are in an invalid drop location
+ if (x < 0 || x > usedWidth)
+ return [-1, DragLocation.DEFAULT];
+
+ let children = this.get_children().filter(c => c.is_visible());
+ let childIndex = Math.min((row * nColumns) + column, children.length);
+
+ // If we're above the grid vertically, we are in an invalid
+ // drop location
+ if (childIndex < 0)
+ return [-1, DragLocation.DEFAULT];
+
+ // If we're past the last visible element in the grid,
+ // we might be allowed to drop there.
+ if (childIndex >= children.length)
+ return [children.length, DragLocation.EMPTY_AREA];
+
+ let child = children[childIndex];
+ let [childMinWidth, childMinHeight, childNaturalWidth, childNaturalHeight] = child.get_preferred_size();
+
+ // This is the width of the cell that contains the icon
+ // (excluding spacing between cells)
+ let childIconWidth = Math.max(this._getHItemSize(), childNaturalWidth);
+
+ // Calculate the original position of the child icon (prior to nudging)
+ let childX;
+ if (rtl)
+ childX = widthWithoutPadding - (column * columnWidth) - childIconWidth;
+ else
+ childX = column * columnWidth;
+
+ let iconLeftX = childX + LEFT_DIVIDER_LEEWAY;
+ let iconRightX = childX + childIconWidth - RIGHT_DIVIDER_LEEWAY
+
+ let dropIndex;
+ let dragLocation;
+
+ x -= this.leftPadding;
+
+ if (x < iconLeftX) {
+ // We are to the left of the icon target
+ if (isFirstIcon || x < 0) {
+ // We are before the leftmost icon on the grid
+ if (rtl) {
+ dropIndex = childIndex + 1;
+ dragLocation = DragLocation.END_EDGE;
+ } else {
+ dropIndex = childIndex;
+ dragLocation = DragLocation.START_EDGE;
+ }
+ } else {
+ // We are between the previous icon (next in RTL) and this one
+ if (rtl)
+ dropIndex = childIndex + 1;
+ else
+ dropIndex = childIndex;
+
+ dragLocation = DragLocation.DEFAULT;
+ }
+ } else if (x >= iconRightX) {
+ // We are to the right of the icon target
+ if (childIndex >= children.length) {
+ // We are beyond the last valid icon
+ // (to the right of the app store / trash can, if present)
+ dropIndex = -1;
+ dragLocation = DragLocation.DEFAULT;
+ } else if (isLastIcon || x >= widthWithoutPadding) {
+ // We are beyond the rightmost icon on the grid
+ if (rtl) {
+ dropIndex = childIndex;
+ dragLocation = DragLocation.START_EDGE;
+ } else {
+ dropIndex = childIndex + 1;
+ dragLocation = DragLocation.END_EDGE;
+ }
+ } else {
+ // We are between this icon and the next one (previous in RTL)
+ if (rtl)
+ dropIndex = childIndex;
+ else
+ dropIndex = childIndex + 1;
+
+ dragLocation = DragLocation.DEFAULT;
+ }
+ } else {
+ // We are over the icon target area
+ dropIndex = childIndex;
+ dragLocation = DragLocation.ON_ICON;
+ }
+
+ return [dropIndex, dragLocation];
+ }
});
var PaginatedIconGrid = GObject.registerClass({
@@ -839,10 +1107,21 @@ var PaginatedIconGrid = GObject.registerClass({
let x = box.x1 + leftEmptySpace + this.leftPadding;
let y = box.y1 + this.topPadding;
let columnIndex = 0;
+ let nChanged = 0;
for (let i = 0; i < children.length; i++) {
let childBox = this._calculateChildBox(children[i], x, y, box);
+
+ // Figure out how much delay to apply
+ if (!childBox.equal(children[i].get_allocation_box()))
+ nChanged++;
+
+ children[i].save_easing_state();
+ children[i].set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
+ children[i].set_easing_delay(ICON_POSITION_DELAY * nChanged);
children[i].allocate(childBox, flags);
+ children[i].restore_easing_state();
+
children[i].show();
columnIndex++;
@@ -861,6 +1140,23 @@ var PaginatedIconGrid = GObject.registerClass({
}
// Overridden from IconGrid
+ _calculateDndRow(y) {
+ let row = super._calculateDndRow(y);
+
+ // If there's no extra space, just return the current value and maintain
+ // the same behavior when without a folder opened.
+ if (!this._extraSpaceData)
+ return row;
+
+ let [ baseRow, nRowsUp, nRowsDown ] = this._extraSpaceData;
+ let newRow = row + nRowsUp;
+
+ if (row > baseRow)
+ newRow -= nRowsDown;
+
+ return newRow;
+ }
+
_getChildrenToAnimate() {
let children = this._getVisibleChildren();
let firstIndex = this._childrenPerPage * this.currentPage;
diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js
index e5bb384797ccd1827d53c0563e47ab69aa428dfc..378cc1b320b85f66d05a23642d7a105cca7f5174 100644
--- a/js/ui/overviewControls.js
+++ b/js/ui/overviewControls.js
@@ -424,17 +424,6 @@ var ControlsManager = class {
layout.connect('allocation-changed', this._updateWorkspacesGeometry.bind(this));
Main.overview.connect('showing', this._updateSpacerVisibility.bind(this));
- Main.overview.connect('item-drag-begin', () => {
- let activePage = this.viewSelector.getActivePage();
- if (activePage != ViewSelector.ViewPage.WINDOWS)
- this.viewSelector.fadeHalf();
- });
- Main.overview.connect('item-drag-end', () => {
- this.viewSelector.fadeIn();
- });
- Main.overview.connect('item-drag-cancelled', () => {
- this.viewSelector.fadeIn();
- });
}
_updateWorkspacesGeometry() {
diff --git a/src/shell-app-system.c b/src/shell-app-system.c
index f632cbe545ee6c68a66ca0e7fcf8cdab0eb6060b..127f29ef02a12f705009d69b8bee574e3b4bf527 100644
--- a/src/shell-app-system.c
+++ b/src/shell-app-system.c
@@ -14,6 +14,14 @@
#include "shell-app-system-private.h"
#include "shell-global.h"
#include "shell-util.h"
+#include "st.h"
+
+/* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That
+ * should be plenty of time for even a slow spinning drive to update
+ * the icon cache.
+ */
+#define RESCAN_TIMEOUT_MS 2500
+#define MAX_RESCAN_RETRIES 6
/* Vendor prefixes are something that can be preprended to a .desktop
* file name. Undo this.
@@ -51,6 +59,9 @@ struct _ShellAppSystemPrivate {
GHashTable *id_to_app;
GHashTable *startup_wm_class_to_id;
GList *installed_apps;
+
+ guint rescan_icons_timeout_id;
+ guint n_rescan_retries;
};
static void shell_app_system_finalize (GObject *object);
@@ -157,12 +168,54 @@ stale_app_remove_func (gpointer key,
return app_is_stale (value);
}
+static gboolean
+rescan_icon_theme_cb (gpointer user_data)
+{
+ ShellAppSystemPrivate *priv;
+ ShellAppSystem *self;
+ StTextureCache *texture_cache;
+ gboolean rescanned;
+
+ self = (ShellAppSystem *) user_data;
+ priv = self->priv;
+
+ texture_cache = st_texture_cache_get_default ();
+ rescanned = st_texture_cache_rescan_icon_theme (texture_cache);
+
+ priv->n_rescan_retries++;
+
+ if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES)
+ {
+ priv->n_rescan_retries = 0;
+ priv->rescan_icons_timeout_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+rescan_icon_theme (ShellAppSystem *self)
+{
+ ShellAppSystemPrivate *priv = self->priv;
+
+ priv->n_rescan_retries = 0;
+
+ if (priv->rescan_icons_timeout_id > 0)
+ return;
+
+ priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS,
+ rescan_icon_theme_cb,
+ self);
+}
+
static void
installed_changed (GAppInfoMonitor *monitor,
gpointer user_data)
{
ShellAppSystem *self = user_data;
+ rescan_icon_theme (self);
scan_startup_wm_class_to_id (self);
g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL);
@@ -200,6 +253,7 @@ shell_app_system_finalize (GObject *object)
g_hash_table_destroy (priv->id_to_app);
g_hash_table_destroy (priv->startup_wm_class_to_id);
g_list_free_full (priv->installed_apps, g_object_unref);
+ g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove);
G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object);
}
diff --git a/src/st/st-texture-cache.c b/src/st/st-texture-cache.c
index eec28007b34bd2152b204dc0f06be4978b74df26..53df7195b8d3d66c43c647adecb6a4728878c45b 100644
--- a/src/st/st-texture-cache.c
+++ b/src/st/st-texture-cache.c
@@ -150,6 +150,14 @@ on_icon_theme_changed (StSettings *settings,
g_signal_emit (cache, signals[ICON_THEME_CHANGED], 0);
}
+static void
+on_gtk_icon_theme_changed (GtkIconTheme *icon_theme,
+ StTextureCache *self)
+{
+ st_texture_cache_evict_icons (self);
+ g_signal_emit (self, signals[ICON_THEME_CHANGED], 0);
+}
+
static void
st_texture_cache_init (StTextureCache *self)
{
@@ -160,6 +168,8 @@ st_texture_cache_init (StTextureCache *self)
self->priv->icon_theme = gtk_icon_theme_new ();
gtk_icon_theme_add_resource_path (self->priv->icon_theme,
"/org/gnome/shell/theme/icons");
+ g_signal_connect (self->priv->icon_theme, "changed",
+ G_CALLBACK (on_gtk_icon_theme_changed), self);
settings = st_settings_get ();
g_signal_connect (settings, "notify::gtk-icon-theme",
@@ -1557,3 +1567,11 @@ st_texture_cache_get_default (void)
instance = g_object_new (ST_TYPE_TEXTURE_CACHE, NULL);
return instance;
}
+
+gboolean
+st_texture_cache_rescan_icon_theme (StTextureCache *cache)
+{
+ StTextureCachePrivate *priv = cache->priv;
+
+ return gtk_icon_theme_rescan_if_needed (priv->icon_theme);
+}
diff --git a/src/st/st-texture-cache.h b/src/st/st-texture-cache.h
index 11d1c4e640bfc1796ea4f2dc5bb6aea3a4d54131..a99316da8f6302c1944328ab2ed22bb639be6346 100644
--- a/src/st/st-texture-cache.h
+++ b/src/st/st-texture-cache.h
@@ -113,4 +113,6 @@ CoglTexture * st_texture_cache_load (StTextureCache *cache,
void *data,
GError **error);
+gboolean st_texture_cache_rescan_icon_theme (StTextureCache *cache);
+
#endif /* __ST_TEXTURE_CACHE_H__ */