--- /dev/null
+/*
+ * Simple stock widget
+ * Jon Parr
+ */
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <string.h>
+#include <hildon/hildon.h>
+#include "lib-stock-home-widget.h"
+#include "lib-stock-settings.h"
+
+HD_DEFINE_PLUGIN_MODULE (StockPlugin, stock_plugin, HD_TYPE_HOME_PLUGIN_ITEM)
+
+static void
+OnClickRefresh(GtkWidget *widget, StockPluginContext *psContext)
+{
+ DebugOut(("OnClickRefresh"));
+
+ /* Disable button while getting latest stocks */
+ gtk_widget_set_sensitive(GTK_WIDGET(psContext->psButton),FALSE);
+ {
+ float fPrice;
+ float fChange;
+ char szBuffer[1024];
+
+ if(RetrieveStockPrice(psContext->hSG,psContext->psSettings->ppszTickers[0],&fPrice,NULL,&fChange) == SG_OK)
+ {
+ char szTime[80];
+ time_t rawtime;
+ struct tm *timeinfo;
+
+ time(&rawtime);
+ timeinfo = localtime(&rawtime);
+ strftime(szTime,80,"(%I:%M%p)",timeinfo);
+
+ /* Update to latest price */
+ sprintf(szBuffer,"%s %.2f %.2f%% %s",psContext->psSettings->ppszTickers[0],fPrice,fChange,szTime);
+
+ hildon_button_set_title(HILDON_BUTTON(psContext->psButton),szBuffer);
+ gtk_widget_queue_draw(GTK_WIDGET(psContext->psBox));
+ }
+ /*
+ If it fails, leave the old price as the user can see the timestamp
+ and know it's not up-to-date. So just notify them of the failure.
+ */
+ else
+ {
+ hildon_banner_show_information(GTK_WIDGET(widget),NULL,"Failed to retrieve stock information, check ticker symbol and try again.");
+ }
+ }
+ gtk_widget_set_sensitive(GTK_WIDGET(psContext->psButton),TRUE);
+}
+
+static gboolean
+UpdateStockOnTimeout(gpointer data)
+{
+ StockPluginContext *psContext = (StockPluginContext*)data;
+
+ DebugOut(("UpdateStockOnTimeout"));
+
+ if(!psContext)
+ return FALSE;
+
+ /* If no longer a timeout, don't do anything and stop */
+ if(!psContext->psSettings->uiUpdateTime)
+ return FALSE;
+
+ /* Update stock price */
+ OnClickRefresh(GTK_WIDGET(psContext->psButton),psContext);
+ return TRUE;
+}
+
+static void
+stock_show_settings_dialog(GtkWidget* widget, gpointer data)
+{
+ StockPluginContext *psContext = (StockPluginContext*)data;
+ GtkWidget *dialog;
+ GtkWidget *content;
+ GtkSizeGroup *group;
+ GtkWidget *addselector;
+ GtkWidget *addbutton;
+ GtkWidget *timeselector;
+ GtkWidget *timebutton;
+
+ DebugOut(("stock_show_settings_dialog"));
+
+ /* Stop timer while settings are one */
+ if(psContext->nTimerID != -1)
+ {
+ g_source_remove(psContext->nTimerID);
+ psContext->nTimerID = -1;
+ }
+
+ dialog = gtk_dialog_new_with_buttons("Settings",NULL,0,"Save",GTK_RESPONSE_ACCEPT,NULL);
+ content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ group = GTK_SIZE_GROUP(gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL));
+
+ /* Add new stock symbol */
+ {
+ int i;
+
+ addbutton = hildon_picker_button_new (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
+ HILDON_BUTTON_ARRANGEMENT_HORIZONTAL);
+
+ addselector = hildon_touch_selector_entry_new_text();
+
+ for(i=0; i < psContext->psSettings->uiNumTickers; i++)
+ {
+ hildon_touch_selector_append_text(HILDON_TOUCH_SELECTOR(addselector),psContext->psSettings->ppszTickers[i]);
+ }
+
+ /* First ticker in ppszTickers is the chosen one */
+ hildon_touch_selector_set_active(HILDON_TOUCH_SELECTOR(addselector), 0, 0);
+
+ hildon_picker_button_set_selector (HILDON_PICKER_BUTTON (addbutton), HILDON_TOUCH_SELECTOR (addselector));
+
+ hildon_button_set_title(HILDON_BUTTON(addbutton), "Select Ticker");
+ }
+
+ /* Set auto update time */
+ {
+ int i;
+ int sel = 0;
+
+ char *aszUpdateTimes[] =
+ {
+ "No update",
+ "1 minute",
+ "15 minutes",
+ "30 minutes",
+ "1 hour"
+ };
+
+ timebutton = hildon_picker_button_new (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
+ HILDON_BUTTON_ARRANGEMENT_HORIZONTAL);
+
+ timeselector = hildon_touch_selector_new_text();
+
+ for(i=0; i < sizeof(aszUpdateTimes)/sizeof(aszUpdateTimes[0]); i++)
+ {
+ hildon_touch_selector_append_text(HILDON_TOUCH_SELECTOR(timeselector),aszUpdateTimes[i]);
+ }
+
+ switch(psContext->psSettings->uiUpdateTime)
+ {
+ case 0: sel = 0; break;
+ case 1: sel = 1; break;
+ case 15: sel = 2; break;
+ case 30: sel = 3; break;
+ case 60: sel = 4; break;
+ }
+
+ hildon_touch_selector_set_active(HILDON_TOUCH_SELECTOR(timeselector), 0, sel);
+ hildon_picker_button_set_selector (HILDON_PICKER_BUTTON (timebutton), HILDON_TOUCH_SELECTOR (timeselector));
+ hildon_button_set_title(HILDON_BUTTON(timebutton), "Automatic Update Period");
+ }
+
+ gtk_container_add(GTK_CONTAINER(content),addbutton);
+ gtk_container_add(GTK_CONTAINER(content),timebutton);
+ gtk_widget_show_all(dialog);
+
+ if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
+ {
+ const char *pszNewTicker;
+ char aszOldTicker[255]={0};
+ guint uiUpdateSelection = 0;
+
+ /* Only perform ops if they actually changed the ticker */
+ if(strcmp(aszOldTicker,psContext->psSettings->ppszTickers[0]))
+ {
+ /* Save off old ticker */
+ strcpy(aszOldTicker,psContext->psSettings->ppszTickers[0]);
+
+ /* Get new ticker */
+ pszNewTicker = hildon_button_get_value(HILDON_BUTTON(addbutton));
+
+ /* Check if ticker already exists */
+ {
+ int i;
+ gboolean bFound = FALSE;
+
+ /* Check it doesn't exist already */
+ for(i=0; i < psContext->psSettings->uiNumTickers; i++)
+ {
+ if(!strcmp(pszNewTicker,psContext->psSettings->ppszTickers[i]))
+ {
+ bFound = TRUE;
+ break;
+ }
+ }
+
+ /* If they just chose another one, just swap the pointers so new one is at the top:
+ */
+ if(bFound)
+ {
+ /* Swapped ith and 0th element */
+ char *psTmp = psContext->psSettings->ppszTickers[i];
+ psContext->psSettings->ppszTickers[i] = psContext->psSettings->ppszTickers[0];
+ psContext->psSettings->ppszTickers[0] = psTmp;
+
+ DebugOut(("Swapped ticker %d (%s) with 0 (%s)",
+ i,
+ psContext->psSettings->ppszTickers[i],
+ psContext->psSettings->ppszTickers[0]));
+ }
+ /* If it doesn't add it to the list, placing chosen one at the front */
+ else
+ {
+ gchar **ppszTickers = g_malloc0(sizeof(char*) * (psContext->psSettings->uiNumTickers+2));
+
+ /* Place the selected one first */
+ ppszTickers[0] = g_strdup(pszNewTicker);
+
+ /* Copy across old tickers */
+ for(i=0; i < psContext->psSettings->uiNumTickers; i++)
+ {
+ /* offset by one as the selected one is first */
+ ppszTickers[i+1] = g_strdup(psContext->psSettings->ppszTickers[i]);
+ }
+
+ /* Free current array */
+ g_strfreev(psContext->psSettings->ppszTickers);
+
+ /* Assign new array */
+ psContext->psSettings->ppszTickers = ppszTickers;
+
+ /* Update count */
+ psContext->psSettings->uiNumTickers++;
+ }
+ }
+
+ /* Force a refresh as the ticker changed */
+ OnClickRefresh(GTK_WIDGET(psContext->psButton),psContext);
+ }
+
+ uiUpdateSelection = hildon_touch_selector_get_active(HILDON_TOUCH_SELECTOR(timeselector),0);
+
+ switch(uiUpdateSelection)
+ {
+ /* No update */
+ case 0: psContext->psSettings->uiUpdateTime = 0; break;
+ case 1: psContext->psSettings->uiUpdateTime = 1; break;
+ case 2: psContext->psSettings->uiUpdateTime = 15; break;
+ case 3: psContext->psSettings->uiUpdateTime = 30; break;
+ case 4: psContext->psSettings->uiUpdateTime = 60; break;
+ }
+
+ if(psContext->psSettings->uiUpdateTime != 0)
+ {
+ /* Remove previous timeout if there was one */
+ if(psContext->nTimerID != -1)
+ {
+ g_source_remove(psContext->nTimerID);
+ }
+
+ /* Add timer (in seconds) */
+ psContext->nTimerID = g_timeout_add_seconds(psContext->psSettings->uiUpdateTime * 60,
+ UpdateStockOnTimeout,
+ psContext);
+ }
+
+ /* Save Settings */
+ stock_save_settings(psContext->psSettings);
+ }
+
+ gtk_widget_destroy (dialog);
+}
+
+static GtkWidget *stock_create_gui(gchar *pszStockLabel,
+ StockPluginContext *psContext)
+{
+ HildonButton *psButton = NULL;
+
+ DebugOut(("stock_create_gui"));
+
+ psButton = (HildonButton*)hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_FINGER_HEIGHT,
+ HILDON_BUTTON_ARRANGEMENT_VERTICAL,
+ pszStockLabel,
+ NULL);
+
+ psContext->psButton = psButton;
+
+ /* Setup button click signal for refreshing stocks */
+ gtk_signal_connect (GTK_OBJECT(psButton),"clicked",GTK_SIGNAL_FUNC(OnClickRefresh),(gpointer)psContext);
+
+ /* Display */
+ gtk_widget_show_all(GTK_WIDGET(psButton));
+
+ return GTK_WIDGET(psButton);
+}
+
+static void
+stock_plugin_finalize (GObject *object)
+{
+ StockPluginContext *psContext = ((StockPlugin*)object)->context;
+
+ DebugOut(("stock_plugin_finalize"));
+
+ if(psContext)
+ {
+ if(psContext->psSettings)
+ {
+ if(psContext->psSettings)
+ g_free(psContext->psSettings->iD);
+
+ stock_free_settings(psContext->psSettings);
+ g_free(psContext->psSettings);
+ }
+
+ if(psContext->nTimerID != -1)
+ {
+ g_source_remove(psContext->nTimerID);
+ psContext->nTimerID = -1;
+ }
+
+ if(psContext->hSG)
+ FreeStockGetter(psContext->hSG);
+
+ g_free(psContext);
+ }
+}
+
+static void
+stock_plugin_init (StockPlugin *desktop_plugin)
+{
+ StockPluginContext *psContext;
+ SGHandle hSG;
+
+ DebugOut(("stock_plugin_init"));
+
+ hSG = InitStockGetter();
+
+ if(!hSG)
+ {
+ return;
+ }
+
+ /* Create plugin context */
+ psContext = g_new0(StockPluginContext,1);
+
+ /* Always create a settings */
+ psContext->psSettings = g_new0(StockPluginSettings,1);
+ psContext->psSettings->iD = g_strdup(STOCK_PLUGIN_APPLET_ID);
+
+ /* Attempt to load settings (defaulted if not present) */
+ stock_read_settings(psContext->psSettings);
+
+ /* Create GUI widget interface (and sets links in context) */
+ psContext->psBox = stock_create_gui("Click to Update",psContext);
+
+ gtk_container_add (GTK_CONTAINER (desktop_plugin), psContext->psBox);
+
+ psContext->hSG = hSG;
+ psContext->nTimerID = -1;
+
+ /* Assign private data to plugin */
+ desktop_plugin->context = psContext;
+
+ /* Remove previous timeout if there was one */
+ if(psContext->nTimerID != -1)
+ {
+ g_source_remove(psContext->nTimerID);
+ }
+
+ /* Setup new one if required */
+ if(psContext->psSettings->uiUpdateTime > 0)
+ {
+ psContext->nTimerID = g_timeout_add_seconds(psContext->psSettings->uiUpdateTime * 60,
+ UpdateStockOnTimeout,
+ psContext);
+ }
+
+ /* Show a settings button */
+ hd_home_plugin_item_set_settings (HD_HOME_PLUGIN_ITEM (desktop_plugin), TRUE);
+
+ /* Connect to the settings button */
+ g_signal_connect(desktop_plugin, "show-settings", G_CALLBACK(stock_show_settings_dialog), psContext);
+}
+
+static void
+stock_plugin_class_init (StockPluginClass *klass)
+{
+ G_OBJECT_CLASS(klass)->finalize = stock_plugin_finalize;
+}
+
+static void
+stock_plugin_class_finalize (StockPluginClass *klass)
+{
+}
--- /dev/null
+/*
+ * Stock Widget: Resource settings
+ * Jon Parr
+ */
+#include "lib-stock-settings.h"
+
+void
+stock_free_settings(StockPluginSettings *psSettings)
+{
+ if(psSettings->ppszTickers)
+ {
+ g_strfreev(psSettings->ppszTickers);
+ psSettings->ppszTickers = NULL;
+ }
+
+ psSettings->uiNumTickers = 0;
+ psSettings->uiUpdateTime = 0;
+}
+
+void
+stock_set_default_settings(StockPluginSettings *psSettings)
+{
+ /* Default to a single ticker ;) */
+ psSettings->uiNumTickers = 1;
+ psSettings->ppszTickers = g_malloc0(sizeof(char*)*2);
+ psSettings->ppszTickers[0] = g_strdup(STOCK_PLUGIN_DEFAULT_TICKER);
+
+ /* Default to no automatic update */
+ psSettings->uiUpdateTime = 0;
+}
+
+void
+stock_read_settings(StockPluginSettings *psSettings)
+{
+ GKeyFile *psKeyFile;
+ gchar *pszFileName;
+ gboolean bExists;
+ gint nItems = 0;
+
+ psKeyFile = g_key_file_new();
+ pszFileName = g_strconcat(g_get_home_dir(), STOCK_PLUGIN_SETTINGS_FILE, NULL);
+ bExists = g_key_file_load_from_file(psKeyFile, pszFileName, G_KEY_FILE_KEEP_COMMENTS, NULL);
+
+ if(bExists)
+ {
+ /*TODO: Check case if config file exists but no data, simply use defaults */
+
+ if(g_key_file_has_key(psKeyFile,psSettings->iD,STOCK_PLUGIN_TICKERS,NULL))
+ {
+ psSettings->ppszTickers =
+ g_key_file_get_string_list(psKeyFile,
+ psSettings->iD,
+ STOCK_PLUGIN_TICKERS,
+ &psSettings->uiNumTickers,
+ NULL);
+ }
+ else
+ {
+ bExists = FALSE;
+ }
+
+ if(g_key_file_has_key(psKeyFile,psSettings->iD,STOCK_PLUGIN_UPDATE_TIME,NULL))
+ {
+ psSettings->uiUpdateTime =
+ g_key_file_get_integer(psKeyFile,
+ psSettings->iD,
+ STOCK_PLUGIN_UPDATE_TIME,
+ NULL);
+ nItems++;
+ }
+ else
+ {
+ if(psSettings->ppszTickers)
+ {
+ g_strfreev(psSettings->ppszTickers);
+ psSettings->ppszTickers = NULL;
+ }
+
+ bExists = FALSE;
+ }
+ }
+
+ /* If it doesn't exists or there's an error reading data, just set defaults */
+ if(!bExists)
+ stock_set_default_settings(psSettings);
+
+ g_key_file_free(psKeyFile);
+ g_free(pszFileName);
+}
+
+
+void
+stock_save_settings(StockPluginSettings *psSettings)
+{
+ GKeyFile *psKeyFile;
+ gchar *pszFileName;
+ gchar *pszFileData;
+ gsize nSize;
+
+ /* Create key file */
+ psKeyFile = g_key_file_new();
+ pszFileName = g_strconcat(g_get_home_dir(), STOCK_PLUGIN_SETTINGS_FILE, NULL);
+ g_key_file_load_from_file(psKeyFile, pszFileName, G_KEY_FILE_KEEP_COMMENTS, NULL);
+
+ /* Set tickers */
+ g_key_file_set_string_list(psKeyFile,
+ psSettings->iD,
+ STOCK_PLUGIN_TICKERS,
+ (const gchar **)psSettings->ppszTickers,
+ psSettings->uiNumTickers);
+
+ /* Set update time */
+ g_key_file_set_integer(psKeyFile,
+ psSettings->iD,
+ STOCK_PLUGIN_UPDATE_TIME,
+ psSettings->uiUpdateTime);
+
+ /* Save to disk */
+ pszFileData = g_key_file_to_data(psKeyFile,&nSize,NULL);
+ g_file_set_contents(pszFileName,pszFileData,nSize,NULL);
+
+ /* Cleanup */
+ g_key_file_free(psKeyFile);
+ g_free(pszFileName);
+ g_free(pszFileData);
+}
--- /dev/null
+/*
+ * Simple wrapper for curl and Yahoo finance queries
+ * to enable grabbing stock data.
+ * Jon Parr
+ */
+#include "stockgetter.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Current maximum query return size */
+#define SG_MAX_BUFFER 512
+
+/* Current query string, note the %s for the stock ticker name */
+#define SG_QUERY_STRING "http://finance.yahoo.com/d/quotes.csv?s=%s&f=l1c"
+
+static size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp)
+{
+ StockBuffer *psContext = (StockBuffer*)userp;
+ int nBytes = size * nmemb;
+
+ /*TODO: Write extendable buffer */
+ if(nBytes > psContext->nCount - psContext->nCurr)
+ {
+ fprintf(stderr,"Ran out of buffer space! (Bytes %d, %d/%d)\n",nBytes,psContext->nCurr,psContext->nCount);
+ return 0;
+ }
+
+ memcpy((void*)(psContext->pszBuffer + psContext->nCurr),buffer,nBytes);
+ psContext->nCurr += nBytes;
+
+ return nmemb;
+}
+
+SGHandle
+InitStockGetter(void)
+{
+ SGHandle hSG;
+ CURL *curl_handle;
+
+ hSG = (SGHandle)malloc(sizeof(struct _SGHandle));
+
+ if(!hSG)
+ return NULL;
+
+ memset(hSG,0,sizeof(struct _SGHandle));
+
+ if(curl_global_init(CURL_GLOBAL_ALL) != 0)
+ goto err_cleanup;
+
+ curl_handle = curl_easy_init();
+
+ if(!curl_handle)
+ goto err_cleanup;
+
+ if(curl_easy_setopt(curl_handle,CURLOPT_WRITEFUNCTION,write_data) != 0)
+ goto err_cleanup;
+
+ if(curl_easy_setopt (curl_handle, CURLOPT_FOLLOWLOCATION, 1) != 0)
+ goto err_cleanup;
+
+ if(curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA,&hSG->sData) != 0)
+ goto err_cleanup;
+
+ hSG->hCurlHandle = curl_handle;
+
+ return hSG;
+
+err_cleanup:
+
+ if(hSG)
+ free(hSG);
+
+ return NULL;
+}
+
+int
+RetrieveStockPrice(SGHandle hSG,
+ char *szTicker,
+ float *pfValue,
+ float *pfChangeReal,
+ float *pfChangePercent)
+{
+ const char *pszQueryString = SG_QUERY_STRING;
+ char aszQuery[255] = {0};
+ float fValue = 0.0f;
+ float fChangeReal = 0.0f;
+ float fChangePer = 0.0f;
+
+ if(!hSG || !pfValue || !szTicker || strlen(szTicker) > 10)
+ return SG_INVALID_PARAMS;
+
+ /* On first lookup setup output buffer */
+ if(!hSG->sData.pszBuffer)
+ {
+ hSG->sData.pszBuffer = (char*)malloc(sizeof(char)*SG_MAX_BUFFER);
+
+ if(!hSG->sData.pszBuffer)
+ return SG_BAD_ALLOC;
+
+ memset(hSG->sData.pszBuffer,0,SG_MAX_BUFFER);
+
+ hSG->sData.nCount = SG_MAX_BUFFER;
+ }
+
+ sprintf(aszQuery,pszQueryString,szTicker);
+
+ if(curl_easy_setopt(hSG->hCurlHandle, CURLOPT_URL,aszQuery) != 0)
+ return SG_LOOKUP_FAILURE;
+
+ if(curl_easy_perform(hSG->hCurlHandle) != 0)
+ return SG_LOOKUP_FAILURE;
+
+ hSG->sData.nCurr = 0;
+
+ if(sscanf(hSG->sData.pszBuffer,"%f,\"%f - %f\"\n",&fValue,&fChangeReal,&fChangePer) != 3)
+ return SG_LOOKUP_FAILURE;
+
+ if(pfValue)
+ *pfValue = fValue;
+
+ if(pfChangeReal)
+ *pfChangeReal = fChangeReal;
+
+ if(pfChangePercent)
+ *pfChangePercent = fChangePer;
+
+ return SG_OK;
+}
+
+int
+FreeStockGetter(SGHandle hSG)
+{
+ if(!hSG)
+ return SG_INVALID_PARAMS;
+
+ curl_easy_cleanup(hSG->hCurlHandle);
+ curl_global_cleanup();
+
+ free(hSG);
+
+ return SG_OK;
+}
+