c09c070b15
Move AppDisplay, WorkspacesDisplay, and ThumbnailsBox from ViewSelector to ControlsManager. This allows to always allocate the correct size for AppDisplay, and will enable for a plethora of further improvements. The end goal is to completely remove ViewSelector, and let ControlsManager handle the layout of everything that's visible in the overview. For now, replace the apps page with a dummy actor in ViewSelector. Adjust various callers around the codebase to not access the ViewSelector directly from the overview anymore. Bind the opacity of the primary workspace to WorkspaceDisplay's opacity. This allows removing the parent opacity hack in place, which will be done by the next commit. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1624>
539 lines
18 KiB
JavaScript
539 lines
18 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported ControlsManager */
|
|
|
|
const { Clutter, Gio, GObject, Meta, Shell, St } = imports.gi;
|
|
|
|
const AppDisplay = imports.ui.appDisplay;
|
|
const Dash = imports.ui.dash;
|
|
const Main = imports.ui.main;
|
|
const Overview = imports.ui.overview;
|
|
const Util = imports.misc.util;
|
|
const ViewSelector = imports.ui.viewSelector;
|
|
const WindowManager = imports.ui.windowManager;
|
|
const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
|
|
const WorkspacesView = imports.ui.workspacesView;
|
|
|
|
const SMALL_WORKSPACE_RATIO = 0.15;
|
|
|
|
var SIDE_CONTROLS_ANIMATION_TIME = Overview.ANIMATION_TIME;
|
|
|
|
var ControlsState = {
|
|
HIDDEN: 0,
|
|
WINDOW_PICKER: 1,
|
|
APP_GRID: 2,
|
|
};
|
|
|
|
var ControlsManagerLayout = GObject.registerClass(
|
|
class ControlsManagerLayout extends Clutter.BoxLayout {
|
|
_init(searchEntry, appDisplay, workspacesDisplay, workspacesThumbnails,
|
|
viewSelector, dash, stateAdjustment) {
|
|
super._init({ orientation: Clutter.Orientation.VERTICAL });
|
|
|
|
this._appDisplay = appDisplay;
|
|
this._workspacesDisplay = workspacesDisplay;
|
|
this._workspacesThumbnails = workspacesThumbnails;
|
|
this._stateAdjustment = stateAdjustment;
|
|
this._searchEntry = searchEntry;
|
|
this._viewSelector = viewSelector;
|
|
this._dash = dash;
|
|
|
|
stateAdjustment.connect('notify::value', () => this.layout_changed());
|
|
}
|
|
|
|
_getWorkspacesBoxForState(state, box, searchHeight, dashHeight, thumbnailsHeight) {
|
|
const workspaceBox = box.copy();
|
|
const [width, height] = workspaceBox.get_size();
|
|
const { spacing } = this;
|
|
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
break;
|
|
case ControlsState.WINDOW_PICKER:
|
|
workspaceBox.set_origin(0,
|
|
searchHeight + spacing +
|
|
(thumbnailsHeight > 0 ? thumbnailsHeight + spacing : 0));
|
|
workspaceBox.set_size(width,
|
|
height -
|
|
dashHeight - spacing -
|
|
searchHeight - spacing -
|
|
(thumbnailsHeight > 0 ? thumbnailsHeight + spacing : 0));
|
|
break;
|
|
case ControlsState.APP_GRID:
|
|
workspaceBox.set_origin(0, searchHeight + spacing);
|
|
workspaceBox.set_size(
|
|
width,
|
|
Math.round(Math.max(height * SMALL_WORKSPACE_RATIO)));
|
|
break;
|
|
}
|
|
|
|
return workspaceBox;
|
|
}
|
|
|
|
vfunc_set_container(container) {
|
|
this._container = container;
|
|
this.hookup_style(container);
|
|
}
|
|
|
|
vfunc_allocate(container, box) {
|
|
const childBox = new Clutter.ActorBox();
|
|
|
|
const { spacing } = this;
|
|
|
|
const [width, height] = box.get_size();
|
|
let availableHeight = height;
|
|
|
|
// Search entry
|
|
const [searchHeight] = this._searchEntry.get_preferred_height(width);
|
|
childBox.set_origin(0, 0);
|
|
childBox.set_size(width, searchHeight);
|
|
this._searchEntry.allocate(childBox);
|
|
|
|
availableHeight -= searchHeight + spacing;
|
|
|
|
// Dash
|
|
const [, dashHeight] = this._dash.get_preferred_height(width);
|
|
childBox.set_origin(0, height - dashHeight);
|
|
childBox.set_size(width, dashHeight);
|
|
this._dash.allocate(childBox);
|
|
|
|
availableHeight -= dashHeight + spacing;
|
|
|
|
// Workspace Thumbnails
|
|
let thumbnailsHeight = 0;
|
|
if (this._workspacesThumbnails.visible) {
|
|
[thumbnailsHeight] =
|
|
this._workspacesThumbnails.get_preferred_height(width);
|
|
thumbnailsHeight = Math.min(
|
|
thumbnailsHeight,
|
|
height * WorkspaceThumbnail.MAX_THUMBNAIL_SCALE);
|
|
childBox.set_origin(0, searchHeight + spacing);
|
|
childBox.set_size(width, thumbnailsHeight);
|
|
this._workspacesThumbnails.allocate(childBox);
|
|
}
|
|
|
|
// Workspaces
|
|
const params = [box, searchHeight, dashHeight, thumbnailsHeight];
|
|
const transitionParams = this._stateAdjustment.getStateTransitionParams();
|
|
|
|
let workspacesBox;
|
|
if (!transitionParams.transitioning) {
|
|
workspacesBox =
|
|
this._getWorkspacesBoxForState(transitionParams.currentState, ...params);
|
|
} else {
|
|
const initialBox =
|
|
this._getWorkspacesBoxForState(transitionParams.initialState, ...params);
|
|
const finalBox =
|
|
this._getWorkspacesBoxForState(transitionParams.finalState, ...params);
|
|
workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress);
|
|
}
|
|
|
|
this._workspacesDisplay.allocate(workspacesBox);
|
|
|
|
// AppDisplay
|
|
const workspaceAppGridBox =
|
|
this._getWorkspacesBoxForState(ControlsState.APP_GRID, ...params);
|
|
|
|
childBox.set_origin(0, searchHeight + spacing + workspaceAppGridBox.get_height());
|
|
childBox.set_size(width,
|
|
height -
|
|
searchHeight - spacing -
|
|
workspaceAppGridBox.get_height() - spacing -
|
|
dashHeight);
|
|
|
|
this._appDisplay.allocate(childBox);
|
|
|
|
// ViewSelector
|
|
childBox.set_origin(0, searchHeight + spacing);
|
|
childBox.set_size(width, availableHeight);
|
|
|
|
this._viewSelector.allocate(childBox);
|
|
}
|
|
});
|
|
|
|
var OverviewAdjustment = GObject.registerClass(
|
|
class OverviewAdjustment extends St.Adjustment {
|
|
_init(actor) {
|
|
super._init({
|
|
actor,
|
|
value: ControlsState.WINDOW_PICKER,
|
|
lower: ControlsState.HIDDEN,
|
|
upper: ControlsState.APP_GRID,
|
|
});
|
|
}
|
|
|
|
getStateTransitionParams() {
|
|
const currentState = this.value;
|
|
|
|
const transition = this.get_transition('value');
|
|
let initialState = transition
|
|
? transition.get_interval().peek_initial_value()
|
|
: currentState;
|
|
let finalState = transition
|
|
? transition.get_interval().peek_final_value()
|
|
: currentState;
|
|
|
|
if (initialState > finalState) {
|
|
initialState = Math.ceil(initialState);
|
|
finalState = Math.floor(finalState);
|
|
} else {
|
|
initialState = Math.floor(initialState);
|
|
finalState = Math.ceil(finalState);
|
|
}
|
|
|
|
const length = Math.abs(finalState - initialState);
|
|
const progress = length > 0
|
|
? Math.abs((currentState - initialState) / length)
|
|
: 1;
|
|
|
|
return {
|
|
transitioning: transition !== null,
|
|
currentState,
|
|
initialState,
|
|
finalState,
|
|
progress,
|
|
};
|
|
}
|
|
});
|
|
|
|
var ControlsManager = GObject.registerClass(
|
|
class ControlsManager extends St.Widget {
|
|
_init() {
|
|
super._init({
|
|
style_class: 'controls-manager',
|
|
x_expand: true,
|
|
y_expand: true,
|
|
clip_to_allocation: true,
|
|
});
|
|
|
|
this._ignoreShowAppsButtonToggle = false;
|
|
|
|
this._searchEntry = new St.Entry({
|
|
style_class: 'search-entry',
|
|
/* Translators: this is the text displayed
|
|
in the search entry when no search is
|
|
active; it should not exceed ~30
|
|
characters. */
|
|
hint_text: _('Type to search'),
|
|
track_hover: true,
|
|
can_focus: true,
|
|
});
|
|
this._searchEntry.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
|
|
const searchEntryBin = new St.Bin({
|
|
child: this._searchEntry,
|
|
x_align: Clutter.ActorAlign.CENTER,
|
|
});
|
|
|
|
this.dash = new Dash.Dash();
|
|
|
|
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._stateAdjustment = new OverviewAdjustment(this);
|
|
this._stateAdjustment.connect('notify::value', this._update.bind(this));
|
|
|
|
this._nWorkspacesNotifyId =
|
|
workspaceManager.connect('notify::n-workspaces',
|
|
this._updateAdjustment.bind(this));
|
|
|
|
this.viewSelector = new ViewSelector.ViewSelector(this._searchEntry,
|
|
this.dash.showAppsButton);
|
|
this.viewSelector.connect('page-empty', this._onPageEmpty.bind(this));
|
|
|
|
this._thumbnailsBox =
|
|
new WorkspaceThumbnail.ThumbnailsBox(this._workspaceAdjustment);
|
|
this._workspacesDisplay = new WorkspacesView.WorkspacesDisplay(
|
|
this._workspaceAdjustment,
|
|
this._stateAdjustment);
|
|
this._appDisplay = new AppDisplay.AppDisplay();
|
|
|
|
this.add_child(searchEntryBin);
|
|
this.add_child(this._appDisplay);
|
|
this.add_child(this.dash);
|
|
this.add_child(this.viewSelector);
|
|
this.add_child(this._thumbnailsBox);
|
|
this.add_child(this._workspacesDisplay);
|
|
|
|
this.layout_manager = new ControlsManagerLayout(searchEntryBin,
|
|
this._appDisplay,
|
|
this._workspacesDisplay,
|
|
this._thumbnailsBox,
|
|
this.viewSelector,
|
|
this.dash,
|
|
this._stateAdjustment);
|
|
|
|
this.dash.showAppsButton.connect('notify::checked',
|
|
this._onShowAppsButtonToggled.bind(this));
|
|
|
|
Main.ctrlAltTabManager.addGroup(
|
|
this.appDisplay,
|
|
_('Applications'),
|
|
'view-app-grid-symbolic', {
|
|
proxy: this,
|
|
focusCallback: () => {
|
|
this.dash.showAppsButton.checked = true;
|
|
this.appDisplay.navigate_focus(
|
|
null, St.DirectionType.TAB_FORWARD, false);
|
|
},
|
|
});
|
|
|
|
Main.ctrlAltTabManager.addGroup(
|
|
this._workspacesDisplay,
|
|
_('Windows'),
|
|
'focus-windows-symbolic', {
|
|
proxy: this,
|
|
focusCallback: () => {
|
|
this.dash.showAppsButton.checked = false;
|
|
this._workspacesDisplay.navigate_focus(
|
|
null, St.DirectionType.TAB_FORWARD, false);
|
|
},
|
|
});
|
|
|
|
Main.wm.addKeybinding(
|
|
'toggle-application-view',
|
|
new Gio.Settings({ schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA }),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
|
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
|
|
this._toggleAppsPage.bind(this));
|
|
|
|
this.connect('destroy', this._onDestroy.bind(this));
|
|
|
|
this._update();
|
|
}
|
|
|
|
_getFitModeForState(state) {
|
|
switch (state) {
|
|
case ControlsState.HIDDEN:
|
|
case ControlsState.WINDOW_PICKER:
|
|
return WorkspacesView.FitMode.SINGLE;
|
|
case ControlsState.APP_GRID:
|
|
return WorkspacesView.FitMode.ALL;
|
|
default:
|
|
return WorkspacesView.FitMode.SINGLE;
|
|
}
|
|
}
|
|
|
|
_getThumbnailsBoxParams() {
|
|
const { initialState, finalState, progress } =
|
|
this._stateAdjustment.getStateTransitionParams();
|
|
|
|
const paramsForState = s => {
|
|
let opacity, scale, translationY;
|
|
switch (s) {
|
|
case ControlsState.HIDDEN:
|
|
case ControlsState.WINDOW_PICKER:
|
|
opacity = 255;
|
|
scale = 1;
|
|
translationY = 0;
|
|
break;
|
|
case ControlsState.APP_GRID:
|
|
opacity = 0;
|
|
scale = 0.5;
|
|
translationY = this._thumbnailsBox.height / 2;
|
|
break;
|
|
default:
|
|
opacity = 255;
|
|
scale = 1;
|
|
translationY = 0;
|
|
break;
|
|
}
|
|
|
|
return { opacity, scale, translationY };
|
|
};
|
|
|
|
const initialParams = paramsForState(initialState);
|
|
const finalParams = paramsForState(finalState);
|
|
|
|
return [
|
|
Util.lerp(initialParams.opacity, finalParams.opacity, progress),
|
|
Util.lerp(initialParams.scale, finalParams.scale, progress),
|
|
Util.lerp(initialParams.translationY, finalParams.translationY, progress),
|
|
];
|
|
}
|
|
|
|
_updateThumbnailsBox(animate = false) {
|
|
const page = this.viewSelector.getActivePage();
|
|
const searching = page === ViewSelector.ViewPage.SEARCH;
|
|
const [opacity, scale, translationY] = this._getThumbnailsBoxParams();
|
|
|
|
const thumbnailsBoxVisible = !searching && opacity !== 0;
|
|
if (thumbnailsBoxVisible) {
|
|
this._thumbnailsBox.opacity = 0;
|
|
this._thumbnailsBox.visible = thumbnailsBoxVisible;
|
|
}
|
|
|
|
const params = {
|
|
opacity: searching ? 0 : opacity,
|
|
duration: animate ? SIDE_CONTROLS_ANIMATION_TIME : 0,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => (this._thumbnailsBox.visible = thumbnailsBoxVisible),
|
|
};
|
|
|
|
if (!searching) {
|
|
params.scale_x = scale;
|
|
params.scale_y = scale;
|
|
params.translation_y = translationY;
|
|
}
|
|
|
|
this._thumbnailsBox.ease(params);
|
|
}
|
|
|
|
_update() {
|
|
const params = this._stateAdjustment.getStateTransitionParams();
|
|
|
|
const fitMode = Util.lerp(
|
|
this._getFitModeForState(params.initialState),
|
|
this._getFitModeForState(params.finalState),
|
|
params.progress);
|
|
|
|
const { fitModeAdjustment } = this._workspacesDisplay;
|
|
fitModeAdjustment.value = fitMode;
|
|
|
|
this._updateThumbnailsBox();
|
|
}
|
|
|
|
_onPageEmpty() {
|
|
const page = this.viewSelector.getActivePage();
|
|
const isActivities = page === ViewSelector.ViewPage.ACTIVITIES;
|
|
|
|
if (isActivities) {
|
|
this._appDisplay.show();
|
|
this._workspacesDisplay.reactive = true;
|
|
this._workspacesDisplay.setPrimaryWorkspaceVisible(true);
|
|
} else {
|
|
this.viewSelector.show();
|
|
}
|
|
|
|
this._updateThumbnailsBox(true);
|
|
|
|
this._appDisplay.ease({
|
|
opacity: isActivities ? 255 : 0,
|
|
duration: SIDE_CONTROLS_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => (this._appDisplay.visible = isActivities),
|
|
});
|
|
this._workspacesDisplay.ease({
|
|
opacity: isActivities ? 255 : 0,
|
|
duration: SIDE_CONTROLS_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => {
|
|
this._workspacesDisplay.reactive = isActivities;
|
|
this._workspacesDisplay.setPrimaryWorkspaceVisible(isActivities);
|
|
},
|
|
});
|
|
this.viewSelector.ease({
|
|
opacity: isActivities ? 0 : 255,
|
|
duration: SIDE_CONTROLS_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => (this.viewSelector.visible = !isActivities),
|
|
});
|
|
}
|
|
|
|
_onShowAppsButtonToggled() {
|
|
if (this._ignoreShowAppsButtonToggle)
|
|
return;
|
|
|
|
const checked = this.dash.showAppsButton.checked;
|
|
|
|
const value = checked
|
|
? ControlsState.APP_GRID : ControlsState.WINDOW_PICKER;
|
|
this._stateAdjustment.remove_transition('value');
|
|
this._stateAdjustment.ease(value, {
|
|
duration: SIDE_CONTROLS_ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
}
|
|
|
|
_toggleAppsPage() {
|
|
if (Main.overview.visible) {
|
|
const checked = this.dash.showAppsButton.checked;
|
|
this.dash.showAppsButton.checked = !checked;
|
|
} else {
|
|
Main.overview.show(ControlsState.APP_GRID);
|
|
}
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
vfunc_unmap() {
|
|
this._workspacesDisplay.hide();
|
|
super.vfunc_unmap();
|
|
}
|
|
|
|
animateToOverview(state, callback) {
|
|
this._ignoreShowAppsButtonToggle = true;
|
|
|
|
this.viewSelector.prepareToEnterOverview();
|
|
this._workspacesDisplay.prepareToEnterOverview();
|
|
if (!this._workspacesDisplay.activeWorkspaceHasMaximizedWindows())
|
|
Main.overview.fadeOutDesktop();
|
|
|
|
this._stateAdjustment.value = ControlsState.HIDDEN;
|
|
this._stateAdjustment.ease(state, {
|
|
duration: Overview.ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onStopped: () => {
|
|
if (callback)
|
|
callback();
|
|
},
|
|
});
|
|
|
|
this.dash.showAppsButton.checked =
|
|
state === ControlsState.APP_GRID;
|
|
|
|
this._ignoreShowAppsButtonToggle = false;
|
|
}
|
|
|
|
animateFromOverview(callback) {
|
|
this._ignoreShowAppsButtonToggle = true;
|
|
|
|
this._workspacesDisplay.prepareToLeaveOverview();
|
|
if (!this._workspacesDisplay.activeWorkspaceHasMaximizedWindows())
|
|
Main.overview.fadeInDesktop();
|
|
|
|
this._stateAdjustment.ease(ControlsState.HIDDEN, {
|
|
duration: Overview.ANIMATION_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onStopped: () => {
|
|
this.dash.showAppsButton.checked = false;
|
|
this._ignoreShowAppsButtonToggle = false;
|
|
|
|
if (callback)
|
|
callback();
|
|
},
|
|
});
|
|
}
|
|
|
|
get searchEntry() {
|
|
return this._searchEntry;
|
|
}
|
|
|
|
get appDisplay() {
|
|
return this._appDisplay;
|
|
}
|
|
});
|