[california/wip/725767-week] Display all-day events in top of week grid
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725767-week] Display all-day events in top of week grid
- Date: Thu, 22 May 2014 02:52:00 +0000 (UTC)
commit 3484831bc2b3d49ee533051c6d32efb1b435b2ae
Author: Jim Nelson <jim yorba org>
Date: Wed May 21 19:51:45 2014 -0700
Display all-day events in top of week grid
src/Makefile.am | 4 +
src/calendar/calendar-week.vala | 2 +-
src/component/component-event.vala | 9 +
src/view/common/common-events-cell.vala | 615 +++++++++++++++++++++++++++++++
src/view/common/common.vala | 31 ++
src/view/month/month-cell.vala | 544 ++--------------------------
src/view/month/month-grid.vala | 11 +-
src/view/month/month.vala | 2 +
src/view/view.vala | 2 +
src/view/week/week-all-day-cell.vala | 73 ++++
src/view/week/week-day-pane.vala | 12 +-
src/view/week/week-grid.vala | 106 +++++-
src/view/week/week.vala | 2 +
13 files changed, 864 insertions(+), 549 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 3728f67..6f66f9f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -133,12 +133,16 @@ california_VALASOURCES = \
view/view-controllable.vala \
view/view-palette.vala \
\
+ view/common/common.vala \
+ view/common/common-events-cell.vala \
+ \
view/month/month.vala \
view/month/month-cell.vala \
view/month/month-controller.vala \
view/month/month-grid.vala \
\
view/week/week.vala \
+ view/week/week-all-day-cell.vala \
view/week/week-controller.vala \
view/week/week-day-pane.vala \
view/week/week-grid.vala \
diff --git a/src/calendar/calendar-week.vala b/src/calendar/calendar-week.vala
index b52d83d..2ff35a0 100644
--- a/src/calendar/calendar-week.vala
+++ b/src/calendar/calendar-week.vala
@@ -101,7 +101,7 @@ public class Week : Unit<Week>, Gee.Comparable<Week>, Gee.Hashable<Week> {
}
public override string to_string() {
- return "week %d of %s (%s)".printf(week_of_year, month_of_year.to_string(), base.to_string());
+ return "week %d of %s (%s)".printf(week_of_year, month_of_year.to_string(),
to_date_span().to_string());
}
}
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index a2fc8c2..adba976 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -63,6 +63,15 @@ public class Event : Instance, Gee.Comparable<Event> {
public bool is_all_day { get; private set; }
/**
+ * Convenience property for determining if { link Event} spans one or more full days.
+ */
+ public bool is_day_spanning {
+ get {
+ return is_all_day || exact_time_span.duration.days >= 1;
+ }
+ }
+
+ /**
* Location of an { link Event}.
*/
public string? location { get; set; default = null; }
diff --git a/src/view/common/common-events-cell.vala b/src/view/common/common-events-cell.vala
new file mode 100644
index 0000000..45dddcc
--- /dev/null
+++ b/src/view/common/common-events-cell.vala
@@ -0,0 +1,615 @@
+/* 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.View.Common {
+
+/**
+ * A (generally) square cell which displays { link Component.Event}s, one per line, with brief
+ * time information and summary and a capped bar for all-day or day-spanning events.
+ */
+
+internal abstract class EventsCell : Gtk.EventBox {
+ public const string PROP_DATE = "date";
+ public const string PROP_NEIGHBORS = "neighbors";
+ public const string PROP_TOP_LINE = "top-line";
+ public const string PROP_TOP_LINE_RGBA = "top-line-rgba";
+ public const string PROP_SELECTED = "selected";
+
+ private const double ROUNDED_CAP_RADIUS = 5.0;
+ private const int POINTED_CAP_WIDTH_PX = 6;
+
+ private const double DEGREES = Math.PI / 180.0;
+
+ private const string KEY_TOOLTIP = "california-events-cell-tooltip";
+
+ private const Calendar.WallTime.PrettyFlag PRETTY_TIME_FLAGS =
+ Calendar.WallTime.PrettyFlag.OPTIONAL_MINUTES
+ | Calendar.WallTime.PrettyFlag.BRIEF_MERIDIEM;
+
+ private enum CapEffect {
+ NONE,
+ BLOCKED,
+ ROUNDED,
+ POINTED
+ }
+
+ /**
+ * The { link Calendar.Date} this { link EventsCell} is displaying.
+ */
+ public Calendar.Date date { get; private set; }
+
+ /**
+ * The horizontal neighbors for this { link EventsCell}.
+ *
+ * Since cells are designed to be displayed horizontally (say, 7 per week), each cell needs
+ * to know the { link Calendar.Date}s of its neighbors so they can arrange line numbers when
+ * displaying all-day and day-spanning events.
+ */
+ public Calendar.DateSpan neighbors { get; private set; }
+
+ /**
+ * Top line (title or summary) text, drawn in { link Palette.normal_font}.
+ *
+ * Set to empty string if space should be reserved but blank, null if not used and class may
+ * use space to draw events.
+ */
+ public string? top_line_text { get; set; default = null; }
+
+ /**
+ * Color of { link top_line_text}.
+ */
+ public Gdk.RGBA top_line_rgba { get; set; }
+
+ // to avoid lots of redraws, only queue_draw() if set changes value
+ private bool _selected = false;
+ public bool selected {
+ get {
+ return _selected;
+ }
+
+ set {
+ if (_selected != value)
+ queue_draw();
+
+ _selected = value;
+ }
+ }
+
+ private Gee.TreeSet<Component.Event> sorted_events = new
Gee.TreeSet<Component.Event>(all_day_comparator);
+ private Gee.HashMap<int, Component.Event> line_to_event = new Gee.HashMap<int, Component.Event>();
+
+ private Gtk.DrawingArea canvas = new Gtk.DrawingArea();
+
+ public EventsCell(Calendar.Date date, Calendar.DateSpan neighbors) {
+ assert(date in neighbors);
+
+ this.date = date;
+ this.neighbors = neighbors;
+ top_line_rgba = Palette.instance.day_in_range;
+
+ // see query_tooltip() for implementation
+ has_tooltip = true;
+
+ // wrap the EventBox around the DrawingArea, which is the real widget of interest for this
+ // class
+ add(canvas);
+
+ notify[PROP_TOP_LINE].connect(queue_draw);
+ notify[PROP_TOP_LINE_RGBA].connect(queue_draw);
+
+ Palette.instance.palette_changed.connect(queue_draw);
+ Calendar.System.instance.is_24hr_changed.connect(on_24hr_changed);
+ Calendar.System.instance.today_changed.connect(on_today_changed);
+
+ canvas.draw.connect(on_draw);
+ }
+
+ ~EventsCell() {
+ Palette.instance.palette_changed.disconnect(queue_draw);
+ Calendar.System.instance.is_24hr_changed.disconnect(on_24hr_changed);
+ Calendar.System.instance.today_changed.disconnect(on_today_changed);
+ }
+
+ /**
+ * Subclasses must provide a translation of a { link Calendar.Date} into a { link EventsCell}
+ * adjoining this one (in whatever container they're associated with).
+ *
+ * This allows for EventCells to communicate with each other to arrange line numbering for
+ * all-day and day-spanning events.
+ */
+ protected abstract EventsCell? get_cell_for_date(Calendar.Date cell_date);
+
+ // this comparator uses the standard Event comparator with one exception: if both Events require
+ // solid span lines, it sorts the one(s) with the furthest out end dates to the top, to ensure
+ // they are at the top of the drawn lines and prevent gaps and skips in the connected bars
+ private static int all_day_comparator(Component.Event a, Component.Event b) {
+ if (a == b)
+ return 0;
+
+ if (!a.is_day_spanning && !b.is_day_spanning)
+ return a.compare_to(b);
+
+ Calendar.DateSpan a_span = a.get_event_date_span(Calendar.Timezone.local);
+ Calendar.DateSpan b_span = b.get_event_date_span(Calendar.Timezone.local);
+
+ int compare = a_span.start_date.compare_to(b_span.start_date);
+ if (compare != 0)
+ return compare;
+
+ compare = b_span.end_date.compare_to(a_span.end_date);
+ if (compare != 0)
+ return compare;
+
+ // to stabilize
+ return a.compare_to(b);
+ }
+
+ /**
+ * Returns true if the point at x,y is within the { link Cell}'s width and height.
+ */
+ public bool is_hit(int x, int y) {
+ return x >= 0 && x < get_allocated_width() && y >= 0 && y < get_allocated_height();
+ }
+
+ /**
+ * Returns the assigned line number for the event, -1 if not found in { link Cell}.
+ */
+ public int get_line_for_event(Component.Event event) {
+ Gee.MapIterator<int, Component.Event> iter = line_to_event.map_iterator();
+ while (iter.next()) {
+ if (iter.get_value().equal_to(event))
+ return iter.get_key();
+ }
+
+ return -1;
+ }
+
+ public void change_date_and_neighbors(Calendar.Date date, Calendar.DateSpan neighbors) {
+ assert(date in neighbors);
+
+ if (!date.equal_to(this.date)) {
+ this.date = date;
+
+ // stored events are now bogus
+ clear();
+ queue_draw();
+ }
+
+ if (!neighbors.equal_to(this.neighbors)) {
+ this.neighbors = neighbors;
+
+ // need to reassign line numbers, as they depend on neighbors
+ assign_line_numbers();
+ queue_draw();
+ }
+ }
+
+ public void clear() {
+ line_to_event.clear();
+
+ foreach (Component.Event event in sorted_events.to_array())
+ internal_remove_event(event);
+
+ queue_draw();
+ }
+
+ public void add_event(Component.Event event) {
+ if (!sorted_events.add(event))
+ return;
+
+ // subscribe to interesting mutable properties
+ event.notify[Component.Event.PROP_SUMMARY].connect(queue_draw);
+ event.notify[Component.Event.PROP_DATE_SPAN].connect(on_span_updated);
+ event.notify[Component.Event.PROP_EXACT_TIME_SPAN].connect(on_span_updated);
+
+ assign_line_numbers();
+
+ queue_draw();
+ }
+
+ private bool internal_remove_event(Component.Event event) {
+ if (!sorted_events.remove(event))
+ return false;
+
+ event.notify[Component.Event.PROP_SUMMARY].disconnect(queue_draw);
+ event.notify[Component.Event.PROP_DATE_SPAN].disconnect(on_span_updated);
+ event.notify[Component.Event.PROP_EXACT_TIME_SPAN].disconnect(on_span_updated);
+
+ return true;
+ }
+
+ public void remove_event(Component.Event event) {
+ if (!internal_remove_event(event))
+ return;
+
+ assign_line_numbers();
+
+ queue_draw();
+ }
+
+ /**
+ * To be called by the owning widget when a calendar's visibility has changed.
+ *
+ * This causes event line numbers to be reassigned and thie { link Cell} redrawn, if the
+ * calendar in question has any events in this date.
+ */
+ public void notify_calendar_visibility_changed(Backing.CalendarSource calendar_source) {
+ if (!traverse<Component.Event>(sorted_events).any((event) => event.calendar_source ==
calendar_source))
+ return;
+
+ // found one
+ assign_line_numbers();
+ queue_draw();
+ }
+
+ // Called internally by other Cells when (a) they're in charge of assigning a multi-day event
+ // its line number for the week and (b) that line number has changed.
+ private void notify_assigned_line_number_changed(Gee.Collection<Component.Event> events) {
+ if (!traverse<Component.Event>(sorted_events).contains_any(events))
+ return;
+
+ assign_line_numbers();
+ queue_draw();
+ }
+
+ // each event gets a line of the cell to draw in; this clears all assigned line numbers and
+ // re-assigns from the sorted set of events, making sure holes are filled where possible ...
+ // if an event starts in this cell or this cell is the first day of a week an event is in,
+ // this cell is responsible for assigning a line number to it, which the other cells of the
+ // same week will honor (so a continuous line can be drawn)
+ private void assign_line_numbers() {
+ Gee.HashMap<int, Component.Event> old_line_to_event = line_to_event;
+ line_to_event = new Gee.HashMap<int, Component.Event>();
+
+ // track each event whose line number this cell is responsible for assigning that gets
+ // reassigned because of this
+ Gee.ArrayList<Component.Event> reassigned = new Gee.ArrayList<Component.Event>();
+
+ foreach (Component.Event event in sorted_events) {
+ if (!event.calendar_source.visible)
+ continue;
+
+ bool notify_reassigned = false;
+ if (event.is_day_spanning) {
+ // get the first day of this week the event exists in ... if not the current cell's
+ // date, get the assigned line number from the first day of this week the event
+ // exists in
+ Calendar.Date first_date = get_event_first_day_in_neighbors(event);
+ if (!date.equal_to(first_date)) {
+ int event_line = -1;
+ EventsCell? cell = get_cell_for_date(first_date);
+ if (cell != null)
+ event_line = cell.get_line_for_event(event);
+
+ if (event_line >= 0) {
+ assign_line_number(event_line, event);
+
+ continue;
+ }
+ } else {
+ // only worried about multi-day events being reassigned, as that's what effects
+ // other cells (i.e. when notifying of reassignment)
+ notify_reassigned = event.get_event_date_span(Calendar.Timezone.local).duration.days > 1;
+ }
+ } else if (!event.is_all_day) {
+ // if timed event is in this date but started elsewhere, don't display (unless it
+ // requires a span, above)
+ Calendar.Date start_date = new Calendar.Date.from_exact_time(
+ event.exact_time_span.start_exact_time.to_timezone(Calendar.Timezone.local));
+ if (!start_date.equal_to(date))
+ continue;
+ }
+
+ // otherwise, a timed event, a single-day event, or a multi-day event which starts here,
+ // so assign
+ int assigned = assign_line_number(-1, event);
+
+ // if this cell assigns the line number and the event is not new and the number has changed,
+ // inform all the other cells following this day's in the current week
+ if (notify_reassigned && old_line_to_event.values.contains(event) &&
old_line_to_event.get(assigned) != event)
+ reassigned.add(event);
+ }
+
+ if (reassigned.size > 0) {
+ // only need to tell cells following this day's neighbors about the reassignment
+ Calendar.DateSpan span = new Calendar.DateSpan(date.next(), neighbors.end_date).clamp_between(
+ neighbors);
+
+ foreach (Calendar.Date span_date in span) {
+ EventsCell? cell = get_cell_for_date(span_date);
+ if (cell != null && cell != this)
+ cell.notify_assigned_line_number_changed(reassigned);
+ }
+ }
+ }
+
+ private int assign_line_number(int force_line_number, Component.Event event) {
+ // kinda dumb, but this prevents holes appearing in lines where, due to the shape of the
+ // all-day events, could be filled
+ int line_number = 0;
+ if (force_line_number < 0) {
+ while (line_to_event.has_key(line_number))
+ line_number++;
+ } else {
+ line_number = force_line_number;
+ }
+
+ line_to_event.set(line_number, event);
+
+ return line_number;
+ }
+
+ public bool has_events() {
+ return sorted_events.size > 0;
+ }
+
+ private void on_24hr_changed() {
+ if (has_events())
+ queue_draw();
+ }
+
+ private void on_today_changed(Calendar.Date old_today, Calendar.Date new_today) {
+ // need to know re: redrawing background color to indicate current day
+ if (date.equal_to(old_today) || date.equal_to(new_today))
+ queue_draw();
+ }
+
+ private void on_span_updated(Object object, ParamSpec param) {
+ Component.Event event = (Component.Event) object;
+
+ // remove from cell if no longer in this day, otherwise remove and add again to sorted_events
+ // to re-sort
+ if (!(date in event.get_event_date_span(Calendar.Timezone.local))) {
+ remove_event(event);
+ } else if (sorted_events.remove(event)) {
+ sorted_events.add(event);
+ assign_line_numbers();
+ }
+
+ queue_draw();
+ }
+
+ public override bool query_tooltip(int x, int y, bool keyboard_mode, Gtk.Tooltip tooltip) {
+ Component.Event? event = get_event_at(Gdk.Point() { x = x, y = y });
+ if (event == null)
+ return false;
+
+ string? tooltip_text = event.get_data<string?>(KEY_TOOLTIP);
+ if (String.is_empty(tooltip_text))
+ return false;
+
+ tooltip.set_text(tooltip_text);
+
+ return true;
+ }
+
+ // Returns the first day of this cell's neighbors that the event is in ... this could be
+ // the event's starting day or the first day of this week (i.e. Monday or Sunday), depending
+ // on the definition of neighbors
+ private Calendar.Date get_event_first_day_in_neighbors(Component.Event event) {
+ // Remember: event start date may be before the date of any of this cell's neighbors
+ Calendar.Date event_start_date = event.get_event_date_span(Calendar.Timezone.local).start_date;
+
+ return (event_start_date in neighbors) ? event_start_date : neighbors.start_date;
+ }
+
+ /**
+ * Override to draw borders at the right time in the layering.
+ *
+ * This keeps solid all-day bars on top of the borders, achieving an effect of continuation.
+ */
+ protected virtual void draw_borders(Cairo.Context ctx) {
+ }
+
+ /**
+ * Can be overridden by children to achieve painters' strategy effects while drawing.
+ */
+ protected virtual bool on_draw(Cairo.Context ctx) {
+ // save and restore context so subclasses don't have to deal with it
+ ctx.save();
+
+ // shade background of cell for selection or if today
+ if (selected) {
+ Gdk.cairo_set_source_rgba(ctx, Palette.instance.selection);
+ ctx.paint();
+ } else if (date.equal_to(Calendar.System.today)) {
+ Gdk.cairo_set_source_rgba(ctx, Palette.instance.current_day);
+ ctx.paint();
+ }
+
+ // draw borders now, before everything else (but after background color)
+ ctx.save();
+ draw_borders(ctx);
+ ctx.restore();
+
+ if (top_line_text != null)
+ draw_line_of_text(ctx, -1, top_line_rgba, top_line_text, CapEffect.NONE, CapEffect.NONE);
+
+ // walk the assigned line numbers for each event and draw
+ Gee.MapIterator<int, Component.Event> iter = line_to_event.map_iterator();
+ while (iter.next()) {
+ Component.Event event = iter.get_value();
+ Calendar.DateSpan date_span = event.get_event_date_span(Calendar.Timezone.local);
+
+ bool display_text = true;
+ if (event.is_day_spanning) {
+ // only show the title if (a) the first day of an all-day event or (b) this is the
+ // first day of a contiguous span of a multi-day event. (b) handles the contingency of a
+ // multi-day event starting in a previous week prior to the top of the current view
+ display_text = date_span.start_date.equal_to(date)
+ || neighbors.start_date.equal_to(date);
+ }
+
+ string text;
+ if (display_text) {
+ if (event.is_all_day) {
+ text = event.summary;
+ } else {
+ Calendar.ExactTime local_start = event.exact_time_span.start_exact_time.to_timezone(
+ Calendar.Timezone.local);
+ text = "%s %s".printf(local_start.to_pretty_time_string(PRETTY_TIME_FLAGS),
event.summary);
+ }
+ } else {
+ text = "";
+ }
+
+ // use caps on both ends of all-day events depending whether this is the start, end,
+ // or start/end of week of continuing event
+ CapEffect left_effect = CapEffect.NONE;
+ CapEffect right_effect = CapEffect.NONE;
+ if (event.is_day_spanning) {
+ if (date_span.start_date.equal_to(date))
+ left_effect = CapEffect.ROUNDED;
+ else if (neighbors.start_date.equal_to(date))
+ left_effect = CapEffect.POINTED;
+ else
+ left_effect = CapEffect.BLOCKED;
+
+ if (date_span.end_date.equal_to(date))
+ right_effect = CapEffect.ROUNDED;
+ else if (neighbors.end_date.equal_to(date))
+ right_effect = CapEffect.POINTED;
+ else
+ right_effect = CapEffect.BLOCKED;
+ }
+
+ Pango.Layout layout = draw_line_of_text(ctx, iter.get_key(),
event.calendar_source.color_as_rgba(),
+ text, left_effect, right_effect);
+ event.set_data<string?>(KEY_TOOLTIP, layout.is_ellipsized() ? text : null);
+ }
+
+ ctx.restore();
+
+ return true;
+ }
+
+ // Returns top y position of line; negative line numbers are treated as top line
+ // The number is currently not clamped to the height of the widget.
+ private int get_line_top_y(int line_number) {
+ int y;
+ if (line_number < 0) {
+ // if no top line, line_number < 0 is bogus
+ y = (top_line_text != null) ? Palette.TEXT_MARGIN_PX : 0;
+ } else {
+ y = Palette.TEXT_MARGIN_PX;
+
+ // starting y of top line
+ if (top_line_text != null)
+ y += Palette.instance.normal_font_height_px + Palette.LINE_PADDING_PX;
+
+ // add additional lines
+ y += line_number * (Palette.instance.small_font_height_px + Palette.LINE_PADDING_PX);
+ }
+
+ return y;
+ }
+
+ // If line number is negative, the top line is drawn; otherwise, zero-based line numbers get
+ // "regular" treatment
+ private Pango.Layout draw_line_of_text(Cairo.Context ctx, int line_number, Gdk.RGBA rgba,
+ string text, CapEffect left_effect, CapEffect right_effect) {
+ bool is_reversed = (left_effect != CapEffect.NONE || right_effect != CapEffect.NONE);
+
+ int left = 0;
+ int right = get_allocated_width();
+ int top = get_line_top_y(line_number);
+ int bottom = top + Palette.instance.small_font_height_px;
+
+ // use event color for text unless reversed, where it becomes the background color
+ Gdk.cairo_set_source_rgba(ctx, rgba);
+ if (is_reversed) {
+ // draw background rectangle in spec'd color with text in white
+ switch (right_effect) {
+ case CapEffect.ROUNDED:
+ ctx.new_sub_path();
+ // sub 2 to avoid touching right calendar line
+ ctx.arc(right - 2 - ROUNDED_CAP_RADIUS, top + ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
+ -90.0 * DEGREES, 0 * DEGREES);
+ ctx.arc(right - 2 - ROUNDED_CAP_RADIUS, bottom - ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
+ 0 * DEGREES, 90.0 * DEGREES);
+ break;
+
+ case CapEffect.POINTED:
+ ctx.move_to(right - POINTED_CAP_WIDTH_PX, top);
+ ctx.line_to(right, top + (Palette.instance.small_font_height_px / 2));
+ ctx.line_to(right - POINTED_CAP_WIDTH_PX, bottom);
+ break;
+
+ case CapEffect.BLOCKED:
+ default:
+ ctx.move_to(right, top);
+ ctx.line_to(right, bottom);
+ break;
+ }
+
+ switch (left_effect) {
+ case CapEffect.ROUNDED:
+ // add one to avoid touching cell to the left's right calendar line
+ ctx.arc(left + 1 + ROUNDED_CAP_RADIUS, bottom - ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
+ 90.0 * DEGREES, 180.0 * DEGREES);
+ ctx.arc(left + 1 + ROUNDED_CAP_RADIUS, top + ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
+ 180.0 * DEGREES, 270.0 * DEGREES);
+ break;
+
+ case CapEffect.POINTED:
+ ctx.line_to(left + POINTED_CAP_WIDTH_PX, bottom);
+ ctx.line_to(left, top + (Palette.instance.small_font_height_px / 2));
+ ctx.line_to(left + POINTED_CAP_WIDTH_PX, top);
+ break;
+
+ case CapEffect.BLOCKED:
+ default:
+ ctx.line_to(left, bottom);
+ ctx.line_to(left, top);
+ break;
+ }
+
+ // fill with event color
+ ctx.fill_preserve();
+
+ // close path from last point (deals with capped and uncapped ends) and paint
+ ctx.close_path();
+ ctx.stroke ();
+
+ // set to white for text
+ Gdk.cairo_set_source_rgba(ctx, Gdk.RGBA() { red = 1.0, green = 1.0, blue = 1.0, alpha = 1.0 });
+ }
+
+ // add a couple of pixels to the text margins if capped
+ int left_text_margin = Palette.TEXT_MARGIN_PX + (left_effect != CapEffect.NONE ? 3 : 0);
+ int right_text_margin = Palette.TEXT_MARGIN_PX + (right_effect != CapEffect.NONE ? 3 : 0);
+
+ Pango.Layout layout = create_pango_layout(text);
+ layout.set_font_description((line_number < 0)
+ ? Palette.instance.normal_font
+ : Palette.instance.small_font);
+ layout.set_ellipsize(Pango.EllipsizeMode.END);
+ layout.set_width((right - left - left_text_margin - right_text_margin) * Pango.SCALE);
+
+ ctx.move_to(left_text_margin, top);
+ Pango.cairo_show_layout(ctx, layout);
+
+ return layout;
+ }
+
+ /**
+ * Returns a hit result for { link Component.Event}, if hit at all.
+ *
+ * The Gdk.Point must be relative to the widget's coordinate system.
+ */
+ public Component.Event? get_event_at(Gdk.Point point) {
+ for (int line_number = 0; line_number < line_to_event.size; line_number++) {
+ int y = get_line_top_y(line_number);
+ if (point.y >= y && point.y < (y + Palette.instance.small_font_height_px))
+ return line_to_event.get(line_number);
+ }
+
+ return null;
+ }
+}
+
+}
+
diff --git a/src/view/common/common.vala b/src/view/common/common.vala
new file mode 100644
index 0000000..0ae0b55
--- /dev/null
+++ b/src/view/common/common.vala
@@ -0,0 +1,31 @@
+/* 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.View.Common {
+
+private int init_count = 0;
+
+public void init() throws Error {
+ if (!Unit.do_init(ref init_count))
+ return;
+
+ // unit initialization
+ Calendar.init();
+ Component.init();
+ Toolkit.init();
+}
+
+public void terminate() {
+ if (!Unit.do_terminate(ref init_count))
+ return;
+
+ Toolkit.terminate();
+ Component.terminate();
+ Calendar.terminate();
+}
+
+}
+
diff --git a/src/view/month/month-cell.vala b/src/view/month/month-cell.vala
index aa3b391..ed41744 100644
--- a/src/view/month/month-cell.vala
+++ b/src/view/month/month-cell.vala
@@ -7,370 +7,43 @@
namespace California.View.Month {
/**
- * A single cell within a { link MonthGrid}.
+ * A square cell in the { link Month.Grid} displaying events.
+ *
+ * @see View.Common.EventsCell
*/
-private class Cell : Gtk.EventBox {
- private const double ROUNDED_CAP_RADIUS = 5.0;
- private const int POINTED_CAP_WIDTH_PX = 6;
-
- private const double DEGREES = Math.PI / 180.0;
-
- private const string KEY_TOOLTIP = "california-view-month-cell-tooltip";
-
- private const Calendar.WallTime.PrettyFlag PRETTY_TIME_FLAGS =
- Calendar.WallTime.PrettyFlag.OPTIONAL_MINUTES
- | Calendar.WallTime.PrettyFlag.BRIEF_MERIDIEM;
-
- private enum CapEffect {
- NONE,
- BLOCKED,
- ROUNDED,
- POINTED
- }
-
+internal class Cell : Common.EventsCell {
public weak Grid owner { get; private set; }
public int row { get; private set; }
public int col { get; private set; }
- // to avoid lots of redraws, only queue_draw() if set changes value
- private Calendar.Date? _date = null;
- public Calendar.Date? date {
- get {
- return _date;
- }
-
- set {
- if ((_date == null || value == null) && _date != value)
- queue_draw();
- else if (_date != null && value != null && !_date.equal_to(value))
- queue_draw();
-
- _date = value;
- }
- }
-
- // to avoid lots of redraws, only queue_draw() if set changes value
- private bool _selected = false;
- public bool selected {
- get {
- return _selected;
- }
+ public Cell(Grid owner, Calendar.Date date, int row, int col) {
+ base (date, date.week_of(owner.first_of_week).to_date_span());
- set {
- if (_selected != value)
- queue_draw();
-
- _selected = value;
- }
- }
-
- private Gee.TreeSet<Component.Event> sorted_events = new
Gee.TreeSet<Component.Event>(all_day_comparator);
- private Gee.HashMap<int, Component.Event> line_to_event = new Gee.HashMap<int, Component.Event>();
-
- private Gtk.DrawingArea canvas = new Gtk.DrawingArea();
-
- public Cell(Grid owner, int row, int col) {
this.owner = owner;
this.row = row;
this.col = col;
- // see query_tooltip() for implementation
- has_tooltip = true;
+ notify[PROP_DATE].connect(update_top_line);
- // wrap the EventBox around the DrawingArea, which is the real widget of interest for this
- // class
- add(canvas);
+ owner.notify[Grid.PROP_FIRST_OF_WEEK].connect(on_first_of_week_changed);
- notify["date"].connect(queue_draw);
- notify["selected"].connect(queue_draw);
- Palette.instance.palette_changed.connect(queue_draw);
- Calendar.System.instance.is_24hr_changed.connect(on_24hr_changed);
- Calendar.System.instance.today_changed.connect(on_today_changed);
-
- canvas.draw.connect(on_draw);
+ update_top_line();
}
~Cell() {
- Palette.instance.palette_changed.disconnect(queue_draw);
- Calendar.System.instance.is_24hr_changed.disconnect(on_24hr_changed);
- Calendar.System.instance.today_changed.disconnect(on_today_changed);
- }
-
- // this comparator uses the standard Event comparator with one exception: if both Events require
- // solid span lines, it sorts the one(s) with the furthest out end dates to the top, to ensure
- // they are at the top of the drawn lines and prevent gaps and skips in the connected bars
- private static int all_day_comparator(Component.Event a, Component.Event b) {
- if (a == b)
- return 0;
-
- if (!requires_span(a) && !requires_span(b))
- return a.compare_to(b);
-
- Calendar.DateSpan a_span = a.get_event_date_span(Calendar.Timezone.local);
- Calendar.DateSpan b_span = b.get_event_date_span(Calendar.Timezone.local);
-
- int compare = a_span.start_date.compare_to(b_span.start_date);
- if (compare != 0)
- return compare;
-
- compare = b_span.end_date.compare_to(a_span.end_date);
- if (compare != 0)
- return compare;
-
- // to stabilize
- return a.compare_to(b);
- }
-
- /**
- * Returns true if the point at x,y is within the { link Cell}'s width and height.
- */
- public bool is_hit(int x, int y) {
- return x >= 0 && x < get_allocated_width() && y >= 0 && y < get_allocated_height();
- }
-
- /**
- * Returns the assigned line number for the event, -1 if not found in { link Cell}.
- */
- public int get_line_for_event(Component.Event event) {
- Gee.MapIterator<int, Component.Event> iter = line_to_event.map_iterator();
- while (iter.next()) {
- if (iter.get_value().equal_to(event))
- return iter.get_key();
- }
-
- return -1;
- }
-
- public void clear() {
- date = null;
- line_to_event.clear();
-
- foreach (Component.Event event in sorted_events.to_array())
- internal_remove_event(event);
-
- queue_draw();
- }
-
- public void add_event(Component.Event event) {
- if (!sorted_events.add(event))
- return;
-
- // subscribe to interesting mutable properties
- event.notify[Component.Event.PROP_SUMMARY].connect(queue_draw);
- event.notify[Component.Event.PROP_DATE_SPAN].connect(on_span_updated);
- event.notify[Component.Event.PROP_EXACT_TIME_SPAN].connect(on_span_updated);
-
- assign_line_numbers();
-
- queue_draw();
- }
-
- private bool internal_remove_event(Component.Event event) {
- if (!sorted_events.remove(event))
- return false;
-
- event.notify[Component.Event.PROP_SUMMARY].disconnect(queue_draw);
- event.notify[Component.Event.PROP_DATE_SPAN].disconnect(on_span_updated);
- event.notify[Component.Event.PROP_EXACT_TIME_SPAN].disconnect(on_span_updated);
-
- return true;
- }
-
- public void remove_event(Component.Event event) {
- if (!internal_remove_event(event))
- return;
-
- assign_line_numbers();
-
- queue_draw();
- }
-
- /**
- * Called by { link Controllable} when a calendar's visibility has changed.
- *
- * This causes event line numbers to be reassigned and thie { link Cell} redrawn, if the
- * calendar in question has any events in this date.
- */
- public void notify_calendar_visibility_changed(Backing.CalendarSource calendar_source) {
- if (!traverse<Component.Event>(sorted_events).any((event) => event.calendar_source ==
calendar_source))
- return;
-
- // found one
- assign_line_numbers();
- queue_draw();
- }
-
- // Called internally by other Cells when (a) they're in charge of assigning a multi-day event
- // its line number for the week and (b) that line number has changed.
- private void notify_assigned_line_number_changed(Gee.Collection<Component.Event> events) {
- if (!traverse<Component.Event>(sorted_events).contains_any(events))
- return;
-
- assign_line_numbers();
- queue_draw();
- }
-
- // criteria for an event requiring a solid span on the grid
- private static bool requires_span(Component.Event event) {
- return event.is_all_day || event.exact_time_span.duration.days >= 1;
+ owner.notify[Grid.PROP_FIRST_OF_WEEK].disconnect(on_first_of_week_changed);
}
- // each event gets a line of the cell to draw in; this clears all assigned line numbers and
- // re-assigns from the sorted set of events, making sure holes are filled where possible ...
- // if an event starts in this cell or this cell is the first day of a week an event is in,
- // this cell is responsible for assigning a line number to it, which the other cells of the
- // same week will honor (so a continuous line can be drawn)
- private void assign_line_numbers() {
- Gee.HashMap<int, Component.Event> old_line_to_event = line_to_event;
- line_to_event = new Gee.HashMap<int, Component.Event>();
-
- // track each event whose line number this cell is responsible for assigning that gets
- // reassigned because of this
- Gee.ArrayList<Component.Event> reassigned = new Gee.ArrayList<Component.Event>();
-
- foreach (Component.Event event in sorted_events) {
- if (!event.calendar_source.visible)
- continue;
-
- bool notify_reassigned = false;
- if (requires_span(event)) {
- // get the first day of this week the event exists in ... if not the current cell's
- // date, get the assigned line number from the first day of this week the event
- // exists in
- Calendar.Date first_date = get_event_first_day_this_week(event);
- if (!date.equal_to(first_date)) {
- int event_line = -1;
- Cell? cell = owner.get_cell_for_date(first_date);
- if (cell != null)
- event_line = cell.get_line_for_event(event);
-
- if (event_line >= 0) {
- assign_line_number(event_line, event);
-
- continue;
- }
- } else {
- // only worried about multi-day events being reassigned, as that's what effects
- // other cells (i.e. when notifying of reassignment)
- notify_reassigned = event.get_event_date_span(Calendar.Timezone.local).duration.days > 1;
- }
- } else if (!event.is_all_day) {
- // if timed event is in this date but started elsewhere, don't display (unless it
- // requires a span, above)
- Calendar.Date start_date = new Calendar.Date.from_exact_time(
- event.exact_time_span.start_exact_time.to_timezone(Calendar.Timezone.local));
- if (!start_date.equal_to(date))
- continue;
- }
-
- // otherwise, a timed event, a single-day event, or a multi-day event which starts here,
- // so assign
- int assigned = assign_line_number(-1, event);
-
- // if this cell assigns the line number and the event is not new and the number has changed,
- // inform all the other cells following this day's in the current week
- if (notify_reassigned && old_line_to_event.values.contains(event) &&
old_line_to_event.get(assigned) != event)
- reassigned.add(event);
- }
-
- if (reassigned.size > 0) {
- // only need to tell cells following this day's in the current week about the reassignment
- Calendar.Week this_week = date.week_of(owner.first_of_week);
- Calendar.DateSpan span = new Calendar.DateSpan(date.next(),
this_week.end_date).clamp_between(this_week);
-
- foreach (Calendar.Date span_date in span) {
- Cell? cell = owner.get_cell_for_date(span_date);
- if (cell != null && cell != this)
- cell.notify_assigned_line_number_changed(reassigned);
- }
- }
- }
-
- private int assign_line_number(int force_line_number, Component.Event event) {
- // kinda dumb, but this prevents holes appearing in lines where, due to the shape of the
- // all-day events, could be filled
- int line_number = 0;
- if (force_line_number < 0) {
- while (line_to_event.has_key(line_number))
- line_number++;
- } else {
- line_number = force_line_number;
- }
-
- line_to_event.set(line_number, event);
-
- return line_number;
- }
-
- public bool has_events() {
- return sorted_events.size > 0;
+ protected override Common.EventsCell? get_cell_for_date(Calendar.Date cell_date) {
+ return owner.get_cell_for_date(cell_date);
}
- private void on_24hr_changed() {
- if (has_events())
- queue_draw();
+ private void on_first_of_week_changed() {
+ change_date_and_neighbors(date, date.week_of(owner.first_of_week).to_date_span());
}
- private void on_today_changed(Calendar.Date old_today, Calendar.Date new_today) {
- // need to know re: redrawing background color to indicate current day
- if (date != null && (date.equal_to(old_today) || date.equal_to(new_today)))
- queue_draw();
- }
-
- private void on_span_updated(Object object, ParamSpec param) {
- if (date == null)
- return;
-
- Component.Event event = (Component.Event) object;
-
- // remove from cell if no longer in this day, otherwise remove and add again to sorted_events
- // to re-sort
- if (!(date in event.get_event_date_span(Calendar.Timezone.local))) {
- remove_event(event);
- } else if (sorted_events.remove(event)) {
- sorted_events.add(event);
- assign_line_numbers();
- }
-
- queue_draw();
- }
-
- public override bool query_tooltip(int x, int y, bool keyboard_mode, Gtk.Tooltip tooltip) {
- Component.Event? event = get_event_at(Gdk.Point() { x = x, y = y });
- if (event == null)
- return false;
-
- string? tooltip_text = event.get_data<string?>(KEY_TOOLTIP);
- if (String.is_empty(tooltip_text))
- return false;
-
- tooltip.set_text(tooltip_text);
-
- return true;
- }
-
- // Returns the first day of this cell's calendar week that the event is in ... this could be
- // the event's starting day or the first day of this week (i.e. Monday or Sunday)
- private Calendar.Date get_event_first_day_this_week(Component.Event event) {
- Calendar.Date event_start_date = event.get_event_date_span(Calendar.Timezone.local).start_date;
-
- Calendar.Week cell_week = date.week_of(owner.first_of_week);
- Calendar.Week event_start_week = event_start_date.week_of(owner.first_of_week);
-
- return cell_week.equal_to(event_start_week) ? event_start_date : cell_week.start_date;
- }
-
- private bool on_draw(Cairo.Context ctx) {
- // shade background of cell for selection or if today
- if (selected) {
- Gdk.cairo_set_source_rgba(ctx, Palette.instance.selection);
- ctx.paint();
- } else if (date != null && date.equal_to(Calendar.System.today)) {
- Gdk.cairo_set_source_rgba(ctx, Palette.instance.current_day);
- ctx.paint();
- }
-
+ protected override void draw_borders(Cairo.Context ctx) {
int width = get_allocated_width();
int height = get_allocated_height();
@@ -396,192 +69,21 @@ private class Cell : Gtk.EventBox {
}
ctx.stroke();
-
- // draw day of month as the top line
- if (date != null) {
- unowned Gdk.RGBA color = (date in owner.month_of_year)
- ? Palette.instance.day_in_range
- : Palette.instance.day_outside_range;
- draw_line_of_text(ctx, -1, color, date.day_of_month.informal_number, CapEffect.NONE,
- CapEffect.NONE);
- }
-
- // walk the assigned line numbers for each event and draw
- Gee.MapIterator<int, Component.Event> iter = line_to_event.map_iterator();
- while (iter.next()) {
- Component.Event event = iter.get_value();
- Calendar.DateSpan date_span = event.get_event_date_span(Calendar.Timezone.local);
-
- bool display_text = true;
- if (requires_span(event)) {
- // only show the title if (a) the first day of an all-day event or (b) this is the
- // first day of a new week of a multi-day even. (b) handles the contingency of a
- // multi-day event starting in a previous week prior to the top of the current view
- display_text = date_span.start_date.equal_to(date)
- || owner.first_of_week.as_day_of_week().equal_to(date.day_of_week);
- }
-
- string text;
- if (display_text) {
- if (event.is_all_day) {
- text = event.summary;
- } else {
- Calendar.ExactTime local_start = event.exact_time_span.start_exact_time.to_timezone(
- Calendar.Timezone.local);
- text = "%s %s".printf(local_start.to_pretty_time_string(PRETTY_TIME_FLAGS),
event.summary);
- }
- } else {
- text = "";
- }
-
- // use caps on both ends of all-day events depending whether this is the start, end,
- // or start/end of week of continuing event
- CapEffect left_effect = CapEffect.NONE;
- CapEffect right_effect = CapEffect.NONE;
- if (requires_span(event)) {
- if (date_span.start_date.equal_to(date))
- left_effect = CapEffect.ROUNDED;
- else if (date.day_of_week == owner.first_of_week.as_day_of_week())
- left_effect = CapEffect.POINTED;
- else
- left_effect = CapEffect.BLOCKED;
-
- if (date_span.end_date.equal_to(date))
- right_effect = CapEffect.ROUNDED;
- else if (date.day_of_week == owner.first_of_week.as_day_of_week().previous())
- right_effect = CapEffect.POINTED;
- else
- right_effect = CapEffect.BLOCKED;
- }
-
- Pango.Layout layout = draw_line_of_text(ctx, iter.get_key(),
event.calendar_source.color_as_rgba(),
- text, left_effect, right_effect);
- event.set_data<string?>(KEY_TOOLTIP, layout.is_ellipsized() ? text : null);
- }
-
- return true;
}
- // Returns top y position of line; negative line numbers are treated as top line
- // The number is currently not clamped to the height of the widget.
- private int get_line_top_y(int line_number) {
- int y;
- if (line_number < 0) {
- y = Palette.TEXT_MARGIN_PX;
- } else {
- // starting y of "regular" lines
- y = Palette.TEXT_MARGIN_PX + Palette.instance.normal_font_height_px + Palette.LINE_PADDING_PX;
+ private void update_top_line() {
+ if (owner.owner.show_outside_month) {
+ top_line_text = null;
- // add additional lines
- y += line_number * (Palette.instance.small_font_height_px + Palette.LINE_PADDING_PX);
- }
-
- return y;
- }
-
- // If line number is negative, the top line is drawn; otherwise, zero-based line numbers get
- // "regular" treatment
- private Pango.Layout draw_line_of_text(Cairo.Context ctx, int line_number, Gdk.RGBA rgba,
- string text, CapEffect left_effect, CapEffect right_effect) {
- bool is_reversed = (left_effect != CapEffect.NONE || right_effect != CapEffect.NONE);
-
- int left = 0;
- int right = get_allocated_width();
- int top = get_line_top_y(line_number);
- int bottom = top + Palette.instance.small_font_height_px;
-
- // use event color for text unless reversed, where it becomes the background color
- Gdk.cairo_set_source_rgba(ctx, rgba);
- if (is_reversed) {
- // draw background rectangle in spec'd color with text in white
- switch (right_effect) {
- case CapEffect.ROUNDED:
- ctx.new_sub_path();
- // sub 2 to avoid touching right calendar line
- ctx.arc(right - 2 - ROUNDED_CAP_RADIUS, top + ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
- -90.0 * DEGREES, 0 * DEGREES);
- ctx.arc(right - 2 - ROUNDED_CAP_RADIUS, bottom - ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
- 0 * DEGREES, 90.0 * DEGREES);
- break;
-
- case CapEffect.POINTED:
- ctx.move_to(right - POINTED_CAP_WIDTH_PX, top);
- ctx.line_to(right, top + (Palette.instance.small_font_height_px / 2));
- ctx.line_to(right - POINTED_CAP_WIDTH_PX, bottom);
- break;
-
- case CapEffect.BLOCKED:
- default:
- ctx.move_to(right, top);
- ctx.line_to(right, bottom);
- break;
- }
-
- switch (left_effect) {
- case CapEffect.ROUNDED:
- // add one to avoid touching cell to the left's right calendar line
- ctx.arc(left + 1 + ROUNDED_CAP_RADIUS, bottom - ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
- 90.0 * DEGREES, 180.0 * DEGREES);
- ctx.arc(left + 1 + ROUNDED_CAP_RADIUS, top + ROUNDED_CAP_RADIUS, ROUNDED_CAP_RADIUS,
- 180.0 * DEGREES, 270.0 * DEGREES);
- break;
-
- case CapEffect.POINTED:
- ctx.line_to(left + POINTED_CAP_WIDTH_PX, bottom);
- ctx.line_to(left, top + (Palette.instance.small_font_height_px / 2));
- ctx.line_to(left + POINTED_CAP_WIDTH_PX, top);
- break;
-
- case CapEffect.BLOCKED:
- default:
- ctx.line_to(left, bottom);
- ctx.line_to(left, top);
- break;
- }
-
- // fill with event color
- ctx.fill_preserve();
-
- // close path from last point (deals with capped and uncapped ends) and paint
- ctx.close_path();
- ctx.stroke ();
-
- // set to white for text
- Gdk.cairo_set_source_rgba(ctx, Gdk.RGBA() { red = 1.0, green = 1.0, blue = 1.0, alpha = 1.0 });
+ return;
}
- // add a couple of pixels to the text margins if capped
- int left_text_margin = Palette.TEXT_MARGIN_PX + (left_effect != CapEffect.NONE ? 3 : 0);
- int right_text_margin = Palette.TEXT_MARGIN_PX + (right_effect != CapEffect.NONE ? 3 : 0);
+ top_line_text = date.day_of_month.informal_number;
- Pango.Layout layout = create_pango_layout(text);
- layout.set_font_description((line_number < 0)
- ? Palette.instance.normal_font
- : Palette.instance.small_font);
- layout.set_ellipsize(Pango.EllipsizeMode.END);
- layout.set_width((right - left - left_text_margin - right_text_margin) * Pango.SCALE);
-
- ctx.move_to(left_text_margin, top);
- Pango.cairo_show_layout(ctx, layout);
-
- return layout;
- }
-
- /**
- * Returns a hit result for { link Component.Event}, if hit at all.
- *
- * The Gdk.Point must be relative to the widget's coordinate system.
- */
- public Component.Event? get_event_at(Gdk.Point point) {
- for (int line_number = 0; line_number < line_to_event.size; line_number++) {
- int y = get_line_top_y(line_number);
- if (point.y >= y && point.y < (y + Palette.instance.small_font_height_px))
- return line_to_event.get(line_number);
- }
-
- return null;
+ top_line_rgba = (date in owner.month_of_year)
+ ? Palette.instance.day_in_range
+ : Palette.instance.day_outside_range;
}
}
}
-
diff --git a/src/view/month/month-grid.vala b/src/view/month/month-grid.vala
index 29cb3ad..6127986 100644
--- a/src/view/month/month-grid.vala
+++ b/src/view/month/month-grid.vala
@@ -77,7 +77,9 @@ private class Grid : Gtk.Grid {
// pre-add grid elements for every cell, which are updated when the MonthYear changes
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
- Cell cell = new Cell(this, row, col);
+ // use today's date as placeholder until update_cells() is called
+ // TODO: try to avoid this on first pass
+ Cell cell = new Cell(this, Calendar.System.today, row, col);
cell.expand = true;
cell.events |= Gdk.EventMask.BUTTON_PRESS_MASK & Gdk.EventMask.BUTTON1_MOTION_MASK;
cell.button_press_event.connect(on_cell_button_event);
@@ -155,15 +157,12 @@ private class Grid : Gtk.Grid {
}
private void update_week(int row, Calendar.Week week) {
+ Calendar.DateSpan week_as_date_span = week.to_date_span();
foreach (Calendar.Date date in week) {
int col = date.day_of_week.ordinal(owner.first_of_week) - 1;
Cell cell = get_cell(row, col);
-
- // if the date is in the month or configured to show days outside the month, set
- // the cell to show that date; otherwise, it'll be cleared
- cell.clear();
- cell.date = (date in month_of_year) || owner.show_outside_month ? date : null;
+ cell.change_date_and_neighbors(date, week_as_date_span);
// add to map for quick lookups
date_to_cell.set(date, cell);
diff --git a/src/view/month/month.vala b/src/view/month/month.vala
index 6c4f016..45096e3 100644
--- a/src/view/month/month.vala
+++ b/src/view/month/month.vala
@@ -17,6 +17,7 @@ public void init() throws Error {
return;
// unit initialization
+ View.Common.init();
Calendar.init();
Component.init();
Backing.init();
@@ -29,6 +30,7 @@ public void terminate() {
Backing.terminate();
Component.terminate();
Calendar.terminate();
+ View.Common.terminate();
}
}
diff --git a/src/view/view.vala b/src/view/view.vala
index 1af104e..ef61572 100644
--- a/src/view/view.vala
+++ b/src/view/view.vala
@@ -21,6 +21,7 @@ public void init() throws Error {
Palette.init();
// subunit initialization
+ View.Common.init();
View.Month.init();
View.Week.init();
}
@@ -31,6 +32,7 @@ public void terminate() {
View.Week.terminate();
View.Month.terminate();
+ View.Common.terminate();
Palette.terminate();
}
diff --git a/src/view/week/week-all-day-cell.vala b/src/view/week/week-all-day-cell.vala
new file mode 100644
index 0000000..617e24a
--- /dev/null
+++ b/src/view/week/week-all-day-cell.vala
@@ -0,0 +1,73 @@
+/* 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.View.Week {
+
+/**
+ * All-day events that span a particular day are drawn in this container.
+ *
+ * @see DayPane
+ */
+
+internal class AllDayCell : Common.EventsCell {
+ public const string PROP_OWNER = "owner";
+
+ private const int LINES_SHOWN = 3;
+
+ public Grid owner { get; private set; }
+
+ public AllDayCell(Grid owner, Calendar.Date date) {
+ base (date, date.week_of(owner.owner.first_of_week).to_date_span());
+
+ this.owner = owner;
+
+ Palette.instance.palette_changed.connect(on_palette_changed);
+
+ // use for initialization
+ on_palette_changed();
+ }
+
+ ~AllDayCell() {
+ Palette.instance.palette_changed.disconnect(on_palette_changed);
+ }
+
+ protected override Common.EventsCell? get_cell_for_date(Calendar.Date cell_date) {
+ return owner.get_all_day_cell_for_date(cell_date);
+ }
+
+ private void on_palette_changed() {
+ // set fixed size for cell, as it won't grow with the toplevel window
+ set_size_request(-1, (Palette.instance.small_font_height_px + Palette.LINE_PADDING_PX) *
LINES_SHOWN);
+ }
+
+ protected override void draw_borders(Cairo.Context ctx) {
+ int width = get_allocated_width();
+ int height = get_allocated_height();
+
+ // draw border lines (creates grid effect)
+ Palette.prepare_hairline(ctx, Palette.instance.border);
+
+ // if last day of week, draw right border prepping for bottom border
+ if (date.equal_to(neighbors.end_date)) {
+ ctx.move_to(width, 0);
+ ctx.line_to(width, height);
+ } else {
+ // otherwise, prepare for bottom border
+ ctx.move_to(width, height);
+ }
+
+ // draw bottom border
+ ctx.line_to(0, height);
+
+ // draw left border
+ ctx.line_to(0, 0);
+
+ ctx.stroke();
+ }
+}
+
+}
+
diff --git a/src/view/week/week-day-pane.vala b/src/view/week/week-day-pane.vala
index 4412386..2365e7f 100644
--- a/src/view/week/week-day-pane.vala
+++ b/src/view/week/week-day-pane.vala
@@ -6,6 +6,13 @@
namespace California.View.Week {
+/**
+ * A long pane displaying hour and half-hour delineations with events displayed as proportional
+ * boxes along the span.
+ *
+ * @see AllDayCell
+ */
+
internal class DayPane : Pane {
public const string PROP_OWNER = "owner";
public const string PROP_DATE = "date";
@@ -131,11 +138,12 @@ internal class DayPane : Pane {
Palette.prepare_hairline(ctx, Palette.instance.border);
foreach (Component.Event event in days_events) {
- // TODO: Show all-day events
+ // All-day events are handled in separate container ...
if (event.is_all_day)
continue;
- // TODO: Show events that spans days
+ // ... as are events that span days (or outside this date, although that technically
+ // shouldn't happen)
Calendar.DateSpan date_span = event.get_event_date_span(Calendar.Timezone.local);
if (!date_span.is_same_day || !(date in date_span))
continue;
diff --git a/src/view/week/week-grid.vala b/src/view/week/week-grid.vala
index 4a1ad47..b55a7d0 100644
--- a/src/view/week/week-grid.vala
+++ b/src/view/week/week-grid.vala
@@ -41,7 +41,11 @@ internal class Grid : Gtk.Box {
private Backing.CalendarSubscriptionManager subscriptions;
private Gee.HashMap<Calendar.Date, DayPane> date_to_panes = new Gee.HashMap<Calendar.Date, DayPane>();
+ private Gee.HashMap<Calendar.Date, AllDayCell> date_to_all_day = new Gee.HashMap<Calendar.Date,
+ AllDayCell>();
private Toolkit.ButtonConnector day_pane_button_connector = new Toolkit.ButtonConnector();
+ Gtk.ScrolledWindow scrolled_panes;
+ Gtk.Widget right_spacer;
public Grid(Controller owner, Calendar.Week week) {
Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
@@ -49,15 +53,29 @@ internal class Grid : Gtk.Box {
this.owner = owner;
this.week = week;
- // hold date labels in a horizontal box
- Gtk.Box label_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
- pack_start(label_box, false, true, 0);
+ // use a top horizontal box to properly space the spacer next to the horizontal grid of
+ // day labels and all-day cells
+ Gtk.Box top_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
+ pack_start(top_box, false, true, 8);
- // fixed size space in top left corner of "grid"
- Gtk.DrawingArea spacer = new Gtk.DrawingArea();
- spacer.set_size_request(HourRunner.REQUESTED_WIDTH, -1);
- spacer.draw.connect(on_draw_bottom_line);
- label_box.pack_start(spacer, false, false, 0);
+ // fixed size space in top left corner of overall grid
+ Gtk.DrawingArea left_spacer = new Gtk.DrawingArea();
+ left_spacer.set_size_request(HourRunner.REQUESTED_WIDTH, -1);
+ left_spacer.draw.connect(on_draw_bottom_line);
+ top_box.pack_start(left_spacer, false, false, 0);
+
+ // hold day labels and all-day cells in a non-scrolling horizontal grid
+ Gtk.Grid top_grid = new Gtk.Grid();
+ top_grid.column_homogeneous = true;
+ top_grid.column_spacing = 0;
+ top_grid.row_homogeneous = false;
+ top_grid.row_spacing = 0;
+ top_box.pack_start(top_grid, true, true, 0);
+
+ // to line up with day panes grid below, need to account for the space of the ScrolledWindow's
+ // scrollbar
+ right_spacer = new Gtk.DrawingArea();
+ top_box.pack_end(right_spacer, false, false, 0);
// hold Panes (DayPanes and HourRunner) in a scrolling Gtk.Grid
Gtk.Grid pane_grid = new Gtk.Grid();
@@ -67,7 +85,7 @@ internal class Grid : Gtk.Box {
pane_grid.row_spacing = 0;
// attach an HourRunner to the left side of the Panes grid
- pane_grid.attach(new HourRunner(this), 0, 0, 1, 1);
+ pane_grid.attach(new HourRunner(this), 0, 1, 1, 1);
// date labels across the top, week panes extending across the bottom ... start col at one
// to account for spacer/HourRunner
@@ -75,29 +93,42 @@ internal class Grid : Gtk.Box {
foreach (Calendar.Date date in week) {
Gtk.Label date_label = new Gtk.Label("%s %d/%d".printf(date.day_of_week.abbrev_name,
date.month_of_year().month.value, date.day_of_month.value));
-
// draw a line along the bottom of the label
date_label.draw.connect(on_draw_bottom_line);
+ top_grid.attach(date_label, col, 0, 1, 1);
- label_box.pack_start(date_label, true, true, 0);
+ // All-day cells (for drawing all-day and day-spanning events) go between the date
+ // label and the day panes
+ AllDayCell all_day_cell = new AllDayCell(this, date);
+ top_grid.attach(all_day_cell, col, 1, 1, 1);
+
+ // save mapping
+ date_to_all_day.set(date, all_day_cell);
DayPane pane = new DayPane(this, date);
pane.expand = true;
day_pane_button_connector.connect_to(pane);
- pane_grid.attach(pane, col, 0, 1, 1);
+ pane_grid.attach(pane, col, 1, 1, 1);
+ // save mapping
date_to_panes.set(date, pane);
col++;
}
// place Panes grid into a GtkScrolledWindow
- Gtk.ScrolledWindow scrolled_panes = new Gtk.ScrolledWindow(null, null);
+ scrolled_panes = new Gtk.ScrolledWindow(null, null);
scrolled_panes.hscrollbar_policy = Gtk.PolicyType.NEVER;
scrolled_panes.vscrollbar_policy = Gtk.PolicyType.ALWAYS;
scrolled_panes.add(pane_grid);
+ // connect_after to ensure border is last thing drawn
+ scrolled_panes.draw.connect_after(on_draw_top_line);
pack_end(scrolled_panes, true, true, 0);
+ // connect scrollbar width to right_spacer (above) so it's the same width
+ scrolled_panes.get_vscrollbar().realize.connect(on_realloc_right_spacer);
+ scrolled_panes.get_vscrollbar().size_allocate.connect(on_realloc_right_spacer);
+
// connect panes' event signal handlers
day_pane_button_connector.clicked.connect(on_day_pane_clicked);
day_pane_button_connector.double_clicked.connect(on_day_pane_double_clicked);
@@ -129,11 +160,22 @@ internal class Grid : Gtk.Box {
});
}
+ private bool on_draw_top_line(Gtk.Widget widget, Cairo.Context ctx) {
+ Palette.prepare_hairline(ctx, Palette.instance.border);
+
+ ctx.move_to(0, 0);
+ ctx.line_to(widget.get_allocated_width(), 0);
+ ctx.stroke();
+
+ return false;
+ }
+
private bool on_draw_bottom_line(Gtk.Widget widget, Cairo.Context ctx) {
int width = widget.get_allocated_width();
int height = widget.get_allocated_height();
Palette.prepare_hairline(ctx, Palette.instance.border);
+
ctx.move_to(0, height);
ctx.line_to(width, height);
ctx.stroke();
@@ -141,6 +183,16 @@ internal class Grid : Gtk.Box {
return false;
}
+ private void on_realloc_right_spacer() {
+ // need to do outside of allocation signal due to some mechanism in GTK that prevents resizes
+ // while resizing
+ Idle.add(() => {
+ right_spacer.set_size_request(scrolled_panes.get_vscrollbar().get_allocated_width(), -1);
+
+ return false;
+ });
+ }
+
private void on_calendar_added(Backing.CalendarSource calendar) {
}
@@ -153,9 +205,15 @@ internal class Grid : Gtk.Box {
return;
foreach (Calendar.Date date in event.get_event_date_span(Calendar.Timezone.local)) {
- DayPane? day_pane = date_to_panes.get(date);
- if (day_pane != null)
- day_pane.add_event(event);
+ if (event.is_day_spanning) {
+ AllDayCell? all_day_cell = date_to_all_day.get(date);
+ if (all_day_cell != null)
+ all_day_cell.add_event(event);
+ } else {
+ DayPane? day_pane = date_to_panes.get(date);
+ if (day_pane != null)
+ day_pane.add_event(event);
+ }
}
}
@@ -165,12 +223,22 @@ internal class Grid : Gtk.Box {
return;
foreach (Calendar.Date date in event.get_event_date_span(Calendar.Timezone.local)) {
- DayPane? day_pane = date_to_panes.get(date);
- if (day_pane != null)
- day_pane.remove_event(event);
+ if (event.is_day_spanning) {
+ AllDayCell? all_day_cell = date_to_all_day.get(date);
+ if (all_day_cell != null)
+ all_day_cell.remove_event(event);
+ } else {
+ DayPane? day_pane = date_to_panes.get(date);
+ if (day_pane != null)
+ day_pane.remove_event(event);
+ }
}
}
+ internal AllDayCell? get_all_day_cell_for_date(Calendar.Date cell_date) {
+ return date_to_all_day.get(cell_date);
+ }
+
private void on_day_pane_clicked(Toolkit.ButtonEvent details) {
if (details.button != Toolkit.Button.PRIMARY)
return;
diff --git a/src/view/week/week.vala b/src/view/week/week.vala
index 3cf1252..4dc39c7 100644
--- a/src/view/week/week.vala
+++ b/src/view/week/week.vala
@@ -21,12 +21,14 @@ public void init() throws Error {
Backing.init();
Component.init();
Toolkit.init();
+ View.Common.init();
}
public void terminate() {
if (!Unit.do_terminate(ref init_count))
return;
+ View.Common.terminate();
Toolkit.terminate();
Component.terminate();
Backing.terminate();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]