// -*- 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() { // 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(); } });