gnome-shell/js/ui/modalDialog.js
Zander Brown 350cd296fa js: Stop using ClutterContainer API
These have been long deprecated over in clutter, and (via several
vtables) simply forward the call to the equivalent ClutterActor methods

Save ourselves the hassle and just use ClutterActor methods directly

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3010>
2023-11-10 20:19:13 +00:00

294 lines
8.5 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Dialog from './dialog.js';
import * as Layout from './layout.js';
import * as Lightbox from './lightbox.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
const OPEN_AND_CLOSE_TIME = 100;
const FADE_OUT_DIALOG_TIME = 1000;
/** @enum {number} */
export const State = {
OPENED: 0,
CLOSED: 1,
OPENING: 2,
CLOSING: 3,
FADED_OUT: 4,
};
export const 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_child(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_child(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_child(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(event) {
if (global.focus_manager.navigate_from_event(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),
});
}
});