const Graphene = imports.gi.Graphene; const GLib = imports.gi.GLib; const Gio = imports.gi.Gio; const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Clutter = imports.gi.Clutter; const St = imports.gi.St; const Shell = imports.gi.Shell; const Meta = imports.gi.Meta; const Lang = imports.lang; const Signals = imports.signals; const Params = imports.misc.params; const Config = imports.misc.config; const GnomeSession = imports.misc.gnomeSession; const AppDisplay = imports.ui.appDisplay; const AppFavorites = imports.ui.appFavorites; const Layout = imports.ui.layout; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; const DND = imports.ui.dnd; const IconGrid = imports.ui.iconGrid; const Util = imports.misc.util; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const PlaceDisplay = Me.imports.placeDisplay; const Convenience = Me.imports.convenience; const MENU_POPUP_TIMEOUT = 600; const ApplicationType = { APPLICATION: 0, PLACE: 1, RECENT: 2, APPSBUTTON: 3 }; const ShortcutsPanelOrientation = { OUTSIDE: 0, INSIDE: 1 }; let recentlyClickedAppLoopId = 0; let recentlyClickedApp = null; let recentlyClickedAppWindows = null; let recentlyClickedAppIndex = 0; // Filter out unnecessary windows, for instance nautilus desktop window. function getInterestingWindows(app) { return app.get_windows().filter((w) => { return !w.skip_taskbar; }); } /* Return the actual position reverseing left and right in rtl */ function getPosition(settings) { let position = settings.get_enum('dock-position'); if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL) { if (position == St.Side.LEFT) position = St.Side.RIGHT; else if (position == St.Side.RIGHT) position = St.Side.LEFT; } return position; } function getAppFromSource(source) { if (source instanceof AppDisplay.AppIcon) { return source.app; } else if (source instanceof ShortcutButton) { if (source._type == ApplicationType.APPLICATION) return source.app; else return null; } else { return null; } } function _getViewFromIcon(icon) { for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) { if (parent instanceof ShortcutsPanel) return parent; } return null; } var MyDragPlaceholderItem = GObject.registerClass( class WorkspacesToDock_MyDragPlaceholderItem extends St.Widget { _init() { super._init({ style_class: 'placeholder', opacity: 0, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER }); this._settings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); let iconSize = this._settings.get_double('shortcuts-panel-icon-size'); this.set_size(iconSize, iconSize); } }); var ShortcutButton = GObject.registerClass({ Signals: { 'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] }, 'sync-tooltip': {}, }, }, class WorkspacesToDock_ShortcutButton extends St.Button { _init(app, appType, panel) { super._init({ style_class: 'app-well-app workspacestodock-shortcut-button', pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), reactive: true, button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO, can_focus: true, }); this.app = app; //this.id = app.get_id(); //this.name = app.get_name(); this._type = appType; this._panel = panel; this._stateChangedId = 0; this._countChangedId = 0; this._maxN = 4; this._settings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); this._gDesktopInterfaceSettings = Convenience.getSettings('org.gnome.desktop.interface'); this._iconContainer = new St.Widget({ layout_manager: new Clutter.BinLayout(), x_expand: true, y_expand: true }); this.set_child(this._iconContainer); this._delegate = this; this._hasDndHover = false; this._folderPreviewId = 0; let iconParams = { setSizeManually: true, showLabel: false }; iconParams['createIcon'] = this._createIcon.bind(this); this.icon = new IconGrid.BaseIcon(null, iconParams); this._iconSize = this._settings.get_double('shortcuts-panel-icon-size'); this.icon.setIconSize(this._iconSize); this.icon.add_style_class_name('workspacestodock-shortcut-button-icon'); if (this._type == ApplicationType.PLACE) { this.icon.add_style_class_name('workspacestodock-shortcut-button-symbolic-icon'); } this._iconContainer.add_child(this.icon); this._dot = new St.Widget({ style_class: 'app-well-app-running-dot', layout_manager: new Clutter.BinLayout(), x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.END, }); this._iconContainer.add_child(this._dot); this.label_actor = this.icon.label; this._menu = null; this._menuManager = new PopupMenu.PopupMenuManager(this); if (this._type != ApplicationType.APPSBUTTON) { this._draggable = DND.makeDraggable(this); this._draggable.connect('drag-begin', () => { this._dragging = true; this.scaleAndFade(); 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; this.undoScaleAndFade(); Main.overview.endItemDrag(this); }); } this._dragMonitor = null; this._itemDragBeginId = Main.overview.connect( 'item-drag-begin', this._onDragBegin.bind(this)); this._itemDragEndId = Main.overview.connect( 'item-drag-end', this._onDragEnd.bind(this)); this._menuTimeoutId = 0; this._stateChangedId = 0; if (this._type == ApplicationType.APPSBUTTON) { this._stateChangedId = Main.overview.viewSelector._showAppsButton.connect('notify::checked', () => { this._onStateChanged.bind(this); }); } else if (this._type == ApplicationType.APPLICATION) { this._stateChangedId = this.app.connect('notify::state', () => { this._onStateChanged(); }); this._countChangedId = this.app.connect('windows-changed', () => { this._onCountChanged.bind(this); }); } this._dot.opacity = 0; this._onStateChanged(); this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { Main.overview.disconnect(this._itemDragBeginId); Main.overview.disconnect(this._itemDragEndId); if (this._stateChangedId > 0) { if (this._type == ApplicationType.APPSBUTTON) { Main.overview.viewSelector._showAppsButton.disconnect(this._stateChangedId); } else { this.app.disconnect(this._stateChangedId); } } this._stateChangedId = 0; if (this._countChangedId > 0) { if (this._type == ApplicationType.APPLICATION) { this.app.disconnect(this._countChangedId); } } this._countChangedId = 0; if (this._dragMonitor) { DND.removeDragMonitor(this._dragMonitor); this._dragMonitor = null; } if (this._draggable) { if (this._dragging) Main.overview.endItemDrag(this); this._draggable = null; } this._removeMenuTimeout(); } _createIcon(iconSize) { if (this._type == ApplicationType.APPLICATION) { return this.app.create_icon_texture(iconSize); } else if (this._type == ApplicationType.PLACE) { // Adjust 'places' symbolic icons by reducing their size // and setting a special class for button padding this._iconSize -= 4; this.actor.add_style_class_name('workspacestodock-shortcut-button-symbolic'); return new St.Icon({gicon: this.app.icon, icon_size: iconSize}); } else if (this._type == ApplicationType.RECENT) { let gicon = Gio.content_type_get_icon(this.app.mime); return new St.Icon({gicon: gicon, icon_size: iconSize}); } else if (this._type == ApplicationType.APPSBUTTON) { return new St.Icon({icon_name: 'view-app-grid-symbolic', icon_size: iconSize}); } } _removeMenuTimeout() { if (this._menuTimeoutId > 0) { GLib.source_remove(this._menuTimeoutId); this._menuTimeoutId = 0; } } _onStateChanged() { if (this._type == ApplicationType.APPSBUTTON) { if (Main.overview.viewSelector._showAppsButton.checked) { this.add_style_pseudo_class('checked'); } else { this.remove_style_pseudo_class('checked'); } } else if (this._type == ApplicationType.APPLICATION) { if (this.app.state != Shell.AppState.STOPPED) { if (!this._settings.get_boolean('shortcuts-panel-show-window-count-indicators')) { this._dot.opacity = 255; } this._onCountChanged(); } else { this._dot.opacity = 0; this._onCountChanged(); } } } _onCountChanged() { if (!this._settings.get_boolean('shortcuts-panel-show-window-count-indicators')) return; let appWindows = this.app.get_windows().filter((w) => { return !w.skip_taskbar; }); let n = appWindows.length; if (n > this._maxN) n = this._maxN; for (let i = 1; i <= this._maxN; i++) { let className = 'workspacestodock-shortcut-button-windowcount-image-'+i; if (i != n) { this.remove_style_class_name(className); } else { this.add_style_class_name(className); } } } _setPopupTimeout() { this._removeMenuTimeout(); this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MENU_POPUP_TIMEOUT, () => { this._menuTimeoutId = 0; this.popupMenu(); return GLib.SOURCE_REMOVE; }); GLib.Source.set_name_by_id(this._menuTimeoutId, '[gnome-shell] this.popupMenu'); } vfunc_leave_event(crossingEvent) { let ret = super.vfunc_leave_event(crossingEvent); this.fake_release(); this._removeMenuTimeout(); return ret; } vfunc_button_press_event(buttonEvent) { super.vfunc_button_press_event(buttonEvent); if (this._type == ApplicationType.APPSBUTTON || this._type == ApplicationType.APPLICATION) { if (buttonEvent.button == 1) { this._setPopupTimeout(); } else if (buttonEvent.button == 3) { this.popupMenu(); return Clutter.EVENT_STOP; } } return Clutter.EVENT_PROPAGATE; } vfunc_touch_event(touchEvent) { super.vfunc_touch_event(touchEvent); if (touchEvent.type == Clutter.EventType.TOUCH_BEGIN) this._setPopupTimeout(); return Clutter.EVENT_PROPAGATE; } vfunc_clicked(button) { this._removeMenuTimeout(); this.activate(button); } _onKeyboardPopupMenu() { this.popupMenu(); this._menu.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); } getId() { return this.app.get_id(); } popupMenu() { if (this._type != ApplicationType.APPSBUTTON && this._type != ApplicationType.APPLICATION) return false; this._removeMenuTimeout(); this.fake_release(); if (this._draggable) this._draggable.fakeRelease(); if (!this._menu) { this._menu = new ShortcutButtonMenu(this); this._menu.connect('activate-window', (menu, window) => { this.activateWindow(window); }); this._menu.connect('open-state-changed', (menu, isPoppedUp) => { if (!isPoppedUp) this._onMenuPoppedDown(); }); let id = Main.overview.connect('hiding', () => { this._menu.close(); }); this.connect('destroy', () => { Main.overview.disconnect(id); }); this._menuManager.addMenu(this._menu); } this.emit('menu-state-changed', true); this._panel.setPopupMenuFlag(true); this._panel.hideThumbnails(); this.set_hover(true); this._menu.popup(); this._menuManager.ignoreRelease(); this.emit('sync-tooltip'); return false; } activateWindow(metaWindow) { if (metaWindow) Main.activateWindow(metaWindow); else Main.overview.hide(); } _onMenuPoppedDown() { this.sync_hover(); this.emit('menu-state-changed', false); this._panel.setPopupMenuFlag(false); this._panel.showThumbnails(); } activate(button) { let event = Clutter.get_current_event(); let modifiers = event ? event.get_state() : 0; let isPrimaryButton = button && button == Clutter.BUTTON_PRIMARY; let isMiddleButton = button && button == Clutter.BUTTON_MIDDLE; let isCtrlPressed = (modifiers & Clutter.ModifierType.CONTROL_MASK) != 0; let openNewWindow = false; if (this._type == ApplicationType.APPLICATION) { openNewWindow = this.app.can_open_new_window() && this.app.state == Shell.AppState.RUNNING && (isCtrlPressed || isMiddleButton); } if (isPrimaryButton) { if (this._type == ApplicationType.APPLICATION) { // let tracker = Shell.WindowTracker.get_default(); // if (this.app.state == Shell.AppState.RUNNING) { // if (this.app == tracker.focus_app && !Main.overview._shown) { // this._cycleThroughWindows(); // } else { // // If we activate the app (this.app.activate), all app // // windows will come to the foreground. We only want to // // activate one window at a time // let windows = getInterestingWindows(this.app); // let w = windows[0]; // Main.activateWindow(w); // } // } else { // this.app.open_new_window(-1); // } if (this.app.state == Shell.AppState.STOPPED || openNewWindow) this.animateLaunch(); if (openNewWindow) this.app.open_new_window(-1); else this.app.activate(); Main.overview.hide(); } else if (this._type == ApplicationType.PLACE) { this.app.launch(global.get_current_time()); } else if (this._type == ApplicationType.RECENT) { Gio.app_info_launch_default_for_uri(this.app.uri, global.create_app_launch_context()); } else if (this._type == ApplicationType.APPSBUTTON) { if (Main.overview.visible) { if (Main.overview.viewSelector._showAppsButton.checked) { Main.overview.hide(); Main.overview.viewSelector._showAppsButton.checked = false; } else { Main.overview.viewSelector._showAppsButton.checked = true; } } else { // passingthru67: ISSUES #49 & #50 // Workaround issue by detecting animation status // Showing the overview after checking the showAppsButton fails // to animate when Gnome animations are enabled. On the other hand, // showing the overview before checking the showAppsButton fails // to scroll when Gnome animations are disabled. if (this._gDesktopInterfaceSettings.get_boolean('enable-animations')) { Main.overview.show(); Main.overview.viewSelector._showAppsButton.checked = true; } else { Main.overview.viewSelector._showAppsButton.checked = true; Main.overview.show(); } } } } else if (isMiddleButton) { if (this._type == ApplicationType.APPLICATION) { this.app.open_new_window(-1); } } return Clutter.EVENT_PROPAGATE; } _cycleThroughWindows() { // Store for a little amount of time last time app was clicked // since the order changes upon window interaction let MEMORY_TIME = 3000; let appWindows = getInterestingWindows(this.app); if(recentlyClickedAppLoopId>0) GLib.source_remove(recentlyClickedAppLoopId); recentlyClickedAppLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MEMORY_TIME, this._resetClickedApp); // If there isn't already a list of windows for the current app, // or the stored list is outdated, use the current windows list. if (!recentlyClickedApp || recentlyClickedApp.get_id() != this.app.get_id() || recentlyClickedAppWindows.length != appWindows.length ) { recentlyClickedApp = this.app; recentlyClickedAppWindows = appWindows; recentlyClickedAppIndex = 0; } recentlyClickedAppIndex ++; let index = recentlyClickedAppIndex % recentlyClickedAppWindows.length; let window = recentlyClickedAppWindows[index]; Main.activateWindow(window); } _resetClickedApp() { if(recentlyClickedAppLoopId>0) GLib.source_remove(recentlyClickedAppLoopId); recentlyClickedAppLoopId=0; recentlyClickedApp =null; recentlyClickedAppWindows = null; recentlyClickedAppIndex = 0; return false; } animateLaunch() { this.icon.animateZoomOut(); } animateLaunchAtPos(x, y) { this.icon.animateZoomOutAtPos(x, y); } scaleIn() { this.scale_x = 0; this.scale_y = 0; this.ease({ scale_x: 1, scale_y: 1, duration: APP_ICON_SCALE_IN_TIME, delay: APP_ICON_SCALE_IN_DELAY, mode: Clutter.AnimationMode.EASE_OUT_QUINT, }); } shellWorkspaceLaunch(params) { let { stack } = new Error(); log('shellWorkspaceLaunch is deprecated, use app.open_new_window() instead\n%s'.format(stack)); params = Params.parse(params, { workspace: -1, timestamp: 0 }); if (this._type == ApplicationType.APPLICATION) { this.app.open_new_window(params.workspace); } else if (this._type == ApplicationType.PLACE) { this.app.launch(global.get_current_time(), params.workspace); } else if (this._type == ApplicationType.RECENT) { Gio.app_info_launch_default_for_uri(this.app.uri, global.create_app_launch_context()); } else if (this._type == ApplicationType.APPSBUTTON) { if (Main.overview.visible) { if (Main.overview.viewSelector._showAppsButton.checked) { Main.overview.hide(); Main.overview.viewSelector._showAppsButton.checked = false; } else { Main.overview.viewSelector._showAppsButton.checked = true; } } else { Main.overview.show(); Main.overview.viewSelector._showAppsButton.checked = true; } } } getDragActor() { let appIcon; if (this._type == ApplicationType.APPLICATION) { appIcon = this.app.create_icon_texture(this._iconSize); } else if (this._type == ApplicationType.PLACE) { appIcon = new St.Icon({gicon: this.app.icon, icon_size: this._iconSize}); } else if (this._type == ApplicationType.RECENT) { let gicon = Gio.content_type_get_icon(this.app.mime); appIcon = new St.Icon({gicon: gicon, icon_size: this._iconSize}); } else if (this._type == ApplicationType.APPSBUTTON) { appIcon = new St.Icon({icon_name: 'view-app-grid-symbolic', icon_size: iconSize}); } return appIcon; } // Returns the original actor that should align with the actor // we show as the item is being dragged. getDragActorSource() { return this.icon.icon; } shouldShowTooltip() { return this.hover && (!this._menu || !this._menu.isOpen); } scaleAndFade() { this.reactive = false; this.ease({ scale_x: 0.75, scale_y: 0.75, opacity: 128, }); } undoScaleAndFade() { this.reactive = true; this.ease({ scale_x: 1.0, scale_y: 1.0, opacity: 255, }); } _canAccept(source) { return false; // let view = _getViewFromIcon(source); // // return source != this && // (source instanceof this.constructor) && // (view instanceof AllView); } _setHoveringByDnd(hovering) { // if (hovering) { // if (this._folderPreviewId > 0) // return; // // this._folderPreviewId = // GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { // this.add_style_pseudo_class('drop'); // this._showFolderPreview(); // this._folderPreviewId = 0; // return GLib.SOURCE_REMOVE; // }); // } else { // if (this._folderPreviewId > 0) { // GLib.source_remove(this._folderPreviewId); // this._folderPreviewId = 0; // } // this._hideFolderPreview(); // this.remove_style_pseudo_class('drop'); // } } _onDragBegin() { this._dragMonitor = { dragMotion: this._onDragMotion.bind(this), }; DND.addDragMonitor(this._dragMonitor); } _onDragMotion(dragEvent) { let target = dragEvent.targetActor; let isHovering = target == this || this.contains(target); let canDrop = this._canAccept(dragEvent.source); let hasDndHover = isHovering && canDrop; if (this._hasDndHover != hasDndHover) { // this._setHoveringByDnd(hasDndHover); this._hasDndHover = hasDndHover; } return DND.DragMotionResult.CONTINUE; } _onDragEnd() { this.remove_style_pseudo_class('drop'); DND.removeDragMonitor(this._dragMonitor); } handleDragOver(source) { if (source == this) return DND.DragMotionResult.NO_DROP; if (!this._canAccept(source)) return DND.DragMotionResult.CONTINUE; return DND.DragMotionResult.MOVE_DROP; } acceptDrop(source) { // this._setHoveringByDnd(false); if (!this._canAccept(source)) return false; let view = _getViewFromIcon(this); let apps = [this.id, source.id]; return view.createFolder(apps); } }); var ShortcutButtonMenu = class WorkspacesToDock_ShortcutButtonMenu extends PopupMenu.PopupMenu { constructor(source) { let settings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); let side = getPosition(settings); super(source, 0.5, side); this._settings = settings; // We want to keep the item hovered while the menu is up this.blockSourceEvents = true; this._source = source; this.actor.add_style_class_name('app-well-menu'); // Chain our visibility and lifecycle to that of the source this._sourceMappedId = source.connect('notify::mapped', () => { if (!source.mapped) this.close(); }); source.connect('destroy', () => { source.disconnect(this._sourceMappedId); this.destroy(); }); Main.uiGroup.add_actor(this.actor); } _redisplay() { this.removeAll(); // passingthru67: appsbutton menu to show extension preferences if (this._source._type == ApplicationType.APPSBUTTON) { let item = this._appendMenuItem(_("Extension Preferences")); item.connect('activate', () => { if (typeof ExtensionUtils.openPrefs === 'function') { ExtensionUtils.openPrefs(); } else { Util.spawn(["gnome-shell-extension-prefs", Me.metadata.uuid]); } }); return; } let windows = this._source.app.get_windows().filter( w => !w.skip_taskbar ); if (windows.length > 0) { this.addMenuItem( /* Translators: This is the heading of a list of open windows */ new PopupMenu.PopupSeparatorMenuItem(_("Open Windows")) ); } windows.forEach(window => { let title = window.title ? window.title : this._source.app.get_name(); let item = this._appendMenuItem(title); item.connect('activate', () => { this.emit('activate-window', window); }); }); if (!this._source.app.is_window_backed()) { this._appendSeparator(); let appInfo = this._source.app.get_app_info(); let actions = appInfo.list_actions(); if (this._source.app.can_open_new_window() && !actions.includes('new-window')) { this._newWindowMenuItem = this._appendMenuItem(_("New Window")); this._newWindowMenuItem.connect('activate', () => { this._source.animateLaunch(); this._source.app.open_new_window(-1); this.emit('activate-window', null); }); this._appendSeparator(); } // if (discreteGpuAvailable && // this._source.app.state == Shell.AppState.STOPPED) { // this._onDiscreteGpuMenuItem = this._appendMenuItem(_("Launch using Dedicated Graphics Card")); // this._onDiscreteGpuMenuItem.connect('activate', () => { // this._source.animateLaunch(); // this._source.app.launch(0, -1, true); // this.emit('activate-window', null); // }); // } for (let i = 0; i < actions.length; i++) { let action = actions[i]; let item = this._appendMenuItem(appInfo.get_action_name(action)); item.connect('activate', (emitter, event) => { if (action == 'new-window') this._source.animateLaunch(); this._source.app.launch_action(action, event.get_time(), -1); this.emit('activate-window', null); }); } let canFavorite = global.settings.is_writable('favorite-apps'); if (canFavorite) { this._appendSeparator(); let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); if (isFavorite) { let item = this._appendMenuItem(_("Remove from Favorites")); item.connect('activate', () => { let favs = AppFavorites.getAppFavorites(); favs.removeFavorite(this._source.app.get_id()); }); } else { let item = this._appendMenuItem(_("Add to Favorites")); item.connect('activate', () => { let favs = AppFavorites.getAppFavorites(); favs.addFavorite(this._source.app.get_id()); }); } } if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop')) { this._appendSeparator(); let item = this._appendMenuItem(_("Show Details")); item.connect('activate', () => { let id = this._source.app.get_id(); let args = GLib.Variant.new('(ss)', [id, '']); Gio.DBus.get(Gio.BusType.SESSION, null, (o, res) => { let bus = Gio.DBus.get_finish(res); bus.call('org.gnome.Software', '/org/gnome/Software', 'org.gtk.Actions', 'Activate', GLib.Variant.new('(sava{sv})', ['details', [args], null]), null, 0, -1, null, null); Main.overview.hide(); }); }); } } } _appendSeparator() { let separator = new PopupMenu.PopupSeparatorMenuItem(); this.addMenuItem(separator); } _appendMenuItem(labelText) { // FIXME: app-well-menu-item style let item = new PopupMenu.PopupMenuItem(labelText); this.addMenuItem(item); return item; } popup(_activatingButton) { this._redisplay(); if (this._settings.get_boolean('shortcuts-panel-popupmenu-arrow-at-top')) { this._arrowAlignment = 0.0; } else { this._arrowAlignment = 0.5; } this.open(); } }; Signals.addSignalMethods(ShortcutButtonMenu.prototype); var ShortcutsPanel = class WorkspacesToDock_ShortcutsPanel { constructor(dock) { this._dock = dock; this._settings = Convenience.getSettings('org.gnome.shell.extensions.workspaces-to-dock'); this._position = getPosition(this._settings); this._isHorizontal = (this._position == St.Side.TOP || this._position == St.Side.BOTTOM); let packVertical = true; if (this._isHorizontal) packVertical = false; // Set _centerContainer property if (this._settings.get_boolean('customize-height') && this._settings.get_boolean('center-thumbnails-on-dock')) { this._centerContainer = true; } else { this._centerContainer = false; } // Set _centerPanelsIndependently property if (this._centerContainer && this._settings.get_int('center-thumbnails-option') == 0) { this._centerPanelsIndependently = true; } else { this._centerPanelsIndependently = false; } this.actor = new St.BoxLayout({ style_class: 'workspace-thumbnails workspacestodock-shortcuts-panel', vertical: packVertical, clip_to_allocation: true, x_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START, y_align: (this._centerContainer && this._centerPanelsIndependently) ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START }); this.actor._delegate = this; this._appSystem = Shell.AppSystem.get_default(); this._appFavorites = AppFavorites.getAppFavorites(); this._installedChangedId = this._appSystem.connect('installed-changed', () => { this._appFavorites.reload(); this.refresh(); }); // Connect to AppSystem and listen for app state changes this._appStateChangedId = this._appSystem.connect('app-state-changed', this._updateRunningApps.bind(this)); // Connect to AppFavorites and listen for favorites changes this._favoritesChangedId = this._appFavorites.connect('changed', this._queueUpdateFavoriteApps.bind(this)); // Connect to item drag signals this._dragPlaceholder = null; this._dragPlaceholderPos = -1; this._itemDragBeginId = Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this)); this._itemDragEndId = Main.overview.connect('item-drag-end', this._onDragEnd.bind(this)); this._itemDragCancelId = Main.overview.connect('item-drag-cancelled', this._onDragCancelled.bind(this)); // Bind Preference Settings this._bindSettingsChanges(); // Populate panel this._populate(); } destroy() { // Disconnect global signals if (this._itemDragBeginId > 0) Main.overview.disconnect(this._itemDragBeginId); if (this._itemDragEndId > 0) Main.overview.disconnect(this._itemDragEndId); if (this._itemDragCancelId > 0) Main.overview.disconnect(this._itemDragCancelId); if (this._installedChangedId > 0) this._appSystem.disconnect(this._installedChangedId); if (this._appStateChangedId > 0) this._appSystem.disconnect(this._appStateChangedId); if (this._favoritesChangedId > 0) this._appFavorites.disconnect(this._favoritesChangedId); // Destroy main clutter actor this.actor.destroy_all_children(); this.actor.destroy(); // Disconnect GSettings signals this._settings.run_dispose(); } _bindSettingsChanges() { this._settings.connect('changed::shortcuts-panel-show-running', () => { this.refresh(); }); this._settings.connect('changed::shortcuts-panel-show-places', () => { this.refresh(); }); this._settings.connect('changed::shortcuts-panel-show-window-count-indicators', () => { this.refresh(); }); this._settings.connect('changed::shortcuts-panel-appsbutton-at-bottom', () => { this.refresh(); }); } _onDragBegin() { this._dragCancelled = false; this._dragMonitor = { dragMotion: this._onDragMotion.bind(this) }; DND.addDragMonitor(this._dragMonitor); } _onDragCancelled() { this._dragCancelled = true; this._endDrag(); } _onDragEnd() { if (this._dragCancelled) return; this._endDrag(); } _endDrag() { this._clearDragPlaceholder(); DND.removeDragMonitor(this._dragMonitor); } _onDragMotion(dragEvent) { let app = getAppFromSource(dragEvent.source); if (app == null) return DND.DragMotionResult.CONTINUE; if (!this.actor.contains(dragEvent.targetActor)) this._clearDragPlaceholder(); return DND.DragMotionResult.CONTINUE; } _clearDragPlaceholder() { if (this._dragPlaceholder) { this._dragPlaceholder.destroy(); this._dragPlaceholder = null; } this._dragPlaceholderPos = -1; } handleDragOver(source, actor, x, y, time) { let app = getAppFromSource(source); // Don't allow favoriting of transient apps if (app == null || app.is_window_backed()) return DND.DragMotionResult.NO_DROP; if (!global.settings.is_writable('favorite-apps')) return DND.DragMotionResult.NO_DROP; let favorites = AppFavorites.getAppFavorites().getFavorites(); let numFavorites = favorites.length; let favPos = favorites.indexOf(app); let children = this._favoriteAppsBox.get_children(); let numChildren = children.length; let boxH; let boxY; if (this._isHorizontal) { boxY = this._favoriteAppsBox.x; boxH = this._favoriteAppsBox.width; } else { boxY = this._favoriteAppsBox.y; boxH = this._favoriteAppsBox.height; } // Keep the placeholder out of the index calculation; assuming that // the remove target has the same size as "normal" items, we don't // need to do the same adjustment there. if (this._dragPlaceholder) { if (this._isHorizontal) { boxH -= this._dragPlaceholder.width; } else { boxH -= this._dragPlaceholder.height; } numChildren--; } let pos; let posY; if (this._isHorizontal) { posY = x - boxY; } else { posY = y - boxY; } pos = Math.floor(posY * numChildren / boxH); if (pos != this._dragPlaceholderPos && pos <= numFavorites) { this._dragPlaceholderPos = pos; // Don't allow positioning before or after self if (favPos != -1 && (pos == favPos || pos == favPos + 1)) { this._clearDragPlaceholder(); return DND.DragMotionResult.CONTINUE; } // If the placeholder already exists, we just move // it, but if we are adding it, expand its size in // an animation // Passingthru67 TODO: // Need to convert the dragPlaceholder to an St.Widget so that // we can use the fadein animation let fadeIn; if (this._dragPlaceholder) { this._dragPlaceholder.destroy(); fadeIn = false; } else { fadeIn = true; } this._dragPlaceholder = new MyDragPlaceholderItem(); this._favoriteAppsBox.insert_child_at_index(this._dragPlaceholder, this._dragPlaceholderPos); // this._dragPlaceholder.show(fadeIn); this._dragPlaceholder.show(); } // Remove the drag placeholder if we are not in the // "favorites zone" if (pos > numFavorites) this._clearDragPlaceholder(); if (!this._dragPlaceholder) return DND.DragMotionResult.NO_DROP; let srcIsFavorite = (favPos != -1); if (srcIsFavorite) { return DND.DragMotionResult.MOVE_DROP; } return DND.DragMotionResult.COPY_DROP; } // Draggable target interface acceptDrop(source, actor, x, y, time) { let app = getAppFromSource(source); // Don't allow favoriting of transient apps if (app == null || app.is_window_backed()) { return false; } if (!global.settings.is_writable('favorite-apps')) return false; let id = app.get_id(); let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); let srcIsFavorite = (id in favorites); let favPos = 0; let children = this._favoriteAppsBox.get_children(); for (let i = 0; i < this._dragPlaceholderPos; i++) { if (this._dragPlaceholder && children[i] == this._dragPlaceholder) continue; let childId = children[i]._delegate.app.get_id(); if (childId == id) { continue; } if (childId in favorites) { favPos++; } } // No drag placeholder means we don't wan't to favorite the app // and we are dragging it to its original position if (!this._dragPlaceholder) return true; Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { let appFavorites = AppFavorites.getAppFavorites(); if (srcIsFavorite) appFavorites.moveFavoriteToPos(id, favPos); else appFavorites.addFavoriteAtPos(id, favPos); return false; }); this._clearDragPlaceholder(); return true; } setPopupMenuFlag(showing) { this._dock.setPopupMenuFlag(showing); } hideThumbnails() { if (this._settings.get_boolean('shortcuts-panel-popupmenu-hide-thumbnails')) { if (this._settings.get_enum('shortcuts-panel-orientation') == ShortcutsPanelOrientation.OUTSIDE) { this._dock._thumbnailsBox.actor.opacity = 0; this.actor.remove_style_class_name('workspacestodock-shortcuts-panel'); this.actor.add_style_class_name('workspacestodock-shortcuts-panel-popupmenu'); // for (let i = 0; i < this._dock._thumbnailsBox._thumbnails.length; i++) { // this._dock._thumbnailsBox._thumbnails[i].actor.opacity = 0; // } // this._dock._thumbnailsBox._indicator.opacity = 0; } } } showThumbnails() { if (this._settings.get_boolean('shortcuts-panel-popupmenu-hide-thumbnails')) { this._dock._thumbnailsBox.actor.opacity = 255; this.actor.remove_style_class_name('workspacestodock-shortcuts-panel-popupmenu'); this.actor.add_style_class_name('workspacestodock-shortcuts-panel'); // for (let i = 0; i < this._dock._thumbnailsBox._thumbnails.length; i++) { // this._dock._thumbnailsBox._thumbnails[i].actor.opacity = 255; // } // this._dock._thumbnailsBox._indicator.opacity = 255; } } setReactiveState(state) { if (state == null) return; // Deactive Apps button this._appsButton.reactive = state; // Deactivate favorites if (this._favoriteAppsBox) { let children = this._favoriteAppsBox.get_children(); for (let i = 0; i < children.length; i++) { children[i].reactive = state; } } // Deactivate running apps if (this._runningAppsBox) { let children = this._runningAppsBox.get_children(); for (let i = 0; i < children.length; i++) { children[i].reactive = state; } } // Deactivate places if (this._placesBox) { let children = this._placesBox.get_children(); for (let i = 0; i < children.length; i++) { children[i].reactive = state; } } } refresh() { this._clear(); this._populate(); } _clear() { this.actor.destroy_all_children(); } _populate() { let packVertical = true; if (this._isHorizontal) packVertical = false; // Add Favorite Apps Box this._favoriteAppsBox = new St.BoxLayout({ vertical: packVertical, style_class: 'workspacestodock-shortcuts-panel workspacestodock-shortcuts-panel-favorites' }); this.actor.add_actor(this._favoriteAppsBox); this._favoriteAppsWorkId = Main.initializeDeferredWork(this._favoriteAppsBox, this._updateFavoriteApps.bind(this)); // Add Running Apps Box if (this._settings.get_boolean('shortcuts-panel-show-running')) { this._runningAppsBox = new St.BoxLayout({ vertical: packVertical, style_class: 'workspacestodock-shortcuts-panel workspacestodock-shortcuts-panel-running' }); this.actor.add_actor(this._runningAppsBox); this._updateRunningApps(); } if (this._settings.get_boolean('shortcuts-panel-show-places')) { this._placesBox = new St.BoxLayout({ vertical: packVertical, style_class: 'workspacestodock-shortcuts-panel workspacestodock-shortcuts-panel-places' }); this.actor.add_actor(this._placesBox); // Get places let placesManager = new PlaceDisplay.PlacesManager(); let special = placesManager.get('special'); let allPlaces = []; allPlaces = allPlaces.concat(special); // Add places to Places Box for (let i = 0; i < allPlaces.length; ++i) { let app = allPlaces[i]; let shortcutButton = new ShortcutButton(app, ApplicationType.PLACE); this._placesBox.add_actor(shortcutButton); } } // Add Apps Button to top or bottom of shortcuts panel this._appsButton = new ShortcutButton(null, ApplicationType.APPSBUTTON, this); if (this._settings.get_boolean('shortcuts-panel-appsbutton-at-bottom')) { let filler = new St.Widget({ style_class: 'popup-separator-menu-item workspacestodock-shortcut-panel-filler', x_expand: true, y_expand: true }); this.actor.add_actor(filler); this.actor.add_actor(this._appsButton); } else { this.actor.insert_child_at_index(this._appsButton, 0); } } _queueUpdateFavoriteApps () { Main.queueDeferredWork(this._favoriteAppsWorkId); } _updateFavoriteApps() { if (!this._favoriteAppsBox) return; // Clear favorite apps box this._favoriteAppsBox.destroy_all_children(); // Apps supposed to be in the favorite apps box let newApps = []; // Get favorites let favorites = this._appFavorites.getFavoriteMap(); for (let id in favorites) { newApps.push(favorites[id]); } // Populate shortcuts panel with favorites for (let i = 0; i < newApps.length; ++i) { let app = newApps[i]; let shortcutButton = new ShortcutButton(app, ApplicationType.APPLICATION, this); this._favoriteAppsBox.add_actor(shortcutButton); } this.emit('update-favorite-apps'); this._updateRunningApps(); } _updateRunningApps() { if (!this._runningAppsBox) return; let children = this._runningAppsBox.get_children().filter((actor) => { return actor && actor._delegate && actor._delegate.app && actor._delegate._type == ApplicationType.APPLICATION; }); // Apps currently in running apps box let oldApps = children.map(function(actor) { return actor._delegate.app; }); // Apps supposed to be in the running apps box let newApps = []; // Get favorites let favorites = this._appFavorites.getFavoriteMap(); // Get running apps let running = this._appSystem.get_running(); for (let i = 0; i < running.length; i++) { let app = running[i]; if (app.get_id() in favorites) continue; newApps.push(app); } // Figure out the actual changes to the list of items; we iterate // over both the list of items currently in the dash and the list // of items expected there, and collect additions and removals. // Moves are both an addition and a removal, where the order of // the operations depends on whether we encounter the position // where the item has been added first or the one from where it // was removed. // There is an assumption that only one item is moved at a given // time; when moving several items at once, everything will still // end up at the right position, but there might be additional // additions/removals (e.g. it might remove all the launchers // and add them back in the new order even if a smaller set of // additions and removals is possible). // If above assumptions turns out to be a problem, we might need // to use a more sophisticated algorithm, e.g. Longest Common // Subsequence as used by diff. let addedItems = []; let removedActors = []; let newIndex = 0; let oldIndex = 0; while (newIndex < newApps.length || oldIndex < oldApps.length) { let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null; let newApp = newApps.length > newIndex ? newApps[newIndex] : null; // No change at oldIndex/newIndex if (oldApp == newApp) { oldIndex++; newIndex++; continue; } // App removed at oldIndex if (oldApp && newApps.indexOf(oldApp) == -1) { removedActors.push(children[oldIndex]); oldIndex++; continue; } // App added at newIndex if (newApp && oldApps.indexOf(newApp) == -1) { addedItems.push({ app: newApp, item: this._createShortcutButton(newApp, ApplicationType.APPLICATION), pos: newIndex }); newIndex++; continue; } // App moved let nextApp = newApps.length > newIndex + 1 ? newApps[newIndex + 1] : null; let insertHere = nextApp && nextApp == oldApp; let alreadyRemoved = removedActors.reduce((result, actor) => { let removedApp = actor.child._delegate.app; return result || removedApp == newApp; }, false); if (insertHere || alreadyRemoved) { let newItem = this._createShortcutButton(newApp, shortcutType); addedItems.push({ app: newApp, item: newItem, pos: newIndex + removedActors.length }); newIndex++; } else { removedActors.push(children[oldIndex]); oldIndex++; } } for (let i = 0; i < addedItems.length; i++) this._runningAppsBox.insert_child_at_index(addedItems[i].item, addedItems[i].pos); for (let i = 0; i < removedActors.length; i++) { let item = removedActors[i]; item.destroy(); } this.emit('update-running-apps'); } _createShortcutButton(app, appType) { let shortcutType = app ? appType : ApplicationType.APPSBUTTON; let shortcutButton = new ShortcutButton(app, shortcutType, this); return shortcutButton; } }; Signals.addSignalMethods(ShortcutsPanel.prototype);