[california] Improved time/date widgets in Create/Update event dialog: Bug #725783
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california] Improved time/date widgets in Create/Update event dialog: Bug #725783
- Date: Wed, 6 Aug 2014 00:07:22 +0000 (UTC)
commit c448068c7165c76b1afcca40c8b60c2d50c89df0
Author: Jim Nelson <jim yorba org>
Date: Tue Aug 5 17:05:02 2014 -0700
Improved time/date widgets in Create/Update event dialog: Bug #725783
This introduces new time/date widgets when creating or editing an
event. Rather than popping up over the existing window, a new card
in the deck is shown (much like when editing recurring events)
allowing the user to visually adjust date and time with mouse and/or
the keyboard.
This also removes the sustained duration auto-adjustment (bug #732031)
src/Makefile.am | 6 +
src/base/base-object.vala | 10 +-
src/calendar/calendar-date-span.vala | 24 ++
src/calendar/calendar-date.vala | 2 +
src/calendar/calendar-exact-time-span.vala | 60 ++++
src/calendar/calendar-exact-time.vala | 21 ++
src/calendar/calendar-wall-time.vala | 109 ++++++-
src/california-resources.xml | 6 +
src/component/component-event.vala | 47 +---
src/host/host-create-update-event.vala | 203 +++----------
src/host/host-create-update-recurring.vala | 31 +--
src/host/host-date-time-widget.vala | 368 +++++++++++++++++++++++
src/host/host-event-time-settings.vala | 197 ++++++++++++
src/host/host-main-window.vala | 8 +-
src/host/host-quick-create-event.vala | 7 +-
src/host/host-show-event.vala | 3 +-
src/rc/create-update-event.ui | 315 +++++++++-----------
src/rc/create-update-recurring.ui | 2 -
src/rc/date-time-widget.ui | 250 +++++++++++++++
src/rc/event-time-settings.ui | 155 ++++++++++
src/tests/tests-calendar-exact-time.vala | 63 ++++
src/tests/tests-calendar-wall-time.vala | 91 +++++--
src/tests/tests.vala | 1 +
src/toolkit/toolkit-button-connector.vala | 148 +++------
src/toolkit/toolkit-button-event.vala | 4 +-
src/toolkit/toolkit-card.vala | 1 -
src/toolkit/toolkit-entry-filter-connector.vala | 87 ++++++
src/view/week/week-day-pane.vala | 2 +-
src/view/week/week-grid.vala | 26 +-
29 files changed, 1701 insertions(+), 546 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 62c334a..a858b9a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -93,6 +93,8 @@ 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 \
host/host-quick-create-event.vala \
@@ -105,6 +107,7 @@ california_VALASOURCES = \
\
tests/tests.vala \
tests/tests-calendar-date.vala \
+ tests/tests-calendar-exact-time.vala \
tests/tests-calendar-month-of-year.vala \
tests/tests-calendar-month-span.vala \
tests/tests-calendar-wall-time.vala \
@@ -123,6 +126,7 @@ california_VALASOURCES = \
toolkit/toolkit-deck-window.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 \
@@ -181,6 +185,8 @@ 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 \
rc/google-login.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-date.vala b/src/calendar/calendar-date.vala
index 9f0e71f..2f8da5c 100644
--- a/src/calendar/calendar-date.vala
+++ b/src/calendar/calendar-date.vala
@@ -296,6 +296,8 @@ public class Date : Unit<Date>, Gee.Comparable<Date>, Gee.Hashable<Date> {
/**
* Returns a { link Date} clamped between the two supplied Dates, inclusive.
+ *
+ * @see Span.clamp_between
*/
public Date clamp(Date min, Date max) {
GLib.Date clone = gdate;
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/calendar/calendar-exact-time.vala b/src/calendar/calendar-exact-time.vala
index 49ff335..d508dae 100644
--- a/src/calendar/calendar-exact-time.vala
+++ b/src/calendar/calendar-exact-time.vala
@@ -162,6 +162,27 @@ public class ExactTime : BaseObject, Gee.Comparable<ExactTime>, Gee.Hashable<Exa
}
/**
+ * Clamp the { link ExactTime} between a supplied floor and ceiling ExactTime.
+ *
+ * If null is passed for either value, it will be ignored (effectively making clamp() work like
+ * a floor() or ceiling() method). If null is passed for both, the current ExactTime is
+ * returned.
+ *
+ * Results are indeterminate if a floor chronologically later than a ceiling is passed in.
+ */
+ public ExactTime clamp(ExactTime? floor, ExactTime? ceiling) {
+ ExactTime clamped = this;
+
+ if (floor != null && clamped.compare_to(floor) < 0)
+ clamped = floor;
+
+ if (ceiling != null && clamped.compare_to(ceiling) > 0)
+ clamped = ceiling;
+
+ return clamped;
+ }
+
+ /**
* See DateTime.to_unix_time.
*/
public time_t to_time_t() {
diff --git a/src/calendar/calendar-wall-time.vala b/src/calendar/calendar-wall-time.vala
index 57de06d..c68d5c2 100644
--- a/src/calendar/calendar-wall-time.vala
+++ b/src/calendar/calendar-wall-time.vala
@@ -348,56 +348,133 @@ public class WallTime : BaseObject, Gee.Comparable<WallTime>, Gee.Hashable<WallT
/**
* Round a unit of the { link WallTime} to a multiple of a supplied value.
*
- * By rounding wall-clock time, not only is the unit in question rounded down to a multiple of
+ * Supply a positive integer to round up, a negative integer to round down.
+ *
+ * By rounding wall-clock time, not only is the unit in question rounded to a multiple of
* the supplied value, but the lesser units are truncated to zero. Thus, 17:23:54 rounded down
* to a multiple of 10 minutes returns 17:20:00.
*
+ * rollover is set to true if rounding by the multiple rolls the WallTime over to the next day.
+ * Rolling back to the previous day isn't possible with this interface; rounding down any value
+ * earlier than midnight results in midnight. Rollover can occur when rounding up.
+ *
+ * It's important to note that zero is treated as a multiple of all values. Hence rounding
+ * 11:56:00 up to a multiple of 17 minutes will result in 12:00:00. (In other words, don't
+ * confuse this method with { link adjust}.
+ *
* If the { link TimeUnit} is already a multiple of the value, no change is made (although
* there's no guarantee that the same WallTime instance will be returned, especially if the
* lesser units are truncated).
*
- * A multiple of zero or a negative value is always rounded to the current WallTime.
- *
- * TODO: An interface to round up (which will need to deal with overflow).
+ * A multiple of zero is always rounded to the current WallTime.
*/
- public WallTime round_down(int multiple, TimeUnit time_unit) {
- if (multiple <= 0)
+ public WallTime round(int multiple, TimeUnit time_unit, out bool rollover) {
+ rollover = false;
+
+ if (multiple == 0)
return this;
- // get value being manipulated
- int current;
+ // get value being manipulated and its max value (min is always zero)
+ int current, max;
switch (time_unit) {
case TimeUnit.HOUR:
current = hour;
+ max = MAX_HOUR;
break;
case TimeUnit.MINUTE:
current = minute;
+ max = MAX_MINUTE;
break;
case TimeUnit.SECOND:
current = second;
+ max = MAX_SECOND;
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ int rounded;
+ if (multiple < 0) {
+ // round down and watch for underflow (which shouldn't happen)
+ rounded = current - (current % multiple.abs());
+ assert(rounded >= 0);
+ } else {
+ assert(multiple > 0);
+
+ // round up and watch for overflow (which can definitely happen)
+ int rem = current % multiple;
+ if (rem != 0) {
+ rounded = current + (multiple - rem);
+ if (rounded > max) {
+ rounded = 0;
+ rollover = true;
+ }
+ } else {
+ // no remainder then on the money
+ rounded = current;
+ }
+ }
+
+ // construct new value and deal with rollover
+ Calendar.WallTime rounded_wall_time;
+ bool adjust_rollover = false;
+ switch (time_unit) {
+ case TimeUnit.HOUR:
+ // no adjust can be done, rollover is rollover here
+ rounded_wall_time = new WallTime(rounded, 0, 0);
+ break;
+
+ case TimeUnit.MINUTE:
+ rounded_wall_time = new WallTime(hour, rounded, 0);
+ if (rollover)
+ rounded_wall_time = rounded_wall_time.adjust(1, TimeUnit.HOUR, out adjust_rollover);
+ break;
+
+ case TimeUnit.SECOND:
+ rounded_wall_time = new WallTime(hour, minute, rounded);
+ if (rollover)
+ rounded_wall_time = rounded_wall_time.adjust(1, TimeUnit.MINUTE, out adjust_rollover);
break;
default:
assert_not_reached();
}
- // round down and watch for underflow (which shouldn't happen)
- int rounded = current - (current % multiple.abs());
- if (rounded < 0)
- rounded = 0;
+ // handle adjustment causing rollover
+ rollover = rollover || adjust_rollover;
+
+ return rounded_wall_time;
+ }
+
+ /**
+ * Adjust the time by the specified amount without affecting other units.
+ *
+ * "Free adjust" is designed to work like adjusting a clock's time where each unit is disengaged
+ * from the others. That is, if the minutes setting is adjusted from 59 to 0, the hour remains
+ * unchanged.
+ *
+ * An amount of zero returns the current { link WallTime}.
+ *
+ * @see adjust
+ */
+ public WallTime free_adjust(int amount, TimeUnit time_unit) {
+ if (amount == 0)
+ return this;
- // return new value
+ // piggyback on adjust() to do the heavy lifting, then rearrange its results
+ WallTime adjusted = adjust(amount, time_unit, null);
switch (time_unit) {
case TimeUnit.HOUR:
- return new WallTime(rounded, 0, 0);
+ return new WallTime(adjusted.hour, minute, second);
case TimeUnit.MINUTE:
- return new WallTime(hour, rounded, 0);
+ return new WallTime(hour, adjusted.minute, second);
case TimeUnit.SECOND:
- return new WallTime(hour, minute, rounded);
+ return new WallTime(hour, minute, adjusted.second);
default:
assert_not_reached();
diff --git a/src/california-resources.xml b/src/california-resources.xml
index edf38ea..6d65154 100644
--- a/src/california-resources.xml
+++ b/src/california-resources.xml
@@ -25,6 +25,12 @@
<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">
<file compressed="true">rc/google-authenticating.ui</file>
</gresource>
<gresource prefix="/org/yorba/california">
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index b4c2d34..ed3e2ed 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -244,6 +244,7 @@ public class Event : Instance, Gee.Comparable<Event> {
*
* This will return a DateSpan whether the Event is a DATE or DATE-TIME VEVENT.
*/
+ // TODO: Make date_span/exact_time_span a separate object
public Calendar.DateSpan get_event_date_span(Calendar.Timezone? tz) {
if (date_span != null)
return date_span;
@@ -350,57 +351,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-event.vala b/src/host/host-create-update-event.vala
index 51aa3cd..757efb9 100644
--- a/src/host/host-create-update-event.vala
+++ b/src/host/host-create-update-event.vala
@@ -17,8 +17,6 @@ namespace California.Host {
public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
public const string ID = "CreateUpdateEvent";
- public const string PROP_SELECTED_DATE_SPAN = "selected-date-span";
-
private const int START_HOUR = 0;
private const int END_HOUR = 23;
private const int MIN_DIVISIONS = 15;
@@ -38,19 +36,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
private Gtk.Entry summary_entry;
[GtkChild]
- private Gtk.Button dtstart_date_button;
-
- [GtkChild]
- private Gtk.ComboBoxText dtstart_time_combo;
-
- [GtkChild]
- private Gtk.Button dtend_date_button;
-
- [GtkChild]
- private Gtk.ComboBoxText dtend_time_combo;
-
- [GtkChild]
- private Gtk.CheckButton all_day_toggle;
+ private Gtk.Label time_summary_label;
[GtkChild]
private Gtk.Entry location_entry;
@@ -64,16 +50,12 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
[GtkChild]
private Gtk.Box rotating_button_box_container;
- public Calendar.DateSpan selected_date_span { get; set; }
-
public bool is_update { get; set; default = false; }
private new Component.Event event = new Component.Event.blank();
- private Gee.HashMap<string, Calendar.WallTime> time_map = new Gee.HashMap<string, Calendar.WallTime>();
+ private EventTimeSettings.Message? dt = null;
private Backing.CalendarSource? original_calendar_source;
private Toolkit.ComboBoxTextModel<Backing.CalendarSource> calendar_model;
- private Gtk.Button? last_date_button_touched = null;
- private bool both_date_buttons_touched = false;
private Toolkit.RotatingButtonBox rotating_button_box = new Toolkit.RotatingButtonBox();
private Toolkit.EntryClearTextConnector summary_clear_text_connector;
@@ -86,29 +68,14 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
private Gtk.Button cancel_recurring_button = new Gtk.Button.with_mnemonic(_("_Cancel"));
public CreateUpdateEvent() {
- // when selected_date_span updates, update date buttons as well
- notify[PROP_SELECTED_DATE_SPAN].connect(() => {
- dtstart_date_button.label = selected_date_span.start_date.to_standard_string();
- dtend_date_button.label = selected_date_span.end_date.to_standard_string();
- });
-
// create button is active only if summary is filled out; all other fields (so far)
// guarantee valid values at all times
summary_clear_text_connector = new Toolkit.EntryClearTextConnector(summary_entry);
- summary_entry.bind_property("text-length", accept_button, "sensitive",
- BindingFlags.SYNC_CREATE);
+ summary_entry.bind_property("text", accept_button, "sensitive", BindingFlags.SYNC_CREATE,
+ transform_summary_to_accept);
location_clear_text_connector = new Toolkit.EntryClearTextConnector(location_entry);
- // hide start/end time widgets if an all-day event ..."no-show-all" needed to avoid the
- // merciless effects of show_all()
- all_day_toggle.bind_property("active", dtstart_time_combo, "visible",
- BindingFlags.INVERT_BOOLEAN | BindingFlags.SYNC_CREATE);
- dtstart_time_combo.no_show_all = true;
- all_day_toggle.bind_property("active", dtend_time_combo, "visible",
- BindingFlags.INVERT_BOOLEAN | BindingFlags.SYNC_CREATE);
- dtend_time_combo.no_show_all = true;
-
// use model to control calendars combo box
calendar_model = new Toolkit.ComboBoxTextModel<Backing.CalendarSource>(calendar_combo,
(cal) => cal.title);
@@ -147,7 +114,17 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
rotating_button_box.valign = Gtk.Align.END;
rotating_button_box_container.add(rotating_button_box);
- update_controls();
+ Calendar.System.instance.is_24hr_changed.connect(on_update_time_summary);
+ }
+
+ ~CreateUpdateEvent() {
+ Calendar.System.instance.is_24hr_changed.disconnect(on_update_time_summary);
+ }
+
+ private bool transform_summary_to_accept(Binding binding, Value source_value, ref Value target_value) {
+ target_value = summary_entry.text_length > 0 && (event != null ? event.is_valid(false) : false);
+
+ return true;
}
public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message) {
@@ -155,7 +132,13 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
if (message == null)
return;
- event = (Component.Event) message;
+ if (message.type() == typeof(EventTimeSettings.Message)) {
+ dt = (EventTimeSettings.Message) message;
+ } else {
+ event = (Component.Event) message;
+ if (dt == null)
+ dt = new EventTimeSettings.Message.from_event(event);
+ }
update_controls();
}
@@ -166,69 +149,7 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
else
summary_entry.text = "";
- Calendar.WallTime initial_start_time, initial_end_time;
- if (event.exact_time_span != null) {
- all_day_toggle.active = false;
- selected_date_span = event.exact_time_span.get_date_span();
- initial_start_time =
- event.exact_time_span.start_exact_time.to_timezone(Calendar.Timezone.local).to_wall_time();
- initial_end_time =
- event.exact_time_span.end_exact_time.to_timezone(Calendar.Timezone.local).to_wall_time();
- } else if (event.date_span != null) {
- all_day_toggle.active = true;
- selected_date_span = event.date_span;
- initial_start_time = Calendar.System.now.to_wall_time();
- initial_end_time = Calendar.System.now.adjust_time(1, Calendar.TimeUnit.HOUR).to_wall_time();
- } else {
- all_day_toggle.active = false;
- selected_date_span = new Calendar.DateSpan(Calendar.System.today, Calendar.System.today);
- initial_start_time = Calendar.System.now.to_wall_time();
- initial_end_time = Calendar.System.now.adjust_time(1, Calendar.TimeUnit.HOUR).to_wall_time();
-
- // set in Component.Event as well, to at least initialize it for use elsewhere while
- // editing (such as the RRULE)
- event.set_event_exact_time_span(new Calendar.ExactTimeSpan(
- new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_start_time),
- new Calendar.ExactTime(Calendar.Timezone.local, Calendar.System.today, initial_end_time)
- ));
- }
-
- // initialize start and end time controls (as in, wall clock time)
- Calendar.WallTime current = new Calendar.WallTime(START_HOUR, Calendar.WallTime.MIN_MINUTE, 0);
- Calendar.WallTime end = new Calendar.WallTime(END_HOUR, Calendar.WallTime.MAX_MINUTE, 0);
- int index = 0;
- int dtstart_active_index = -1, dtend_active_index = -1;
- bool rollover = false;
- while (current.compare_to(end) <= 0 && !rollover) {
- string fmt = current.to_pretty_string(Calendar.WallTime.PrettyFlag.NONE);
-
- dtstart_time_combo.append_text(fmt);
- dtend_time_combo.append_text(fmt);
-
- // use the latest time for each end of the span to initialize combo boxes, looking for
- // exact match, otherwise taking the *next* index (to default to the future slot, not
- // one that's past)
- int cmp = initial_start_time.compare_to(current);
- if (cmp == 0)
- dtstart_active_index = index;
- else if (cmp > 0)
- dtstart_active_index = index + 1;
-
- cmp = initial_end_time.compare_to(current);
- if (cmp == 0)
- dtend_active_index = index;
- else if (cmp > 0)
- dtend_active_index = index + 1;
-
- index++;
-
- time_map.set(fmt, current);
- current = current.adjust(MIN_DIVISIONS, Calendar.TimeUnit.MINUTE, out rollover);
- }
-
- // set initial indices, careful to avoid overrun
- dtstart_time_combo.set_active(dtstart_active_index.clamp(0, index - 1));
- dtend_time_combo.set_active(dtend_active_index.clamp(0, index - 1));
+ on_update_time_summary();
// set combo to event's calendar
if (event.calendar_source != null) {
@@ -249,41 +170,17 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
original_calendar_source = event.calendar_source;
}
- [GtkCallback]
- private void on_date_button_clicked(Gtk.Button button) {
- bool is_dtstart = (button == dtstart_date_button);
-
- // if both buttons have been touched, go into free-selection mode with the dates, otherwise
- // respect the original span duration
- both_date_buttons_touched =
- both_date_buttons_touched
- || (last_date_button_touched != null && last_date_button_touched != button);
-
- Toolkit.CalendarPopup popup = new Toolkit.CalendarPopup(button,
- is_dtstart ? selected_date_span.start_date : selected_date_span.end_date);
-
- popup.date_selected.connect((date) => {
- // if both buttons touched, use free date selection, otherwise respect the original
- // span duration
- if (both_date_buttons_touched) {
- selected_date_span = new Calendar.DateSpan(
- is_dtstart ? date : selected_date_span.start_date,
- !is_dtstart ? date : selected_date_span.end_date
- );
- } else {
- selected_date_span = is_dtstart
- ? selected_date_span.adjust_start_date(date)
- : selected_date_span.adjust_end_date(date);
- }
- });
-
- popup.dismissed.connect(() => {
- popup.destroy();
- });
-
- popup.show_all();
-
- last_date_button_touched = button;
+ private void on_update_time_summary() {
+ // use the Message, not the Event, to load this up
+ time_summary_label.visible = true;
+ if (dt.date_span != null) {
+ time_summary_label.label = dt.date_span.to_pretty_string(Calendar.Date.PrettyFlag.NONE);
+ } else if (dt.exact_time_span != null) {
+ time_summary_label.label =
dt.exact_time_span.to_timezone(Calendar.Timezone.local).to_pretty_string(
+ Calendar.Date.PrettyFlag.NONE, Calendar.ExactTimeSpan.PrettyFlag.NONE);
+ } else {
+ time_summary_label.visible = false;
+ }
}
[GtkCallback]
@@ -295,6 +192,14 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
jump_to_card_by_name(CreateUpdateRecurring.ID, event);
}
+ [GtkCallback]
+ private void on_edit_time_button_clicked() {
+ if (dt == null)
+ dt = new EventTimeSettings.Message.from_event(event);
+
+ jump_to_card_by_name(EventTimeSettings.ID, dt);
+ }
+
private void on_accept_button_clicked() {
if (calendar_model.active == null)
return;
@@ -322,13 +227,13 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
// if updating the master, don't replace the dtstart/dtend, but do want to adjust it from
// DATE to DATE-TIME or vice-versa
if (!replace_dtstart) {
- if (target.is_all_day != all_day_toggle.active) {
- if (all_day_toggle.active) {
+ if (target.is_all_day != dt.is_all_day) {
+ if (dt.is_all_day) {
target.timed_to_all_day_event();
} else {
target.all_day_to_timed_event(
- time_map.get(dtstart_time_combo.get_active_text()),
- time_map.get(dtend_time_combo.get_active_text()),
+ dt.exact_time_span.start_exact_time.to_wall_time(),
+ dt.exact_time_span.end_exact_time.to_wall_time(),
Calendar.Timezone.local
);
}
@@ -337,18 +242,10 @@ public class CreateUpdateEvent : Gtk.Grid, Toolkit.Card {
return;
}
- if (all_day_toggle.active) {
- target.set_event_date_span(selected_date_span);
- } else {
- target.set_event_exact_time_span(
- new Calendar.ExactTimeSpan(
- new Calendar.ExactTime(Calendar.Timezone.local, selected_date_span.start_date,
- time_map.get(dtstart_time_combo.get_active_text())),
- new Calendar.ExactTime(Calendar.Timezone.local, selected_date_span.end_date,
- time_map.get(dtend_time_combo.get_active_text()))
- )
- );
- }
+ if (dt.is_all_day)
+ target.set_event_date_span(dt.date_span);
+ else
+ target.set_event_exact_time_span(dt.exact_time_span);
}
private void create_update_event(Component.Event target, bool replace_dtstart) {
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..03f772f
--- /dev/null
+++ b/src/host/host-date-time-widget.vala
@@ -0,0 +1,368 @@
+/* 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 const string PROP_FLOOR = "floor";
+ public const string PROP_CEILING = "ceiling";
+ public const string PROP_OUT_OF_RANGE = "out-of-range";
+
+ 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(); }
+
+ public Calendar.ExactTime? floor { get; set; default = null; }
+
+ public Calendar.ExactTime? ceiling { get; set; default = null; }
+
+ /**
+ * Indicates if the widgets are filled-in with invalid values or is valid but out of the range
+ * of { link floor} and/or { link ceiling}.
+ */
+ public bool out_of_range { get; protected set; default = false; }
+
+ [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);
+
+ // 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));
+
+ // set sensitivities for up/down widgets
+ foreach (Gtk.Widget widget in
+ iterate<Gtk.Widget>(hour_up, hour_down, minutes_up, minutes_down, meridiem_up, meridiem_down)) {
+ bind_property(PROP_DATE, widget, "sensitive", BindingFlags.SYNC_CREATE,
+ transform_adjustment_widget_to_sensitive);
+ bind_property(PROP_WALL_TIME, widget, "sensitive", BindingFlags.SYNC_CREATE,
+ transform_adjustment_widget_to_sensitive);
+ bind_property(PROP_FLOOR, widget, "sensitive", BindingFlags.SYNC_CREATE,
+ transform_adjustment_widget_to_sensitive);
+ bind_property(PROP_CEILING, widget, "sensitive", BindingFlags.SYNC_CREATE,
+ transform_adjustment_widget_to_sensitive);
+ }
+
+ // update out_of_range when its dependencies change
+ bind_property(PROP_DATE, this, PROP_OUT_OF_RANGE, BindingFlags.SYNC_CREATE,
+ transform_to_out_of_range);
+ bind_property(PROP_WALL_TIME, this, PROP_OUT_OF_RANGE, BindingFlags.SYNC_CREATE,
+ transform_to_out_of_range);
+ bind_property(PROP_FLOOR, this, PROP_OUT_OF_RANGE, BindingFlags.SYNC_CREATE,
+ transform_to_out_of_range);
+ bind_property(PROP_CEILING, this, PROP_OUT_OF_RANGE, BindingFlags.SYNC_CREATE,
+ transform_to_out_of_range);
+
+ bind_bool_to_time_controls(PROP_ENABLE_DATE, iterate<Gtk.Widget>(calendar));
+
+ // use signal handlers to initialize widgets
+ on_date_changed();
+ on_wall_time_changed();
+
+ connect_property_signals();
+ connect_widget_signals();
+
+ // honor 24-hour time
+ Calendar.System.instance.is_24hr_changed.connect(system_24hr_changed);
+ system_24hr_changed();
+
+ // GTK 3.12 requires this in order to constrain GtkEntry width, older versions were happy
+ // with width_chars alone
+#if GTK_312
+ hour_entry.max_width_chars = minutes_entry.max_width_chars = 2;
+#endif
+ }
+
+ ~DateTimeWidget() {
+ Calendar.System.instance.is_24hr_changed.disconnect(system_24hr_changed);
+ }
+
+ 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);
+ }
+
+ // Determine if the up/down adjustments should be sensitive (if they're next value is valid)
+ private bool transform_adjustment_widget_to_sensitive(Binding binding, Value source_value,
+ ref Value target_value) {
+ int amount;
+ Calendar.TimeUnit time_unit;
+ if (!adjust_time_controls((Gtk.Widget) binding.target, out amount, out time_unit))
+ return false;
+
+ target_value = is_valid_date_time(date, wall_time.adjust(amount, time_unit, null));
+
+ return true;
+ }
+
+ private bool transform_to_out_of_range(Binding binding, Value source_value, ref Value target_value) {
+ target_value = is_valid_date_time(date, wall_time);
+
+ return true;
+ }
+
+ 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);
+ hour_entry.changed.connect(on_time_entry_changed);
+ minutes_entry.changed.connect(on_time_entry_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);
+ hour_entry.changed.disconnect(on_time_entry_changed);
+ minutes_entry.changed.disconnect(on_time_entry_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.widget, out amount, out time_unit))
+ return Toolkit.PROPAGATE;
+
+ // use free_adjust() to adjust each unit individually without affecting others
+ Calendar.WallTime new_wall_time = wall_time.free_adjust(amount, time_unit);
+
+ // ensure it's clamped ... this assignment will update the entry fields, so don't
+ // disconnect widget signals
+ if (is_valid_date_time(date, new_wall_time))
+ wall_time = new_wall_time;
+
+ return Toolkit.STOP;
+ }
+
+ private bool adjust_time_controls(Gtk.Widget widget, out int amount, out Calendar.TimeUnit time_unit) {
+ if (widget == hour_up) {
+ amount = 1;
+ time_unit = Calendar.TimeUnit.HOUR;
+ } else if (widget == hour_down) {
+ amount = -1;
+ time_unit = Calendar.TimeUnit.HOUR;
+ } else if (widget == minutes_up) {
+ amount = 5;
+ time_unit = Calendar.TimeUnit.MINUTE;
+ } else if (widget == minutes_down) {
+ amount = -5;
+ time_unit = Calendar.TimeUnit.MINUTE;
+ } else if (widget == meridiem_up) {
+ amount = 12;
+ time_unit = Calendar.TimeUnit.HOUR;
+ } else if (widget == meridiem_down) {
+ amount = -12;
+ time_unit = Calendar.TimeUnit.HOUR;
+ } else {
+ amount = 0;
+ time_unit = Calendar.TimeUnit.HOUR;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool is_valid_date_time(Calendar.Date proposed_date, Calendar.WallTime proposed_time) {
+ Calendar.ExactTime exact_time = new Calendar.ExactTime(Calendar.Timezone.local, proposed_date,
+ proposed_time);
+
+ return exact_time.clamp(floor, ceiling).equal_to(exact_time);
+ }
+
+ 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 && is_valid_date_time(selected, wall_time) && !selected.equal_to(date))
+ date = selected;
+
+ // even if user picked invalid date, this resets selection to valid one
+ on_date_changed();
+
+ 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(Calendar.System.is_24hr ? wall_time.hour : 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();
+ }
+
+ private void on_time_entry_changed() {
+ Calendar.WallTime new_wall_time;
+ bool valid = validate_time_entries(out new_wall_time);
+
+ disconnect_property_signals();
+
+ out_of_range = !valid;
+ if (valid)
+ wall_time = new_wall_time;
+
+ connect_property_signals();
+ }
+
+ private bool validate_time_entries(out Calendar.WallTime new_wall_time) {
+ // maintain current until validated
+ new_wall_time = wall_time;
+
+ if (String.is_empty(hour_entry.text) || String.is_empty(minutes_entry.text))
+ return false;
+
+ int hour = int.parse(hour_entry.text);
+ if (!Calendar.System.is_24hr && meridiem_label.label == Calendar.FMT_PM && hour < 12)
+ hour += 12;
+
+ int min = int.parse(minutes_entry.text);
+
+ if (hour > Calendar.WallTime.MAX_HOUR || hour < 0 || min > Calendar.WallTime.MAX_MINUTE || min < 0)
+ return false;
+
+ Calendar.WallTime entry_wall_time = new Calendar.WallTime(hour, min, 0);
+
+ Calendar.ExactTime entry_time = new Calendar.ExactTime(Calendar.Timezone.local, date,
entry_wall_time);
+ if (floor != null && entry_time.compare_to(floor) < 0)
+ return false;
+
+ if (ceiling != null && entry_time.compare_to(ceiling) > 0)
+ return false;
+
+ new_wall_time = entry_wall_time;
+
+ return true;
+ }
+
+ private void system_24hr_changed() {
+ meridiem_label.visible = meridiem_up.visible = meridiem_down.visible = !Calendar.System.is_24hr;
+ meridiem_label.no_show_all = meridiem_up.no_show_all = meridiem_down.no_show_all =
Calendar.System.is_24hr;
+
+ // redo time widgets
+ on_wall_time_changed();
+ }
+}
+
+}
+
diff --git a/src/host/host-event-time-settings.vala b/src/host/host-event-time-settings.vala
new file mode 100644
index 0000000..9440ce2
--- /dev/null
+++ b/src/host/host-event-time-settings.vala
@@ -0,0 +1,197 @@
+/* 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/event-time-settings.ui")]
+public class EventTimeSettings : Gtk.Box, Toolkit.Card {
+ public const string ID = "CaliforniaHostEventTimeSettings";
+
+ public class Message : Object {
+ public Calendar.DateSpan? date_span { get; private set; default = null; }
+
+ public Calendar.ExactTimeSpan? exact_time_span { get; private set; default = null; }
+
+ public bool is_all_day { get { return exact_time_span == null; } }
+
+ public Message.for_date_span(Calendar.DateSpan date_span) {
+ reset_date_span(date_span);
+ }
+
+ public Message.for_exact_time_span(Calendar.ExactTimeSpan exact_time_span) {
+ reset_exact_time_span(exact_time_span);
+ }
+
+ public Message.from_event(Component.Event event) {
+ if (event.is_all_day)
+ reset_date_span(event.date_span);
+ else
+ reset_exact_time_span(event.exact_time_span);
+ }
+
+ public void reset_date_span(Calendar.DateSpan date_span) {
+ this.date_span = date_span;
+ exact_time_span = null;
+ }
+
+ public void reset_exact_time_span(Calendar.ExactTimeSpan exact_time_span) {
+ date_span = null;
+ this.exact_time_span = exact_time_span;
+ }
+
+ public Calendar.DateSpan get_event_date_span(Calendar.Timezone? tz) {
+ if (date_span != null)
+ return date_span;
+
+ return new Calendar.DateSpan.from_exact_time_span(
+ tz != null ? exact_time_span.to_timezone(tz) : exact_time_span);
+ }
+ }
+
+ [GtkChild]
+ private Gtk.Label summary_label;
+
+ [GtkChild]
+ private Gtk.Box from_box;
+
+ [GtkChild]
+ private Gtk.Box to_box;
+
+ [GtkChild]
+ private Gtk.CheckButton all_day_checkbutton;
+
+ [GtkChild]
+ private Gtk.Button ok_button;
+
+ public string card_id { get { return ID; } }
+ public string? title { get { return null; } }
+ public Gtk.Widget? default_widget { get { return null; } }
+ public Gtk.Widget? initial_focus { get { return null; } }
+
+ private Message? message = null;
+ private DateTimeWidget from_widget = new DateTimeWidget();
+ private DateTimeWidget to_widget = new DateTimeWidget();
+
+ public EventTimeSettings() {
+ // need to manually pack the date/time widgets
+ from_box.pack_start(from_widget);
+ to_box.pack_start(to_widget);
+
+ from_widget.notify[DateTimeWidget.PROP_DATE].connect(on_from_changed);
+ from_widget.notify[DateTimeWidget.PROP_WALL_TIME].connect(on_from_changed);
+ to_widget.notify[DateTimeWidget.PROP_DATE].connect(on_to_changed);
+ to_widget.notify[DateTimeWidget.PROP_WALL_TIME].connect(on_to_changed);
+ all_day_checkbutton.notify["active"].connect(on_update_summary);
+
+ from_widget.bind_property(DateTimeWidget.PROP_OUT_OF_RANGE, ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_oor_to_sensitive);
+ to_widget.bind_property(DateTimeWidget.PROP_OUT_OF_RANGE, ok_button, "sensitive",
+ BindingFlags.SYNC_CREATE, transform_oor_to_sensitive);
+
+ 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);
+
+ Calendar.System.instance.is_24hr_changed.connect(on_update_summary);
+ }
+
+ ~EventTimeSettings() {
+ Calendar.System.instance.is_24hr_changed.disconnect(on_update_summary);
+ }
+
+ private bool transform_oor_to_sensitive(Binding binding, Value source_value, ref Value target_value) {
+ target_value = !to_widget.out_of_range && !from_widget.out_of_range;
+
+ return true;
+ }
+
+ public void jumped_to(Toolkit.Card? from, Toolkit.Card.Jump reason, Value? message_value) {
+ message = (Message) message_value;
+
+ Calendar.DateSpan date_span = message.get_event_date_span(Calendar.Timezone.local);
+ from_widget.date = date_span.start_date;
+ to_widget.date = date_span.end_date;
+
+ // 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 (message.exact_time_span != null) {
+ Calendar.ExactTimeSpan time_span = message.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();
+ } else {
+ // set to defaults in case user wants to change from all-day to timed event
+ from_widget.wall_time = Calendar.System.now.to_wall_time().round(15, Calendar.TimeUnit.MINUTE,
+ null);
+ if (date_span.is_same_day) {
+ // one-hour event is default
+ to_widget.wall_time = from_widget.wall_time.adjust(1, Calendar.TimeUnit.HOUR, null);
+ } else {
+ // different days, same time on each day
+ to_widget.wall_time = from_widget.wall_time;
+ }
+ }
+
+ all_day_checkbutton.active = (message.exact_time_span == null);
+ }
+
+ [GtkCallback]
+ private void on_cancel_button_clicked() {
+ jump_back();
+ }
+
+ [GtkCallback]
+ private void on_ok_button_clicked() {
+ if (all_day_checkbutton.active)
+ message.reset_date_span(get_date_span());
+ else
+ message.reset_exact_time_span(get_exact_time_span());
+
+ jump_to_card_by_name(CreateUpdateEvent.ID, message);
+ }
+
+ // This does not respect the all-day checkbox
+ private Calendar.DateSpan get_date_span() {
+ return new Calendar.DateSpan(from_widget.date, to_widget.date);
+ }
+
+ // 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)
+ );
+ }
+
+ 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);
+ }
+
+ private void on_from_changed() {
+ // clamp to_widget to not allow earlier date/times than from_widget
+ to_widget.floor = new Calendar.ExactTime(Calendar.System.timezone, from_widget.date,
+ from_widget.wall_time);
+
+ on_update_summary();
+ }
+
+ private void on_to_changed() {
+ // clamp from_widget to not allow later date/times than to_widget
+ from_widget.ceiling = new Calendar.ExactTime(Calendar.System.timezone, to_widget.date,
+ to_widget.wall_time);
+
+ on_update_summary();
+ }
+}
+
+}
+
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index a753a18..5ad0fc7 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -435,9 +435,11 @@ public class MainWindow : Gtk.ApplicationWindow {
CreateUpdateRecurring create_update_recurring = new CreateUpdateRecurring();
+ EventTimeSettings event_time_settings = new EventTimeSettings();
+
Toolkit.Deck deck = new Toolkit.Deck();
deck.add_cards(
- iterate<Toolkit.Card>(quick_create, create_update, create_update_recurring)
+ iterate<Toolkit.Card>(quick_create, create_update, create_update_recurring, event_time_settings)
.to_array_list()
);
@@ -456,9 +458,11 @@ public class MainWindow : Gtk.ApplicationWindow {
CreateUpdateRecurring create_update_recurring = new CreateUpdateRecurring();
+ EventTimeSettings event_time_settings = new EventTimeSettings();
+
Toolkit.Deck deck = new Toolkit.Deck();
deck.add_cards(
- iterate<Toolkit.Card>(show_event, create_update, create_update_recurring)
+ iterate<Toolkit.Card>(show_event, create_update, create_update_recurring, event_time_settings)
.to_array_list()
);
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index d7d6da0..5d9c5a9 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
@@ -137,6 +138,10 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
if (event == null)
event = new Component.Event.blank();
+ // ensure it's at least valid
+ if (!event.is_valid(false))
+ event.set_event_date_span(Calendar.System.today.to_date_span());
+
// jump to Create/Update dialog and remove this Card from the Deck ... this ensures
// that if the user presses Cancel in the Create/Update dialog the Deck exits rather
// than returns here (via jump_home_or_user_closed())
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-event.ui b/src/rc/create-update-event.ui
index 92b2637..9961279 100644
--- a/src/rc/create-update-event.ui
+++ b/src/rc/create-update-event.ui
@@ -10,8 +10,8 @@
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
- <property name="row_spacing">8</property>
- <property name="column_homogeneous">True</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
<child>
<object class="GtkEntry" id="summary_entry">
<property name="visible">True</property>
@@ -25,23 +25,57 @@
<property name="placeholder_text" translatable="yes">Untitled event</property>
</object>
<packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="summary_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Summary</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
- <property name="width">2</property>
+ <property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
- <object class="GtkBox" id="dt_selection_box">
+ <object class="GtkLabel" id="time_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Time</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </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="GtkBox" id="time_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">4</property>
- <property name="baseline_position">top</property>
<child>
- <object class="GtkLabel" id="from_label">
+ <object class="GtkLabel" id="time_summary_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="label" translatable="yes" comments="As in "From <date> <time>
to <date> <time>"">From</property>
+ <property name="label">(none)</property>
+ <property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -50,208 +84,148 @@
</packing>
</child>
<child>
- <object class="GtkButton" id="dtstart_date_button">
- <property name="label">dtstart</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="xalign">0.52999997138977051</property>
- <signal name="clicked" handler="on_date_button_clicked" object="CaliforniaHostCreateUpdateEvent"
swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkComboBoxText" id="dtstart_time_combo">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="to_label">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes" comments="As in "From <date> <time>
to <date> <time>"">to</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="dtend_date_button">
- <property name="label">dtend</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <signal name="clicked" handler="on_date_button_clicked" object="CaliforniaHostCreateUpdateEvent"
swapped="no"/>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkComboBoxText" id="dtend_time_combo">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkCheckButton" id="all_day_toggle">
- <property name="label" translatable="yes">_All-day</property>
+ <object class="GtkButton" id="recurring_button">
+ <property name="label" translatable="yes">Re_peats…</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
+ <property name="receives_default">True</property>
+ <property name="margin_left">8</property>
<property name="use_underline">True</property>
- <property name="xalign">0</property>
- <property name="image_position">right</property>
- <property name="draw_indicator">True</property>
+ <signal name="clicked" handler="on_recurring_button_clicked"
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="position">6</property>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
</packing>
</child>
<child>
- <object class="GtkButton" id="recurring_button">
- <property name="label" translatable="yes">Re_peats...</property>
+ <object class="GtkButton" id="edit_time_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
- <property name="margin_left">8</property>
- <property name="use_underline">True</property>
- <signal name="clicked" handler="on_recurring_button_clicked"
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
+ <property name="tooltip_text" translatable="yes">Set the start and end time</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="on_edit_time_button_clicked"
object="CaliforniaHostCreateUpdateEvent" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">alarm-symbolic</property>
+ </object>
+ </child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="pack_type">end</property>
- <property name="position">7</property>
+ <property name="position">2</property>
</packing>
</child>
</object>
<packing>
- <property name="left_attach">0</property>
+ <property name="left_attach">1</property>
<property name="top_attach">1</property>
- <property name="width">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="location_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
- <object class="GtkGrid" id="grid1">
+ <object class="GtkLabel" id="location_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="row_spacing">6</property>
- <property name="column_spacing">6</property>
- <child>
- <object class="GtkLabel" id="location_label">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="xalign">1</property>
- <property name="label" translatable="yes">_Location</property>
- <property name="use_underline">True</property>
- <property name="mnemonic_widget">location_entry</property>
- <property name="single_line_mode">True</property>
- <style>
- <class name="dim-label"/>
- </style>
- </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="GtkEntry" id="location_entry">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="activates_default">True</property>
- </object>
- <packing>
- <property name="left_attach">1</property>
- <property name="top_attach">0</property>
- <property name="width">1</property>
- <property name="height">1</property>
- </packing>
- </child>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Location</property>
+ <property name="use_underline">True</property>
+ <property name="single_line_mode">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </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="description_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">_Description</property>
+ <property name="use_underline">True</property>
+ <property name="wrap">True</property>
+ <property name="mnemonic_widget">description_textview</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <property name="min_content_height">75</property>
<child>
- <object class="GtkLabel" id="description_label">
+ <object class="GtkViewport" id="viewport1">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xalign">1</property>
- <property name="yalign">0</property>
- <property name="label" translatable="yes">_Description</property>
- <property name="use_underline">True</property>
- <property name="wrap">True</property>
- <property name="mnemonic_widget">description_textview</property>
- <style>
- <class name="dim-label"/>
- </style>
- </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="GtkScrolledWindow" id="scrolledwindow1">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="shadow_type">in</property>
- <property name="min_content_height">75</property>
<child>
- <object class="GtkViewport" id="viewport1">
+ <object class="GtkTextView" id="description_textview">
<property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkTextView" id="description_textview">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="hexpand">True</property>
- <property name="vexpand">True</property>
- <property name="wrap_mode">word</property>
- </object>
- </child>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="wrap_mode">word</property>
</object>
</child>
</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>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="calendar_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Calendar</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
- <property name="top_attach">2</property>
- <property name="width">2</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
@@ -259,11 +233,12 @@
<object class="GtkComboBoxText" id="calendar_combo">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="halign">start</property>
</object>
<packing>
- <property name="left_attach">0</property>
- <property name="top_attach">3</property>
- <property name="width">2</property>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
@@ -280,7 +255,7 @@
</object>
<packing>
<property name="left_attach">0</property>
- <property name="top_attach">4</property>
+ <property name="top_attach">5</property>
<property name="width">2</property>
<property name="height">1</property>
</packing>
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
new file mode 100644
index 0000000..41a38cc
--- /dev/null
+++ b/src/rc/event-time-settings.ui
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.16.1 -->
+<interface>
+ <requires lib="gtk+" version="3.10"/>
+ <template class="CaliforniaHostEventTimeSettings" parent="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">8</property>
+ <child>
+ <object class="GtkLabel" id="summary_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">4</property>
+ <property name="label">(empty)</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <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="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="to_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">4</property>
+ <property name="margin_right">4</property>
+ <property name="label" translatable="yes" comments="As in "From 9pm to
10pm"">to</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="to_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="all_day_checkbutton">
+ <property name="label" translatable="yes">_All-day event</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">end</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="buttonbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="margin_top">8</property>
+ <property name="spacing">8</property>
+ <property name="homogeneous">True</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <signal name="clicked" handler="on_cancel_button_clicked"
object="CaliforniaHostEventTimeSettings" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="label" translatable="yes">_OK</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <signal name="clicked" handler="on_ok_button_clicked" object="CaliforniaHostEventTimeSettings"
swapped="no"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </template>
+</interface>
diff --git a/src/tests/tests-calendar-exact-time.vala b/src/tests/tests-calendar-exact-time.vala
new file mode 100644
index 0000000..7d4b9e5
--- /dev/null
+++ b/src/tests/tests-calendar-exact-time.vala
@@ -0,0 +1,63 @@
+/* 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.Tests {
+
+internal class CalendarExactTime : UnitTest.Harness {
+ private Calendar.ExactTime? now;
+ private Calendar.ExactTime? past;
+ private Calendar.ExactTime? future;
+
+ public CalendarExactTime() {
+ add_case("clamp-floor-unaltered", clamp_floor_unaltered);
+ add_case("clamp-floor-altered", clamp_floor_altered);
+ add_case("clamp-ceiling-unaltered", clamp_ceiling_unaltered);
+ add_case("clamp-ceiling-altered", clamp_ceiling_altered);
+ add_case("clamp-both-unaltered", clamp_both_unaltered);
+ add_case("clamp-both-altered", clamp_both_altered);
+ }
+
+ protected override void setup() throws Error {
+ Calendar.init();
+
+ now = Calendar.System.now;
+ past = now.adjust_time(-1, Calendar.TimeUnit.MINUTE);
+ future = now.adjust_time(1, Calendar.TimeUnit.MINUTE);
+ }
+
+ protected override void teardown() {
+ now = past = future = null;
+
+ Calendar.terminate();
+ }
+
+ private bool clamp_floor_unaltered() throws Error {
+ return now.clamp(past, null).equal_to(now);
+ }
+
+ private bool clamp_floor_altered() throws Error {
+ return now.clamp(future, null).equal_to(future);
+ }
+
+ private bool clamp_ceiling_unaltered() throws Error {
+ return now.clamp(null, future).equal_to(now);
+ }
+
+ private bool clamp_ceiling_altered() throws Error {
+ return now.clamp(null, past).equal_to(past);
+ }
+
+ private bool clamp_both_unaltered() throws Error {
+ return now.clamp(past, future).equal_to(now);
+ }
+
+ private bool clamp_both_altered() throws Error {
+ return now.clamp(past, past).equal_to(past);
+ }
+}
+
+}
+
diff --git a/src/tests/tests-calendar-wall-time.vala b/src/tests/tests-calendar-wall-time.vala
index ef974de..5844cfd 100644
--- a/src/tests/tests-calendar-wall-time.vala
+++ b/src/tests/tests-calendar-wall-time.vala
@@ -8,12 +8,17 @@ namespace California.Tests {
internal class CalendarWallTime : UnitTest.Harness {
public CalendarWallTime() {
- add_case("round-down-perverse", round_down_perverse);
- add_case("round-down-zero", round_down_zero);
+ add_case("round-zero", round_zero);
add_case("round-down-hour-no-change", round_down_hour_no_change);
add_case("round-down-hour-change", round_down_hour_change);
add_case("round-down-minute", round_down_minute);
add_case("round-down-second", round_down_second);
+ add_case("round-down-no-rollover", round_down_no_rollover);
+ add_case("round-up-hour-no-change", round_up_hour_no_change);
+ add_case("round-up-hour-change", round_up_hour_change);
+ add_case("round-up-minute", round_up_minute);
+ add_case("round-up-second", round_up_second);
+ add_case("round-up-rollover", round_up_rollover);
}
protected override void setup() throws Error {
@@ -24,46 +29,92 @@ internal class CalendarWallTime : UnitTest.Harness {
Calendar.terminate();
}
- private bool round_down_perverse() throws Error {
+ private bool round_zero() throws Error {
Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
- Calendar.WallTime round_down = wall_time.round_down(-1, Calendar.TimeUnit.MINUTE);
+ bool rollover;
+ Calendar.WallTime rounded = wall_time.round(0, Calendar.TimeUnit.HOUR, out rollover);
- return wall_time.equal_to(round_down);
- }
-
- private bool round_down_zero() throws Error {
- Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
- Calendar.WallTime round_down = wall_time.round_down(0, Calendar.TimeUnit.HOUR);
-
- return wall_time.equal_to(round_down);
+ return !rollover && wall_time.equal_to(rounded);
}
private bool round_down_hour_no_change() throws Error {
Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
- Calendar.WallTime round_down = wall_time.round_down(2, Calendar.TimeUnit.HOUR);
+ bool rollover;
+ Calendar.WallTime round_down = wall_time.round(-2, Calendar.TimeUnit.HOUR, out rollover);
- return round_down.hour == 10 && round_down.minute == 0 && round_down.second == 0;
+ return !rollover && round_down.hour == 10 && round_down.minute == 0 && round_down.second == 0;
}
private bool round_down_hour_change() throws Error {
Calendar.WallTime wall_time = new Calendar.WallTime(9, 12, 14);
- Calendar.WallTime round_down = wall_time.round_down(2, Calendar.TimeUnit.HOUR);
+ bool rollover;
+ Calendar.WallTime round_down = wall_time.round(-2, Calendar.TimeUnit.HOUR, out rollover);
- return round_down.hour == 8 && round_down.minute == 0 && round_down.second == 0;
+ return !rollover && round_down.hour == 8 && round_down.minute == 0 && round_down.second == 0;
}
private bool round_down_minute() throws Error {
Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
- Calendar.WallTime round_down = wall_time.round_down(10, Calendar.TimeUnit.MINUTE);
+ bool rollover;
+ Calendar.WallTime round_down = wall_time.round(-10, Calendar.TimeUnit.MINUTE, out rollover);
- return round_down.hour == 10 && round_down.minute == 10 && round_down.second == 0;
+ return !rollover && round_down.hour == 10 && round_down.minute == 10 && round_down.second == 0;
}
private bool round_down_second() throws Error {
Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 16);
- Calendar.WallTime round_down = wall_time.round_down(15, Calendar.TimeUnit.SECOND);
+ bool rollover;
+ Calendar.WallTime round_down = wall_time.round(-15, Calendar.TimeUnit.SECOND, out rollover);
+
+ return !rollover && round_down.hour == 10 && round_down.minute == 12 && round_down.second == 15;
+ }
+
+ private bool round_down_no_rollover() throws Error {
+ Calendar.WallTime wall_time = Calendar.WallTime.earliest;
+ bool rollover;
+ Calendar.WallTime round_down = wall_time.round(-15, Calendar.TimeUnit.SECOND, out rollover);
+
+ return !rollover && round_down.equal_to(wall_time);
+ }
+
+ private bool round_up_hour_no_change() throws Error {
+ Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
+ bool rollover;
+ Calendar.WallTime round_up = wall_time.round(2, Calendar.TimeUnit.HOUR, out rollover);
+
+ return !rollover && round_up.hour == 10 && round_up.minute == 0 && round_up.second == 0;
+ }
+
+ private bool round_up_hour_change() throws Error {
+ Calendar.WallTime wall_time = new Calendar.WallTime(9, 12, 14);
+ bool rollover;
+ Calendar.WallTime round_up = wall_time.round(2, Calendar.TimeUnit.HOUR, out rollover);
+
+ return !rollover && round_up.hour == 10 && round_up.minute == 0 && round_up.second == 0;
+ }
+
+ private bool round_up_minute() throws Error {
+ Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 14);
+ bool rollover;
+ Calendar.WallTime round_up = wall_time.round(10, Calendar.TimeUnit.MINUTE, out rollover);
+
+ return !rollover && round_up.hour == 10 && round_up.minute == 20 && round_up.second == 0;
+ }
+
+ private bool round_up_second() throws Error {
+ Calendar.WallTime wall_time = new Calendar.WallTime(10, 12, 16);
+ bool rollover;
+ Calendar.WallTime round_up = wall_time.round(15, Calendar.TimeUnit.SECOND, out rollover);
+
+ return !rollover && round_up.hour == 10 && round_up.minute == 12 && round_up.second == 30;
+ }
+
+ private bool round_up_rollover() throws Error {
+ Calendar.WallTime wall_time = new Calendar.WallTime(23, 55, 16);
+ bool rollover;
+ Calendar.WallTime round_up = wall_time.round(15, Calendar.TimeUnit.MINUTE, out rollover);
- return round_down.hour == 10 && round_down.minute == 12 && round_down.second == 15;
+ return rollover && round_up.hour == 0 && round_up.minute == 0 && round_up.second == 0;
}
}
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index 24c171e..11a3a47 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -17,6 +17,7 @@ public int run(string[] args) {
UnitTest.Harness.register(new CalendarMonthSpan());
UnitTest.Harness.register(new CalendarMonthOfYear());
UnitTest.Harness.register(new CalendarWallTime());
+ UnitTest.Harness.register(new CalendarExactTime());
UnitTest.Harness.register(new QuickAdd());
UnitTest.Harness.register(new QuickAddRecurring());
diff --git a/src/toolkit/toolkit-button-connector.vala b/src/toolkit/toolkit-button-connector.vala
index 0549f57..a91f943 100644
--- a/src/toolkit/toolkit-button-connector.vala
+++ b/src/toolkit/toolkit-button-connector.vala
@@ -20,46 +20,12 @@ namespace California.Toolkit {
*/
public class ButtonConnector : EventConnector {
- // GDK reports 250ms is used to determine if a click is a double-click (and another 250ms for
- // triple-click), so pause just a little more than that to determine if all the clicking is
- // done
- private const int CLICK_DETERMINATION_DELAY_MSEC = 255;
-
- // The actual ButtonEvent, with some useful functionality for release timeouts
- private class InternalButtonEvent : ButtonEvent {
- private Scheduled? scheduled_timeout = null;
-
- public signal void release_timeout();
-
- public InternalButtonEvent(Gtk.Widget widget, Gdk.EventButton event) {
- base (widget, event);
- }
-
- public override void update_press(Gtk.Widget widget, Gdk.EventButton press_event) {
- base.update_press(widget, press_event);
-
- if (scheduled_timeout != null)
- scheduled_timeout.cancel();
- }
-
- public override void update_release(Gtk.Widget widget, Gdk.EventButton release_event) {
- base.update_release(widget, release_event);
-
- scheduled_timeout = new Scheduled.once_after_msec(CLICK_DETERMINATION_DELAY_MSEC,
- on_timeout, Priority.LOW);
- }
-
- private void on_timeout() {
- release_timeout();
- }
- }
-
- private Gee.HashMap<Gtk.Widget, InternalButtonEvent> primary_states = new Gee.HashMap<
- Gtk.Widget, InternalButtonEvent>();
- private Gee.HashMap<Gtk.Widget, InternalButtonEvent> secondary_states = new Gee.HashMap<
- Gtk.Widget, InternalButtonEvent>();
- private Gee.HashMap<Gtk.Widget, InternalButtonEvent> tertiary_states = new Gee.HashMap<
- Gtk.Widget, InternalButtonEvent>();
+ private Gee.HashMap<Gtk.Widget, ButtonEvent> primary_states = new Gee.HashMap<
+ Gtk.Widget, ButtonEvent>();
+ private Gee.HashMap<Gtk.Widget, ButtonEvent> secondary_states = new Gee.HashMap<
+ Gtk.Widget, ButtonEvent>();
+ private Gee.HashMap<Gtk.Widget, ButtonEvent> tertiary_states = new Gee.HashMap<
+ Gtk.Widget, ButtonEvent>();
/**
* The "raw" "button-pressed" signal received by { link ButtonConnector}.
@@ -80,33 +46,26 @@ public class ButtonConnector : EventConnector {
/**
* Fired when a button is pressed and released once.
*
- * The "guaranteed" flag is important to distinguish here. If set, that indicates a timeout
- * has occurred and the user did not follow the click with a second or third. If not set,
- * this was fired immediately after the user released the button and it is unknown if the user
- * intends to follow it with more clicks.
- *
- * Because no timeout has occurred, unguaranteed clicks can be processed immediately if they
- * occur on a widget or location where double- and triple-clicks are meaningless.
- *
- * NOTE: This means "clicked" (and { link double_clicked} and { link triple_clicked} will be
- * fired ''twice'', once unguaranteed, once guaranteed. To prevent double-processing, handlers
- * should always check the flag.
+ * Note that this can be fired after { link double_clicked} and { link triple_clicked}. That
+ * indicates that the user double- or triple-clicked ''and'' the other signal handlers did not
+ * return { link Toolkit.STOP}, indicating the event was unhandled or unabsorbed by the signal
+ * handlers. If either returns STOP, "clicked" will not fire.
*/
- public signal void clicked(ButtonEvent details, bool guaranteed);
+ public signal bool clicked(ButtonEvent details);
/**
* Fired when a button is pressed and released twice in succession.
*
- * See { link clicked} for an explanation of the { link guaranteed} flag.
+ * See { link clicked} for an explanation of signal firing order.
*/
- public signal void double_clicked(ButtonEvent details, bool guaranteed);
+ public signal bool double_clicked(ButtonEvent details);
/**
* Fired when a button is pressed and released thrice in succession.
*
- * See { link clicked} for an explanation of the { link guaranteed} flag.
+ * See { link clicked} for an explanation of signal firing order.
*/
- public signal void triple_clicked(ButtonEvent details, bool guaranteed);
+ public signal bool triple_clicked(ButtonEvent details);
/**
* Create a new { link ButtonConnector} for monitoring (mouse) button events from Gtk.Widgets.
@@ -119,7 +78,7 @@ public class ButtonConnector : EventConnector {
* Subclasses may override this method to hook into this event before or after the signal
* has fired.
*
- * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
+ * @return { link STOP} or { link PROPAGATE}.
*/
protected virtual bool notify_pressed(Gtk.Widget widget, Button button, Gdk.Point point,
Gdk.EventType event_type) {
@@ -130,7 +89,7 @@ public class ButtonConnector : EventConnector {
* Subclasses may override this method to hook into this event before or after the signal
* has fired.
*
- * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
+ * @return { link STOP} or { link PROPAGATE}.
*/
protected virtual bool notify_released(Gtk.Widget widget, Button button, Gdk.Point point,
Gdk.EventType event_type) {
@@ -141,24 +100,24 @@ public class ButtonConnector : EventConnector {
* Subclasses may override this method to hook into this event before or after the signal
* has fired.
*/
- protected virtual void notify_clicked(ButtonEvent details, bool guaranteed) {
- clicked(details, guaranteed);
+ protected virtual bool notify_clicked(ButtonEvent details) {
+ return clicked(details);
}
/**
* Subclasses may override this method to hook into this event before or after the signal
* has fired.
*/
- protected virtual void notify_double_clicked(ButtonEvent details, bool guaranteed) {
- double_clicked(details, guaranteed);
+ protected virtual bool notify_double_clicked(ButtonEvent details) {
+ return double_clicked(details);
}
/**
* Subclasses may override this method to hook into this event before or after the signal
* has fired.
*/
- protected virtual void notify_triple_clicked(ButtonEvent details, bool guaranteed) {
- triple_clicked(details, guaranteed);
+ protected virtual bool notify_triple_clicked(ButtonEvent details) {
+ return triple_clicked(details);
}
protected override void connect_signals(Gtk.Widget widget) {
@@ -182,7 +141,7 @@ public class ButtonConnector : EventConnector {
tertiary_states.unset(widget);
}
- private Gee.HashMap<Gtk.Widget, InternalButtonEvent>? get_states_map(Button button) {
+ private Gee.HashMap<Gtk.Widget, ButtonEvent>? get_states_map(Button button) {
switch (button) {
case Button.PRIMARY:
return primary_states;
@@ -208,7 +167,7 @@ public class ButtonConnector : EventConnector {
}
private bool process_button_event(Gtk.Widget widget, Gdk.EventButton event,
- Button button, Gee.HashMap<Gtk.Widget, InternalButtonEvent>? button_states) {
+ Button button, Gee.HashMap<Gtk.Widget, ButtonEvent>? button_states) {
switch(event.type) {
case Gdk.EventType.BUTTON_PRESS:
case Gdk.EventType.2BUTTON_PRESS:
@@ -227,10 +186,9 @@ public class ButtonConnector : EventConnector {
// previous press (possible for multiple press events to arrive back-to-back
// when double- and triple-clicking)
if (button_states != null) {
- InternalButtonEvent? details = button_states.get(widget);
+ ButtonEvent? details = button_states.get(widget);
if (details == null) {
- details = new InternalButtonEvent(widget, event);
- details.release_timeout.connect(on_release_timeout);
+ details = new ButtonEvent(widget, event);
button_states.set(widget, details);
} else {
details.update_press(widget, event);
@@ -251,26 +209,11 @@ public class ButtonConnector : EventConnector {
// update saved state (if any) with release info and start timer
if (button_states != null) {
- InternalButtonEvent? details = button_states.get(widget);
+ ButtonEvent? details = button_states.get(widget);
if (details != null) {
- // fire "unguaranteed" clicked signals now (with button release) rather than
- // wait for timeout using the current value of press_type before the details
- // are updated
- switch (details.press_type) {
- case Gdk.EventType.BUTTON_PRESS:
- notify_clicked(details, false);
- break;
-
- case Gdk.EventType.2BUTTON_PRESS:
- notify_double_clicked(details, false);
- break;
-
- case Gdk.EventType.3BUTTON_PRESS:
- notify_triple_clicked(details, false);
- break;
- }
-
details.update_release(widget, event);
+
+ return synthesize_click(details);
}
}
break;
@@ -279,27 +222,38 @@ public class ButtonConnector : EventConnector {
return Toolkit.PROPAGATE;
}
- private void on_release_timeout(InternalButtonEvent details) {
- // release button timed-out, meaning it's time to evaluate where the sequence stands and
- // notify subscribers
+ private bool synthesize_click(ButtonEvent details) {
+ // use Accept flags to indicate which signal(s) to fire
+ bool result = Toolkit.PROPAGATE;
switch (details.press_type) {
case Gdk.EventType.BUTTON_PRESS:
- notify_clicked(details, true);
+ result = notify_clicked(details);
break;
case Gdk.EventType.2BUTTON_PRESS:
- notify_double_clicked(details, true);
+ result = notify_double_clicked(details);
+ if (result != Toolkit.STOP)
+ result = notify_clicked(details);
break;
case Gdk.EventType.3BUTTON_PRESS:
- notify_triple_clicked(details, true);
+ result = notify_triple_clicked(details);
+ if (result != Toolkit.STOP) {
+ result = notify_double_clicked(details);
+ if (result != Toolkit.STOP)
+ result = notify_clicked(details);
+ }
break;
}
- // drop state, now finished with it
- Gee.HashMap<Gtk.Widget, InternalButtonEvent>? states_map = get_states_map(details.button);
- if (states_map != null)
- states_map.unset(details.widget);
+ if (result == Toolkit.STOP) {
+ // drop event entirely
+ Gee.HashMap<Gtk.Widget, ButtonEvent>? states_map = get_states_map(details.button);
+ if (states_map != null)
+ states_map.unset(details.widget);
+ }
+
+ return result;
}
}
diff --git a/src/toolkit/toolkit-button-event.vala b/src/toolkit/toolkit-button-event.vala
index 775267e..25319a0 100644
--- a/src/toolkit/toolkit-button-event.vala
+++ b/src/toolkit/toolkit-button-event.vala
@@ -106,7 +106,7 @@ public class ButtonEvent : BaseObject {
}
// Update state with the next button press
- internal virtual void update_press(Gtk.Widget widget, Gdk.EventButton press_event) {
+ internal void update_press(Gtk.Widget widget, Gdk.EventButton press_event) {
assert(this.widget == widget);
assert(Button.from_button_event(press_event) == button);
@@ -116,7 +116,7 @@ public class ButtonEvent : BaseObject {
}
// Update state with the next button release and start the release timer
- internal virtual void update_release(Gtk.Widget widget, Gdk.EventButton release_event) {
+ internal void update_release(Gtk.Widget widget, Gdk.EventButton release_event) {
assert(this.widget == widget);
assert(Button.from_button_event(release_event) == button);
diff --git a/src/toolkit/toolkit-card.vala b/src/toolkit/toolkit-card.vala
index 8e2daee..7450aea 100644
--- a/src/toolkit/toolkit-card.vala
+++ b/src/toolkit/toolkit-card.vala
@@ -163,7 +163,6 @@ public interface Card : Gtk.Widget {
* This is called before dealing with { link default_widget} and { link initial_focus}, so
* changes to those properties in this call, if need be.
*/
- // TODO: Use a JumpContext object instead.
public abstract void jumped_to(Card? from, Jump reason, Value? message);
/**
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;
+ }
+}
+
+}
+
diff --git a/src/view/week/week-day-pane.vala b/src/view/week/week-day-pane.vala
index 5090355..cf3a558 100644
--- a/src/view/week/week-day-pane.vala
+++ b/src/view/week/week-day-pane.vala
@@ -152,7 +152,7 @@ internal class DayPane : Pane, Common.InstanceContainer {
public void update_selection(Calendar.WallTime wall_time) {
// round down to the nearest 15-minute mark
- Calendar.WallTime rounded_time = wall_time.round_down(15, Calendar.TimeUnit.MINUTE);
+ Calendar.WallTime rounded_time = wall_time.round(-15, Calendar.TimeUnit.MINUTE, null);
// assign start first, end second (ordering doesn't matter, possible to select upwards)
if (selection_start == null) {
diff --git a/src/view/week/week-grid.vala b/src/view/week/week-grid.vala
index 1920b6c..6556a02 100644
--- a/src/view/week/week-grid.vala
+++ b/src/view/week/week-grid.vala
@@ -318,35 +318,35 @@ internal class Grid : Gtk.Box {
return date_to_all_day.get(cell_date);
}
- private void on_instance_container_clicked(Toolkit.ButtonEvent details, bool guaranteed) {
- // only interested in unguaranteed clicks on the primary mouse button
- if (details.button != Toolkit.Button.PRIMARY || guaranteed)
- return;
+ private bool on_instance_container_clicked(Toolkit.ButtonEvent details) {
+ if (details.button != Toolkit.Button.PRIMARY)
+ return Toolkit.PROPAGATE;
Common.InstanceContainer instance_container = (Common.InstanceContainer) details.widget;
Component.Event? event = instance_container.get_event_at(details.press_point);
if (event != null)
owner.request_display_event(event, instance_container, details.press_point);
+
+ return Toolkit.STOP;
}
- private void on_instance_container_double_clicked(Toolkit.ButtonEvent details, bool guaranteed) {
- // only interested in unguaranteed double-clicks on the primary mouse button
- if (details.button != Toolkit.Button.PRIMARY || guaranteed)
- return;
+ private bool on_instance_container_double_clicked(Toolkit.ButtonEvent details) {
+ if (details.button != Toolkit.Button.PRIMARY)
+ return Toolkit.PROPAGATE;
Common.InstanceContainer instance_container = (Common.InstanceContainer) details.widget;
// if an event is at this location, don't process
if (instance_container.get_event_at(details.press_point) != null)
- return;
+ return Toolkit.PROPAGATE;
// if a DayPane, use double-click to determine rounded time of the event's start
DayPane? day_pane = instance_container as DayPane;
if (day_pane != null) {
// convert click into starting time on the day pane rounded down to the nearest half-hour
- Calendar.WallTime wall_time = day_pane.get_wall_time(details.press_point.y).round_down(
- 30, Calendar.TimeUnit.MINUTE);
+ Calendar.WallTime wall_time = day_pane.get_wall_time(details.press_point.y).round(-30,
+ Calendar.TimeUnit.MINUTE, null);
Calendar.ExactTime start_time = new Calendar.ExactTime(Calendar.Timezone.local,
day_pane.date, wall_time);
@@ -355,12 +355,14 @@ internal class Grid : Gtk.Box {
new Calendar.ExactTimeSpan(start_time, start_time.adjust_time(1, Calendar.TimeUnit.HOUR)),
day_pane, details.press_point);
- return;
+ return Toolkit.STOP;
}
// otherwise, an all-day-cell, so request an all-day event
owner.request_create_all_day_event(instance_container.contained_span, instance_container,
details.press_point);
+
+ return Toolkit.STOP;
}
private void on_day_pane_motion(Toolkit.MotionEvent details) {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]