summaryrefslogtreecommitdiffstats
path: root/js/ui/overviewControls.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/overviewControls.js')
-rw-r--r--js/ui/overviewControls.js519
1 files changed, 519 insertions, 0 deletions
diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js
new file mode 100644
index 0000000..b5e89bb
--- /dev/null
+++ b/js/ui/overviewControls.js
@@ -0,0 +1,519 @@
+// -*- 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;
+const Overview = imports.ui.overview;
+
+var SIDE_CONTROLS_ANIMATION_TIME = Overview.ANIMATION_TIME;
+
+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),
+ },
+}, class SlideLayout extends Clutter.FixedLayout {
+ _init(params) {
+ this._slideX = 1;
+ 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) {
+ 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;
+ actorBox.x2 = actorBox.x1 + (child.x_expand ? availWidth : natWidth);
+ actorBox.y1 = box.y1;
+ actorBox.y2 = actorBox.y1 + availHeight;
+
+ child.allocate(actorBox);
+ }
+
+ // 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;
+ }
+});
+
+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 %s'.format(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.translation_x === translationEnd)
+ return;
+
+ this.translation_x = translationStart;
+ this.ease({
+ translation_x: translationEnd,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ duration: SIDE_CONTROLS_ANIMATION_TIME,
+ });
+ }
+
+ _onOverviewHiding() {
+ // 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 visibility
+ 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': {} },
+}, class ControlsLayout extends Clutter.BinLayout {
+ vfunc_allocate(container, box) {
+ super.vfunc_allocate(container, box);
+ 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);
+
+ let workspaceManager = global.workspace_manager;
+ let activeWorkspaceIndex = workspaceManager.get_active_workspace_index();
+
+ this._workspaceAdjustment = new St.Adjustment({
+ actor: this,
+ value: activeWorkspaceIndex,
+ lower: 0,
+ page_increment: 1,
+ page_size: 1,
+ step_increment: 0,
+ upper: workspaceManager.n_workspaces,
+ });
+
+ this._nWorkspacesNotifyId =
+ workspaceManager.connect('notify::n-workspaces',
+ this._updateAdjustment.bind(this));
+
+ this._thumbnailsBox =
+ new WorkspaceThumbnail.ThumbnailsBox(this._workspaceAdjustment);
+ this._thumbnailsSlider = new ThumbnailsSlider(this._thumbnailsBox);
+
+ this.viewSelector = new ViewSelector.ViewSelector(searchEntry,
+ this._workspaceAdjustment, 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_child(this.viewSelector);
+ this._group.add_actor(this._thumbnailsSlider);
+
+ Main.overview.connect('showing', this._updateSpacerVisibility.bind(this));
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ global.workspace_manager.disconnect(this._nWorkspacesNotifyId);
+ }
+
+ _updateAdjustment() {
+ let workspaceManager = global.workspace_manager;
+ let newNumWorkspaces = workspaceManager.n_workspaces;
+ let activeIndex = workspaceManager.get_active_workspace_index();
+
+ this._workspaceAdjustment.upper = newNumWorkspaces;
+
+ // A workspace might have been inserted or removed before the active
+ // one, causing the adjustment to go out of sync, so update the value
+ this._workspaceAdjustment.remove_transition('value');
+ this._workspaceAdjustment.value = activeIndex;
+ }
+
+ _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();
+ }
+});