2013-02-16 12:41:11 -05:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2019-01-31 09:07:06 -05:00
|
|
|
/* exported GrabHelper */
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2023-06-08 00:52:46 -04:00
|
|
|
const Clutter = imports.gi.Clutter;
|
|
|
|
const St = imports.gi.St;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
const Params = imports.misc.params;
|
|
|
|
|
|
|
|
// GrabHelper:
|
|
|
|
// @owner: the actor that owns the GrabHelper
|
2012-08-20 16:05:00 -04:00
|
|
|
// @params: optional parameters to pass to Main.pushModal()
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
|
|
|
// 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().
|
2017-10-30 21:19:44 -04:00
|
|
|
var GrabHelper = class GrabHelper {
|
|
|
|
constructor(owner, params) {
|
2019-05-29 15:43:27 -04:00
|
|
|
if (!(owner instanceof Clutter.Actor))
|
|
|
|
throw new Error('GrabHelper owner must be a Clutter.Actor');
|
|
|
|
|
2012-02-28 12:16:12 -05:00
|
|
|
this._owner = owner;
|
2012-08-20 16:05:00 -04:00
|
|
|
this._modalParams = params;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
this._grabStack = [];
|
|
|
|
|
2014-08-28 13:43:05 -04:00
|
|
|
this._ignoreUntilRelease = false;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
this._modalCount = 0;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
_isWithinGrabbedActor(actor) {
|
2013-02-16 23:27:23 -05:00
|
|
|
let currentActor = this.currentGrab.actor;
|
2012-02-28 12:16:12 -05:00
|
|
|
while (actor) {
|
2012-11-26 14:47:59 -05:00
|
|
|
if (actor == currentActor)
|
|
|
|
return true;
|
2012-02-28 12:16:12 -05:00
|
|
|
actor = actor.get_parent();
|
|
|
|
}
|
|
|
|
return false;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
get currentGrab() {
|
2012-09-15 13:14:52 -04:00
|
|
|
return this._grabStack[this._grabStack.length - 1] || {};
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2012-02-29 19:09:41 -05:00
|
|
|
get grabbed() {
|
|
|
|
return this._grabStack.length > 0;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-29 19:09:41 -05:00
|
|
|
|
|
|
|
get grabStack() {
|
|
|
|
return this._grabStack;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-29 19:09:41 -05:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
_findStackIndex(actor) {
|
2012-02-28 12:16:12 -05:00
|
|
|
if (!actor)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
for (let i = 0; i < this._grabStack.length; i++) {
|
|
|
|
if (this._grabStack[i].actor === actor)
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return -1;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
_actorInGrabStack(actor) {
|
2012-11-26 14:55:03 -05:00
|
|
|
while (actor) {
|
|
|
|
let idx = this._findStackIndex(actor);
|
|
|
|
if (idx >= 0)
|
|
|
|
return idx;
|
|
|
|
actor = actor.get_parent();
|
|
|
|
}
|
|
|
|
return -1;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-11-26 14:55:03 -05:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
isActorGrabbed(actor) {
|
2012-02-28 12:16:12 -05:00
|
|
|
return this._findStackIndex(actor) >= 0;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
// grab:
|
|
|
|
// @params: A bunch of parameters, see below
|
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2017-10-30 20:03:21 -04:00
|
|
|
grab(params) {
|
2020-03-29 17:51:13 -04:00
|
|
|
params = Params.parse(params, {
|
|
|
|
actor: null,
|
|
|
|
focus: null,
|
|
|
|
onUngrab: null,
|
|
|
|
});
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
let focus = global.stage.key_focus;
|
|
|
|
let hadFocus = focus && this._isWithinGrabbedActor(focus);
|
2012-08-16 13:40:51 -04:00
|
|
|
let newFocus = params.actor;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
if (this.isActorGrabbed(params.actor))
|
2012-08-21 19:40:11 -04:00
|
|
|
return true;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
params.savedFocus = focus;
|
|
|
|
|
2013-05-23 17:20:30 -04:00
|
|
|
if (!this._takeModalGrab())
|
2012-09-18 22:04:57 -04:00
|
|
|
return false;
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2013-02-16 12:52:37 -05:00
|
|
|
this._grabStack.push(params);
|
|
|
|
|
2013-02-11 11:52:09 -05:00
|
|
|
if (params.focus) {
|
2012-02-29 19:09:41 -05:00
|
|
|
params.focus.grab_key_focus();
|
2013-05-23 17:11:40 -04:00
|
|
|
} else if (newFocus && hadFocus) {
|
2018-11-27 07:58:25 -05:00
|
|
|
if (!newFocus.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
|
2013-02-11 11:52:09 -05:00
|
|
|
newFocus.grab_key_focus();
|
|
|
|
}
|
2012-08-21 19:40:11 -04:00
|
|
|
|
|
|
|
return true;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2019-12-18 21:28:50 -05:00
|
|
|
grabAsync(params) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
params.onUngrab = resolve;
|
|
|
|
|
|
|
|
if (!this.grab(params))
|
|
|
|
reject(new Error('Grab failed'));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
_takeModalGrab() {
|
2019-08-19 15:38:51 -04:00
|
|
|
let firstGrab = this._modalCount == 0;
|
2012-09-18 22:04:57 -04:00
|
|
|
if (firstGrab) {
|
2021-11-25 04:49:42 -05:00
|
|
|
let grab = Main.pushModal(this._owner, this._modalParams);
|
2022-04-30 02:18:39 -04:00
|
|
|
if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
|
2021-11-25 04:49:42 -05:00
|
|
|
Main.popModal(grab);
|
2012-09-18 22:04:57 -04:00
|
|
|
return false;
|
2021-11-25 04:49:42 -05:00
|
|
|
}
|
2013-05-23 17:20:30 -04:00
|
|
|
|
2021-11-25 04:49:42 -05:00
|
|
|
this._grab = grab;
|
2021-11-17 18:56:44 -05:00
|
|
|
this._capturedEventId = this._owner.connect('captured-event',
|
|
|
|
(actor, event) => {
|
|
|
|
return this.onCapturedEvent(event);
|
|
|
|
});
|
2012-09-18 22:04:57 -04:00
|
|
|
}
|
2012-09-15 01:38:40 -04:00
|
|
|
|
2012-09-18 22:04:57 -04:00
|
|
|
this._modalCount++;
|
|
|
|
return true;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-09-15 01:38:40 -04:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
_releaseModalGrab() {
|
2012-09-15 01:38:40 -04:00
|
|
|
this._modalCount--;
|
|
|
|
if (this._modalCount > 0)
|
|
|
|
return;
|
|
|
|
|
2021-11-17 18:56:44 -05:00
|
|
|
this._owner.disconnect(this._capturedEventId);
|
2014-08-28 13:43:05 -04:00
|
|
|
this._ignoreUntilRelease = false;
|
2013-05-23 17:20:30 -04:00
|
|
|
|
2021-11-25 04:49:42 -05:00
|
|
|
Main.popModal(this._grab);
|
|
|
|
this._grab = null;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-09-15 01:38:40 -04:00
|
|
|
|
2012-02-28 12:16:12 -05:00
|
|
|
// 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.
|
2017-10-30 20:03:21 -04:00
|
|
|
ignoreRelease() {
|
2014-08-28 13:43:05 -04:00
|
|
|
this._ignoreUntilRelease = true;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
// ungrab:
|
|
|
|
// @params: The parameters for the grab; see below.
|
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2012-02-28 12:16:12 -05:00
|
|
|
//
|
2013-05-23 14:52:43 -04:00
|
|
|
// 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.
|
2017-10-30 20:03:21 -04:00
|
|
|
ungrab(params) {
|
2020-03-29 17:51:13 -04:00
|
|
|
params = Params.parse(params, {
|
|
|
|
actor: this.currentGrab.actor,
|
|
|
|
isUser: false,
|
|
|
|
});
|
2012-02-28 12:16:12 -05:00
|
|
|
|
|
|
|
let grabStackIndex = this._findStackIndex(params.actor);
|
|
|
|
if (grabStackIndex < 0)
|
|
|
|
return;
|
|
|
|
|
2012-09-15 01:38:40 -04:00
|
|
|
let focus = global.stage.key_focus;
|
|
|
|
let hadFocus = focus && this._isWithinGrabbedActor(focus);
|
|
|
|
|
2012-08-16 13:48:22 -04:00
|
|
|
let poppedGrabs = this._grabStack.slice(grabStackIndex);
|
2012-08-19 21:12:46 -04:00
|
|
|
// "Pop" all newly ungrabbed actors off the grab stack
|
|
|
|
// by truncating the array.
|
2012-02-28 12:16:12 -05:00
|
|
|
this._grabStack.length = grabStackIndex;
|
|
|
|
|
2012-08-19 21:12:46 -04:00
|
|
|
for (let i = poppedGrabs.length - 1; i >= 0; i--) {
|
|
|
|
let poppedGrab = poppedGrabs[i];
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2012-08-19 21:12:46 -04:00
|
|
|
if (poppedGrab.onUngrab)
|
2012-12-10 03:55:14 -05:00
|
|
|
poppedGrab.onUngrab(params.isUser);
|
2012-08-19 21:12:46 -04:00
|
|
|
|
2013-05-23 17:20:30 -04:00
|
|
|
this._releaseModalGrab();
|
2012-12-30 12:32:41 -05:00
|
|
|
}
|
|
|
|
|
2012-09-15 01:38:40 -04:00
|
|
|
if (hadFocus) {
|
|
|
|
let poppedGrab = poppedGrabs[0];
|
2013-02-09 17:05:43 -05:00
|
|
|
if (poppedGrab.savedFocus)
|
|
|
|
poppedGrab.savedFocus.grab_key_focus();
|
2012-02-28 12:16:12 -05:00
|
|
|
}
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
2012-02-28 12:16:12 -05:00
|
|
|
|
2017-10-30 20:03:21 -04:00
|
|
|
onCapturedEvent(event) {
|
2012-02-28 12:16:12 -05:00
|
|
|
let type = event.type();
|
2012-12-30 12:32:41 -05:00
|
|
|
|
|
|
|
if (type == Clutter.EventType.KEY_PRESS &&
|
|
|
|
event.get_key_symbol() == Clutter.KEY_Escape) {
|
|
|
|
this.ungrab({ isUser: true });
|
2013-11-29 13:17:34 -05:00
|
|
|
return Clutter.EVENT_STOP;
|
2012-12-30 12:32:41 -05:00
|
|
|
}
|
|
|
|
|
2014-07-22 06:50:54 -04:00
|
|
|
let motion = type == Clutter.EventType.MOTION;
|
2012-02-28 12:16:12 -05:00
|
|
|
let press = type == Clutter.EventType.BUTTON_PRESS;
|
|
|
|
let release = type == Clutter.EventType.BUTTON_RELEASE;
|
|
|
|
let button = press || release;
|
|
|
|
|
2014-07-24 12:31:51 -04:00
|
|
|
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;
|
|
|
|
|
2019-08-19 13:55:49 -04:00
|
|
|
if (touch && !global.display.is_pointer_emulating_sequence(event.get_event_sequence()))
|
2014-07-24 12:31:51 -04:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
|
|
|
|
if (this._ignoreUntilRelease && (motion || release || touch)) {
|
|
|
|
if (release || touchEnd)
|
2014-07-22 06:50:54 -04:00
|
|
|
this._ignoreUntilRelease = false;
|
2021-03-18 14:00:13 -04:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2012-02-28 12:16:12 -05:00
|
|
|
}
|
|
|
|
|
2022-02-25 07:07:49 -05:00
|
|
|
const targetActor = global.stage.get_event_actor(event);
|
|
|
|
|
2022-01-31 06:16:47 -05:00
|
|
|
if (type === Clutter.EventType.ENTER ||
|
|
|
|
type === Clutter.EventType.LEAVE ||
|
2022-02-25 07:07:49 -05:00
|
|
|
this.currentGrab.actor.contains(targetActor))
|
2022-01-31 06:16:47 -05:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
|
2022-03-09 17:11:03 -05:00
|
|
|
if (Main.keyboard.maybeHandleEvent(event))
|
2013-11-29 13:17:34 -05:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2012-09-21 10:58:03 -04:00
|
|
|
|
2014-07-24 12:31:51 -04:00
|
|
|
if (button || touchBegin) {
|
2014-07-22 06:50:54 -04:00
|
|
|
// If we have a press event, ignore the next
|
|
|
|
// motion/release events.
|
2014-07-24 12:31:51 -04:00
|
|
|
if (press || touchBegin)
|
2014-07-22 06:50:54 -04:00
|
|
|
this._ignoreUntilRelease = true;
|
|
|
|
|
2022-02-25 07:07:49 -05:00
|
|
|
let i = this._actorInGrabStack(targetActor) + 1;
|
2012-12-10 03:55:14 -05:00
|
|
|
this.ungrab({ actor: this._grabStack[i].actor, isUser: true });
|
2013-11-29 13:17:34 -05:00
|
|
|
return Clutter.EVENT_STOP;
|
2012-02-28 12:16:12 -05:00
|
|
|
}
|
|
|
|
|
2013-11-29 13:17:34 -05:00
|
|
|
return Clutter.EVENT_STOP;
|
2017-10-30 21:19:44 -04:00
|
|
|
}
|
|
|
|
};
|