// -*- 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); let workspaceManager = global.workspace_manager; let activeWorkspaceIndex = workspaceManager.get_active_workspace_index(); this._workspaceAdjustment = new St.Adjustment({ 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); layout.connect('allocation-changed', this._updateWorkspacesGeometry.bind(this)); 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.value = activeIndex; } _updateWorkspacesGeometry() { let [x, y] = this.get_transformed_position(); let [width, height] = this.get_transformed_size(); let geometry = { x, y, width, 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(); } });