gnome-shell/js/ui/grabHelper.js

296 lines
9.2 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import St from 'gi://St';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
/**
* GrabHelper:
*
* Creates a new GrabHelper object, for dealing with keyboard and pointer grabs
* associated with a set of actors.
*
* Note that the grab can be automatically dropped at any time by the user, and
* your code just needs to deal with it; you shouldn't adjust behavior directly
* after you call ungrab(), but instead pass an 'onUngrab' callback when you
* call grab().
*/
export class GrabHelper {
/**
* @param {Clutter.Actor} owner the actor that owns the GrabHelper
* @param {*} params optional parameters to pass to Main.pushModal()
*/
constructor(owner, params) {
if (!(owner instanceof Clutter.Actor))
throw new Error('GrabHelper owner must be a Clutter.Actor');
this._owner = owner;
this._modalParams = params;
this._grabStack = [];
this._ignoreUntilRelease = false;
this._modalCount = 0;
}
_isWithinGrabbedActor(actor) {
let currentActor = this.currentGrab.actor;
while (actor) {
if (actor == currentActor)
return true;
actor = actor.get_parent();
}
return false;
}
get currentGrab() {
return this._grabStack[this._grabStack.length - 1] || {};
}
get grabbed() {
return this._grabStack.length > 0;
}
get grabStack() {
return this._grabStack;
}
_findStackIndex(actor) {
if (!actor)
return -1;
for (let i = 0; i < this._grabStack.length; i++) {
if (this._grabStack[i].actor === actor)
return i;
}
return -1;
}
_actorInGrabStack(actor) {
while (actor) {
let idx = this._findStackIndex(actor);
if (idx >= 0)
return idx;
actor = actor.get_parent();
}
return -1;
}
isActorGrabbed(actor) {
return this._findStackIndex(actor) >= 0;
}
// grab:
// @params: A bunch of parameters, see below
//
// The general effect of a "grab" is to ensure that the passed in actor
// and all actors inside the grab get exclusive control of the mouse and
// keyboard, with the grab automatically being dropped if the user tries
// to dismiss it. The actor is passed in through @params.actor.
//
// grab() can be called multiple times, with the scope of the grab being
// changed to a different actor every time. A nested grab does not have
// to have its grabbed actor inside the parent grab actors.
//
// Grabs can be automatically dropped if the user tries to dismiss it
// in one of two ways: the user clicking outside the currently grabbed
// actor, or the user typing the Escape key.
//
// If the user clicks outside the grabbed actors, and the clicked on
// actor is part of a previous grab in the stack, grabs will be popped
// until that grab is active. However, the click event will not be
// replayed to the actor.
//
// If the user types the Escape key, one grab from the grab stack will
// be popped.
//
// When a grab is popped by user interacting as described above, if you
// pass a callback as @params.onUngrab, it will be called with %true.
//
// If @params.focus is not null, we'll set the key focus directly
// to that actor instead of navigating in @params.actor. This is for
// use cases like menus, where we want to grab the menu actor, but keep
// focus on the clicked on menu item.
grab(params) {
params = Params.parse(params, {
actor: null,
focus: null,
onUngrab: null,
});
let focus = global.stage.key_focus;
let hadFocus = focus && this._isWithinGrabbedActor(focus);
let newFocus = params.actor;
if (this.isActorGrabbed(params.actor))
return true;
params.savedFocus = focus;
if (!this._takeModalGrab())
return false;
this._grabStack.push(params);
if (params.focus) {
params.focus.grab_key_focus();
} else if (newFocus && hadFocus) {
if (!newFocus.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
newFocus.grab_key_focus();
}
return true;
}
grabAsync(params) {
return new Promise((resolve, reject) => {
params.onUngrab = resolve;
if (!this.grab(params))
reject(new Error('Grab failed'));
});
}
_takeModalGrab() {
let firstGrab = this._modalCount == 0;
if (firstGrab) {
let grab = Main.pushModal(this._owner, this._modalParams);
if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
Main.popModal(grab);
return false;
}
this._grab = grab;
this._capturedEventId = this._owner.connect('captured-event',
(actor, event) => {
return this.onCapturedEvent(event);
});
}
this._modalCount++;
return true;
}
_releaseModalGrab() {
this._modalCount--;
if (this._modalCount > 0)
return;
this._owner.disconnect(this._capturedEventId);
this._ignoreUntilRelease = false;
Main.popModal(this._grab);
this._grab = null;
}
// ignoreRelease:
//
// Make sure that the next button release event evaluated by the
// capture event handler returns false. This is designed for things
// like the ComboBoxMenu that go away on press, but need to eat
// the next release event.
ignoreRelease() {
this._ignoreUntilRelease = true;
}
// ungrab:
// @params: The parameters for the grab; see below.
//
// Pops @params.actor from the grab stack, potentially dropping
// the grab. If the actor is not on the grab stack, this call is
// ignored with no ill effects.
//
// If the actor is not at the top of the grab stack, grabs are
// popped until the grabbed actor is at the top of the grab stack.
// The onUngrab callback for every grab is called for every popped
// grab with the parameter %false.
ungrab(params) {
params = Params.parse(params, {
actor: this.currentGrab.actor,
isUser: false,
});
let grabStackIndex = this._findStackIndex(params.actor);
if (grabStackIndex < 0)
return;
let focus = global.stage.key_focus;
let hadFocus = focus && this._isWithinGrabbedActor(focus);
let poppedGrabs = this._grabStack.slice(grabStackIndex);
// "Pop" all newly ungrabbed actors off the grab stack
// by truncating the array.
this._grabStack.length = grabStackIndex;
for (let i = poppedGrabs.length - 1; i >= 0; i--) {
let poppedGrab = poppedGrabs[i];
if (poppedGrab.onUngrab)
poppedGrab.onUngrab(params.isUser);
this._releaseModalGrab();
}
if (hadFocus) {
let poppedGrab = poppedGrabs[0];
if (poppedGrab.savedFocus)
poppedGrab.savedFocus.grab_key_focus();
}
}
onCapturedEvent(event) {
let type = event.type();
if (type == Clutter.EventType.KEY_PRESS &&
event.get_key_symbol() == Clutter.KEY_Escape) {
this.ungrab({ isUser: true });
return Clutter.EVENT_STOP;
}
let motion = type == Clutter.EventType.MOTION;
let press = type == Clutter.EventType.BUTTON_PRESS;
let release = type == Clutter.EventType.BUTTON_RELEASE;
let button = press || release;
let touchUpdate = type == Clutter.EventType.TOUCH_UPDATE;
let touchBegin = type == Clutter.EventType.TOUCH_BEGIN;
let touchEnd = type == Clutter.EventType.TOUCH_END;
let touch = touchUpdate || touchBegin || touchEnd;
if (touch && !global.display.is_pointer_emulating_sequence(event.get_event_sequence()))
return Clutter.EVENT_PROPAGATE;
if (this._ignoreUntilRelease && (motion || release || touch)) {
if (release || touchEnd)
this._ignoreUntilRelease = false;
return Clutter.EVENT_PROPAGATE;
}
const targetActor = global.stage.get_event_actor(event);
if (type === Clutter.EventType.ENTER ||
type === Clutter.EventType.LEAVE ||
this.currentGrab.actor.contains(targetActor))
return Clutter.EVENT_PROPAGATE;
if (Main.keyboard.maybeHandleEvent(event))
return Clutter.EVENT_PROPAGATE;
if (button || touchBegin) {
// If we have a press event, ignore the next
// motion/release events.
if (press || touchBegin)
this._ignoreUntilRelease = true;
let i = this._actorInGrabStack(targetActor) + 1;
this.ungrab({ actor: this._grabStack[i].actor, isUser: true });
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_STOP;
}
}