// 4 september 2015 #include "uipriv_unix.h" // LONGTERM imitate gnome-calendar's day/month/year entries above the calendar // LONGTERM allow entering a 24-hour hour in the hour spinbutton and adjust accordingly #define uiprivDateTimePickerWidgetType (uiprivDateTimePickerWidget_get_type()) #define uiprivDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidget)) #define isDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), uiprivDateTimePickerWidgetType)) #define uiprivDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidgetClass)) #define isDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), uiprivDateTimePickerWidget)) #define getDateTimePickerWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), uiprivDateTimePickerWidgetType, uiprivDateTimePickerWidgetClass)) typedef struct uiprivDateTimePickerWidget uiprivDateTimePickerWidget; typedef struct uiprivDateTimePickerWidgetClass uiprivDateTimePickerWidgetClass; struct uiprivDateTimePickerWidget { GtkToggleButton parent_instance; gulong toggledSignal; gboolean hasTime; gboolean hasDate; GtkWidget *window; GtkWidget *box; GtkWidget *calendar; GtkWidget *timebox; GtkWidget *hours; GtkWidget *minutes; GtkWidget *seconds; GtkWidget *ampm; gulong calendarBlock; gulong hoursBlock; gulong minutesBlock; gulong secondsBlock; gulong ampmBlock; GdkDevice *keyboard; GdkDevice *mouse; }; struct uiprivDateTimePickerWidgetClass { GtkToggleButtonClass parent_class; }; G_DEFINE_TYPE(uiprivDateTimePickerWidget, uiprivDateTimePickerWidget, GTK_TYPE_TOGGLE_BUTTON) static int realSpinValue(GtkSpinButton *spinButton) { GtkAdjustment *adj; adj = gtk_spin_button_get_adjustment(spinButton); return (int) gtk_adjustment_get_value(adj); } static void setRealSpinValue(GtkSpinButton *spinButton, int value, gulong block) { GtkAdjustment *adj; g_signal_handler_block(spinButton, block); adj = gtk_spin_button_get_adjustment(spinButton); gtk_adjustment_set_value(adj, value); g_signal_handler_unblock(spinButton, block); } static GDateTime *selected(uiprivDateTimePickerWidget *d) { // choose a day for which all times are likely to be valid for the default date in case we're only dealing with time guint year = 1970, month = 1, day = 1; guint hour = 0, minute = 0, second = 0; if (d->hasDate) { gtk_calendar_get_date(GTK_CALENDAR(d->calendar), &year, &month, &day); month++; // GtkCalendar/GDateTime differences } if (d->hasTime) { hour = realSpinValue(GTK_SPIN_BUTTON(d->hours)); if (realSpinValue(GTK_SPIN_BUTTON(d->ampm)) != 0) hour += 12; minute = realSpinValue(GTK_SPIN_BUTTON(d->minutes)); second = realSpinValue(GTK_SPIN_BUTTON(d->seconds)); } return g_date_time_new_local(year, month, day, hour, minute, second); } static void setLabel(uiprivDateTimePickerWidget *d) { GDateTime *dt; char *fmt; char *msg; gboolean free; dt = selected(d); free = FALSE; if (d->hasDate && d->hasTime) { // don't use D_T_FMT; that's too verbose fmt = g_strdup_printf("%s %s", nl_langinfo(D_FMT), nl_langinfo(T_FMT)); free = TRUE; } else if (d->hasDate) fmt = nl_langinfo(D_FMT); else fmt = nl_langinfo(T_FMT); msg = g_date_time_format(dt, fmt); gtk_button_set_label(GTK_BUTTON(d), msg); g_free(msg); if (free) g_free(fmt); g_date_time_unref(dt); } static int changedSignal; static void dateTimeChanged(uiprivDateTimePickerWidget *d) { g_signal_emit(d, changedSignal, 0); setLabel(d); // TODO fire event here instead? } // we don't want ::toggled to be sent again static void setActive(uiprivDateTimePickerWidget *d, gboolean active) { g_signal_handler_block(d, d->toggledSignal); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d), active); g_signal_handler_unblock(d, d->toggledSignal); } // like startGrab() below, a lot of this is in the order that GtkComboBox does it static void endGrab(uiprivDateTimePickerWidget *d) { if (d->keyboard != NULL) gdk_device_ungrab(d->keyboard, GDK_CURRENT_TIME); gdk_device_ungrab(d->mouse, GDK_CURRENT_TIME); gtk_device_grab_remove(d->window, d->mouse); d->keyboard = NULL; d->mouse = NULL; } static void hidePopup(uiprivDateTimePickerWidget *d) { endGrab(d); gtk_widget_hide(d->window); setActive(d, FALSE); } // this consolidates a good chunk of what GtkComboBox does static gboolean startGrab(uiprivDateTimePickerWidget *d) { GdkDevice *dev; guint32 time; GdkWindow *window; GdkDevice *keyboard, *mouse; dev = gtk_get_current_event_device(); if (dev == NULL) { // this is what GtkComboBox does // since no device was set, just use the first available "master device" GdkDisplay *disp; GdkDeviceManager *dm; GList *list; disp = gtk_widget_get_display(GTK_WIDGET(d)); dm = gdk_display_get_device_manager(disp); list = gdk_device_manager_list_devices(dm, GDK_DEVICE_TYPE_MASTER); dev = (GdkDevice *) (list->data); g_list_free(list); } time = gtk_get_current_event_time(); keyboard = dev; mouse = gdk_device_get_associated_device(dev); if (gdk_device_get_source(dev) != GDK_SOURCE_KEYBOARD) { dev = mouse; mouse = keyboard; keyboard = dev; } window = gtk_widget_get_window(d->window); if (keyboard != NULL) if (gdk_device_grab(keyboard, window, GDK_OWNERSHIP_WINDOW, TRUE, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, NULL, time) != GDK_GRAB_SUCCESS) return FALSE; if (mouse != NULL) if (gdk_device_grab(mouse, window, GDK_OWNERSHIP_WINDOW, TRUE, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK, NULL, time) != GDK_GRAB_SUCCESS) { if (keyboard != NULL) gdk_device_ungrab(keyboard, time); return FALSE; } gtk_device_grab_add(d->window, mouse, TRUE); d->keyboard = keyboard; d->mouse = mouse; return TRUE; } // based on gtk_combo_box_list_position() in the GTK+ source code static void allocationToScreen(uiprivDateTimePickerWidget *d, gint *x, gint *y) { GdkWindow *window; GtkAllocation a; GtkRequisition aWin; GdkScreen *screen; GdkRectangle workarea; int otherY; gtk_widget_get_allocation(GTK_WIDGET(d), &a); gtk_widget_get_preferred_size(d->window, &aWin, NULL); *x = 0; *y = 0; if (!gtk_widget_get_has_window(GTK_WIDGET(d))) { *x = a.x; *y = a.y; } window = gtk_widget_get_window(GTK_WIDGET(d)); gdk_window_get_root_coords(window, *x, *y, x, y); if (gtk_widget_get_direction(GTK_WIDGET(d)) == GTK_TEXT_DIR_RTL) *x += a.width - aWin.width; // now adjust to prevent the box from going offscreen screen = gtk_widget_get_screen(GTK_WIDGET(d)); gdk_screen_get_monitor_workarea(screen, gdk_screen_get_monitor_at_window(screen, window), &workarea); if (*x < workarea.x) // too far to the left? *x = workarea.x; else if (*x + aWin.width > (workarea.x + workarea.width)) // too far to the right? *x = (workarea.x + workarea.width) - aWin.width; // this isn't the same algorithm used by GtkComboBox // first, get our two choices; *y for down and otherY for up otherY = *y - aWin.height; *y += a.height; // and use otherY if we're too low if (*y + aWin.height >= workarea.y + workarea.height) *y = otherY; } static void showPopup(uiprivDateTimePickerWidget *d) { GtkWidget *toplevel; gint x, y; // GtkComboBox does it toplevel = gtk_widget_get_toplevel(GTK_WIDGET(d)); if (GTK_IS_WINDOW(toplevel)) gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(d->window)); allocationToScreen(d, &x, &y); gtk_window_move(GTK_WINDOW(d->window), x, y); gtk_widget_show(d->window); setActive(d, TRUE); if (!startGrab(d)) hidePopup(d); } static void onToggled(GtkToggleButton *b, gpointer data) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(b); if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d))) showPopup(d); else hidePopup(d); } static gboolean grabBroken(GtkWidget *w, GdkEventGrabBroken *e, gpointer data) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data); hidePopup(d); return TRUE; // this is what GtkComboBox does } static gboolean buttonReleased(GtkWidget *w, GdkEventButton *e, gpointer data) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data); int winx, winy; GtkAllocation wina; gboolean in; gtk_widget_get_allocation(d->window, &wina); winx = 0; winy = 0; if (!gtk_widget_get_has_window(d->window)) { winx = wina.x; winy = wina.y; } gdk_window_get_root_coords(gtk_widget_get_window(d->window), winx, winy, &winx, &winy); in = TRUE; if (e->x_root < winx) in = FALSE; if (e->x_root >= (winx + wina.width)) in = FALSE; if (e->y_root < winy) in = FALSE; if (e->y_root >= (winy + wina.height)) in = FALSE; if (!in) hidePopup(d); return TRUE; // this is what GtkComboBox does } static gint hoursSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data) { double *out = (double *) ptr; const gchar *text; int value; text = gtk_entry_get_text(GTK_ENTRY(sb)); value = (int) g_strtod(text, NULL); if (value < 0 || value > 12) return GTK_INPUT_ERROR; if (value == 12) // 12 to the user is 0 internally value = 0; *out = (double) value; return TRUE; } static gboolean hoursSpinboxOutput(GtkSpinButton *sb, gpointer data) { gchar *text; int value; value = realSpinValue(sb); if (value == 0) // 0 internally is 12 to the user value = 12; text = g_strdup_printf("%d", value); gtk_entry_set_text(GTK_ENTRY(sb), text); g_free(text); return TRUE; } static gboolean zeroPadSpinbox(GtkSpinButton *sb, gpointer data) { gchar *text; int value; value = realSpinValue(sb); text = g_strdup_printf("%02d", value); gtk_entry_set_text(GTK_ENTRY(sb), text); g_free(text); return TRUE; } // this is really hacky but we can't use GtkCombobox here :( static gint ampmSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data) { double *out = (double *) ptr; const gchar *text; char firstAM, firstPM; text = gtk_entry_get_text(GTK_ENTRY(sb)); // LONGTERM don't use ASCII here for case insensitivity firstAM = g_ascii_tolower(nl_langinfo(AM_STR)[0]); firstPM = g_ascii_tolower(nl_langinfo(PM_STR)[0]); for (; *text != '\0'; text++) if (g_ascii_tolower(*text) == firstAM) { *out = 0; return TRUE; } else if (g_ascii_tolower(*text) == firstPM) { *out = 1; return TRUE; } return GTK_INPUT_ERROR; } static gboolean ampmSpinboxOutput(GtkSpinButton *sb, gpointer data) { int value; value = gtk_spin_button_get_value_as_int(sb); if (value == 0) gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(AM_STR)); else gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(PM_STR)); return TRUE; } static void spinboxChanged(GtkSpinButton *sb, gpointer data) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data); dateTimeChanged(d); } static GtkWidget *newSpinbox(uiprivDateTimePickerWidget *d, int min, int max, gint (*input)(GtkSpinButton *, gpointer, gpointer), gboolean (*output)(GtkSpinButton *, gpointer), gulong *block) { GtkWidget *sb; sb = gtk_spin_button_new_with_range(min, max, 1); gtk_spin_button_set_digits(GTK_SPIN_BUTTON(sb), 0); gtk_spin_button_set_wrap(GTK_SPIN_BUTTON(sb), TRUE); gtk_orientable_set_orientation(GTK_ORIENTABLE(sb), GTK_ORIENTATION_VERTICAL); *block = g_signal_connect(sb, "value-changed", G_CALLBACK(spinboxChanged), d); if (input != NULL) g_signal_connect(sb, "input", G_CALLBACK(input), NULL); if (output != NULL) g_signal_connect(sb, "output", G_CALLBACK(output), NULL); return sb; } static void dateChanged(GtkCalendar *c, gpointer data) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(data); dateTimeChanged(d); } static void setDateOnly(uiprivDateTimePickerWidget *d) { d->hasTime = FALSE; gtk_container_remove(GTK_CONTAINER(d->box), d->timebox); } static void setTimeOnly(uiprivDateTimePickerWidget *d) { d->hasDate = FALSE; gtk_container_remove(GTK_CONTAINER(d->box), d->calendar); } static void uiprivDateTimePickerWidget_setTime(uiprivDateTimePickerWidget *d, GDateTime *dt) { gint year, month, day; gint hour; // notice how we block signals from firing if (d->hasDate) { g_date_time_get_ymd(dt, &year, &month, &day); month--; // GDateTime/GtkCalendar differences g_signal_handler_block(d->calendar, d->calendarBlock); gtk_calendar_select_month(GTK_CALENDAR(d->calendar), month, year); gtk_calendar_select_day(GTK_CALENDAR(d->calendar), day); g_signal_handler_unblock(d->calendar, d->calendarBlock); } if (d->hasTime) { hour = g_date_time_get_hour(dt); if (hour >= 12) { hour -= 12; setRealSpinValue(GTK_SPIN_BUTTON(d->ampm), 1, d->ampmBlock); } setRealSpinValue(GTK_SPIN_BUTTON(d->hours), hour, d->hoursBlock); setRealSpinValue(GTK_SPIN_BUTTON(d->minutes), g_date_time_get_minute(dt), d->minutesBlock); setRealSpinValue(GTK_SPIN_BUTTON(d->seconds), g_date_time_get_seconds(dt), d->secondsBlock); } g_date_time_unref(dt); } static void uiprivDateTimePickerWidget_init(uiprivDateTimePickerWidget *d) { d->window = gtk_window_new(GTK_WINDOW_POPUP); gtk_window_set_resizable(GTK_WINDOW(d->window), FALSE); gtk_window_set_attached_to(GTK_WINDOW(d->window), GTK_WIDGET(d)); gtk_window_set_decorated(GTK_WINDOW(d->window), FALSE); gtk_window_set_deletable(GTK_WINDOW(d->window), FALSE); gtk_window_set_type_hint(GTK_WINDOW(d->window), GDK_WINDOW_TYPE_HINT_COMBO); gtk_window_set_skip_taskbar_hint(GTK_WINDOW(d->window), TRUE); gtk_window_set_skip_pager_hint(GTK_WINDOW(d->window), TRUE); gtk_window_set_has_resize_grip(GTK_WINDOW(d->window), FALSE); gtk_container_set_border_width(GTK_CONTAINER(d->window), 12); // and make it stand out a bit gtk_style_context_add_class(gtk_widget_get_style_context(d->window), "frame"); d->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_container_add(GTK_CONTAINER(d->window), d->box); d->calendar = gtk_calendar_new(); d->calendarBlock = g_signal_connect(d->calendar, "day-selected", G_CALLBACK(dateChanged), d); gtk_container_add(GTK_CONTAINER(d->box), d->calendar); d->timebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_widget_set_valign(d->timebox, GTK_ALIGN_CENTER); gtk_container_add(GTK_CONTAINER(d->box), d->timebox); d->hours = newSpinbox(d, 0, 11, hoursSpinboxInput, hoursSpinboxOutput, &(d->hoursBlock)); gtk_container_add(GTK_CONTAINER(d->timebox), d->hours); gtk_container_add(GTK_CONTAINER(d->timebox), gtk_label_new(":")); d->minutes = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->minutesBlock)); gtk_container_add(GTK_CONTAINER(d->timebox), d->minutes); gtk_container_add(GTK_CONTAINER(d->timebox), gtk_label_new(":")); d->seconds = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->secondsBlock)); gtk_container_add(GTK_CONTAINER(d->timebox), d->seconds); // LONGTERM this should be the case, but that interferes with grabs // switch to it when we can drop GTK+ 3.10 and use popovers #if 0 d->ampm = gtk_combo_box_text_new(); gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "AM"); gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "PM"); #endif d->ampm = newSpinbox(d, 0, 1, ampmSpinboxInput, ampmSpinboxOutput, &(d->ampmBlock)); gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(d->ampm), FALSE); gtk_widget_set_valign(d->ampm, GTK_ALIGN_CENTER); gtk_container_add(GTK_CONTAINER(d->timebox), d->ampm); gtk_widget_show_all(d->box); g_signal_connect(d->window, "grab-broken-event", G_CALLBACK(grabBroken), d); g_signal_connect(d->window, "button-release-event", G_CALLBACK(buttonReleased), d); d->toggledSignal = g_signal_connect(d, "toggled", G_CALLBACK(onToggled), NULL); d->keyboard = NULL; d->mouse = NULL; d->hasTime = TRUE; d->hasDate = TRUE; // set the current date/time uiprivDateTimePickerWidget_setTime(d, g_date_time_new_now_local()); } static void uiprivDateTimePickerWidget_dispose(GObject *obj) { uiprivDateTimePickerWidget *d = uiprivDateTimePickerWidget(obj); if (d->window != NULL) { gtk_widget_destroy(d->window); d->window = NULL; } G_OBJECT_CLASS(uiprivDateTimePickerWidget_parent_class)->dispose(obj); } static void uiprivDateTimePickerWidget_finalize(GObject *obj) { G_OBJECT_CLASS(uiprivDateTimePickerWidget_parent_class)->finalize(obj); } static void uiprivDateTimePickerWidget_class_init(uiprivDateTimePickerWidgetClass *class) { G_OBJECT_CLASS(class)->dispose = uiprivDateTimePickerWidget_dispose; G_OBJECT_CLASS(class)->finalize = uiprivDateTimePickerWidget_finalize; changedSignal = g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } struct uiDateTimePicker { uiUnixControl c; GtkWidget *widget; uiprivDateTimePickerWidget *d; void (*onChanged)(uiDateTimePicker *, void *); void *onChangedData; gulong setBlock; }; uiUnixControlAllDefaults(uiDateTimePicker) static void defaultOnChanged(uiDateTimePicker *d, void *data) { // do nothing } void uiDateTimePickerTime(uiDateTimePicker *d, struct tm *time) { time_t t; struct tm tmbuf; GDateTime *dt; dt = selected(d->d); t = g_date_time_to_unix(dt); g_date_time_unref(dt); // Copy time to minimize a race condition // time.h functions use global non-thread-safe data tmbuf = *localtime(&t); memcpy(time, &tmbuf, sizeof (struct tm)); } void uiDateTimePickerSetTime(uiDateTimePicker *d, const struct tm *time) { time_t t; struct tm tmbuf; // TODO find a better way to avoid this; possibly by removing the signal entirely, or the call to dateTimeChanged() (most likely both) g_signal_handler_block(d->d, d->setBlock); // Copy time because mktime() modifies its argument memcpy(&tmbuf, time, sizeof (struct tm)); t = mktime(&tmbuf); uiprivDateTimePickerWidget_setTime(d->d, g_date_time_new_from_unix_local(t)); dateTimeChanged(d->d); g_signal_handler_unblock(d->d, d->setBlock); } void uiDateTimePickerOnChanged(uiDateTimePicker *d, void (*f)(uiDateTimePicker *, void *), void *data) { d->onChanged = f; d->onChangedData = data; } static void onChanged(uiprivDateTimePickerWidget *d, gpointer data) { uiDateTimePicker *c; c = uiDateTimePicker(data); (*(c->onChanged))(c, c->onChangedData); } static GtkWidget *newDTP(void) { GtkWidget *w; w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL)); setLabel(uiprivDateTimePickerWidget(w)); return w; } static GtkWidget *newDP(void) { GtkWidget *w; w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL)); setDateOnly(uiprivDateTimePickerWidget(w)); setLabel(uiprivDateTimePickerWidget(w)); return w; } static GtkWidget *newTP(void) { GtkWidget *w; w = GTK_WIDGET(g_object_new(uiprivDateTimePickerWidgetType, "label", "", NULL)); setTimeOnly(uiprivDateTimePickerWidget(w)); setLabel(uiprivDateTimePickerWidget(w)); return w; } uiDateTimePicker *finishNewDateTimePicker(GtkWidget *(*fn)(void)) { uiDateTimePicker *d; uiUnixNewControl(uiDateTimePicker, d); d->widget = (*fn)(); d->d = uiprivDateTimePickerWidget(d->widget); d->setBlock = g_signal_connect(d->widget, "changed", G_CALLBACK(onChanged), d); uiDateTimePickerOnChanged(d, defaultOnChanged, NULL); return d; } uiDateTimePicker *uiNewDateTimePicker(void) { return finishNewDateTimePicker(newDTP); } uiDateTimePicker *uiNewDatePicker(void) { return finishNewDateTimePicker(newDP); } uiDateTimePicker *uiNewTimePicker(void) { return finishNewDateTimePicker(newTP); }