Merging upstream version 0.8.0 (Closes: #639729, #761971).
[debian/lxpanel.git] / plugins / dclock.c
CommitLineData
6cc5e1a6 1/**
00916e98 2 * Copyright (c) 2006-2014 LxDE Developers, see the file AUTHORS for details.
6cc5e1a6
DB
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software Foundation,
16 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
00916e98 19#include "plugin.h"
89173f95 20#include "misc.h"
00916e98
AG
21
22#include <libfm/fm-gtk.h>
23
6cc5e1a6
DB
24#include <time.h>
25#include <sys/time.h>
26#include <sys/types.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <string.h>
30#include <glib/gi18n.h>
6cc5e1a6 31
6cc5e1a6
DB
32#include "dbg.h"
33
34#define DEFAULT_TIP_FORMAT "%A %x"
35#define DEFAULT_CLOCK_FORMAT "%R"
36
1ea75322 37/* Private context for digital clock plugin. */
6cc5e1a6 38typedef struct {
00916e98
AG
39 GtkWidget * plugin; /* Back pointer to plugin */
40 LXPanel * panel;
41 config_setting_t *settings;
1ea75322
DB
42 GtkWidget * clock_label; /* Label containing clock value */
43 GtkWidget * clock_icon; /* Icon when icon_only */
44 GtkWidget * calendar_window; /* Calendar window, if it is being displayed */
45 char * clock_format; /* Format string for clock value */
46 char * tooltip_format; /* Format string for tooltip value */
47 char * action; /* Command to execute on a click */
48 gboolean bold; /* True if bold font */
49 gboolean icon_only; /* True if icon only (no clock value) */
f8c25730 50 gboolean center_text;
1ea75322 51 guint timer; /* Timer for periodic update */
24d886e1
DB
52 enum {
53 AWAITING_FIRST_CHANGE, /* Experimenting to determine interval, waiting for first change */
54 AWAITING_SECOND_CHANGE, /* Experimenting to determine interval, waiting for second change */
55 ONE_SECOND_INTERVAL, /* Determined that one second interval is necessary */
56 ONE_MINUTE_INTERVAL /* Determined that one minute interval is sufficient */
00916e98 57 } expiration_interval;
24d886e1
DB
58 int experiment_count; /* Count of experiments that have been done to determine interval */
59 char * prev_clock_value; /* Previous value of clock */
60 char * prev_tooltip_value; /* Previous value of tooltip */
1ea75322
DB
61} DClockPlugin;
62
1ea75322 63static gboolean dclock_update_display(DClockPlugin * dc);
00916e98
AG
64static void dclock_destructor(gpointer user_data);
65static gboolean dclock_apply_configuration(gpointer user_data);
1ea75322
DB
66
67/* Display a window containing the standard calendar widget. */
934ecce5 68static GtkWidget * dclock_create_calendar(DClockPlugin * dc)
6cc5e1a6 69{
1ea75322
DB
70 /* Create a new window. */
71 GtkWidget * win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
89173f95
AG
72 gint x, y;
73
6cc5e1a6
DB
74 gtk_window_set_default_size(GTK_WINDOW(win), 180, 180);
75 gtk_window_set_decorated(GTK_WINDOW(win), FALSE);
1ea75322 76 gtk_window_set_resizable(GTK_WINDOW(win), FALSE);
6cc5e1a6
DB
77 gtk_container_set_border_width(GTK_CONTAINER(win), 5);
78 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(win), TRUE);
79 gtk_window_set_skip_pager_hint(GTK_WINDOW(win), TRUE);
00916e98 80 gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_UTILITY);
1ea75322
DB
81 gtk_window_stick(GTK_WINDOW(win));
82
83 /* Create a vertical box as a child of the window. */
84 GtkWidget * box = gtk_vbox_new(FALSE, 0);
10862fa6 85 gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(box));
6cc5e1a6 86
1ea75322
DB
87 /* Create a standard calendar widget as a child of the vertical box. */
88 GtkWidget * calendar = gtk_calendar_new();
00916e98 89 gtk_calendar_set_display_options(
1ea75322
DB
90 GTK_CALENDAR(calendar),
91 GTK_CALENDAR_SHOW_WEEK_NUMBERS | GTK_CALENDAR_SHOW_DAY_NAMES | GTK_CALENDAR_SHOW_HEADING);
00916e98 92 gtk_box_pack_start(GTK_BOX(box), calendar, TRUE, TRUE, 0);
89173f95 93 gtk_widget_show_all(box);
6cc5e1a6 94
89173f95
AG
95 /* Preset the widget position right now to not move it across the screen */
96 lxpanel_plugin_popup_set_position_helper(dc->panel, dc->plugin, win, &x, &y);
97 gtk_window_move(GTK_WINDOW(win), x, y);
934ecce5 98
1ea75322
DB
99 /* Return the widget. */
100 return win;
6cc5e1a6
DB
101}
102
1ea75322 103/* Handler for "button-press-event" event from main widget. */
00916e98 104static gboolean dclock_button_press_event(GtkWidget * widget, GdkEventButton * evt, LXPanel * panel)
6cc5e1a6 105{
00916e98 106 DClockPlugin * dc = lxpanel_plugin_get_data(widget);
6cc5e1a6 107
1ea75322
DB
108 /* If an action is set, execute it. */
109 if (dc->action != NULL)
00916e98 110 fm_launch_command_simple(NULL, NULL, 0, dc->action, NULL);
1ea75322
DB
111
112 /* If no action is set, toggle the presentation of the calendar. */
113 else
114 {
115 if (dc->calendar_window == NULL)
116 {
934ecce5 117 dc->calendar_window = dclock_create_calendar(dc);
1ea75322
DB
118 gtk_widget_show_all(dc->calendar_window);
119 }
120 else
121 {
122 gtk_widget_destroy(dc->calendar_window);
123 dc->calendar_window = NULL;
6cc5e1a6
DB
124 }
125 }
1ea75322 126 return TRUE;
6cc5e1a6
DB
127}
128
24d886e1 129/* Set the timer. */
00916e98 130static void dclock_timer_set(DClockPlugin * dc, struct timeval *current_time)
24d886e1
DB
131{
132 int milliseconds = 1000;
133
134 /* Get current time to millisecond resolution. */
00916e98 135 if (gettimeofday(current_time, NULL) >= 0)
24d886e1
DB
136 {
137 /* Compute number of milliseconds until next second boundary. */
00916e98 138 milliseconds = 1000 - (current_time->tv_usec / 1000);
24d886e1
DB
139
140 /* If the expiration interval is the minute boundary,
141 * add number of milliseconds after that until next minute boundary. */
142 if (dc->expiration_interval == ONE_MINUTE_INTERVAL)
143 {
00916e98 144 time_t seconds = 60 - (current_time->tv_sec - (current_time->tv_sec / 60) * 60);
24d886e1
DB
145 milliseconds += seconds * 1000;
146 }
147 }
148
149 /* Be defensive, and set the timer. */
150 if (milliseconds <= 0)
151 milliseconds = 1000;
152 dc->timer = g_timeout_add(milliseconds, (GSourceFunc) dclock_update_display, (gpointer) dc);
153}
154
1ea75322
DB
155/* Periodic timer callback.
156 * Also used during initialization and configuration change to do a redraw. */
157static gboolean dclock_update_display(DClockPlugin * dc)
6cc5e1a6 158{
1ea75322 159 /* Determine the current time. */
00916e98
AG
160 struct timeval now;
161 struct tm * current_time;
162
163 if (g_source_is_destroyed(g_main_current_source()))
164 return FALSE;
165
166 dclock_timer_set(dc, &now);
167 current_time = localtime(&now.tv_sec);
24d886e1
DB
168
169 /* Determine the content of the clock label and tooltip. */
170 char clock_value[64];
171 char tooltip_value[64];
172 clock_value[0] = '\0';
173 if (dc->clock_format != NULL)
174 strftime(clock_value, sizeof(clock_value), dc->clock_format, current_time);
175 tooltip_value[0] = '\0';
176 if (dc->tooltip_format != NULL)
177 strftime(tooltip_value, sizeof(tooltip_value), dc->tooltip_format, current_time);
6cc5e1a6 178
1ea75322 179 /* When we write the clock value, it causes the panel to do a full relayout.
24d886e1
DB
180 * Since this function may be called too often while the timing experiment is underway,
181 * we take the trouble to check if the string actually changed first. */
1ea75322 182 if (( ! dc->icon_only)
24d886e1 183 && ((dc->prev_clock_value == NULL) || (strcmp(dc->prev_clock_value, clock_value) != 0)))
1ea75322 184 {
1ea75322
DB
185 /* Convert "\n" escapes in the user's format string to newline characters. */
186 char * newlines_converted = NULL;
24d886e1 187 if (strstr(clock_value, "\\n") != NULL)
1ea75322 188 {
24d886e1 189 newlines_converted = g_strdup(clock_value); /* Just to get enough space for the converted result */
1ea75322
DB
190 char * p;
191 char * q;
24d886e1 192 for (p = clock_value, q = newlines_converted; *p != '\0'; p += 1)
1ea75322
DB
193 {
194 if ((p[0] == '\\') && (p[1] == 'n'))
195 {
196 *q++ = '\n';
197 p += 1;
198 }
199 else
200 *q++ = *p;
6cc5e1a6 201 }
1ea75322
DB
202 *q = '\0';
203 }
204
24d886e1 205 gchar * utf8 = g_locale_to_utf8(((newlines_converted != NULL) ? newlines_converted : clock_value), -1, NULL, NULL, NULL);
1ea75322
DB
206 if (utf8 != NULL)
207 {
00916e98 208 lxpanel_draw_label_text(dc->panel, dc->clock_label, utf8, dc->bold, 1, TRUE);
1ea75322
DB
209 g_free(utf8);
210 }
211 g_free(newlines_converted);
6cc5e1a6
DB
212 }
213
1ea75322 214 /* Determine the content of the tooltip. */
24d886e1 215 gchar * utf8 = g_locale_to_utf8(tooltip_value, -1, NULL, NULL, NULL);
1ea75322
DB
216 if (utf8 != NULL)
217 {
00916e98 218 gtk_widget_set_tooltip_text(dc->plugin, utf8);
1ea75322
DB
219 g_free(utf8);
220 }
24d886e1
DB
221
222 /* Conduct an experiment to see how often the value changes.
223 * Use this to decide whether we update the value every second or every minute.
224 * We need to account for the possibility that the experiment is being run when we cross a minute boundary. */
225 if (dc->expiration_interval < ONE_SECOND_INTERVAL)
226 {
227 if (dc->prev_clock_value == NULL)
228 {
229 /* Initiate the experiment. */
230 dc->prev_clock_value = g_strdup(clock_value);
231 dc->prev_tooltip_value = g_strdup(tooltip_value);
232 }
233 else
234 {
235 if (((dc->icon_only) || (strcmp(dc->prev_clock_value, clock_value) == 0))
236 && (strcmp(dc->prev_tooltip_value, tooltip_value) == 0))
237 {
238 dc->experiment_count += 1;
239 if (dc->experiment_count > 3)
240 {
241 /* No change within 3 seconds. Assume change no more often than once per minute. */
242 dc->expiration_interval = ONE_MINUTE_INTERVAL;
243 g_free(dc->prev_clock_value);
244 g_free(dc->prev_tooltip_value);
245 dc->prev_clock_value = NULL;
246 dc->prev_tooltip_value = NULL;
247 }
248 }
249 else if (dc->expiration_interval == AWAITING_FIRST_CHANGE)
250 {
251 /* We have a change at the beginning of the experiment, but we do not know when the next change might occur.
252 * Continue the experiment for 3 more seconds. */
253 dc->expiration_interval = AWAITING_SECOND_CHANGE;
254 dc->experiment_count = 0;
255 g_free(dc->prev_clock_value);
256 g_free(dc->prev_tooltip_value);
257 dc->prev_clock_value = g_strdup(clock_value);
258 dc->prev_tooltip_value = g_strdup(tooltip_value);
259 }
260 else
261 {
262 /* We have a second change. End the experiment. */
263 dc->expiration_interval = ((dc->experiment_count > 3) ? ONE_MINUTE_INTERVAL : ONE_SECOND_INTERVAL);
264 g_free(dc->prev_clock_value);
265 g_free(dc->prev_tooltip_value);
266 dc->prev_clock_value = NULL;
267 dc->prev_tooltip_value = NULL;
268 }
269 }
270 }
271
272 /* Reset the timer and return. */
24d886e1 273 return FALSE;
6cc5e1a6
DB
274}
275
1ea75322 276/* Plugin constructor. */
00916e98 277static GtkWidget *dclock_constructor(LXPanel *panel, config_setting_t *settings)
6cc5e1a6 278{
1ea75322
DB
279 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
280 DClockPlugin * dc = g_new0(DClockPlugin, 1);
00916e98
AG
281 GtkWidget * p;
282 const char *str;
283 int tmp_int;
6cc5e1a6 284
1ea75322 285 /* Load parameters from the configuration file. */
00916e98
AG
286 if (config_setting_lookup_string(settings, "ClockFmt", &str))
287 dc->clock_format = g_strdup(str);
288 if (config_setting_lookup_string(settings, "TooltipFmt", &str))
289 dc->tooltip_format = g_strdup(str);
290 if (config_setting_lookup_string(settings, "Action", &str))
291 dc->action = g_strdup(str);
292 if (config_setting_lookup_int(settings, "BoldFont", &tmp_int))
293 dc->bold = tmp_int != 0;
294 if (config_setting_lookup_int(settings, "IconOnly", &tmp_int))
295 dc->icon_only = tmp_int != 0;
296 if (config_setting_lookup_int(settings, "CenterText", &tmp_int))
297 dc->center_text = tmp_int != 0;
298
299 /* Save construction pointers */
300 dc->panel = panel;
301 dc->settings = settings;
6cc5e1a6 302
1ea75322 303 /* Allocate top level widget and set into Plugin widget pointer. */
00916e98
AG
304 dc->plugin = p = gtk_event_box_new();
305 lxpanel_plugin_set_data(p, dc, dclock_destructor);
6cc5e1a6 306
1ea75322 307 /* Allocate a horizontal box as the child of the top level. */
f8c25730 308 GtkWidget * hbox = gtk_hbox_new(TRUE, 0);
00916e98 309 gtk_container_add(GTK_CONTAINER(p), hbox);
1ea75322 310 gtk_widget_show(hbox);
6cc5e1a6 311
1ea75322
DB
312 /* Create a label and an image as children of the horizontal box.
313 * Only one of these is visible at a time, controlled by user preference. */
314 dc->clock_label = gtk_label_new(NULL);
315 gtk_misc_set_alignment(GTK_MISC(dc->clock_label), 0.5, 0.5);
316 gtk_misc_set_padding(GTK_MISC(dc->clock_label), 4, 0);
317 gtk_container_add(GTK_CONTAINER(hbox), dc->clock_label);
89173f95 318 dc->clock_icon = lxpanel_image_new_for_icon(panel, "clock", -1, NULL);
1ea75322 319 gtk_container_add(GTK_CONTAINER(hbox), dc->clock_icon);
6cc5e1a6 320
24d886e1
DB
321 /* Initialize the clock display. */
322 if (dc->clock_format == NULL)
0f7f2ef3 323 dc->clock_format = g_strdup(_(DEFAULT_CLOCK_FORMAT));
24d886e1 324 if (dc->tooltip_format == NULL)
0f7f2ef3 325 dc->tooltip_format = g_strdup(_(DEFAULT_TIP_FORMAT));
1ea75322 326 dclock_apply_configuration(p);
6cc5e1a6 327
1ea75322 328 /* Show the widget and return. */
00916e98 329 return p;
1ea75322 330}
6cc5e1a6 331
1ea75322 332/* Plugin destructor. */
00916e98 333static void dclock_destructor(gpointer user_data)
6cc5e1a6 334{
00916e98 335 DClockPlugin * dc = user_data;
6cc5e1a6 336
1ea75322
DB
337 /* Remove the timer. */
338 if (dc->timer != 0)
6cc5e1a6
DB
339 g_source_remove(dc->timer);
340
1ea75322
DB
341 /* Ensure that the calendar is dismissed. */
342 if (dc->calendar_window != NULL)
343 gtk_widget_destroy(dc->calendar_window);
10862fa6 344
1ea75322
DB
345 /* Deallocate all memory. */
346 g_free(dc->clock_format);
347 g_free(dc->tooltip_format);
6cc5e1a6 348 g_free(dc->action);
24d886e1
DB
349 g_free(dc->prev_clock_value);
350 g_free(dc->prev_tooltip_value);
1ea75322 351 g_free(dc);
6cc5e1a6
DB
352}
353
1ea75322 354/* Callback when the configuration dialog has recorded a configuration change. */
00916e98 355static gboolean dclock_apply_configuration(gpointer user_data)
6cc5e1a6 356{
00916e98
AG
357 GtkWidget * p = user_data;
358 DClockPlugin * dc = lxpanel_plugin_get_data(p);
359 struct timeval now;
360
361 /* stop the updater now */
362 if (dc->timer)
363 g_source_remove(dc->timer);
1ea75322
DB
364
365 /* Set up the icon or the label as the displayable widget. */
366 if (dc->icon_only)
367 {
1ea75322
DB
368 gtk_widget_show(dc->clock_icon);
369 gtk_widget_hide(dc->clock_label);
370 }
371 else
372 {
373 gtk_widget_show(dc->clock_label);
374 gtk_widget_hide(dc->clock_icon);
375 }
00916e98 376
f8c25730
DB
377 if (dc->center_text)
378 {
00916e98 379 gtk_label_set_justify(GTK_LABEL(dc->clock_label), GTK_JUSTIFY_CENTER);
f8c25730
DB
380 }
381 else
382 {
00916e98 383 gtk_label_set_justify(GTK_LABEL(dc->clock_label), GTK_JUSTIFY_LEFT);
f8c25730 384 }
1ea75322 385
24d886e1
DB
386 /* Rerun the experiment to determine update interval and update the display. */
387 g_free(dc->prev_clock_value);
388 g_free(dc->prev_tooltip_value);
389 dc->expiration_interval = AWAITING_FIRST_CHANGE;
390 dc->experiment_count = 0;
391 dc->prev_clock_value = NULL;
392 dc->prev_tooltip_value = NULL;
00916e98 393 dclock_timer_set(dc, &now);
934ecce5
DB
394
395 /* Hide the calendar. */
396 if (dc->calendar_window != NULL)
397 {
398 gtk_widget_destroy(dc->calendar_window);
399 dc->calendar_window = NULL;
400 }
00916e98
AG
401
402 /* Save configuration */
403 config_group_set_string(dc->settings, "ClockFmt", dc->clock_format);
404 config_group_set_string(dc->settings, "TooltipFmt", dc->tooltip_format);
405 config_group_set_string(dc->settings, "Action", dc->action);
406 config_group_set_int(dc->settings, "BoldFont", dc->bold);
407 config_group_set_int(dc->settings, "IconOnly", dc->icon_only);
408 config_group_set_int(dc->settings, "CenterText", dc->center_text);
409 return FALSE;
6cc5e1a6
DB
410}
411
1ea75322 412/* Callback when the configuration dialog is to be shown. */
00916e98 413static GtkWidget *dclock_configure(LXPanel *panel, GtkWidget *p)
6cc5e1a6 414{
00916e98
AG
415 DClockPlugin * dc = lxpanel_plugin_get_data(p);
416 return lxpanel_generic_config_dlg(_("Digital Clock"), panel,
417 dclock_apply_configuration, p,
1ea75322
DB
418 _("Clock Format"), &dc->clock_format, CONF_TYPE_STR,
419 _("Tooltip Format"), &dc->tooltip_format, CONF_TYPE_STR,
00916e98 420 _("Format codes: man 3 strftime; %n for line break"), NULL, CONF_TYPE_TRIM,
1ea75322
DB
421 _("Action when clicked (default: display calendar)"), &dc->action, CONF_TYPE_STR,
422 _("Bold font"), &dc->bold, CONF_TYPE_BOOL,
423 _("Tooltip only"), &dc->icon_only, CONF_TYPE_BOOL,
f8c25730 424 _("Center text"), &dc->center_text, CONF_TYPE_BOOL,
1ea75322 425 NULL);
6cc5e1a6
DB
426}
427
1ea75322 428/* Plugin descriptor. */
00916e98
AG
429LXPanelPluginInit lxpanel_static_plugin_dclock = {
430 .name = N_("Digital Clock"),
431 .description = N_("Display digital clock and tooltip"),
432
433 .new_instance = dclock_constructor,
434 .config = dclock_configure,
00916e98 435 .button_press_event = dclock_button_press_event
6cc5e1a6 436};