2
* System tray plugin to lxpanel
4
* Copyright (c) 2008-2014 LxDE Developers, see the file AUTHORS for details.
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.
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.
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
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 */
30
#include <gdk-pixbuf/gdk-pixbuf.h>
31
#include <glib/gi18n.h>
35
#include "icon-grid.h"
37
/* Standards reference: http://standards.freedesktop.org/systemtray-spec/ */
39
/* Protocol constants. */
40
#define SYSTEM_TRAY_REQUEST_DOCK 0
41
#define SYSTEM_TRAY_BEGIN_MESSAGE 1
42
#define SYSTEM_TRAY_CANCEL_MESSAGE 2
44
#define SYSTEM_TRAY_ORIENTATION_HORZ 0
45
#define SYSTEM_TRAY_ORIENTATION_VERT 1
47
struct _balloon_message;
51
/* Representative of a balloon message. */
52
typedef struct _balloon_message {
53
struct _balloon_message * flink; /* Forward link */
54
Window window; /* X window ID */
55
long timeout; /* Time in milliseconds to display message; 0 if no timeout */
56
long length; /* Message string length */
57
long id; /* Client supplied unique message ID */
58
long remaining_length; /* Remaining length expected of incomplete message */
59
char * string; /* Message string */
62
/* Representative of a tray client. */
63
typedef struct _tray_client {
64
struct _tray_client * client_flink; /* Forward link to next task in X window ID order */
65
struct _tray_plugin * tr; /* Back pointer to tray plugin */
66
Window window; /* X window ID */
67
GtkWidget * socket; /* Socket */
70
/* Private context for system tray plugin. */
71
typedef struct _tray_plugin {
72
GtkWidget * plugin; /* Back pointer to Plugin */
74
TrayClient * client_list; /* List of tray clients */
75
BalloonMessage * incomplete_messages; /* List of balloon messages for which we are awaiting data */
76
BalloonMessage * messages; /* List of balloon messages actively being displayed or waiting to be displayed */
77
GtkWidget * balloon_message_popup; /* Popup showing balloon message */
78
guint balloon_message_timer; /* Timer controlling balloon message */
79
GtkWidget * invisible; /* Invisible window that holds manager selection */
80
Window invisible_window; /* X window ID of invisible window */
81
GdkAtom selection_atom; /* Atom for _NET_SYSTEM_TRAY_S%d */
84
static void balloon_message_display(TrayPlugin * tr, BalloonMessage * msg);
85
static void balloon_incomplete_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id);
86
static void balloon_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id);
87
static void tray_unmanage_selection(TrayPlugin * tr);
88
static void tray_destructor(gpointer user_data);
90
/* Look up a client in the client list. */
91
static TrayClient * client_lookup(TrayPlugin * tr, Window window)
94
for (tc = tr->client_list; tc != NULL; tc = tc->client_flink)
96
if (tc->window == window)
98
if (tc->window > window)
105
static void client_print(TrayPlugin * tr, char c, TrayClient * tc, XClientMessageEvent * xevent)
107
char *name = get_utf8_property(tc->window, a_NET_WM_NAME);
108
int pid = get_net_wm_pid(tc->window);
109
XClientMessageEvent xcm = {0};
112
g_debug("tray: %c%p, winid 0x%lx: %s (PID %d), plug %p, serial no %lu, send_event %c, format %d",
113
c, tc, tc->window, name, pid,
114
gtk_socket_get_plug_window(GTK_SOCKET(tc->socket)),
115
xevent->serial, xevent->send_event ? 'y' : 'n', xevent->format);
120
/* Delete a client. */
121
static void client_delete(TrayPlugin * tr, TrayClient * tc, gboolean unlink, gboolean remove)
123
//client_print(tr, '-', tc, NULL);
127
if (tr->client_list == tc)
128
tr->client_list = tc->client_flink;
131
/* Locate the task and its predecessor in the list and then remove it. For safety, ensure it is found. */
132
TrayClient * tc_pred = NULL;
133
TrayClient * tc_cursor;
135
tc_cursor = tr->client_list;
136
((tc_cursor != NULL) && (tc_cursor != tc));
137
tc_pred = tc_cursor, tc_cursor = tc_cursor->client_flink) ;
139
tc_pred->client_flink = tc->client_flink;
143
/* Clear out any balloon messages. */
144
balloon_incomplete_message_remove(tr, tc->window, TRUE, 0);
145
balloon_message_remove(tr, tc->window, TRUE, 0);
147
/* Remove the socket from the icon grid. */
149
gtk_widget_destroy(tc->socket);
151
/* Deallocate the client structure. */
155
/*** Balloon message display ***/
157
/* Free a balloon message structure. */
158
static void balloon_message_free(BalloonMessage * message)
160
g_free(message->string);
164
/* General code to deactivate a message and optionally display the next.
165
* This is used in three scenarios: balloon clicked, timeout expired, destructor. */
166
static void balloon_message_advance(TrayPlugin * tr, gboolean destroy_timer, gboolean display_next)
168
/* Remove the message from the queue. */
169
BalloonMessage * msg = tr->messages;
170
tr->messages = msg->flink;
172
/* Cancel the timer, if set. This is not done when the timer has expired. */
173
if ((destroy_timer) && (tr->balloon_message_timer != 0))
174
g_source_remove(tr->balloon_message_timer);
175
tr->balloon_message_timer = 0;
177
/* Destroy the widget. */
178
if (tr->balloon_message_popup != NULL)
179
gtk_widget_destroy(tr->balloon_message_popup);
180
tr->balloon_message_popup = NULL;
182
/* Free the message. */
183
balloon_message_free(msg);
185
/* If there is another message waiting in the queue, display it. This is not done in the destructor. */
186
if ((display_next) && (tr->messages != NULL))
187
balloon_message_display(tr, tr->messages);
190
/* Handler for "button-press-event" from balloon message popup menu item. */
191
static gboolean balloon_message_activate_event(GtkWidget * widget, GdkEventButton * event, TrayPlugin * tr)
193
balloon_message_advance(tr, TRUE, TRUE);
197
/* Timer expiration for balloon message. */
198
static gboolean balloon_message_timeout(TrayPlugin * tr)
200
if (!g_source_is_destroyed(g_main_current_source()))
201
balloon_message_advance(tr, FALSE, TRUE);
205
/* Create the graphic elements to display a balloon message. */
206
static void balloon_message_display(TrayPlugin * tr, BalloonMessage * msg)
208
/* Create a window and an item containing the text. */
209
tr->balloon_message_popup = gtk_window_new(GTK_WINDOW_POPUP);
210
GtkWidget * balloon_text = gtk_label_new(msg->string);
211
gtk_label_set_line_wrap(GTK_LABEL(balloon_text), TRUE);
212
gtk_misc_set_alignment(GTK_MISC(balloon_text), 0.5, 0.5);
213
gtk_container_add(GTK_CONTAINER(tr->balloon_message_popup), balloon_text);
214
gtk_widget_show(balloon_text);
215
gtk_container_set_border_width(GTK_CONTAINER(tr->balloon_message_popup), 4);
217
/* Connect signals. Clicking the popup dismisses it and displays the next message, if any. */
218
gtk_widget_add_events(tr->balloon_message_popup, GDK_BUTTON_PRESS_MASK);
219
g_signal_connect(tr->balloon_message_popup, "button-press-event", G_CALLBACK(balloon_message_activate_event), (gpointer) tr);
221
/* Compute the desired position in screen coordinates near the tray plugin. */
224
lxpanel_plugin_popup_set_position_helper(tr->panel, tr->plugin, tr->balloon_message_popup, &x, &y);
226
/* Show the popup. */
227
gtk_window_move(GTK_WINDOW(tr->balloon_message_popup), x, y);
228
gtk_widget_show(tr->balloon_message_popup);
230
/* Set a timer, if the client specified one. Both are in units of milliseconds. */
231
if (msg->timeout != 0)
232
tr->balloon_message_timer = g_timeout_add(msg->timeout, (GSourceFunc) balloon_message_timeout, tr);
235
/* Add a balloon message to the tail of the message queue. If it is the only element, display it immediately. */
236
static void balloon_message_queue(TrayPlugin * tr, BalloonMessage * msg)
238
if (tr->messages == NULL)
241
balloon_message_display(tr, msg);
245
BalloonMessage * msg_pred;
246
for (msg_pred = tr->messages; ((msg_pred != NULL) && (msg_pred->flink != NULL)); msg_pred = msg_pred->flink) ;
247
if (msg_pred != NULL)
248
msg_pred->flink = msg;
252
/* Remove an incomplete message from the queue, selected by window and optionally also client's ID.
253
* Used in two scenarios: client issues CANCEL (ID significant), client plug removed (ID don't care). */
254
static void balloon_incomplete_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id)
256
BalloonMessage * msg_pred = NULL;
257
BalloonMessage * msg = tr->incomplete_messages;
260
/* Establish successor in case of deletion. */
261
BalloonMessage * msg_succ = msg->flink;
263
if ((msg->window == window) && ((all_ids) || (msg->id == id)))
265
/* Found a message matching the criteria. Unlink and free it. */
266
if (msg_pred == NULL)
267
tr->incomplete_messages = msg->flink;
269
msg_pred->flink = msg->flink;
270
balloon_message_free(msg);
275
/* Advance to successor. */
280
/* Remove a message from the message queue, selected by window and optionally also client's ID.
281
* Used in two scenarios: client issues CANCEL (ID significant), client plug removed (ID don't care). */
282
static void balloon_message_remove(TrayPlugin * tr, Window window, gboolean all_ids, long id)
284
BalloonMessage * msg_pred = NULL;
285
BalloonMessage * msg_head = tr->messages;
286
BalloonMessage * msg = msg_head;
289
/* Establish successor in case of deletion. */
290
BalloonMessage * msg_succ = msg->flink;
292
if ((msg->window == window) && ((all_ids) || (msg->id == id)))
294
/* Found a message matching the criteria. */
295
if (msg_pred == NULL)
297
/* The message is at the queue head, so is being displayed. Stop the display. */
298
tr->messages = msg->flink;
299
if (tr->balloon_message_timer != 0)
301
g_source_remove(tr->balloon_message_timer);
302
tr->balloon_message_timer = 0;
304
if (tr->balloon_message_popup != NULL)
306
gtk_widget_destroy(tr->balloon_message_popup);
307
tr->balloon_message_popup = NULL;
311
msg_pred->flink = msg->flink;
313
/* Free the message. */
314
balloon_message_free(msg);
319
/* Advance to successor. */
323
/* If there is a new message head, display it now. */
324
if ((tr->messages != msg_head) && (tr->messages != NULL))
325
balloon_message_display(tr, tr->messages);
328
/*** Event interfaces ***/
330
/* Handle a balloon message SYSTEM_TRAY_BEGIN_MESSAGE event. */
331
static void balloon_message_begin_event(TrayPlugin * tr, XClientMessageEvent * xevent)
333
TrayClient * client = client_lookup(tr, xevent->window);
336
/* Check if the message ID already exists. */
337
balloon_incomplete_message_remove(tr, xevent->window, FALSE, xevent->data.l[4]);
339
/* Allocate a BalloonMessage structure describing the message. */
340
BalloonMessage * msg = g_new0(BalloonMessage, 1);
341
msg->window = xevent->window;
342
msg->timeout = xevent->data.l[2];
343
msg->length = xevent->data.l[3];
344
msg->id = xevent->data.l[4];
345
msg->remaining_length = msg->length;
346
msg->string = g_new0(char, msg->length + 1);
348
/* Message length of 0 indicates that no follow-on messages will be sent. */
349
if (msg->length == 0)
350
balloon_message_queue(tr, msg);
353
/* Add the new message to the queue to await its message text. */
354
msg->flink = tr->incomplete_messages;
355
tr->incomplete_messages = msg;
360
/* Handle a balloon message SYSTEM_TRAY_CANCEL_MESSAGE event. */
361
static void balloon_message_cancel_event(TrayPlugin * tr, XClientMessageEvent * xevent)
363
/* Remove any incomplete messages on this window with the specified ID. */
364
balloon_incomplete_message_remove(tr, xevent->window, TRUE, 0);
366
/* Remove any displaying or waiting messages on this window with the specified ID. */
367
TrayClient * client = client_lookup(tr, xevent->window);
369
balloon_message_remove(tr, xevent->window, FALSE, xevent->data.l[2]);
372
/* Handle a balloon message _NET_SYSTEM_TRAY_MESSAGE_DATA event. */
373
static void balloon_message_data_event(TrayPlugin * tr, XClientMessageEvent * xevent)
375
/* Look up the pending message in the list. */
376
BalloonMessage * msg_pred = NULL;
377
BalloonMessage * msg;
378
for (msg = tr->incomplete_messages; msg != NULL; msg_pred = msg, msg = msg->flink)
380
if (xevent->window == msg->window)
382
/* Append the message segment to the message. */
383
int length = MIN(msg->remaining_length, 20);
384
memcpy((msg->string + msg->length - msg->remaining_length), &xevent->data, length);
385
msg->remaining_length -= length;
387
/* If the message has been completely collected, display it. */
388
if (msg->remaining_length == 0)
390
/* Unlink the message from the structure. */
391
if (msg_pred == NULL)
392
tr->incomplete_messages = msg->flink;
394
msg_pred->flink = msg->flink;
396
/* If the client window is valid, queue the message. Otherwise discard it. */
397
TrayClient * client = client_lookup(tr, msg->window);
399
balloon_message_queue(tr, msg);
401
balloon_message_free(msg);
408
/* Handler for request dock message. */
409
static void trayclient_request_dock(TrayPlugin * tr, XClientMessageEvent * xevent)
411
/* Search for the window in the client list. Set up context to do an insert right away if needed. */
412
TrayClient * tc_pred = NULL;
413
TrayClient * tc_cursor;
414
for (tc_cursor = tr->client_list; tc_cursor != NULL; tc_pred = tc_cursor, tc_cursor = tc_cursor->client_flink)
416
if (tc_cursor->window == (Window)xevent->data.l[2])
417
return; /* We already got this notification earlier, ignore this one. */
418
if (tc_cursor->window > (Window)xevent->data.l[2])
422
/* Allocate and initialize new client structure. */
423
TrayClient * tc = g_new0(TrayClient, 1);
424
tc->window = xevent->data.l[2];
427
/* Allocate a socket. This is the tray side of the Xembed connection. */
428
tc->socket = gtk_socket_new();
430
/* Add the socket to the icon grid. */
431
gtk_container_add(GTK_CONTAINER(tr->plugin), tc->socket);
432
gtk_widget_show(tc->socket);
434
/* Connect the socket to the plug. This can only be done after the socket is realized. */
435
gtk_socket_add_id(GTK_SOCKET(tc->socket), tc->window);
437
//fprintf(stderr, "Notice: checking plug %ud\n", tc->window );
438
/* Checks if the plug has been created inside of the socket. */
439
if (gtk_socket_get_plug_window ( GTK_SOCKET(tc->socket) ) == NULL) {
440
//fprintf(stderr, "Notice: removing plug %ud\n", tc->window );
441
gtk_widget_destroy(tc->socket);
446
/* Link the client structure into the client list. */
449
tc->client_flink = tr->client_list;
450
tr->client_list = tc;
454
tc->client_flink = tc_pred->client_flink;
455
tc_pred->client_flink = tc;
459
/* GDK event filter. */
460
static GdkFilterReturn tray_event_filter(XEvent * xev, GdkEvent * event, TrayPlugin * tr)
462
if (xev->type == DestroyNotify)
464
/* Look for DestroyNotify events on tray icon windows and update state.
465
* We do it this way rather than with a "plug_removed" event because delivery
466
* of plug_removed events is observed to be unreliable if the client
467
* disconnects within less than 10 ms. */
468
XDestroyWindowEvent * xev_destroy = (XDestroyWindowEvent *) xev;
469
TrayClient * tc = client_lookup(tr, xev_destroy->window);
471
client_delete(tr, tc, TRUE, TRUE);
474
else if (xev->type == ClientMessage)
476
if (xev->xclient.message_type == a_NET_SYSTEM_TRAY_OPCODE)
478
/* Client message of type _NET_SYSTEM_TRAY_OPCODE.
479
* Dispatch on the request. */
480
switch (xev->xclient.data.l[1])
482
case SYSTEM_TRAY_REQUEST_DOCK:
483
/* If a Request Dock event on the invisible window, which is holding the manager selection, execute it. */
484
if (xev->xclient.window == tr->invisible_window)
486
trayclient_request_dock(tr, (XClientMessageEvent *) xev);
487
return GDK_FILTER_REMOVE;
491
case SYSTEM_TRAY_BEGIN_MESSAGE:
492
/* If a Begin Message event. look up the tray icon and execute it. */
493
balloon_message_begin_event(tr, (XClientMessageEvent *) xev);
494
return GDK_FILTER_REMOVE;
496
case SYSTEM_TRAY_CANCEL_MESSAGE:
497
/* If a Cancel Message event. look up the tray icon and execute it. */
498
balloon_message_cancel_event(tr, (XClientMessageEvent *) xev);
499
return GDK_FILTER_REMOVE;
503
else if (xev->xclient.message_type == a_NET_SYSTEM_TRAY_MESSAGE_DATA)
505
/* Client message of type _NET_SYSTEM_TRAY_MESSAGE_DATA.
506
* Look up the tray icon and execute it. */
507
balloon_message_data_event(tr, (XClientMessageEvent *) xev);
508
return GDK_FILTER_REMOVE;
512
else if ((xev->type == SelectionClear)
513
&& (xev->xclient.window == tr->invisible_window))
515
/* Look for SelectionClear events on the invisible window, which is holding the manager selection.
516
* This should not happen. */
517
tray_unmanage_selection(tr);
520
return GDK_FILTER_CONTINUE;
523
/* Delete the selection on the invisible window. */
524
static void tray_unmanage_selection(TrayPlugin * tr)
526
if (tr->invisible != NULL)
528
GtkWidget * invisible = tr->invisible;
529
GdkDisplay * display = gtk_widget_get_display(invisible);
530
if (gdk_selection_owner_get_for_display(display, tr->selection_atom) == gtk_widget_get_window(invisible))
532
guint32 timestamp = gdk_x11_get_server_time(gtk_widget_get_window(invisible));
533
gdk_selection_owner_set_for_display(
541
/* Destroy the invisible window. */
542
tr->invisible = NULL;
543
tr->invisible_window = None;
544
gtk_widget_destroy(invisible);
545
g_object_unref(G_OBJECT(invisible));
549
/* Plugin constructor. */
550
static GtkWidget *tray_constructor(LXPanel *panel, config_setting_t *settings)
554
/* Get the screen and display. */
555
GdkScreen * screen = gtk_widget_get_screen(GTK_WIDGET(panel));
556
Screen * xscreen = GDK_SCREEN_XSCREEN(screen);
557
GdkDisplay * display = gdk_screen_get_display(screen);
559
/* Create the selection atom. This has the screen number in it, so cannot be done ahead of time. */
560
char * selection_atom_name = g_strdup_printf("_NET_SYSTEM_TRAY_S%d", gdk_screen_get_number(screen));
561
Atom selection_atom = gdk_x11_get_xatom_by_name_for_display(display, selection_atom_name);
562
GdkAtom gdk_selection_atom = gdk_atom_intern(selection_atom_name, FALSE);
563
g_free(selection_atom_name);
565
/* If the selection is already owned, there is another tray running. */
566
if (XGetSelectionOwner(GDK_DISPLAY_XDISPLAY(display), selection_atom) != None)
568
g_warning("tray: another systray already running");
572
/* Create an invisible window to hold the selection. */
573
GtkWidget * invisible = gtk_invisible_new_for_screen(screen);
574
gtk_widget_realize(invisible);
575
gtk_widget_add_events(invisible, GDK_PROPERTY_CHANGE_MASK | GDK_STRUCTURE_MASK);
577
/* Try to claim the _NET_SYSTEM_TRAY_Sn selection. */
578
guint32 timestamp = gdk_x11_get_server_time(gtk_widget_get_window(invisible));
579
if (gdk_selection_owner_set_for_display(
581
gtk_widget_get_window(invisible),
586
/* Send MANAGER client event (ICCCM). */
587
XClientMessageEvent xev;
588
xev.type = ClientMessage;
589
xev.window = RootWindowOfScreen(xscreen);
590
xev.message_type = a_MANAGER;
592
xev.data.l[0] = timestamp;
593
xev.data.l[1] = selection_atom;
594
xev.data.l[2] = GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible));
595
xev.data.l[3] = 0; /* manager specific data */
596
xev.data.l[4] = 0; /* manager specific data */
597
XSendEvent(GDK_DISPLAY_XDISPLAY(display), RootWindowOfScreen(xscreen), False, StructureNotifyMask, (XEvent *) &xev);
599
/* Set the orientation property.
600
* We always set "horizontal" since even vertical panels are designed to use a lot of width. */
601
gulong data = SYSTEM_TRAY_ORIENTATION_HORZ;
603
GDK_DISPLAY_XDISPLAY(display),
604
GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible)),
605
a_NET_SYSTEM_TRAY_ORIENTATION,
608
(guchar *) &data, 1);
612
gtk_widget_destroy(invisible);
613
g_printerr("tray: System tray didn't get the system tray manager selection\n");
617
/* Allocate plugin context and set into Plugin private data pointer and static variable. */
618
TrayPlugin * tr = g_new0(TrayPlugin, 1);
620
tr->selection_atom = gdk_selection_atom;
621
/* Add GDK event filter. */
622
gdk_window_add_filter(NULL, (GdkFilterFunc) tray_event_filter, tr);
623
/* Reference the window since it is never added to a container. */
624
tr->invisible = g_object_ref_sink(G_OBJECT(invisible));
625
tr->invisible_window = GDK_WINDOW_XWINDOW(gtk_widget_get_window(invisible));
627
/* Allocate top level widget and set into Plugin widget pointer. */
628
tr->plugin = p = panel_icon_grid_new(panel_get_orientation(panel),
629
panel_get_icon_size(panel),
630
panel_get_icon_size(panel),
631
3, 0, panel_get_height(panel));
632
lxpanel_plugin_set_data(p, tr, tray_destructor);
633
gtk_widget_set_name(p, "tray");
634
gtk_container_set_border_width(GTK_CONTAINER(p), 1);
639
/* Plugin destructor. */
640
static void tray_destructor(gpointer user_data)
642
TrayPlugin * tr = user_data;
644
/* Remove GDK event filter. */
645
gdk_window_remove_filter(NULL, (GdkFilterFunc) tray_event_filter, tr);
647
/* Make sure we drop the manager selection. */
648
tray_unmanage_selection(tr);
650
/* Deallocate incomplete messages. */
651
while (tr->incomplete_messages != NULL)
653
BalloonMessage * msg_succ = tr->incomplete_messages->flink;
654
balloon_message_free(tr->incomplete_messages);
655
tr->incomplete_messages = msg_succ;
658
/* Terminate message display and deallocate messages. */
659
while (tr->messages != NULL)
660
balloon_message_advance(tr, TRUE, FALSE);
662
/* Deallocate client list - widgets are already destroyed. */
663
while (tr->client_list != NULL)
664
client_delete(tr, tr->client_list, TRUE, FALSE);
669
/* Callback when panel configuration changes. */
670
static void tray_panel_configuration_changed(LXPanel *panel, GtkWidget *p)
672
/* Set orientation into the icon grid. */
673
panel_icon_grid_set_geometry(PANEL_ICON_GRID(p), panel_get_orientation(panel),
674
panel_get_icon_size(panel),
675
panel_get_icon_size(panel),
676
3, 0, panel_get_height(panel));
679
/* Plugin descriptor. */
680
LXPanelPluginInit lxpanel_static_plugin_tray = {
681
.name = N_("System Tray"),
682
.description = N_("System tray"),
684
/* Set a flag to identify the system tray. It is special in that only one per system can exist. */
685
.one_per_system = TRUE,
687
.new_instance = tray_constructor,
688
.reconfigure = tray_panel_configuration_changed
691
/* vim: set sw=4 sts=4 et : */