gnome-shell/js/ui/overview.js

740 lines
25 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
const Gtk = imports.gi.Gtk;
const Meta = imports.gi.Meta;
const Mainloop = imports.mainloop;
const Signals = imports.signals;
const Lang = imports.lang;
const St = imports.gi.St;
const Shell = imports.gi.Shell;
const Gdk = imports.gi.Gdk;
const CenterLayout = imports.ui.centerLayout;
const Dash = imports.ui.dash;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const Panel = imports.ui.panel;
const Params = imports.misc.params;
const Tweener = imports.ui.tweener;
const ViewSelector = imports.ui.viewSelector;
const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
// Time for initial animation going into Overview mode
const ANIMATION_TIME = 0.25;
const DND_WINDOW_SWITCH_TIMEOUT = 1250;
const GLSL_DIM_EFFECT_DECLARATIONS = '';
const GLSL_DIM_EFFECT_CODE = '\
vec2 dist = cogl_tex_coord_in[0].xy - vec2(0.5, 0.5); \
float elipse_radius = 0.5; \
/* from https://bugzilla.gnome.org/show_bug.cgi?id=669798: \
the alpha on the gradient goes from 165 at its darkest to 98 at its most transparent. */ \
float y = 165.0 / 255.0; \
float x = 98.0 / 255.0; \
/* interpolate darkening value, based on distance from screen center */ \
float val = min(length(dist), elipse_radius); \
float a = mix(x, y, val / elipse_radius); \
/* dim_factor varies from [1.0 -> 0.5] when overview is showing \
We use it to smooth value, then we clamp it to valid color interval */ \
a = clamp(a - cogl_color_in.r + 0.5, 0.0, 1.0); \
/* We\'re blending between: color and black color (obviously omitted in the equation) */ \
cogl_color_out.xyz = cogl_color_out.xyz * (1.0 - a); \
cogl_color_out.a = 1.0;';
const ShellInfo = new Lang.Class({
Name: 'ShellInfo',
_init: function() {
this._source = null;
this._undoCallback = null;
},
_onUndoClicked: function() {
if (this._undoCallback)
this._undoCallback();
this._undoCallback = null;
if (this._source)
this._source.destroy();
},
setMessage: function(text, options) {
options = Params.parse(options, { undoCallback: null,
forFeedback: false
});
let undoCallback = options.undoCallback;
let forFeedback = options.forFeedback;
if (this._source == null) {
this._source = new MessageTray.SystemNotificationSource();
this._source.connect('destroy', Lang.bind(this,
function() {
this._source = null;
}));
Main.messageTray.add(this._source);
}
let notification = null;
if (this._source.notifications.length == 0) {
notification = new MessageTray.Notification(this._source, text, null);
notification.setTransient(true);
notification.setForFeedback(forFeedback);
} else {
notification = this._source.notifications[0];
notification.update(text, null, { clear: true });
}
this._undoCallback = undoCallback;
if (undoCallback) {
notification.addButton('system-undo', _("Undo"));
notification.connect('action-invoked', Lang.bind(this, this._onUndoClicked));
}
this._source.notify(notification);
}
});
const Overview = new Lang.Class({
Name: 'Overview',
_init: function() {
this._overviewCreated = false;
this._initCalled = false;
Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
this._sessionUpdated();
},
_createOverview: function() {
if (this._overviewCreated)
return;
if (this.isDummy)
return;
this._overviewCreated = true;
// The main BackgroundActor is inside global.window_group which is
// hidden when displaying the overview, so we create a new
// one. Instances of this class share a single CoglTexture behind the
// scenes which allows us to show the background with different
// rendering options without duplicating the texture data.
this._background = Meta.BackgroundActor.new_for_screen(global.screen);
this._background.add_glsl_snippet(Meta.SnippetHook.FRAGMENT,
GLSL_DIM_EFFECT_DECLARATIONS,
GLSL_DIM_EFFECT_CODE,
false);
this._background.hide();
global.overlay_group.add_actor(this._background);
this._desktopFade = new St.Bin();
global.overlay_group.add_actor(this._desktopFade);
/* Translators: This is the main view to select
activities. See also note for "Activities" string. */
this._overview = new St.BoxLayout({ name: 'overview',
accessible_name: _("Overview"),
reactive: true,
vertical: true });
this._overview._delegate = this;
let layout = new CenterLayout.CenterLayout();
this._group = new St.Widget({ name: 'overview-group',
layout_manager: layout });
this._spacing = 0;
this._group.connect('style-changed',
Lang.bind(this, function() {
let node = this._group.get_theme_node();
let spacing = node.get_length('spacing');
if (spacing != this._spacing) {
this._spacing = spacing;
}
}));
this._capturedEventId = 0;
this._buttonPressId = 0;
this.visible = false; // animating to overview, in overview, animating out
this._shown = false; // show() and not hide()
this._shownTemporarily = false; // showTemporarily() and not hideTemporarily()
this._modal = false; // have a modal grab
this.animationInProgress = false;
this._hideInProgress = false;
// 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._overview.add_actor(this._coverPane);
this._coverPane.connect('event', Lang.bind(this, function (actor, event) { return true; }));
this._overview.hide();
global.overlay_group.add_actor(this._overview);
this._coverPane.hide();
// XDND
this._dragMonitor = {
dragMotion: Lang.bind(this, this._onDragMotion)
};
Main.xdndHandler.connect('drag-begin', Lang.bind(this, this._onDragBegin));
Main.xdndHandler.connect('drag-end', Lang.bind(this, this._onDragEnd));
global.screen.connect('restacked', Lang.bind(this, this._onRestacked));
this._windowSwitchTimeoutId = 0;
this._windowSwitchTimestamp = 0;
this._lastActiveWorkspaceIndex = -1;
this._lastHoveredWindow = null;
this._needsFakePointerEvent = false;
if (this._initCalled)
this.init();
},
_sessionUpdated: function() {
this.isDummy = !Main.sessionMode.hasOverview;
this._createOverview();
},
// The members we construct that are implemented in JS might
// want to access the overview as Main.overview to connect
// signal handlers and so forth. So we create them after
// construction in this init() method.
init: function() {
this._initCalled = true;
if (this.isDummy)
return;
this._shellInfo = new ShellInfo();
// Add a clone of the panel to the overview so spacing and such is
// automatic
this._panelGhost = new St.Bin({ child: new Clutter.Clone({ source: Main.panel.actor }),
reactive: false,
opacity: 0 });
this._overview.add_actor(this._panelGhost);
this._searchEntry = new St.Entry({ name: 'searchEntry',
/* Translators: this is the text displayed
in the search entry when no search is
active; it should not exceed ~30
characters. */
hint_text: _("Type to search..."),
track_hover: true,
can_focus: true });
this._searchEntryBin = new St.Bin({ child: this._searchEntry,
x_align: St.Align.MIDDLE });
this._overview.add_actor(this._searchEntryBin);
// TODO - recalculate everything when desktop size changes
this._dash = new Dash.Dash();
this._group.add_actor(this._dash.actor);
this.dashIconSize = this._dash.iconSize;
this._dash.connect('icon-size-changed',
Lang.bind(this, function() {
this.dashIconSize = this._dash.iconSize;
}));
// Translators: this is the name of the dock/favorites area on
// the left of the overview
Main.ctrlAltTabManager.addGroup(this._dash.actor, _("Dash"), 'user-bookmarks-symbolic');
this._viewSelector = new ViewSelector.ViewSelector(this._searchEntry,
this._dash.showAppsButton);
this._group.add_actor(this._viewSelector.actor);
this._thumbnailsBox = new WorkspaceThumbnail.ThumbnailsBox();
this._group.add_actor(this._thumbnailsBox.actor);
Main.ctrlAltTabManager.addGroup(this._thumbnailsBox.actor, _("Workspaces"), 'view-list-symbolic');
// Add our same-line elements after the search entry
this._overview.add_actor(this._group);
// Then account for message tray
this._messageTrayGhost = new St.Bin({ child: new Clutter.Clone({ source: Main.messageTray.actor }),
reactive: false,
opacity: 0,
x_fill: true,
y_fill: true });
this._overview.add_actor(this._messageTrayGhost);
this._viewSelector.connect('page-changed', Lang.bind(this,
function() {
this._setSideControlsVisibility(false);
}));
this.connect('item-drag-begin', Lang.bind(this,
function() {
this._setSideControlsVisibility(true);
}));
this.connect('item-drag-cancelled', Lang.bind(this,
function() {
this._setSideControlsVisibility(false);
}));
this.connect('item-drag-end', Lang.bind(this,
function() {
this._setSideControlsVisibility(false);
}));
Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._relayout));
this._relayout();
},
_setSideControlsVisibility: function(inDrag) {
// Ignore the case when we're leaving the overview, since
// actors will be made visible again when entering the overview
// next time, and animating them while doing so is just
// unnecesary noise
if (!this.visible || this.animationInProgress)
return;
let appsActive = this._viewSelector.getAppsActive();
let searchActive = this._viewSelector.getSearchActive();
let dashVisible = !searchActive || inDrag;
let thumbnailsVisible = (!searchActive && !appsActive) || inDrag;
let trayVisible = !searchActive;
if (dashVisible)
this._dash.show();
else
this._dash.hide();
if (thumbnailsVisible)
this._thumbnailsBox.show();
else
this._thumbnailsBox.hide();
if (trayVisible)
Main.messageTray.show();
else
Main.messageTray.hide();
},
addSearchProvider: function(provider) {
this._viewSelector.addSearchProvider(provider);
},
removeSearchProvider: function(provider) {
this._viewSelector.removeSearchProvider(provider);
},
//
// options:
// - undoCallback (function): the callback to be called if undo support is needed
// - forFeedback (boolean): whether the message is for direct feedback of a user action
//
setMessage: function(text, options) {
if (this.isDummy)
return;
this._shellInfo.setMessage(text, options);
},
_onDragBegin: function() {
DND.addDragMonitor(this._dragMonitor);
// Remember the workspace we started from
this._lastActiveWorkspaceIndex = global.screen.get_active_workspace_index();
},
_onDragEnd: function(time) {
// In case the drag was canceled while in the overview
// we have to go back to where we started and hide
// the overview
if (this._shownTemporarily) {
global.screen.get_workspace_by_index(this._lastActiveWorkspaceIndex).activate(time);
this.hideTemporarily();
}
this._resetWindowSwitchTimeout();
this._lastHoveredWindow = null;
DND.removeDragMonitor(this._dragMonitor);
this.endItemDrag();
},
_resetWindowSwitchTimeout: function() {
if (this._windowSwitchTimeoutId != 0) {
Mainloop.source_remove(this._windowSwitchTimeoutId);
this._windowSwitchTimeoutId = 0;
this._needsFakePointerEvent = false;
}
},
_fakePointerEvent: function() {
let display = Gdk.Display.get_default();
let deviceManager = display.get_device_manager();
let pointer = deviceManager.get_client_pointer();
let [screen, pointerX, pointerY] = pointer.get_position();
pointer.warp(screen, pointerX, pointerY);
},
_onDragMotion: function(dragEvent) {
let targetIsWindow = dragEvent.targetActor &&
dragEvent.targetActor._delegate &&
dragEvent.targetActor._delegate.metaWindow &&
!(dragEvent.targetActor._delegate instanceof WorkspaceThumbnail.WindowClone);
this._windowSwitchTimestamp = global.get_current_time();
if (targetIsWindow &&
dragEvent.targetActor._delegate.metaWindow == this._lastHoveredWindow)
return DND.DragMotionResult.CONTINUE;
this._lastHoveredWindow = null;
this._resetWindowSwitchTimeout();
if (targetIsWindow) {
this._lastHoveredWindow = dragEvent.targetActor._delegate.metaWindow;
this._windowSwitchTimeoutId = Mainloop.timeout_add(DND_WINDOW_SWITCH_TIMEOUT,
Lang.bind(this, function() {
this._needsFakePointerEvent = true;
Main.activateWindow(dragEvent.targetActor._delegate.metaWindow,
this._windowSwitchTimestamp);
this.hideTemporarily();
this._lastHoveredWindow = null;
}));
}
return DND.DragMotionResult.CONTINUE;
},
addAction: function(action) {
if (this.isDummy)
return;
this._overview.add_action(action);
},
_getDesktopClone: function() {
let windows = global.get_window_actors().filter(function(w) {
return w.meta_window.get_window_type() == Meta.WindowType.DESKTOP;
});
if (windows.length == 0)
return null;
let window = windows[0];
let clone = new Clutter.Clone({ source: window.get_texture(),
x: window.x, y: window.y });
clone.source.connect('destroy', Lang.bind(this, function() {
clone.destroy();
}));
return clone;
},
_relayout: function () {
// To avoid updating the position and size of the workspaces
// we just hide the overview. The positions will be updated
// when it is next shown.
this.hide();
let primary = Main.layoutManager.primaryMonitor;
let contentY = Main.panel.actor.height;
let contentHeight = primary.height - contentY - Main.messageTray.actor.height;
this._overview.set_position(primary.x, primary.y);
this._overview.set_size(primary.width, primary.height);
this._coverPane.set_position(0, contentY);
this._coverPane.set_size(primary.width, contentHeight);
},
_onRestacked: function() {
let stack = global.get_window_actors();
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;
}
this.emit('sync-window-stacking', stackIndices);
},
//// Public methods ////
beginItemDrag: function(source) {
this.emit('item-drag-begin');
},
cancelledItemDrag: function(source) {
this.emit('item-drag-cancelled');
},
endItemDrag: function(source) {
this.emit('item-drag-end');
},
beginWindowDrag: function(source) {
this.emit('window-drag-begin');
},
cancelledWindowDrag: function(source) {
this.emit('window-drag-cancelled');
},
endWindowDrag: function(source) {
this.emit('window-drag-end');
},
// show:
//
// Animates the overview visible and grabs mouse and keyboard input
show : function() {
if (this.isDummy)
return;
if (this._shown)
return;
this._shown = true;
this._syncInputMode();
if (!this._modal)
return;
this._animateVisible();
},
fadeInDesktop: function() {
this._desktopFade.opacity = 0;
this._desktopFade.show();
Tweener.addTween(this._desktopFade,
{ opacity: 255,
time: ANIMATION_TIME,
transition: 'easeOutQuad' });
},
fadeOutDesktop: function() {
if (!this._desktopFade.child)
this._desktopFade.child = this._getDesktopClone();
this._desktopFade.opacity = 255;
this._desktopFade.show();
Tweener.addTween(this._desktopFade,
{ opacity: 0,
time: ANIMATION_TIME,
transition: 'easeOutQuad'
});
},
_animateVisible: function() {
if (this.visible || this.animationInProgress)
return;
this.visible = true;
this.animationInProgress = true;
// 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.
//
// Disable unredirection while in the overview
Meta.disable_unredirect_for_screen(global.screen);
global.window_group.hide();
this._overview.show();
this._background.show();
this._viewSelector.show();
this._overview.opacity = 0;
Tweener.addTween(this._overview,
{ opacity: 255,
transition: 'easeOutQuad',
time: ANIMATION_TIME,
onComplete: this._showDone,
onCompleteScope: this
});
Tweener.addTween(this._background,
{ dim_factor: 0.8,
time: ANIMATION_TIME,
transition: 'easeOutQuad'
});
this._coverPane.raise_top();
this._coverPane.show();
this.emit('showing');
},
// showTemporarily:
//
// Animates the overview visible without grabbing mouse and keyboard input;
// if show() has already been called, this has no immediate effect, but
// will result in the overview not being hidden until hideTemporarily() is
// called.
showTemporarily: function() {
if (this.isDummy)
return;
if (this._shownTemporarily)
return;
this._syncInputMode();
this._animateVisible();
this._shownTemporarily = true;
},
// hide:
//
// Reverses the effect of show()
hide: function() {
if (this.isDummy)
return;
if (!this._shown)
return;
if (!this._shownTemporarily)
this._animateNotVisible();
this._shown = false;
this._syncInputMode();
},
// hideTemporarily:
//
// Reverses the effect of showTemporarily()
hideTemporarily: function() {
if (this.isDummy)
return;
if (!this._shownTemporarily)
return;
if (!this._shown)
this._animateNotVisible();
this._shownTemporarily = false;
this._syncInputMode();
},
toggle: function() {
if (this.isDummy)
return;
if (this.visible)
this.hide();
else
this.show();
},
//// Private methods ////
_syncInputMode: function() {
// We delay input mode changes during animation so that when removing the
// overview we don't have a problem with the release of a press/release
// going to an application.
if (this.animationInProgress)
return;
if (this._shown) {
if (!this._modal) {
if (Main.pushModal(this._overview,
{ keybindingMode: Main.KeybindingMode.OVERVIEW }))
this._modal = true;
else
this.hide();
}
} else if (this._shownTemporarily) {
if (this._modal) {
Main.popModal(this._overview);
this._modal = false;
}
global.stage_input_mode = Shell.StageInputMode.FULLSCREEN;
} else {
if (this._modal) {
Main.popModal(this._overview);
this._modal = false;
}
else if (global.stage_input_mode == Shell.StageInputMode.FULLSCREEN)
global.stage_input_mode = Shell.StageInputMode.NORMAL;
}
},
_animateNotVisible: function() {
if (!this.visible || this.animationInProgress)
return;
this.animationInProgress = true;
this._hideInProgress = true;
this._viewSelector.zoomFromOverview();
// Make other elements fade out.
Tweener.addTween(this._overview,
{ opacity: 0,
transition: 'easeOutQuad',
time: ANIMATION_TIME,
onComplete: this._hideDone,
onCompleteScope: this
});
Tweener.addTween(this._background,
{ dim_factor: 1.0,
time: ANIMATION_TIME,
transition: 'easeOutQuad'
});
this._coverPane.raise_top();
this._coverPane.show();
this.emit('hiding');
},
_showDone: function() {
this.animationInProgress = false;
this._desktopFade.hide();
this._coverPane.hide();
this.emit('shown');
// Handle any calls to hide* while we were showing
if (!this._shown && !this._shownTemporarily)
this._animateNotVisible();
this._syncInputMode();
global.sync_pointer();
},
_hideDone: function() {
// Re-enable unredirection
Meta.enable_unredirect_for_screen(global.screen);
global.window_group.show();
this._viewSelector.hide();
this._desktopFade.hide();
this._background.hide();
this._overview.hide();
this.visible = false;
this.animationInProgress = false;
this._hideInProgress = false;
this._coverPane.hide();
this.emit('hidden');
// Handle any calls to show* while we were hiding
if (this._shown || this._shownTemporarily)
this._animateVisible();
this._syncInputMode();
// Fake a pointer event if requested
if (this._needsFakePointerEvent) {
this._fakePointerEvent();
this._needsFakePointerEvent = false;
}
}
});
Signals.addSignalMethods(Overview.prototype);