From 586d72f1fe8628cfbbaeee556f9c688762ae0201 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Wed, 2 Feb 2011 18:09:20 +0100 Subject: [PATCH] Add dock extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an extension which shows a dock on the right, contributed by Tor-björn Claesson. --- configure.ac | 5 +- extensions/Makefile.am | 2 +- extensions/dock/Makefile.am | 3 + extensions/dock/extension.js | 554 +++++++++++++++++++++++++++++++ extensions/dock/metadata.json.in | 8 + extensions/dock/stylesheet.css | 54 +++ 6 files changed, 623 insertions(+), 3 deletions(-) create mode 100644 extensions/dock/Makefile.am create mode 100644 extensions/dock/extension.js create mode 100644 extensions/dock/metadata.json.in create mode 100644 extensions/dock/stylesheet.css diff --git a/configure.ac b/configure.ac index 61e8986d..4dafcf5f 100644 --- a/configure.ac +++ b/configure.ac @@ -21,7 +21,7 @@ GLIB_GSETTINGS ADDITIONAL_PACKAGES= dnl keep this in sync with extensions/Makefile.am -ALL_EXTENSIONS="example alternate-tab xrandr-indicator windowsNavigator auto-move-windows" +ALL_EXTENSIONS="example alternate-tab xrandr-indicator windowsNavigator auto-move-windows dock" AC_ARG_ENABLE([extensions], [AS_HELP_STRING([--enable-extensions],[Space separated list of extensions to enable. Default is that all extensions are built.])], [], @@ -34,7 +34,7 @@ for e in $enable_extensions; do ENABLED_EXTENSIONS="$ENABLED_EXTENSIONS $e" ADDITIONAL_PACKAGES="gnome-desktop-3.0 >= 2.91.6" ;; - alternate-tab|example|windowsNavigator|auto-move-windows) + alternate-tab|example|windowsNavigator|auto-move-windows|dock) ENABLED_EXTENSIONS="$ENABLED_EXTENSIONS $e" ;; *) @@ -55,6 +55,7 @@ dnl Please keep this sorted alphabetically AC_CONFIG_FILES([ extensions/alternate-tab/Makefile extensions/auto-move-windows/Makefile + extensions/dock/Makefile extensions/example/Makefile extensions/windowsNavigator/Makefile extensions/xrandr-indicator/Makefile diff --git a/extensions/Makefile.am b/extensions/Makefile.am index 0bcbb85b..21524cc5 100644 --- a/extensions/Makefile.am +++ b/extensions/Makefile.am @@ -1,3 +1,3 @@ -DIST_SUBDIRS = example alternate-tab xrandr-indicator windowsNavigator auto-move-windows +DIST_SUBDIRS = example alternate-tab xrandr-indicator windowsNavigator auto-move-windows dock SUBDIRS = $(ENABLED_EXTENSIONS) diff --git a/extensions/dock/Makefile.am b/extensions/dock/Makefile.am new file mode 100644 index 00000000..25318582 --- /dev/null +++ b/extensions/dock/Makefile.am @@ -0,0 +1,3 @@ +EXTENSION_ID = dock + +include ../../extension.mk diff --git a/extensions/dock/extension.js b/extensions/dock/extension.js new file mode 100644 index 00000000..0ab4dc03 --- /dev/null +++ b/extensions/dock/extension.js @@ -0,0 +1,554 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Pango = imports.gi.Pango; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Shell = imports.gi.Shell; +const Lang = imports.lang; +const Signals = imports.signals; +const St = imports.gi.St; +const Mainloop = imports.mainloop; + +const AppFavorites = imports.ui.appFavorites; +const DND = imports.ui.dnd; +const Main = imports.ui.main; +const Overview = imports.ui.overview; +const PopupMenu = imports.ui.popupMenu; +const Search = imports.ui.search; +const Tweener = imports.ui.tweener; +const Workspace = imports.ui.workspace; +const AppDisplay = imports.ui.appDisplay; +const AltTab = imports.ui.altTab; + +const Gettext = imports.gettext.domain('gnome-shell-extensions'); +const _ = Gettext.gettext; + +const DOCKICON_SIZE = 48; +const DND_RAISE_APP_TIMEOUT = 500; + +function Dock() { + this._init(); +} + +Dock.prototype = { + _init : function() { + this._placeholderText = null; + this._menus = []; + this._menuDisplays = []; + + this._favorites = []; + + this._spacing = 4; + this._item_size = DOCKICON_SIZE; + + this.actor = new St.BoxLayout({ name: 'dock', vertical: true, reactive: true }); + //this.actor._delegate = this; + this._grid = new Shell.GenericContainer(); + this.actor.add(this._grid, { expand: true, y_align: St.Align.START }); + this.actor.connect('style-changed', Lang.bind(this, this._onStyleChanged)); + + this._grid.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this._grid.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this._grid.connect('allocate', Lang.bind(this, this._allocate)); + + this._workId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._redisplay)); + + this._tracker = Shell.WindowTracker.get_default(); + this._appSystem = Shell.AppSystem.get_default(); + + this._appSystem.connect('installed-changed', Lang.bind(this, this._queueRedisplay)); + AppFavorites.getAppFavorites().connect('changed', Lang.bind(this, this._queueRedisplay)); + this._tracker.connect('app-state-changed', Lang.bind(this, this._queueRedisplay)); + + Main.chrome.addActor(this.actor, { visibleInOverview: false }); + }, + + _appIdListToHash: function(apps) { + let ids = {}; + for (let i = 0; i < apps.length; i++) + ids[apps[i].get_id()] = apps[i]; + return ids; + }, + + _queueRedisplay: function () { + Main.queueDeferredWork(this._workId); + }, + + _redisplay: function () { + this.removeAll(); + + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); + + /* hardcode here pending some design about how exactly desktop contexts behave */ + let contextId = ''; + + let running = this._tracker.get_running_apps(contextId); + let runningIds = this._appIdListToHash(running); + + let icons = 0; + + let nFavorites = 0; + for (let id in favorites) { + let app = favorites[id]; + let display = new DockIcon(app); + this.addItem(display.actor); + nFavorites++; + icons++; + } + + for (let i = 0; i < running.length; i++) { + let app = running[i]; + if (app.get_id() in favorites) + continue; + let display = new DockIcon(app); + icons++; + this.addItem(display.actor); + } + if (this._placeholderText) { + this._placeholderText.destroy(); + this._placeholderText = null; + } + + if (running.length == 0 && nFavorites == 0) { + this._placeholderText = new St.Label({ text: _("Drag here to add favorites") }); + this.actor.add_actor(this._placeholderText); + } + + let primary = global.get_primary_monitor(); + let height = (icons)*(this._item_size + this._spacing) + 2*this._spacing; + this.actor.set_size(this._item_size + 4*this._spacing, height); + this.actor.set_position(primary.width-this._item_size-this._spacing-2, (primary.height-height)/2); + }, + + _getPreferredWidth: function (grid, forHeight, alloc) { + alloc.min_size = this._item_size; + alloc.natural_size = this._item_size + this._spacing; + }, + + _getPreferredHeight: function (grid, forWidth, alloc) { + let children = this._grid.get_children(); + let nRows = children.length; + let totalSpacing = Math.max(0, nRows - 1) * this._spacing; + let height = nRows * this._item_size + totalSpacing; + alloc.min_size = height; + alloc.natural_size = height; + }, + + _allocate: function (grid, box, flags) { + let children = this._grid.get_children(); + + let x = box.x1 + this._spacing; + let y = box.y1 + this._spacing; + + for (let i = 0; i < children.length; i++) { + let childBox = new Clutter.ActorBox(); + childBox.x1 = x; + childBox.y1 = y; + childBox.x2 = childBox.x1 + this._item_size; + childBox.y2 = childBox.y1 + this._item_size; + children[i].allocate(childBox, flags); + y += this._item_size + this._spacing; + } + }, + + + _onStyleChanged: function() { + let themeNode = this.actor.get_theme_node(); + let [success, len] = themeNode.get_length('spacing', false); + if (success) + this._spacing = len; + [success, len] = themeNode.get_length('-shell-grid-item-size', false); + if (success) + this._item_size = len; + this._grid.queue_relayout(); + }, + + removeAll: function () { + this._grid.get_children().forEach(Lang.bind(this, function (child) { + child.destroy(); + })); + }, + + addItem: function(actor) { + this._grid.add_actor(actor); + } +}; +Signals.addSignalMethods(Dock.prototype); + +function DockIcon(app) { + this._init(app); +} + +DockIcon.prototype = { + _init : function(app) { + this.app = app; + this.actor = new St.Button({ style_class: 'dock-app', + button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO, + reactive: true, + x_fill: true, + y_fill: true }); + this.actor._delegate = this; + this.actor.set_size(DOCKICON_SIZE, DOCKICON_SIZE); + + this._icon = this.app.create_icon_texture(DOCKICON_SIZE); + this.actor.set_child(this._icon); + + this.actor.connect('clicked', Lang.bind(this, this._onClicked)); + + this._menu = null; + this._menuManager = new PopupMenu.PopupMenuManager(this); + + this._has_focus = false; + + let tracker = Shell.WindowTracker.get_default(); + tracker.connect('notify::focus-app', Lang.bind(this, this._onStateChanged)); + + this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + this.actor.connect('notify::hover', Lang.bind(this, this._hoverChanged)); + + //this._dragDropTimeoutId = 0; + this._menuTimeoutId = 0; + this._stateChangedId = this.app.connect('notify::state', + Lang.bind(this, this._onStateChanged)); + this._onStateChanged(); + }, + + _onDestroy: function() { + if (this._stateChangedId > 0) + this.app.disconnect(this._stateChangedId); + this._stateChangedId = 0; + this._removeMenuTimeout(); + }, + + _removeMenuTimeout: function() { + if (this._menuTimeoutId > 0) { + Mainloop.source_remove(this._menuTimeoutId); + this._menuTimeoutId = 0; + } + }, + + _hoverChanged: function(actor) { + if (actor != this.actor) { + this._has_focus = false; + // this._removeDragDropTimeout(); + } else { + this._has_focus = true; + /* + if (window_chooser) { + windows = this.app.get_windows(); + if (windows.length > 1) { + window_chooser.show(windows, this.actor); + } else { + window_chooser.hide(); + } + }*/ + } + return false; + }, + + /* + _removeDragDropTimeout: function() { + if (this._dragDropTimeoutId > 0) { + Mainloop.source_remove(this._dragDropTimeoutId); + this._dragDropTimeoutId = 0; + } + }, + + _startDNDTimeout: function() { + this._menuTimeoutId = Mainloop.timeout_add(DND_RAISE_APP_TIMEOUT, + Lang.bind(this, function() { + if (this.app.state == Shell.AppState.RUNNING) + this.app.activate(); + })); + }, + */ + + _onStateChanged: function() { + let tracker = Shell.WindowTracker.get_default(); + let focusedApp = tracker.focus_app; + if (this.app.state != Shell.AppState.STOPPED) { + this.actor.add_style_class_name('running'); + if (this.app == focusedApp) { + this.actor.add_style_class_name('focused'); + } else { + this.actor.remove_style_class_name('focused'); + } + } else { + this.actor.remove_style_class_name('focused'); + this.actor.remove_style_class_name('running'); + } + }, + + _onButtonPress: function(actor, event) { + let button = event.get_button(); + if (button == 1) { + this._removeMenuTimeout(); + this._menuTimeoutId = Mainloop.timeout_add(AppDisplay.MENU_POPUP_TIMEOUT, Lang.bind(this, function() { + this.popupMenu(); + })); + } + }, + + _onClicked: function(actor, button) { + this._removeMenuTimeout(); + + if (button == 1) { + this._onActivate(Clutter.get_current_event()); + } else if (button == 2) { + // Last workspace is always empty + let launchWorkspace = global.screen.get_workspace_by_index(global.screen.n_workspaces - 1); + launchWorkspace.activate(global.get_current_time()); + this.emit('launching'); + this.app.open_new_window(-1); + } else if (button == 3) { + this.popupMenu(); + } + return false; + }, + + getId: function() { + return this.app.get_id(); + }, + + popupMenu: function() { + this._removeMenuTimeout(); + this.actor.fake_release(); + + if (!this._menu) { + this._menu = new DockIconMenu(this); + /* this._menu.connect('highlight-window', Lang.bind(this, function (menu, window) { + this.highlightWindow(window); + })); */ + this._menu.connect('activate-window', Lang.bind(this, function (menu, window) { + this.activateWindow(window); + })); + this._menu.connect('popup', Lang.bind(this, function (menu, isPoppedUp) { + if (!isPoppedUp) + this._onMenuPoppedDown(); + })); + + this._menuManager.addMenu(this._menu, true); + } + + this._menu.popup(); + + return false; + }, + + /* + highlightWindow: function(metaWindow) { + if (this._didActivateWindow) + return; + if (!this._getRunning()) + return; + Main.overview.getWorkspacesForWindow(metaWindow).setHighlightWindow(metaWindow); + },*/ + + activateWindow: function(metaWindow) { + if (metaWindow) { + this._didActivateWindow = true; + Main.activateWindow(metaWindow); + } /* else { + Main.overview.hide(); + } */ + }, + + setSelected: function (isSelected) { + this._selected = isSelected; + if (this._selected) + this.actor.add_style_class_name('selected'); + else + this.actor.remove_style_class_name('selected'); + }, + + /* + _onMenuPoppedUp: function() { + if (this._getRunning()) { + Main.overview.getWorkspacesForWindow(null).setApplicationWindowSelection(this.app.get_id()); + this._setWindowSelection = true; + this._didActivateWindow = false; + } + }, + */ + + _onMenuPoppedDown: function() { + this.actor.sync_hover(); + /* + if (this._didActivateWindow) + return; + if (!this._setWindowSelection) + return; + + Main.overview.getWorkspacesForWindow(null).setApplicationWindowSelection(null); + this._setWindowSelection = false;*/ + }, + + _getRunning: function() { + return this.app.state != Shell.AppState.STOPPED; + }, + + _onActivate: function (event) { + this.emit('launching'); + let modifiers = Shell.get_event_state(event); + + if (modifiers & Clutter.ModifierType.CONTROL_MASK + && this.app.state == Shell.AppState.RUNNING) { + this.app.open_new_window(); + } else { + let tracker = Shell.WindowTracker.get_default(); + let focusedApp = tracker.focus_app; + + if (this.app == focusedApp) { + let windows = this.app.get_windows(); + let current_workspace = global.screen.get_active_workspace(); + for (let i = 0; i < windows.length; i++) { + let w = windows[i]; + if (w.get_workspace() == current_workspace) + w.minimize(); + } + } else { + this.app.activate(-1); + } + } + Main.overview.hide(); + }, + + shellWorkspaceLaunch : function() { + this.app.open_new_window(); + } +}; +Signals.addSignalMethods(DockIcon.prototype); + +function DockIconMenu(source) { + this._init(source); +} + +DockIconMenu.prototype = { + __proto__: AppDisplay.AppIconMenu.prototype, + + _init: function(source) { + PopupMenu.PopupMenu.prototype._init.call(this, source.actor, St.Align.MIDDLE, St.Side.RIGHT, 0); + + this._source = source; + + this.connect('activate', Lang.bind(this, this._onActivate)); + this.connect('open-state-changed', Lang.bind(this, this._onOpenStateChanged)); + + this.actor.add_style_class_name('dock-menu'); + + // Chain our visibility and lifecycle to that of the source + source.actor.connect('notify::mapped', Lang.bind(this, function () { + if (!source.actor.mapped) + this.close(); + })); + source.actor.connect('destroy', Lang.bind(this, function () { this.actor.destroy(); })); + + Main.chrome.addActor(this.actor); + }, + + _redisplay: function() { + this.removeAll(); + + let windows = this._source.app.get_windows(); + + // Display the app windows menu items and the separator between windows + // of the current desktop and other windows. + let activeWorkspace = global.screen.get_active_workspace(); + let separatorShown = windows.length > 0 && windows[0].get_workspace() != activeWorkspace; + + for (let i = 0; i < windows.length; i++) { + if (!separatorShown && windows[i].get_workspace() != activeWorkspace) { + this._appendSeparator(); + separatorShown = true; + } + let item = this._appendMenuItem(windows[i].title); + item._window = windows[i]; + } + + if (windows.length > 0) + this._appendSeparator(); + + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + + this._newWindowMenuItem = windows.length > 0 ? this._appendMenuItem(_("New Window")) : null; + + this._quitAppMenuItem = windows.length >0 ? this._appendMenuItem(_("Quit Application")) : null; + + if (windows.length > 0) + this._appendSeparator(); + this._toggleFavoriteMenuItem = this._appendMenuItem(isFavorite ? + _("Remove from Favorites") + : _("Add to Favorites")); + + this._highlightedItem = null; + }, + + _onActivate: function (actor, child) { + if (child._window) { + let metaWindow = child._window; + this.emit('activate-window', metaWindow); + } else if (child == this._newWindowMenuItem) { + this._source.app.open_new_window(); + this.emit('activate-window', null); + } else if (child == this._quitAppMenuItem) { + this._source.app.request_quit(); + } else if (child == this._toggleFavoriteMenuItem) { + let favs = AppFavorites.getAppFavorites(); + let isFavorite = favs.isFavorite(this._source.app.get_id()); + if (isFavorite) + favs.removeFavorite(this._source.app.get_id()); + else + favs.addFavorite(this._source.app.get_id()); + } + this.close(); + } +} + +/* +function WindowChooser() { + this._init(); +} + +WindowChooser.prototype = { + _init : function() { + this.actor = new Shell.GenericContainer({ name: 'dockWindowChooser', + reactive: true }); + this._thumbnailList = null; + Main.uiGroup.add_actor(this.actor); + }, + + hide : function() { + if (!this._thumbnailList) { + Tweener.addTween(this._thumbnailList.actor, + { opacity: 0, + time: POPUP_FADE_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, + function() { + this._thumbnailList.actor.destroy(); + }) + }); + this._thumbnailList.destroy(); + this._thumbnailList = null; + this.actor.hide(); + } + }, + + show : function(windows, parent) { + if (!this._thumbnailList) { + this._thumbnailList = AltTab.thumbnailList (windows); + this._thumbnailList.actor.show(); + this.actor.show(); + } + } +} +*/ + +function main(extensionMeta) { + imports.gettext.bindtextdomain('gnome-shell-extensions', extensionMeta.localedir); + + let dock = new Dock(); +} diff --git a/extensions/dock/metadata.json.in b/extensions/dock/metadata.json.in new file mode 100644 index 00000000..6c5c6f4b --- /dev/null +++ b/extensions/dock/metadata.json.in @@ -0,0 +1,8 @@ +{ +"uuid": "@uuid@", +"name": "Dock", +"description": "A dock for the GNOME Shell -- displays favorite and running applications", +"original-author": "tclaesson@gmail.com", +"shell-version": [ "@shell_current@" ], +"localedir": "@LOCALEDIR@", +} diff --git a/extensions/dock/stylesheet.css b/extensions/dock/stylesheet.css new file mode 100644 index 00000000..abdb3de1 --- /dev/null +++ b/extensions/dock/stylesheet.css @@ -0,0 +1,54 @@ +#dock { + border-radius: 9px; + background-color: rgba(0,0,0,0.9); + border-width: 2px; + border-color: #5f5f5f; +} + /* Panel */ +.dock-app { + padding: 4px; + width: 70px; + height: 70px; + border-radius: 4px; + transition-duration: 100; +} + +.dock-app.running { + padding: 3px; + border: 1px solid #181818; + background-gradient-direction: vertical; + background-gradient-start: #3d3d3d; + background-gradient-end: #181818; +} + +.dock-app.selected { + padding: 3px; + border: 1px solid #666666; +} + +.dock-app.focused { + padding: 3px; + border: 1px solid #5f5f5f; + background-gradient-direction: vertical; + background-gradient-start: rgba(61,61,61,0.8); + background-gradient-end: rgba(24,24,24,0.2); +} + +.dock-app:hover { + padding: 3px; + border: 1px solid #666666; + background-gradient-direction: vertical; + background-gradient-start: rgba(61,61,61,0.8); + background-gradient-end: rgba(24,24,24,0.2); + transition-duration: 100; +} + +.dock-app:active { + padding: 3px; + background-color: #1e1e1e; + border: 1px solid #5f5f5f; +} + +.dock-menu { + font-size: 12px +}