Added a graphical image representing increase/decrease in price.
[stockwidget] / lib-stock-home-widget.c
1 /*
2  * Simple stock widget
3  * Jon Parr
4  */
5
6 #include <gtk/gtk.h>
7 #include <gdk/gdk.h>
8 #include <string.h>
9 #include <hildon/hildon.h>
10 #include "lib-stock-home-widget.h"
11 #include "lib-stock-settings.h"
12
13 HD_DEFINE_PLUGIN_MODULE (StockPlugin, stock_plugin, HD_TYPE_HOME_PLUGIN_ITEM)
14
15 static void
16 OnClickUpdate(GtkWidget *widget, GdkEventButton *event, StockPluginContext *psContext)
17 {
18         char szBuffer[1024];
19
20         DebugOut(("OnClickUpdate"));
21         
22         sprintf(szBuffer,"Retrieving <span foreground=\"red\">%s</span>...",psContext->psSettings->ppszTickers[0]);
23
24         /* Hide the stock arrow when we're updating */
25         psContext->bArrowActive = FALSE;
26         gtk_widget_hide(GTK_WIDGET(psContext->psArrow));
27         
28         gtk_label_set_markup(GTK_LABEL(psContext->psLabel),szBuffer);
29         gtk_widget_queue_draw(GTK_WIDGET(psContext->psParent));
30 }
31
32 static void
33 OnClickRefresh(GtkWidget *widget, GdkEventButton *event, StockPluginContext *psContext)
34 {
35         DebugOut(("OnClickRefresh"));
36         
37         /* Disable button while getting latest stocks */
38         {
39                 char    szBuffer[1024]; 
40                 float   fPrice;
41                 float   fChange;
42
43                 if(RetrieveStockPrice(psContext->hSG,psContext->psSettings->ppszTickers[0],&fPrice,NULL,&fChange) == SG_OK)
44                 {
45                         char            *pszColour = NULL;
46                         time_t           rawtime;
47                         struct tm       *timeinfo;
48                         char             szTime[80];
49                         int                      nImageSel;
50
51                         time(&rawtime);
52                         timeinfo = localtime(&rawtime);
53                         strftime(szTime,80,"(%I:%M%p)",timeinfo);
54                         
55                         /* Highlight  the change in price */
56                         if(fChange > 0.0f)
57                         {
58                                 nImageSel = STOCK_IMAGE_INCREASE;
59                                 pszColour = "green";
60                         }
61                         else
62                         {
63                                 nImageSel = STOCK_IMAGE_DECREASE;
64                                 pszColour = "red";
65                         }
66
67                         gtk_image_set_from_pixbuf(GTK_IMAGE(psContext->psArrow),psContext->asStockImage[nImageSel]);
68
69                         if(!psContext->bArrowActive)
70                         {
71                                 gtk_widget_show(psContext->psArrow);
72                                 psContext->bArrowActive = TRUE;
73                         }
74
75                         /* Update to latest price */
76                         sprintf(szBuffer,
77                                         "%s %.2f <span foreground=\"%s\">%.2f%%</span> <span foreground=\"grey\">%s</span>",
78                                         psContext->psSettings->ppszTickers[0],fPrice,pszColour,fChange,szTime);
79
80                         gtk_label_set_markup(GTK_LABEL(psContext->psLabel),szBuffer);
81                         gtk_widget_queue_draw(GTK_WIDGET(psContext->psParent));
82                 }
83                 /*
84                   If it fails, leave the old price as the user can see the timestamp
85                   and know it's not up-to-date. So just notify them of the failure.
86                  */
87                 else
88                 {
89                         gtk_label_set_markup(GTK_LABEL(psContext->psLabel),"<span foreground=\"red\"><b>Failed</b></span>: Check ticker and try again.");
90                         gtk_widget_queue_draw(GTK_WIDGET(psContext->psParent));
91                 }
92         }
93 }
94
95 static gboolean
96 UpdateStockOnTimeout(gpointer data)
97 {
98         StockPluginContext *psContext = (StockPluginContext*)data;
99
100         DebugOut(("UpdateStockOnTimeout"));
101         
102         if(!psContext)
103                 return FALSE;
104
105         /* If no longer a timeout, don't do anything and stop */
106         if(!psContext->psSettings->uiUpdateTime)
107                 return FALSE;
108
109         /* Update stock price */
110         OnClickRefresh(GTK_WIDGET(psContext->psLabel),NULL,psContext);
111         return TRUE;
112 }
113
114 static void
115 stock_show_settings_dialog(GtkWidget* widget, gpointer data)
116 {
117         StockPluginContext      *psContext = (StockPluginContext*)data;
118         GtkWidget                       *dialog;
119         GtkWidget                       *content;
120         GtkSizeGroup            *group;
121         GtkWidget                       *addselector;
122         GtkWidget                       *addbutton;
123         GtkWidget                       *timeselector;
124         GtkWidget                       *timebutton;
125
126         DebugOut(("stock_show_settings_dialog"));
127         
128         /* Stop timer while settings are one */
129         if(psContext->nTimerID != -1)
130         {
131                 g_source_remove(psContext->nTimerID);
132                 psContext->nTimerID = -1;
133         }
134         
135         dialog  = gtk_dialog_new_with_buttons("Settings",NULL,0,"Save",GTK_RESPONSE_ACCEPT,NULL);
136         content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
137         group   = GTK_SIZE_GROUP(gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL));
138
139         /* Add new stock symbol */
140         {
141                 int i;
142                 
143                 addbutton = hildon_picker_button_new (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
144                                                                                           HILDON_BUTTON_ARRANGEMENT_HORIZONTAL);
145
146                 addselector = hildon_touch_selector_entry_new_text();
147
148                 for(i=0; i < psContext->psSettings->uiNumTickers; i++)
149                 {
150                         hildon_touch_selector_append_text(HILDON_TOUCH_SELECTOR(addselector),psContext->psSettings->ppszTickers[i]);
151                 }
152
153                 /* First ticker in ppszTickers is the chosen one */
154                 hildon_touch_selector_set_active(HILDON_TOUCH_SELECTOR(addselector), 0, 0);
155                 
156                 hildon_picker_button_set_selector (HILDON_PICKER_BUTTON (addbutton), HILDON_TOUCH_SELECTOR (addselector));
157                 
158                 hildon_button_set_title(HILDON_BUTTON(addbutton), "Select Ticker");
159         }
160
161         /* Set auto update time */
162         {
163                 int i;
164                 int sel = 0;
165
166                 char *aszUpdateTimes[] =
167                 {
168                         "No update",
169                         "1 minute",
170                         "15 minutes",
171                         "30 minutes",
172                         "1 hour"
173                 };
174                 
175                 timebutton = hildon_picker_button_new (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
176                                                                                           HILDON_BUTTON_ARRANGEMENT_HORIZONTAL);
177
178                 timeselector = hildon_touch_selector_new_text();
179
180                 for(i=0; i < sizeof(aszUpdateTimes)/sizeof(aszUpdateTimes[0]); i++)
181                 {
182                         hildon_touch_selector_append_text(HILDON_TOUCH_SELECTOR(timeselector),aszUpdateTimes[i]);
183                 }
184
185                 switch(psContext->psSettings->uiUpdateTime)
186                 {
187                         case 0:  sel = 0; break;
188                         case 1:  sel = 1; break;
189                         case 15: sel = 2; break;
190                         case 30: sel = 3; break;
191                         case 60: sel = 4; break;
192                 }
193                 
194                 hildon_touch_selector_set_active(HILDON_TOUCH_SELECTOR(timeselector), 0, sel);
195                 hildon_picker_button_set_selector (HILDON_PICKER_BUTTON (timebutton), HILDON_TOUCH_SELECTOR (timeselector));
196                 hildon_button_set_title(HILDON_BUTTON(timebutton), "Automatic Update Period");
197         }
198
199         gtk_container_add(GTK_CONTAINER(content),addbutton);
200         gtk_container_add(GTK_CONTAINER(content),timebutton);
201         gtk_widget_show_all(dialog);
202
203         if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) 
204         {
205                 const char *pszNewTicker;
206                 char aszOldTicker[255]={0};
207                 guint uiUpdateSelection = 0;
208
209                 /* Only perform ops if they actually changed the ticker */
210                 if(strcmp(aszOldTicker,psContext->psSettings->ppszTickers[0]))
211                 {
212                         /* Save off old ticker */
213                         strcpy(aszOldTicker,psContext->psSettings->ppszTickers[0]);
214                 
215                         /* Get new ticker */
216                         pszNewTicker = hildon_button_get_value(HILDON_BUTTON(addbutton));
217
218                         /* Check if ticker already exists */
219                         {
220                                 int i;
221                                 gboolean bFound = FALSE;
222                         
223                                 /* Check it doesn't exist already */
224                                 for(i=0; i < psContext->psSettings->uiNumTickers; i++)
225                                 {
226                                         if(!strcmp(pszNewTicker,psContext->psSettings->ppszTickers[i]))
227                                         {
228                                                 bFound = TRUE;
229                                                 break;
230                                         }
231                                 }
232
233                                 /* If they just chose another one, just swap the pointers so new one is at the top:
234                                  */
235                                 if(bFound)
236                                 {
237                                         /* Swapped ith and 0th element */
238                                         char *psTmp = psContext->psSettings->ppszTickers[i];
239                                         psContext->psSettings->ppszTickers[i] = psContext->psSettings->ppszTickers[0];
240                                         psContext->psSettings->ppszTickers[0] = psTmp;
241
242                                         DebugOut(("Swapped ticker %d (%s) with 0 (%s)",
243                                                           i,
244                                                           psContext->psSettings->ppszTickers[i],
245                                                           psContext->psSettings->ppszTickers[0]));
246                                 }
247                                 /* If it doesn't add it to the list, placing chosen one at the front */
248                                 else
249                                 {
250                                         gchar **ppszTickers = g_malloc0(sizeof(char*) * (psContext->psSettings->uiNumTickers+2));
251
252                                         /* Place the selected one first */
253                                         ppszTickers[0] = g_strdup(pszNewTicker);
254                                 
255                                         /* Copy across old tickers */
256                                         for(i=0; i < psContext->psSettings->uiNumTickers; i++)
257                                         {
258                                                 /* offset by one as the selected one is first */
259                                                 ppszTickers[i+1] = g_strdup(psContext->psSettings->ppszTickers[i]);
260                                         }
261
262                                         /* Free current array */
263                                         g_strfreev(psContext->psSettings->ppszTickers);
264
265                                         /* Assign new array */
266                                         psContext->psSettings->ppszTickers = ppszTickers;
267
268                                         /* Update count */
269                                         psContext->psSettings->uiNumTickers++;
270                                 }
271                         }
272
273                         /* Force a refresh as the ticker changed */
274                         OnClickRefresh(GTK_WIDGET(psContext->psLabel),NULL,psContext);
275                 }
276
277                 uiUpdateSelection = hildon_touch_selector_get_active(HILDON_TOUCH_SELECTOR(timeselector),0);
278                 
279                 switch(uiUpdateSelection)
280                 {
281                         /* No update */
282                         case 0: psContext->psSettings->uiUpdateTime =  0; break;
283                         case 1: psContext->psSettings->uiUpdateTime =  1; break;
284                         case 2: psContext->psSettings->uiUpdateTime = 15; break;
285                         case 3: psContext->psSettings->uiUpdateTime = 30; break;
286                         case 4: psContext->psSettings->uiUpdateTime = 60; break;
287                 }
288
289                 if(psContext->psSettings->uiUpdateTime != 0)
290                 {
291                         /* Remove previous timeout if there was one */
292                         if(psContext->nTimerID != -1)
293                         {
294                                 g_source_remove(psContext->nTimerID);
295                         }
296                         
297                         /* Add timer (in seconds) */
298                         psContext->nTimerID = g_timeout_add_seconds(psContext->psSettings->uiUpdateTime * 60,
299                                                                                                                 UpdateStockOnTimeout,
300                                                                                                                 psContext);
301                 }
302
303                 /* Save Settings */
304                 stock_save_settings(psContext->psSettings);
305         }
306         
307         gtk_widget_destroy (dialog);
308 }
309
310 static GtkWidget *stock_create_gui(gchar                                *pszStockLabel,
311                                                                    StockPluginContext   *psContext)
312 {
313         GtkWidget *hbox;
314         GtkWidget *arrow;
315         GtkWidget *label;
316         GtkWidget *eventbox;
317
318         DebugOut(("stock_create_gui"));
319
320         psContext->asStockImage[STOCK_IMAGE_INCREASE] = gdk_pixbuf_new_from_file(STOCK_PLUGIN_RESOURCE_UP_ARROW,   NULL);
321         psContext->asStockImage[STOCK_IMAGE_DECREASE] = gdk_pixbuf_new_from_file(STOCK_PLUGIN_RESOURCE_DOWN_ARROW, NULL);
322         
323         label    = gtk_label_new(pszStockLabel);
324         eventbox = gtk_event_box_new();
325         arrow    = gtk_image_new_from_pixbuf(psContext->asStockImage[0]);
326         hbox     = gtk_hbox_new(FALSE,5);
327
328         gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox),FALSE);
329         gtk_container_set_border_width(GTK_CONTAINER(eventbox),10);
330
331         gtk_box_pack_start(GTK_BOX(hbox), arrow, FALSE, FALSE, 0);
332         gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
333         
334         gtk_container_add(GTK_CONTAINER(eventbox),hbox);
335
336         g_signal_connect(GTK_CONTAINER(eventbox),"button-press-event",G_CALLBACK(OnClickUpdate),(gpointer)psContext);
337         g_signal_connect(GTK_CONTAINER(eventbox),"button-release-event",G_CALLBACK(OnClickRefresh),(gpointer)psContext);
338
339         /* Display all but the arrow */
340         gtk_widget_show_all(eventbox);
341         gtk_widget_hide(arrow); 
342         psContext->bArrowActive = FALSE;        
343
344         psContext->psArrow        = arrow;
345         psContext->psLabel        = label;
346         psContext->psEventBox = eventbox;
347         
348         return eventbox;
349 }
350
351 static void
352 stock_plugin_finalize (GObject *object)
353 {
354         StockPluginContext *psContext = ((StockPlugin*)object)->context;
355
356         DebugOut(("stock_plugin_finalize"));
357         
358         if(psContext)
359         {
360                 if(psContext->psSettings)
361                 {
362                         if(psContext->psSettings)
363                                 g_free(psContext->psSettings->iD);
364                         
365                         stock_free_settings(psContext->psSettings);
366                         g_free(psContext->psSettings);
367                 }
368
369                 if(psContext->nTimerID != -1)
370                 {
371                         g_source_remove(psContext->nTimerID);
372                         psContext->nTimerID = -1;
373                 }
374
375                 if(psContext->asStockImage[STOCK_IMAGE_DECREASE] != NULL)
376                         g_object_unref(psContext->asStockImage[STOCK_IMAGE_DECREASE]);
377                 
378                 if(psContext->asStockImage[STOCK_IMAGE_INCREASE] != NULL)
379                         g_object_unref(psContext->asStockImage[STOCK_IMAGE_INCREASE]);
380
381                 if(psContext->hSG)
382                         FreeStockGetter(psContext->hSG);
383                 
384                 g_free(psContext);
385         }
386 }
387
388 static void
389 stock_plugin_init (StockPlugin *desktop_plugin)
390 {
391         StockPluginContext *psContext;
392         SGHandle hSG;
393
394         DebugOut(("stock_plugin_init"));
395         
396         hSG = InitStockGetter();
397
398         if(!hSG)
399         {
400                 return;
401         }
402
403         /* Create plugin context */
404         psContext = g_new0(StockPluginContext,1);
405
406         /* Always create a settings */
407         psContext->psSettings     = g_new0(StockPluginSettings,1);
408         psContext->psSettings->iD = g_strdup(STOCK_PLUGIN_APPLET_ID);
409
410         /* Attempt to load settings (defaulted if not present) */
411         stock_read_settings(psContext->psSettings);
412
413         /* Create GUI widget interface (and sets links in context) */
414         stock_create_gui("Click to Update",psContext);
415         
416         gtk_container_add(GTK_CONTAINER(desktop_plugin), psContext->psEventBox);
417         
418         psContext->hSG          = hSG;
419         psContext->nTimerID = -1;
420         psContext->psParent = GTK_WIDGET(desktop_plugin);
421
422         /* Assign private data to plugin */
423         desktop_plugin->context = psContext;
424         
425         /* Remove previous timeout if there was one */
426         if(psContext->nTimerID != -1)
427         {
428                 g_source_remove(psContext->nTimerID);
429         }
430
431         /* Setup new one if required */
432         if(psContext->psSettings->uiUpdateTime > 0)
433         {
434                 psContext->nTimerID = g_timeout_add_seconds(psContext->psSettings->uiUpdateTime * 60,
435                                                                                                         UpdateStockOnTimeout,
436                                                                                                         psContext);
437         }
438
439         /* Show a settings button */
440         hd_home_plugin_item_set_settings (HD_HOME_PLUGIN_ITEM (desktop_plugin), TRUE);
441
442         /* Connect to the settings button */
443         g_signal_connect(desktop_plugin, "show-settings", G_CALLBACK(stock_show_settings_dialog), psContext);
444 }
445
446 static void
447 stock_plugin_realize(GtkWidget* widget)
448 {
449         GdkScreen *screen = gtk_widget_get_screen(widget);
450         gtk_widget_set_colormap(widget, gdk_screen_get_rgba_colormap(screen));
451         gtk_widget_set_app_paintable(widget, TRUE);
452         GTK_WIDGET_CLASS(stock_plugin_parent_class)->realize(widget);
453 }
454
455 void
456 cairo_rounded_rectangle(cairo_t *cr,
457                                                 double x,
458                                                 double y,
459                                                 double width,
460                                                 double height,
461                                                 double corner_div)
462 {
463         cairo_pattern_t *pat;
464         
465         double          aspect            = 1.0;
466         double          corner_radius = height / corner_div;
467         double          radius            = corner_radius / aspect;
468         double          degrees           = 3.14159265 / 180.0;
469
470         cairo_new_sub_path (cr);
471         cairo_arc (cr, x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
472         cairo_arc (cr, x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
473         cairo_arc (cr, x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
474         cairo_arc (cr, x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
475         cairo_close_path (cr);
476
477         pat = cairo_pattern_create_linear(0.0, 0.0, 0.0, height);
478
479         cairo_pattern_add_color_stop_rgba(pat,0.0, 0.8, 0.8, 0.8, 0.7);
480         cairo_pattern_add_color_stop_rgba(pat,1.0, 0.0, 0.0, 0.0, 0.7);
481
482         cairo_set_source(cr,pat);
483         cairo_fill_preserve (cr);
484
485         cairo_set_line_width (cr, 0.0);
486         cairo_stroke (cr);
487 }
488
489 static gboolean
490 stock_plugin_expose(GtkWidget* widget, GdkEventExpose *event)
491 {
492         cairo_t*                         cr;
493         
494         cr = gdk_cairo_create(GDK_DRAWABLE(widget->window));
495         
496         cairo_rounded_rectangle(cr, 0, 0, event->area.width, event->area.height, 10.0);
497         cairo_destroy(cr);
498         
499         return GTK_WIDGET_CLASS(stock_plugin_parent_class)->expose_event(widget, event);
500 }
501
502 static void
503 stock_plugin_class_init (StockPluginClass *klass)
504 {
505         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
506         widget_class->realize                   = stock_plugin_realize;
507         widget_class->expose_event              = stock_plugin_expose;
508         G_OBJECT_CLASS(klass)->finalize = stock_plugin_finalize;
509 }
510
511 static void
512 stock_plugin_class_finalize (StockPluginClass *klass)
513 {
514 }