9497ddd68a
The workspace switcher blocks state updates while the indicator is animating. Since commit9c1940ef9d
the indicator is considered to be animating when the workspace adjustment's value doesn't equal the active workspace. There is one case though where this breaks badly: When a workspace is inserted before the active one, the adjustment's upper and value properties are changed without transitions. But if that change happens while there's an ongoing transition to the previously active workspace, the value gets out of sync with the active workspace and we end up blocking state updates indefinitely. Fix this by removing any transitions before setting the adjustment value. https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2625 (cherry picked from commit2e80995f19
)
540 lines
17 KiB
JavaScript
540 lines
17 KiB
JavaScript
// -*- 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),
|
|
},
|
|
}, 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, 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;
|
|
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;
|
|
}
|
|
});
|
|
|
|
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 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.remove_transition('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();
|
|
}
|
|
});
|