diff options
Diffstat (limited to 'invest-applet/invest/invest-applet.c')
-rw-r--r-- | invest-applet/invest/invest-applet.c | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/invest-applet/invest/invest-applet.c b/invest-applet/invest/invest-applet.c new file mode 100644 index 00000000..15d72780 --- /dev/null +++ b/invest-applet/invest/invest-applet.c @@ -0,0 +1,720 @@ +/* + * MATE Invest Applet + * Copyright (C) 2004-2005 Raphael Slinckx + * Copyright (C) 2009-2010 Enrico Minack + * Copyright (C) 2012-2025 MATE developers + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <config.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <json-glib/json-glib.h> +#include <libsoup/soup.h> +#include <mate-panel-applet.h> +#include <mate-panel-applet-gsettings.h> + +typedef struct _InvestApplet InvestApplet; +typedef struct _InvestAppletClass InvestAppletClass; + +static gchar* create_stock_tooltip (InvestApplet *applet); +static void free_stock_data (InvestApplet *applet); +static gint get_valid_stock_indices (InvestApplet *applet, gint *valid_indices); +static void update_portfolio_display (InvestApplet *applet, const gchar *message, gdouble change_percent); +static gboolean invest_applet_cycle_stocks (InvestApplet *applet); +static void display_stock_at_index (InvestApplet *applet, gint stock_index); +static void clear_timeout (guint *timeout_id); + +#define INVEST_TYPE_APPLET (invest_applet_get_type ()) +#define INVEST_APPLET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), INVEST_TYPE_APPLET, InvestApplet)) + +struct _InvestApplet { + MatePanelApplet parent; + + GtkWidget *label; + GtkWidget *direction_icon; /* icon for stock price going up/down/neutral */ + GSettings *settings; + SoupSession *soup_session; + + guint update_timeout_id; + gchar *portfolio_summary; + gdouble total_change_percent; + gint refresh_interval; + gint cycle_interval; + + gint pending_requests; + gint total_symbols; + gchar **stock_symbols; + gdouble *stock_prices; + gdouble *stock_changes; + gboolean *stock_valid; + + /* for cycling throuhg multiple stocks */ + gint cycle_position; + guint cycle_timeout_id; +}; + +struct _InvestAppletClass { + MatePanelAppletClass parent_class; +}; + +G_DEFINE_TYPE (InvestApplet, invest_applet, PANEL_TYPE_APPLET); + +#define DEFAULT_UPDATE_INTERVAL 15 /* 15 minutes */ +#define DEFAULT_CYCLE_INTERVAL 5 /* 5 seconds */ + +static void +invest_applet_update_display (InvestApplet *applet) +{ + gchar *text; + const gchar *direction_icon; + + if (applet->portfolio_summary) { + if (applet->total_change_percent > 0) { + text = g_strdup_printf ("%s (+%.2f%%)", + applet->portfolio_summary, + applet->total_change_percent); + direction_icon = "invest_up"; + } else if (applet->total_change_percent < 0) { + text = g_strdup_printf ("%s (%.2f%%)", + applet->portfolio_summary, + applet->total_change_percent); + direction_icon = "invest_down"; + } else { + text = g_strdup_printf ("%s (0.00%%)", applet->portfolio_summary); + direction_icon = "invest_neutral"; + } + } else { + text = g_strdup (_("No stocks configured")); + direction_icon = "invest_neutral"; + } + + gtk_label_set_text (GTK_LABEL (applet->label), text); + gtk_image_set_from_icon_name (GTK_IMAGE (applet->direction_icon), direction_icon, GTK_ICON_SIZE_MENU); + g_free (text); +} + +static void +on_stock_data_received (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + gpointer *user_data_with_index = (gpointer *)user_data; + InvestApplet *applet = INVEST_APPLET (user_data_with_index[0]); + gint symbol_index = GPOINTER_TO_INT (user_data_with_index[1]); + + JsonParser *parser = NULL; + JsonNode *root; + JsonObject *root_obj; + GError *error = NULL; + + if (msg->status_code != SOUP_STATUS_OK) { + g_warning ("Failed to fetch stock data for symbol %d: %s", symbol_index, msg->reason_phrase); + goto cleanup; + } + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, msg->response_body->data, + msg->response_body->length, &error)) { + g_warning ("Failed to parse JSON for symbol %d: %s", symbol_index, error->message); + g_error_free (error); + goto cleanup; + } + + root = json_parser_get_root (parser); + root_obj = json_node_get_object (root); + + /* Parse data for single stock. The expected JSON from the Yahoo Finance API should look like this: + * { + * "chart": { + * "result": [{ + * "meta": { + * "currency": "USD", + * "exchangeName": "NMS", + * "longName": "Apple Inc.", + * "previousClose": 148.50, + * "regularMarketPrice": 150.25, + * "shortName": "Apple Inc.", + * "symbol": "AAPL", + * ... + * }, + * "timestamp": [...], + * "indicators": {...} + * }], + * "error": null + * } + * } + */ + if (json_object_has_member (root_obj, "chart")) { + JsonObject *chart = json_object_get_object_member (root_obj, "chart"); + if (json_object_has_member (chart, "result")) { + JsonArray *results = json_object_get_array_member (chart, "result"); + if (json_array_get_length (results) > 0) { + JsonObject *result = json_array_get_object_element (results, 0); + JsonObject *meta = json_object_get_object_member (result, "meta"); + + if (meta) { + gdouble current_price = json_object_get_double_member (meta, "regularMarketPrice"); + gdouble prev_close = json_object_get_double_member (meta, "previousClose"); + + if (current_price > 0 && prev_close > 0) { + gdouble change_percent = ((current_price - prev_close) / prev_close) * 100.0; + + /* store individual stock data */ + applet->stock_prices[symbol_index] = current_price; + applet->stock_changes[symbol_index] = change_percent; + applet->stock_valid[symbol_index] = TRUE; + } + } + } + } + } + +cleanup: + if (parser) { + g_object_unref (parser); + } + applet->pending_requests--; + + if (applet->pending_requests == 0) { + if (applet->total_symbols > 0) { + clear_timeout (&applet->cycle_timeout_id); + + applet->cycle_position = 0; + + gint valid_indices[applet->total_symbols]; + gint valid_count = get_valid_stock_indices (applet, valid_indices); + + if (valid_count > 0) { + display_stock_at_index (applet, valid_indices[0]); + + /* start cycle timer if we have multiple stocks */ + if (!applet->cycle_timeout_id && valid_count > 1) { + applet->cycle_timeout_id = g_timeout_add_seconds (applet->cycle_interval, (GSourceFunc) invest_applet_cycle_stocks, applet); + } + + gchar *tooltip_text = create_stock_tooltip (applet); + gtk_widget_set_tooltip_text (GTK_WIDGET (applet), tooltip_text); + g_free (tooltip_text); + } else { + update_portfolio_display (applet, _("No valid stock data"), 0.0); + } + } else { + update_portfolio_display (applet, _("No valid stock data"), 0.0); + } + } + + g_free (user_data_with_index); +} + +/* fetch stock data from Yahoo! Finance for all configured symbols */ +static gboolean +invest_applet_update_stocks (gpointer user_data) +{ + InvestApplet *applet = INVEST_APPLET (user_data); + gchar **symbols; + gint symbol_count; + + if (!G_IS_SETTINGS (applet->settings)) { + g_warning ("Settings not available yet"); + return G_SOURCE_CONTINUE; + } + + symbols = g_settings_get_strv (applet->settings, "stock-symbols"); + + if (!symbols || !symbols[0]) { + update_portfolio_display (applet, _("No stocks configured"), 0.0); + g_strfreev (symbols); + return G_SOURCE_CONTINUE; + } + + symbol_count = g_strv_length (symbols); + + free_stock_data (applet); + + applet->pending_requests = symbol_count; + applet->total_symbols = symbol_count; + applet->stock_symbols = g_strdupv (symbols); + applet->stock_prices = g_malloc0 (symbol_count * sizeof (gdouble)); + applet->stock_changes = g_malloc0 (symbol_count * sizeof (gdouble)); + applet->stock_valid = g_malloc0 (symbol_count * sizeof (gboolean)); + + applet->cycle_position = 0; + clear_timeout (&applet->cycle_timeout_id); + + /* initialize networking */ + if (!applet->soup_session) { + applet->soup_session = soup_session_new (); + } + + /* make separate request for each stock symbol */ + for (gint i = 0; i < symbol_count; i++) { + gchar *url = g_strdup_printf ("https://query2.finance.yahoo.com/v8/finance/chart/%s", symbols[i]); + SoupMessage *msg = soup_message_new ("GET", url); + + /* HACK: avoid rate limiting */ + soup_message_headers_replace (msg->request_headers, "User-Agent", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"); + + gpointer *user_data_with_index = g_malloc (2 * sizeof (gpointer)); + user_data_with_index[0] = applet; + user_data_with_index[1] = GINT_TO_POINTER (i); + + soup_session_queue_message (applet->soup_session, msg, on_stock_data_received, user_data_with_index); + g_free (url); + } + + g_strfreev (symbols); + return G_SOURCE_CONTINUE; +} + +static void +invest_applet_preferences_cb (GtkAction *action, + InvestApplet *applet) +{ + GtkWidget *dialog, *content_area, *entry, *label, *spin_button, *refresh_label, *cycle_label, *cycle_spin; + GtkWidget *hbox1, *hbox2, *hbox3; + gchar **symbols; + gchar *symbols_text; + gint response, refresh_interval, cycle_interval; + + dialog = gtk_dialog_new_with_buttons (_("Investment Applet Preferences"), + NULL, + GTK_DIALOG_MODAL, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_OK"), GTK_RESPONSE_OK, + NULL); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_window_set_default_size (GTK_WINDOW (dialog), 350, 150); + + content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); + gtk_container_set_border_width (GTK_CONTAINER (content_area), 12); + + /* Stock symbols */ + hbox1 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + label = gtk_label_new (_("Stock symbols:")); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_widget_set_size_request (label, 120, -1); + entry = gtk_entry_new (); + gtk_box_pack_start (GTK_BOX (hbox1), label, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox1), entry, TRUE, TRUE, 0); + + /* Refresh interval */ + hbox2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + refresh_label = gtk_label_new (_("Refresh interval (minutes):")); + gtk_label_set_xalign (GTK_LABEL (refresh_label), 0.0); + gtk_widget_set_size_request (refresh_label, 120, -1); + spin_button = gtk_spin_button_new_with_range (1, 60, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (spin_button), 0); + gtk_box_pack_start (GTK_BOX (hbox2), refresh_label, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox2), spin_button, FALSE, FALSE, 0); + + /* Cycle interval */ + hbox3 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + cycle_label = gtk_label_new (_("Cycle interval (seconds):")); + gtk_label_set_xalign (GTK_LABEL (cycle_label), 0.0); + gtk_widget_set_size_request (cycle_label, 120, -1); + cycle_spin = gtk_spin_button_new_with_range (1, 60, 1); + gtk_spin_button_set_digits (GTK_SPIN_BUTTON (cycle_spin), 0); + gtk_box_pack_start (GTK_BOX (hbox3), cycle_label, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox3), cycle_spin, FALSE, FALSE, 0); + + if (!G_IS_SETTINGS (applet->settings)) { + g_warning ("Settings not available in preferences"); + gtk_widget_destroy (dialog); + return; + } + + symbols = g_settings_get_strv (applet->settings, "stock-symbols"); + symbols_text = g_strjoinv (",", symbols); + gtk_entry_set_text (GTK_ENTRY (entry), symbols_text); + g_free (symbols_text); + g_strfreev (symbols); + + refresh_interval = g_settings_get_int (applet->settings, "refresh-interval"); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (spin_button), refresh_interval); + + cycle_interval = g_settings_get_int (applet->settings, "cycle-interval"); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (cycle_spin), cycle_interval); + + /* Pack all rows into content area */ + gtk_box_pack_start (GTK_BOX (content_area), hbox1, FALSE, FALSE, 6); + gtk_box_pack_start (GTK_BOX (content_area), hbox2, FALSE, FALSE, 6); + gtk_box_pack_start (GTK_BOX (content_area), hbox3, FALSE, FALSE, 6); + + gtk_widget_show_all (dialog); + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == GTK_RESPONSE_OK) { + const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry)); + gchar **new_symbols = g_strsplit (text, ",", -1); + gint new_refresh_interval = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spin_button)); + gint new_cycle_interval = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (cycle_spin)); + + /* trim whitespaces */ + for (gint i = 0; new_symbols[i]; i++) { + g_strstrip (new_symbols[i]); + } + + g_settings_set_strv (applet->settings, "stock-symbols", (const gchar * const *)new_symbols); + g_settings_set_int (applet->settings, "refresh-interval", new_refresh_interval); + g_settings_set_int (applet->settings, "cycle-interval", new_cycle_interval); + g_strfreev (new_symbols); + + /* Update refresh interval if changed */ + if (new_refresh_interval != applet->refresh_interval) { + clear_timeout (&applet->update_timeout_id); + applet->refresh_interval = new_refresh_interval; + applet->update_timeout_id = g_timeout_add_seconds (new_refresh_interval * 60, + invest_applet_update_stocks, + applet); + } + + /* Update cycle interval if changed */ + if (new_cycle_interval != applet->cycle_interval) { + clear_timeout (&applet->cycle_timeout_id); + applet->cycle_interval = new_cycle_interval; + /* Restart cycle timer with new interval */ + if (applet->total_symbols > 0) { + applet->cycle_timeout_id = g_timeout_add_seconds (new_cycle_interval, (GSourceFunc) invest_applet_cycle_stocks, applet); + } + } + + /* Trigger update */ + invest_applet_update_stocks (applet); + } + + gtk_widget_destroy (dialog); +} + +static void +invest_applet_refresh_cb (GtkAction *action, + InvestApplet *applet) +{ + invest_applet_update_stocks (applet); +} + +static void +invest_applet_help_cb (GtkAction *action, + InvestApplet *applet) +{ + GError *error = NULL; + + gtk_show_uri_on_window (NULL, "help:mate-invest-applet", gtk_get_current_event_time (), &error); + + if (error) { + GtkWidget *dialog = gtk_message_dialog_new (NULL, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + _("Could not display help")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", error->message); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + g_error_free (error); + } +} + +static void +invest_applet_about_cb (GtkAction *action, + InvestApplet *applet) +{ + const gchar *authors[] = { + "Raphael Slinckx <[email protected]>", + "Enrico Minack <[email protected]>", + "MATE developers", + NULL + }; + + gtk_show_about_dialog (NULL, + "program-name", _("Invest"), + "logo-icon-name", "mate-invest-applet", + "version", VERSION, + "comments", _("Track your invested money."), + "copyright", "Copyright \xc2\xa9 2004-2005 Raphael Slinckx\nCopyright \xc2\xa9 2009-2010 Enrico Minack\nCopyright \xc2\xa9 2012-2025 MATE developers", + "authors", authors, + NULL); +} + +static gboolean +invest_applet_cycle_stocks (InvestApplet *applet) +{ + if (applet->total_symbols == 0) { + return G_SOURCE_CONTINUE; + } + + /* find valid stocks */ + gint valid_indices[applet->total_symbols]; + gint valid_count = get_valid_stock_indices (applet, valid_indices); + + if (valid_count == 0) { + update_portfolio_display (applet, _("No valid stock data"), 0.0); + return G_SOURCE_CONTINUE; + } + + if (valid_count == 1) { + /* single stock, nothing to cycle, stop timer */ + return G_SOURCE_REMOVE; + } + + /* multiple stocks, cycle to next stock */ + applet->cycle_position = (applet->cycle_position + 1) % valid_count; + + /* display current stock */ + display_stock_at_index (applet, valid_indices[applet->cycle_position]); + + return G_SOURCE_CONTINUE; +} + +static gboolean +invest_applet_button_press (GtkWidget *widget, + GdkEventButton *event, + InvestApplet *applet) +{ + if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { + /* cycle to next stock */ + invest_applet_cycle_stocks (applet); + return TRUE; + } + + return FALSE; +} + +static void +invest_applet_destroy (GtkWidget *widget, + InvestApplet *applet) +{ + clear_timeout (&applet->update_timeout_id); + clear_timeout (&applet->cycle_timeout_id); + + if (applet->soup_session) { + g_object_unref (applet->soup_session); + applet->soup_session = NULL; + } + + if (applet->settings) { + g_object_unref (applet->settings); + applet->settings = NULL; + } + + free_stock_data (applet); +} + +static void +invest_applet_init (InvestApplet *applet) +{ + GtkWidget *hbox; + + /* Settings will be initialized in the factory function */ + applet->settings = NULL; + applet->refresh_interval = DEFAULT_UPDATE_INTERVAL; + applet->cycle_interval = DEFAULT_CYCLE_INTERVAL; + + applet->pending_requests = 0; + applet->total_symbols = 0; + applet->stock_symbols = NULL; + applet->stock_prices = NULL; + applet->stock_changes = NULL; + applet->stock_valid = NULL; + + /* init stock cycling in panel display */ + applet->cycle_position = 0; + applet->cycle_timeout_id = 0; + + /* init networking */ + applet->soup_session = soup_session_new (); + + /* UI */ + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); + + applet->direction_icon = gtk_image_new_from_icon_name ("dialog-information", GTK_ICON_SIZE_MENU); + applet->label = gtk_label_new (_("Loading...")); + + gtk_box_pack_start (GTK_BOX (hbox), applet->direction_icon, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (hbox), applet->label, FALSE, FALSE, 0); + + gtk_container_add (GTK_CONTAINER (applet), hbox); + + g_signal_connect (G_OBJECT (applet), "button-press-event", + G_CALLBACK (invest_applet_button_press), applet); + g_signal_connect (G_OBJECT (applet), "destroy", + G_CALLBACK (invest_applet_destroy), applet); + + const GtkActionEntry menu_actions[] = { + { "InvestRefresh", "view-refresh", N_("_Refresh"), NULL, NULL, G_CALLBACK (invest_applet_refresh_cb) }, + { "InvestPreferences", "preferences-system", N_("_Preferences"), NULL, NULL, G_CALLBACK (invest_applet_preferences_cb) }, + { "InvestHelp", "help-browser", N_("_Help"), NULL, NULL, G_CALLBACK (invest_applet_help_cb) }, + { "InvestAbout", "help-about", N_("_About"), NULL, NULL, G_CALLBACK (invest_applet_about_cb) } + }; + + GtkActionGroup *action_group = gtk_action_group_new ("Invest Applet Actions"); + gtk_action_group_set_translation_domain (action_group, GETTEXT_PACKAGE); + gtk_action_group_add_actions (action_group, menu_actions, G_N_ELEMENTS (menu_actions), applet); + + const gchar *ui = "<menuitem name=\"Invest Refresh\" action=\"InvestRefresh\" />" + "<separator />" + "<menuitem name=\"Invest Preferences\" action=\"InvestPreferences\" />" + "<menuitem name=\"Invest Help\" action=\"InvestHelp\" />" + "<menuitem name=\"Invest About\" action=\"InvestAbout\" />"; + + mate_panel_applet_setup_menu (MATE_PANEL_APPLET (applet), ui, action_group); + g_object_unref (action_group); + + gtk_widget_show_all (GTK_WIDGET (applet)); +} + +static void +invest_applet_class_init (InvestAppletClass *class) +{ +} + +static gboolean +invest_applet_factory (MatePanelApplet *applet, + const gchar *iid, + gpointer data) +{ + if (!g_strcmp0 (iid, "InvestApplet")) { + InvestApplet *invest_applet = INVEST_APPLET (applet); + + g_set_application_name (_("Investment Applet")); + gtk_window_set_default_icon_name ("mate-invest-applet"); + + /* Set applet flags first */ + mate_panel_applet_set_flags (applet, MATE_PANEL_APPLET_EXPAND_MINOR); + + /* Initialize settings after applet is set up */ + invest_applet->settings = mate_panel_applet_settings_new (applet, "org.mate.panel.applet.invest"); + + /* Load refresh interval from settings */ + if (G_IS_SETTINGS (invest_applet->settings)) { + invest_applet->refresh_interval = g_settings_get_int (invest_applet->settings, "refresh-interval"); + invest_applet->cycle_interval = g_settings_get_int (invest_applet->settings, "cycle-interval"); + } + + /* Start periodic updates */ + invest_applet->update_timeout_id = g_timeout_add_seconds (invest_applet->refresh_interval * 60, + invest_applet_update_stocks, + invest_applet); + + /* Load initial data */ + invest_applet_update_stocks (invest_applet); + invest_applet_update_display (invest_applet); + + return TRUE; + } + + return FALSE; +} + +static gchar* +create_stock_tooltip (InvestApplet *applet) +{ + GString *tooltip = g_string_new (""); + gint valid_count = 0; + + g_string_append (tooltip, _("Portfolio Summary:\n")); + + for (gint i = 0; i < applet->total_symbols; i++) { + if (applet->stock_valid[i]) { + g_string_append_printf (tooltip, "%s: $%.2f (%.2f%%)\n", + applet->stock_symbols[i], + applet->stock_prices[i], + applet->stock_changes[i]); + valid_count++; + } else { + g_string_append_printf (tooltip, "%s: No data\n", + applet->stock_symbols[i]); + } + } + + if (valid_count == 0) { + g_string_free (tooltip, TRUE); + return g_strdup (_("No valid stock data")); + } + + return g_string_free (tooltip, FALSE); +} + + +static void +free_stock_data (InvestApplet *applet) +{ + g_strfreev (applet->stock_symbols); + g_free (applet->stock_prices); + g_free (applet->stock_changes); + g_free (applet->stock_valid); + g_free (applet->portfolio_summary); + + applet->stock_symbols = NULL; + applet->stock_prices = NULL; + applet->stock_changes = NULL; + applet->stock_valid = NULL; + applet->portfolio_summary = NULL; +} + +static gint +get_valid_stock_indices (InvestApplet *applet, gint *valid_indices) +{ + gint valid_count = 0; + + for (gint i = 0; i < applet->total_symbols; i++) { + if (applet->stock_valid[i]) { + valid_indices[valid_count] = i; + valid_count++; + } + } + + return valid_count; +} + +static void +display_stock_at_index (InvestApplet *applet, gint stock_index) +{ + gchar *message = g_strdup_printf ("%s: $%.2f", + applet->stock_symbols[stock_index], + applet->stock_prices[stock_index]); + update_portfolio_display (applet, message, applet->stock_changes[stock_index]); + g_free (message); +} + +static void +clear_timeout (guint *timeout_id) +{ + if (*timeout_id) { + g_source_remove (*timeout_id); + *timeout_id = 0; + } +} + +static void +update_portfolio_display (InvestApplet *applet, const gchar *message, gdouble change_percent) +{ + g_free (applet->portfolio_summary); + applet->portfolio_summary = g_strdup (message); + applet->total_change_percent = change_percent; + invest_applet_update_display (applet); +} + +MATE_PANEL_APPLET_OUT_PROCESS_FACTORY ("InvestAppletFactory", + INVEST_TYPE_APPLET, + "Investment applet", + invest_applet_factory, + NULL) |