gnome-shell/js/ui/modalDialog.js
Florian Müllner 5766d4111a modalDialog: Fix fading out dialog
The dialog's state property has been read-only since
commit 2f6323afc, but the callback at the end of the
fade transition still tries to set the value directly.

https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6506

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2696>
2023-03-19 19:54:45 +00:00

289 lines
8.5 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ModalDialog */
const { Atk, Clutter, GObject, Shell, St } = imports.gi;
const Dialog = imports.ui.dialog;
const Layout = imports.ui.layout;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const Params = imports.misc.params;
var OPEN_AND_CLOSE_TIME = 100;
var FADE_OUT_DIALOG_TIME = 1000;
var State = {
OPENED: 0,
CLOSED: 1,
OPENING: 2,
CLOSING: 3,
FADED_OUT: 4,
};
var ModalDialog = GObject.registerClass({
Properties: {
'state': GObject.ParamSpec.int('state', 'Dialog state', 'state',
GObject.ParamFlags.READABLE,
Math.min(...Object.values(State)),
Math.max(...Object.values(State)),
State.CLOSED),
},
Signals: { 'opened': {}, 'closed': {} },
}, class ModalDialog extends St.Widget {
_init(params) {
super._init({
visible: false,
reactive: true,
x: 0,
y: 0,
accessible_role: Atk.Role.DIALOG,
});
params = Params.parse(params, {
shellReactive: false,
styleClass: null,
actionMode: Shell.ActionMode.SYSTEM_MODAL,
shouldFadeIn: true,
shouldFadeOut: true,
destroyOnClose: true,
});
this._state = State.CLOSED;
this._hasModal = false;
this._actionMode = params.actionMode;
this._shellReactive = params.shellReactive;
this._shouldFadeIn = params.shouldFadeIn;
this._shouldFadeOut = params.shouldFadeOut;
this._destroyOnClose = params.destroyOnClose;
Main.layoutManager.modalDialogGroup.add_actor(this);
const constraint = new Clutter.BindConstraint({
source: global.stage,
coordinate: Clutter.BindCoordinate.ALL,
});
this.add_constraint(constraint);
this.backgroundStack = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true,
y_expand: true,
});
this._backgroundBin = new St.Bin({ child: this.backgroundStack });
this._monitorConstraint = new Layout.MonitorConstraint();
this._backgroundBin.add_constraint(this._monitorConstraint);
this.add_actor(this._backgroundBin);
this.dialogLayout = new Dialog.Dialog(this.backgroundStack, params.styleClass);
this.contentLayout = this.dialogLayout.contentLayout;
this.buttonLayout = this.dialogLayout.buttonLayout;
if (!this._shellReactive) {
this._lightbox = new Lightbox.Lightbox(this, {
inhibitEvents: true,
radialEffect: true,
});
this._lightbox.highlight(this._backgroundBin);
this._eventBlocker = new Clutter.Actor({ reactive: true });
this.backgroundStack.add_actor(this._eventBlocker);
}
global.focus_manager.add_group(this.dialogLayout);
this._initialKeyFocus = null;
this._initialKeyFocusDestroyId = 0;
this._savedKeyFocus = null;
}
get state() {
return this._state;
}
_setState(state) {
if (this._state == state)
return;
this._state = state;
this.notify('state');
}
vfunc_key_press_event() {
if (global.focus_manager.navigate_from_event(Clutter.get_current_event()))
return Clutter.EVENT_STOP;
return Clutter.EVENT_PROPAGATE;
}
vfunc_captured_event(event) {
if (Main.keyboard.maybeHandleEvent(event))
return Clutter.EVENT_STOP;
return Clutter.EVENT_PROPAGATE;
}
clearButtons() {
this.dialogLayout.clearButtons();
}
setButtons(buttons) {
this.clearButtons();
for (let buttonInfo of buttons)
this.addButton(buttonInfo);
}
addButton(buttonInfo) {
return this.dialogLayout.addButton(buttonInfo);
}
_fadeOpen(onPrimary) {
if (onPrimary)
this._monitorConstraint.primary = true;
else
this._monitorConstraint.index = global.display.get_current_monitor();
this._setState(State.OPENING);
this.dialogLayout.opacity = 255;
if (this._lightbox)
this._lightbox.lightOn();
this.opacity = 0;
this.show();
this.ease({
opacity: 255,
duration: this._shouldFadeIn ? OPEN_AND_CLOSE_TIME : 0,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this._setState(State.OPENED);
this.emit('opened');
},
});
}
setInitialKeyFocus(actor) {
this._initialKeyFocus?.disconnectObject(this);
this._initialKeyFocus = actor;
actor.connectObject('destroy',
() => (this._initialKeyFocus = null), this);
}
open(timestamp, onPrimary) {
if (this.state == State.OPENED || this.state == State.OPENING)
return true;
if (!this.pushModal(timestamp))
return false;
this._fadeOpen(onPrimary);
return true;
}
_closeComplete() {
this._setState(State.CLOSED);
this.hide();
this.emit('closed');
if (this._destroyOnClose)
this.destroy();
}
close(timestamp) {
if (this.state == State.CLOSED || this.state == State.CLOSING)
return;
this._setState(State.CLOSING);
this.popModal(timestamp);
this._savedKeyFocus = null;
if (this._shouldFadeOut) {
this.ease({
opacity: 0,
duration: OPEN_AND_CLOSE_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._closeComplete(),
});
} else {
this._closeComplete();
}
}
// Drop modal status without closing the dialog; this makes the
// dialog insensitive as well, so it needs to be followed shortly
// by either a close() or a pushModal()
popModal(timestamp) {
if (!this._hasModal)
return;
let focus = global.stage.key_focus;
if (focus && this.contains(focus))
this._savedKeyFocus = focus;
else
this._savedKeyFocus = null;
Main.popModal(this._grab, timestamp);
this._grab = null;
this._hasModal = false;
if (!this._shellReactive)
this.backgroundStack.set_child_above_sibling(this._eventBlocker, null);
}
pushModal(timestamp) {
if (this._hasModal)
return true;
let params = { actionMode: this._actionMode };
if (timestamp)
params['timestamp'] = timestamp;
let grab = Main.pushModal(this, params);
if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
Main.popModal(grab);
return false;
}
this._grab = grab;
Main.layoutManager.emit('system-modal-opened');
this._hasModal = true;
if (this._savedKeyFocus) {
this._savedKeyFocus.grab_key_focus();
this._savedKeyFocus = null;
} else {
let focus = this._initialKeyFocus || this.dialogLayout.initialKeyFocus;
focus.grab_key_focus();
}
if (!this._shellReactive)
this.backgroundStack.set_child_below_sibling(this._eventBlocker, null);
return true;
}
// This method is like close, but fades the dialog out much slower,
// and leaves the lightbox in place. Once in the faded out state,
// the dialog can be brought back by an open call, or the lightbox
// can be dismissed by a close call.
//
// The main point of this method is to give some indication to the user
// that the dialog response has been acknowledged but will take a few
// moments before being processed.
// e.g., if a user clicked "Log Out" then the dialog should go away
// immediately, but the lightbox should remain until the logout is
// complete.
_fadeOutDialog(timestamp) {
if (this.state == State.CLOSED || this.state == State.CLOSING)
return;
if (this.state == State.FADED_OUT)
return;
this.popModal(timestamp);
this.dialogLayout.ease({
opacity: 0,
duration: FADE_OUT_DIALOG_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._setState(State.FADED_OUT),
});
}
});