diff --git a/data/Makefile.am b/data/Makefile.am index f0ca34ec1..6fa151ed8 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -21,16 +21,24 @@ dist_images_DATA = \ themedir = $(pkgdatadir)/theme dist_theme_DATA = \ - theme/gnome-shell.css \ - theme/close.svg \ + theme/add-workspace.svg \ theme/close-window.svg \ - theme/scroll-button-down.png \ + theme/close.svg \ + theme/gnome-shell.css \ + theme/mosaic-view-active.svg \ + theme/mosaic-view.svg \ + theme/remove-workspace.svg \ theme/scroll-button-down-hover.png \ - theme/scroll-button-up.png \ + theme/scroll-button-down.png \ theme/scroll-button-up-hover.png \ + theme/scroll-button-up.png \ theme/scroll-vhandle.png \ theme/section-back.svg \ - theme/section-more.svg + theme/section-more.svg \ + theme/single-view-active.svg \ + theme/single-view.svg \ + theme/switch-scroll-hhandle.svg + schemadir = @GCONF_SCHEMA_FILE_DIR@ schema_DATA = gnome-shell.schemas diff --git a/data/theme/add-workspace.svg b/data/theme/add-workspace.svg new file mode 100644 index 000000000..6df7cbdb6 --- /dev/null +++ b/data/theme/add-workspace.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 67d917bae..5828beba1 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -137,6 +137,24 @@ StTooltip { color: white; } +.workspaces-bar { + height: 60px; +} + +.workspace-indicator { + width: 24px; + height: 15px; + background: rgba(155,155,155,0.8); + border-spacing: 15px; +} + +.workspace-indicator-active { + width: 24px; + height: 15px; + background: rgba(255,255,255,0.8); + border-spacing: 15px; +} + .window-caption { background: rgba(0,0,0,0.8); border: 1px solid rgba(128,128,128,0.40); @@ -154,6 +172,67 @@ StTooltip { -shell-close-overlap: 16px; } +.single-view-add { + background-image: url("add-workspace.svg"); + width: 24px; + height: 15px; +} + +.single-view-remove { + background-image: url("remove-workspace.svg"); + width: 24px; + height: 15px; +} + +.switch-view-single { + background-image: url("single-view.svg"); + width: 24px; + height: 15px; +} + +.switch-view-mosaic { + background-image: url("mosaic-view.svg"); + width: 24px; + height: 15px; +} + +.switch-view-single:checked { + background-image: url("single-view-active.svg"); + width: 24px; + height: 15px; +} + +.switch-view-mosaic:checked { + background-image: url("mosaic-view-active.svg"); + width: 24px; + height: 15px; +} + +.scroll-separator { + width: 9px; + height: 15px; +} + +#SwitchScroll { + height: 15px; +} + +#SwitchScroll StBin{ + border: 1px solid rgba(128,128,128,0.40); + border-radius: 5px; +} + +#SwitchScroll StButton#hhandle { + border-image: url("switch-scroll-hhandle.svg") 5; +} + +#SwitchScroll StButton#backward-stepper, +#SwitchScroll StButton#forward-stepper +{ + width: 0px; + border: 0px; +} + /* Dash */ #dash { diff --git a/data/theme/mosaic-view-active.svg b/data/theme/mosaic-view-active.svg new file mode 100644 index 000000000..296e776d0 --- /dev/null +++ b/data/theme/mosaic-view-active.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/data/theme/mosaic-view.svg b/data/theme/mosaic-view.svg new file mode 100644 index 000000000..b6ec4c3fd --- /dev/null +++ b/data/theme/mosaic-view.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/data/theme/remove-workspace.svg b/data/theme/remove-workspace.svg new file mode 100644 index 000000000..006330289 --- /dev/null +++ b/data/theme/remove-workspace.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/theme/single-view-active.svg b/data/theme/single-view-active.svg new file mode 100644 index 000000000..d7350ac5b --- /dev/null +++ b/data/theme/single-view-active.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/single-view.svg b/data/theme/single-view.svg new file mode 100644 index 000000000..c053e4f3d --- /dev/null +++ b/data/theme/single-view.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/switch-scroll-hhandle.svg b/data/theme/switch-scroll-hhandle.svg new file mode 100644 index 000000000..ae465d955 --- /dev/null +++ b/data/theme/switch-scroll-hhandle.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index 4eb388348..9d54f7445 100644 --- a/js/ui/Makefile.am +++ b/js/ui/Makefile.am @@ -30,4 +30,5 @@ dist_jsui_DATA = \ widget.js \ widgetBox.js \ windowManager.js \ - workspaces.js + workspacesView.js \ + workspace.js diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 47492288e..b9ba04f48 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -19,7 +19,7 @@ const DND = imports.ui.dnd; const GenericDisplay = imports.ui.genericDisplay; const Main = imports.ui.main; const Search = imports.ui.search; -const Workspaces = imports.ui.workspaces; +const Workspace = imports.ui.workspace; const APPICON_SIZE = 48; const WELL_MAX_COLUMNS = 8; @@ -697,7 +697,7 @@ AppIconMenu.prototype = { }, _findMetaWindowForActor: function (actor) { - if (actor._delegate instanceof Workspaces.WindowClone) + if (actor._delegate instanceof Workspace.WindowClone) return actor._delegate.metaWindow; else if (actor.get_meta_window) return actor.get_meta_window(); @@ -1011,7 +1011,7 @@ AppWell.prototype = { let app = null; if (source instanceof AppDisplayItem) { app = this._appSystem.get_app(source.getId()); - } else if (source instanceof Workspaces.WindowClone) { + } else if (source instanceof Workspace.WindowClone) { app = this._tracker.get_window_app(source.metaWindow); } diff --git a/js/ui/overview.js b/js/ui/overview.js index ce6fbfab5..3ec61a138 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -8,6 +8,7 @@ const Mainloop = imports.mainloop; const Shell = imports.gi.Shell; const Signals = imports.signals; const Lang = imports.lang; +const St = imports.gi.St; const AppDisplay = imports.ui.appDisplay; const DocDisplay = imports.ui.docDisplay; @@ -16,7 +17,7 @@ const Main = imports.ui.main; const Panel = imports.ui.panel; const Dash = imports.ui.dash; const Tweener = imports.ui.tweener; -const Workspaces = imports.ui.workspaces; +const WorkspacesView = imports.ui.workspacesView; const ROOT_OVERVIEW_COLOR = new Clutter.Color(); ROOT_OVERVIEW_COLOR.from_pixel(0x000000ff); @@ -76,7 +77,6 @@ const NUMBER_OF_SECTIONS_IN_SEARCH = 2; let wideScreen = false; let displayGridColumnWidth = null; let displayGridRowHeight = null; -let addRemoveButtonSize = null; function Overview() { this._init(); @@ -87,6 +87,9 @@ Overview.prototype = { this._group = new Clutter.Group(); this._group._delegate = this; + this._workspacesViewSwitch = new WorkspacesView.WorkspacesViewSwitch(); + this._workspacesViewSwitch.connect('view-changed', Lang.bind(this, this._onViewChanged)); + this.visible = false; this.animationInProgress = false; this._hideInProgress = false; @@ -139,6 +142,45 @@ Overview.prototype = { this._workspaces = null; }, + _createControlsBar: function() { + this._workspacesBar = new St.BoxLayout({ 'pack-start': true, + style_class: 'workspaces-bar' }); + this._workspacesBar.move_by(this._workspacesBarX, this._workspacesBarY); + + let controlsBar = this._workspacesViewSwitch.createControlsBar(); + let bar = this._workspaces.createControllerBar(); + this._workspacesBar.add(bar, { expand: true, 'x-fill': true, 'y-fill': true, + y_align: St.Align.MIDDLE, x_align: St.Align.START }); + this._workspacesBar.add(controlsBar, {x_align: St.Align.END}); + this._workspacesBar.width = this._workspacesBarWidth; + + this._group.add_actor(this._workspacesBar); + this._workspacesBar.raise(this._workspaces.actor); + }, + + _onViewChanged: function() { + if (!this.visible) + return; + //Remove old worspacesView + this._group.remove_actor(this._workspacesBar); + this._workspaces.hide(); + this._group.remove_actor(this._workspaces.actor); + this._workspaces.destroy(); + this._workspacesBar.destroy(); + + this._workspaces = this._workspacesViewSwitch.createCurrentWorkspaceView(this._workspacesWidth, this._workspacesHeight, + this._workspacesX, this._workspacesY, false); + + //Show new workspacesView + this._group.add_actor(this._workspaces.actor); + this._dash.actor.raise(this._workspaces.actor); + + this._createControlsBar(); + + // Set new position and scale to workspaces. + this.emit('showing'); + }, + _recalculateGridSizes: function () { let primary = global.get_primary_monitor(); wideScreen = (primary.width/primary.height > WIDE_SCREEN_CUT_OFF_RATIO) && @@ -188,9 +230,9 @@ Overview.prototype = { this._dash.searchResults.actor.height = this._workspacesHeight; // place the 'Add Workspace' button in the bottom row of the grid - addRemoveButtonSize = Math.floor(displayGridRowHeight * 3/5); - this._addButtonX = this._workspacesX + this._workspacesWidth - addRemoveButtonSize; - this._addButtonY = primary.height - Math.floor(displayGridRowHeight * 4/5); + this._workspacesBarX = this._workspacesX; + this._workspacesBarWidth = primary.width - this._workspacesBarX - WORKSPACE_GRID_PADDING; + this._workspacesBarY = primary.height - displayGridRowHeight + 5; // The parent (this._group) is positioned at the top left of the primary monitor // while this._backOver occupies the entire screen. @@ -298,8 +340,8 @@ Overview.prototype = { this._dash.show(); /* TODO: make this stuff dynamic */ - this._workspaces = new Workspaces.Workspaces(this._workspacesWidth, this._workspacesHeight, - this._workspacesX, this._workspacesY); + this._workspaces = this._workspacesViewSwitch.createCurrentWorkspaceView(this._workspacesWidth, this._workspacesHeight, + this._workspacesX, this._workspacesY, true); this._group.add_actor(this._workspaces.actor); // The workspaces actor is as big as the screen, so we have to raise the dash above it @@ -307,11 +349,7 @@ Overview.prototype = { // be as big as the screen. this._dash.actor.raise(this._workspaces.actor); - // Create (+) button - this._addButton = new AddWorkspaceButton(addRemoveButtonSize, this._addButtonX, this._addButtonY, Lang.bind(this, this._acceptNewWorkspaceDrop)); - this._addButton.actor.connect('button-release-event', Lang.bind(this, this._addNewWorkspace)); - this._group.add_actor(this._addButton.actor); - this._addButton.actor.raise(this._workspaces.actor); + this._createControlsBar(); // All the the actors in the window group are completely obscured, // hiding the group holding them while the Overview is displayed greatly @@ -363,9 +401,8 @@ Overview.prototype = { this._activeDisplayPane.close(); this._workspaces.hide(); - this._addButton.actor.destroy(); - this._addButton.actor = null; - this._addButton = null; + this._workspacesBar.destroy(); + this._workspacesBar = null; // Create a zoom in effect by transforming the Overview group so that // the active workspace fills up the whole screen. The opposite @@ -448,7 +485,7 @@ Overview.prototype = { this._dash.hide(); this._group.hide(); - this.visible = false; + this.visible = false; this.animationInProgress = false; this._hideInProgress = false; @@ -456,61 +493,6 @@ Overview.prototype = { Main.popModal(this._dash.actor); this.emit('hidden'); - }, - - _addNewWorkspace: function() { - global.screen.append_new_workspace(false, global.get_current_time()); - }, - - _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) { - this._addNewWorkspace(); - return this._workspaces.acceptNewWorkspaceDrop(source, dropActor, x, y, time); } }; Signals.addSignalMethods(Overview.prototype); - -// Note that mutter has a compile-time limit of 36 -const MAX_WORKSPACES = 16; - -function AddWorkspaceButton(buttonSize, buttonX, buttonY, acceptDropCallback) { - this._init(buttonSize, buttonX, buttonY, acceptDropCallback); -} - -AddWorkspaceButton.prototype = { - _init: function(buttonSize, buttonX, buttonY, acceptDropCallback) { - this.actor = new Clutter.Group({ x: buttonX, - y: buttonY, - width: global.screen_width - buttonX, - height: global.screen_height - buttonY, - reactive: true }); - this.actor._delegate = this; - this._acceptDropCallback = acceptDropCallback; - - let plus = new Clutter.Texture({ x: 0, - y: 0, - width: buttonSize, - height: buttonSize }); - plus.set_from_file(global.imagedir + 'add-workspace.svg'); - this.actor.add_actor(plus); - - global.screen.connect('notify::n-workspaces', Lang.bind(this, this._nWorkspacesChanged)); - this._nWorkspacesChanged(); - }, - - _nWorkspacesChanged: function() { - let canAddAnother = global.screen.n_workspaces < MAX_WORKSPACES; - - if (canAddAnother && !this.actor.reactive) { - this.actor.reactive = true; - this.actor.opacity = 255; - } else if (!canAddAnother && this.actor.reactive) { - this.actor.reactive = false; - this.actor.opacity = 85; - } - }, - - // Draggable target interface - acceptDrop: function(source, actor, x, y, time) { - return this.reactive && this._acceptDropCallback(source, actor, x, y, time); - } -}; diff --git a/js/ui/workspaces.js b/js/ui/workspace.js similarity index 70% rename from js/ui/workspaces.js rename to js/ui/workspace.js index 427d3138b..49d0bed56 100644 --- a/js/ui/workspaces.js +++ b/js/ui/workspace.js @@ -322,7 +322,7 @@ WindowOverlay.prototype = { this._parentActor = parentActor; let title = new St.Label({ style_class: "window-caption", - text : metaWindow.title }); + text: metaWindow.title }); title.connect('style-changed', Lang.bind(this, this._onStyleChanged)); title.clutter_text.ellipsize = Pango.EllipsizeMode.END; @@ -368,6 +368,8 @@ WindowOverlay.prototype = { x, y); if (actor == this._windowClone.actor) { this.closeButton.show(); + // Reposition the close button in case we've changed display modes + this._updatePositions(); } this.title.show(); }, @@ -375,7 +377,7 @@ WindowOverlay.prototype = { fadeIn: function() { this.title.opacity = 0; this.title.show(); - this.title.raise_top(); + this._parentActor.raise_top(); Tweener.addTween(this.title, { opacity: 255, time: Overview.ANIMATION_TIME, @@ -391,6 +393,12 @@ WindowOverlay.prototype = { this.title.height + this.title._spacing; }, + _updatePositions: function() { + let [cloneX, cloneY] = this._windowClone.actor.get_transformed_position(); + let [cloneWidth, cloneHeight] = this._windowClone.actor.get_transformed_size(); + this.updatePositions(cloneX, cloneY, cloneWidth, cloneHeight); + }, + /** * @cloneX: x position of windowClone * @cloneY: y position of windowClone @@ -460,7 +468,9 @@ WindowOverlay.prototype = { }, _onEnter: function() { - this.closeButton.raise_top(); + this._updatePositions(); + + this._parentActor.raise_top(); this.closeButton.show(); this.emit('show-close-button'); }, @@ -510,6 +520,10 @@ WindowOverlay.prototype = { Signals.addSignalMethods(WindowOverlay.prototype); +const WindowPositionFlags = { + ZOOM: 1 << 0, + ANIMATE: 1 << 1 +}; /** * @workspaceNum: Workspace index @@ -524,9 +538,14 @@ function Workspace(workspaceNum, parentActor) { Workspace.prototype = { _init : function(workspaceNum, parentActor) { this.workspaceNum = workspaceNum; + this._windowOverlaysGroup = new Clutter.Group(); + // Without this the drop area will be overlapped. + this._windowOverlaysGroup.set_size(0, 0); + this._metaWorkspace = global.screen.get_workspace_by_index(workspaceNum); - this.parentActor = parentActor; + parentActor.add_actor(this._windowOverlaysGroup); + this._parentActor = parentActor; this.actor = new Clutter.Group(); this.actor._delegate = this; @@ -578,7 +597,6 @@ Workspace.prototype = { this._windowRemovedId = this._metaWorkspace.connect('window-removed', Lang.bind(this, this._windowRemoved)); - this._removeButton = null; this._visible = false; this._frame = null; @@ -586,51 +604,6 @@ Workspace.prototype = { this.leavingOverview = false; }, - updateRemovable : function() { - let removable = (this._windows.length == 1 /* just desktop */ && - this.workspaceNum != 0 && - this.workspaceNum == global.screen.n_workspaces - 1); - - if (removable) { - if (this._removeButton) - return; - - this._removeButton = new Clutter.Texture({ width: Overview.addRemoveButtonSize, - height: Overview.addRemoveButtonSize, - reactive: true - }); - this._removeButton.set_from_file(global.imagedir + "remove-workspace.svg"); - this._removeButton.connect('button-release-event', Lang.bind(this, this._removeSelf)); - - this.actor.add_actor(this._removeButton); - this._adjustRemoveButton(); - this._adjustRemoveButtonId = this.actor.connect('notify::scale-x', Lang.bind(this, this._adjustRemoveButton)); - - if (this._visible) { - this._removeButton.set_opacity(0); - Tweener.addTween(this._removeButton, - { opacity: 255, - time: Overview.ANIMATION_TIME, - transition: "easeOutQuad" - }); - } - } else { - if (!this._removeButton) - return; - - if (this._visible) { - Tweener.addTween(this._removeButton, - { opacity: 0, - time: Overview.ANIMATION_TIME, - transition: "easeOutQuad", - onComplete: this._removeRemoveButton, - onCompleteScope: this - }); - } else - this._removeRemoveButton(); - } - }, - _lookupIndex: function (metaWindow) { let index, clone; for (let i = 0; i < this._windows.length; i++) { @@ -661,7 +634,7 @@ Workspace.prototype = { this._showOnlyWindows = showOnlyWindows; this._resetCloneVisibility(); if (reposition) - this.positionWindows(false); + this.positionWindows(WindowPositionFlags.ANIMATE); }, /** @@ -697,20 +670,6 @@ Workspace.prototype = { this._lightbox.highlight(actor); }, - _adjustRemoveButton : function() { - this._removeButton.set_scale(1.0 / this.actor.scale_x, - 1.0 / this.actor.scale_y); - this._removeButton.set_position( - (this.actor.width - this._removeButton.width / this.actor.scale_x) / 2, - (this.actor.height - this._removeButton.height / this.actor.scale_y) / 2); - }, - - _removeRemoveButton : function() { - this._removeButton.destroy(); - this._removeButton = null; - this.actor.disconnect(this._adjustRemoveButtonId); - }, - // Mark the workspace selected/not-selected setSelected : function(selected) { // Don't draw a frame if we only have one workspace @@ -1007,13 +966,18 @@ Workspace.prototype = { /** * positionWindows: - * @workspaceZooming: If true, then the workspace is moving at the same time and we need to take that into account. + * @flags: + * ZOOM - workspace is moving at the same time and we need to take that into account. + * ANIMATE - Indicates that we need animate changing position. */ - positionWindows : function(workspaceZooming) { + positionWindows : function(flags) { let totalVisible = 0; let visibleWindows = this._getVisibleWindows(); + let workspaceZooming = flags & WindowPositionFlags.ZOOM; + let animate = flags & WindowPositionFlags.ANIMATE; + // Start the animations let slots = this._computeAllWindowSlots(visibleWindows.length); visibleWindows = this._orderWindowsByMotionAndStartup(visibleWindows, slots); @@ -1028,18 +992,24 @@ Workspace.prototype = { let [x, y, scale] = this._computeWindowRelativeLayout(metaWindow, slot); overlay.hide(); - Tweener.addTween(clone.actor, - { x: x, - y: y, - scale_x: scale, - scale_y: scale, - workspace_relative: workspaceZooming ? this : null, - time: Overview.ANIMATION_TIME, - transition: "easeOutQuad", - onComplete: Lang.bind(this, function() { - this._fadeInWindowOverlay(clone, overlay); - }) - }); + if (animate) { + Tweener.addTween(clone.actor, + { x: x, + y: y, + scale_x: scale, + scale_y: scale, + workspace_relative: workspaceZooming ? this : null, + time: Overview.ANIMATION_TIME, + transition: "easeOutQuad", + onComplete: Lang.bind(this, function() { + this._fadeInWindowOverlay(clone, overlay); + }) + }); + } else { + clone.actor.set_position(x, y); + clone.actor.set_scale(scale, scale); + this._fadeInWindowOverlay(clone, overlay); + } } }, @@ -1127,8 +1097,7 @@ Workspace.prototype = { } clone.destroy(); - this.positionWindows(false); - this.updateRemovable(); + this.positionWindows(WindowPositionFlags.ANIMATE); }, _windowAdded : function(metaWorkspace, metaWin) { @@ -1148,9 +1117,9 @@ Workspace.prototype = { })); return; } - + if (!this._isOverviewWindow(win)) - return; + return; let clone = this._addWindowClone(win); @@ -1164,29 +1133,19 @@ Workspace.prototype = { clone.actor.set_scale (scale, scale); } - this.positionWindows(false); - this.updateRemovable(); + this.positionWindows(WindowPositionFlags.ANIMATE); }, // Animate the full-screen to Overview transition. - zoomToOverview : function() { + zoomToOverview : function(animate) { this.actor.set_position(this.gridX, this.gridY); this.actor.set_scale(this.scale, this.scale); // Position and scale the windows. - this.positionWindows(true); - - // Fade in the remove button if available, so that it doesn't appear - // too abrubtly and doesn't start at a too big size. - if (this._removeButton) { - Tweener.removeTweens(this._removeButton); - this._removeButton.opacity = 0; - Tweener.addTween(this._removeButton, - { opacity: 255, - time: Overview.ANIMATION_TIME, - transition: 'easeOutQuad' - }); - } + if (animate) + this.positionWindows(WindowPositionFlags.ANIMATE | WindowPositionFlags.ZOOM); + else + this.positionWindows(WindowPositionFlags.ZOOM); this._visible = true; }, @@ -1200,17 +1159,6 @@ Workspace.prototype = { Main.overview.connect('hidden', Lang.bind(this, this._doneLeavingOverview)); - // Fade out the remove button if available, so that it doesn't - // disappear too abrubtly and doesn't become too big. - if (this._removeButton) { - Tweener.removeTweens(this._removeButton); - Tweener.addTween(this._removeButton, - { opacity: 0, - time: Overview.ANIMATION_TIME, - transition: 'easeOutQuad' - }); - } - // Position and scale the windows. for (let i = 1; i < this._windows.length; i++) { let clone = this._windows[i]; @@ -1226,7 +1174,7 @@ Workspace.prototype = { }); } - this._visible = false; + this._visible = false; }, // Animates grid shrinking/expanding when a row or column @@ -1264,7 +1212,7 @@ Workspace.prototype = { this._visible = true; }, - + // Animates the removal of a workspace slideOut : function(onComplete) { let destX = this.actor.x, destY = this.actor.y; @@ -1291,11 +1239,12 @@ Workspace.prototype = { // making its exit. this._desktop.reactive = false; }, - + destroy : function() { Tweener.removeTweens(this.actor); this.actor.destroy(); this.actor = null; + this._windowOverlaysGroup.destroy(); this._metaWorkspace.disconnect(this._windowAddedId); this._metaWorkspace.disconnect(this._windowRemovedId); @@ -1321,7 +1270,7 @@ Workspace.prototype = { // Create a clone of a (non-desktop) window and add it to the window list _addWindowClone : function(win) { let clone = new WindowClone(win); - let overlay = new WindowOverlay(clone, this.parentActor); + let overlay = new WindowOverlay(clone, this._windowOverlaysGroup); clone.connect('selected', Lang.bind(this, this._onCloneSelected)); @@ -1421,399 +1370,3 @@ Workspace.prototype = { }; Signals.addSignalMethods(Workspace.prototype); - -function Workspaces(width, height, x, y) { - this._init(width, height, x, y); -} - -Workspaces.prototype = { - _init : function(width, height, x, y) { - this.actor = new St.Bin({ style_class: "workspaces" }); - this._actor = new Clutter.Group(); - - this.actor.add_actor(this._actor); - - this._width = width; - this._height = height; - this._x = x; - this._y = y; - - this._windowSelectionAppId = null; - - this._workspaces = []; - - this._highlightWindow = null; - - let activeWorkspaceIndex = global.screen.get_active_workspace_index(); - let activeWorkspace; - - // Create and position workspace objects - for (let w = 0; w < global.screen.n_workspaces; w++) { - this._addWorkspaceActor(w); - if (w == activeWorkspaceIndex) { - activeWorkspace = this._workspaces[w]; - activeWorkspace.setSelected(true); - } - } - activeWorkspace.actor.raise_top(); - this._positionWorkspaces(); - - let lastWorkspace = this._workspaces[this._workspaces.length - 1]; - lastWorkspace.updateRemovable(true); - - // Position/scale the desktop windows and their children after the - // workspaces have been created. This cannot be done first because - // window movement depends on the Workspaces object being accessible - // as an Overview member. - this._overviewShowingId = - Main.overview.connect('showing', - Lang.bind(this, function() { - this._onRestacked(); - for (let w = 0; w < this._workspaces.length; w++) - this._workspaces[w].zoomToOverview(); - })); - - // Track changes to the number of workspaces - this._nWorkspacesNotifyId = - global.screen.connect('notify::n-workspaces', - Lang.bind(this, this._workspacesChanged)); - this._switchWorkspaceNotifyId = - global.window_manager.connect('switch-workspace', - Lang.bind(this, this._activeWorkspaceChanged)); - this._restackedNotifyId = - global.screen.connect('restacked', - Lang.bind(this, this._onRestacked)); - }, - - _lookupWorkspaceForMetaWindow: function (metaWindow) { - for (let i = 0; i < this._workspaces.length; i++) { - if (this._workspaces[i].containsMetaWindow(metaWindow)) - return this._workspaces[i]; - } - return null; - }, - - _lookupCloneForMetaWindow: function (metaWindow) { - for (let i = 0; i < this._workspaces.length; i++) { - let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow); - if (clone) - return clone; - } - return null; - }, - - setHighlightWindow: function (metaWindow) { - // Looping over all workspaces is easier than keeping track of the last - // highlighted window while trying to handle the window or workspace possibly - // going away. - for (let i = 0; i < this._workspaces.length; i++) { - this._workspaces[i].setHighlightWindow(null); - } - if (metaWindow != null) { - let workspace = this._lookupWorkspaceForMetaWindow(metaWindow); - workspace.setHighlightWindow(metaWindow); - } - }, - - _clearApplicationWindowSelection: function(reposition) { - if (this._windowSelectionAppId == null) - return; - this._windowSelectionAppId = null; - - for (let i = 0; i < this._workspaces.length; i++) { - this._workspaces[i].setLightboxMode(false); - this._workspaces[i].setShowOnlyWindows(null, reposition); - } - }, - - /** - * setApplicationWindowSelection: - * @appid: Application identifier string - * - * Enter a mode which shows only the windows owned by the - * given application, and allow highlighting of a specific - * window with setHighlightWindow(). - */ - setApplicationWindowSelection: function (appId) { - if (appId == null) { - this._clearApplicationWindowSelection(true); - return; - } - - if (appId == this._windowSelectionAppId) - return; - - this._windowSelectionAppId = appId; - - let appSys = Shell.AppSystem.get_default(); - - let showOnlyWindows = {}; - let app = appSys.get_app(appId); - let windows = app.get_windows(); - for (let i = 0; i < windows.length; i++) { - showOnlyWindows[windows[i]] = 1; - } - - for (let i = 0; i < this._workspaces.length; i++) { - this._workspaces[i].setLightboxMode(true); - this._workspaces[i].setShowOnlyWindows(showOnlyWindows, true); - } - }, - - /** - * activateWindowFromOverview: - * @metaWindow: A #MetaWindow - * @time: Integer even timestamp - * - * This function exits the overview, switching to the given @metaWindow. - * If an application filter is in effect, it will be cleared. - */ - activateWindowFromOverview: function (metaWindow, time) { - if (this._windowSelectionAppId != null) { - this._clearApplicationWindowSelection(false); - } - - Main.activateWindow(metaWindow, time); - Main.overview.hide(); - }, - - hide : function() { - let activeWorkspaceIndex = global.screen.get_active_workspace_index(); - let activeWorkspace = this._workspaces[activeWorkspaceIndex]; - - this._positionWorkspaces(); - activeWorkspace.actor.raise_top(); - - for (let w = 0; w < this._workspaces.length; w++) - this._workspaces[w].zoomFromOverview(); - }, - - destroy : function() { - for (let w = 0; w < this._workspaces.length; w++) - this._workspaces[w].destroy(); - this._workspaces = []; - - this.actor.destroy(); - this.actor = null; - - Main.overview.disconnect(this._overviewShowingId); - global.screen.disconnect(this._nWorkspacesNotifyId); - global.window_manager.disconnect(this._switchWorkspaceNotifyId); - global.screen.disconnect(this._restackedNotifyId); - }, - - getScale : function() { - return this._workspaces[0].scale; - }, - - // Get the grid position of the active workspace. - getActiveWorkspacePosition : function() { - let activeWorkspaceIndex = global.screen.get_active_workspace_index(); - let activeWorkspace = this._workspaces[activeWorkspaceIndex]; - - return [activeWorkspace.gridX, activeWorkspace.gridY]; - }, - - // Assign grid positions to workspaces. We can't just do a simple - // row-major or column-major numbering, because we don't want the - // existing workspaces to get rearranged when we add a row or - // column. So we alternate between adding to rows and adding to - // columns. (So, eg, when going from a 2x2 grid of 4 workspaces to - // a 3x2 grid of 5 workspaces, the 4 existing workspaces stay - // where they are, and the 5th one is added to the end of the - // first row.) - // - // FIXME: need to make the metacity internal layout agree with this! - _positionWorkspaces : function() { - let gridWidth = Math.ceil(Math.sqrt(this._workspaces.length)); - let gridHeight = Math.ceil(this._workspaces.length / gridWidth); - - let wsWidth = (this._width - (gridWidth - 1) * GRID_SPACING) / gridWidth; - let wsHeight = (this._height - (gridHeight - 1) * GRID_SPACING) / gridHeight; - let scale = wsWidth / global.screen_width; - - let span = 1, n = 0, row = 0, col = 0, horiz = true; - - for (let w = 0; w < this._workspaces.length; w++) { - let workspace = this._workspaces[w]; - - workspace.gridRow = row; - workspace.gridCol = col; - - workspace.gridX = this._x + workspace.gridCol * (wsWidth + GRID_SPACING); - workspace.gridY = this._y + workspace.gridRow * (wsHeight + GRID_SPACING); - workspace.scale = scale; - - if (horiz) { - col++; - if (col == span) { - row = 0; - horiz = false; - } - } else { - row++; - if (row == span) { - col = 0; - horiz = true; - span++; - } - } - } - }, - - _workspacesChanged : function() { - let oldNumWorkspaces = this._workspaces.length; - let newNumWorkspaces = global.screen.n_workspaces; - - if (oldNumWorkspaces == newNumWorkspaces) - return; - - let oldScale = this._workspaces[0].scale; - let oldGridWidth = Math.ceil(Math.sqrt(oldNumWorkspaces)); - let oldGridHeight = Math.ceil(oldNumWorkspaces / oldGridWidth); - let lostWorkspaces = []; - - // The old last workspace is no longer removable. - this._workspaces[oldNumWorkspaces - 1].updateRemovable(); - - if (newNumWorkspaces > oldNumWorkspaces) { - // Create new workspace groups - for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) { - this._addWorkspaceActor(w); - } - - } else { - // Truncate the list of workspaces - // FIXME: assumes that the workspaces are being removed from - // the end of the list, not the start/middle - lostWorkspaces = this._workspaces.splice(newNumWorkspaces); - } - - // The new last workspace may be removable - let newLastWorkspace = this._workspaces[this._workspaces.length - 1]; - newLastWorkspace.updateRemovable(); - - // Figure out the new layout - this._positionWorkspaces(); - let newScale = this._workspaces[0].scale; - let newGridWidth = Math.ceil(Math.sqrt(newNumWorkspaces)); - let newGridHeight = Math.ceil(newNumWorkspaces / newGridWidth); - - if (newGridWidth != oldGridWidth || newGridHeight != oldGridHeight) { - // We need to resize/move the existing workspaces/windows - let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces); - for (let w = 0; w < existingWorkspaces; w++) - this._workspaces[w].resizeToGrid(oldScale); - } - - if (newScale != oldScale) { - // The workspace scale affects window size/positioning because we clamp - // window size to a 1:1 ratio and never scale them up - let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces); - for (let w = 0; w < existingWorkspaces; w++) - this._workspaces[w].positionWindows(false); - } - - if (newNumWorkspaces > oldNumWorkspaces) { - // Slide new workspaces in from offscreen - for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) - this._workspaces[w].slideIn(oldScale); - } else { - // Slide old workspaces out - for (let w = 0; w < lostWorkspaces.length; w++) { - let workspace = lostWorkspaces[w]; - workspace.slideOut(function () { workspace.destroy(); }); - } - - // FIXME: deal with windows on the lost workspaces - } - - // Reset the selection state; if we went from > 1 workspace to 1, - // this has the side effect of removing the frame border - let activeIndex = global.screen.get_active_workspace_index(); - this._workspaces[activeIndex].setSelected(true); - }, - - _activeWorkspaceChanged : function(wm, from, to, direction) { - this._workspaces[from].setSelected(false); - this._workspaces[to].setSelected(true); - }, - - _addWorkspaceActor : function(workspaceNum) { - let workspace = new Workspace(workspaceNum, this._actor); - this._workspaces[workspaceNum] = workspace; - this._actor.add_actor(workspace.actor); - }, - - _onRestacked: function() { - let stack = global.get_windows(); - let stackIndices = {}; - - for (let i = 0; i < stack.length; i++) { - // Use the stable sequence for an integer to use as a hash key - stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i; - } - - for (let i = 0; i < this._workspaces.length; i++) - this._workspaces[i].syncStacking(stackIndices); - }, - - // Handles a drop onto the (+) button; assumes the new workspace - // has already been added - acceptNewWorkspaceDrop : function(source, dropActor, x, y, time) { - return this._workspaces[this._workspaces.length - 1].acceptDrop(source, dropActor, x, y, time); - } -}; - -// Create a SpecialPropertyModifier to let us move windows in a -// straight line on the screen even though their containing workspace -// is also moving. -Tweener.registerSpecialPropertyModifier("workspace_relative", _workspaceRelativeModifier, _workspaceRelativeGet); - -function _workspaceRelativeModifier(workspace) { - let [startX, startY] = Main.overview.getPosition(); - let overviewPosX, overviewPosY, overviewScale; - - if (!workspace) - return []; - - if (workspace.leavingOverview) { - let [zoomedInX, zoomedInY] = Main.overview.getZoomedInPosition(); - overviewPosX = { begin: startX, end: zoomedInX }; - overviewPosY = { begin: startY, end: zoomedInY }; - overviewScale = { begin: Main.overview.getScale(), - end: Main.overview.getZoomedInScale() }; - } else { - overviewPosX = { begin: startX, end: 0 }; - overviewPosY = { begin: startY, end: 0 }; - overviewScale = { begin: Main.overview.getScale(), end: 1 }; - } - - return [ { name: "x", - parameters: { workspacePos: workspace.gridX, - overviewPos: overviewPosX, - overviewScale: overviewScale } }, - { name: "y", - parameters: { workspacePos: workspace.gridY, - overviewPos: overviewPosY, - overviewScale: overviewScale } } - ]; -} - -function _workspaceRelativeGet(begin, end, time, params) { - let curOverviewPos = (1 - time) * params.overviewPos.begin + - time * params.overviewPos.end; - let curOverviewScale = (1 - time) * params.overviewScale.begin + - time * params.overviewScale.end; - - // Calculate the screen position of the window. - let screen = (1 - time) * - ((begin + params.workspacePos) * params.overviewScale.begin + - params.overviewPos.begin) + - time * - ((end + params.workspacePos) * params.overviewScale.end + - params.overviewPos.end); - - // Return the workspace coordinates. - return (screen - curOverviewPos) / curOverviewScale - params.workspacePos; -} diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js new file mode 100644 index 000000000..d75e3e3a2 --- /dev/null +++ b/js/ui/workspacesView.js @@ -0,0 +1,956 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Big = imports.gi.Big; +const Clutter = imports.gi.Clutter; +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gdk = imports.gi.Gdk; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Signals = imports.signals; + +const DND = imports.ui.dnd; +const Lightbox = imports.ui.lightbox; +const Main = imports.ui.main; +const Overview = imports.ui.overview; +const Panel = imports.ui.panel; +const Tweener = imports.ui.tweener; +const Workspace = imports.ui.workspace; + +const WORKSPACE_SWITCH_TIME = 0.25; +// Note that mutter has a compile-time limit of 36 +const MAX_WORKSPACES = 16; + +const GRID_SPACING = 15; + +const WorkspacesViewType = { + SINGLE: 0, + MOSAIC: 1 +}; + +function GenericWorkspacesView(width, height, x, y, animate) { + this._init(width, height, x, y, animate); +} + +GenericWorkspacesView.prototype = { + _init: function(width, height, x, y, animate) { + this.actor = new St.Bin({ style_class: "workspaces" }); + this._actor = new Clutter.Group(); + + this.actor.add_actor(this._actor); + + this._width = width; + this._height = height; + this._x = x; + this._y = y; + + this._windowSelectionAppId = null; + + this._workspaces = []; + + this._highlightWindow = null; + + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + + // Create and position workspace objects + for (let w = 0; w < global.screen.n_workspaces; w++) { + this._addWorkspaceActor(w); + } + this._workspaces[activeWorkspaceIndex].actor.raise_top(); + this._positionWorkspaces(); + + // Position/scale the desktop windows and their children after the + // workspaces have been created. This cannot be done first because + // window movement depends on the Workspaces object being accessible + // as an Overview member. + this._overviewShowingId = + Main.overview.connect('showing', + Lang.bind(this, function() { + this._onRestacked(); + for (let w = 0; w < this._workspaces.length; w++) + this._workspaces[w].zoomToOverview(animate); + })); + + // Track changes to the number of workspaces + this._nWorkspacesNotifyId = + global.screen.connect('notify::n-workspaces', + Lang.bind(this, this._workspacesChanged)); + this._switchWorkspaceNotifyId = + global.window_manager.connect('switch-workspace', + Lang.bind(this, this._activeWorkspaceChanged)); + this._restackedNotifyId = + global.screen.connect('restacked', + Lang.bind(this, this._onRestacked)); + }, + + _lookupWorkspaceForMetaWindow: function (metaWindow) { + for (let i = 0; i < this._workspaces.length; i++) { + if (this._workspaces[i].containsMetaWindow(metaWindow)) + return this._workspaces[i]; + } + return null; + }, + + _lookupCloneForMetaWindow: function (metaWindow) { + for (let i = 0; i < this._workspaces.length; i++) { + let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow); + if (clone) + return clone; + } + return null; + }, + + setHighlightWindow: function (metaWindow) { + // Looping over all workspaces is easier than keeping track of the last + // highlighted window while trying to handle the window or workspace possibly + // going away. + for (let i = 0; i < this._workspaces.length; i++) { + this._workspaces[i].setHighlightWindow(null); + } + if (metaWindow != null) { + let workspace = this._lookupWorkspaceForMetaWindow(metaWindow); + workspace.setHighlightWindow(metaWindow); + } + }, + + _clearApplicationWindowSelection: function(reposition) { + if (this._windowSelectionAppId == null) + return; + this._windowSelectionAppId = null; + + for (let i = 0; i < this._workspaces.length; i++) { + this._workspaces[i].setLightboxMode(false); + this._workspaces[i].setShowOnlyWindows(null, reposition); + } + }, + + /** + * setApplicationWindowSelection: + * @appid: Application identifier string + * + * Enter a mode which shows only the windows owned by the + * given application, and allow highlighting of a specific + * window with setHighlightWindow(). + */ + setApplicationWindowSelection: function (appId) { + if (appId == null) { + this._clearApplicationWindowSelection(true); + return; + } + + if (appId == this._windowSelectionAppId) + return; + + this._windowSelectionAppId = appId; + + let appSys = Shell.AppSystem.get_default(); + + let showOnlyWindows = {}; + let app = appSys.get_app(appId); + let windows = app.get_windows(); + for (let i = 0; i < windows.length; i++) { + showOnlyWindows[windows[i]] = 1; + } + + for (let i = 0; i < this._workspaces.length; i++) { + this._workspaces[i].setLightboxMode(true); + this._workspaces[i].setShowOnlyWindows(showOnlyWindows, true); + } + }, + + /** + * activateWindowFromOverview: + * @metaWindow: A #MetaWindow + * @time: Integer even timestamp + * + * This function exits the overview, switching to the given @metaWindow. + * If an application filter is in effect, it will be cleared. + */ + activateWindowFromOverview: function (metaWindow, time) { + if (this._windowSelectionAppId != null) { + this._clearApplicationWindowSelection(false); + } + + Main.activateWindow(metaWindow, time); + Main.overview.hide(); + }, + + hide: function() { + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + let activeWorkspace = this._workspaces[activeWorkspaceIndex]; + + this._positionWorkspaces(); + activeWorkspace.actor.raise_top(); + + for (let w = 0; w < this._workspaces.length; w++) + this._workspaces[w].zoomFromOverview(); + }, + + destroy: function() { + for (let w = 0; w < this._workspaces.length; w++) + this._workspaces[w].destroy(); + this._workspaces = []; + + this.actor.destroy(); + this.actor = null; + + Main.overview.disconnect(this._overviewShowingId); + global.screen.disconnect(this._nWorkspacesNotifyId); + global.window_manager.disconnect(this._switchWorkspaceNotifyId); + global.screen.disconnect(this._restackedNotifyId); + }, + + getScale: function() { + return this._workspaces[0].scale; + }, + + _onRestacked: function() { + let stack = global.get_windows(); + let stackIndices = {}; + + for (let i = 0; i < stack.length; i++) { + // Use the stable sequence for an integer to use as a hash key + stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i; + } + + for (let i = 0; i < this._workspaces.length; i++) + this._workspaces[i].syncStacking(stackIndices); + }, + + // Handles a drop onto the (+) button; assumes the new workspace + // has already been added + acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) { + return this._workspaces[this._workspaces.length - 1].acceptDrop(source, dropActor, x, y, time); + }, + + // Get the grid position of the active workspace. + getActiveWorkspacePosition: function() { + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + let activeWorkspace = this._workspaces[activeWorkspaceIndex]; + + return [activeWorkspace.gridX, activeWorkspace.gridY]; + }, + + createControllerBar: function() { + throw new Error("Not implemented"); + }, + + _positionWorkspaces: function() { + throw new Error("Not implemented"); + }, + + _workspacesChanged: function() { + throw new Error("Not implemented"); + }, + + _activeWorkspaceChanged: function() { + throw new Error("Not implemented"); + }, + + _addWorkspaceActor: function() { + throw new Error("Not implemented"); + } +} + +function MosaicView(width, height, x, y, animate) { + this._init(width, height, x, y, animate); +} + +MosaicView.prototype = { + __proto__: GenericWorkspacesView.prototype, + + _init: function(width, height, x, y, animate) { + GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate); + + this._workspaces[global.screen.get_active_workspace_index()].setSelected(true); + + this._removeButton = null; + this._addButton = null; + }, + + // Assign grid positions to workspaces. We can't just do a simple + // row-major or column-major numbering, because we don't want the + // existing workspaces to get rearranged when we add a row or + // column. So we alternate between adding to rows and adding to + // columns. (So, eg, when going from a 2x2 grid of 4 workspaces to + // a 3x2 grid of 5 workspaces, the 4 existing workspaces stay + // where they are, and the 5th one is added to the end of the + // first row.) + // + // FIXME: need to make the metacity internal layout agree with this! + _positionWorkspaces: function() { + let gridWidth = Math.ceil(Math.sqrt(this._workspaces.length)); + let gridHeight = Math.ceil(this._workspaces.length / gridWidth); + + let wsWidth = (this._width - (gridWidth - 1) * GRID_SPACING) / gridWidth; + let wsHeight = (this._height - (gridHeight - 1) * GRID_SPACING) / gridHeight; + let scale = wsWidth / global.screen_width; + + let span = 1, n = 0, row = 0, col = 0, horiz = true; + + for (let w = 0; w < this._workspaces.length; w++) { + let workspace = this._workspaces[w]; + + workspace.gridRow = row; + workspace.gridCol = col; + + workspace.gridX = this._x + workspace.gridCol * (wsWidth + GRID_SPACING); + workspace.gridY = this._y + workspace.gridRow * (wsHeight + GRID_SPACING); + workspace.scale = scale; + + if (horiz) { + col++; + if (col == span) { + row = 0; + horiz = false; + } + } else { + row++; + if (row == span) { + col = 0; + horiz = true; + span++; + } + } + } + }, + + _workspacesChanged: function() { + let oldNumWorkspaces = this._workspaces.length; + let newNumWorkspaces = global.screen.n_workspaces; + + if (oldNumWorkspaces == newNumWorkspaces) + return; + + let oldScale = this._workspaces[0].scale; + let oldGridWidth = Math.ceil(Math.sqrt(oldNumWorkspaces)); + let oldGridHeight = Math.ceil(oldNumWorkspaces / oldGridWidth); + let lostWorkspaces = []; + + // The old last workspace is no longer removable. + + if (newNumWorkspaces > oldNumWorkspaces) { + // Create new workspace groups + for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) { + this._addWorkspaceActor(w); + } + + } else { + // Truncate the list of workspaces + // FIXME: assumes that the workspaces are being removed from + // the end of the list, not the start/middle + lostWorkspaces = this._workspaces.splice(newNumWorkspaces); + } + + // The new last workspace may be removable + let newLastWorkspace = this._workspaces[this._workspaces.length - 1]; + + // Figure out the new layout + this._positionWorkspaces(); + let newScale = this._workspaces[0].scale; + let newGridWidth = Math.ceil(Math.sqrt(newNumWorkspaces)); + let newGridHeight = Math.ceil(newNumWorkspaces / newGridWidth); + + if (newGridWidth != oldGridWidth || newGridHeight != oldGridHeight) { + // We need to resize/move the existing workspaces/windows + let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces); + for (let w = 0; w < existingWorkspaces; w++) + this._workspaces[w].resizeToGrid(oldScale); + } + + if (newScale != oldScale) { + // The workspace scale affects window size/positioning because we clamp + // window size to a 1:1 ratio and never scale them up + let existingWorkspaces = Math.min(oldNumWorkspaces, newNumWorkspaces); + for (let w = 0; w < existingWorkspaces; w++) + this._workspaces[w].positionWindows(Workspace.WindowPositionFlags.ANIMATE); + } + + if (newNumWorkspaces > oldNumWorkspaces) { + // Slide new workspaces in from offscreen + for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) + this._workspaces[w].slideIn(oldScale); + } else { + // Slide old workspaces out + for (let w = 0; w < lostWorkspaces.length; w++) { + let workspace = lostWorkspaces[w]; + workspace.slideOut(function () { workspace.destroy(); }); + } + + // FIXME: deal with windows on the lost workspaces + } + + // Reset the selection state; if we went from > 1 workspace to 1, + // this has the side effect of removing the frame border + let activeIndex = global.screen.get_active_workspace_index(); + this._workspaces[activeIndex].setSelected(true); + + this._updateButtonsVisibility(); + }, + + _activeWorkspaceChanged: function(wm, from, to, direction) { + this._workspaces[from].setSelected(false); + this._workspaces[to].setSelected(true); + }, + + _addWorkspaceActor: function(workspaceNum) { + let workspace = new Workspace.Workspace(workspaceNum, this._actor); + this._workspaces[workspaceNum] = workspace; + this._actor.add_actor(workspace.actor); + }, + + createControllerBar: function() { + let actor = new St.BoxLayout({ 'pack-start': true }); + let bin = new St.Bin(); + let addButton = new St.Button({ style_class: "single-view-add" }); + this._addButton = addButton; + addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace)); + addButton._delegate = addButton; + addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) { + return this._acceptNewWorkspaceDrop(source, actor, x, y, time); + }); + actor.add(bin, { x_align: St.Align.END }); + bin.set_child(addButton); + bin.set_alignment(St.Align.END, St.Align.START); + + bin = new St.Bin(); + let removeButton = new St.Button({ style_class: "single-view-remove" }); + this._removeButton = removeButton; + removeButton.connect('clicked', Lang.bind(this, function() { + if (this._workspaces.length <= 1) + return; + global.screen.remove_workspace(this._workspaces[this._workspaces.length - 1]._metaWorkspace, global.get_current_time()); + })); + actor.add(bin, { expand: true, x_fill: true, x_align: St.Align.END }); + this._updateButtonsVisibility(); + bin.set_child(removeButton); + bin.set_alignment(St.Align.END, St.Align.START); + + return actor; + }, + + _updateButtonsVisibility: function() { + //_removeButton may yet not exist. + if (this._removeButton == null) + return; + if (global.screen.n_workspaces == 1) + this._removeButton.hide(); + else + this._removeButton.show(); + if (this._addButton == null) + return; + if (global.screen.n_workspaces >= MAX_WORKSPACES) + this._addButton.hide(); + else + this._addButton.show(); + }, + + _addNewWorkspace: function() { + global.screen.append_new_workspace(false, global.get_current_time()); + }, + + _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) { + this._addNewWorkspace(); + return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time); + } +}; + +// Create a SpecialPropertyModifier to let us move windows in a +// straight line on the screen even though their containing workspace +// is also moving. +Tweener.registerSpecialPropertyModifier("workspace_relative", _workspaceRelativeModifier, _workspaceRelativeGet); + +function SingleView(width, height, x, y, animate) { + this._init(width, height, x, y, animate); +} + +SingleView.prototype = { + __proto__: GenericWorkspacesView.prototype, + + _init: function(width, height, x, y, animate) { + this._scroll = null; + GenericWorkspacesView.prototype._init.call(this, width, height, x, y, animate); + + this._actor.set_clip(x, y, width, height); + this._addButton = null; + this._removeButton = null; + this._indicatorsPanel = null; + this._indicatorsPanelWidth = null; + + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + for (let w = 0; w < this._workspaces.length; w++) { + if (w != activeWorkspaceIndex) { + this._workspaces[w].actor.hide(); + continue; + } + this._workspaces[w].actor.show(); + this._workspaces[w]._windowOverlaysGroup.show(); + } + }, + + _positionWorkspaces: function() { + let position = global.screen.get_active_workspace_index(); + let scale = this._width / global.screen_width; + + if (this._scroll != null) + position = this._scroll.adjustment.value; + let isInt = (Math.round(position) === position); + + for (let w = 0; w < this._workspaces.length; w++) { + let workspace = this._workspaces[w]; + + workspace.gridRow = 0; + workspace.gridCol = 0; + + workspace.scale = scale; + workspace.actor.set_scale(scale, scale); + workspace.gridX = this._x + (w - position) * workspace.actor.width; + workspace.gridY = this._y; + workspace.actor.set_position(workspace.gridX, workspace.gridY); + if (isInt) { + if (this.actor.get_stage() != null) + workspace.positionWindows(0); + if (w == position) { + workspace._windowOverlaysGroup.show(); + workspace.actor.show(); + } else { + workspace._windowOverlaysGroup.hide(); + workspace.actor.hide(); + } + } else { + workspace._windowOverlaysGroup.hide(); + if (Math.abs(w - position) <= 1) + workspace.actor.show(); + else + workspace.actor.hide(); + } + } + }, + + _workspacesChanged: function() { + let oldNumWorkspaces = this._workspaces.length; + let newNumWorkspaces = global.screen.n_workspaces; + + if (oldNumWorkspaces == newNumWorkspaces) + return; + + if (this._scroll != null) { + let adj = this._scroll.get_adjustment(); + adj.upper = newNumWorkspaces; + this._scroll.adjustment = adj; + } + let lostWorkspaces = []; + + if (newNumWorkspaces > oldNumWorkspaces) { + // Create new workspace groups + for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) { + this._addWorkspaceActor(w); + this._workspaces[w].actor.hide(); + } + + } else { + for (let i = 0; i < this._workspaces.length; i++) + this._workspaces[i].destroy(); + this._actor.remove_all(); + + //Without this will be a lot of warnings + this._actor.hide(); + + this._workspaces = []; + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + for (let w = 0; w < global.screen.n_workspaces; w++) { + this._addWorkspaceActor(w); + if (w == activeWorkspaceIndex) { + this._workspaces[w].actor.show(); + } else { + this._workspaces[w].actor.hide(); + } + } + this._actor.show(); + } + this._positionWorkspaces(); + + // Reset the selection state; if we went from > 1 workspace to 1, + // this has the side effect of removing the frame border + let activeIndex = global.screen.get_active_workspace_index(); + this._workspaces[activeIndex].actor.show(); + this._workspaces[activeIndex]._windowOverlaysGroup.show(); + + this._updatePanelVisibility(); + }, + + _activeWorkspaceChanged: function(wm, from, to, direction) { + this._updatePanelVisibility(); + let showAnimation = true; + + if (this._scroll != null) { + let adj = this._scroll.get_adjustment(); + if (Math.round(adj.value - to) != adj.value - to) + showAnimation = false; + if (adj.value - to == 0) + showAnimation = false; + adj.value = to; + this._scroll.adjustment = adj; + } + if (showAnimation) { + let fx; + if (from > to) { + fx = this._workspaces[0].actor.width; + } else { + fx = -this._workspaces[0].actor.width; + } + this._workspaces[from]._windowOverlaysGroup.hide(); + this._workspaces[to].actor.set_position(this._x - fx, this._workspaces[to].gridY); + this._workspaces[to].actor.show(); + Tweener.addTween(this._workspaces[to].actor, + { x: this._x, + transition: 'easeOutQuad', + time: WORKSPACE_SWITCH_TIME + }); + + Tweener.addTween(this._workspaces[from].actor, + { x: this._x + fx, + transition: 'easeOutQuad', + time: WORKSPACE_SWITCH_TIME, + onComplete: this._positionWorkspaces, + onCompleteScope: this + }); + } else + this._positionWorkspaces(); + }, + + _addWorkspaceActor: function(workspaceNum) { + let workspace = new Workspace.Workspace(workspaceNum, this._actor); + this._actor.add_actor(workspace.actor); + workspace._windowOverlaysGroup.hide(); + + this._workspaces[workspaceNum] = workspace; + }, + + createControllerBar: function() { + let panel = new St.BoxLayout({ 'pack-start': true, vertical: true }); + + let actor = new St.BoxLayout({ 'pack-start': true }); + let adj = new St.Adjustment({ value: global.screen.get_active_workspace_index(), + lower: 0, + 'page-increment': 1, + 'page-size': 1, + 'step-increment': 1, + upper: this._workspaces.length }); + this._scroll = new St.ScrollBar({ adjustment: null, vertical: false, name: 'SwitchScroll' }); + + this._scroll.connect('notify::adjustment', Lang.bind(this, function() { + this._scroll.adjustment.connect('notify::value', Lang.bind(this, function () { + if (Math.abs(Math.round(this._scroll.adjustment.value) - this._scroll.adjustment.value) < 0.1) { + this._scroll.adjustment.set_value (Math.round(this._scroll.adjustment.value)); + this._workspaces[Math.round(this._scroll.adjustment.value)]._metaWorkspace.activate(global.get_current_time()); + } else + this._positionWorkspaces(); + })); + })); + this._scroll.adjustment = adj; + + let addButton = new St.Button({ style_class: "single-view-add" }); + this._addButton = addButton; + addButton.connect('clicked', Lang.bind(this, this._addNewWorkspace)); + addButton._delegate = addButton; + addButton._delegate.acceptDrop = Lang.bind(this, function(source, actor, x, y, time) { + return this._acceptNewWorkspaceDrop(source, actor, x, y, time); + }); + actor.add(addButton, {x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false}); + + let removeButton = new St.Button({ style_class: "single-view-remove" }); + this._removeButton = removeButton; + removeButton.connect('clicked', Lang.bind(this, function() { + if (this._workspaces.length <= 1) + return; + let index = global.screen.get_active_workspace_index(); + if (index == 0) + return; + global.screen.remove_workspace(this._workspaces[index]._metaWorkspace, global.get_current_time()); + })); + actor.add(removeButton, { x_align: St.Align.END, y_align: St.Align.START, 'y-fill': false }); + this._updatePanelVisibility(); + + panel.add(this._createPositionalIndicator(), {expand: true, 'x-fill': true, 'y-fill': true}); + panel.add(this._scroll, { expand: true, + 'x-fill': true, + 'y-fill': false, + y_align: St.Align.START }); + // backward-stepper/forward-stepper has const width (= height) + let separator = new St.Button({ style_class: 'scroll-separator' }); + actor.add(separator, {}); + + actor.add(panel, {expand: true, 'x-fill': true, 'y-fill': true}); + + separator = new St.Button({ style_class: 'scroll-separator' }); + actor.add(separator, {}); + + return actor; + }, + + _addIndicatorClone: function(i, active) { + let actor = new St.Button({ style_class: 'workspace-indicator' }); + if (active) { + actor.style_class = 'workspace-indicator-active'; + } + actor.connect('button-release-event', Lang.bind(this, function() { + if (this._workspaces[i] != undefined) + this._workspaces[i]._metaWorkspace.activate(global.get_current_time()); + })); + + this._indicatorsPanel.add_actor(actor); + + let [a, spacing] = actor.get_theme_node().get_length('border-spacing', false); + if (this._indicatorsPanelWidth < spacing * (i + 1) + actor.width * (i + 1)) + actor.hide(); + actor.x = spacing * i + actor.width * i; + }, + + _fillPositionalIndicator: function() { + if (this._indicatorsPanel == null || this._indicatorsPanelWidth == null) + return; + let width = this._indicatorsPanelWidth; + this._indicatorsPanel.remove_all(); + + let activeWorkspaceIndex = global.screen.get_active_workspace_index(); + for (let i = 0; i < this._workspaces.length; i++) { + this._addIndicatorClone(i, i == activeWorkspaceIndex); + } + this._indicatorsPanel.x = (this._indicatorsPanelWidth - this._indicatorsPanel.width) / 2; + }, + + _createPositionalIndicator: function() { + let actor = new St.Bin({ style_class: 'panel-button' }); + let group = new Clutter.Group(); + + this._indicatorsPanel = new Shell.GenericContainer(); + this._indicatorsPanel.connect('get-preferred-width', Lang.bind(this, function (actor, fh, alloc) { + let children = actor.get_children(); + let width = 0; + for (let i = 0; i < children.length; i++) { + if (!children[i].visible) + continue; + if (children[i].x + children[i].width <= width) + continue; + width = children[i].x + children[i].width; + } + alloc.min_size = width; + alloc.nat_size = width; + })); + this._indicatorsPanel.connect('get-preferred-height', Lang.bind(this, function (actor, fw, alloc) { + let children = actor.get_children(); + let height = 0; + if (children.length) + height = children[0].height; + alloc.min_size = height; + alloc.nat_size = height; + })); + this._indicatorsPanel.connect('allocate', Lang.bind(this, function (actor, box, flags) { + let children = actor.get_children(); + for (let i = 0; i < children.length; i++) { + if (!children[i].visible) + continue; + let childBox = new Clutter.ActorBox(); + childBox.x1 = children[i].x; + childBox.y1 = 0; + childBox.x2 = children[i].x + children[i].width; + childBox.y2 = children[i].height; + children[i].allocate(childBox, flags); + } + })); + + group.add_actor(this._indicatorsPanel); + actor.set_child(group); + actor.set_alignment(St.Align.START, St.Align.START); + actor.set_fill(true, true); + this._indicatorsPanel.hide(); + actor.connect('notify::width', Lang.bind(this, function(actor) { + this._indicatorsPanelWidth = actor.width; + this._updatePanelVisibility(); + })); + actor.connect('destroy', Lang.bind(this, function() { + this._indicatorsPanel = null; + })); + return actor; + }, + + _updatePanelVisibility: function() { + let n = global.screen.n_workspaces; + if (this._removeButton != null) { + // set opacity here, because if hide it, _scroll will fill this space. + if (global.screen.get_active_workspace_index() == 0) + this._removeButton.set_opacity(0); + else + this._removeButton.set_opacity(255); + } + if (this._addButton != null) { + // same here + this._addButton.set_opacity((global.screen.n_workspaces < MAX_WORKSPACES) * 255); + } + if (this._scroll != null) { + if (n > 1) + this._scroll.show(); + else + this._scroll.hide(); + } + if (this._indicatorsPanel != null) { + if (n == 1) { + this._indicatorsPanel.hide(); + } else { + this._indicatorsPanel.show(); + } + } + this._fillPositionalIndicator(); + }, + + _addNewWorkspace: function() { + // Button with opacity 0 is clickable. + if (global.screen.n_workspaces >= MAX_WORKSPACES) + return; + global.screen.append_new_workspace(false, global.get_current_time()); + this._workspaces[this._workspaces.length - 1]._metaWorkspace.activate(Clutter.get_current_event_time()); + }, + + _acceptNewWorkspaceDrop: function(source, dropActor, x, y, time) { + this._addNewWorkspace(); + return this.acceptNewWorkspaceDrop(source, dropActor, x, y, time); + } +}; + +function _workspaceRelativeModifier(workspace) { + let [startX, startY] = Main.overview.getPosition(); + let overviewPosX, overviewPosY, overviewScale; + + if (!workspace) + return []; + + if (workspace.leavingOverview) { + let [zoomedInX, zoomedInY] = Main.overview.getZoomedInPosition(); + overviewPosX = { begin: startX, end: zoomedInX }; + overviewPosY = { begin: startY, end: zoomedInY }; + overviewScale = { begin: Main.overview.getScale(), + end: Main.overview.getZoomedInScale() }; + } else { + overviewPosX = { begin: startX, end: 0 }; + overviewPosY = { begin: startY, end: 0 }; + overviewScale = { begin: Main.overview.getScale(), end: 1 }; + } + + return [ { name: "x", + parameters: { workspacePos: workspace.gridX, + overviewPos: overviewPosX, + overviewScale: overviewScale } }, + { name: "y", + parameters: { workspacePos: workspace.gridY, + overviewPos: overviewPosY, + overviewScale: overviewScale } } + ]; +} + +function _workspaceRelativeGet(begin, end, time, params) { + let curOverviewPos = (1 - time) * params.overviewPos.begin + + time * params.overviewPos.end; + let curOverviewScale = (1 - time) * params.overviewScale.begin + + time * params.overviewScale.end; + + // Calculate the screen position of the window. + let screen = (1 - time) * + ((begin + params.workspacePos) * params.overviewScale.begin + + params.overviewPos.begin) + + time * + ((end + params.workspacePos) * params.overviewScale.end + + params.overviewPos.end); + + // Return the workspace coordinates. + return (screen - curOverviewPos) / curOverviewScale - params.workspacePos; +} + +function WorkspacesViewSwitch() { + this._init(); +} + +WorkspacesViewSwitch.prototype = { + VIEW_KEY: 'view', + + _init: function() { + this._gconf = Shell.GConf.get_default(); + this._mosaicViewButton = null; + this._singleViewButton = null; + this._currentViewType = this._gconf.get_int(this.VIEW_KEY); + this._controlsBar = null; + }, + + _setView: function(view) { + this._mosaicViewButton.set_checked(WorkspacesViewType.MOSAIC == view); + this._singleViewButton.set_checked(WorkspacesViewType.SINGLE == view); + + if (this._currentViewType == view) + return; + this._currentViewType = view; + this._gconf.set_int(this.VIEW_KEY, view); + this.emit('view-changed'); + }, + + createCurrentWorkspaceView: function(width, height, x, y, animate) { + switch (this._currentViewType) { + case WorkspacesViewType.SINGLE: + return new SingleView(width, height, x, y, animate); + case WorkspacesViewType.MOSAIC: + return new MosaicView(width, height, x, y, animate); + default: + return new MosaicView(width, height, x, y, animate); + } + }, + + createControlsBar: function() { + let actor = new St.BoxLayout(); + + this._mosaicViewButton = new St.Button({ style_class: "switch-view-mosaic" }); + this._mosaicViewButton.set_toggle_mode(true); + this._mosaicViewButton.connect('clicked', Lang.bind(this, function() { + this._setView(WorkspacesViewType.MOSAIC); + })); + actor.add(this._mosaicViewButton, {'y-fill' : false, 'y-align' : St.Align.START}); + + this._singleViewButton = new St.Button({ style_class: "switch-view-single" }); + this._singleViewButton.set_toggle_mode(true); + this._singleViewButton.connect('clicked', Lang.bind(this, function() { + this._setView(WorkspacesViewType.SINGLE); + })); + actor.add(this._singleViewButton, {'y-fill' : false, 'y-align' : St.Align.START}); + + if (this._currentViewType == WorkspacesViewType.MOSAIC) + this._mosaicViewButton.set_checked(true); + else + this._singleViewButton.set_checked(true); + + this._nWorkspacesNotifyId = + global.screen.connect('notify::n-workspaces', + Lang.bind(this, this._workspacesChanged)); + + actor.connect('destroy', Lang.bind(this, function() { + this._controlsBar = null; + global.screen.disconnect(this._nWorkspacesNotifyId); + })); + + this._controlsBar = actor; + this._workspacesChanged(); + return actor; + }, + + _workspacesChanged: function() { + if (this._controlsBar == null) + return; + if (global.screen.n_workspaces == 1) + this._controlsBar.set_opacity(0); + else + this._controlsBar.set_opacity(255); + } +}; + +Signals.addSignalMethods(WorkspacesViewSwitch.prototype);