summaryrefslogtreecommitdiffstats
path: root/js/ui/grabHelper.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/grabHelper.js')
-rw-r--r--js/ui/grabHelper.js291
1 files changed, 291 insertions, 0 deletions
diff --git a/js/ui/grabHelper.js b/js/ui/grabHelper.js
new file mode 100644
index 0000000..650bec4
--- /dev/null
+++ b/js/ui/grabHelper.js
@@ -0,0 +1,291 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported GrabHelper */
+
+const { Clutter, St } = imports.gi;
+
+const Main = imports.ui.main;
+const Params = imports.misc.params;
+
+// GrabHelper:
+// @owner: the actor that owns the GrabHelper
+// @params: optional parameters to pass to Main.pushModal()
+//
+// 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().
+var GrabHelper = class GrabHelper {
+ 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;
+ }
+};