[california] Show all-day events with a single spanning bar: Closes bgo #725778



commit 62fb481126690945f530cf3581c0dc4465d82b16
Author: Jim Nelson <jim yorba org>
Date:   Thu Apr 24 18:34:52 2014 -0700

    Show all-day events with a single spanning bar: Closes bgo #725778
    
    All-day events now are shown as a bar that span every day of their
    duration.  The beginning and end of the span are capped with rounded
    corners.  If the event continues on to the next calendar week, the
    ends have pointed (triangular) caps.

 src/Makefile.am                         |    1 +
 src/calendar/calendar-date-span.vala    |   15 ++
 src/collection/collection-iterable.vala |    9 +
 src/component/component-event.vala      |    5 +-
 src/tests/tests-calendar-date.vala      |   67 +++++++
 src/tests/tests.vala                    |    1 +
 src/view/month/month-cell.vala          |  323 ++++++++++++++++++++++++++++---
 src/view/month/month-controllable.vala  |   18 ++-
 8 files changed, 406 insertions(+), 33 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 57105f0..1e8ebda 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -99,6 +99,7 @@ california_VALASOURCES = \
        manager/manager-window.vala \
        \
        tests/tests.vala \
+       tests/tests-calendar-date.vala \
        tests/tests-quick-add.vala \
        \
        toolkit/toolkit.vala \
diff --git a/src/calendar/calendar-date-span.vala b/src/calendar/calendar-date-span.vala
index 8bc1336..a7ffc29 100644
--- a/src/calendar/calendar-date-span.vala
+++ b/src/calendar/calendar-date-span.vala
@@ -188,6 +188,21 @@ public class DateSpan : BaseObject, Collection.SimpleIterable<Date>, Span<Date>,
     }
     
     /**
+     * Returns a { link DateSpan} with starting and ending points within the boundary specified
+     * (inclusive).
+     *
+     * If this DateSpan is within the clamped dates, this object may be returned.
+     *
+     * This method will not expand a DateSpan to meet the clamp range.
+     */
+    public DateSpan clamp(DateSpan span) {
+        Date new_start = (start_date.compare_to(span.start_date) < 0) ? span.start_date : start_date;
+        Date new_end = (end_date.compare_to(span.end_date) > 0) ? span.end_date : end_date;
+        
+        return new DateSpan(new_start, new_end);
+    }
+    
+    /**
      * Compares two { link DateSpan}s by their { link start_date}.
      */
     public int compare_to(DateSpan other) {
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 1bd9cbf..2276d8e 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -131,6 +131,15 @@ public class Iterable<G> : BaseObject {
         return false;
     }
     
+    public bool contains_any(Gee.Collection<G> c) {
+        foreach (G g in this) {
+            if (c.contains(g))
+                return true;
+        }
+        
+        return false;
+    }
+    
     public bool all(owned Gee.Predicate<G> f) {
         foreach (G g in this) {
             if (!f(g))
diff --git a/src/component/component-event.vala b/src/component/component-event.vala
index bd68069..3923383 100644
--- a/src/component/component-event.vala
+++ b/src/component/component-event.vala
@@ -347,11 +347,14 @@ public class Event : Instance, Gee.Comparable<Event> {
         }
         
         if (exact_time_span != null
+            && other_event.date_span != null
             && !new Calendar.DateSpan.from_exact_time_span(exact_time_span).equal_to(other_event.date_span)) 
{
             return false;
         }
         
-        if (!date_span.equal_to(new Calendar.DateSpan.from_exact_time_span(other_event.exact_time_span))) {
+        if (date_span != null
+            && other_event.exact_time_span != null
+            && !date_span.equal_to(new Calendar.DateSpan.from_exact_time_span(other_event.exact_time_span))) 
{
             return false;
         }
         
diff --git a/src/tests/tests-calendar-date.vala b/src/tests/tests-calendar-date.vala
new file mode 100644
index 0000000..2620dd9
--- /dev/null
+++ b/src/tests/tests-calendar-date.vala
@@ -0,0 +1,67 @@
+/* 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 {
+
+private class CalendarDate : UnitTest.Harness {
+    public CalendarDate() {
+        add_case("clamp-start", clamp_start);
+        add_case("clamp-end", clamp_end);
+        add_case("clamp-both", clamp_both);
+        add_case("clamp-neither", clamp_neither);
+    }
+    
+    protected override void setup() throws Error {
+        Calendar.init();
+    }
+    
+    protected override void teardown() {
+        Calendar.terminate();
+    }
+    
+    private Calendar.Date from_today(int days) {
+        return Calendar.System.today.adjust(days, Calendar.DateUnit.DAY);
+    }
+    
+    private Calendar.DateSpan span_from_today(int start_days, int end_days) {
+        return new Calendar.DateSpan(from_today(start_days), from_today(end_days));
+    }
+    
+    private bool clamp_start() throws Error {
+        Calendar.DateSpan span = span_from_today(0, 5);
+        Calendar.DateSpan clamp = span_from_today(1, 5);
+        Calendar.DateSpan adj = span.clamp(clamp);
+        
+        return adj.start_date.equal_to(clamp.start_date) && adj.end_date.equal_to(span.end_date);
+    }
+    
+    private bool clamp_end() throws Error {
+        Calendar.DateSpan span = span_from_today(0, 5);
+        Calendar.DateSpan clamp = span_from_today(0, 4);
+        Calendar.DateSpan adj = span.clamp(clamp);
+        
+        return adj.start_date.equal_to(span.start_date) && adj.end_date.equal_to(clamp.end_date);
+    }
+    
+    private bool clamp_both() throws Error {
+        Calendar.DateSpan span = span_from_today(0, 5);
+        Calendar.DateSpan clamp = span_from_today(1, 4);
+        Calendar.DateSpan adj = span.clamp(clamp);
+        
+        return adj.start_date.equal_to(clamp.start_date) && adj.end_date.equal_to(clamp.end_date);
+    }
+    
+    private bool clamp_neither() throws Error {
+        Calendar.DateSpan span = span_from_today(0, 5);
+        Calendar.DateSpan clamp = span_from_today(-1, 6);
+        Calendar.DateSpan adj = span.clamp(clamp);
+        
+        return adj.start_date.equal_to(span.start_date) && adj.end_date.equal_to(span.end_date);
+    }
+}
+
+}
+
diff --git a/src/tests/tests.vala b/src/tests/tests.vala
index 327609f..0684618 100644
--- a/src/tests/tests.vala
+++ b/src/tests/tests.vala
@@ -8,6 +8,7 @@ namespace California.Tests {
 
 public int run(string[] args) {
     UnitTest.Harness.register(new QuickAdd());
+    UnitTest.Harness.register(new CalendarDate());
     
     return UnitTest.Harness.exec_all();
 }
diff --git a/src/view/month/month-cell.vala b/src/view/month/month-cell.vala
index dd1ee0f..2ee1959 100644
--- a/src/view/month/month-cell.vala
+++ b/src/view/month/month-cell.vala
@@ -17,12 +17,24 @@ public class Cell : Gtk.EventBox {
     private const int TEXT_MARGIN_PX = 2;
     private const int LINE_SPACING_PX = 4;
     
+    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
+    }
+    
     public weak Controllable owner { get; private set; }
     public int row { get; private set; }
     public int col { get; private set; }
@@ -59,7 +71,7 @@ public class Cell : Gtk.EventBox {
         }
     }
     
-    private Gee.TreeSet<Component.Event> days_events = new Gee.TreeSet<Component.Event>();
+    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>();
     
     // TODO: We may need to get these colors from the theme
@@ -104,7 +116,7 @@ public class Cell : Gtk.EventBox {
     internal static void init() {
         top_line_font = new Pango.FontDescription();
         top_line_font.set_size(TOP_LINE_FONT_SIZE_PT * Pango.SCALE);
-    
+        
         line_font = new Pango.FontDescription();
         line_font.set_size(LINE_FONT_SIZE_PT * Pango.SCALE);
         
@@ -117,6 +129,28 @@ public class Cell : Gtk.EventBox {
         line_font = null;
     }
     
+    // this comparator uses the standard Event comparator with one exception: if both Events are
+    // all-day, 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_all_day || !b.is_all_day)
+            return a.compare_to(b);
+        
+        int compare = a.date_span.start_date.compare_to(b.date_span.start_date);
+        if (compare != 0)
+            return compare;
+        
+        compare = b.date_span.end_date.compare_to(a.date_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.
      */
@@ -124,13 +158,27 @@ public class Cell : Gtk.EventBox {
         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;
-        days_events.clear();
+        sorted_events.clear();
+        line_to_event.clear();
     }
     
     public void add_event(Component.Event event) {
-        if (!days_events.add(event))
+        if (!sorted_events.add(event))
             return;
         
         // subscribe to interesting mutable properties
@@ -138,22 +186,131 @@ public class Cell : Gtk.EventBox {
         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();
     }
     
     public void remove_event(Component.Event event) {
-        if (!days_events.remove(event))
+        if (!sorted_events.remove(event))
             return;
         
         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);
         
+        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();
+    }
+    
+    // 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 all_day_assigned_here = false;
+            if (event.is_all_day) {
+                // 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
+                    all_day_assigned_here = event.date_span.duration() > 1;
+                }
+            }
+            
+            // 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 (all_day_assigned_here && 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(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 days_events.size > 0;
+        return sorted_events.size > 0;
     }
     
     private void on_24hr_changed() {
@@ -173,12 +330,14 @@ public class Cell : Gtk.EventBox {
         
         Component.Event event = (Component.Event) object;
         
-        // remove from cell if no longer in this day, otherwise remove and add again to days_events
+        // 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)))
+        if (!(date in event.get_event_date_span(Calendar.Timezone.local))) {
             remove_event(event);
-        else if (days_events.remove(event))
-            days_events.add(event);
+        } else if (sorted_events.remove(event)) {
+            sorted_events.add(event);
+            assign_line_numbers();
+        }
         
         queue_draw();
     }
@@ -197,6 +356,17 @@ public class Cell : Gtk.EventBox {
         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) {
         // calculate extents if not already calculated;
         if (line_height_px < 0 || top_line_height_px < 0)
@@ -241,32 +411,54 @@ public class Cell : Gtk.EventBox {
         // draw day of month as the top line
         if (date != null) {
             Gdk.RGBA color = (date in owner.month_of_year) ? RGBA_DAY_OF_MONTH : RGBA_DAY_OUTSIDE_MONTH;
-            draw_line_of_text(ctx, -1, color, date.day_of_month.informal_number);
+            draw_line_of_text(ctx, -1, color, date.day_of_month.informal_number, CapEffect.NONE,
+                CapEffect.NONE);
         }
         
-        // represents the line number being drawn (zero-based for remaining lines)
-        int line_number = 0;
-        line_to_event.clear();
-        
-        // draw all events in chronological order, all-day events first, storing lookup data
-        // as the "lines" are drawn ... make sure to convert them all to local timezone
-        foreach (Component.Event event in days_events) {
-            if (!event.calendar_source.visible)
-                continue;
+        // 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();
             
-            string text;
+            string text, tooltip_text;
             if (event.is_all_day) {
-                text = event.summary;
+                // 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
+                bool display_text = event.date_span.start_date.equal_to(date)
+                    || owner.first_of_week.as_day_of_week().equal_to(date.day_of_week);
+                text = display_text ? event.summary : "";
+                tooltip_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);
+                tooltip_text = 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_all_day) {
+                if (event.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 (event.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, line_number, event.calendar_source.color_as_rgba(),
-                text);
-            line_to_event.set(line_number++, event);
-            event.set_data<string?>(KEY_TOOLTIP, layout.is_ellipsized() ? text : null);
+            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() ? tooltip_text : null);
         }
         
         return true;
@@ -304,14 +496,85 @@ public class Cell : Gtk.EventBox {
     
     // 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) {
+    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 + line_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 + (line_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 + (line_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 = TEXT_MARGIN_PX + (left_effect != CapEffect.NONE ? 3 : 0);
+        int right_text_margin = TEXT_MARGIN_PX + (right_effect != CapEffect.NONE ? 3 : 0);
+        
         Pango.Layout layout = create_pango_layout(text);
         layout.set_font_description((line_number < 0) ? top_line_font : line_font);
         layout.set_ellipsize(Pango.EllipsizeMode.END);
-        layout.set_width((get_allocated_width() - (TEXT_MARGIN_PX * 2)) * Pango.SCALE);
+        layout.set_width((right - left - left_text_margin - right_text_margin) * Pango.SCALE);
         
-        Gdk.cairo_set_source_rgba(ctx, rgba);
-        ctx.move_to(TEXT_MARGIN_PX, get_line_top_y(line_number));
+        ctx.move_to(left_text_margin, top);
         Pango.cairo_show_layout(ctx, layout);
         
         return layout;
diff --git a/src/view/month/month-controllable.vala b/src/view/month/month-controllable.vala
index 3707df8..cf5b34f 100644
--- a/src/view/month/month-controllable.vala
+++ b/src/view/month/month-controllable.vala
@@ -168,6 +168,10 @@ public class Controllable : Gtk.Grid, View.Controllable {
         return (Cell) get_child_at(col, row);
     }
     
+    internal Cell? get_cell_for_date(Calendar.Date date) {
+        return date_to_cell.get(date);
+    }
+    
     private void foreach_cell(CellCallback callback) {
         foreach (unowned Gtk.Widget widget in get_children()) {
             // watch for Gtk.Labels across the top
@@ -286,15 +290,25 @@ public class Controllable : Gtk.Grid, View.Controllable {
     }
     
     private void on_calendar_added(Backing.CalendarSource calendar) {
-        calendar.notify[Backing.Source.PROP_VISIBLE].connect(queue_draw);
+        calendar.notify[Backing.Source.PROP_VISIBLE].connect(on_calendar_visibility_changed);
         calendar.notify[Backing.Source.PROP_COLOR].connect(queue_draw);
     }
     
     private void on_calendar_removed(Backing.CalendarSource calendar) {
-        calendar.notify[Backing.Source.PROP_VISIBLE].disconnect(queue_draw);
+        calendar.notify[Backing.Source.PROP_VISIBLE].disconnect(on_calendar_visibility_changed);
         calendar.notify[Backing.Source.PROP_COLOR].disconnect(queue_draw);
     }
     
+    private void on_calendar_visibility_changed(Object o, ParamSpec pspec) {
+        Backing.CalendarSource calendar = (Backing.CalendarSource) o;
+        
+        foreach_cell((cell) => {
+            cell.notify_calendar_visibility_changed(calendar);
+            
+            return true;
+        });
+    }
+    
     private void on_instance_added(Component.Instance instance) {
         Component.Event? event = instance as Component.Event;
         if (event == null)


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