gnome-shell/js/ui/overview.js

515 lines
20 KiB
JavaScript

/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Lang = imports.lang;
const AppDisplay = imports.ui.appDisplay;
const DocDisplay = imports.ui.docDisplay;
const GenericDisplay = imports.ui.genericDisplay;
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 ROOT_OVERVIEW_COLOR = new Clutter.Color();
ROOT_OVERVIEW_COLOR.from_pixel(0x000000ff);
// Time for initial animation going into Overview mode
const ANIMATION_TIME = 0.25;
// 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;
let wideScreen = false;
let displayGridColumnWidth = null;
let displayGridRowHeight = null;
let addRemoveButtonSize = null;
function Overview() {
this._init();
}
Overview.prototype = {
_init : function() {
this._group = new Clutter.Group();
this._group._delegate = this;
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 Clutter.Rectangle({ color: ROOT_OVERVIEW_COLOR });
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 Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
spacing: 6 });
// 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.lower_bottom();
this._coverPane.lower_bottom();
this._workspaces = null;
},
_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();
this._group.set_position(primary.x, primary.y);
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);
this._workspacesX = displayGridColumnWidth + WORKSPACE_GRID_PADDING;
this._workspacesY = Math.floor(displayGridRowHeight + WORKSPACE_GRID_PADDING * (primary.height / primary.width));
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;
// 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);
// 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;
this._transparentBackground.set_position(this._paneContainer.x, this._paneContainer.y);
this._transparentBackground.set_size(primary.width - this._paneContainer.x,
this._paneContainer.height);
if (this._activeDisplayPane != null)
this._activeDisplayPane.actor.width = displayGridColumnWidth * 2;
},
addPane: function (pane) {
this._paneContainer.append(pane.actor, Big.BoxPackFlags.NONE);
// 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) {
pane.actor.width = displayGridColumnWidth * 2;
this._activeDisplayPane = pane;
this._transparentBackground.raise_top();
this._paneContainer.raise_top();
if (backgroundEventId != null)
this._transparentBackground.disconnect(backgroundEventId);
backgroundEventId = this._transparentBackground.connect('button-release-event', Lang.bind(this, function () {
this._activeDisplayPane.close();
return true;
}));
this._workspaces.actor.opacity = 64;
} else if (pane == this._activeDisplayPane) {
this._activeDisplayPane = null;
if (backgroundEventId != null) {
this._transparentBackground.disconnect(backgroundEventId);
backgroundEventId = null;
}
this._transparentBackground.lower_bottom();
this._paneContainer.lower_bottom();
this._workspaces.actor.opacity = 255;
}
}));
},
//// Draggable target interface ////
// Closes any active panes if a GenericDisplayItem is being
// dragged over the Overview, i.e. as soon as it starts being dragged.
// This allows the user to place the item on any workspace.
handleDragOver : function(source, actor, x, y, time) {
if (source instanceof GenericDisplay.GenericDisplayItem
|| source instanceof AppDisplay.AppIcon) {
if (this._activeDisplayPane != null)
this._activeDisplayPane.close();
return true;
}
return false;
},
//// Public methods ////
// 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._workspaces = new Workspaces.Workspaces(this._workspacesWidth, this._workspacesHeight,
this._workspacesX, this._workspacesY);
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);
// 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);
// 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 to 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();
this._addButton.actor.destroy();
this._addButton.actor = null;
this._addButton = null;
// 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;
},
/**
* activateWindow:
* @metaWindow: A #MetaWindow
* @time: Event timestamp integer
*
* Make the given MetaWindow be the focus window, switching
* to the workspace it's on if necessary. This function
* should only be used when the Overview is currently active;
* outside of that, use the relevant methods on MetaDisplay.
*/
activateWindow: function (metaWindow, time) {
this._workspaces.activateWindowFromOverview(metaWindow, time);
},
//// 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._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');
},
_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._acceptDropCallback(source, actor, x, y, time);
}
};