diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index db9913c5b..34f6b3f38 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -188,6 +188,149 @@ URLHighlighter.prototype = { } }; +function FocusGrabber() { + this._init(); +} + +FocusGrabber.prototype = { + _init: function() { + this.actor = null; + + this._hasFocus = false; + // We use this._prevFocusedWindow and this._prevKeyFocusActor to return the + // focus where it previously belonged after a focus grab, unless the user + // has explicitly changed that. + this._prevFocusedWindow = null; + this._prevKeyFocusActor = null; + + this._focusActorChangedId = 0; + this._stageInputModeChangedId = 0; + this._capturedEventId = 0; + this._togglingFocusGrabMode = false; + + Main.overview.connect('showing', Lang.bind(this, + function() { + this._toggleFocusGrabMode(); + })); + Main.overview.connect('hidden', Lang.bind(this, + function() { + this._toggleFocusGrabMode(); + })); + }, + + grabFocus: function(actor) { + if (this._hasFocus) + return; + + this.actor = actor; + + let metaDisplay = global.screen.get_display(); + + this._prevFocusedWindow = metaDisplay.focus_window; + this._prevKeyFocusActor = global.stage.get_key_focus(); + + if (!Main.overview.visible) + global.set_stage_input_mode(Shell.StageInputMode.FOCUSED); + + // Use captured-event to notice clicks outside the focused actor + // without consuming them. + this._capturedEventId = global.stage.connect('captured-event', Lang.bind(this, this._onCapturedEvent)); + + this._stageInputModeChangedId = global.connect('notify::stage-input-mode', Lang.bind(this, this._stageInputModeChanged)); + this._focusActorChangedId = global.stage.connect('notify::key-focus', Lang.bind(this, this._focusActorChanged)); + + this._hasFocus = true; + + this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); + this.emit('focus-grabbed'); + }, + + _focusActorChanged: function() { + let focusedActor = global.stage.get_key_focus(); + if (!focusedActor || !this.actor.contains(focusedActor)) { + this._prevKeyFocusActor = null; + this.ungrabFocus(); + } + }, + + _stageInputModeChanged: function() { + this.ungrabFocus(); + }, + + _onCapturedEvent: function(actor, event) { + let source = event.get_source(); + switch (event.type()) { + case Clutter.EventType.BUTTON_PRESS: + if (!this.actor.contains(source)) + this.ungrabFocus(); + break; + case Clutter.EventType.KEY_PRESS: + let symbol = event.get_key_symbol(); + if (symbol == Clutter.Escape) { + this.emit('escape-pressed'); + return true; + } + break; + } + + return false; + }, + + ungrabFocus: function() { + if (!this._hasFocus) + return; + + let metaDisplay = global.screen.get_display(); + + if (this._focusActorChangedId > 0) { + global.stage.disconnect(this._focusActorChangedId); + this._focusActorChangedId = 0; + } + + if (this._stageInputModeChangedId) { + global.disconnect(this._stageInputModeChangedId); + this._stageInputModeChangedId = 0; + } + + if (this._capturedEventId > 0) { + global.stage.disconnect(this._capturedEventId); + this._capturedEventId = 0; + } + + this._hasFocus = false; + this.emit('focus-ungrabbed'); + + if (this._prevFocusedWindow && !metaDisplay.focus_window) { + metaDisplay.set_input_focus_window(this._prevFocusedWindow, false, global.get_current_time()); + this._prevFocusedWindow = null; + } + if (this._prevKeyFocusActor) { + global.stage.set_key_focus(this._prevKeyFocusActor); + this._prevKeyFocusActor = null; + } else { + // We don't want to keep any actor inside the previously focused actor focused. + let focusedActor = global.stage.get_key_focus(); + if (focusedActor && this.actor.contains(focusedActor)) + global.stage.set_key_focus(null); + } + if (!this._togglingFocusGrabMode) + this.actor = null; + }, + + // Because we grab focus differently in the overview + // and in the main view, we need to change how it is + // done when we move between the two. + _toggleFocusGrabMode: function() { + if (this._hasFocus) { + this._togglingFocusGrabMode = true; + this.ungrabFocus(); + this.grabFocus(this.actor); + this._togglingFocusGrabMode = false; + } + } +} +Signals.addSignalMethods(FocusGrabber.prototype); + // Notification: // @source: the notification's Source // @title: the title @@ -262,20 +405,6 @@ Notification.prototype = { this._titleFitsInBannerMode = true; this._spacing = 0; - this._buttonFocusManager = null; - this._hasFocus = false; - this._lockTrayOnFocusGrab = false; - // We use this._prevFocusedWindow and this._prevKeyFocusActor to return the - // focus where it previously belonged after a focus grab, unless the user - // has explicitly changed that. - this._prevFocusedWindow = null; - this._prevKeyFocusActor = null; - - this._focusActorChangedId = 0; - this._stageInputModeChangedId = 0; - this._capturedEventId = 0; - this._keyPressId = 0; - source.connect('destroy', Lang.bind(this, // Avoid passing 'source' as an argument to this.destroy() function () { @@ -292,6 +421,8 @@ Notification.prototype = { this._onClicked(); })); + this._buttonFocusManager = St.FocusManager.get_for_stage(global.stage); + // The first line should have the title, followed by the // banner text, but ellipsized if they won't both fit. We can't // make St.Table or St.BoxLayout do this the way we want (don't @@ -313,15 +444,6 @@ Notification.prototype = { this._bannerBox.add_actor(this._bannerLabel); this.update(title, banner, params); - - Main.overview.connect('showing', Lang.bind(this, - function() { - this._toggleFocusGrabMode(); - })); - Main.overview.connect('hidden', Lang.bind(this, - function() { - this._toggleFocusGrabMode(); - })); }, // update: @@ -515,8 +637,6 @@ Notification.prototype = { button.label = label; } - if (!this._buttonFocusManager) - this._buttonFocusManager = St.FocusManager.get_for_stage(global.stage); if (this._buttonBox.get_children().length > 0) this._buttonFocusManager.remove_group(this._buttonBox); @@ -651,67 +771,6 @@ Notification.prototype = { this._bannerLabel.opacity = 255; }, - grabFocus: function(lockTray) { - if (this._hasFocus) - return; - - this._lockTrayOnFocusGrab = lockTray; - - let metaDisplay = global.screen.get_display(); - - this._prevFocusedWindow = metaDisplay.focus_window; - this._prevKeyFocusActor = global.stage.get_key_focus(); - - if (!Main.overview.visible) - global.set_stage_input_mode(Shell.StageInputMode.FOCUSED); - - // Use captured-event to notice clicks outside the notification - // without consuming them. - this._capturedEventId = global.stage.connect('captured-event', Lang.bind(this, this._onCapturedEvent)); - - this._stageInputModeChangedId = global.connect('notify::stage-input-mode', Lang.bind(this, this._stageInputModeChanged)); - this._focusActorChangedId = global.stage.connect('notify::key-focus', Lang.bind(this, this._focusActorChanged)); - - this._hasFocus = true; - - if (this._buttonFocusManager) - this._buttonBox.get_children()[0].grab_key_focus(); - - if (lockTray) - Main.messageTray.lock(); - }, - - _focusActorChanged: function() { - let focusedActor = global.stage.get_key_focus(); - if (!focusedActor || !this.actor.contains(focusedActor)) { - this._prevKeyFocusActor = null; - this.ungrabFocus(); - } - }, - - _stageInputModeChanged: function() { - this.ungrabFocus(); - }, - - _onCapturedEvent: function(actor, event) { - let source = event.get_source(); - switch (event.type()) { - case Clutter.EventType.BUTTON_PRESS: - if (!this.actor.contains(source)) - this.ungrabFocus(); - break; - case Clutter.EventType.KEY_PRESS: - let symbol = event.get_key_symbol(); - if (symbol == Clutter.Escape) { - Main.messageTray.escapeTray(); - return true; - } - break; - } - - return false; - }, - _onActionInvoked: function(actor, mouseButtonClicked, id) { this.emit('action-invoked', id); if (!this.resident) { @@ -734,55 +793,6 @@ Notification.prototype = { this.destroy(); }, - ungrabFocus: function() { - if (!this._hasFocus) - return; - - let metaDisplay = global.screen.get_display(); - - if (this._focusActorChangedId > 0) { - global.stage.disconnect(this._focusActorChangedId); - this._focusActorChangedId = 0; - } - - if (this._stageInputModeChangedId) { - global.disconnect(this._stageInputModeChangedId); - this._stageInputModeChangedId = 0; - } - - if (this._capturedEventId > 0) { - global.stage.disconnect(this._capturedEventId); - this._capturedEventId = 0; - } - - this._hasFocus = false; - Main.messageTray.unlock(); - - if (this._prevFocusedWindow && !metaDisplay.focus_window) { - metaDisplay.set_input_focus_window(this._prevFocusedWindow, false, global.get_current_time()); - this._prevFocusedWindow = null; - } - if (this._prevKeyFocusActor) { - global.stage.set_key_focus(this._prevKeyFocusActor); - this._prevKeyFocusActor = null; - } else { - // We don't want to keep the actor inside the notification focused. - let focusedActor = global.stage.get_key_focus(); - if (focusedActor && this.actor.contains(focusedActor)) - global.stage.set_key_focus(null); - } - }, - - // Because we grab focus differently in the overview - // and in the main view, we need to change how it is - // done when we move between the two. - _toggleFocusGrabMode: function() { - if (this._hasFocus) { - this.ungrabFocus(); - this.grabFocus(this._lockTrayOnFocusGrab); - } - }, - destroy: function(reason) { if (this._destroyed) return; @@ -975,6 +985,15 @@ MessageTray.prototype = { // of the other items are collapsed. this._imaginarySummaryItemTitleWidth = 0; + this._focusGrabber = new FocusGrabber(); + this._focusGrabber.connect('focus-grabbed', Lang.bind(this, + function() { + if (this._summaryNotification) + this._lock(); + })); + this._focusGrabber.connect('focus-ungrabbed', Lang.bind(this, this._unlock)); + this._focusGrabber.connect('escape-pressed', Lang.bind(this, this._escapeTray)); + this._trayState = State.HIDDEN; this._locked = false; this._useLongerTrayLeftTimeout = false; @@ -1006,7 +1025,7 @@ MessageTray.prototype = { function() { this._overviewVisible = true; if (this._locked) - this.unlock(); + this._unlock(); else this._updateState(); })); @@ -1014,7 +1033,7 @@ MessageTray.prototype = { function() { this._overviewVisible = false; if (this._locked) - this.unlock(); + this._unlock(); else this._updateState(); })); @@ -1180,11 +1199,11 @@ MessageTray.prototype = { this._notificationQueue.splice(index, 1); }, - lock: function() { + _lock: function() { this._locked = true; }, - unlock: function() { + _unlock: function() { if (!this._locked) return; this._locked = false; @@ -1408,8 +1427,8 @@ MessageTray.prototype = { return false; }, - escapeTray: function() { - this.unlock(); + _escapeTray: function() { + this._unlock(); this._pointerInTray = false; this._pointerInSummary = false; this._updateNotificationTimeout(0); @@ -1538,7 +1557,7 @@ MessageTray.prototype = { _showNotification: function() { this._notification = this._notificationQueue.shift(); this._notificationClickedId = this._notification.connect('done-displaying', - Lang.bind(this, this.escapeTray)); + Lang.bind(this, this._escapeTray)); this._notificationBin.child = this._notification.actor; this._notificationBin.opacity = 0; @@ -1633,7 +1652,7 @@ MessageTray.prototype = { }, _hideNotification: function() { - this._notification.ungrabFocus(); + this._focusGrabber.ungrabFocus(); if (this._notificationExpandedId) { this._notification.disconnect(this._notificationExpandedId); this._notificationExpandedId = 0; @@ -1665,7 +1684,7 @@ MessageTray.prototype = { _expandNotification: function(autoExpanding) { // Don't grab focus in notifications that are auto-expanded. if (!autoExpanding) - this._notification.grabFocus(false); + this._focusGrabber.grabFocus(this._notification.actor); if (!this._notificationExpandedId) this._notificationExpandedId = @@ -1688,7 +1707,7 @@ MessageTray.prototype = { // We use this function to grab focus when the user moves the pointer // to a notification with CRITICAL urgency that was already auto-expanded. _ensureNotificationFocused: function() { - this._notification.grabFocus(false); + this._focusGrabber.grabFocus(this._notification.actor); }, _showSummary: function(withTimeout) { @@ -1741,13 +1760,13 @@ MessageTray.prototype = { _showSummaryNotification: function() { this._summaryNotification = this._clickedSummaryItem.source.notification; this._summaryNotificationClickedId = this._summaryNotification.connect('done-displaying', - Lang.bind(this, this.escapeTray)); + Lang.bind(this, this._escapeTray)); let index = this._notificationQueue.indexOf(this._summaryNotification); if (index != -1) this._notificationQueue.splice(index, 1); this._summaryNotificationBoxPointer.bin.child = this._summaryNotification.actor; - this._summaryNotification.grabFocus(true); + this._focusGrabber.grabFocus(this._summaryNotification.actor); if (!this._summaryNotificationExpandedId) this._summaryNotificationExpandedId = this._summaryNotification.connect('expanded', Lang.bind(this, this._onSummaryNotificationExpanded)); @@ -1802,7 +1821,7 @@ MessageTray.prototype = { if (this._summaryState != State.SHOWN) this._unsetClickedSummaryItem(); - this._summaryNotification.ungrabFocus(); + this._focusGrabber.ungrabFocus(); this._summaryNotificationState = State.HIDING; this._summaryNotificationBoxPointer.hide(true, Lang.bind(this, this._hideSummaryNotificationCompleted)); }, diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js index 8dcbfed5a..33fc9a6c4 100644 --- a/js/ui/telepathyClient.js +++ b/js/ui/telepathyClient.js @@ -598,7 +598,8 @@ Notification.prototype = { MessageTray.Notification.prototype._init.call(this, source, source.title, null, { customContent: true }); this.setResident(true); - this._responseEntry = new St.Entry({ style_class: 'chat-response' }); + this._responseEntry = new St.Entry({ style_class: 'chat-response', + can_focus: true }); this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated)); this.setActionArea(this._responseEntry); @@ -683,13 +684,6 @@ Notification.prototype = { this._history.unshift({ actor: label, time: (Date.now() / 1000), realMessage: false}); }, - grabFocus: function(lockTray) { - // Need to call the base class function first so that - // it saves where the key focus was before. - MessageTray.Notification.prototype.grabFocus.call(this, lockTray); - global.stage.set_key_focus(this._responseEntry.clutter_text); - }, - _onEntryActivated: function() { let text = this._responseEntry.get_text(); if (text == '')