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__ */