350 lines
12 KiB
C
350 lines
12 KiB
C
#include "TimerGraphWindow.h"
|
|
#include "TimerTaskTree.h"
|
|
#include "TimerGraph.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <math.h>
|
|
|
|
enum {
|
|
DATA_COLUMN = 0,
|
|
SEVEN_DAY_COLUMN,
|
|
THIRTY_DAY_COLUMN,
|
|
SIX_MONTH_COLUMN,
|
|
YEAR_COLUMN,
|
|
ALL_TIME_COLUMN,
|
|
N_COLUMNS
|
|
};
|
|
|
|
typedef struct {
|
|
gint64 max;
|
|
gint64 min;
|
|
gint64 avg;
|
|
} TimerStatSet;
|
|
|
|
struct _TimerGraphWindow {
|
|
GtkApplicationWindow parent;
|
|
|
|
GtkWidget *closeButton;
|
|
GtkWidget *chartBox;
|
|
GtkWidget *dayChart;
|
|
GtkWidget *monthChart;
|
|
|
|
GtkWidget *table;
|
|
GtkTreeStore *tableStore;
|
|
|
|
TimerDataPoint *dayData;
|
|
gsize dayDataLen;
|
|
|
|
TimerDataPoint *taskData;
|
|
gsize taskDataLen;
|
|
};
|
|
|
|
G_DEFINE_TYPE(TimerGraphWindow, timer_graph_window, GTK_TYPE_DIALOG);
|
|
|
|
static gboolean came_after_or_same_day(GDateTime *start, GDateTime *date) {
|
|
int y1, m1, d1, y2, m2, d2;
|
|
g_date_time_get_ymd(start, &y1, &m1, &d1);
|
|
g_date_time_get_ymd(date, &y2, &m2, &d2);
|
|
return (y1 < y2) || (y1 == y2 && m1 < m2) ||
|
|
(y1 == y2 && m1 == m2 && d1 <= d2);
|
|
}
|
|
|
|
static gint day_offset(GDateTime *start, GDateTime *end) {
|
|
gint64 su = g_date_time_to_unix(start);
|
|
gint64 eu = g_date_time_to_unix(end);
|
|
return round((float) (eu - su) / (60 * 60 * 24));
|
|
}
|
|
|
|
static TimerStatSet timer_graph_window_get_stats_for_days(TimerGraphWindow *self, gsize days) {
|
|
GDateTime *ref = NULL;
|
|
if (days != 0) {
|
|
GDateTime *now = g_date_time_new_now_local();
|
|
ref = g_date_time_add_days(now, -days);
|
|
g_date_time_unref(now);
|
|
} else {
|
|
days = self->dayDataLen;
|
|
}
|
|
gint64 total = 0, max = 0, min = G_MAXINT64;
|
|
gsize i;
|
|
for (i = 0; i < self->dayDataLen; ++i) {
|
|
if (ref == NULL || came_after_or_same_day(ref, self->dayData[i].date)) {
|
|
total += self->dayData[i].lenght;
|
|
if (self->dayData[i].lenght > max) {
|
|
max = self->dayData[i].lenght;
|
|
}
|
|
if (self->dayData[i].lenght < min) {
|
|
min = self->dayData[i].lenght;
|
|
}
|
|
}
|
|
}
|
|
if (ref != NULL) {
|
|
g_date_time_unref(ref);
|
|
}
|
|
if (min == G_MAXINT64) {
|
|
min = 0;
|
|
}
|
|
int avg = 0;
|
|
if (days != 0) {
|
|
avg = floor((float)total / (float)days);
|
|
}
|
|
return (TimerStatSet){max, min, avg};
|
|
}
|
|
|
|
|
|
static char *format_hours_and_min(gint64 time) {
|
|
int hour = floor(time / 3600.0f);
|
|
int min = floor(time / 60.0f) - (hour * 60);
|
|
return g_strdup_printf("%02d:%02d", hour, min);
|
|
}
|
|
|
|
static void timer_graph_window_add_table_data(TimerGraphWindow *self) {
|
|
TimerStatSet d7 = timer_graph_window_get_stats_for_days(self, 7);
|
|
TimerStatSet d30 = timer_graph_window_get_stats_for_days(self, 30);
|
|
TimerStatSet d183 = timer_graph_window_get_stats_for_days(self, 183);
|
|
TimerStatSet d365 = timer_graph_window_get_stats_for_days(self, 365);
|
|
TimerStatSet da = timer_graph_window_get_stats_for_days(self, 0);
|
|
|
|
GtkTreeIter highIter;
|
|
gtk_tree_store_append(self->tableStore, &highIter, NULL);
|
|
char *d7ms = format_hours_and_min(d7.max);
|
|
char *d30ms = format_hours_and_min(d30.max);
|
|
char *d183ms = format_hours_and_min(d183.max);
|
|
char *d365ms = format_hours_and_min(d365.max);
|
|
char *dams = format_hours_and_min(da.max);
|
|
gtk_tree_store_set(self->tableStore, &highIter, DATA_COLUMN, "High",
|
|
SEVEN_DAY_COLUMN, d7ms, THIRTY_DAY_COLUMN, d30ms,
|
|
SIX_MONTH_COLUMN, d183ms, YEAR_COLUMN, d365ms,
|
|
ALL_TIME_COLUMN, dams, -1);
|
|
g_free(d7ms);
|
|
g_free(d30ms);
|
|
g_free(d183ms);
|
|
g_free(d365ms);
|
|
g_free(dams);
|
|
|
|
/* Dad requested I remove this
|
|
GtkTreeIter lowIter;
|
|
gtk_tree_store_append(self->tableStore, &lowIter, NULL);
|
|
char *d7ls = format_hours_and_min(d7.min);
|
|
char *d30ls = format_hours_and_min(d30.min);
|
|
char *d183ls = format_hours_and_min(d183.min);
|
|
char *d365ls = format_hours_and_min(d365.min);
|
|
char *dals = format_hours_and_min(da.min);
|
|
gtk_tree_store_set(self->tableStore, &lowIter, DATA_COLUMN, "Low",
|
|
SEVEN_DAY_COLUMN, d7ls, THIRTY_DAY_COLUMN, d30ls,
|
|
SIX_MONTH_COLUMN, d183ls, YEAR_COLUMN, d365ls,
|
|
ALL_TIME_COLUMN, dals, -1);
|
|
g_free(d7ls);
|
|
g_free(d30ls);
|
|
g_free(d183ls);
|
|
g_free(d365ls);
|
|
g_free(dals); */
|
|
|
|
GtkTreeIter avgIter;
|
|
gtk_tree_store_append(self->tableStore, &avgIter, NULL);
|
|
char *d7as = format_hours_and_min(d7.avg);
|
|
char *d30as = format_hours_and_min(d30.avg);
|
|
char *d183as = format_hours_and_min(d183.avg);
|
|
char *d365as = format_hours_and_min(d365.avg);
|
|
char *daas = format_hours_and_min(da.avg);
|
|
gtk_tree_store_set(self->tableStore, &avgIter, DATA_COLUMN, "Average",
|
|
SEVEN_DAY_COLUMN, d7as, THIRTY_DAY_COLUMN, d30as,
|
|
SIX_MONTH_COLUMN, d183as, YEAR_COLUMN, d365as,
|
|
ALL_TIME_COLUMN, daas, -1);
|
|
g_free(d7as);
|
|
g_free(d30as);
|
|
g_free(d183as);
|
|
g_free(d365as);
|
|
g_free(daas);
|
|
}
|
|
|
|
static TimerGraphPoint *timer_graph_window_get_day_graph_data(TimerGraphWindow *self) {
|
|
TimerGraphPoint *pts = g_malloc_n(14, sizeof(TimerGraphPoint));
|
|
gsize i;
|
|
for (i = 0; i < 14; ++i) {
|
|
pts[i].x = i + 1;
|
|
pts[i].y = 0;
|
|
}
|
|
GDateTime *now = g_date_time_new_now_local();
|
|
for (i = 0; i < self->dayDataLen; ++i) {
|
|
int off = day_offset(self->dayData[i].date, now);
|
|
if (off <= 14) {
|
|
pts[off - 1].y += (int) (self->dayData[i].lenght / 60);
|
|
}
|
|
}
|
|
g_date_time_unref(now);
|
|
return pts;
|
|
}
|
|
|
|
static void timer_graph_window_make_day_graph(TimerGraphWindow *self) {
|
|
TimerGraphPoint *pts = timer_graph_window_get_day_graph_data(self);
|
|
self->dayChart = timer_graph_new(pts, 14, "Day", "Time (Total Minutes)", "Last 14 Days");
|
|
gtk_widget_set_vexpand(self->dayChart, TRUE);
|
|
gtk_widget_set_hexpand(self->dayChart, TRUE);
|
|
g_free(pts);
|
|
gtk_box_pack_start(GTK_BOX(self->chartBox), self->dayChart, TRUE, TRUE, 0);
|
|
}
|
|
|
|
static gboolean same_month(GDateTime *d1, GDateTime *d2) {
|
|
int y1, m1, y2, m2;
|
|
g_date_time_get_ymd(d1, &y1, &m1, NULL);
|
|
g_date_time_get_ymd(d2, &y2, &m2, NULL);
|
|
return y1 == y2 && m1 == m2;
|
|
}
|
|
|
|
static int get_days_for_month(int month, int year) {
|
|
switch (month) {
|
|
case 1:
|
|
case 3:
|
|
case 5:
|
|
case 7:
|
|
case 8:
|
|
case 10:
|
|
case 12:
|
|
return 31;
|
|
case 4:
|
|
case 6:
|
|
case 9:
|
|
case 11:
|
|
return 30;
|
|
case 2:
|
|
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) {
|
|
return 29;
|
|
} else {
|
|
return 28;
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static TimerGraphPoint *timer_graph_window_get_month_graph_data(TimerGraphWindow *self) {
|
|
TimerGraphPoint *pts = g_malloc_n(12, sizeof(TimerGraphPoint));
|
|
GDateTime *now = g_date_time_new_now_local();
|
|
GDateTime *ref = g_date_time_add_months(now, -1);
|
|
g_date_time_unref(now);
|
|
gsize i;
|
|
for (i = 0; i < 12; ++i) {
|
|
pts[i].x = i + 1;
|
|
int total = 0;
|
|
gsize j;
|
|
for (j = 0; j < self->dayDataLen; ++j) {
|
|
if (same_month(self->dayData[j].date, ref)) {
|
|
total += (int) (self->dayData[j].lenght / 60);
|
|
}
|
|
}
|
|
int y, m;
|
|
g_date_time_get_ymd(ref, &y, &m, NULL);
|
|
pts[i].y = total / get_days_for_month(m, y);
|
|
GDateTime *new = g_date_time_add_months(ref, -1);
|
|
g_date_time_unref(ref);
|
|
ref = new;
|
|
}
|
|
g_date_time_unref(ref);
|
|
return pts;
|
|
}
|
|
|
|
static void timer_graph_window_make_month_graph(TimerGraphWindow *self) {
|
|
TimerGraphPoint *pts = timer_graph_window_get_month_graph_data(self);
|
|
self->monthChart = timer_graph_new(pts, 12, "Month", "Time (Average Minutes)", "Last Year");
|
|
gtk_widget_set_vexpand(self->monthChart, TRUE);
|
|
gtk_widget_set_hexpand(self->monthChart, TRUE);
|
|
g_free(pts);
|
|
gtk_box_pack_start(GTK_BOX(self->chartBox), self->monthChart, TRUE, TRUE, 0);
|
|
}
|
|
|
|
TimerGraphWindow *timer_graph_window_new(TimerApplication *app,
|
|
TimerDataPoint *dayData, gsize dayDataLen, TimerDataPoint *taskData, gsize taskDataLen) {
|
|
TimerGraphWindow *w =
|
|
g_object_new(TIMER_TYPE_GRAPH_WINDOW, "application", app, "title", "Statistics", NULL);
|
|
w->dayData = dayData;
|
|
w->taskData = taskData;
|
|
w->dayDataLen = dayDataLen;
|
|
w->taskDataLen = taskDataLen;
|
|
timer_graph_window_add_table_data(w);
|
|
timer_graph_window_make_day_graph(w);
|
|
timer_graph_window_make_month_graph(w);
|
|
GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(w));
|
|
GdkDisplay *display = gdk_screen_get_display(screen);
|
|
GdkMonitor *monitor = gdk_display_get_primary_monitor(display);
|
|
if (monitor) {
|
|
GdkRectangle monSize;
|
|
gdk_monitor_get_geometry(monitor, &monSize);
|
|
gtk_window_set_default_size(GTK_WINDOW(w), monSize.width * 0.75f, monSize.height * 0.75f);
|
|
}
|
|
gtk_widget_show_all(GTK_WIDGET(w));
|
|
return w;
|
|
}
|
|
|
|
static void close_button_callback(GtkButton *btn, TimerGraphWindow *win) {
|
|
gtk_window_close(GTK_WINDOW(win));
|
|
}
|
|
|
|
static void timer_graph_window_build_ui(TimerGraphWindow *self) {
|
|
self->closeButton = gtk_dialog_add_button(GTK_DIALOG(self), "Close", 0);
|
|
GtkWidget *topBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
|
self->table = gtk_tree_view_new();
|
|
gtk_widget_set_halign(self->table, GTK_ALIGN_CENTER);
|
|
gtk_box_pack_start(GTK_BOX(topBox), self->table, FALSE, FALSE, 0);
|
|
self->chartBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
|
gtk_container_add(GTK_CONTAINER(topBox), self->chartBox);
|
|
gtk_container_add(
|
|
GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(self))), topBox);
|
|
}
|
|
|
|
static void timer_graph_window_add_tree_columns(TimerGraphWindow *self) {
|
|
gtk_tree_view_append_column(
|
|
GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"", gtk_cell_renderer_text_new(), "text", DATA_COLUMN, NULL));
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"Last 7 Days", gtk_cell_renderer_text_new(),
|
|
"text", SEVEN_DAY_COLUMN, NULL));
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"Last 30 Days",
|
|
gtk_cell_renderer_text_new(), "text",
|
|
THIRTY_DAY_COLUMN, NULL));
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"Last 6 Months",
|
|
gtk_cell_renderer_text_new(), "text",
|
|
SIX_MONTH_COLUMN, NULL));
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"Last Year", gtk_cell_renderer_text_new(),
|
|
"text", YEAR_COLUMN, NULL));
|
|
gtk_tree_view_append_column(GTK_TREE_VIEW(self->table),
|
|
gtk_tree_view_column_new_with_attributes(
|
|
"All Time", gtk_cell_renderer_text_new(),
|
|
"text", ALL_TIME_COLUMN, NULL));
|
|
}
|
|
|
|
static void timer_graph_window_finalize(GObject *self) {
|
|
timer_free_task_data(TIMER_GRAPH_WINDOW(self)->dayData, TIMER_GRAPH_WINDOW(self)->dayDataLen);
|
|
timer_free_task_data(TIMER_GRAPH_WINDOW(self)->taskData, TIMER_GRAPH_WINDOW(self)->taskDataLen);
|
|
G_OBJECT_CLASS(timer_graph_window_parent_class)->finalize(self);
|
|
}
|
|
|
|
static void timer_graph_window_class_init(TimerGraphWindowClass *class) {
|
|
G_OBJECT_CLASS(class)->finalize = timer_graph_window_finalize;
|
|
}
|
|
|
|
static void timer_graph_window_init(TimerGraphWindow *self) {
|
|
gtk_window_set_keep_above(GTK_WINDOW(self), TRUE);
|
|
gtk_window_set_position(GTK_WINDOW(self), GTK_WIN_POS_MOUSE);
|
|
timer_graph_window_build_ui(self);
|
|
g_signal_connect(self->closeButton, "clicked",
|
|
G_CALLBACK(close_button_callback), self);
|
|
gtk_tree_selection_set_mode(
|
|
gtk_tree_view_get_selection(GTK_TREE_VIEW(self->table)),
|
|
GTK_SELECTION_NONE);
|
|
self->tableStore = gtk_tree_store_new(
|
|
N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
|
|
G_TYPE_STRING, G_TYPE_STRING);
|
|
gtk_tree_view_set_model(GTK_TREE_VIEW(self->table),
|
|
GTK_TREE_MODEL(self->tableStore));
|
|
timer_graph_window_add_tree_columns(self);
|
|
}
|