[california/wip/725783-time] Broke out date/time widgets, cleaned up dialog, getting close



commit 55cba366b5d1ad8e3e88d2033681226c18dc0e42
Author: Jim Nelson <jim yorba org>
Date:   Tue Jul 29 19:00:38 2014 -0700

    Broke out date/time widgets, cleaned up dialog, getting close

 src/Makefile.am                                 |    4 +-
 src/base/base-object.vala                       |   10 +-
 src/calendar/calendar-date-span.vala            |   24 ++
 src/calendar/calendar-exact-time-span.vala      |   60 ++++++
 src/california-resources.xml                    |    3 +
 src/component/component-event.vala              |   46 +----
 src/host/host-create-update-recurring.vala      |   31 +---
 src/host/host-date-time-widget.vala             |  229 ++++++++++++++++++++
 src/host/host-event-time-settings.vala          |  194 +++++------------
 src/host/host-quick-create-event.vala           |    3 +-
 src/host/host-show-event.vala                   |    3 +-
 src/rc/create-update-recurring.ui               |    2 -
 src/rc/date-time-widget.ui                      |  250 ++++++++++++++++++++++
 src/rc/event-time-settings.ui                   |  260 +----------------------
 src/toolkit/toolkit-entry-filter-connector.vala |   87 ++++++++
 15 files changed, 736 insertions(+), 470 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index dd14329..95d1833 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -93,6 +93,7 @@ california_VALASOURCES = \
        host/host-calendar-list-item.vala \
        host/host-create-update-event.vala \
        host/host-create-update-recurring.vala \
+       host/host-date-time-widget.vala \
        host/host-event-time-settings.vala \
        host/host-import-calendar.vala \
        host/host-main-window.vala \
@@ -122,9 +123,9 @@ california_VALASOURCES = \
        toolkit/toolkit-combo-box-text-model.vala \
        toolkit/toolkit-deck.vala \
        toolkit/toolkit-deck-window.vala \
-       toolkit/toolkit-editable-filter.vala \
        toolkit/toolkit-editable-label.vala \
        toolkit/toolkit-entry-clear-text-connector.vala \
+       toolkit/toolkit-entry-filter-connector.vala \
        toolkit/toolkit-event-connector.vala \
        toolkit/toolkit-listbox-model.vala \
        toolkit/toolkit-motion-connector.vala \
@@ -183,6 +184,7 @@ california_RC = \
        rc/calendar-manager-list-item.ui \
        rc/create-update-event.ui \
        rc/create-update-recurring.ui \
+       rc/date-time-widget.ui \
        rc/event-time-settings.ui \
        rc/google-authenticating.ui \
        rc/google-calendar-list.ui \
diff --git a/src/base/base-object.vala b/src/base/base-object.vala
index 97779dd..b4a391e 100644
--- a/src/base/base-object.vala
+++ b/src/base/base-object.vala
@@ -15,7 +15,15 @@ namespace California {
  */
 
 public abstract class BaseObject : Object {
-    public BaseObject() {
+    /**
+     * Returns the base class name as a string.
+     *
+     * This can be used as a dummy to_string() for { link BaseObject}s that don't carry state to
+     * report.
+     */
+    public string classname { get { return get_type().name(); } }
+    
+    protected BaseObject() {
     }
     
     /**
diff --git a/src/calendar/calendar-date-span.vala b/src/calendar/calendar-date-span.vala
index ac13827..f46f3b8 100644
--- a/src/calendar/calendar-date-span.vala
+++ b/src/calendar/calendar-date-span.vala
@@ -73,6 +73,30 @@ public class DateSpan : UnitSpan<Date> {
     }
     
     /**
+     * Returns a prettified string describing the { link Event}'s time span in as concise and
+     * economical manner possible.
+     *
+     * The supplied { link Date} pretty flags are applied to the two Date strings.  If either of
+     * the { link DateSpan} crosses a year boundary, the INCLUDE_YEAR flag is automatically added.
+     */
+    public string to_pretty_string(Calendar.Date.PrettyFlag date_flags) {
+        if (!start_date.year.equal_to(Calendar.System.today.year)
+            || !end_date.year.equal_to(Calendar.System.today.year)) {
+            date_flags |= Calendar.Date.PrettyFlag.INCLUDE_YEAR;
+        }
+        
+        if (is_same_day) {
+            // One-day event, print that date's "<full date>", including year if not
+            // current year
+            return start_date.to_pretty_string(date_flags);
+        }
+        
+        // Prints a span of dates, i.e. "Monday, January 3 to Thursday, January 6"
+        return _("%s to %s").printf(start_date.to_pretty_string(date_flags),
+            end_date.to_pretty_string(date_flags));
+    }
+    
+    /**
      * @inheritDoc
      */
     public override bool contains(Date date) {
diff --git a/src/calendar/calendar-exact-time-span.vala b/src/calendar/calendar-exact-time-span.vala
index 60aa3bd..d4632af 100644
--- a/src/calendar/calendar-exact-time-span.vala
+++ b/src/calendar/calendar-exact-time-span.vala
@@ -17,6 +17,18 @@ namespace California.Calendar {
 
 public class ExactTimeSpan : BaseObject, Gee.Comparable<ExactTimeSpan>, Gee.Hashable<ExactTimeSpan> {
     /**
+     * Pretty-printing flags for { link to_pretty_string}.
+     */
+    [Flags]
+    public enum PrettyFlag {
+        NONE = 0,
+        /**
+         * Use multiple lines to format string if lengthy.
+         */
+        ALLOW_MULTILINE
+    }
+    
+    /**
      * Starting { link ExactTime} of the span.
      *
      * start_exact_time will always be earlier to or equal to { link end_exact_time}.
@@ -96,6 +108,54 @@ public class ExactTimeSpan : BaseObject, Gee.Comparable<ExactTimeSpan>, Gee.Hash
     }
     
     /**
+     * Returns a prettified string describing the { link Event}'s time span in as concise and
+     * economical manner possible.
+     *
+     * The supplied { link Date} pretty flags are applied to the two Date strings.  If either of
+     * the { link DateSpan} crosses a year boundary, the INCLUDE_YEAR flag is automatically added.
+     */
+    public string to_pretty_string(Calendar.Date.PrettyFlag date_flags, PrettyFlag time_flags) {
+        bool allow_multiline = (time_flags & PrettyFlag.ALLOW_MULTILINE) != 0;
+        
+        if (!start_date.year.equal_to(Calendar.System.today.year)
+            || !end_date.year.equal_to(Calendar.System.today.year)) {
+            date_flags |= Calendar.Date.PrettyFlag.INCLUDE_YEAR;
+        }
+        
+        if (is_same_day) {
+            // A span of time, i.e. "3:30pm to 4:30pm"
+            string timespan = _("%s to %s").printf(
+                start_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE),
+                end_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE));
+            
+            // Single-day timed event, print "<full date>, <full start time> to <full end time>",
+            // including year if not current year
+            return "%s, %s".printf(start_date.to_pretty_string(date_flags), timespan);
+        }
+        
+        if (allow_multiline) {
+            // Multi-day timed event, print "<full time>, <full date>" on both lines,
+            // including year if either not current year
+            // Prints two full time and date strings on separate lines, i.e.:
+            // 12 January 2012, 3:30pm
+            // 13 January 2013, 6:30am
+            return _("%s, %s\n%s, %s").printf(
+                start_exact_time.to_pretty_date_string(date_flags),
+                start_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE),
+                end_exact_time.to_pretty_date_string(date_flags),
+                end_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE));
+        }
+        
+        // Prints full time and date strings on a single line, i.e.:
+        // 12 January 2012, 3:30pm to 13 January 2013, 6:30am
+        return _("%s, %s to %s, %s").printf(
+                start_exact_time.to_pretty_date_string(date_flags),
+                start_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE),
+                end_exact_time.to_pretty_date_string(date_flags),
+                end_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE));
+    }
+    
+    /**
      * Compares the { link start_exact_time} of two { link ExactTimeSpan}s.
      */
     public int compare_to(ExactTimeSpan other) {
diff --git a/src/california-resources.xml b/src/california-resources.xml
index 7aac897..6d65154 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -25,6 +25,9 @@
         <file compressed="false">rc/create-update-recurring.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
+        <file compressed="false">rc/date-time-widget.ui</file>
+    </gresource>
+    <gresource prefix="/org/yorba/california">
         <file compressed="false">rc/event-time-settings.ui</file>
     </gresource>
     <gresource prefix="/org/yorba/california">
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index b4c2d34..1a0e224 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -350,57 +350,23 @@ public class Event : Instance, Gee.Comparable<Event> {
      *
      * @return null if no time/date information is specified
      */
-    public string? get_event_time_pretty_string(Calendar.Timezone timezone) {
+    public string? get_event_time_pretty_string(Calendar.Date.PrettyFlag date_flags,
+        Calendar.ExactTimeSpan.PrettyFlag time_flags, Calendar.Timezone timezone) {
         if (date_span == null && exact_time_span == null)
             return null;
         
         // if any dates are not in current year, display year in all dates
-        Calendar.Date.PrettyFlag date_flags = Calendar.Date.PrettyFlag.NONE;
         Calendar.DateSpan date_span = get_event_date_span(timezone);
         if (!date_span.start_date.year.equal_to(Calendar.System.today.year)
             || !date_span.end_date.year.equal_to(Calendar.System.today.year)) {
             date_flags |= Calendar.Date.PrettyFlag.INCLUDE_YEAR;
         }
         
-        // span string is kinda tricky
-        string span;
-        if (is_all_day) {
-            if (date_span.is_same_day) {
-                // All-day one-day event, print that date's "<full date>", including year if not
-                // current year
-                span = date_span.start_date.to_pretty_string(date_flags);
-            } else {
-                // Prints a span of dates, i.e. "Monday, January 3 to Thursday, January 6"
-                span = _("%s to %s").printf(date_span.start_date.to_pretty_string(date_flags),
-                    date_span.end_date.to_pretty_string(date_flags));
-            }
-        } else {
-            Calendar.ExactTimeSpan exact_time_span = exact_time_span.to_timezone(timezone);
-            if (exact_time_span.is_same_day) {
-                // A span of time, i.e. "3:30pm to 4:30pm"
-                string timespan = _("%s to %s").printf(
-                    
exact_time_span.start_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE),
-                    exact_time_span.end_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE));
-                
-                // Single-day timed event, print "<full date>, <full start time> to <full end time>",
-                // including year if not current year
-                span = "%s, %s".printf(exact_time_span.start_date.to_pretty_string(date_flags),
-                    timespan);
-            } else {
-                // Multi-day timed event, print "<full time>, <full date>" on both lines,
-                // including year if either not current year
-                // Prints two full time and date strings on separate lines, i.e.:
-                // 12 January 2012, 3:30pm
-                // 13 January 2013, 6:30am
-                span = _("%s, %s\n%s, %s").printf(
-                    exact_time_span.start_exact_time.to_pretty_date_string(date_flags),
-                    
exact_time_span.start_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE),
-                    exact_time_span.end_exact_time.to_pretty_date_string(date_flags),
-                    exact_time_span.end_exact_time.to_pretty_time_string(Calendar.WallTime.PrettyFlag.NONE));
-            }
-        }
+        // if all day, just use the DateSpan's pretty string
+        if (is_all_day)
+            return date_span.to_pretty_string(date_flags);
         
-        return span;
+        return exact_time_span.to_timezone(timezone).to_pretty_string(date_flags, time_flags);
     }
     
     /**
diff --git a/src/host/host-create-update-recurring.vala b/src/host/host-create-update-recurring.vala
index ce6c4eb..8d9fb7f 100644
--- a/src/host/host-create-update-recurring.vala
+++ b/src/host/host-create-update-recurring.vala
@@ -107,7 +107,7 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     private Component.Event? master = null;
     private Gee.HashMap<Calendar.DayOfWeek, Gtk.CheckButton> on_day_checkbuttons = new Gee.HashMap<
         Calendar.DayOfWeek, Gtk.CheckButton>();
-    private bool blocking_insert_text_numbers_only_signal = false;
+    private Toolkit.EntryFilterConnector numeric_filter = new Toolkit.EntryFilterConnector.only_numeric();
     
     public CreateUpdateRecurring() {
         // "Repeating event" checkbox activates almost every other control in this dialog
@@ -141,6 +141,9 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
         on_day_checkbuttons[Calendar.DayOfWeek.FRI] = friday_checkbutton;
         on_day_checkbuttons[Calendar.DayOfWeek.SAT] = saturday_checkbutton;
         
+        numeric_filter.connect_to(every_entry);
+        numeric_filter.connect_to(after_entry);
+        
         // Ok button's sensitivity is tied to a whole-lotta controls here
         make_recurring_checkbutton.bind_property("active", ok_button, "sensitive",
             BindingFlags.SYNC_CREATE, transform_to_ok_button_sensitive);
@@ -445,32 +448,6 @@ public class CreateUpdateRecurring : Gtk.Grid, Toolkit.Card {
     }
     
     [GtkCallback]
-    private void on_insert_text_numbers_only(Gtk.Editable editable, string new_text, int new_text_length,
-        ref int position) {
-        // prevent recursion when our modified text is inserted (i.e. allow the base handler to
-        // deal new text directly)
-        if (blocking_insert_text_numbers_only_signal)
-            return;
-        
-        // filter out everything not a number
-        string numbers_only = from_string(new_text)
-            .filter(ch => ch.isdigit())
-            .to_string(ch => ch.to_string());
-        
-        // insert new text into place, ensure this handler doesn't attempt to process this
-        // modified text ... would use SignalHandler.block_by_func() and unblock_by_func(), but
-        // the bindings are ungood
-        if (!String.is_empty(numbers_only)) {
-            blocking_insert_text_numbers_only_signal = true;
-            editable.insert_text(numbers_only, numbers_only.length, ref position);
-            blocking_insert_text_numbers_only_signal = false;
-        }
-        
-        // don't let the base handler have at the original text
-        Signal.stop_emission_by_name(editable, "insert-text");
-    }
-    
-    [GtkCallback]
     private void on_cancel_button_clicked() {
         jump_back();
     }
diff --git a/src/host/host-date-time-widget.vala b/src/host/host-date-time-widget.vala
new file mode 100644
index 0000000..c14475b
--- /dev/null
+++ b/src/host/host-date-time-widget.vala
@@ -0,0 +1,229 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Host {
+
+[GtkTemplate (ui = "/org/yorba/california/rc/date-time-widget.ui")]
+public class DateTimeWidget : Gtk.Box {
+    public const string PROP_ENABLE_TIME = "enable-time";
+    public const string PROP_ENABLE_DATE = "enable-date";
+    public const string PROP_DATE = "date";
+    public const string PROP_WALL_TIME = "wall-time";
+    
+    public bool enable_time { get; set; default = true; }
+    
+    public bool enable_date { get; set; default = true; }
+    
+    public Calendar.Date date { get; set; default = Calendar.System.today; }
+    
+    public Calendar.WallTime wall_time { get; set; default = Calendar.System.now.to_wall_time(); }
+    
+    [GtkChild]
+    private Gtk.Calendar calendar;
+    
+    [GtkChild]
+    private Gtk.Entry hour_entry;
+    
+    [GtkChild]
+    private Gtk.Label colon_label;
+    
+    [GtkChild]
+    private Gtk.Entry minutes_entry;
+    
+    [GtkChild]
+    private Gtk.Label meridiem_label;
+    
+    [GtkChild]
+    private Gtk.EventBox hour_up;
+    
+    [GtkChild]
+    private Gtk.EventBox hour_down;
+    
+    [GtkChild]
+    private Gtk.EventBox minutes_up;
+    
+    [GtkChild]
+    private Gtk.EventBox minutes_down;
+    
+    [GtkChild]
+    private Gtk.EventBox meridiem_up;
+    
+    [GtkChild]
+    private Gtk.EventBox meridiem_down;
+    
+    private Toolkit.ButtonConnector button_connector = new Toolkit.ButtonConnector();
+    private Toolkit.EntryFilterConnector numeric_filter = new Toolkit.EntryFilterConnector.only_numeric();
+    
+    public DateTimeWidget() {
+        button_connector.connect_to(hour_up);
+        button_connector.connect_to(hour_down);
+        button_connector.connect_to(minutes_up);
+        button_connector.connect_to(minutes_down);
+        button_connector.connect_to(meridiem_up);
+        button_connector.connect_to(meridiem_down);
+        
+        numeric_filter.connect_to(hour_entry);
+        numeric_filter.connect_to(minutes_entry);
+        
+        // use signal handlers to initialize widgets
+        on_date_changed();
+        on_wall_time_changed();
+        
+        connect_property_signals();
+        connect_widget_signals();
+        
+        // specifically-enabled sensitivities
+        bind_bool_to_time_controls(PROP_ENABLE_TIME, iterate<Gtk.Widget>(
+            hour_up, hour_down, minutes_up, minutes_down, meridiem_up, meridiem_down,
+            hour_entry, colon_label, minutes_entry, meridiem_label));
+        
+        bind_bool_to_time_controls(PROP_ENABLE_DATE, iterate<Gtk.Widget>(calendar));
+    }
+    
+    private void bind_bool_to_time_controls(string property, California.Iterable<Gtk.Widget> time_widgets) {
+        foreach (Gtk.Widget time_widget in time_widgets)
+            bind_property(property, time_widget, "sensitive", BindingFlags.SYNC_CREATE);
+    }
+    
+    private void connect_property_signals() {
+        notify[PROP_DATE].connect(on_date_changed);
+        notify[PROP_WALL_TIME].connect(on_wall_time_changed);
+    }
+    
+    private void disconnect_property_signals() {
+        notify[PROP_DATE].disconnect(on_date_changed);
+        notify[PROP_WALL_TIME].disconnect(on_wall_time_changed);
+    }
+    
+    private void connect_widget_signals() {
+        button_connector.clicked.connect(on_time_adjustment_clicked);
+        
+        calendar.day_selected.connect(on_calendar_day_selected);
+        calendar.month_changed.connect(on_calendar_month_or_year_changed);
+        calendar.next_year.connect(on_calendar_month_or_year_changed);
+        calendar.prev_year.connect(on_calendar_month_or_year_changed);
+    }
+    
+    private void disconnect_widget_signals() {
+        button_connector.clicked.disconnect(on_time_adjustment_clicked);
+        
+        calendar.day_selected.disconnect(on_calendar_day_selected);
+        calendar.month_changed.disconnect(on_calendar_month_or_year_changed);
+        calendar.next_year.disconnect(on_calendar_month_or_year_changed);
+        calendar.prev_year.disconnect(on_calendar_month_or_year_changed);
+    }
+    
+    private bool on_time_adjustment_clicked(Toolkit.ButtonEvent details) {
+        if (details.button != Toolkit.Button.PRIMARY)
+            return Toolkit.PROPAGATE;
+        
+        int amount;
+        Calendar.TimeUnit time_unit;
+        if (!adjust_time_controls(details, out amount, out time_unit))
+            return Toolkit.PROPAGATE;
+        
+        // this will update the entry fields, so don't disconnect widget signals
+        wall_time = wall_time.adjust(amount, time_unit, null);
+        
+        return Toolkit.STOP;
+    }
+    
+    private bool adjust_time_controls(Toolkit.ButtonEvent details, out int amount, out Calendar.TimeUnit 
time_unit) {
+        if (details.widget == hour_up) {
+            amount = 1;
+            time_unit = Calendar.TimeUnit.HOUR;
+        } else if (details.widget == hour_down) {
+            amount = -1;
+            time_unit = Calendar.TimeUnit.HOUR;
+        } else if (details.widget == minutes_up) {
+            amount = 1;
+            time_unit = Calendar.TimeUnit.MINUTE;
+        } else if (details.widget == minutes_down) {
+            amount = -1;
+            time_unit = Calendar.TimeUnit.MINUTE;
+        } else if (details.widget == meridiem_up) {
+            amount = 12;
+            time_unit = Calendar.TimeUnit.HOUR;
+        } else if (details.widget == meridiem_down) {
+            amount = -12;
+            time_unit = Calendar.TimeUnit.HOUR;
+        } else {
+            amount = 0;
+            time_unit = Calendar.TimeUnit.HOUR;
+            
+            return false;
+        }
+        
+        return true;
+    }
+    
+    private Calendar.Date? get_selected_date() {
+        if (calendar.day == 0)
+            return null;
+        
+        try {
+            return new Calendar.Date(
+                Calendar.DayOfMonth.for(calendar.day),
+                Calendar.Month.for(calendar.month + 1),
+                new Calendar.Year(calendar.year)
+            );
+        } catch (CalendarError calerr) {
+            debug("Unable to generate date from Gtk.Calendar: %s", calerr.message);
+            
+            return null;
+        }
+    }
+    
+    private void on_calendar_day_selected() {
+        disconnect_property_signals();
+        
+        Calendar.Date? selected = get_selected_date();
+        if (selected != null && !selected.equal_to(date))
+            date = selected;
+        
+        connect_property_signals();
+    }
+    
+    private void on_calendar_month_or_year_changed() {
+        // If selected month/year is not for the current date, don't select the day of that month/year
+        // ... if selected month/year is for the current date, ensure that the day is selected ...
+        // and as a fallback, don't select the day of the month/year
+        Calendar.Date? selected = get_selected_date();
+        if (selected != null) {
+            if (selected.month_of_year().equal_to(date.month_of_year()))
+                calendar.day = date.day_of_month.value;
+            else
+                calendar.day = 0;
+        } else if (date.month.value == (calendar.month + 1) && date.year.value == calendar.year) {
+            calendar.day = date.day_of_month.value;
+        } else {
+            calendar.day = 0;
+        }
+    }
+    
+    private void on_date_changed() {
+        disconnect_widget_signals();
+        
+        calendar.day = date.day_of_month.value;
+        calendar.month = date.month.value - 1;
+        calendar.year = date.year.value;
+        
+        connect_widget_signals();
+    }
+    
+    private void on_wall_time_changed() {
+        disconnect_widget_signals();
+        
+        hour_entry.text = "%d".printf(wall_time.12hour);
+        minutes_entry.text = "%02d".printf(wall_time.minute);
+        meridiem_label.label = wall_time.is_pm ? Calendar.FMT_PM : Calendar.FMT_AM;
+        
+        connect_widget_signals();
+    }
+}
+
+}
+
diff --git a/src/host/host-event-time-settings.vala b/src/host/host-event-time-settings.vala
index f6e0e72..2e9da3b 100644
--- a/src/host/host-event-time-settings.vala
+++ b/src/host/host-event-time-settings.vala
@@ -10,53 +10,14 @@ namespace California.Host {
 public class EventTimeSettings : Gtk.Box, Toolkit.Card {
     public const string ID = "CaliforniaHostEventTimeSettings";
     
-    private class TimeControls {
-        public Gtk.EventBox hour_up;
-        public Gtk.EventBox hour_down;
-        public Gtk.EventBox minutes_up;
-        public Gtk.EventBox minutes_down;
-        public Gtk.EventBox meridiem_up;
-        public Gtk.EventBox meridiem_down;
-        public Gtk.Entry hour_entry;
-        public Gtk.Entry minutes_entry;
-        public Gtk.Label meridiem_label;
-    }
-    
-    [GtkChild]
-    private Gtk.Calendar from_calendar;
-    
-    [GtkChild]
-    private Gtk.Calendar to_calendar;
-    
-    [GtkChild]
-    private Gtk.Entry from_hour_entry;
-    
-    [GtkChild]
-    private Gtk.Label from_colon_label;
-    
-    [GtkChild]
-    private Gtk.Entry from_minutes_entry;
-    
     [GtkChild]
-    private Gtk.Label from_meridiem_label;
+    private Gtk.Label summary_label;
     
     [GtkChild]
-    private Gtk.EventBox from_hour_up;
+    private Gtk.Box from_box;
     
     [GtkChild]
-    private Gtk.EventBox from_hour_down;
-    
-    [GtkChild]
-    private Gtk.EventBox from_minutes_up;
-    
-    [GtkChild]
-    private Gtk.EventBox from_minutes_down;
-    
-    [GtkChild]
-    private Gtk.EventBox from_meridiem_up;
-    
-    [GtkChild]
-    private Gtk.EventBox from_meridiem_down;
+    private Gtk.Box to_box;
     
     [GtkChild]
     private Gtk.CheckButton all_day_checkbutton;
@@ -67,129 +28,80 @@ public class EventTimeSettings : Gtk.Box, Toolkit.Card {
     public Gtk.Widget? initial_focus { get { return null; } }
     
     private new Component.Event? event = null;
-    private Calendar.WallTime? start_time = null;
-    private TimeControls from_controls = new TimeControls();
-    public Toolkit.ButtonConnector button_connector = new Toolkit.ButtonConnector();
+    private DateTimeWidget from_widget = new DateTimeWidget();
+    private DateTimeWidget to_widget = new DateTimeWidget();
     
     public EventTimeSettings() {
-        from_controls.hour_up = from_hour_up;
-        from_controls.hour_down = from_hour_down;
-        from_controls.minutes_up = from_minutes_up;
-        from_controls.minutes_down = from_minutes_down;
-        from_controls.meridiem_up = from_meridiem_up;
-        from_controls.meridiem_down = from_meridiem_down;
-        from_controls.hour_entry = from_hour_entry;
-        from_controls.minutes_entry = from_minutes_entry;
-        from_controls.meridiem_label = from_meridiem_label;
-        
-        button_connector.connect_to(from_hour_up);
-        button_connector.connect_to(from_hour_down);
-        button_connector.connect_to(from_minutes_up);
-        button_connector.connect_to(from_minutes_down);
-        button_connector.connect_to(from_meridiem_up);
-        button_connector.connect_to(from_meridiem_down);
+        // need to manually pack the date/time widgets
+        from_box.pack_start(from_widget);
+        to_box.pack_start(to_widget);
         
-        button_connector.clicked.connect(on_time_adjustment_clicked);
+        from_widget.notify[DateTimeWidget.PROP_DATE].connect(on_update_summary);
+        from_widget.notify[DateTimeWidget.PROP_WALL_TIME].connect(on_update_summary);
+        to_widget.notify[DateTimeWidget.PROP_DATE].connect(on_update_summary);
+        to_widget.notify[DateTimeWidget.PROP_WALL_TIME].connect(on_update_summary);
+        all_day_checkbutton.notify["active"].connect(on_update_summary);
         
-        bind_all_day_to_time_controls(iterate<Gtk.Widget>(
-            from_hour_up, from_hour_down,
-            from_minutes_up, from_minutes_down,
-            from_meridiem_up, from_meridiem_down,
-            from_hour_entry, from_colon_label, from_minutes_entry, from_meridiem_label).to_array_list());
-    }
-    
-    private void bind_all_day_to_time_controls(Gee.List<Gtk.Widget> time_widgets) {
-        foreach (Gtk.Widget time_widget in time_widgets) {
-            all_day_checkbutton.bind_property("active", time_widget, "sensitive",
-                BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
-        }
+        all_day_checkbutton.bind_property("active", from_widget, DateTimeWidget.PROP_ENABLE_TIME,
+            BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
+        all_day_checkbutton.bind_property("active", to_widget, DateTimeWidget.PROP_ENABLE_TIME,
+            BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
     }
     
     public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
         event = (Component.Event) message;
         
-        start_time = event.is_all_day
-            ? Calendar.System.now.to_wall_time()
-            : event.exact_time_span.start_exact_time.to_wall_time();
+        // only set wall time if not all day; let old wall times float so user can return to them
+        // later while Deck is active
+        if (!event.is_all_day) {
+            Calendar.ExactTimeSpan time_span = event.exact_time_span.to_timezone(Calendar.Timezone.local);
+            from_widget.wall_time = time_span.start_exact_time.to_wall_time();
+            to_widget.wall_time = time_span.end_exact_time.to_wall_time();
+        }
         
-        init_controls();
-    }
-    
-    private void init_controls() {
         Calendar.DateSpan event_span = event.get_event_date_span(Calendar.Timezone.local);
-        init_calendar(from_calendar, event_span.start_date);
-        init_calendar(to_calendar, event_span.end_date);
+        from_widget.date = event_span.start_date;
+        to_widget.date = event_span.end_date;
         
         all_day_checkbutton.active = event.is_all_day;
-        init_time_controls(from_controls, start_time);
-    }
-    
-    private void init_calendar(Gtk.Calendar calendar, Calendar.Date date) {
-        calendar.day = date.day_of_month.value;
-        calendar.month = date.month.value - 1;
-        calendar.year = date.year.value;
     }
     
-    private void init_time_controls(TimeControls controls, Calendar.WallTime wall_time) {
-        controls.hour_entry.text = "%d".printf(wall_time.12hour);
-        controls.minutes_entry.text = "%02d".printf(wall_time.minute);
-        controls.meridiem_label.label = wall_time.is_pm ? Calendar.FMT_PM : Calendar.FMT_AM;
+    [GtkCallback]
+    private void on_cancel_button_clicked() {
+        jump_back();
     }
     
-    private bool on_time_adjustment_clicked(Toolkit.ButtonEvent details) {
-        // ignored late "guaranteed" clicks and clicks from anything but primary button
-        if (details.button != Toolkit.Button.PRIMARY)
-            return Toolkit.PROPAGATE;
-        
-        int amount;
-        Calendar.TimeUnit time_unit;
-        if (!adjust_time_controls(from_controls, details, out amount, out time_unit))
-            return Toolkit.PROPAGATE;
-        
-        start_time = start_time.adjust(amount, time_unit, null);
-        init_time_controls(from_controls, start_time);
+    [GtkCallback]
+    private void on_ok_button_clicked() {
+        if (all_day_checkbutton.active)
+            event.set_event_date_span(get_date_span());
+        else
+            event.set_event_exact_time_span(get_exact_time_span());
         
-        return Toolkit.STOP;
+        jump_to_card_by_name(CreateUpdateEvent.ID, event);
     }
     
-    private bool adjust_time_controls(TimeControls controls, Toolkit.ButtonEvent details, out int amount,
-        out Calendar.TimeUnit time_unit) {
-        if (details.widget == controls.hour_up) {
-            amount = 1;
-            time_unit = Calendar.TimeUnit.HOUR;
-        } else if (details.widget == controls.hour_down) {
-            amount = -1;
-            time_unit = Calendar.TimeUnit.HOUR;
-        } else if (details.widget == controls.minutes_up) {
-            amount = 1;
-            time_unit = Calendar.TimeUnit.MINUTE;
-        } else if (details.widget == controls.minutes_down) {
-            amount = -1;
-            time_unit = Calendar.TimeUnit.MINUTE;
-        } else if (details.widget == controls.meridiem_up) {
-            amount = 12;
-            time_unit = Calendar.TimeUnit.HOUR;
-        } else if (details.widget == controls.meridiem_down) {
-            amount = -12;
-            time_unit = Calendar.TimeUnit.HOUR;
-        } else {
-            amount = 0;
-            time_unit = Calendar.TimeUnit.HOUR;
-            
-            return false;
-        }
-        
-        return true;
+    // This does not respect the all-day checkbox
+    private Calendar.DateSpan get_date_span() {
+        return new Calendar.DateSpan(from_widget.date, to_widget.date);
     }
     
-    [GtkCallback]
-    private void on_cancel_button_clicked() {
-        jump_back();
+    // This does not respect the all-day checkbox
+    private Calendar.ExactTimeSpan get_exact_time_span() {
+        return new Calendar.ExactTimeSpan(
+            new Calendar.ExactTime(Calendar.System.timezone, from_widget.date, from_widget.wall_time),
+            new Calendar.ExactTime(Calendar.System.timezone, to_widget.date, to_widget.wall_time)
+        );
     }
     
-    [GtkCallback]
-    private void on_ok_button_clicked() {
-        jump_back();
+    private void on_update_summary() {
+        Calendar.Date.PrettyFlag date_flags = Calendar.Date.PrettyFlag.NONE;
+        Calendar.ExactTimeSpan.PrettyFlag time_flags = Calendar.ExactTimeSpan.PrettyFlag.NONE;
+        
+        if (all_day_checkbutton.active)
+            summary_label.label = get_date_span().to_pretty_string(date_flags);
+        else
+            summary_label.label = get_exact_time_span().to_pretty_string(date_flags, time_flags);
     }
 }
 
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index 7fc1dad..33195b0 100644
--- a/src/host/host-quick-create-event.vala
+++ b/src/host/host-quick-create-event.vala
@@ -69,7 +69,8 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
         string eg;
         if (event != null && (event.date_span != null || event.exact_time_span != null)) {
             when_box.visible = true;
-            when_text_label.label = event.get_event_time_pretty_string(Calendar.Timezone.local);
+            when_text_label.label = event.get_event_time_pretty_string(Calendar.Date.PrettyFlag.NONE,
+                Calendar.ExactTimeSpan.PrettyFlag.ALLOW_MULTILINE, Calendar.Timezone.local);
             if (event.date_span != null)
                 eg = _("Example: Dinner at Tadich Grill 7:30pm");
             else
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index 663130a..ea4d5be 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -118,7 +118,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         set_label(where_label, where_text, event.location);
         
         // time
-        set_label(when_label, when_text, event.get_event_time_pretty_string(Calendar.Timezone.local));
+        set_label(when_label, when_text, event.get_event_time_pretty_string(Calendar.Date.PrettyFlag.NONE,
+            Calendar.ExactTimeSpan.PrettyFlag.NONE, Calendar.Timezone.local));
         
         // description
         set_label(null, description_text, Markup.linkify(escape(event.description), linkify_delegate));
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index 205cef3..dccd51d 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -369,7 +369,6 @@
                     <property name="width_chars">5</property>
                     <property name="input_purpose">number</property>
                     <signal name="changed" handler="on_after_entry_changed" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
-                    <signal name="insert-text" handler="on_insert_text_numbers_only" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -434,7 +433,6 @@
                 <property name="width_chars">5</property>
                 <property name="input_purpose">number</property>
                 <signal name="changed" handler="on_every_entry_changed" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
-                <signal name="insert-text" handler="on_insert_text_numbers_only" 
object="CaliforniaHostCreateUpdateRecurring" swapped="no"/>
               </object>
               <packing>
                 <property name="expand">False</property>
diff --git a/src/rc/date-time-widget.ui b/src/rc/date-time-widget.ui
new file mode 100644
index 0000000..4fbf1d7
--- /dev/null
+++ b/src/rc/date-time-widget.ui
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="CaliforniaHostDateTimeWidget" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">center</property>
+    <property name="valign">center</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">8</property>
+    <child>
+      <object class="GtkCalendar" id="calendar">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="halign">end</property>
+        <property name="hexpand">False</property>
+        <property name="year">2014</property>
+        <property name="month">6</property>
+        <property name="day">23</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">False</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="time_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="hexpand">False</property>
+        <property name="vexpand">False</property>
+        <child>
+          <object class="GtkEntry" id="hour_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="hexpand">False</property>
+            <property name="vexpand">False</property>
+            <property name="max_length">2</property>
+            <property name="width_chars">2</property>
+            <property name="xalign">1</property>
+            <property name="input_purpose">digits</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="colon_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="hexpand">False</property>
+            <property name="label" translatable="yes">:</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEntry" id="minutes_entry">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="halign">start</property>
+            <property name="hexpand">False</property>
+            <property name="max_length">2</property>
+            <property name="width_chars">2</property>
+            <property name="input_purpose">digits</property>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="hour_up">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_hour_up_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">up</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="hour_down">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_hour_down_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">down</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="meridiem_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label">am</property>
+            <property name="width_chars">3</property>
+            <property name="max_width_chars">2</property>
+          </object>
+          <packing>
+            <property name="left_attach">3</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="minutes_up">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_minutes_up_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">up</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="minutes_down">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_minutes_down_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">down</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">2</property>
+            <property name="top_attach">2</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="meridiem_up">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_meridiem_up_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">up</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">3</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEventBox" id="meridiem_down">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="visible_window">False</property>
+            <child>
+              <object class="GtkArrow" id="from_meridiem_down_arrow">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="hexpand">False</property>
+                <property name="arrow_type">down</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">3</property>
+            <property name="top_attach">2</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/rc/event-time-settings.ui b/src/rc/event-time-settings.ui
index 0dac5af..41a38cc 100644
--- a/src/rc/event-time-settings.ui
+++ b/src/rc/event-time-settings.ui
@@ -24,254 +24,19 @@
       </packing>
     </child>
     <child>
-      <object class="GtkBox" id="box1">
+      <object class="GtkBox" id="date_time_widgets_box">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="hexpand">True</property>
         <property name="spacing">4</property>
         <child>
           <object class="GtkBox" id="from_box">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="halign">end</property>
-            <property name="hexpand">False</property>
             <property name="orientation">vertical</property>
-            <property name="spacing">4</property>
             <child>
-              <object class="GtkCalendar" id="from_calendar">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="halign">end</property>
-                <property name="hexpand">False</property>
-                <property name="year">2014</property>
-                <property name="month">6</property>
-                <property name="day">23</property>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkGrid" id="from_time_grid">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="halign">center</property>
-                <property name="valign">center</property>
-                <property name="hexpand">False</property>
-                <property name="vexpand">False</property>
-                <child>
-                  <object class="GtkEntry" id="from_hour_entry">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="halign">center</property>
-                    <property name="valign">center</property>
-                    <property name="hexpand">False</property>
-                    <property name="vexpand">False</property>
-                    <property name="max_length">2</property>
-                    <property name="width_chars">2</property>
-                    <property name="xalign">1</property>
-                    <property name="input_purpose">digits</property>
-                  </object>
-                  <packing>
-                    <property name="left_attach">0</property>
-                    <property name="top_attach">1</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="from_colon_label">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="halign">center</property>
-                    <property name="valign">center</property>
-                    <property name="hexpand">False</property>
-                    <property name="label" translatable="yes">:</property>
-                  </object>
-                  <packing>
-                    <property name="left_attach">1</property>
-                    <property name="top_attach">1</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEntry" id="from_minutes_entry">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="halign">start</property>
-                    <property name="hexpand">False</property>
-                    <property name="max_length">2</property>
-                    <property name="width_chars">2</property>
-                    <property name="input_purpose">digits</property>
-                  </object>
-                  <packing>
-                    <property name="left_attach">2</property>
-                    <property name="top_attach">1</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_hour_up">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_hour_up_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">up</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">0</property>
-                    <property name="top_attach">0</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_hour_down">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_hour_down_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">down</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">0</property>
-                    <property name="top_attach">2</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkLabel" id="from_meridiem_label">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="label">am</property>
-                    <property name="width_chars">3</property>
-                    <property name="max_width_chars">2</property>
-                  </object>
-                  <packing>
-                    <property name="left_attach">3</property>
-                    <property name="top_attach">1</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_minutes_up">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_minutes_up_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">up</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">2</property>
-                    <property name="top_attach">0</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_minutes_down">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_minutes_down_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">down</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">2</property>
-                    <property name="top_attach">2</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_meridiem_up">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_meridiem_up_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">up</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">3</property>
-                    <property name="top_attach">0</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkEventBox" id="from_meridiem_down">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="visible_window">False</property>
-                    <child>
-                      <object class="GtkArrow" id="from_meridiem_down_arrow">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="hexpand">False</property>
-                        <property name="arrow_type">down</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="left_attach">3</property>
-                    <property name="top_attach">2</property>
-                    <property name="width">1</property>
-                    <property name="height">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <placeholder/>
-                </child>
-                <child>
-                  <placeholder/>
-                </child>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-              </packing>
+              <placeholder/>
             </child>
           </object>
           <packing>
@@ -302,23 +67,6 @@
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="orientation">vertical</property>
-            <property name="spacing">4</property>
-            <child>
-              <object class="GtkCalendar" id="to_calendar">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="halign">start</property>
-                <property name="hexpand">False</property>
-                <property name="year">2014</property>
-                <property name="month">6</property>
-                <property name="day">23</property>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
             <child>
               <placeholder/>
             </child>
diff --git a/src/toolkit/toolkit-entry-filter-connector.vala b/src/toolkit/toolkit-entry-filter-connector.vala
new file mode 100644
index 0000000..a856d2e
--- /dev/null
+++ b/src/toolkit/toolkit-entry-filter-connector.vala
@@ -0,0 +1,87 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+namespace California.Toolkit {
+
+/**
+ * A connector that allows for filtering all text inserted into a Gtk.Entry.
+ */
+
+public class EntryFilterConnector : BaseObject {
+    private Gee.MapFunc<string, string> filter;
+    private Gee.HashSet<Gtk.Entry> entries = new Gee.HashSet<Gtk.Entry>();
+    private Gee.HashSet<Gtk.Entry> in_signal = new Gee.HashSet<Gtk.Entry>();
+    
+    /**
+     * A generic filtering mechanism for all connected Gtk.Entry's.
+     */
+    public EntryFilterConnector(Gee.MapFunc<string, string> filter) {
+        this.filter = filter;
+    }
+    
+    /**
+     * A specific filter for allowing only numeric input.
+     */
+    public EntryFilterConnector.only_numeric() {
+        this (numeric_filter);
+    }
+    
+    ~EntryFilterConnector() {
+        traverse_safely<Gtk.Entry>(entries).iterate(disconnect_from);
+    }
+    
+    public void connect_to(Gtk.Entry entry) {
+        if (!entries.add(entry))
+            return;
+        
+        entry.insert_text.connect(on_entry_insert);
+    }
+    
+    public void disconnect_from(Gtk.Entry entry) {
+        if (!entries.remove(entry))
+            return;
+        
+        entry.insert_text.disconnect(on_entry_insert);
+    }
+    
+    private static string numeric_filter(owned string str) {
+        return from_string(str)
+            .filter(ch => ch.isdigit())
+            .to_string(ch => ch.to_string());
+    }
+    
+    private void on_entry_insert(Gtk.Editable editable, string new_text, int new_text_length,
+        ref int position) {
+        Gtk.Entry entry = (Gtk.Entry) editable;
+        
+        // prevent recursion when our modified text is inserted (i.e. allow the base handler to
+        // deal with new text directly)
+        if (entry in in_signal)
+            return;
+        
+        // filter
+        string filtered = filter(new_text);
+        
+        // insert new text into place, ensure this handler doesn't attempt to process this
+        // modified text ... would use SignalHandler.block_by_func() and unblock_by_func(), but
+        // the bindings are ungood
+        if (!String.is_empty(filtered)) {
+            in_signal.add(entry);
+            editable.insert_text(filtered, filtered.length, ref position);
+            in_signal.remove(entry);
+        }
+        
+        // don't let the base handler have at the original text
+        Signal.stop_emission_by_name(editable, "insert-text");
+    }
+    
+    public override string to_string() {
+        return classname;
+    }
+}
+
+}
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]