workspace: Add window overlay to WindowClone

Add the window overlays we're currently showing using the WindowOverlay
class to the WindowClone class and implement them using
ClutterConstraints instead of the old fixed position/size layout, which
had to be used because the workspaces were scaled, and the title and app
icon were kept unscaled using a separate layer.

Specifically, this is done by adding the ClutterClones to a static
container owned by the WindowClone and adding the elements of the
overlay as children to the WindowClone itself. That way the
overlay-elements can use the container as a source for their constraints
and we avoid having to make sure the overlays remain visible above the
ClutterClones.

We're not using the new overlays yet, they're hidden by default and
showOverlay() isn't called anywhere yet.

https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1298
This commit is contained in:
Jonas Dreßler 2020-06-02 13:52:33 +02:00
parent 40123ae6da
commit 33ab53068e

View File

@ -216,9 +216,16 @@ var WindowClone = GObject.registerClass({
reactive: true,
can_focus: true,
accessible_role: Atk.Role.PUSH_BUTTON,
layout_manager: new WindowCloneLayout(),
});
this._windowContainer = new Clutter.Actor();
// gjs currently can't handle setting an actors layout manager during
// the initialization of the actor if that layout manager keeps track
// of its container, so set the layout manager after creating the
// container
this._windowContainer.layout_manager = new WindowCloneLayout();
this.add_child(this._windowContainer);
this.set_offscreen_redirect(Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY);
this._addWindow(realWindow.meta_window);
@ -230,8 +237,8 @@ var WindowClone = GObject.registerClass({
this._dragSlot = [0, 0, 0, 0];
this._stackAbove = null;
this.layout_manager.connect('notify::bounding-box', () =>
this.emit('size-changed'));
this._windowContainer.layout_manager.connect(
'notify::bounding-box', () => this.emit('size-changed'));
this._windowDestroyId =
this.realWindow.connect('destroy', () => this.destroy());
@ -258,6 +265,200 @@ var WindowClone = GObject.registerClass({
this._selected = false;
this._closeRequested = false;
this._idleHideOverlayId = 0;
this._border = new St.Widget({
visible: false,
style_class: 'window-clone-border',
});
this._borderConstraint = new Clutter.BindConstraint({
source: this._windowContainer,
coordinate: Clutter.BindCoordinate.SIZE,
});
this._border.add_constraint(this._borderConstraint);
this._border.add_constraint(new Clutter.AlignConstraint({
source: this._windowContainer,
align_axis: Clutter.AlignAxis.BOTH,
factor: 0.5,
}));
this._border.connect('style-changed',
this._onBorderStyleChanged.bind(this));
this._title = new St.Label({
visible: false,
style_class: 'window-caption',
text: this._getCaption(),
reactive: true,
});
this._title.add_constraint(new Clutter.AlignConstraint({
source: this._windowContainer,
align_axis: Clutter.AlignAxis.X_AXIS,
factor: 0.5,
}));
this._title.add_constraint(new Clutter.AlignConstraint({
source: this._windowContainer,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
factor: 1,
}));
this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
this.label_actor = this._title;
this._updateCaptionId = this.metaWindow.connect('notify::title', () => {
this._title.text = this._getCaption();
});
const layout = Meta.prefs_get_button_layout();
const side = layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
? St.Side.LEFT : St.Side.RIGHT;
this._closeButton = new St.Button({
visible: false,
style_class: 'window-close',
child: new St.Icon({ icon_name: 'window-close-symbolic' }),
});
this._closeButton.add_constraint(new Clutter.BindConstraint({
source: this._border,
coordinate: Clutter.BindCoordinate.POSITION,
}));
this._closeButton.add_constraint(new Clutter.AlignConstraint({
source: this._border,
align_axis: Clutter.AlignAxis.X_AXIS,
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
factor: side === St.Side.LEFT ? 0 : 1,
}));
this._closeButton.add_constraint(new Clutter.AlignConstraint({
source: this._border,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
factor: 0,
}));
this._closeButton.connect('clicked', () => this.deleteAll());
this.add_child(this._border);
this.add_child(this._title);
this.add_child(this._closeButton);
}
vfunc_get_preferred_width(forHeight) {
const themeNode = this.get_theme_node();
// Only include window previews in size request, not chrome
const [minWidth, natWidth] =
this._windowContainer.get_preferred_width(
themeNode.adjust_for_height(forHeight));
return themeNode.adjust_preferred_width(minWidth, natWidth);
}
vfunc_get_preferred_height(forWidth) {
const themeNode = this.get_theme_node();
const [minHeight, natHeight] =
this._windowContainer.get_preferred_height(
themeNode.adjust_for_width(forWidth));
return themeNode.adjust_preferred_height(minHeight, natHeight);
}
vfunc_allocate(box) {
this.set_allocation(box);
for (const child of this)
child.allocate_available_size(0, 0, box.get_width(), box.get_height());
}
_onBorderStyleChanged() {
let borderNode = this._border.get_theme_node();
this._borderSize = borderNode.get_border_width(St.Side.TOP);
// Increase the size of the border actor so the border outlines
// the bounding box
this._borderConstraint.offset = this._borderSize * 2;
}
_windowCanClose() {
return this.metaWindow.can_close() &&
!this.hasAttachedDialogs();
}
_getCaption() {
if (this.metaWindow.title)
return this.metaWindow.title;
let tracker = Shell.WindowTracker.get_default();
let app = tracker.get_window_app(this.metaWindow);
return app.get_name();
}
chromeHeights() {
this._border.ensure_style();
this._title.ensure_style();
const [, closeButtonHeight] = this._closeButton.get_preferred_height(-1);
const [, titleHeight] = this._title.get_preferred_height(-1);
return [this._borderSize + closeButtonHeight / 2,
Math.max(this._borderSize, (titleHeight / 2) + (this._borderSize / 2))];
}
chromeWidths() {
this._border.ensure_style();
const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
return [this._borderSize,
this._borderSize + closeButtonWidth / 2];
}
showOverlay(animate) {
const ongoingTransition = this._border.get_transition('opacity');
// Don't do anything if we're fully visible already
if (this._border.visible && !ongoingTransition)
return;
// If we're supposed to animate and an animation in our direction
// is already happening, let that one continue
if (animate &&
ongoingTransition &&
ongoingTransition.get_interval().peek_final_value() === 255)
return;
const toShow = this._windowCanClose()
? [this._border, this._title, this._closeButton]
: [this._border, this._title];
toShow.forEach(a => {
a.opacity = 0;
a.show();
a.ease({
opacity: 255,
duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
});
}
hideOverlay(animate) {
const ongoingTransition = this._border.get_transition('opacity');
// Don't do anything if we're fully hidden already
if (!this._border.visible && !ongoingTransition)
return;
// If we're supposed to animate and an animation in our direction
// is already happening, let that one continue
if (animate &&
ongoingTransition &&
ongoingTransition.get_interval().peek_final_value() === 0)
return;
[this._border, this._title, this._closeButton].forEach(a => {
a.opacity = 255;
a.ease({
opacity: 0,
duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => a.hide(),
});
});
}
_addWindow(metaWindow) {
@ -271,7 +472,7 @@ var WindowClone = GObject.registerClass({
// To avoid this, we hide it from pick.
Shell.util_set_hidden_from_pick(clone, true);
this.layout_manager.addWindow(clone, metaWindow);
this._windowContainer.layout_manager.addWindow(clone, metaWindow);
}
vfunc_has_overlaps() {
@ -290,11 +491,12 @@ var WindowClone = GObject.registerClass({
}
deleteAll() {
const windows = this.layout_manager.getWindows();
const windows = this._windowContainer.layout_manager.getWindows();
// Delete all windows, starting from the bottom-most (most-modal) one
for (const window of windows.reverse()) {
const metaWindow = this.layout_manager.getMetaWindow(window);
const metaWindow =
this._windowContainer.layout_manager.getMetaWindow(window);
metaWindow.delete(global.get_current_time());
}
@ -318,7 +520,7 @@ var WindowClone = GObject.registerClass({
}
hasAttachedDialogs() {
return this.get_n_children() > 1;
return this._windowContainer.layout_manager.getWindows().length > 1;
}
_updateAttachedDialogs() {
@ -338,7 +540,7 @@ var WindowClone = GObject.registerClass({
}
get boundingBox() {
const box = this.layout_manager.bounding_box;
const box = this._windowContainer.layout_manager.bounding_box;
return {
x: box.x1,
@ -349,7 +551,7 @@ var WindowClone = GObject.registerClass({
}
get windowCenter() {
const box = this.layout_manager.bounding_box;
const box = this._windowContainer.layout_manager.bounding_box;
return new Graphene.Point({
x: box.get_x() + box.get_width() / 2,
@ -392,11 +594,18 @@ var WindowClone = GObject.registerClass({
this.metaWindow._delegate = null;
this._delegate = null;
this.metaWindow.disconnect(this._updateCaptionId);
if (this._longPressLater) {
Meta.later_remove(this._longPressLater);
delete this._longPressLater;
}
if (this._idleHideOverlayId > 0) {
GLib.source_remove(this._idleHideOverlayId);
this._idleHideOverlayId = 0;
}
if (this.inDrag) {
this.emit('drag-end');
this.inDrag = false;
@ -415,6 +624,26 @@ var WindowClone = GObject.registerClass({
vfunc_leave_event(crossingEvent) {
this.emit('hide-chrome');
if (this._idleHideOverlayId > 0)
GLib.source_remove(this._idleHideOverlayId);
this._idleHideOverlayId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => {
if (this._closeButton['has-pointer'] ||
this._title['has-pointer'])
return GLib.SOURCE_CONTINUE;
if (!this['has-pointer'])
this.hideOverlay(true);
this._idleHideOverlayId = 0;
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');
return super.vfunc_leave_event(crossingEvent);
}
@ -425,6 +654,7 @@ var WindowClone = GObject.registerClass({
vfunc_key_focus_out() {
super.vfunc_key_focus_out();
this.hideOverlay(true);
this.emit('hide-chrome');
}