2e63709450
GrabHelper automatically releases grabs when the user clicks outside the grabbed actors. However at least for the message-tray (which is the only user of grabHelper at the moment), we must ignore any events from the On-Screen-Keyboard, to prevent the tray from hiding at every key press. https://bugzilla.gnome.org/show_bug.cgi?id=683546
359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
|
|
const Main = imports.ui.main;
|
|
const Params = imports.misc.params;
|
|
|
|
function _navigateActor(actor) {
|
|
if (!actor)
|
|
return;
|
|
|
|
let needsGrab = true;
|
|
if (actor instanceof St.Widget)
|
|
needsGrab = !actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
|
|
if (needsGrab)
|
|
actor.grab_key_focus();
|
|
}
|
|
|
|
// GrabHelper:
|
|
// @owner: the actor that owns the 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().
|
|
const GrabHelper = new Lang.Class({
|
|
Name: 'GrabHelper',
|
|
|
|
_init: function(owner) {
|
|
this._owner = owner;
|
|
|
|
this._grabStack = [];
|
|
|
|
this._actors = [];
|
|
this._capturedEventId = 0;
|
|
this._eventId = 0;
|
|
this._keyFocusNotifyId = 0;
|
|
this._focusWindowChangedId = 0;
|
|
this._ignoreRelease = false;
|
|
|
|
this._modalCount = 0;
|
|
this._grabFocusCount = 0;
|
|
},
|
|
|
|
// addActor:
|
|
// @actor: an actor
|
|
//
|
|
// Adds @actor to the set of actors that are allowed to process events
|
|
// during a grab.
|
|
addActor: function(actor) {
|
|
actor.__grabHelperDestroyId = actor.connect('destroy', Lang.bind(this, function() { this.removeActor(actor); }));
|
|
this._actors.push(actor);
|
|
},
|
|
|
|
// removeActor:
|
|
// @actor: an actor
|
|
//
|
|
// Removes @actor from the set of actors that are allowed to
|
|
// process events during a grab.
|
|
removeActor: function(actor) {
|
|
let index = this._actors.indexOf(actor);
|
|
if (index != -1)
|
|
this._actors.splice(index, 1);
|
|
if (actor.__grabHelperDestroyId) {
|
|
actor.disconnect(actor.__grabHelperDestroyId);
|
|
delete actor.__grabHelperDestroyId;
|
|
}
|
|
},
|
|
|
|
_isWithinGrabbedActor: function(actor) {
|
|
while (actor) {
|
|
if (this._actors.indexOf(actor) != -1)
|
|
return true;
|
|
actor = actor.get_parent();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
get currentGrab() {
|
|
return this._grabStack[this._grabStack.length - 1] || {};
|
|
},
|
|
|
|
_findStackIndex: function(actor) {
|
|
if (!actor)
|
|
return -1;
|
|
|
|
for (let i = 0; i < this._grabStack.length; i++) {
|
|
if (this._grabStack[i].actor === actor)
|
|
return i;
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
isActorGrabbed: function(actor) {
|
|
return this._findStackIndex(actor) >= 0;
|
|
},
|
|
|
|
// grab:
|
|
// @params: A bunch of parameters, see below
|
|
//
|
|
// Grabs the mouse and keyboard, according to the GrabHelper's
|
|
// parameters. If @newFocus is not %null, then the keyboard focus
|
|
// is moved to the first #StWidget:can-focus widget inside it.
|
|
//
|
|
// The grab will automatically be dropped if:
|
|
// - The user clicks outside the grabbed actors
|
|
// - The user types Escape
|
|
// - The keyboard focus is moved outside the grabbed actors
|
|
// - A window is focused
|
|
//
|
|
// If @params.actor is not null, then it will be focused as the
|
|
// new actor. If you attempt to grab an already focused actor, the
|
|
// request to be focused will be ignored. The actor will not be
|
|
// added to the grab stack, so do not call a paired ungrab().
|
|
//
|
|
// If @params contains { modal: true }, then grab() will push a modal
|
|
// on the owner of the GrabHelper. As long as there is at least one
|
|
// { modal: true } actor on the grab stack, the grab will be kept.
|
|
// When the last { modal: true } actor is ungrabbed, then the modal
|
|
// will be dropped. A modal grab can fail if there is already a grab
|
|
// in effect from aother application; in this case the function returns
|
|
// false and nothing happens. Non-modal grabs can never fail.
|
|
//
|
|
// If @params contains { grabFocus: true }, then if you call grab()
|
|
// while the shell is outside the overview, it will set the stage
|
|
// input mode to %Shell.StageInputMode.FOCUSED, and ungrab() will
|
|
// revert it back, and re-focus the previously-focused window (if
|
|
// another window hasn't been explicitly focused before then).
|
|
grab: function(params) {
|
|
params = Params.parse(params, { actor: null,
|
|
modal: false,
|
|
grabFocus: false,
|
|
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 (params.modal && !this._takeModalGrab())
|
|
return false;
|
|
|
|
if (params.grabFocus && !this._takeFocusGrab(hadFocus))
|
|
return false;
|
|
|
|
if (hadFocus || params.grabFocus)
|
|
_navigateActor(newFocus);
|
|
|
|
this._grabStack.push(params);
|
|
return true;
|
|
},
|
|
|
|
_takeModalGrab: function() {
|
|
let firstGrab = (this._modalCount == 0);
|
|
if (firstGrab) {
|
|
if (!Main.pushModal(this._owner))
|
|
return false;
|
|
|
|
this._capturedEventId = global.stage.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
|
|
this._eventId = global.stage.connect('event', Lang.bind(this, this._onEvent));
|
|
}
|
|
|
|
this._modalCount++;
|
|
return true;
|
|
},
|
|
|
|
_releaseModalGrab: function() {
|
|
this._modalCount--;
|
|
if (this._modalCount > 0)
|
|
return;
|
|
|
|
if (this._capturedEventId > 0) {
|
|
global.stage.disconnect(this._capturedEventId);
|
|
this._capturedEventId = 0;
|
|
}
|
|
|
|
if (this._eventId > 0) {
|
|
global.stage.disconnect(this._eventId);
|
|
this._eventId = 0;
|
|
}
|
|
|
|
Main.popModal(this._owner);
|
|
global.sync_pointer();
|
|
},
|
|
|
|
_takeFocusGrab: function(hadFocus) {
|
|
let firstGrab = (this._grabFocusCount == 0);
|
|
if (firstGrab) {
|
|
let metaDisplay = global.screen.get_display();
|
|
|
|
this._grabbedFromKeynav = hadFocus;
|
|
this._preGrabInputMode = global.stage_input_mode;
|
|
this._prevFocusedWindow = metaDisplay.focus_window;
|
|
|
|
if (this._preGrabInputMode == Shell.StageInputMode.NONREACTIVE ||
|
|
this._preGrabInputMode == Shell.StageInputMode.NORMAL) {
|
|
global.set_stage_input_mode(Shell.StageInputMode.FOCUSED);
|
|
}
|
|
|
|
this._keyFocusNotifyId = global.stage.connect('notify::key-focus', Lang.bind(this, this._onKeyFocusChanged));
|
|
this._focusWindowChangedId = metaDisplay.connect('notify::focus-window', Lang.bind(this, this._focusWindowChanged));
|
|
}
|
|
|
|
this._grabFocusCount++;
|
|
return true;
|
|
},
|
|
|
|
_releaseFocusGrab: function() {
|
|
this._grabFocusCount--;
|
|
if (this._grabFocusCount > 0)
|
|
return;
|
|
|
|
if (this._keyFocusNotifyId > 0) {
|
|
global.stage.disconnect(this._keyFocusNotifyId);
|
|
this._keyFocusNotifyId = 0;
|
|
}
|
|
|
|
if (!this._focusWindowChanged > 0) {
|
|
let metaDisplay = global.screen.get_display();
|
|
metaDisplay.disconnect(this._focusWindowChangedId);
|
|
this._focusWindowChangedId = 0;
|
|
}
|
|
|
|
let prePopInputMode = global.stage_input_mode;
|
|
|
|
if (this._grabbedFromKeynav) {
|
|
if (this._preGrabInputMode == Shell.StageInputMode.FOCUSED &&
|
|
prePopInputMode != Shell.StageInputMode.FULLSCREEN)
|
|
global.set_stage_input_mode(Shell.StageInputMode.FOCUSED);
|
|
}
|
|
|
|
if (this._prevFocusedWindow) {
|
|
let metaDisplay = global.screen.get_display();
|
|
if (!metaDisplay.focus_window) {
|
|
metaDisplay.set_input_focus_window(this._prevFocusedWindow,
|
|
false, global.get_current_time());
|
|
}
|
|
}
|
|
},
|
|
|
|
// 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: function() {
|
|
this._ignoreRelease = true;
|
|
},
|
|
|
|
// ungrab:
|
|
// @params: The parameters for the grab; see below.
|
|
//
|
|
// Pops an actor from the grab stack, potentially dropping the grab.
|
|
//
|
|
// If the actor that was popped from the grab stack was not the actor
|
|
// That was passed in, this call is ignored.
|
|
ungrab: function(params) {
|
|
params = Params.parse(params, { actor: this.currentGrab.actor });
|
|
|
|
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();
|
|
|
|
if (poppedGrab.modal)
|
|
this._releaseModalGrab();
|
|
|
|
if (poppedGrab.grabFocus)
|
|
this._releaseFocusGrab();
|
|
}
|
|
|
|
if (hadFocus) {
|
|
let poppedGrab = poppedGrabs[0];
|
|
_navigateActor(poppedGrab.savedFocus);
|
|
}
|
|
},
|
|
|
|
_onCapturedEvent: function(actor, event) {
|
|
let type = event.type();
|
|
let press = type == Clutter.EventType.BUTTON_PRESS;
|
|
let release = type == Clutter.EventType.BUTTON_RELEASE;
|
|
let button = press || release;
|
|
|
|
if (release && this._ignoreRelease) {
|
|
this._ignoreRelease = false;
|
|
return false;
|
|
}
|
|
|
|
if (!button && this._modalCount == 0)
|
|
return false;
|
|
|
|
if (this._isWithinGrabbedActor(event.get_source()))
|
|
return false;
|
|
|
|
if (Main.keyboard.shouldTakeEvent(event))
|
|
return false;
|
|
|
|
if (button) {
|
|
// If we have a press event, ignore the next event,
|
|
// which should be a release event.
|
|
if (press)
|
|
this._ignoreRelease = true;
|
|
this.ungrab({ actor: this._grabStack[0].actor });
|
|
}
|
|
|
|
return this._modalCount > 0;
|
|
},
|
|
|
|
// We catch 'event' rather than 'key-press-event' so that we get
|
|
// a chance to run before the overview's own Escape check
|
|
_onEvent: function(actor, event) {
|
|
if (event.type() == Clutter.EventType.KEY_PRESS &&
|
|
event.get_key_symbol() == Clutter.KEY_Escape) {
|
|
this.ungrab();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
_onKeyFocusChanged: function() {
|
|
let focus = global.stage.key_focus;
|
|
if (!focus || !this._isWithinGrabbedActor(focus))
|
|
this.ungrab();
|
|
},
|
|
|
|
_focusWindowChanged: function() {
|
|
let metaDisplay = global.screen.get_display();
|
|
if (metaDisplay.focus_window != null)
|
|
this.ungrab();
|
|
}
|
|
});
|