workspace: Split WindowPreview into a separate file
The workspace.js file is quite large and is a bit confusing when it comes to the term "window" in there, because it can either refer to a WindowPreview of a complete window or to an individual window like an attached dialog. So try to avoid that confusion and split the new WindowPreview class and its WindowPreviewLayout layout manager out into a new windowPreview.js file. https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1307
This commit is contained in:
parent
8357739ef8
commit
e4cbe5126a
@ -108,6 +108,7 @@
|
|||||||
<file>ui/windowAttentionHandler.js</file>
|
<file>ui/windowAttentionHandler.js</file>
|
||||||
<file>ui/windowMenu.js</file>
|
<file>ui/windowMenu.js</file>
|
||||||
<file>ui/windowManager.js</file>
|
<file>ui/windowManager.js</file>
|
||||||
|
<file>ui/windowPreview.js</file>
|
||||||
<file>ui/workspace.js</file>
|
<file>ui/workspace.js</file>
|
||||||
<file>ui/workspaceSwitcherPopup.js</file>
|
<file>ui/workspaceSwitcherPopup.js</file>
|
||||||
<file>ui/workspaceThumbnail.js</file>
|
<file>ui/workspaceThumbnail.js</file>
|
||||||
|
736
js/ui/windowPreview.js
Normal file
736
js/ui/windowPreview.js
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
|
/* exported WindowPreview */
|
||||||
|
|
||||||
|
const { Atk, Clutter, GLib, GObject,
|
||||||
|
Graphene, Meta, Pango, Shell, St } = imports.gi;
|
||||||
|
|
||||||
|
const DND = imports.ui.dnd;
|
||||||
|
|
||||||
|
var WINDOW_DND_SIZE = 256;
|
||||||
|
|
||||||
|
var WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
|
||||||
|
var WINDOW_OVERLAY_FADE_TIME = 200;
|
||||||
|
|
||||||
|
var DRAGGING_WINDOW_OPACITY = 100;
|
||||||
|
|
||||||
|
var WindowPreviewLayout = GObject.registerClass({
|
||||||
|
Properties: {
|
||||||
|
'bounding-box': GObject.ParamSpec.boxed(
|
||||||
|
'bounding-box', 'Bounding box', 'Bounding box',
|
||||||
|
GObject.ParamFlags.READABLE,
|
||||||
|
Clutter.ActorBox.$gtype),
|
||||||
|
},
|
||||||
|
}, class WindowPreviewLayout extends Clutter.LayoutManager {
|
||||||
|
_init() {
|
||||||
|
super._init();
|
||||||
|
|
||||||
|
this._container = null;
|
||||||
|
this._boundingBox = new Clutter.ActorBox();
|
||||||
|
this._windows = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
_layoutChanged() {
|
||||||
|
let frameRect;
|
||||||
|
|
||||||
|
for (const windowInfo of this._windows.values()) {
|
||||||
|
const frame = windowInfo.metaWindow.get_frame_rect();
|
||||||
|
frameRect = frameRect ? frameRect.union(frame) : frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frameRect)
|
||||||
|
frameRect = new Meta.Rectangle();
|
||||||
|
|
||||||
|
const oldBox = this._boundingBox.copy();
|
||||||
|
this._boundingBox.set_origin(frameRect.x, frameRect.y);
|
||||||
|
this._boundingBox.set_size(frameRect.width, frameRect.height);
|
||||||
|
|
||||||
|
if (!this._boundingBox.equal(oldBox))
|
||||||
|
this.notify('bounding-box');
|
||||||
|
|
||||||
|
// Always call layout_changed(), a size or position change of an
|
||||||
|
// attached dialog might not affect the boundingBox
|
||||||
|
this.layout_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_set_container(container) {
|
||||||
|
this._container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_get_preferred_height(_container, _forWidth) {
|
||||||
|
return [0, this._boundingBox.get_height()];
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_get_preferred_width(_container, _forHeight) {
|
||||||
|
return [0, this._boundingBox.get_width()];
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_allocate(container, box) {
|
||||||
|
// If the scale isn't 1, we weren't allocated our preferred size
|
||||||
|
// and have to scale the children allocations accordingly.
|
||||||
|
const scaleX = box.get_width() / this._boundingBox.get_width();
|
||||||
|
const scaleY = box.get_height() / this._boundingBox.get_height();
|
||||||
|
|
||||||
|
const childBox = new Clutter.ActorBox();
|
||||||
|
|
||||||
|
for (const child of container) {
|
||||||
|
if (!child.visible)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const windowInfo = this._windows.get(child);
|
||||||
|
if (windowInfo) {
|
||||||
|
const bufferRect = windowInfo.metaWindow.get_buffer_rect();
|
||||||
|
childBox.set_origin(
|
||||||
|
bufferRect.x - this._boundingBox.x1,
|
||||||
|
bufferRect.y - this._boundingBox.y1);
|
||||||
|
|
||||||
|
const [, , natWidth, natHeight] = child.get_preferred_size();
|
||||||
|
childBox.set_size(natWidth, natHeight);
|
||||||
|
|
||||||
|
childBox.x1 *= scaleX;
|
||||||
|
childBox.x2 *= scaleX;
|
||||||
|
childBox.y1 *= scaleY;
|
||||||
|
childBox.y2 *= scaleY;
|
||||||
|
|
||||||
|
child.allocate(childBox);
|
||||||
|
} else {
|
||||||
|
child.allocate_preferred_size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addWindow:
|
||||||
|
* @param {Meta.Window} window: the MetaWindow instance
|
||||||
|
*
|
||||||
|
* Creates a ClutterActor drawing the texture of @window and adds it
|
||||||
|
* to the container. If @window is already part of the preview, this
|
||||||
|
* function will do nothing.
|
||||||
|
*
|
||||||
|
* @returns {Clutter.Actor} The newly created actor drawing @window
|
||||||
|
*/
|
||||||
|
addWindow(window) {
|
||||||
|
const index = [...this._windows.values()].findIndex(info =>
|
||||||
|
info.metaWindow === window);
|
||||||
|
|
||||||
|
if (index !== -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const windowActor = window.get_compositor_private();
|
||||||
|
const actor = new Clutter.Clone({ source: windowActor });
|
||||||
|
|
||||||
|
this._windows.set(actor, {
|
||||||
|
metaWindow: window,
|
||||||
|
windowActor,
|
||||||
|
sizeChangedId: window.connect('size-changed', () =>
|
||||||
|
this._layoutChanged()),
|
||||||
|
positionChangedId: window.connect('position-changed', () =>
|
||||||
|
this._layoutChanged()),
|
||||||
|
windowActorDestroyId: windowActor.connect('destroy', () =>
|
||||||
|
actor.destroy()),
|
||||||
|
destroyId: actor.connect('destroy', () =>
|
||||||
|
this.removeWindow(window)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this._container.add_child(actor);
|
||||||
|
|
||||||
|
this._layoutChanged();
|
||||||
|
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removeWindow:
|
||||||
|
* @param {Meta.Window} window: the window to remove from the preview
|
||||||
|
*
|
||||||
|
* Removes a MetaWindow @window from the preview which has been added
|
||||||
|
* previously using addWindow(). If @window is not part of preview,
|
||||||
|
* this function will do nothing.
|
||||||
|
*/
|
||||||
|
removeWindow(window) {
|
||||||
|
const entry = [...this._windows].find(
|
||||||
|
([, i]) => i.metaWindow === window);
|
||||||
|
|
||||||
|
if (!entry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const [actor, windowInfo] = entry;
|
||||||
|
|
||||||
|
windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
|
||||||
|
windowInfo.metaWindow.disconnect(windowInfo.positionChangedId);
|
||||||
|
windowInfo.windowActor.disconnect(windowInfo.windowActorDestroyId);
|
||||||
|
actor.disconnect(windowInfo.destroyId);
|
||||||
|
|
||||||
|
this._windows.delete(actor);
|
||||||
|
this._container.remove_child(actor);
|
||||||
|
|
||||||
|
this._layoutChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getWindows:
|
||||||
|
*
|
||||||
|
* Gets an array of all MetaWindows that were added to the layout
|
||||||
|
* using addWindow(), ordered by the insertion order.
|
||||||
|
*
|
||||||
|
* @returns {Array} An array including all windows
|
||||||
|
*/
|
||||||
|
getWindows() {
|
||||||
|
return [...this._windows.values()].map(i => i.metaWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
get bounding_box() {
|
||||||
|
return this._boundingBox;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var WindowPreview = GObject.registerClass({
|
||||||
|
Signals: {
|
||||||
|
'drag-begin': {},
|
||||||
|
'drag-cancelled': {},
|
||||||
|
'drag-end': {},
|
||||||
|
'selected': { param_types: [GObject.TYPE_UINT] },
|
||||||
|
'show-chrome': {},
|
||||||
|
'size-changed': {},
|
||||||
|
},
|
||||||
|
}, class WindowPreview extends St.Widget {
|
||||||
|
_init(metaWindow, workspace) {
|
||||||
|
this.metaWindow = metaWindow;
|
||||||
|
this.metaWindow._delegate = this;
|
||||||
|
this._windowActor = metaWindow.get_compositor_private();
|
||||||
|
this._workspace = workspace;
|
||||||
|
|
||||||
|
super._init({
|
||||||
|
reactive: true,
|
||||||
|
can_focus: true,
|
||||||
|
accessible_role: Atk.Role.PUSH_BUTTON,
|
||||||
|
offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 WindowPreviewLayout();
|
||||||
|
this.add_child(this._windowContainer);
|
||||||
|
|
||||||
|
this._addWindow(metaWindow);
|
||||||
|
|
||||||
|
this._delegate = this;
|
||||||
|
|
||||||
|
this.slotId = 0;
|
||||||
|
this._stackAbove = null;
|
||||||
|
|
||||||
|
this._windowContainer.layout_manager.connect(
|
||||||
|
'notify::bounding-box', layout => {
|
||||||
|
// A bounding box of 0x0 means all windows were removed
|
||||||
|
if (layout.bounding_box.get_area() > 0)
|
||||||
|
this.emit('size-changed');
|
||||||
|
});
|
||||||
|
|
||||||
|
this._windowDestroyId =
|
||||||
|
this._windowActor.connect('destroy', () => this.destroy());
|
||||||
|
|
||||||
|
this._updateAttachedDialogs();
|
||||||
|
this.x = this.boundingBox.x;
|
||||||
|
this.y = this.boundingBox.y;
|
||||||
|
|
||||||
|
let clickAction = new Clutter.ClickAction();
|
||||||
|
clickAction.connect('clicked', () => this._activate());
|
||||||
|
clickAction.connect('long-press', this._onLongPress.bind(this));
|
||||||
|
this.add_action(clickAction);
|
||||||
|
this.connect('destroy', this._onDestroy.bind(this));
|
||||||
|
|
||||||
|
this._draggable = DND.makeDraggable(this,
|
||||||
|
{ restoreOnSuccess: true,
|
||||||
|
manualMode: true,
|
||||||
|
dragActorMaxSize: WINDOW_DND_SIZE,
|
||||||
|
dragActorOpacity: DRAGGING_WINDOW_OPACITY });
|
||||||
|
this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
|
||||||
|
this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
|
||||||
|
this._draggable.connect('drag-end', this._onDragEnd.bind(this));
|
||||||
|
this.inDrag = false;
|
||||||
|
|
||||||
|
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._borderCenter = new Clutter.Actor();
|
||||||
|
this._border.bind_property('visible', this._borderCenter, 'visible',
|
||||||
|
GObject.BindingFlags.SYNC_CREATE);
|
||||||
|
this._borderCenterConstraint = new Clutter.BindConstraint({
|
||||||
|
source: this._windowContainer,
|
||||||
|
coordinate: Clutter.BindCoordinate.SIZE,
|
||||||
|
});
|
||||||
|
this._borderCenter.add_constraint(this._borderCenterConstraint);
|
||||||
|
this._borderCenter.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.BindConstraint({
|
||||||
|
source: this._borderCenter,
|
||||||
|
coordinate: Clutter.BindCoordinate.POSITION,
|
||||||
|
}));
|
||||||
|
this._title.add_constraint(new Clutter.AlignConstraint({
|
||||||
|
source: this._borderCenter,
|
||||||
|
align_axis: Clutter.AlignAxis.X_AXIS,
|
||||||
|
factor: 0.5,
|
||||||
|
}));
|
||||||
|
this._title.add_constraint(new Clutter.AlignConstraint({
|
||||||
|
source: this._borderCenter,
|
||||||
|
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();
|
||||||
|
this._closeButtonSide =
|
||||||
|
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._borderCenter,
|
||||||
|
coordinate: Clutter.BindCoordinate.POSITION,
|
||||||
|
}));
|
||||||
|
this._closeButton.add_constraint(new Clutter.AlignConstraint({
|
||||||
|
source: this._borderCenter,
|
||||||
|
align_axis: Clutter.AlignAxis.X_AXIS,
|
||||||
|
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
|
||||||
|
factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
|
||||||
|
}));
|
||||||
|
this._closeButton.add_constraint(new Clutter.AlignConstraint({
|
||||||
|
source: this._borderCenter,
|
||||||
|
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._borderCenter);
|
||||||
|
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;
|
||||||
|
this._borderCenterConstraint.offset = this._borderSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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);
|
||||||
|
|
||||||
|
const topOversize = (this._borderSize / 2) + (closeButtonHeight / 2);
|
||||||
|
const bottomOversize = Math.max(
|
||||||
|
this._borderSize,
|
||||||
|
(titleHeight / 2) + (this._borderSize / 2));
|
||||||
|
|
||||||
|
return [topOversize, bottomOversize];
|
||||||
|
}
|
||||||
|
|
||||||
|
chromeWidths() {
|
||||||
|
this._border.ensure_style();
|
||||||
|
const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
|
||||||
|
|
||||||
|
const leftOversize = this._closeButtonSide === St.Side.LEFT
|
||||||
|
? (this._borderSize / 2) + (closeButtonWidth / 2)
|
||||||
|
: this._borderSize;
|
||||||
|
const rightOversize = this._closeButtonSide === St.Side.LEFT
|
||||||
|
? this._borderSize
|
||||||
|
: (this._borderSize / 2) + (closeButtonWidth / 2);
|
||||||
|
|
||||||
|
return [leftOversize, rightOversize];
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('show-chrome');
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const clone = this._windowContainer.layout_manager.addWindow(metaWindow);
|
||||||
|
|
||||||
|
// We expect this to be used for all interaction rather than
|
||||||
|
// the ClutterClone; as the former is reactive and the latter
|
||||||
|
// is not, this just works for most cases. However, for DND all
|
||||||
|
// actors are picked, so DND operations would operate on the clone.
|
||||||
|
// To avoid this, we hide it from pick.
|
||||||
|
Shell.util_set_hidden_from_pick(clone, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_has_overlaps() {
|
||||||
|
return this._hasAttachedDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
_deleteAll() {
|
||||||
|
const windows = this._windowContainer.layout_manager.getWindows();
|
||||||
|
|
||||||
|
// Delete all windows, starting from the bottom-most (most-modal) one
|
||||||
|
for (const window of windows.reverse())
|
||||||
|
window.delete(global.get_current_time());
|
||||||
|
|
||||||
|
this._closeRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDialog(win) {
|
||||||
|
let parent = win.get_transient_for();
|
||||||
|
while (parent.is_attached_dialog())
|
||||||
|
parent = parent.get_transient_for();
|
||||||
|
|
||||||
|
// Display dialog if it is attached to our metaWindow
|
||||||
|
if (win.is_attached_dialog() && parent == this.metaWindow)
|
||||||
|
this._addWindow(win);
|
||||||
|
|
||||||
|
// The dialog popped up after the user tried to close the window,
|
||||||
|
// assume it's a close confirmation and leave the overview
|
||||||
|
if (this._closeRequested)
|
||||||
|
this._activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasAttachedDialogs() {
|
||||||
|
return this._windowContainer.layout_manager.getWindows().length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateAttachedDialogs() {
|
||||||
|
let iter = win => {
|
||||||
|
let actor = win.get_compositor_private();
|
||||||
|
|
||||||
|
if (!actor)
|
||||||
|
return false;
|
||||||
|
if (!win.is_attached_dialog())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
this._addWindow(win);
|
||||||
|
win.foreach_transient(iter);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
this.metaWindow.foreach_transient(iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
get boundingBox() {
|
||||||
|
const box = this._windowContainer.layout_manager.bounding_box;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: box.x1,
|
||||||
|
y: box.y1,
|
||||||
|
width: box.get_width(),
|
||||||
|
height: box.get_height(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get windowCenter() {
|
||||||
|
const box = this._windowContainer.layout_manager.bounding_box;
|
||||||
|
|
||||||
|
return new Graphene.Point({
|
||||||
|
x: box.get_x() + box.get_width() / 2,
|
||||||
|
y: box.get_y() + box.get_height() / 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the actor just below us, respecting reparenting done by DND code
|
||||||
|
_getActualStackAbove() {
|
||||||
|
if (this._stackAbove == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (this.inDrag) {
|
||||||
|
if (this._stackAbove._delegate)
|
||||||
|
return this._stackAbove._delegate._getActualStackAbove();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return this._stackAbove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStackAbove(actor) {
|
||||||
|
this._stackAbove = actor;
|
||||||
|
if (this.inDrag)
|
||||||
|
// We'll fix up the stack after the drag
|
||||||
|
return;
|
||||||
|
|
||||||
|
let parent = this.get_parent();
|
||||||
|
let actualAbove = this._getActualStackAbove();
|
||||||
|
if (actualAbove == null)
|
||||||
|
parent.set_child_below_sibling(this, null);
|
||||||
|
else
|
||||||
|
parent.set_child_above_sibling(this, actualAbove);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDestroy() {
|
||||||
|
this._windowActor.disconnect(this._windowDestroyId);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_activate() {
|
||||||
|
this._selected = true;
|
||||||
|
this.emit('selected', global.get_current_time());
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_enter_event(crossingEvent) {
|
||||||
|
this.showOverlay(true);
|
||||||
|
return super.vfunc_enter_event(crossingEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_leave_event(crossingEvent) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_key_focus_in() {
|
||||||
|
super.vfunc_key_focus_in();
|
||||||
|
this.showOverlay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_key_focus_out() {
|
||||||
|
super.vfunc_key_focus_out();
|
||||||
|
this.hideOverlay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
vfunc_key_press_event(keyEvent) {
|
||||||
|
let symbol = keyEvent.keyval;
|
||||||
|
let isEnter = symbol == Clutter.KEY_Return || symbol == Clutter.KEY_KP_Enter;
|
||||||
|
if (isEnter) {
|
||||||
|
this._activate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.vfunc_key_press_event(keyEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onLongPress(action, actor, state) {
|
||||||
|
// Take advantage of the Clutter policy to consider
|
||||||
|
// a long-press canceled when the pointer movement
|
||||||
|
// exceeds dnd-drag-threshold to manually start the drag
|
||||||
|
if (state == Clutter.LongPressState.CANCEL) {
|
||||||
|
let event = Clutter.get_current_event();
|
||||||
|
this._dragTouchSequence = event.get_event_sequence();
|
||||||
|
|
||||||
|
if (this._longPressLater)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// A click cancels a long-press before any click handler is
|
||||||
|
// run - make sure to not start a drag in that case
|
||||||
|
this._longPressLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
|
||||||
|
delete this._longPressLater;
|
||||||
|
if (this._selected)
|
||||||
|
return;
|
||||||
|
let [x, y] = action.get_coords();
|
||||||
|
action.release();
|
||||||
|
this._draggable.startDrag(x, y, global.get_current_time(), this._dragTouchSequence, event.get_device());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.showOverlay(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDragBegin(_draggable, _time) {
|
||||||
|
this.inDrag = true;
|
||||||
|
this.hideOverlay(false);
|
||||||
|
this.emit('drag-begin');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(source, actor, x, y, time) {
|
||||||
|
return this._workspace.handleDragOver(source, actor, x, y, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptDrop(source, actor, x, y, time) {
|
||||||
|
return this._workspace.acceptDrop(source, actor, x, y, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDragCancelled(_draggable, _time) {
|
||||||
|
this.emit('drag-cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDragEnd(_draggable, _time, _snapback) {
|
||||||
|
this.inDrag = false;
|
||||||
|
|
||||||
|
// We may not have a parent if DnD completed successfully, in
|
||||||
|
// which case our clone will shortly be destroyed and replaced
|
||||||
|
// with a new one on the target workspace.
|
||||||
|
let parent = this.get_parent();
|
||||||
|
if (parent !== null) {
|
||||||
|
if (this._stackAbove == null)
|
||||||
|
parent.set_child_below_sibling(this, null);
|
||||||
|
else
|
||||||
|
parent.set_child_above_sibling(this, this._stackAbove);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this['has-pointer'])
|
||||||
|
this.showOverlay(true);
|
||||||
|
|
||||||
|
this.emit('drag-end');
|
||||||
|
}
|
||||||
|
});
|
@ -1,24 +1,17 @@
|
|||||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
/* exported Workspace */
|
/* exported Workspace */
|
||||||
|
|
||||||
const { Atk, Clutter, GLib, GObject,
|
const { Clutter, GLib, GObject, Meta, St } = imports.gi;
|
||||||
Graphene, Meta, Pango, Shell, St } = imports.gi;
|
|
||||||
|
|
||||||
const DND = imports.ui.dnd;
|
const DND = imports.ui.dnd;
|
||||||
const Main = imports.ui.main;
|
const Main = imports.ui.main;
|
||||||
const Overview = imports.ui.overview;
|
const Overview = imports.ui.overview;
|
||||||
|
const { WindowPreview } = imports.ui.windowPreview;
|
||||||
var WINDOW_DND_SIZE = 256;
|
|
||||||
|
|
||||||
var WINDOW_PREVIEW_MAXIMUM_SCALE = 1.0;
|
var WINDOW_PREVIEW_MAXIMUM_SCALE = 1.0;
|
||||||
|
|
||||||
var WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
|
|
||||||
var WINDOW_OVERLAY_FADE_TIME = 200;
|
|
||||||
|
|
||||||
var WINDOW_REPOSITIONING_DELAY = 750;
|
var WINDOW_REPOSITIONING_DELAY = 750;
|
||||||
|
|
||||||
var DRAGGING_WINDOW_OPACITY = 100;
|
|
||||||
|
|
||||||
// When calculating a layout, we calculate the scale of windows and the percent
|
// When calculating a layout, we calculate the scale of windows and the percent
|
||||||
// of the available area the new layout uses. If the values for the new layout,
|
// of the available area the new layout uses. If the values for the new layout,
|
||||||
// when weighted with the values as below, are worse than the previous layout's,
|
// when weighted with the values as below, are worse than the previous layout's,
|
||||||
@ -33,728 +26,6 @@ function _interpolate(start, end, step) {
|
|||||||
return start + (end - start) * step;
|
return start + (end - start) * step;
|
||||||
}
|
}
|
||||||
|
|
||||||
var WindowPreviewLayout = GObject.registerClass({
|
|
||||||
Properties: {
|
|
||||||
'bounding-box': GObject.ParamSpec.boxed(
|
|
||||||
'bounding-box', 'Bounding box', 'Bounding box',
|
|
||||||
GObject.ParamFlags.READABLE,
|
|
||||||
Clutter.ActorBox.$gtype),
|
|
||||||
},
|
|
||||||
}, class WindowPreviewLayout extends Clutter.LayoutManager {
|
|
||||||
_init() {
|
|
||||||
super._init();
|
|
||||||
|
|
||||||
this._container = null;
|
|
||||||
this._boundingBox = new Clutter.ActorBox();
|
|
||||||
this._windows = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
_layoutChanged() {
|
|
||||||
let frameRect;
|
|
||||||
|
|
||||||
for (const windowInfo of this._windows.values()) {
|
|
||||||
const frame = windowInfo.metaWindow.get_frame_rect();
|
|
||||||
frameRect = frameRect ? frameRect.union(frame) : frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frameRect)
|
|
||||||
frameRect = new Meta.Rectangle();
|
|
||||||
|
|
||||||
const oldBox = this._boundingBox.copy();
|
|
||||||
this._boundingBox.set_origin(frameRect.x, frameRect.y);
|
|
||||||
this._boundingBox.set_size(frameRect.width, frameRect.height);
|
|
||||||
|
|
||||||
if (!this._boundingBox.equal(oldBox))
|
|
||||||
this.notify('bounding-box');
|
|
||||||
|
|
||||||
// Always call layout_changed(), a size or position change of an
|
|
||||||
// attached dialog might not affect the boundingBox
|
|
||||||
this.layout_changed();
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_set_container(container) {
|
|
||||||
this._container = container;
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_get_preferred_height(_container, _forWidth) {
|
|
||||||
return [0, this._boundingBox.get_height()];
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_get_preferred_width(_container, _forHeight) {
|
|
||||||
return [0, this._boundingBox.get_width()];
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_allocate(container, box) {
|
|
||||||
// If the scale isn't 1, we weren't allocated our preferred size
|
|
||||||
// and have to scale the children allocations accordingly.
|
|
||||||
const scaleX = box.get_width() / this._boundingBox.get_width();
|
|
||||||
const scaleY = box.get_height() / this._boundingBox.get_height();
|
|
||||||
|
|
||||||
const childBox = new Clutter.ActorBox();
|
|
||||||
|
|
||||||
for (const child of container) {
|
|
||||||
if (!child.visible)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const windowInfo = this._windows.get(child);
|
|
||||||
if (windowInfo) {
|
|
||||||
const bufferRect = windowInfo.metaWindow.get_buffer_rect();
|
|
||||||
childBox.set_origin(
|
|
||||||
bufferRect.x - this._boundingBox.x1,
|
|
||||||
bufferRect.y - this._boundingBox.y1);
|
|
||||||
|
|
||||||
const [, , natWidth, natHeight] = child.get_preferred_size();
|
|
||||||
childBox.set_size(natWidth, natHeight);
|
|
||||||
|
|
||||||
childBox.x1 *= scaleX;
|
|
||||||
childBox.x2 *= scaleX;
|
|
||||||
childBox.y1 *= scaleY;
|
|
||||||
childBox.y2 *= scaleY;
|
|
||||||
|
|
||||||
child.allocate(childBox);
|
|
||||||
} else {
|
|
||||||
child.allocate_preferred_size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* addWindow:
|
|
||||||
* @param {Meta.Window} window: the MetaWindow instance
|
|
||||||
*
|
|
||||||
* Creates a ClutterActor drawing the texture of @window and adds it
|
|
||||||
* to the container. If @window is already part of the preview, this
|
|
||||||
* function will do nothing.
|
|
||||||
*
|
|
||||||
* @returns {Clutter.Actor} The newly created actor drawing @window
|
|
||||||
*/
|
|
||||||
addWindow(window) {
|
|
||||||
const index = [...this._windows.values()].findIndex(info =>
|
|
||||||
info.metaWindow === window);
|
|
||||||
|
|
||||||
if (index !== -1)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const windowActor = window.get_compositor_private();
|
|
||||||
const actor = new Clutter.Clone({ source: windowActor });
|
|
||||||
|
|
||||||
this._windows.set(actor, {
|
|
||||||
metaWindow: window,
|
|
||||||
windowActor,
|
|
||||||
sizeChangedId: window.connect('size-changed', () =>
|
|
||||||
this._layoutChanged()),
|
|
||||||
positionChangedId: window.connect('position-changed', () =>
|
|
||||||
this._layoutChanged()),
|
|
||||||
windowActorDestroyId: windowActor.connect('destroy', () =>
|
|
||||||
actor.destroy()),
|
|
||||||
destroyId: actor.connect('destroy', () =>
|
|
||||||
this.removeWindow(window)),
|
|
||||||
});
|
|
||||||
|
|
||||||
this._container.add_child(actor);
|
|
||||||
|
|
||||||
this._layoutChanged();
|
|
||||||
|
|
||||||
return actor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removeWindow:
|
|
||||||
* @param {Meta.Window} window: the window to remove from the preview
|
|
||||||
*
|
|
||||||
* Removes a MetaWindow @window from the preview which has been added
|
|
||||||
* previously using addWindow(). If @window is not part of preview,
|
|
||||||
* this function will do nothing.
|
|
||||||
*/
|
|
||||||
removeWindow(window) {
|
|
||||||
const entry = [...this._windows].find(
|
|
||||||
([, i]) => i.metaWindow === window);
|
|
||||||
|
|
||||||
if (!entry)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const [actor, windowInfo] = entry;
|
|
||||||
|
|
||||||
windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
|
|
||||||
windowInfo.metaWindow.disconnect(windowInfo.positionChangedId);
|
|
||||||
windowInfo.windowActor.disconnect(windowInfo.windowActorDestroyId);
|
|
||||||
actor.disconnect(windowInfo.destroyId);
|
|
||||||
|
|
||||||
this._windows.delete(actor);
|
|
||||||
this._container.remove_child(actor);
|
|
||||||
|
|
||||||
this._layoutChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getWindows:
|
|
||||||
*
|
|
||||||
* Gets an array of all MetaWindows that were added to the layout
|
|
||||||
* using addWindow(), ordered by the insertion order.
|
|
||||||
*
|
|
||||||
* @returns {Array} An array including all windows
|
|
||||||
*/
|
|
||||||
getWindows() {
|
|
||||||
return [...this._windows.values()].map(i => i.metaWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
get bounding_box() {
|
|
||||||
return this._boundingBox;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var WindowPreview = GObject.registerClass({
|
|
||||||
Signals: {
|
|
||||||
'drag-begin': {},
|
|
||||||
'drag-cancelled': {},
|
|
||||||
'drag-end': {},
|
|
||||||
'selected': { param_types: [GObject.TYPE_UINT] },
|
|
||||||
'show-chrome': {},
|
|
||||||
'size-changed': {},
|
|
||||||
},
|
|
||||||
}, class WindowPreview extends St.Widget {
|
|
||||||
_init(metaWindow, workspace) {
|
|
||||||
this.metaWindow = metaWindow;
|
|
||||||
this.metaWindow._delegate = this;
|
|
||||||
this._windowActor = metaWindow.get_compositor_private();
|
|
||||||
this._workspace = workspace;
|
|
||||||
|
|
||||||
super._init({
|
|
||||||
reactive: true,
|
|
||||||
can_focus: true,
|
|
||||||
accessible_role: Atk.Role.PUSH_BUTTON,
|
|
||||||
offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 WindowPreviewLayout();
|
|
||||||
this.add_child(this._windowContainer);
|
|
||||||
|
|
||||||
this._addWindow(metaWindow);
|
|
||||||
|
|
||||||
this._delegate = this;
|
|
||||||
|
|
||||||
this.slotId = 0;
|
|
||||||
this._stackAbove = null;
|
|
||||||
|
|
||||||
this._windowContainer.layout_manager.connect(
|
|
||||||
'notify::bounding-box', layout => {
|
|
||||||
// A bounding box of 0x0 means all windows were removed
|
|
||||||
if (layout.bounding_box.get_area() > 0)
|
|
||||||
this.emit('size-changed');
|
|
||||||
});
|
|
||||||
|
|
||||||
this._windowDestroyId =
|
|
||||||
this._windowActor.connect('destroy', () => this.destroy());
|
|
||||||
|
|
||||||
this._updateAttachedDialogs();
|
|
||||||
this.x = this.boundingBox.x;
|
|
||||||
this.y = this.boundingBox.y;
|
|
||||||
|
|
||||||
let clickAction = new Clutter.ClickAction();
|
|
||||||
clickAction.connect('clicked', () => this._activate());
|
|
||||||
clickAction.connect('long-press', this._onLongPress.bind(this));
|
|
||||||
this.add_action(clickAction);
|
|
||||||
this.connect('destroy', this._onDestroy.bind(this));
|
|
||||||
|
|
||||||
this._draggable = DND.makeDraggable(this,
|
|
||||||
{ restoreOnSuccess: true,
|
|
||||||
manualMode: true,
|
|
||||||
dragActorMaxSize: WINDOW_DND_SIZE,
|
|
||||||
dragActorOpacity: DRAGGING_WINDOW_OPACITY });
|
|
||||||
this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
|
|
||||||
this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
|
|
||||||
this._draggable.connect('drag-end', this._onDragEnd.bind(this));
|
|
||||||
this.inDrag = false;
|
|
||||||
|
|
||||||
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._borderCenter = new Clutter.Actor();
|
|
||||||
this._border.bind_property('visible', this._borderCenter, 'visible',
|
|
||||||
GObject.BindingFlags.SYNC_CREATE);
|
|
||||||
this._borderCenterConstraint = new Clutter.BindConstraint({
|
|
||||||
source: this._windowContainer,
|
|
||||||
coordinate: Clutter.BindCoordinate.SIZE,
|
|
||||||
});
|
|
||||||
this._borderCenter.add_constraint(this._borderCenterConstraint);
|
|
||||||
this._borderCenter.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.BindConstraint({
|
|
||||||
source: this._borderCenter,
|
|
||||||
coordinate: Clutter.BindCoordinate.POSITION,
|
|
||||||
}));
|
|
||||||
this._title.add_constraint(new Clutter.AlignConstraint({
|
|
||||||
source: this._borderCenter,
|
|
||||||
align_axis: Clutter.AlignAxis.X_AXIS,
|
|
||||||
factor: 0.5,
|
|
||||||
}));
|
|
||||||
this._title.add_constraint(new Clutter.AlignConstraint({
|
|
||||||
source: this._borderCenter,
|
|
||||||
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();
|
|
||||||
this._closeButtonSide =
|
|
||||||
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._borderCenter,
|
|
||||||
coordinate: Clutter.BindCoordinate.POSITION,
|
|
||||||
}));
|
|
||||||
this._closeButton.add_constraint(new Clutter.AlignConstraint({
|
|
||||||
source: this._borderCenter,
|
|
||||||
align_axis: Clutter.AlignAxis.X_AXIS,
|
|
||||||
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
|
|
||||||
factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
|
|
||||||
}));
|
|
||||||
this._closeButton.add_constraint(new Clutter.AlignConstraint({
|
|
||||||
source: this._borderCenter,
|
|
||||||
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._borderCenter);
|
|
||||||
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;
|
|
||||||
this._borderCenterConstraint.offset = this._borderSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
_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);
|
|
||||||
|
|
||||||
const topOversize = (this._borderSize / 2) + (closeButtonHeight / 2);
|
|
||||||
const bottomOversize = Math.max(
|
|
||||||
this._borderSize,
|
|
||||||
(titleHeight / 2) + (this._borderSize / 2));
|
|
||||||
|
|
||||||
return [topOversize, bottomOversize];
|
|
||||||
}
|
|
||||||
|
|
||||||
chromeWidths() {
|
|
||||||
this._border.ensure_style();
|
|
||||||
const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
|
|
||||||
|
|
||||||
const leftOversize = this._closeButtonSide === St.Side.LEFT
|
|
||||||
? (this._borderSize / 2) + (closeButtonWidth / 2)
|
|
||||||
: this._borderSize;
|
|
||||||
const rightOversize = this._closeButtonSide === St.Side.LEFT
|
|
||||||
? this._borderSize
|
|
||||||
: (this._borderSize / 2) + (closeButtonWidth / 2);
|
|
||||||
|
|
||||||
return [leftOversize, rightOversize];
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit('show-chrome');
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const clone = this._windowContainer.layout_manager.addWindow(metaWindow);
|
|
||||||
|
|
||||||
// We expect this to be used for all interaction rather than
|
|
||||||
// the ClutterClone; as the former is reactive and the latter
|
|
||||||
// is not, this just works for most cases. However, for DND all
|
|
||||||
// actors are picked, so DND operations would operate on the clone.
|
|
||||||
// To avoid this, we hide it from pick.
|
|
||||||
Shell.util_set_hidden_from_pick(clone, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_has_overlaps() {
|
|
||||||
return this._hasAttachedDialogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
_deleteAll() {
|
|
||||||
const windows = this._windowContainer.layout_manager.getWindows();
|
|
||||||
|
|
||||||
// Delete all windows, starting from the bottom-most (most-modal) one
|
|
||||||
for (const window of windows.reverse())
|
|
||||||
window.delete(global.get_current_time());
|
|
||||||
|
|
||||||
this._closeRequested = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
addDialog(win) {
|
|
||||||
let parent = win.get_transient_for();
|
|
||||||
while (parent.is_attached_dialog())
|
|
||||||
parent = parent.get_transient_for();
|
|
||||||
|
|
||||||
// Display dialog if it is attached to our metaWindow
|
|
||||||
if (win.is_attached_dialog() && parent == this.metaWindow)
|
|
||||||
this._addWindow(win);
|
|
||||||
|
|
||||||
// The dialog popped up after the user tried to close the window,
|
|
||||||
// assume it's a close confirmation and leave the overview
|
|
||||||
if (this._closeRequested)
|
|
||||||
this._activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasAttachedDialogs() {
|
|
||||||
return this._windowContainer.layout_manager.getWindows().length > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateAttachedDialogs() {
|
|
||||||
let iter = win => {
|
|
||||||
let actor = win.get_compositor_private();
|
|
||||||
|
|
||||||
if (!actor)
|
|
||||||
return false;
|
|
||||||
if (!win.is_attached_dialog())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
this._addWindow(win);
|
|
||||||
win.foreach_transient(iter);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
this.metaWindow.foreach_transient(iter);
|
|
||||||
}
|
|
||||||
|
|
||||||
get boundingBox() {
|
|
||||||
const box = this._windowContainer.layout_manager.bounding_box;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: box.x1,
|
|
||||||
y: box.y1,
|
|
||||||
width: box.get_width(),
|
|
||||||
height: box.get_height(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get windowCenter() {
|
|
||||||
const box = this._windowContainer.layout_manager.bounding_box;
|
|
||||||
|
|
||||||
return new Graphene.Point({
|
|
||||||
x: box.get_x() + box.get_width() / 2,
|
|
||||||
y: box.get_y() + box.get_height() / 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the actor just below us, respecting reparenting done by DND code
|
|
||||||
_getActualStackAbove() {
|
|
||||||
if (this._stackAbove == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (this.inDrag) {
|
|
||||||
if (this._stackAbove._delegate)
|
|
||||||
return this._stackAbove._delegate._getActualStackAbove();
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return this._stackAbove;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setStackAbove(actor) {
|
|
||||||
this._stackAbove = actor;
|
|
||||||
if (this.inDrag)
|
|
||||||
// We'll fix up the stack after the drag
|
|
||||||
return;
|
|
||||||
|
|
||||||
let parent = this.get_parent();
|
|
||||||
let actualAbove = this._getActualStackAbove();
|
|
||||||
if (actualAbove == null)
|
|
||||||
parent.set_child_below_sibling(this, null);
|
|
||||||
else
|
|
||||||
parent.set_child_above_sibling(this, actualAbove);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDestroy() {
|
|
||||||
this._windowActor.disconnect(this._windowDestroyId);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_activate() {
|
|
||||||
this._selected = true;
|
|
||||||
this.emit('selected', global.get_current_time());
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_enter_event(crossingEvent) {
|
|
||||||
this.showOverlay(true);
|
|
||||||
return super.vfunc_enter_event(crossingEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_leave_event(crossingEvent) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_key_focus_in() {
|
|
||||||
super.vfunc_key_focus_in();
|
|
||||||
this.showOverlay(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_key_focus_out() {
|
|
||||||
super.vfunc_key_focus_out();
|
|
||||||
this.hideOverlay(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
vfunc_key_press_event(keyEvent) {
|
|
||||||
let symbol = keyEvent.keyval;
|
|
||||||
let isEnter = symbol == Clutter.KEY_Return || symbol == Clutter.KEY_KP_Enter;
|
|
||||||
if (isEnter) {
|
|
||||||
this._activate();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.vfunc_key_press_event(keyEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onLongPress(action, actor, state) {
|
|
||||||
// Take advantage of the Clutter policy to consider
|
|
||||||
// a long-press canceled when the pointer movement
|
|
||||||
// exceeds dnd-drag-threshold to manually start the drag
|
|
||||||
if (state == Clutter.LongPressState.CANCEL) {
|
|
||||||
let event = Clutter.get_current_event();
|
|
||||||
this._dragTouchSequence = event.get_event_sequence();
|
|
||||||
|
|
||||||
if (this._longPressLater)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// A click cancels a long-press before any click handler is
|
|
||||||
// run - make sure to not start a drag in that case
|
|
||||||
this._longPressLater = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
|
|
||||||
delete this._longPressLater;
|
|
||||||
if (this._selected)
|
|
||||||
return;
|
|
||||||
let [x, y] = action.get_coords();
|
|
||||||
action.release();
|
|
||||||
this._draggable.startDrag(x, y, global.get_current_time(), this._dragTouchSequence, event.get_device());
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.showOverlay(true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragBegin(_draggable, _time) {
|
|
||||||
this.inDrag = true;
|
|
||||||
this.hideOverlay(false);
|
|
||||||
this.emit('drag-begin');
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDragOver(source, actor, x, y, time) {
|
|
||||||
return this._workspace.handleDragOver(source, actor, x, y, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptDrop(source, actor, x, y, time) {
|
|
||||||
return this._workspace.acceptDrop(source, actor, x, y, time);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragCancelled(_draggable, _time) {
|
|
||||||
this.emit('drag-cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDragEnd(_draggable, _time, _snapback) {
|
|
||||||
this.inDrag = false;
|
|
||||||
|
|
||||||
// We may not have a parent if DnD completed successfully, in
|
|
||||||
// which case our clone will shortly be destroyed and replaced
|
|
||||||
// with a new one on the target workspace.
|
|
||||||
let parent = this.get_parent();
|
|
||||||
if (parent !== null) {
|
|
||||||
if (this._stackAbove == null)
|
|
||||||
parent.set_child_below_sibling(this, null);
|
|
||||||
else
|
|
||||||
parent.set_child_above_sibling(this, this._stackAbove);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this['has-pointer'])
|
|
||||||
this.showOverlay(true);
|
|
||||||
|
|
||||||
this.emit('drag-end');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var WindowPositionFlags = {
|
var WindowPositionFlags = {
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
INITIAL: 1 << 0,
|
INITIAL: 1 << 0,
|
||||||
|
Loading…
Reference in New Issue
Block a user