// -*- 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_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), }); } });