Enabling multithreaded compilation.
[debian/lxpanel.git] / src / plugins / tray.c
1 /**
2 * System tray plugin to lxpanel
3 *
4 * Copyright (c) 2008 LxDE Developers, see the file AUTHORS for details.
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 *
20 */
21
22 /** Contains code adapted from na-tray-manager.c
23 * Copyright (C) 2002 Anders Carlsson <andersca@gnu.org>
24 * Copyright (C) 2003-2006 Vincent Untz */
25
26 #include <stdlib.h>
27 #include <unistd.h>
28 #include <string.h>
29
30 #include <gdk-pixbuf/gdk-pixbuf.h>
31 #include <glib/gi18n.h>
32
33 #include "panel.h"
34 #include "misc.h"
35 #include "plugin.h"
36 #include "bg.h"
37 #include "icon-grid.h"
38
39 /* Standards reference: http://standards.freedesktop.org/systemtray-spec/ */
40
41 /* Protocol constants. */
42 #define SYSTEM_TRAY_REQUEST_DOCK 0
43 #define SYSTEM_TRAY_BEGIN_MESSAGE 1
44 #define SYSTEM_TRAY_CANCEL_MESSAGE 2
45
46 #define SYSTEM_TRAY_ORIENTATION_HORZ 0
47 #define SYSTEM_TRAY_ORIENTATION_VERT 1
48
49 //#define DEBUG
50 #include "dbg.h"
51
52 struct _balloon_message;
53 struct _tray_client;
54 struct _tray_plugin;
55
56 /* Representative of a balloon message. */
57 typedef struct _balloon_message {
58 struct _balloon_message * flink; /* Forward link */
59 Window window; /* X window ID */
60 long timeout; /* Time in milliseconds to display message; 0 if no timeout */
61 long length; /* Message string length */
62 long id; /* Client supplied unique message ID */
63 long remaining_length; /* Remaining length expected of incomplete message */
64 char * string; /* Message string */
65 } BalloonMessage;
66
67 /* Representative of a tray client. */
68 typedef struct _tray_client {
69 struct _tray_client * client_flink; /* Forward link to next task in X window ID order */
70 struct _tray_plugin * tr; /* Back pointer to tray plugin */
71 Window window; /* X window ID */
72 GtkWidget * socket; /* Socket */
73 } TrayClient;
74
75 /* Private context for system tray plugin. */
76 typedef struct _tray_plugin {
77 Plugin * plugin; /* Back pointer to Plugin */
78 IconGrid * icon_grid; /* Icon grid to manage tray presentation */
79 TrayClient * client_list; /* List of tray clients */
80 BalloonMessage * incomplete_messages; /* List of balloon messages for which we are awaiting data */
81 BalloonMessage * messages; /* List of balloon messages actively being displayed or waiting to be displayed */
82 GtkWidget * balloon_message_popup; /* Popup showing balloon message */
83 guint balloon_message_timer; /* Timer controlling balloon message */
84 GtkWidget * invisible; /* Invisible window that holds manager selection */
85 Window invisible_window; /* X window ID of invisible window */
86 GdkAtom selection_atom; /* Atom for _NET_SYSTEM_TRAY_S%d */
87 } TrayPlugin;
88
89 static TrayClient * client_lookup(TrayPlugin * tr, Window win);
90 static void client_delete(TrayPlugin * tr, TrayClient * tc, gboolean unlink);
91 static void balloon_message_free(BalloonMessage * message);
92 static void balloon_message_advance(TrayPlugin * tr, gboolean destroy_timer, gboolean display_next);
93 static gboolean balloon_message_activate_event(GtkWidget * widget, GdkEventButton * event, TrayPlugin * tr);
94 static gboolean balloon_message_timeout(TrayPlugin * tr);
95 static void balloon_message_display(TrayPlugin * tr, BalloonMessage * msg);
96 static void balloon_message_queue(TrayPlugin * tr, BalloonMessage * msg);
97 static void balloon_incomplete_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id);
98 static void balloon_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id);
99 static void balloon_message_begin_event(TrayPlugin * tr, XClientMessageEvent * xevent);
100 static void balloon_message_cancel_event(TrayPlugin * tr, XClientMessageEvent * xevent);
101 static void balloon_message_data_event(TrayPlugin * tr, XClientMessageEvent * xevent);
102 static void trayclient_request_dock(TrayPlugin * tr, XClientMessageEvent * xevent);
103 static GdkFilterReturn tray_event_filter(XEvent * xev, GdkEvent * event, TrayPlugin * tr);
104 static void tray_unmanage_selection(TrayPlugin * tr);
105 static int tray_constructor(Plugin * p, char ** fp);
106 static void tray_destructor(Plugin * p);
107 static void tray_panel_configuration_changed(Plugin * p);
108
109 /* Look up a client in the client list. */
110 static TrayClient * client_lookup(TrayPlugin * tr, Window window)
111 {
112 TrayClient * tc;
113 for (tc = tr->client_list; tc != NULL; tc = tc->client_flink)
114 {
115 if (tc->window == window)
116 return tc;
117 if (tc->window > window)
118 break;
119 }
120 return NULL;
121 }
122
123 static void client_print(TrayPlugin * tr, char c, TrayClient * tc, XClientMessageEvent * xevent)
124 {
125 int const log_at_level = LOG_ALL;
126 if (log_at_level <= log_level) {
127 char *name = get_utf8_property(tc->window, a_NET_WM_NAME);
128 int pid = get_net_wm_pid(tc->window);
129 XClientMessageEvent xcm = {0};
130 if (!xevent)
131 xevent = &xcm;
132 LOG(log_at_level, "tray: %c%p, winid 0x%lx: %s (PID %d), plug %p, serial no %lu, send_event %c, format %d\n",
133 c, tc, tc->window, name, pid,
134 gtk_socket_get_plug_window(GTK_SOCKET(tc->socket)),
135 xevent->serial, xevent->send_event ? 'y' : 'n', xevent->format);
136 g_free(name);
137 }
138 }
139
140 /* Delete a client. */
141 static void client_delete(TrayPlugin * tr, TrayClient * tc, gboolean unlink)
142 {
143 client_print(tr, '-', tc, NULL);
144
145 if (unlink)
146 {
147 if (tr->client_list == tc)
148 tr->client_list = tc->client_flink;
149 else
150 {
151 /* Locate the task and its predecessor in the list and then remove it. For safety, ensure it is found. */
152 TrayClient * tc_pred = NULL;
153 TrayClient * tc_cursor;
154 for (
155 tc_cursor = tr->client_list;
156 ((tc_cursor != NULL) && (tc_cursor != tc));
157 tc_pred = tc_cursor, tc_cursor = tc_cursor->client_flink) ;
158 if (tc_cursor == tc)
159 tc_pred->client_flink = tc->client_flink;
160 }
161 }
162
163 /* Clear out any balloon messages. */
164 balloon_incomplete_message_remove(tr, tc->window, TRUE, 0);
165 balloon_message_remove(tr, tc->window, TRUE, 0);
166
167 /* Remove the socket from the icon grid. */
168 icon_grid_remove(tr->icon_grid, tc->socket);
169
170 /* Deallocate the client structure. */
171 g_free(tc);
172 }
173
174 /*** Balloon message display ***/
175
176 /* Free a balloon message structure. */
177 static void balloon_message_free(BalloonMessage * message)
178 {
179 g_free(message->string);
180 g_free(message);
181 }
182
183 /* General code to deactivate a message and optionally display the next.
184 * This is used in three scenarios: balloon clicked, timeout expired, destructor. */
185 static void balloon_message_advance(TrayPlugin * tr, gboolean destroy_timer, gboolean display_next)
186 {
187 /* Remove the message from the queue. */
188 BalloonMessage * msg = tr->messages;
189 tr->messages = msg->flink;
190
191 /* Cancel the timer, if set. This is not done when the timer has expired. */
192 if ((destroy_timer) && (tr->balloon_message_timer != 0))
193 g_source_remove(tr->balloon_message_timer);
194 tr->balloon_message_timer = 0;
195
196 /* Destroy the widget. */
197 if (tr->balloon_message_popup != NULL)
198 gtk_widget_destroy(tr->balloon_message_popup);
199 tr->balloon_message_popup = NULL;
200
201 /* Free the message. */
202 balloon_message_free(msg);
203
204 /* If there is another message waiting in the queue, display it. This is not done in the destructor. */
205 if ((display_next) && (tr->messages != NULL))
206 balloon_message_display(tr, tr->messages);
207 }
208
209 /* Handler for "button-press-event" from balloon message popup menu item. */
210 static gboolean balloon_message_activate_event(GtkWidget * widget, GdkEventButton * event, TrayPlugin * tr)
211 {
212 balloon_message_advance(tr, TRUE, TRUE);
213 return TRUE;
214 }
215
216 /* Timer expiration for balloon message. */
217 static gboolean balloon_message_timeout(TrayPlugin * tr)
218 {
219 balloon_message_advance(tr, FALSE, TRUE);
220 return FALSE;
221 }
222
223 /* Create the graphic elements to display a balloon message. */
224 static void balloon_message_display(TrayPlugin * tr, BalloonMessage * msg)
225 {
226 /* Create a window and an item containing the text. */
227 tr->balloon_message_popup = gtk_window_new(GTK_WINDOW_POPUP);
228 GtkWidget * balloon_text = gtk_label_new(msg->string);
229 gtk_label_set_line_wrap(GTK_LABEL(balloon_text), TRUE);
230 gtk_misc_set_alignment(GTK_MISC(balloon_text), 0.5, 0.5);
231 gtk_widget_show(balloon_text);
232 gtk_container_add(GTK_CONTAINER(tr->balloon_message_popup), balloon_text);
233 gtk_container_set_border_width(GTK_CONTAINER(tr->balloon_message_popup), 4);
234
235 /* Connect signals. Clicking the popup dismisses it and displays the next message, if any. */
236 gtk_widget_add_events(tr->balloon_message_popup, GDK_BUTTON_PRESS_MASK);
237 g_signal_connect(tr->balloon_message_popup, "button_press_event", G_CALLBACK(balloon_message_activate_event), (gpointer) tr);
238
239 /* Get the allocation of the popup menu. */
240 GtkRequisition popup_req;
241 gtk_widget_size_request(GTK_WIDGET(tr->balloon_message_popup), &popup_req);
242
243 /* Compute the desired position in screen coordinates near the tray plugin. */
244 int x;
245 int y;
246 plugin_popup_set_position_helper(tr->plugin, tr->plugin->pwid, tr->balloon_message_popup, &popup_req, &x, &y);
247
248 /* Push onscreen. */
249 int screen_width = gdk_screen_width();
250 int screen_height = gdk_screen_height();
251 if ((x + popup_req.width) > screen_width)
252 x -= (x + popup_req.width) - screen_width;
253 if ((y + popup_req.height) > screen_height)
254 y -= (y + popup_req.height) - screen_height;
255
256 /* Show the popup. */
257 gtk_window_move(GTK_WINDOW(tr->balloon_message_popup), x, y);
258 gtk_widget_show(tr->balloon_message_popup);
259
260 /* Set a timer, if the client specified one. Both are in units of milliseconds. */
261 if (msg->timeout != 0)
262 tr->balloon_message_timer = g_timeout_add(msg->timeout, (GSourceFunc) balloon_message_timeout, tr);
263 }
264
265 /* Add a balloon message to the tail of the message queue. If it is the only element, display it immediately. */
266 static void balloon_message_queue(TrayPlugin * tr, BalloonMessage * msg)
267 {
268 if (tr->messages == NULL)
269 {
270 tr->messages = msg;
271 balloon_message_display(tr, msg);
272 }
273 else
274 {
275 BalloonMessage * msg_pred;
276 for (msg_pred = tr->messages; ((msg_pred != NULL) && (msg_pred->flink != NULL)); msg_pred = msg_pred->flink) ;
277 if (msg_pred != NULL)
278 msg_pred->flink = msg;
279 }
280 }
281
282 /* Remove an incomplete message from the queue, selected by window and optionally also client's ID.
283 * Used in two scenarios: client issues CANCEL (ID significant), client plug removed (ID don't care). */
284 static void balloon_incomplete_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id)
285 {
286 BalloonMessage * msg_pred = NULL;
287 BalloonMessage * msg = tr->incomplete_messages;
288 while (msg != NULL)
289 {
290 /* Establish successor in case of deletion. */
291 BalloonMessage * msg_succ = msg->flink;
292
293 if ((msg->window == window) && ((all_ids) || (msg->id == id)))
294 {
295 /* Found a message matching the criteria. Unlink and free it. */
296 if (msg_pred == NULL)
297 tr->incomplete_messages = msg->flink;
298 else
299 msg_pred->flink = msg->flink;
300 balloon_message_free(msg);
301 }
302 else
303 msg_pred = msg;
304
305 /* Advance to successor. */
306 msg = msg_succ;
307 }
308 }
309
310 /* Remove a message from the message queue, selected by window and optionally also client's ID.
311 * Used in two scenarios: client issues CANCEL (ID significant), client plug removed (ID don't care). */
312 static void balloon_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id)
313 {
314 BalloonMessage * msg_pred = NULL;
315 BalloonMessage * msg_head = tr->messages;
316 BalloonMessage * msg = msg_head;
317 while (msg != NULL)
318 {
319 /* Establish successor in case of deletion. */
320 BalloonMessage * msg_succ = msg->flink;
321
322 if ((msg->window == window) && ((all_ids) || (msg->id == id)))
323 {
324 /* Found a message matching the criteria. */
325 if (msg_pred == NULL)
326 {
327 /* The message is at the queue head, so is being displayed. Stop the display. */
328 tr->messages = msg->flink;
329 if (tr->balloon_message_timer != 0)
330 {
331 g_source_remove(tr->balloon_message_timer);
332 tr->balloon_message_timer = 0;
333 }
334 if (tr->balloon_message_popup != NULL)
335 {
336 gtk_widget_destroy(tr->balloon_message_popup);
337 tr->balloon_message_popup = NULL;
338 }
339 }
340 else
341 msg_pred->flink = msg->flink;
342
343 /* Free the message. */
344 balloon_message_free(msg);
345 }
346 else
347 msg_pred = msg;
348
349 /* Advance to successor. */
350 msg = msg_succ;
351 }
352
353 /* If there is a new message head, display it now. */
354 if ((tr->messages != msg_head) && (tr->messages != NULL))
355 balloon_message_display(tr, tr->messages);
356 }
357
358 /*** Event interfaces ***/
359
360 /* Handle a balloon message SYSTEM_TRAY_BEGIN_MESSAGE event. */
361 static void balloon_message_begin_event(TrayPlugin * tr, XClientMessageEvent * xevent)
362 {
363 TrayClient * client = client_lookup(tr, xevent->window);
364 if (client != NULL)
365 {
366 /* Check if the message ID already exists. */
367 balloon_incomplete_message_remove(tr, xevent->window, FALSE, xevent->data.l[4]);
368
369 /* Allocate a BalloonMessage structure describing the message. */
370 BalloonMessage * msg = g_new0(BalloonMessage, 1);
371 msg->window = xevent->window;
372 msg->timeout = xevent->data.l[2];
373 msg->length = xevent->data.l[3];
374 msg->id = xevent->data.l[4];
375 msg->remaining_length = msg->length;
376 msg->string = g_new0(char, msg->length + 1);
377
378 /* Message length of 0 indicates that no follow-on messages will be sent. */
379 if (msg->length == 0)
380 balloon_message_queue(tr, msg);
381 else
382 {
383 /* Add the new message to the queue to await its message text. */
384 msg->flink = tr->incomplete_messages;
385 tr->incomplete_messages = msg;
386 }
387 }
388 }
389
390 /* Handle a balloon message SYSTEM_TRAY_CANCEL_MESSAGE event. */
391 static void balloon_message_cancel_event(TrayPlugin * tr, XClientMessageEvent * xevent)
392 {
393 /* Remove any incomplete messages on this window with the specified ID. */
394 balloon_incomplete_message_remove(tr, xevent->window, TRUE, 0);
395
396 /* Remove any displaying or waiting messages on this window with the specified ID. */
397 TrayClient * client = client_lookup(tr, xevent->window);
398 if (client != NULL)
399 balloon_message_remove(tr, xevent->window, FALSE, xevent->data.l[2]);
400 }
401
402 /* Handle a balloon message _NET_SYSTEM_TRAY_MESSAGE_DATA event. */
403 static void balloon_message_data_event(TrayPlugin * tr, XClientMessageEvent * xevent)
404 {
405 /* Look up the pending message in the list. */
406 BalloonMessage * msg_pred = NULL;
407 BalloonMessage * msg;
408 for (msg = tr->incomplete_messages; msg != NULL; msg_pred = msg, msg = msg->flink)
409 {
410 if (xevent->window == msg->window)
411 {
412 /* Append the message segment to the message. */
413 int length = MIN(msg->remaining_length, 20);
414 memcpy((msg->string + msg->length - msg->remaining_length), &xevent->data, length);
415 msg->remaining_length -= length;
416
417 /* If the message has been completely collected, display it. */
418 if (msg->remaining_length == 0)
419 {
420 /* Unlink the message from the structure. */
421 if (msg_pred == NULL)
422 tr->incomplete_messages = msg->flink;
423 else
424 msg_pred->flink = msg->flink;
425
426 /* If the client window is valid, queue the message. Otherwise discard it. */
427 TrayClient * client = client_lookup(tr, msg->window);
428 if (client != NULL)
429 balloon_message_queue(tr, msg);
430 else
431 balloon_message_free(msg);
432 }
433 break;
434 }
435 }
436 }
437
438 /* Handler for request dock message. */
439 static void trayclient_request_dock(TrayPlugin * tr, XClientMessageEvent * xevent)
440 {
441 /* Search for the window in the client list. Set up context to do an insert right away if needed. */
442 TrayClient * tc_pred = NULL;
443 TrayClient * tc_cursor;
444 for (tc_cursor = tr->client_list; tc_cursor != NULL; tc_pred = tc_cursor, tc_cursor = tc_cursor->client_flink)
445 {
446 if (tc_cursor->window == xevent->data.l[2])
447 return; /* We already got this notification earlier, ignore this one. */
448 if (tc_cursor->window > xevent->data.l[2])
449 break;
450 }
451
452 /* Allocate and initialize new client structure. */
453 TrayClient * tc = g_new0(TrayClient, 1);
454 tc->window = xevent->data.l[2];
455 tc->tr = tr;
456
457 /* Allocate a socket. This is the tray side of the Xembed connection. */
458 tc->socket = gtk_socket_new();
459
460 /* Link the client structure into the client list. */
461 if (tc_pred == NULL)
462 {
463 tc->client_flink = tr->client_list;
464 tr->client_list = tc;
465 }
466 else
467 {
468 tc->client_flink = tc_pred->client_flink;
469 tc_pred->client_flink = tc;
470 }
471
472 /* Add the socket to the icon grid. */
473 icon_grid_add(tr->icon_grid, tc->socket, TRUE);
474
475 /* Connect the socket to the plug. This can only be done after the socket is realized. */
476 gtk_socket_add_id(GTK_SOCKET(tc->socket), tc->window);
477 client_print(tr, '+', tc, xevent);
478 }
479
480 /* GDK event filter. */
481 static GdkFilterReturn tray_event_filter(XEvent * xev, GdkEvent * event, TrayPlugin * tr)
482 {
483 if (xev->type == DestroyNotify)
484 {
485 /* Look for DestroyNotify events on tray icon windows and update state.
486 * We do it this way rather than with a "plug_removed" event because delivery
487 * of plug_removed events is observed to be unreliable if the client
488 * disconnects within less than 10 ms. */
489 XDestroyWindowEvent * xev_destroy = (XDestroyWindowEvent *) xev;
490 TrayClient * tc = client_lookup(tr, xev_destroy->window);
491 if (tc != NULL)
492 client_delete(tr, tc, TRUE);
493 }
494
495 else if (xev->type == ClientMessage)
496 {
497 if (xev->xclient.message_type == a_NET_SYSTEM_TRAY_OPCODE)
498 {
499 /* Client message of type _NET_SYSTEM_TRAY_OPCODE.
500 * Dispatch on the request. */
501 switch (xev->xclient.data.l[1])
502 {
503 case SYSTEM_TRAY_REQUEST_DOCK:
504 /* If a Request Dock event on the invisible window, which is holding the manager selection, execute it. */
505 if (xev->xclient.window == tr->invisible_window)
506 {
507 trayclient_request_dock(tr, (XClientMessageEvent *) xev);
508 return GDK_FILTER_REMOVE;
509 }
510 break;
511
512 case SYSTEM_TRAY_BEGIN_MESSAGE:
513 /* If a Begin Message event. look up the tray icon and execute it. */
514 balloon_message_begin_event(tr, (XClientMessageEvent *) xev);
515 return GDK_FILTER_REMOVE;
516
517 case SYSTEM_TRAY_CANCEL_MESSAGE:
518 /* If a Cancel Message event. look up the tray icon and execute it. */
519 balloon_message_cancel_event(tr, (XClientMessageEvent *) xev);
520 return GDK_FILTER_REMOVE;
521 }
522 }
523
524 else if (xev->xclient.message_type == a_NET_SYSTEM_TRAY_MESSAGE_DATA)
525 {
526 /* Client message of type _NET_SYSTEM_TRAY_MESSAGE_DATA.
527 * Look up the tray icon and execute it. */
528 balloon_message_data_event(tr, (XClientMessageEvent *) xev);
529 return GDK_FILTER_REMOVE;
530 }
531 }
532
533 else if ((xev->type == SelectionClear)
534 && (xev->xclient.window == tr->invisible_window))
535 {
536 /* Look for SelectionClear events on the invisible window, which is holding the manager selection.
537 * This should not happen. */
538 tray_unmanage_selection(tr);
539 }
540
541 return GDK_FILTER_CONTINUE;
542 }
543
544 /* Delete the selection on the invisible window. */
545 static void tray_unmanage_selection(TrayPlugin * tr)
546 {
547 if (tr->invisible != NULL)
548 {
549 GtkWidget * invisible = tr->invisible;
550 GdkDisplay * display = gtk_widget_get_display(invisible);
551 #if GTK_CHECK_VERSION(2,14,0)
552 if (gdk_selection_owner_get_for_display(display, tr->selection_atom) == gtk_widget_get_window(invisible))
553 #else
554 if (gdk_selection_owner_get_for_display(display, tr->selection_atom) == invisible->window)
555 #endif
556 {
557 #if GTK_CHECK_VERSION(2,14,0)
558 guint32 timestamp = gdk_x11_get_server_time(gtk_widget_get_window(invisible));
559 #else
560 guint32 timestamp = gdk_x11_get_server_time(invisible->window);
561 #endif
562 gdk_selection_owner_set_for_display(
563 display,
564 NULL,
565 tr->selection_atom,
566 timestamp,
567 TRUE);
568 }
569
570 /* Destroy the invisible window. */
571 tr->invisible = NULL;
572 tr->invisible_window = None;
573 gtk_widget_destroy(invisible);
574 g_object_unref(G_OBJECT(invisible));
575 }
576 }
577
578 /* Plugin constructor. */
579 static int tray_constructor(Plugin * p, char ** fp)
580 {
581 /* Read configuration from file. */
582 line s;
583 s.len = 256;
584 if (fp != NULL)
585 {
586 while (lxpanel_get_line(fp, &s) != LINE_BLOCK_END)
587 {
588 ERR("tray: illegal in this context %s\n", s.str);
589 return 0;
590 }
591 }
592
593 /* Allocate plugin context and set into Plugin private data pointer and static variable. */
594 TrayPlugin * tr = g_new0(TrayPlugin, 1);
595 p->priv = tr;
596 tr->plugin = p;
597
598 /* Get the screen and display. */
599 GdkScreen * screen = gtk_widget_get_screen(GTK_WIDGET(p->panel->topgwin));
600 Screen * xscreen = GDK_SCREEN_XSCREEN(screen);
601 GdkDisplay * display = gdk_screen_get_display(screen);
602
603 /* Create the selection atom. This has the screen number in it, so cannot be done ahead of time. */
604 char * selection_atom_name = g_strdup_printf("_NET_SYSTEM_TRAY_S%d", gdk_screen_get_number(screen));
605 Atom selection_atom = gdk_x11_get_xatom_by_name_for_display(display, selection_atom_name);
606 tr->selection_atom = gdk_atom_intern(selection_atom_name, FALSE);
607 g_free(selection_atom_name);
608
609 /* If the selection is already owned, there is another tray running. */
610 if (XGetSelectionOwner(GDK_DISPLAY_XDISPLAY(display), selection_atom) != None)
611 {
612 ERR("tray: another systray already running\n");
613 return 1;
614 }
615
616 /* Create an invisible window to hold the selection. */
617 GtkWidget * invisible = gtk_invisible_new_for_screen(screen);
618 gtk_widget_realize(invisible);
619 gtk_widget_add_events(invisible, GDK_PROPERTY_CHANGE_MASK | GDK_STRUCTURE_MASK);
620
621 /* Try to claim the _NET_SYSTEM_TRAY_Sn selection. */
622 #if GTK_CHECK_VERSION(2,14,0)
623 guint32 timestamp = gdk_x11_get_server_time(gtk_widget_get_window(invisible));
624 #else
625 guint32 timestamp = gdk_x11_get_server_time(invisible->window);
626 #endif
627 if (gdk_selection_owner_set_for_display(
628 display,
629 #if GTK_CHECK_VERSION(2,14,0)
630 gtk_widget_get_window(invisible),
631 #else
632 invisible->window,
633 #endif
634 tr->selection_atom,
635 timestamp,
636 TRUE))
637 {
638 /* Send MANAGER client event (ICCCM). */
639 XClientMessageEvent xev;
640 xev.type = ClientMessage;
641 xev.window = RootWindowOfScreen(xscreen);
642 xev.message_type = a_MANAGER;
643 xev.format = 32;
644 xev.data.l[0] = timestamp;
645 xev.data.l[1] = selection_atom;
646 #if GTK_CHECK_VERSION(2,14,0)
647 xev.data.l[2] = GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible));
648 #else
649 xev.data.l[2] = GDK_WINDOW_XWINDOW(invisible->window);
650 #endif
651 xev.data.l[3] = 0; /* manager specific data */
652 xev.data.l[4] = 0; /* manager specific data */
653 XSendEvent(GDK_DISPLAY_XDISPLAY(display), RootWindowOfScreen(xscreen), False, StructureNotifyMask, (XEvent *) &xev);
654
655 /* Set the orientation property.
656 * We always set "horizontal" since even vertical panels are designed to use a lot of width. */
657 gulong data = SYSTEM_TRAY_ORIENTATION_HORZ;
658 XChangeProperty(
659 GDK_DISPLAY_XDISPLAY(display),
660 #if GTK_CHECK_VERSION(2,14,0)
661 GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible)),
662 #else
663 GDK_WINDOW_XWINDOW(invisible->window),
664 #endif
665 a_NET_SYSTEM_TRAY_ORIENTATION,
666 XA_CARDINAL, 32,
667 PropModeReplace,
668 (guchar *) &data, 1);
669
670 /* Add GDK event filter. */
671 gdk_window_add_filter(NULL, (GdkFilterFunc) tray_event_filter, tr);
672
673 /* Reference the window since it is never added to a container. */
674 tr->invisible = invisible;
675 #if GTK_CHECK_VERSION(2,14,0)
676 tr->invisible_window = GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible));
677 #else
678 tr->invisible_window = GDK_WINDOW_XWINDOW(invisible->window);
679 #endif
680 g_object_ref(G_OBJECT(invisible));
681 }
682 else
683 {
684 gtk_widget_destroy(invisible);
685 g_printerr("tray: System tray didn't get the system tray manager selection\n");
686 return 0;
687 }
688
689 /* Allocate top level widget and set into Plugin widget pointer. */
690 p->pwid = gtk_event_box_new();
691 #if GTK_CHECK_VERSION(2,18,0)
692 gtk_widget_set_has_window(p->pwid,FALSE);
693 #else
694 GTK_WIDGET_SET_FLAGS(p->pwid, GTK_NO_WINDOW);
695 #endif
696 gtk_widget_set_name(p->pwid, "tray");
697 gtk_container_set_border_width(GTK_CONTAINER(p->pwid), 1);
698
699 /* Create an icon grid to manage the container. */
700 GtkOrientation bo = (p->panel->orientation == ORIENT_HORIZ) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
701 tr->icon_grid = icon_grid_new(p->panel, p->pwid, bo, p->panel->icon_size, p->panel->icon_size, 3, 0, p->panel->height);
702 return 1;
703 }
704
705 /* Plugin destructor. */
706 static void tray_destructor(Plugin * p)
707 {
708 TrayPlugin * tr = (TrayPlugin *) p->priv;
709
710 /* Remove GDK event filter. */
711 gdk_window_remove_filter(NULL, (GdkFilterFunc) tray_event_filter, tr);
712
713 /* Make sure we drop the manager selection. */
714 tray_unmanage_selection(tr);
715
716 /* Deallocate incomplete messages. */
717 while (tr->incomplete_messages != NULL)
718 {
719 BalloonMessage * msg_succ = tr->incomplete_messages->flink;
720 balloon_message_free(tr->incomplete_messages);
721 tr->incomplete_messages = msg_succ;
722 }
723
724 /* Terminate message display and deallocate messages. */
725 while (tr->messages != NULL)
726 balloon_message_advance(tr, TRUE, FALSE);
727
728 /* Deallocate client list. */
729 while (tr->client_list != NULL)
730 client_delete(tr, tr->client_list, TRUE);
731
732 /* Deallocate memory. */
733 if (tr->icon_grid != NULL)
734 icon_grid_free(tr->icon_grid);
735
736 g_free(tr);
737 }
738
739 /* Callback when panel configuration changes. */
740 static void tray_panel_configuration_changed(Plugin * p)
741 {
742 /* Set orientation into the icon grid. */
743 TrayPlugin * tr = (TrayPlugin *) p->priv;
744 if (tr->icon_grid != NULL)
745 {
746 GtkOrientation bo = (p->panel->orientation == ORIENT_HORIZ) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
747 icon_grid_set_geometry(tr->icon_grid, bo, p->panel->icon_size, p->panel->icon_size, 3, 0, p->panel->height);
748 }
749 }
750
751 /* Plugin descriptor. */
752 PluginClass tray_plugin_class = {
753
754 PLUGINCLASS_VERSIONING,
755
756 type : "tray",
757 name : N_("System Tray"),
758 version: "1.0",
759 description : N_("System tray"),
760
761 /* Set a flag to identify the system tray. It is special in that only one per system can exist. */
762 one_per_system : TRUE,
763
764 constructor : tray_constructor,
765 destructor : tray_destructor,
766 config : NULL,
767 save : NULL,
768 panel_configuration_changed : tray_panel_configuration_changed
769
770 };
771
772 /* vim: set sw=4 sts=4 et : */