From 7f17fcfafc21d3ac483e99213276aec38019fac7 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 13 Jan 2011 15:04:37 -0500 Subject: [PATCH] messageTray: forward clicks on trayicon SummaryItems to the icon If the user clicks on the title of a trayicon's SummaryItem, forward that click to the trayicon. Also adjust gnome_shell_plugin_xevent_filter() so that if the trayicon takes a grab as a result of this, we don't hide the message tray. https://bugzilla.gnome.org/show_bug.cgi?id=630842 --- js/ui/messageTray.js | 28 +++++++++----- js/ui/notificationDaemon.js | 21 +++++++++-- src/gnome-shell-plugin.c | 32 ++++++++++------ src/shell-tray-icon.c | 73 +++++++++++++++++++++++++++++++++++++ src/shell-tray-icon.h | 3 ++ 5 files changed, 132 insertions(+), 25 deletions(-) diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index ae1060aa6..e34d69350 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -874,6 +874,14 @@ Source.prototype = { this.emit('destroy'); }, + // A subclass can redefine this to "steal" clicks from the + // summaryitem; Use Clutter.get_current_event() to get the + // details, return true to prevent the default handling from + // ocurring. + handleSummaryClick: function() { + return false; + }, + //// Protected methods //// // The subclass must call this at least once to set the summary icon. @@ -903,6 +911,7 @@ SummaryItem.prototype = { this.source = source; this.actor = new St.Button({ style_class: 'summary-source-button', reactive: true, + button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE, track_hover: true }); this._sourceBox = new St.BoxLayout({ style_class: 'summary-source' }); @@ -1165,9 +1174,9 @@ MessageTray.prototype = { this._onSummaryItemHoverChanged(summaryItem); })); - summaryItem.actor.connect('button-press-event', Lang.bind(this, - function (actor, event) { - this._onSummaryItemClicked(summaryItem, event); + summaryItem.actor.connect('clicked', Lang.bind(this, + function (actor, button) { + this._onSummaryItemClicked(summaryItem, button); })); source.connect('destroy', Lang.bind(this, this._onSourceDestroy)); @@ -1404,13 +1413,14 @@ MessageTray.prototype = { this._expandedSummaryItem.setEllipsization(Pango.EllipsizeMode.END); }, - _onSummaryItemClicked: function(summaryItem, event) { - let clickedButton = event.get_button(); - if (!this._clickedSummaryItem || - this._clickedSummaryItem != summaryItem || - this._clickedSummaryItemMouseButton != clickedButton) { + _onSummaryItemClicked: function(summaryItem, button) { + if (summaryItem.source.handleSummaryClick()) + this._unsetClickedSummaryItem(); + else if (!this._clickedSummaryItem || + this._clickedSummaryItem != summaryItem || + this._clickedSummaryItemMouseButton != button) { this._clickedSummaryItem = summaryItem; - this._clickedSummaryItemMouseButton = clickedButton; + this._clickedSummaryItemMouseButton = button; } else { this._unsetClickedSummaryItem(); } diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index adfe870df..c3c1e34b7 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -1,5 +1,6 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ +const Clutter = imports.gi.Clutter; const DBus = imports.dbus; const GLib = imports.gi.GLib; const Lang = imports.lang; @@ -441,7 +442,7 @@ Source.prototype = { this.title = this.app.get_name(); else this.useNotificationIcon = true; - this._isTrayIcon = false; + this._trayIcon = null; }, notify: function(notification, icon) { @@ -452,6 +453,18 @@ Source.prototype = { MessageTray.Source.prototype.notify.call(this, notification); }, + handleSummaryClick: function() { + if (!this._trayIcon) + return false; + + let event = Clutter.get_current_event(); + if (event.type() != Clutter.EventType.BUTTON_RELEASE) + return false; + + this._trayIcon.click(event); + return true; + }, + _setApp: function() { if (this.app) return; @@ -466,7 +479,7 @@ Source.prototype = { // Only override the icon if we were previously using // notification-based icons (ie, not a trayicon) or if it was unset before - if (!this._isTrayIcon) { + if (!this._trayIcon) { this.useNotificationIcon = false; this._setSummaryIcon(this.app.create_icon_texture (this.ICON_SIZE)); } @@ -475,7 +488,7 @@ Source.prototype = { setTrayIcon: function(icon) { this._setSummaryIcon(icon); this.useNotificationIcon = false; - this._isTrayIcon = true; + this._trayIcon = icon; }, open: function(notification) { @@ -483,7 +496,7 @@ Source.prototype = { }, _notificationRemoved: function() { - if (!this._isTrayIcon) + if (!this._trayIcon) this.destroy(); }, diff --git a/src/gnome-shell-plugin.c b/src/gnome-shell-plugin.c index 8bf7441be..b922ca39c 100644 --- a/src/gnome-shell-plugin.c +++ b/src/gnome-shell-plugin.c @@ -321,20 +321,28 @@ gnome_shell_plugin_xevent_filter (MetaPlugin *plugin, } #endif - /* When the pointer leaves the stage to enter a child of the stage - * (like a notification icon), we don't want to produce Clutter leave - * events. But Clutter treats all leave events identically, so we - * need hide the detail = NotifyInferior events from it. - * - * Since Clutter doesn't see any event at all, this does mean that - * it won't produce an enter event on a Clutter actor that surrounds - * the child (unless it gets a MotionNotify before the Enter event). - * Other weirdness is likely also possible. - */ if ((xev->xany.type == EnterNotify || xev->xany.type == LeaveNotify) - && xev->xcrossing.detail == NotifyInferior && xev->xcrossing.window == clutter_x11_get_stage_window (CLUTTER_STAGE (clutter_stage_get_default ()))) - return TRUE; + { + /* If the pointer enters a child of the stage window (eg, a + * trayicon), we want to consider it to still be in the stage, + * so don't let Clutter see the event. + */ + if (xev->xcrossing.detail == NotifyInferior) + return TRUE; + + /* If the pointer is grabbed by a window it is not currently in, + * filter that out as well. In particular, if a trayicon grabs + * the pointer after a click on its label, we don't want to hide + * the message tray. Filtering out this event will leave Clutter + * out of sync, but that happens fairly often with grabs, and we + * can work around it. (Eg, shell_global_sync_pointer().) + */ + if (xev->xcrossing.mode == NotifyGrab && + (xev->xcrossing.detail == NotifyNonlinear || + xev->xcrossing.detail == NotifyNonlinearVirtual)) + return TRUE; + } /* * Pass the event to shell-global diff --git a/src/shell-tray-icon.c b/src/shell-tray-icon.c index 079a48e51..08bffa8b7 100644 --- a/src/shell-tray-icon.c +++ b/src/shell-tray-icon.c @@ -165,3 +165,76 @@ shell_tray_icon_new (ShellEmbeddedWindow *window) "window", window, NULL); } + +/** + * shell_tray_icon_click: + * @icon: a #ShellTrayIcon + * @event: the #ClutterEvent triggering the fake click + * + * Fakes a press and release on @icon. @event must be a + * %CLUTTER_BUTTON_RELEASE event. Its relevant details will be passed + * on to the icon, but its coordinates will be ignored; the click is + * always made on the center of @icon. + */ +void +shell_tray_icon_click (ShellTrayIcon *icon, + ClutterEvent *event) +{ + XButtonEvent xbevent; + XCrossingEvent xcevent; + GdkWindow *remote_window; + GdkScreen *screen; + int x_root, y_root; + Display *xdisplay; + Window xwindow, xrootwindow; + + g_return_if_fail (clutter_event_type (event) == CLUTTER_BUTTON_RELEASE); + + gdk_error_trap_push (); + + remote_window = gtk_socket_get_plug_window (GTK_SOCKET (icon->priv->socket)); + xwindow = GDK_WINDOW_XID (remote_window); + xdisplay = GDK_WINDOW_XDISPLAY (remote_window); + screen = gdk_window_get_screen (remote_window); + xrootwindow = GDK_WINDOW_XID (gdk_screen_get_root_window (screen)); + gdk_window_get_origin (remote_window, &x_root, &y_root); + + /* First make the icon believe the pointer is inside it */ + xcevent.type = EnterNotify; + xcevent.window = xwindow; + xcevent.root = xrootwindow; + xcevent.subwindow = None; + xcevent.time = clutter_event_get_time (event); + xcevent.x = gdk_window_get_width (remote_window) / 2; + xcevent.y = gdk_window_get_height (remote_window) / 2; + xcevent.x_root = x_root + xcevent.x; + xcevent.y_root = y_root + xcevent.y; + xcevent.mode = NotifyNormal; + xcevent.detail = NotifyNonlinear; + xcevent.same_screen = True; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xcevent); + + /* Now do the click */ + xbevent.type = ButtonPress; + xbevent.window = xwindow; + xbevent.root = xrootwindow; + xbevent.subwindow = None; + xbevent.time = xcevent.time; + xbevent.x = xcevent.x; + xbevent.y = xcevent.y; + xbevent.x_root = xcevent.x_root; + xbevent.y_root = xcevent.y_root; + xbevent.state = clutter_event_get_state (event); + xbevent.button = clutter_event_get_button (event); + xbevent.same_screen = True; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xbevent); + + xbevent.type = ButtonRelease; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xbevent); + + /* And move the pointer back out */ + xcevent.type = LeaveNotify; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xcevent); + + gdk_error_trap_pop_ignored (); +} diff --git a/src/shell-tray-icon.h b/src/shell-tray-icon.h index 88e2a1406..565b3060a 100644 --- a/src/shell-tray-icon.h +++ b/src/shell-tray-icon.h @@ -31,4 +31,7 @@ struct _ShellTrayIconClass GType shell_tray_icon_get_type (void) G_GNUC_CONST; ClutterActor *shell_tray_icon_new (ShellEmbeddedWindow *window); +void shell_tray_icon_click (ShellTrayIcon *icon, + ClutterEvent *event); + #endif /* __SHELL_TRAY_ICON_H__ */