gnome-shell/js/ui/overviewControls.js

523 lines
16 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ControlsManager */
const { Clutter, GObject, Meta, St } = imports.gi;
const Dash = imports.ui.dash;
const Main = imports.ui.main;
const Params = imports.misc.params;
const ViewSelector = imports.ui.viewSelector;
const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
var SIDE_CONTROLS_ANIMATION_TIME = 160;
function getRtlSlideDirection(direction, actor) {
let rtl = (actor.text_direction == Clutter.TextDirection.RTL);
if (rtl)
direction = (direction == SlideDirection.LEFT)
? SlideDirection.RIGHT : SlideDirection.LEFT;
return direction;
}
var SlideDirection = {
LEFT: 0,
RIGHT: 1
};
var SlideLayout = GObject.registerClass({
Properties: {
'slide-x': GObject.ParamSpec.double(
'slide-x', 'slide-x', 'slide-x',
GObject.ParamFlags.READWRITE,
0, 1, 1),
'translation-x': GObject.ParamSpec.double(
'translation-x', 'translation-x', 'translation-x',
GObject.ParamFlags.READWRITE,
-Infinity, Infinity, 0)
}
}, class SlideLayout extends Clutter.FixedLayout {
_init(params) {
this._slideX = 1;
this._translationX = 0;
this._direction = SlideDirection.LEFT;
super._init(params);
}
vfunc_get_preferred_width(container, forHeight) {
let child = container.get_first_child();
let [minWidth, natWidth] = child.get_preferred_width(forHeight);
minWidth *= this._slideX;
natWidth *= this._slideX;
return [minWidth, natWidth];
}
vfunc_allocate(container, box, flags) {
let child = container.get_first_child();
let availWidth = Math.round(box.x2 - box.x1);
let availHeight = Math.round(box.y2 - box.y1);
let [, natWidth] = child.get_preferred_width(availHeight);
// Align the actor inside the clipped box, as the actor's alignment
// flags only determine what to do if the allocated box is bigger
// than the actor's box.
let realDirection = getRtlSlideDirection(this._direction, child);
let alignX = (realDirection == SlideDirection.LEFT)
? availWidth - natWidth
: availWidth - natWidth * this._slideX;
let actorBox = new Clutter.ActorBox();
actorBox.x1 = box.x1 + alignX + this._translationX;
actorBox.x2 = actorBox.x1 + (child.x_expand ? availWidth : natWidth);
actorBox.y1 = box.y1;
actorBox.y2 = actorBox.y1 + availHeight;
child.allocate(actorBox, flags);
}
// eslint-disable-next-line camelcase
set slide_x(value) {
if (this._slideX == value)
return;
this._slideX = value;
this.notify('slide-x');
this.layout_changed();
}
// eslint-disable-next-line camelcase
get slide_x() {
return this._slideX;
}
set slideDirection(direction) {
this._direction = direction;
this.layout_changed();
}
get slideDirection() {
return this._direction;
}
// eslint-disable-next-line camelcase
set translation_x(value) {
if (this._translationX == value)
return;
this._translationX = value;
this.notify('translation-x');
this.layout_changed();
}
// eslint-disable-next-line camelcase
get translation_x() {
return this._translationX;
}
});
var SlidingControl = GObject.registerClass(
class SlidingControl extends St.Widget {
_init(params) {
params = Params.parse(params, { slideDirection: SlideDirection.LEFT });
this.layout = new SlideLayout();
this.layout.slideDirection = params.slideDirection;
super._init({
layout_manager: this.layout,
style_class: 'overview-controls',
clip_to_allocation: true
});
this._visible = true;
this._inDrag = false;
Main.overview.connect('hiding', this._onOverviewHiding.bind(this));
Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this));
Main.overview.connect('item-drag-end', this._onDragEnd.bind(this));
Main.overview.connect('item-drag-cancelled', this._onDragEnd.bind(this));
Main.overview.connect('window-drag-begin', this._onWindowDragBegin.bind(this));
Main.overview.connect('window-drag-cancelled', this._onWindowDragEnd.bind(this));
Main.overview.connect('window-drag-end', this._onWindowDragEnd.bind(this));
}
_getSlide() {
throw new GObject.NotImplementedError(`_getSlide in ${this.constructor.name}`);
}
_updateSlide() {
this.ease_property('@layout.slide-x', this._getSlide(), {
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
duration: SIDE_CONTROLS_ANIMATION_TIME,
});
}
getVisibleWidth() {
let child = this.get_first_child();
let [, , natWidth] = child.get_preferred_size();
return natWidth;
}
_getTranslation() {
let child = this.get_first_child();
let direction = getRtlSlideDirection(this.layout.slideDirection, child);
let visibleWidth = this.getVisibleWidth();
if (direction == SlideDirection.LEFT)
return - visibleWidth;
else
return visibleWidth;
}
_updateTranslation() {
let translationStart = 0;
let translationEnd = 0;
let translation = this._getTranslation();
let shouldShow = (this._getSlide() > 0);
if (shouldShow) {
translationStart = translation;
} else {
translationEnd = translation;
}
if (this.layout.translation_x == translationEnd)
return;
this.layout.translation_x = translationStart;
this.ease_property('@layout.translation-x', translationEnd, {
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
duration: SIDE_CONTROLS_ANIMATION_TIME,
});
}
_onOverviewHiding() {
2014-08-30 21:30:48 -04:00
// We need to explicitly slideOut since showing pages
// doesn't imply sliding out, instead, hiding the overview does.
this.slideOut();
}
_onWindowDragBegin() {
this._onDragBegin();
}
_onWindowDragEnd() {
this._onDragEnd();
}
_onDragBegin() {
this._inDrag = true;
this._updateTranslation();
this._updateSlide();
}
_onDragEnd() {
this._inDrag = false;
this._updateSlide();
}
fadeIn() {
this.ease({
opacity: 255,
duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
mode: Clutter.AnimationMode.EASE_IN_QUAD
});
}
fadeHalf() {
this.ease({
opacity: 128,
duration: SIDE_CONTROLS_ANIMATION_TIME / 2,
mode: Clutter.AnimationMode.EASE_OUT_QUAD
});
}
slideIn() {
this._visible = true;
// we will update slide_x and the translation from pageEmpty
}
slideOut() {
this._visible = false;
this._updateTranslation();
// we will update slide_x from pageEmpty
}
pageEmpty() {
// When pageEmpty is received, there's no visible view in the
// selector; this means we can now safely set the full slide for
// the next page, since slideIn or slideOut might have been called,
// changing the visiblity
this.remove_transition('@layout.slide-x');
this.layout.slide_x = this._getSlide();
this._updateTranslation();
}
});
var ThumbnailsSlider = GObject.registerClass(
class ThumbnailsSlider extends SlidingControl {
_init(thumbnailsBox) {
super._init({ slideDirection: SlideDirection.RIGHT });
this._thumbnailsBox = thumbnailsBox;
this.request_mode = Clutter.RequestMode.WIDTH_FOR_HEIGHT;
this.reactive = true;
this.track_hover = true;
this.add_actor(this._thumbnailsBox);
Main.layoutManager.connect('monitors-changed', this._updateSlide.bind(this));
global.workspace_manager.connect('active-workspace-changed',
this._updateSlide.bind(this));
global.workspace_manager.connect('notify::n-workspaces',
this._updateSlide.bind(this));
this.connect('notify::hover', this._updateSlide.bind(this));
this._thumbnailsBox.bind_property('visible', this, 'visible', GObject.BindingFlags.SYNC_CREATE);
}
_getAlwaysZoomOut() {
// Always show the pager on hover, during a drag, or if workspaces are
// actually used, e.g. there are windows on any non-active workspace
let workspaceManager = global.workspace_manager;
let alwaysZoomOut = this.hover ||
this._inDrag ||
!Meta.prefs_get_dynamic_workspaces() ||
workspaceManager.n_workspaces > 2 ||
workspaceManager.get_active_workspace_index() != 0;
if (!alwaysZoomOut) {
let monitors = Main.layoutManager.monitors;
let primary = Main.layoutManager.primaryMonitor;
/* Look for any monitor to the right of the primary, if there is
* one, we always keep zoom out, otherwise its hard to reach
* the thumbnail area without passing into the next monitor. */
for (let i = 0; i < monitors.length; i++) {
if (monitors[i].x >= primary.x + primary.width) {
alwaysZoomOut = true;
break;
}
}
}
return alwaysZoomOut;
}
getNonExpandedWidth() {
let child = this.get_first_child();
return child.get_theme_node().get_length('visible-width');
}
_onDragEnd() {
this.sync_hover();
super._onDragEnd();
}
_getSlide() {
if (!this._visible)
return 0;
let alwaysZoomOut = this._getAlwaysZoomOut();
if (alwaysZoomOut)
return 1;
let child = this.get_first_child();
let preferredHeight = child.get_preferred_height(-1)[1];
let expandedWidth = child.get_preferred_width(preferredHeight)[1];
return this.getNonExpandedWidth() / expandedWidth;
}
getVisibleWidth() {
let alwaysZoomOut = this._getAlwaysZoomOut();
if (alwaysZoomOut)
return super.getVisibleWidth();
else
return this.getNonExpandedWidth();
}
});
var DashSlider = GObject.registerClass(
class DashSlider extends SlidingControl {
_init(dash) {
super._init({ slideDirection: SlideDirection.LEFT });
this._dash = dash;
// SlideLayout reads the actor's expand flags to decide
// whether to allocate the natural size to its child, or the whole
// available allocation
this._dash.x_expand = true;
this.x_expand = true;
this.x_align = Clutter.ActorAlign.START;
this.y_expand = true;
this.add_actor(this._dash);
this._dash.connect('icon-size-changed', this._updateSlide.bind(this));
}
_getSlide() {
if (this._visible || this._inDrag)
return 1;
else
return 0;
}
_onWindowDragBegin() {
this.fadeHalf();
}
_onWindowDragEnd() {
this.fadeIn();
}
});
var DashSpacer = GObject.registerClass(
class DashSpacer extends St.Widget {
_init(params) {
super._init(params);
this._bindConstraint = null;
}
setDashActor(dashActor) {
if (this._bindConstraint) {
this.remove_constraint(this._bindConstraint);
this._bindConstraint = null;
}
if (dashActor) {
this._bindConstraint = new Clutter.BindConstraint({ source: dashActor,
coordinate: Clutter.BindCoordinate.SIZE });
this.add_constraint(this._bindConstraint);
}
}
vfunc_get_preferred_width(forHeight) {
if (this._bindConstraint)
return this._bindConstraint.source.get_preferred_width(forHeight);
return super.vfunc_get_preferred_width(forHeight);
}
vfunc_get_preferred_height(forWidth) {
if (this._bindConstraint)
return this._bindConstraint.source.get_preferred_height(forWidth);
return super.vfunc_get_preferred_height(forWidth);
}
});
var ControlsLayout = GObject.registerClass({
Signals: { 'allocation-changed': { flags: GObject.SignalFlags.RUN_LAST } },
}, class ControlsLayout extends Clutter.BinLayout {
vfunc_allocate(container, box, flags) {
super.vfunc_allocate(container, box, flags);
this.emit('allocation-changed');
}
});
var ControlsManager = GObject.registerClass(
class ControlsManager extends St.Widget {
_init(searchEntry) {
let layout = new ControlsLayout();
super._init({
layout_manager: layout,
x_expand: true,
y_expand: true,
clip_to_allocation: true
});
this.dash = new Dash.Dash();
this._dashSlider = new DashSlider(this.dash);
this._dashSpacer = new DashSpacer();
this._dashSpacer.setDashActor(this._dashSlider);
this._thumbnailsBox = new WorkspaceThumbnail.ThumbnailsBox();
this._thumbnailsSlider = new ThumbnailsSlider(this._thumbnailsBox);
this.viewSelector = new ViewSelector.ViewSelector(searchEntry,
this.dash.showAppsButton);
this.viewSelector.connect('page-changed', this._setVisibility.bind(this));
this.viewSelector.connect('page-empty', this._onPageEmpty.bind(this));
this._group = new St.BoxLayout({ name: 'overview-group',
x_expand: true, y_expand: true });
this.add_actor(this._group);
this.add_actor(this._dashSlider);
this._group.add_actor(this._dashSpacer);
this._group.add(this.viewSelector, { x_fill: true, expand: true });
this._group.add_actor(this._thumbnailsSlider);
layout.connect('allocation-changed', this._updateWorkspacesGeometry.bind(this));
Main.overview.connect('showing', this._updateSpacerVisibility.bind(this));
}
_updateWorkspacesGeometry() {
let [x, y] = this.get_transformed_position();
let [width, height] = this.get_transformed_size();
let geometry = { x: x, y: y, width: width, height: height };
let spacing = this.get_theme_node().get_length('spacing');
let dashWidth = this._dashSlider.getVisibleWidth() + spacing;
let thumbnailsWidth = this._thumbnailsSlider.getNonExpandedWidth() + spacing;
geometry.width -= dashWidth;
geometry.width -= thumbnailsWidth;
if (this.get_text_direction() == Clutter.TextDirection.LTR)
geometry.x += dashWidth;
else
geometry.x += thumbnailsWidth;
this.viewSelector.setWorkspacesFullGeometry(geometry);
}
_setVisibility() {
// 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
// unnecessary noise
if (!Main.overview.visible ||
(Main.overview.animationInProgress && !Main.overview.visibleTarget))
return;
let activePage = this.viewSelector.getActivePage();
let dashVisible = (activePage == ViewSelector.ViewPage.WINDOWS ||
activePage == ViewSelector.ViewPage.APPS);
let thumbnailsVisible = (activePage == ViewSelector.ViewPage.WINDOWS);
if (dashVisible)
this._dashSlider.slideIn();
else
this._dashSlider.slideOut();
if (thumbnailsVisible)
this._thumbnailsSlider.slideIn();
else
this._thumbnailsSlider.slideOut();
}
_updateSpacerVisibility() {
if (Main.overview.animationInProgress && !Main.overview.visibleTarget)
return;
let activePage = this.viewSelector.getActivePage();
this._dashSpacer.visible = (activePage == ViewSelector.ViewPage.WINDOWS);
}
_onPageEmpty() {
this._dashSlider.pageEmpty();
this._thumbnailsSlider.pageEmpty();
this._updateSpacerVisibility();
}
});