/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ const Clutter = imports.gi.Clutter; const Mainloop = imports.mainloop; const Signals = imports.signals; const Lang = imports.lang; const St = imports.gi.St; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const GenericDisplay = imports.ui.genericDisplay; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Panel = imports.ui.panel; const Dash = imports.ui.dash; const Tweener = imports.ui.tweener; const WorkspacesView = imports.ui.workspacesView; // Time for initial animation going into Overview mode const ANIMATION_TIME = 0.25; // Time for pane menus to fade in/out const PANE_FADE_TIME = 0.1; // We divide the screen into a grid of rows and columns, which we use // to help us position the Overview components, such as the side panel // that lists applications and documents, the workspaces display, and // the button for adding additional workspaces. // In the regular mode, the side panel takes up one column on the left, // and the workspaces display takes up the remaining columns. // In the expanded side panel display mode, the side panel takes up two // columns, and the workspaces display slides all the way to the right, // being visible only in the last quarter of the right-most column. // In the future, this mode will have more components, such as a display // of documents which were recently opened with a given application, which // will take up the remaining sections of the display. const WIDE_SCREEN_CUT_OFF_RATIO = 1.4; // A common netbook resolution is 1024x600, which trips the widescreen // ratio. However that leaves way too few pixels for the dash. So // just treat this as a regular screen. const WIDE_SCREEN_MINIMUM_HEIGHT = 768; const COLUMNS_REGULAR_SCREEN = 4; const ROWS_REGULAR_SCREEN = 8; const COLUMNS_WIDE_SCREEN = 5; const ROWS_WIDE_SCREEN = 10; const DEFAULT_PADDING = 4; // Padding around workspace grid / Spacing between Dash and Workspaces const WORKSPACE_GRID_PADDING = 12; const COLUMNS_FOR_WORKSPACES_REGULAR_SCREEN = 3; const ROWS_FOR_WORKSPACES_REGULAR_SCREEN = 6; const COLUMNS_FOR_WORKSPACES_WIDE_SCREEN = 4; const ROWS_FOR_WORKSPACES_WIDE_SCREEN = 8; // A multi-state; PENDING is used during animations const STATE_ACTIVE = true; const STATE_PENDING_INACTIVE = false; const STATE_INACTIVE = false; const SHADOW_COLOR = new Clutter.Color(); SHADOW_COLOR.from_pixel(0x00000033); const TRANSPARENT_COLOR = new Clutter.Color(); TRANSPARENT_COLOR.from_pixel(0x00000000); const SHADOW_WIDTH = 6; const NUMBER_OF_SECTIONS_IN_SEARCH = 2; const INFO_BAR_HIDE_TIMEOUT = 10; let wideScreen = false; let displayGridColumnWidth = null; let displayGridRowHeight = null; function InfoBar() { this._init(); } InfoBar.prototype = { _init: function() { this.actor = new St.Bin({ style_class: 'info-bar-panel', x_fill: true, y_fill: false }); this._label = new St.Label(); this._undo = new St.Button({ style_class: 'info-bar-link-button' }); let bin = new St.Bin({ x_fill: false, y_fill: false, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); this.actor.set_child(bin); let box = new St.BoxLayout({ style_class: 'info-bar' }); bin.set_child(box); this._timeoutId = 0; box.add(this._label, {'y-fill' : false, 'y-align' : St.Align.MIDDLE}); box.add(this._undo); this.actor.set_opacity(0); this._undoCallback = null; this._undo.connect('clicked', Lang.bind(this, this._onUndoClicked)); }, _onUndoClicked: function() { Mainloop.source_remove(this._timeoutId); this._timeoutId = 0; if (this._undoCallback) this._undoCallback(); this.actor.set_opacity(0); this._undoCallback = null; }, _hideDone: function() { this._undoCallback = null; }, _hide: function() { Tweener.addTween(this.actor, { opacity: 0, transition: 'easeOutQuad', time: ANIMATION_TIME, onComplete: this._hideDone, onCompleteScope: this }); }, _onTimeout: function() { this._timeoutId = 0; this._hide(); return false; }, setMessage: function(text, undoCallback, undoLabel) { if (this._timeoutId) Mainloop.source_remove(this._timeoutId); this._timeout = false; this._label.text = text; Tweener.addTween(this.actor, { opacity: 255, transition: 'easeOutQuad', time: ANIMATION_TIME }); this._timeoutId = Mainloop.timeout_add_seconds(INFO_BAR_HIDE_TIMEOUT, Lang.bind(this, this._onTimeout)); if (undoLabel) this._undo.label = undoLabel; else this._undo.label = _("Undo"); this._undoCallback = undoCallback; if (undoCallback) this._undo.show(); else this._undo.hide(); } }; function Overview() { this._init(); } Overview.prototype = { _init : function() { this._group = new St.Group({ style_class: 'overview' }); this._group._delegate = this; this._group.connect('destroy', Lang.bind(this, function() { if (this._lightbox) { this._lightbox.destroy(); this._lightbox = null; } })); this.infoBar = new InfoBar(); this._group.add_actor(this.infoBar.actor); this._workspacesManager = null; this._lightbox = null; this.visible = false; this.animationInProgress = false; this._hideInProgress = false; this._recalculateGridSizes(); this._activeDisplayPane = null; // During transitions, we raise this to the top to avoid having the overview // area be reactive; it causes too many issues such as double clicks on // Dash elements, or mouseover handlers in the workspaces. this._coverPane = new Clutter.Rectangle({ opacity: 0, reactive: true }); this._group.add_actor(this._coverPane); this._coverPane.connect('event', Lang.bind(this, function (actor, event) { return true; })); // Similar to the cover pane but used for dialogs ("panes"); see the comments // in addPane below. this._transparentBackground = new Clutter.Rectangle({ opacity: 0, reactive: true }); this._group.add_actor(this._transparentBackground); // Background color for the Overview this._backOver = new St.Label(); this._group.add_actor(this._backOver); this._group.hide(); global.overlay_group.add_actor(this._group); // TODO - recalculate everything when desktop size changes this._dash = new Dash.Dash(); this._group.add_actor(this._dash.actor); // Container to hold popup pane chrome. this._paneContainer = new St.BoxLayout({ style_class: 'overview-pane' }); // Note here we explicitly don't set the paneContainer to be reactive yet; that's done // inside the notify::visible handler on panes. this._paneContainer.connect('button-release-event', Lang.bind(this, function(background) { this._activeDisplayPane.close(); return true; })); this._group.add_actor(this._paneContainer); this._transparentBackground.lower_bottom(); this._paneContainer.hide(); this._coverPane.lower_bottom(); this.workspaces = null; }, _onViewChanged: function() { if (!this.visible) return; this.workspaces = this._workspacesManager.workspacesView; // Show new workspacesView this._group.add_actor(this.workspaces.actor); this._workspacesBar.raise(this.workspaces.actor); this._dash.actor.raise(this.workspaces.actor); }, _recalculateGridSizes: function () { let primary = global.get_primary_monitor(); wideScreen = (primary.width/primary.height > WIDE_SCREEN_CUT_OFF_RATIO) && (primary.height >= WIDE_SCREEN_MINIMUM_HEIGHT); // We divide the screen into an imaginary grid which helps us determine the layout of // different visual components. if (wideScreen) { displayGridColumnWidth = Math.floor(primary.width / COLUMNS_WIDE_SCREEN); displayGridRowHeight = Math.floor(primary.height / ROWS_WIDE_SCREEN); } else { displayGridColumnWidth = Math.floor(primary.width / COLUMNS_REGULAR_SCREEN); displayGridRowHeight = Math.floor(primary.height / ROWS_REGULAR_SCREEN); } }, relayout: function () { let primary = global.get_primary_monitor(); let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL); this._recalculateGridSizes(); this._group.set_position(primary.x, primary.y); this._group.set_size(primary.width, primary.height); let contentY = Panel.PANEL_HEIGHT; let contentHeight = primary.height - contentY; this._coverPane.set_position(0, contentY); this._coverPane.set_size(primary.width, contentHeight); let workspaceColumnsUsed = wideScreen ? COLUMNS_FOR_WORKSPACES_WIDE_SCREEN : COLUMNS_FOR_WORKSPACES_REGULAR_SCREEN; let workspaceRowsUsed = wideScreen ? ROWS_FOR_WORKSPACES_WIDE_SCREEN : ROWS_FOR_WORKSPACES_REGULAR_SCREEN; this._workspacesWidth = displayGridColumnWidth * workspaceColumnsUsed - WORKSPACE_GRID_PADDING * 2; // We scale the vertical padding by (primary.height / primary.width) // so that the workspace preserves its aspect ratio. this._workspacesHeight = Math.floor(displayGridRowHeight * workspaceRowsUsed - WORKSPACE_GRID_PADDING * (primary.height / primary.width) * 2); if (rtl) { this._workspacesX = WORKSPACE_GRID_PADDING; } else { this._workspacesX = displayGridColumnWidth + WORKSPACE_GRID_PADDING; } this._workspacesY = Math.floor(displayGridRowHeight + WORKSPACE_GRID_PADDING * (primary.height / primary.width)); if (rtl) { this._dash.actor.set_position(primary.width - displayGridColumnWidth, contentY); } else { this._dash.actor.set_position(0, contentY); } this._dash.actor.set_size(displayGridColumnWidth, contentHeight); this._dash.searchArea.height = this._workspacesY - contentY; this._dash.sectionArea.height = this._workspacesHeight; this._dash.searchResults.actor.height = this._workspacesHeight; this.infoBar.actor.set_position(displayGridColumnWidth, Panel.PANEL_HEIGHT); this.infoBar.actor.set_size(primary.width - displayGridColumnWidth, this._workspacesY - Panel.PANEL_HEIGHT); this.infoBar.actor.raise_top(); // place the 'Add Workspace' button in the bottom row of the grid this._workspacesBarX = this._workspacesX; this._workspacesBarWidth = this._workspacesWidth; this._workspacesBarY = primary.height - displayGridRowHeight; // The parent (this._group) is positioned at the top left of the primary monitor // while this._backOver occupies the entire screen. this._backOver.set_position(- primary.x, - primary.y); this._backOver.set_size(global.screen_width, global.screen_height); this._paneContainer.set_position(this._dash.actor.x + this._dash.actor.width + DEFAULT_PADDING, this._workspacesY); // Dynamic width this._paneContainer.height = this._workspacesHeight; if (rtl) { this._paneContainer.connect('notify::width', Lang.bind(this, function (paneContainer) { paneContainer.x = this._dash.actor.x - (DEFAULT_PADDING + paneContainer.width); })); } this._transparentBackground.set_position(primary.x, primary.y); this._transparentBackground.set_size(primary.width, primary.height); }, addPane: function (pane, align) { pane.actor.height = .9 * this._workspacesHeight; this._paneContainer.add(pane.actor, { expand: true, y_fill: false, y_align: align }); // When a pane is displayed, we raise the transparent background to the top // and connect to button-release-event on it, then raise the pane above that. // The idea here is that clicking anywhere outside the pane should close it. // When the active pane is closed, undo the effect. let backgroundEventId = null; pane.connect('open-state-changed', Lang.bind(this, function (pane, isOpen) { if (isOpen) { this._activeDisplayPane = pane; this._transparentBackground.raise_top(); this._paneContainer.raise_top(); this._paneContainer.show(); this._paneReady = false; if (backgroundEventId != null) this._transparentBackground.disconnect(backgroundEventId); backgroundEventId = this._transparentBackground.connect('captured-event', Lang.bind(this, function (actor, event) { if (event.get_source() != this._transparentBackground) return false; if (event.type() == Clutter.EventType.BUTTON_PRESS) this._paneReady = true; if (event.type() == Clutter.EventType.BUTTON_RELEASE && this._paneReady) this._activeDisplayPane.close(); return true; })); if (!this._lightbox) this._lightbox = new Lightbox.Lightbox(this._group, { fadeTime: PANE_FADE_TIME }); this._lightbox.show(); this._lightbox.highlight(this._paneContainer); } else if (pane == this._activeDisplayPane) { this._activeDisplayPane = null; if (backgroundEventId != null) { this._transparentBackground.disconnect(backgroundEventId); backgroundEventId = null; } this._transparentBackground.lower_bottom(); this._paneContainer.hide(); this._lightbox.hide(); } })); }, //// Public methods //// beginItemDrag: function(source) { // Close any active panes if @source is a GenericDisplayItem. // This allows the user to place the item on any workspace. if (source instanceof GenericDisplay.GenericDisplayItem) if (this._activeDisplayPane != null) this._activeDisplayPane.close(); this.emit('item-drag-begin'); }, endItemDrag: function(source) { this.emit('item-drag-end'); }, // Returns the scale the Overview has when we just start zooming out // to overview mode. That is, when just the active workspace is showing. getZoomedInScale : function() { return 1 / this.workspaces.getScale(); }, // Returns the position the Overview has when we just start zooming out // to overview mode. That is, when just the active workspace is showing. getZoomedInPosition : function() { let [posX, posY] = this.workspaces.getActiveWorkspacePosition(); let scale = this.getZoomedInScale(); return [- posX * scale, - posY * scale]; }, // Returns the current scale of the Overview. getScale : function() { return this._group.scaleX; }, // Returns the current position of the Overview. getPosition : function() { return [this._group.x, this._group.y]; }, show : function() { if (this.visible) return; if (!Main.pushModal(this._dash.actor)) return; this.visible = true; this.animationInProgress = true; this._dash.show(); /* TODO: make this stuff dynamic */ this._workspacesManager = new WorkspacesView.WorkspacesManager(this._workspacesWidth, this._workspacesHeight, this._workspacesX, this._workspacesY); this._workspacesManager.connect('view-changed', Lang.bind(this, this._onViewChanged)); this.workspaces = this._workspacesManager.workspacesView; 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 // for drag and drop to work. In the future we should fix the workspaces to not // be as big as the screen. this._dash.actor.raise(this.workspaces.actor); this._workspacesBar = this._workspacesManager.controlsBar.actor; this._workspacesBar.set_position(this._workspacesBarX, this._workspacesBarY); this._workspacesBar.width = this._workspacesBarWidth; this._group.add_actor(this._workspacesBar); this._workspacesBar.raise(this.workspaces.actor); // All the the actors in the window group are completely obscured, // hiding the group holding them while the Overview is displayed greatly // increases performance of the Overview especially when there are many // windows visible. // // If we switched to displaying the actors in the Overview rather than // clones of them, this would obviously no longer be necessary. global.window_group.hide(); this._group.show(); // Create a zoom out effect. First scale the Overview group up and // position it so that the active workspace fills up the whole screen, // then transform the group to its normal dimensions and position. // The opposite transition is used in hide(). this._group.scaleX = this._group.scaleY = this.getZoomedInScale(); [this._group.x, this._group.y] = this.getZoomedInPosition(); let primary = global.get_primary_monitor(); Tweener.addTween(this._group, { x: primary.x, y: primary.y, scaleX: 1, scaleY: 1, transition: 'easeOutQuad', time: ANIMATION_TIME, onComplete: this._showDone, onCompleteScope: this }); // Make Dash fade in so that it doesn't appear too big. this._dash.actor.opacity = 0; Tweener.addTween(this._dash.actor, { opacity: 255, transition: 'easeOutQuad', time: ANIMATION_TIME }); this._coverPane.raise_top(); this.emit('showing'); }, hide: function() { if (!this.visible || this._hideInProgress) return; this.animationInProgress = true; this._hideInProgress = true; if (this._activeDisplayPane != null) this._activeDisplayPane.close(); this.workspaces.hide(); // Create a zoom in effect by transforming the Overview group so that // the active workspace fills up the whole screen. The opposite // transition is used in show(). let scale = this.getZoomedInScale(); let [posX, posY] = this.getZoomedInPosition(); Tweener.addTween(this._group, { x: posX, y: posY, scaleX: scale, scaleY: scale, transition: 'easeOutQuad', time: ANIMATION_TIME, onComplete: this._hideDone, onCompleteScope: this }); // Make Dash fade out so that it doesn't appear to big. Tweener.addTween(this._dash.actor, { opacity: 0, transition: 'easeOutQuad', time: ANIMATION_TIME }); this._coverPane.raise_top(); this.emit('hiding'); }, toggle: function() { if (this.visible) this.hide(); else this.show(); }, /** * getWorkspacesForWindow: * @metaWindow: A #MetaWindow * * Returns the Workspaces object associated with the given window. * This method is not be accessible if the overview is not open * and will return %null. */ getWorkspacesForWindow: function(metaWindow) { return this.workspaces; }, //// Private methods //// _showDone: function() { if (this._hideInProgress) return; this.animationInProgress = false; this._coverPane.lower_bottom(); this.emit('shown'); }, _hideDone: function() { global.window_group.show(); this.workspaces.destroy(); this.workspaces = null; this._workspacesBar.destroy(); this._workspacesBar = null; this._workspacesManager = null; this._dash.hide(); this._group.hide(); this.visible = false; this.animationInProgress = false; this._hideInProgress = false; this._coverPane.lower_bottom(); Main.popModal(this._dash.actor); this.emit('hidden'); } }; Signals.addSignalMethods(Overview.prototype);