summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--invest-applet/invest/Makefile.am1
-rw-r--r--invest-applet/invest/invest-applet-chart.c699
-rw-r--r--invest-applet/invest/invest-applet-chart.h35
-rw-r--r--invest-applet/invest/invest-applet.c73
-rw-r--r--invest-applet/invest/invest-applet.h77
5 files changed, 846 insertions, 39 deletions
diff --git a/invest-applet/invest/Makefile.am b/invest-applet/invest/Makefile.am
index cb1115b5..2ee22ab1 100644
--- a/invest-applet/invest/Makefile.am
+++ b/invest-applet/invest/Makefile.am
@@ -11,6 +11,7 @@ AM_CPPFLAGS = \
APPLET_SOURCES = \
invest-applet.c \
+ invest-applet-chart.c \
$(NULL)
APPLET_LIBS = \
diff --git a/invest-applet/invest/invest-applet-chart.c b/invest-applet/invest/invest-applet-chart.c
new file mode 100644
index 00000000..1114b900
--- /dev/null
+++ b/invest-applet/invest/invest-applet-chart.c
@@ -0,0 +1,699 @@
+/*
+ * MATE Invest Applet - Chart functionality
+ * Copyright (C) 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 <gtk/gtk.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+#include <cairo/cairo.h>
+#include <mate-panel-applet.h>
+#include <mate-panel-applet-gsettings.h>
+#include "invest-applet-chart.h"
+
+typedef struct _StockChartData StockChartData;
+
+struct _StockChartData {
+ gchar *symbol;
+ gdouble *prices;
+ gint64 *timestamps;
+ gint data_count;
+ gboolean valid;
+};
+
+struct _InvestChart {
+ InvestApplet *applet;
+ GtkWidget *window;
+ GtkWidget *drawing_area;
+ StockChartData *chart_data;
+ gint chart_data_count;
+ gchar *chart_range;
+ gchar *chart_interval;
+};
+
+static void fetch_chart_data (InvestChart *chart);
+static void on_chart_data_received (SoupSession *session, SoupMessage *msg, gpointer user_data);
+static gboolean chart_draw_cb (GtkWidget *widget, cairo_t *cr, InvestChart *chart);
+static gboolean chart_window_key_press (GtkWidget *widget, GdkEventKey *event, InvestChart *chart);
+static void chart_range_button_clicked (GtkWidget *widget, InvestChart *chart);
+static void create_chart_toolbar (InvestChart *chart, GtkWidget *parent);
+static void free_chart_data (InvestChart *chart);
+static void draw_loading_message (cairo_t *cr, gint width, gint height, const gchar *message);
+static void on_chart_window_destroy (GtkWidget *widget, InvestChart *chart);
+
+InvestChart*
+invest_chart_new (InvestApplet *applet)
+{
+ InvestChart *chart = g_malloc0 (sizeof (InvestChart));
+ chart->applet = applet;
+ chart->chart_data = NULL;
+ chart->chart_data_count = 0;
+ chart->chart_range = g_strdup ("1d");
+ chart->chart_interval = g_strdup ("1m");
+ return chart;
+}
+
+void
+invest_chart_free (InvestChart *chart)
+{
+ if (!chart) {
+ return;
+ }
+
+ if (chart->window) {
+ invest_chart_hide (chart);
+ }
+
+ free_chart_data (chart);
+ g_free (chart->chart_range);
+ g_free (chart->chart_interval);
+ g_free (chart);
+}
+
+static void
+on_chart_window_destroy (GtkWidget *widget, InvestChart *chart)
+{
+ chart->window = NULL;
+ chart->drawing_area = NULL;
+}
+
+void
+invest_chart_show (InvestChart *chart)
+{
+ if (chart->window && gtk_widget_get_visible (chart->window)) {
+ /* Chart is already visible, just bring it to front */
+ gtk_window_present (GTK_WINDOW (chart->window));
+ return;
+ }
+
+ /* Set default chart range if not already set */
+ if (!chart->chart_range) {
+ chart->chart_range = g_strdup ("1d");
+ chart->chart_interval = g_strdup ("1m");
+ }
+
+ /* Create chart window */
+ chart->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title (GTK_WINDOW (chart->window), _("Stock Chart"));
+ gtk_window_set_default_size (GTK_WINDOW (chart->window), 800, 500);
+ gtk_window_set_resizable (GTK_WINDOW (chart->window), TRUE);
+ gtk_window_set_modal (GTK_WINDOW (chart->window), FALSE);
+
+ g_signal_connect (chart->window, "destroy", G_CALLBACK (on_chart_window_destroy), chart);
+
+ /* Create main vertical box */
+ GtkWidget *main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
+ gtk_container_add (GTK_CONTAINER (chart->window), main_vbox);
+
+ /* Create toolbar with time range buttons */
+ create_chart_toolbar (chart, main_vbox);
+
+ /* Create drawing area for chart */
+ chart->drawing_area = gtk_drawing_area_new ();
+ gtk_widget_set_vexpand (chart->drawing_area, TRUE);
+ gtk_widget_set_hexpand (chart->drawing_area, TRUE);
+ g_signal_connect (chart->drawing_area, "draw", G_CALLBACK (chart_draw_cb), chart);
+
+ /* Add drawing area to main box */
+ gtk_box_pack_start (GTK_BOX (main_vbox), chart->drawing_area, TRUE, TRUE, 0);
+
+ /* We want to close window easily with Esc */
+ g_signal_connect (chart->window, "key-press-event", G_CALLBACK (chart_window_key_press), chart);
+
+ gtk_widget_set_can_focus (chart->window, TRUE);
+ gtk_widget_show_all (chart->window);
+
+ fetch_chart_data (chart);
+}
+
+void
+invest_chart_hide (InvestChart *chart)
+{
+ if (!chart->window) {
+ return;
+ }
+
+ gtk_widget_destroy (chart->window);
+ chart->window = NULL;
+ chart->drawing_area = NULL;
+}
+
+gboolean
+invest_chart_is_visible (InvestChart *chart)
+{
+ if (!chart || !chart->window) {
+ return FALSE;
+ }
+ return gtk_widget_get_visible (chart->window);
+}
+
+void
+invest_chart_refresh_data (InvestChart *chart)
+{
+ /* Only refresh if chart is visible */
+ if (chart && chart->window && gtk_widget_get_visible (chart->window)) {
+ fetch_chart_data (chart);
+ }
+}
+
+static void
+fetch_chart_data (InvestChart *chart)
+{
+ gchar **symbols;
+ gint symbol_count;
+
+ if (!G_IS_SETTINGS (chart->applet->settings)) {
+ g_warning ("Settings not available for chart data");
+ return;
+ }
+
+ symbols = g_settings_get_strv (chart->applet->settings, "stock-symbols");
+ if (!symbols || !symbols[0]) {
+ g_strfreev (symbols);
+ return;
+ }
+
+ symbol_count = g_strv_length (symbols);
+
+ /* Free existing chart data */
+ free_chart_data (chart);
+
+ /* Allocate chart data array */
+ chart->chart_data = g_malloc0 (symbol_count * sizeof (StockChartData));
+ chart->chart_data_count = symbol_count;
+
+ /* Initialize chart data for each stock symbol */
+ for (gint i = 0; i < symbol_count; i++) {
+ chart->chart_data[i].symbol = g_strdup (symbols[i]);
+ chart->chart_data[i].valid = FALSE;
+ chart->chart_data[i].prices = NULL;
+ chart->chart_data[i].timestamps = NULL;
+ chart->chart_data[i].data_count = 0;
+ }
+
+ /* This will show loading state */
+ if (chart->window && gtk_widget_get_visible (chart->window)) {
+ gtk_widget_queue_draw (chart->window);
+ }
+
+ /* Fetch chart the actual stock data */
+ for (gint i = 0; i < symbol_count; i++) {
+ const gchar *range = chart->chart_range ? chart->chart_range : "1d";
+ const gchar *interval = chart->chart_interval ? chart->chart_interval : "1m";
+ gchar *url = g_strdup_printf ("https://query2.finance.yahoo.com/v8/finance/chart/%s?interval=%s&range=%s", symbols[i], interval, range);
+ 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] = chart;
+ user_data_with_index[1] = GINT_TO_POINTER (i);
+
+ /* Queue the actual request to the Yahoo Finance API */
+ soup_session_queue_message (chart->applet->soup_session, msg, on_chart_data_received, user_data_with_index);
+ g_free (url);
+ }
+
+ g_strfreev (symbols);
+}
+
+static void
+on_chart_data_received (SoupSession *session, SoupMessage *msg, gpointer user_data)
+{
+ gpointer *user_data_with_index = (gpointer *)user_data;
+ InvestChart *chart = (InvestChart *)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 chart 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 chart 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);
+
+ /* This was... fun. The structure for the chart data looks like this:
+ *
+ * {
+ * "chart": {
+ * "result": [
+ * {
+ * "timestamp": [ ... ],
+ * "indicators": {
+ * "quote": [{
+ * "close": [ ... ]
+ * }]
+ * }
+ * }
+ * ]
+ * }
+ * }
+ *
+ * The closing prices for each stock map to the same index of
+ * the corresponding timestamps, so we need to parse them all
+ * separately but store them together for display later.
+ */
+ if (json_object_has_member (root_obj, "chart")) {
+ JsonObject *chart_obj = json_object_get_object_member (root_obj, "chart");
+ if (json_object_has_member (chart_obj, "result") && !json_object_get_null_member (chart_obj, "result")) {
+ JsonArray *results = json_object_get_array_member (chart_obj, "result");
+ if (results && json_array_get_length (results) > 0) {
+ JsonObject *result = json_array_get_object_element (results, 0);
+ if (result) {
+ /* Parse timestamps */
+ if (json_object_has_member (result, "timestamp")) {
+ JsonArray *timestamps = json_object_get_array_member (result, "timestamp");
+ if (timestamps) {
+ gint timestamp_count = json_array_get_length (timestamps);
+ /* Allocate memory for all the timestamps */
+ chart->chart_data[symbol_index].timestamps = g_malloc0 (timestamp_count * sizeof (gint64));
+ chart->chart_data[symbol_index].data_count = timestamp_count;
+
+ /* Then store them */
+ for (gint i = 0; i < timestamp_count; i++) {
+ JsonNode *timestamp_node = json_array_get_element (timestamps, i);
+ if (json_node_get_value_type (timestamp_node) == G_TYPE_INT64) {
+ chart->chart_data[symbol_index].timestamps[i] = json_node_get_int (timestamp_node);
+ }
+ }
+ }
+ }
+
+ /* Parse price data */
+ if (json_object_has_member (result, "indicators")) {
+ JsonObject *indicators = json_object_get_object_member (result, "indicators");
+ if (json_object_has_member (indicators, "quote")) {
+ JsonArray *quotes = json_object_get_array_member (indicators, "quote");
+ if (quotes && json_array_get_length (quotes) > 0) {
+ /* For some reason quote is an array, but there's only one quote for each stock symbol */
+ JsonObject *quote = json_array_get_object_element (quotes, 0);
+ if (quote && json_object_has_member (quote, "close")) {
+ JsonArray *close_prices = json_object_get_array_member (quote, "close");
+ if (close_prices) {
+ gint price_count = json_array_get_length (close_prices);
+ /* Allocate memory for all the closing prices */
+ chart->chart_data[symbol_index].prices = g_malloc0 (price_count * sizeof (gdouble));
+
+ /* Then store them */
+ for (gint i = 0; i < price_count; i++) {
+ JsonNode *price_node = json_array_get_element (close_prices, i);
+ if (json_node_get_value_type (price_node) == G_TYPE_DOUBLE) {
+ chart->chart_data[symbol_index].prices[i] = json_node_get_double (price_node);
+ }
+ }
+ /* We assume that the existence of data constitutes valid data */
+ chart->chart_data[symbol_index].valid = TRUE;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+cleanup:
+ if (parser) {
+ g_object_unref (parser);
+ }
+
+ /* Redraw chart if window is still visible */
+ if (chart->window && gtk_widget_get_visible (chart->window)) {
+ gtk_widget_queue_draw (chart->window);
+ }
+
+ g_free (user_data_with_index);
+}
+
+static gboolean
+chart_draw_cb (GtkWidget *widget, cairo_t *cr, InvestChart *chart)
+{
+ gint width, height;
+ GtkAllocation allocation;
+ gtk_widget_get_size_request (widget, &width, &height);
+ if (width <= 0 || height <= 0) {
+ gtk_widget_get_allocated_size (widget, &allocation, NULL);
+ width = allocation.width;
+ height = allocation.height;
+ }
+
+ /* Clear background */
+ cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+ cairo_paint (cr);
+
+ /* Check if we have any valid data to display */
+ gboolean has_valid_data = FALSE;
+ if (chart->chart_data && chart->chart_data_count > 0) {
+ for (gint i = 0; i < chart->chart_data_count; i++) {
+ if (chart->chart_data[i].valid && chart->chart_data[i].data_count > 0) {
+ has_valid_data = TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!has_valid_data) {
+ draw_loading_message (cr, width, height, _("Loading chart data..."));
+ return FALSE;
+ }
+
+ /* Find min/max prices for scaling the chart properly */
+ gdouble min_price = G_MAXDOUBLE;
+ gdouble max_price = G_MINDOUBLE;
+ gint64 min_time = G_MAXINT64;
+ gint64 max_time = G_MININT64;
+
+ for (gint i = 0; i < chart->chart_data_count; i++) {
+ if (chart->chart_data[i].valid && chart->chart_data[i].data_count > 0) {
+ for (gint j = 0; j < chart->chart_data[i].data_count; j++) {
+ if (chart->chart_data[i].prices[j] > 0) {
+ min_price = MIN (min_price, chart->chart_data[i].prices[j]);
+ max_price = MAX (max_price, chart->chart_data[i].prices[j]);
+ }
+ }
+ if (chart->chart_data[i].timestamps[0] > 0 &&
+ chart->chart_data[i].timestamps[chart->chart_data[i].data_count - 1] > 0 &&
+ chart->chart_data[i].timestamps[0] < 9999999999) { /* HACK: This works, but there's probably a better way */
+ min_time = MIN (min_time, chart->chart_data[i].timestamps[0]);
+ max_time = MAX (max_time, chart->chart_data[i].timestamps[chart->chart_data[i].data_count - 1]);
+ }
+ }
+ }
+
+ if (min_price == G_MAXDOUBLE || max_price == G_MINDOUBLE || min_time == G_MAXINT64 || max_time == G_MININT64) {
+ draw_loading_message (cr, width, height, _("No chart data available"));
+ return FALSE;
+ }
+
+ /* Add some padding to price range */
+ gdouble price_range = max_price - min_price;
+ min_price -= price_range * 0.05;
+ max_price += price_range * 0.05;
+
+ /* Calculate scaling factors */
+ gdouble x_scale = (gdouble)(width - 100) / (max_time - min_time);
+ gdouble y_scale = (gdouble)(height - 100) / (max_price - min_price);
+
+ /* Draw grid */
+ cairo_set_source_rgb (cr, 0.9, 0.9, 0.9);
+ cairo_set_line_width (cr, 1.0);
+
+ /* Horizontal grid lines */
+ for (gint i = 0; i <= 10; i++) {
+ gdouble y = 50 + (gdouble)i * (height - 100) / 10;
+ cairo_move_to (cr, 50, y);
+ cairo_line_to (cr, width - 50, y);
+ cairo_stroke (cr);
+ }
+
+ /* Vertical grid lines */
+ for (gint i = 0; i <= 10; i++) {
+ gdouble x = 50 + (gdouble)i * (width - 100) / 10;
+ cairo_move_to (cr, x, 50);
+ cairo_line_to (cr, x, height - 50);
+ cairo_stroke (cr);
+ }
+
+ /* Draw price labels */
+ cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
+ cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
+ cairo_set_font_size (cr, 10);
+
+ for (gint i = 0; i <= 5; i++) {
+ gdouble price = max_price - (gdouble)i * (max_price - min_price) / 5; /* Inverted for correct Y-axis */
+ gdouble y = 50 + (gdouble)i * (height - 100) / 5;
+ gchar price_str[32];
+ g_snprintf (price_str, sizeof (price_str), "%.2f", price);
+ cairo_move_to (cr, 5, y + 3);
+ cairo_show_text (cr, price_str);
+ }
+
+ /* Draw time labels */
+ for (gint i = 0; i <= 5; i++) {
+ gint64 time = min_time + (gint64)i * (max_time - min_time) / 5;
+ gdouble x = 50 + (gdouble)i * (width - 100) / 5;
+
+ /* Convert timestamp to local time */
+ GDateTime *dt = g_date_time_new_from_unix_local (time);
+ gchar time_str[64];
+
+ /* Format based on time range */
+ if (chart->chart_range && g_strcmp0 (chart->chart_range, "1d") == 0) {
+ /* For today view, show time */
+ g_snprintf (time_str, sizeof (time_str), "%02d:%02d",
+ g_date_time_get_hour (dt), g_date_time_get_minute (dt));
+ } else if (chart->chart_range && (g_strcmp0 (chart->chart_range, "5d") == 0 ||
+ g_strcmp0 (chart->chart_range, "1mo") == 0 ||
+ g_strcmp0 (chart->chart_range, "3mo") == 0)) {
+ /* For weekly/monthly/quarterly views, show date */
+ g_snprintf (time_str, sizeof (time_str), "%02d/%02d",
+ g_date_time_get_month (dt), g_date_time_get_day_of_month (dt));
+ } else {
+ /* For yearly and longer views, show month/year */
+ g_snprintf (time_str, sizeof (time_str), "%02d/%d",
+ g_date_time_get_month (dt), g_date_time_get_year (dt));
+ }
+
+ cairo_move_to (cr, x - 20, height - 20);
+ cairo_show_text (cr, time_str);
+ g_date_time_unref (dt);
+ }
+
+ /* Draw stock price lines */
+ const gchar *colors[] = {
+ "#CC0000",
+ "#3465A4",
+ "#73D216",
+ "#FCE94F",
+ "#AD7FA8",
+ "#F57900",
+ "#C17D11",
+ "#555753"
+ };
+
+ /* Collect valid stocks and their current prices for sorting */
+ typedef struct {
+ gint index;
+ gdouble current_price;
+ } StockSortInfo;
+
+ StockSortInfo *sort_info = g_malloc0 (chart->chart_data_count * sizeof (StockSortInfo));
+ gint valid_count = 0;
+
+ for (gint i = 0; i < chart->chart_data_count; i++) {
+ if (chart->chart_data[i].valid && chart->chart_data[i].data_count > 0) {
+ sort_info[valid_count].index = i;
+ sort_info[valid_count].current_price = chart->chart_data[i].prices[chart->chart_data[i].data_count - 1];
+ valid_count++;
+ }
+ }
+
+ /* Sort by current price (highest to lowest) */
+ for (gint i = 0; i < valid_count - 1; i++) {
+ for (gint j = i + 1; j < valid_count; j++) {
+ if (sort_info[i].current_price < sort_info[j].current_price) {
+ StockSortInfo temp = sort_info[i];
+ sort_info[i] = sort_info[j];
+ sort_info[j] = temp;
+ }
+ }
+ }
+
+ /* Draw lines and legend in sorted order */
+ for (gint i = 0; i < valid_count; i++) {
+ gint stock_index = sort_info[i].index;
+
+ /* Set color for this stock */
+ gint color_idx = i % G_N_ELEMENTS (colors);
+ GdkRGBA color;
+ gdk_rgba_parse (&color, colors[color_idx]);
+ gdk_cairo_set_source_rgba (cr, &color);
+ cairo_set_line_width (cr, 2.0);
+
+ /* Draw price line */
+ gboolean first_point = TRUE;
+
+ for (gint j = 0; j < chart->chart_data[stock_index].data_count; j++) {
+ if (chart->chart_data[stock_index].prices[j] > 0 &&
+ chart->chart_data[stock_index].timestamps[j] > 0 &&
+ chart->chart_data[stock_index].timestamps[j] < 9999999999) { /* Just to be safe... */
+
+ gdouble x = 50 + (gdouble)j * (width - 100) / (chart->chart_data[stock_index].data_count - 1);
+ gdouble y = 50 + (max_price - chart->chart_data[stock_index].prices[j]) * y_scale;
+
+ if (first_point) {
+ cairo_move_to (cr, x, y);
+ first_point = FALSE;
+ } else {
+ cairo_line_to (cr, x, y);
+ }
+ }
+ }
+ cairo_stroke (cr);
+
+ /* Draw legend with current price */
+ gdouble current_price = chart->chart_data[stock_index].prices[chart->chart_data[stock_index].data_count - 1];
+ gchar *legend_text = g_strdup_printf ("%s: $%.2f", chart->chart_data[stock_index].symbol, current_price);
+ cairo_move_to (cr, width - 200, 30 + i * 20);
+ cairo_show_text (cr, legend_text);
+ g_free (legend_text);
+ }
+
+ g_free (sort_info);
+
+ return FALSE;
+}
+
+static gboolean
+chart_window_key_press (GtkWidget *widget, GdkEventKey *event, InvestChart *chart)
+{
+ if (event->keyval == GDK_KEY_Escape) {
+ invest_chart_hide (chart);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+create_chart_toolbar (InvestChart *chart, GtkWidget *parent)
+{
+ /* Let's use a horizontal box as a toolbar */
+ GtkWidget *toolbar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
+ gtk_container_set_border_width (GTK_CONTAINER (toolbar), 5);
+ gtk_box_pack_start (GTK_BOX (parent), toolbar, FALSE, FALSE, 0);
+
+ GtkWidget *label = gtk_label_new (_("Time Range:"));
+ gtk_box_pack_start (GTK_BOX (toolbar), label, FALSE, FALSE, 5);
+
+ /* Buttons for different time ranges */
+ struct {
+ const gchar *text;
+ const gchar *range;
+ const gchar *interval;
+ } ranges[] = {
+ { "Today", "1d", "1m" },
+ { "Week", "5d", "5m" },
+ { "Month", "1mo", "30m" },
+ { "YTD", "ytd", "1d" },
+ { "Year", "1y", "1d" },
+ { "5Y", "5y", "1wk" },
+ { "All", "max", "1mo" }
+ };
+
+ for (gint i = 0; i < G_N_ELEMENTS (ranges); i++) {
+ GtkWidget *button = gtk_button_new_with_label (ranges[i].text);
+ gtk_widget_set_size_request (button, 60, 30);
+
+ /* Store range and interval data in button */
+ g_object_set_data (G_OBJECT (button), "range", (gpointer)ranges[i].range);
+ g_object_set_data (G_OBJECT (button), "interval", (gpointer)ranges[i].interval);
+
+ g_signal_connect (button, "clicked", G_CALLBACK (chart_range_button_clicked), chart);
+ gtk_box_pack_start (GTK_BOX (toolbar), button, FALSE, FALSE, 2);
+
+ /* Disable the currently-selected button */
+ if (chart->chart_range && g_strcmp0 (chart->chart_range, ranges[i].range) == 0) {
+ gtk_widget_set_sensitive (button, FALSE);
+ }
+ }
+
+ gtk_box_pack_start (GTK_BOX (toolbar), gtk_label_new (""), TRUE, TRUE, 0);
+}
+
+static void
+free_chart_data (InvestChart *chart)
+{
+ if (!chart->chart_data) {
+ return;
+ }
+
+ for (gint i = 0; i < chart->chart_data_count; i++) {
+ g_free (chart->chart_data[i].symbol);
+ g_free (chart->chart_data[i].prices);
+ g_free (chart->chart_data[i].timestamps);
+ }
+
+ g_free (chart->chart_data);
+ chart->chart_data = NULL;
+ chart->chart_data_count = 0;
+}
+
+static void
+chart_range_button_clicked (GtkWidget *widget, InvestChart *chart)
+{
+ /* Get range and interval from button data */
+ const gchar *range = (const gchar *)g_object_get_data (G_OBJECT (widget), "range");
+ const gchar *interval = (const gchar *)g_object_get_data (G_OBJECT (widget), "interval");
+
+ if (!range || !interval) {
+ return;
+ }
+
+ /* Update chart's range and interval */
+ g_free (chart->chart_range);
+ g_free (chart->chart_interval);
+ chart->chart_range = g_strdup (range);
+ chart->chart_interval = g_strdup (interval);
+
+ /* Re-enable all buttons and disable the newly-selected one */
+ GtkWidget *parent = gtk_widget_get_parent (widget);
+ if (GTK_IS_BOX (parent)) {
+ GList *children = gtk_container_get_children (GTK_CONTAINER (parent));
+ for (GList *l = children; l; l = l->next) {
+ GtkWidget *child = GTK_WIDGET (l->data);
+ if (GTK_IS_BUTTON (child)) {
+ gtk_widget_set_sensitive (child, TRUE);
+ }
+ }
+ g_list_free (children);
+ }
+
+ /* Disable the clicked button */
+ gtk_widget_set_sensitive (widget, FALSE);
+
+ /* Fetch new chart data with updated range */
+ fetch_chart_data (chart);
+}
+
+static void
+draw_loading_message (cairo_t *cr, gint width, gint height, const gchar *message)
+{
+ cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
+ cairo_select_font_face (cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
+ cairo_set_font_size (cr, 16);
+ cairo_move_to (cr, width / 2 - 50, height / 2);
+ cairo_show_text (cr, message);
+}
diff --git a/invest-applet/invest/invest-applet-chart.h b/invest-applet/invest/invest-applet-chart.h
new file mode 100644
index 00000000..3e14917d
--- /dev/null
+++ b/invest-applet/invest/invest-applet-chart.h
@@ -0,0 +1,35 @@
+/*
+ * MATE Invest Applet - Chart functionality header
+ * Copyright (C) 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
+ */
+
+#ifndef INVEST_APPLET_CHART_H
+#define INVEST_APPLET_CHART_H
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include "invest-applet.h"
+typedef struct _InvestChart InvestChart;
+
+InvestChart* invest_chart_new (InvestApplet *applet);
+void invest_chart_free (InvestChart *chart);
+void invest_chart_show (InvestChart *chart);
+void invest_chart_hide (InvestChart *chart);
+gboolean invest_chart_is_visible (InvestChart *chart);
+void invest_chart_refresh_data (InvestChart *chart);
+
+#endif /* INVEST_APPLET_CHART_H */
diff --git a/invest-applet/invest/invest-applet.c b/invest-applet/invest/invest-applet.c
index 8d83aa46..979d90fb 100644
--- a/invest-applet/invest/invest-applet.c
+++ b/invest-applet/invest/invest-applet.c
@@ -28,9 +28,8 @@
#include <libsoup/soup.h>
#include <mate-panel-applet.h>
#include <mate-panel-applet-gsettings.h>
-
-typedef struct _InvestApplet InvestApplet;
-typedef struct _InvestAppletClass InvestAppletClass;
+#include "invest-applet.h"
+#include "invest-applet-chart.h"
static gchar* create_stock_tooltip (InvestApplet *applet);
static void free_stock_data (InvestApplet *applet);
@@ -40,39 +39,6 @@ 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 *stock_summary;
- gdouble 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 */
@@ -392,6 +358,9 @@ invest_applet_preferences_cb (GtkAction *action,
/* Trigger update */
invest_applet_update_stocks (applet);
+
+ /* Refresh chart data if chart is visible */
+ invest_chart_refresh_data (applet->chart);
}
gtk_widget_destroy (dialog);
@@ -405,6 +374,17 @@ invest_applet_refresh_cb (GtkAction *action,
}
static void
+invest_applet_show_chart_cb (GtkAction *action,
+ InvestApplet *applet)
+{
+ if (invest_chart_is_visible (applet->chart)) {
+ invest_chart_hide (applet->chart);
+ } else {
+ invest_chart_show (applet->chart);
+ }
+}
+
+static void
invest_applet_help_cb (GtkAction *action,
InvestApplet *applet)
{
@@ -483,8 +463,11 @@ invest_applet_button_press (GtkWidget *widget,
InvestApplet *applet)
{
if (event->button == 1 && event->type == GDK_BUTTON_PRESS) {
- /* cycle to next stock */
- invest_applet_cycle_stocks (applet);
+ if (invest_chart_is_visible (applet->chart)) {
+ invest_chart_hide (applet->chart);
+ } else {
+ invest_chart_show (applet->chart);
+ }
return TRUE;
}
@@ -498,6 +481,14 @@ invest_applet_destroy (GtkWidget *widget,
clear_timeout (&applet->update_timeout_id);
clear_timeout (&applet->cycle_timeout_id);
+ if (applet->chart) {
+ if (invest_chart_is_visible (applet->chart)) {
+ invest_chart_hide (applet->chart);
+ }
+ invest_chart_free (applet->chart);
+ applet->chart = NULL;
+ }
+
if (applet->soup_session) {
g_object_unref (applet->soup_session);
applet->soup_session = NULL;
@@ -532,6 +523,9 @@ invest_applet_init (InvestApplet *applet)
applet->cycle_position = 0;
applet->cycle_timeout_id = 0;
+ /* init chart functionality */
+ applet->chart = invest_chart_new (applet);
+
/* init networking */
applet->soup_session = soup_session_new ();
@@ -553,6 +547,7 @@ invest_applet_init (InvestApplet *applet)
const GtkActionEntry menu_actions[] = {
{ "InvestRefresh", "view-refresh", N_("_Refresh"), NULL, NULL, G_CALLBACK (invest_applet_refresh_cb) },
+ { "InvestShowChart", "invest_neutral", N_("_Show Chart"), NULL, NULL, G_CALLBACK (invest_applet_show_chart_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) }
@@ -563,6 +558,7 @@ invest_applet_init (InvestApplet *applet)
gtk_action_group_add_actions (action_group, menu_actions, G_N_ELEMENTS (menu_actions), applet);
const gchar *ui = "<menuitem name=\"Invest Refresh\" action=\"InvestRefresh\" />"
+ "<menuitem name=\"Invest Show Chart\" action=\"InvestShowChart\" />"
"<separator />"
"<menuitem name=\"Invest Preferences\" action=\"InvestPreferences\" />"
"<menuitem name=\"Invest Help\" action=\"InvestHelp\" />"
@@ -648,7 +644,6 @@ create_stock_tooltip (InvestApplet *applet)
return g_string_free (tooltip, FALSE);
}
-
static void
free_stock_data (InvestApplet *applet)
{
diff --git a/invest-applet/invest/invest-applet.h b/invest-applet/invest/invest-applet.h
new file mode 100644
index 00000000..ce610474
--- /dev/null
+++ b/invest-applet/invest/invest-applet.h
@@ -0,0 +1,77 @@
+/*
+ * MATE Invest Applet - Main applet header
+ * Copyright (C) 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
+ */
+
+#ifndef INVEST_APPLET_H
+#define INVEST_APPLET_H
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <mate-panel-applet.h>
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+typedef struct _InvestApplet InvestApplet;
+typedef struct _InvestAppletClass InvestAppletClass;
+
+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 *stock_summary;
+ gdouble 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 through multiple stocks */
+ gint cycle_position;
+ guint cycle_timeout_id;
+
+ /* Chart functionality */
+ struct _InvestChart *chart;
+};
+
+struct _InvestAppletClass {
+ MatePanelAppletClass parent_class;
+};
+
+#define INVEST_TYPE_APPLET (invest_applet_get_type())
+#define INVEST_APPLET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), INVEST_TYPE_APPLET, InvestApplet))
+#define INVEST_APPLET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), INVEST_TYPE_APPLET, InvestAppletClass))
+#define INVEST_IS_APPLET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), INVEST_TYPE_APPLET))
+#define INVEST_IS_APPLET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), INVEST_TYPE_APPLET))
+#define INVEST_APPLET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), INVEST_TYPE_APPLET, InvestAppletClass))
+
+GType invest_applet_get_type(void);
+
+G_END_DECLS
+
+#endif /* INVEST_APPLET_H */