[california] Migrate to GTK+ 3.12.2: Bug #732129



commit 4e3b6d002fc306b00edfd3bcfd9ceac7ac1403c4
Author: Jim Nelson <jim yorba org>
Date:   Wed Aug 20 14:34:07 2014 -0700

    Migrate to GTK+ 3.12.2: Bug #732129
    
    This moves California back onto GTK+ 3.12.  Gtk.Popover use is more
    spare than initially planned.  In particular, there is no popover
    used that is attached to a header bar button due to a Unity bug.
    
    Aesthetic issues remain, but to keep this patch manageable, they will
    be attacked in future commits.

 configure.ac                                 |    5 +-
 debian/control                               |    2 +-
 src/Makefile.am                              |    5 +-
 src/collection/collection-iterable.vala      |    4 +-
 src/host/host-date-time-widget.vala          |    4 -
 src/host/host-main-window.vala               |  116 ++++++++++++++++++-------
 src/host/host-quick-create-event.vala        |   10 +-
 src/host/host-show-event.vala                |   36 ++++++---
 src/manager/manager-window.vala              |   18 +---
 src/rc/create-update-recurring.ui            |    6 +-
 src/rc/show-event.ui                         |    5 +-
 src/toolkit/toolkit-deck-popover.vala        |  102 ++++++++++++++++++++++
 src/toolkit/toolkit-deck-window.vala         |    4 +-
 src/toolkit/toolkit-deck.vala                |   31 ++++++--
 src/toolkit/toolkit-rotating-button-box.vala |   61 +++++++++++++-
 src/toolkit/toolkit.vala                     |   29 +++++++
 src/view/month/month-controller.vala         |   16 ++++
 src/view/view-controllable.vala              |   45 ++++++++++
 src/view/view.vala                           |    4 +
 src/view/week/week-controller.vala           |   15 ++++
 src/view/week/week-day-pane.vala             |   11 +++
 src/view/week/week-grid.vala                 |    4 +-
 22 files changed, 435 insertions(+), 98 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index ad884c8..f2e142d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -25,7 +25,7 @@ AC_SUBST(CPPFLAGS)
 AC_SUBST(LDFLAGS)
 
 GLIB_REQUIRED=2.38.0
-GTK_REQUIRED=3.10.7
+GTK_REQUIRED=3.12.2
 GEE_REQUIRED=0.10.5
 ECAL_REQUIRED=3.8.5
 LIBSOUP_REQUIRED=2.44
@@ -45,9 +45,6 @@ PKG_CHECK_MODULES(CALIFORNIA, [
        gobject-introspection-1.0 >= $GOBJECT_INTROSPECTION_REQUIRED
 ])
 
-# Look for GTK+ 3.12 or better
-AM_CONDITIONAL(IS_GTK_312, pkg-config --exists 'gtk+-3.0 >= 3.12')
-
 AC_SUBST(CALIFORNIA_CFLAGS)
 AC_SUBST(CALIFORNIA_LIBS)
 
diff --git a/debian/control b/debian/control
index aeaf13a..c0234b6 100644
--- a/debian/control
+++ b/debian/control
@@ -5,7 +5,7 @@ Maintainer: Jim Nelson <jim yorba org>
 Build-Depends: debhelper (>= 8),
  libgee-0.8-dev (>= 0.10.5),
  libglib2.0-dev (>= 2.38.0),
- libgtk-3-dev (>= 3.10.7),
+ libgtk-3-dev (>= 3.12.2),
  valac (>= 0.22.1),
  intltool,
  libecal1.2-dev (>= 3.8.5),
diff --git a/src/Makefile.am b/src/Makefile.am
index 8a99e77..f11b6a9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -133,6 +133,7 @@ california_VALASOURCES = \
        toolkit/toolkit-card.vala \
        toolkit/toolkit-combo-box-text-model.vala \
        toolkit/toolkit-deck.vala \
+       toolkit/toolkit-deck-popover.vala \
        toolkit/toolkit-deck-window.vala \
        toolkit/toolkit-editable-label.vala \
        toolkit/toolkit-entry-clear-text-connector.vala \
@@ -213,10 +214,6 @@ if ENABLE_UNITY
 california_OPTIONAL_VALAFLAGS += --define ENABLE_UNITY
 endif
 
-if IS_GTK_312
-california_OPTIONAL_VALAFLAGS += --define GTK_312
-endif
-
 if HAVE__NL_TIME_FIRST_WEEKDAY
 california_OPTIONAL_VALAFLAGS += --define HAVE__NL_TIME_FIRST_WEEKDAY
 endif
diff --git a/src/collection/collection-iterable.vala b/src/collection/collection-iterable.vala
index 66a1e79..d51e82d 100644
--- a/src/collection/collection-iterable.vala
+++ b/src/collection/collection-iterable.vala
@@ -22,7 +22,9 @@ public California.Iterable<G> traverse<G>(Gee.Iterable<G>? gee_iterable) {
  * it.
  *
  * "Safe iteration" means later operations that remove elements while iterating do not cause an
- * assertion.
+ * assertion.  This involves creating a copy of the supplied Gee.Iterable, meaning that any changes
+ * made in subsequence operations (i.e. { link California.Iterable.filter} are not reflected in
+ * the passed-in collection.
  *
  * An empty Gee.Iterable is created and used if null is passed in.
  */
diff --git a/src/host/host-date-time-widget.vala b/src/host/host-date-time-widget.vala
index 4b2a804..3f43a56 100644
--- a/src/host/host-date-time-widget.vala
+++ b/src/host/host-date-time-widget.vala
@@ -118,11 +118,7 @@ public class DateTimeWidget : Gtk.Box {
         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() {
diff --git a/src/host/host-main-window.vala b/src/host/host-main-window.vala
index 26dd5fd..d8643b5 100644
--- a/src/host/host-main-window.vala
+++ b/src/host/host-main-window.vala
@@ -83,6 +83,8 @@ public class MainWindow : Gtk.ApplicationWindow {
         { ACTION_RESET_FONT, on_reset_font }
     };
     
+    public Gtk.Button calendar_button { get; private set; }
+    
     private Gtk.Button quick_add_button;
     private View.Palette palette;
     private View.Controllable month_view;
@@ -171,7 +173,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         view_switcher.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED);
         view_switcher.get_style_context().add_class(Gtk.STYLE_CLASS_RAISED);
         
-        // pack left-side of window
+        // pack left-side of header bar
         headerbar.pack_start(today);
         headerbar.pack_start(view_switcher);
         
@@ -180,11 +182,11 @@ public class MainWindow : Gtk.ApplicationWindow {
         quick_add_button.tooltip_text = _("Quick add event (Ctrl+N)");
         quick_add_button.set_action_name(DETAILED_ACTION_QUICK_CREATE_EVENT);
         
-        Gtk.Button calendars = new Gtk.Button.from_icon_name("x-office-calendar-symbolic",
+        calendar_button = new Gtk.Button.from_icon_name("x-office-calendar-symbolic",
             Gtk.IconSize.MENU);
-        calendars.valign = Gtk.Align.CENTER;
-        calendars.tooltip_text = _("Calendars (Ctrl+L)");
-        calendars.set_action_name(Application.DETAILED_ACTION_CALENDAR_MANAGER);
+        calendar_button.valign = Gtk.Align.CENTER;
+        calendar_button.tooltip_text = _("Calendars (Ctrl+L)");
+        calendar_button.set_action_name(Application.DETAILED_ACTION_CALENDAR_MANAGER);
         
         Gtk.MenuButton window_menu = new Gtk.MenuButton();
         window_menu.valign = Gtk.Align.CENTER;
@@ -199,20 +201,13 @@ public class MainWindow : Gtk.ApplicationWindow {
         size.add_widget(custom_title.next_button);
         size.add_widget(custom_title.prev_button);
         size.add_widget(quick_add_button);
-        size.add_widget(calendars);
+        size.add_widget(calendar_button);
         size.add_widget(window_menu);
         
-        // pack right-side of window ... note that this was fixed in 3.12, reversing the order of
-        // how widgets need to be packed at the end
-#if GTK_312
+        // pack right-side of header bar
         headerbar.pack_end(window_menu);
-        headerbar.pack_end(calendars);
-        headerbar.pack_end(quick_add_button);
-#else
+        headerbar.pack_end(calendar_button);
         headerbar.pack_end(quick_add_button);
-        headerbar.pack_end(calendars);
-        headerbar.pack_end(window_menu);
-#endif
         
         Gtk.Box layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
         // if on Unity, since headerbar is not the titlebar, need to pack it like any other widget
@@ -323,7 +318,7 @@ public class MainWindow : Gtk.ApplicationWindow {
         }
     }
     
-    private void show_deck(Gtk.Widget relative_to, Gdk.Point? for_location, Toolkit.Deck deck) {
+    private void show_deck_window(Toolkit.Deck deck) {
         Toolkit.DeckWindow deck_window = new Toolkit.DeckWindow(this, deck);
         
         // when the dialog closes, reset View.Controllable state (selection is maintained while
@@ -345,8 +340,34 @@ public class MainWindow : Gtk.ApplicationWindow {
         deck_window.destroy();
     }
     
+    private void show_deck_popover(Gtk.Widget relative_to, Gdk.Point? for_location, Toolkit.Deck deck) {
+        Toolkit.DeckPopover deck_popover = new Toolkit.DeckPopover(relative_to, for_location, deck);
+        
+        // when the popover closes, reset View.Controllable state (selection is maintained while
+        // use is viewing/editing interaction) and destroy widgets
+        deck_popover.dismiss.connect(() => {
+            current_controller.unselect_all();
+            Toolkit.destroy_later(deck_popover);
+        });
+        
+        deck_popover.deck.failure.connect((msg) => {
+            Application.instance.error_message(msg);
+        });
+        
+        deck_popover.show_all();
+    }
+    
     private void on_quick_create_event() {
-        quick_create_event(null, quick_add_button, null);
+        // switch to Today and execute Quick Add when transition is complete
+        current_controller.today();
+        current_controller.execute_when_not_transitioning(do_quick_create_event);
+    }
+    
+    private void do_quick_create_event(View.Controllable controller) {
+        Gtk.Widget? today_widget = controller.get_widget_for_date(Calendar.System.today);
+        assert(today_widget != null);
+        
+        on_request_create_all_day_event(Calendar.System.today.to_date_span(), today_widget, null);
     }
     
     private void on_jump_to_today() {
@@ -415,29 +436,58 @@ public class MainWindow : Gtk.ApplicationWindow {
     private void quick_create_event(Component.Event? initial, Gtk.Widget relative_to, Gdk.Point? 
for_location) {
         QuickCreateEvent quick_create = new QuickCreateEvent();
         
-        CreateUpdateEvent create_update = new CreateUpdateEvent();
-        create_update.is_update = false;
-        
-        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, event_time_settings)
-            .to_array_list()
-        );
+        deck.add_cards(iterate<Toolkit.Card>(quick_create).to_array_list());
         
-        // initialize the Deck with the initial event (if any)
         deck.go_home(initial);
         
-        show_deck(relative_to, for_location, deck);
+        deck.dismiss.connect(() => {
+            if (quick_create.edit_required)
+                edit_event(quick_create.event);
+        });
+        
+        show_deck_popover(relative_to, for_location, deck);
     }
     
     private void on_request_display_event(Component.Event event, Gtk.Widget relative_to,
         Gdk.Point? for_location) {
         ShowEvent show_event = new ShowEvent();
         
+        Toolkit.Deck deck = new Toolkit.Deck();
+        deck.add_cards(iterate<Toolkit.Card>(show_event).to_array_list());
+        
+        deck.go_home(event);
+        
+        deck.dismiss.connect(() => {
+            if (!show_event.edit_requested)
+                return;
+                
+            // pass a clone of the existing event for editing
+            Component.Event clone;
+            try {
+                clone = event.clone() as Component.Event;
+            } catch (Error err) {
+                Application.instance.error_message(_("Unable to edit event: %s").printf(err.message));
+                
+                return;
+            }
+            
+            edit_event(clone);
+        });
+        
+        show_deck_popover(relative_to, for_location, deck);
+    }
+    
+    private void edit_event(Component.Event event) {
+        // use Idle loop so popovers have a chance to hide before bringing up DeckWindow
+        Idle.add(() => {
+            on_edit_event(event);
+            
+            return false;
+        }, Priority.LOW + 100);
+    }
+    
+    private void on_edit_event(Component.Event event) {
         CreateUpdateEvent create_update = new CreateUpdateEvent();
         create_update.is_update = true;
         
@@ -447,14 +497,14 @@ public class MainWindow : Gtk.ApplicationWindow {
         
         Toolkit.Deck deck = new Toolkit.Deck();
         deck.add_cards(
-            iterate<Toolkit.Card>(show_event, create_update, create_update_recurring, event_time_settings)
+            iterate<Toolkit.Card>(create_update, create_update_recurring, event_time_settings)
             .to_array_list()
         );
         
-        // "initialize" the Deck with the requested Event (because ShowEvent is first, it's home)
+        // "initialize" the Deck with the requested Event
         deck.go_home(event);
         
-        show_deck(relative_to, for_location, deck);
+        show_deck_window(deck);
     }
 }
 
diff --git a/src/host/host-quick-create-event.vala b/src/host/host-quick-create-event.vala
index 7b35170..bcde5e4 100644
--- a/src/host/host-quick-create-event.vala
+++ b/src/host/host-quick-create-event.vala
@@ -16,6 +16,8 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
     
     public new Component.Event? event { get; private set; default = null; }
     
+    public bool edit_required { get; private set; default = false; }
+    
     public Gtk.Widget? default_widget { get { return create_button; } }
     
     public Gtk.Widget? initial_focus { get { return details_entry; } }
@@ -143,11 +145,9 @@ public class QuickCreateEvent : Gtk.Grid, Toolkit.Card {
         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())
-        jump_to_card_by_name(CreateUpdateEvent.ID, event);
-        deck.remove_cards(iterate<Toolkit.Card>(this).to_array_list());
+        edit_required = true;
+        
+        notify_user_closed();
     }
     
     private async void create_event_async(Cancellable? cancellable) {
diff --git a/src/host/host-show-event.vala b/src/host/host-show-event.vala
index d5cd3f4..9179a95 100644
--- a/src/host/host-show-event.vala
+++ b/src/host/host-show-event.vala
@@ -25,6 +25,8 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     
     public Gtk.Widget? initial_focus { get { return close_button; } }
     
+    public bool edit_requested { get; private set; default = false; }
+    
     [GtkChild]
     private Gtk.Label summary_text;
     
@@ -47,6 +49,9 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     private Gtk.Label calendar_text;
     
     [GtkChild]
+    private Gtk.ScrolledWindow description_text_window;
+    
+    [GtkChild]
     private Gtk.Label description_text;
     
     [GtkChild]
@@ -59,10 +64,11 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     private Gtk.Button close_button = new Gtk.Button.with_mnemonic(_("_Close"));
     private Gtk.Button update_button = new Gtk.Button.with_mnemonic(_("_Edit"));
     private Gtk.Button remove_button = new Gtk.Button.with_mnemonic(_("_Delete"));
-    private Gtk.Button remove_all_button = new Gtk.Button.with_mnemonic(_("Delete A_ll Events"));
-    private Gtk.Button remove_this_button = new Gtk.Button.with_mnemonic(_("Delete _This Event"));
+    private Gtk.Label delete_label = new Gtk.Label(_("Delete"));
+    private Gtk.Button remove_all_button = new Gtk.Button.with_mnemonic(_("A_ll Events"));
+    private Gtk.Button remove_this_button = new Gtk.Button.with_mnemonic(_("_This Event"));
     private Gtk.Button remove_this_future_button = new Gtk.Button.with_mnemonic(
-        _("Delete This and _Future Events"));
+        _("This & _Future Events"));
     private Gtk.Button cancel_remove_button = new Gtk.Button.with_mnemonic(_("_Cancel"));
     
     public ShowEvent() {
@@ -89,11 +95,16 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         rotating_button_box.pack_end(FAMILY_NORMAL, update_button);
         rotating_button_box.pack_end(FAMILY_NORMAL, close_button);
         
+        delete_label.xalign = 1.0f;
+        delete_label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL);
+        rotating_button_box.pack_start(FAMILY_REMOVING, delete_label);
         rotating_button_box.pack_end(FAMILY_REMOVING, remove_this_button);
         rotating_button_box.pack_end(FAMILY_REMOVING, remove_this_future_button);
         rotating_button_box.pack_end(FAMILY_REMOVING, remove_all_button);
         rotating_button_box.pack_end(FAMILY_REMOVING, cancel_remove_button);
         
+        rotating_button_box.get_family_container(FAMILY_REMOVING).homogeneous = false;
+        
         rotating_button_box.expand = true;
         rotating_button_box.halign = Gtk.Align.FILL;
         rotating_button_box.valign = Gtk.Align.END;
@@ -113,6 +124,13 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
         event = message as Component.Event;
         assert(event != null);
         
+        description_text.bind_property("visible", description_text_window, "visible",
+            BindingFlags.SYNC_CREATE);
+        description_text.bind_property("no-show-all", description_text_window, "no-show-all",
+            BindingFlags.SYNC_CREATE);
+        
+        rotating_button_box.show_hide_family(FAMILY_REMOVING, event.is_generated_instance);
+        
         build_display();
     }
     
@@ -182,9 +200,6 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     private void on_remove_button_clicked() {
         // If recurring (and so this is a generated instance of the VEVENT, not the VEVENT itself),
         // reveal additional remove buttons
-        //
-        // TODO: Gtk.Stack would be a better widget for this animation, but it's unavailable in
-        // Glade as of GTK+ 3.12.
         if (event.is_generated_instance) {
             rotating_button_box.family = FAMILY_REMOVING;
             
@@ -211,12 +226,9 @@ public class ShowEvent : Gtk.Grid, Toolkit.Card {
     }
     
     private void on_update_button_clicked() {
-        // pass a clone of the existing event for editing
-        try {
-            jump_to_card_by_name(CreateUpdateEvent.ID, event.clone() as Component.Event);
-        } catch (Error err) {
-            notify_failure(_("Unable to update event: %s").printf(err.message));
-        }
+        edit_requested = true;
+        
+        notify_user_closed();
     }
     
     private void on_close_button_clicked() {
diff --git a/src/manager/manager-window.vala b/src/manager/manager-window.vala
index fe61b5e..08e3546 100644
--- a/src/manager/manager-window.vala
+++ b/src/manager/manager-window.vala
@@ -11,31 +11,21 @@ namespace California.Manager {
  */
 
 public class Window : Toolkit.DeckWindow {
-    private static Manager.Window? instance = null;
-    
     private CalendarList calendar_list = new CalendarList();
     
-    private Window(Gtk.Window? parent) {
-        base (parent, null);
+    private Window(Gtk.Window? window) {
+        base (window, null);
         
         deck.add_cards(iterate<Toolkit.Card>(calendar_list).to_array_list());
         Activator.prepare_deck(deck, null);
     }
     
-    public static void display(Gtk.Window? parent) {
-        // only allow one instance at a time
-        if (instance != null) {
-            instance.present_with_time(Gdk.CURRENT_TIME);
-            
-            return;
-        }
+    public static void display(Gtk.Window? window) {
+        Manager.Window instance = new Manager.Window(window);
         
-        instance = new Manager.Window(parent);
         instance.show_all();
         instance.run();
         instance.destroy();
-        
-        instance = null;
     }
     
     public override bool key_release_event(Gdk.EventKey event) {
diff --git a/src/rc/create-update-recurring.ui b/src/rc/create-update-recurring.ui
index dccd51d..1e52fc5 100644
--- a/src/rc/create-update-recurring.ui
+++ b/src/rc/create-update-recurring.ui
@@ -5,10 +5,8 @@
   <template class="CaliforniaHostCreateUpdateRecurring" parent="GtkGrid">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
-    <property name="margin_left">8</property>
-    <property name="margin_right">8</property>
-    <property name="margin_top">8</property>
-    <property name="margin_bottom">8</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
     <property name="row_spacing">6</property>
     <child>
       <object class="GtkCheckButton" id="make_recurring_checkbutton">
diff --git a/src/rc/show-event.ui b/src/rc/show-event.ui
index dd031f7..29dd4e8 100644
--- a/src/rc/show-event.ui
+++ b/src/rc/show-event.ui
@@ -5,6 +5,8 @@
   <template class="CaliforniaHostShowEvent" parent="GtkGrid">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
     <property name="row_spacing">6</property>
     <child>
       <object class="GtkLabel" id="summary_text">
@@ -27,7 +29,7 @@
       </packing>
     </child>
     <child>
-      <object class="GtkScrolledWindow" id="scrolledwindow1">
+      <object class="GtkScrolledWindow" id="description_text_window">
         <property name="visible">True</property>
         <property name="can_focus">True</property>
         <property name="margin_top">6</property>
@@ -187,6 +189,7 @@
       <object class="GtkBox" id="rotating_button_box_container">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="margin_top">8</property>
         <child>
           <placeholder/>
         </child>
diff --git a/src/toolkit/toolkit-deck-popover.vala b/src/toolkit/toolkit-deck-popover.vala
new file mode 100644
index 0000000..8f8da88
--- /dev/null
+++ b/src/toolkit/toolkit-deck-popover.vala
@@ -0,0 +1,102 @@
+/* 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 GtkPopover with special support for { link Deck}s.
+ */
+
+public class DeckPopover : Gtk.Popover {
+    public Deck deck { get; private set; }
+    
+    /**
+     * See { link Card.dismiss}
+     */
+    public signal void dismiss(bool user_request, bool final);
+    
+    private bool preserve_mode;
+    private bool forcing_mode = false;
+    
+    public DeckPopover(Gtk.Widget rel_to, Gdk.Point? for_location, Deck? starter_deck) {
+        Object (relative_to: rel_to);
+        
+        preserve_mode = modal;
+        
+        // treat "closed" signal as dismissal by user request
+        closed.connect(() => {
+            dismiss(true, true);
+        });
+        
+        notify["modal"].connect(on_modal_changed);
+        
+        this.deck = starter_deck ?? new Deck();
+        
+        if (for_location != null) {
+            Gdk.Rectangle for_location_rect = Gdk.Rectangle() { x = for_location.x, y = for_location.y,
+                width = 1, height = 1 };
+            pointing_to = for_location_rect;
+        }
+        
+        // because adding/removing cards can cause deep in Gtk.Widget the Popover to lose focus,
+        // those operations can prematurely close the Popover.  Catching these signals allow for
+        // DeckWindow to go modeless during the operation and not close.  (RotatingButtonBox has a
+        // similar issue.)
+        //
+        // TODO: This is fixed in GTK+ 3.13.6.  When 3.14 is baseline requirement, this code can
+        // be removed.
+        deck.notify["transition-running"].connect(on_transition_running_changed);
+        deck.adding_removing_cards.connect(on_adding_removing_cards);
+        deck.added_removed_cards.connect(on_added_removed_cards);
+        deck.dismiss.connect(on_deck_dismissed);
+        
+        // store Deck in box so margin can be applied
+        Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
+        box.margin = 4;
+        box.add(deck);
+        
+        add(box);
+    }
+    
+    ~DeckPopover() {
+        deck.notify["transition-running"].disconnect(on_transition_running_changed);
+        deck.adding_removing_cards.disconnect(on_adding_removing_cards);
+        deck.added_removed_cards.disconnect(on_added_removed_cards);
+        deck.dismiss.disconnect(on_deck_dismissed);
+    }
+    
+    // if the modal value changes, preserve it (unless it's changing because we're forcing it to
+    // go modal/modeless during transitions we're attempting to work around)
+    private void on_modal_changed() {
+        if (!forcing_mode)
+            preserve_mode = modal;
+    }
+    
+    private void force_mode(bool modal) {
+        forcing_mode = true;
+        this.modal = modal;
+        forcing_mode = false;
+    }
+    
+    private void on_transition_running_changed() {
+        force_mode(deck.transition_running ? false : preserve_mode);
+    }
+    
+    private void on_adding_removing_cards() {
+        force_mode(false);
+    }
+    
+    private void on_added_removed_cards() {
+        force_mode(preserve_mode);
+    }
+    
+    private void on_deck_dismissed(bool user_request, bool final) {
+        dismiss(user_request, final);
+    }
+}
+
+}
+
diff --git a/src/toolkit/toolkit-deck-window.vala b/src/toolkit/toolkit-deck-window.vala
index 44d1872..08c4331 100644
--- a/src/toolkit/toolkit-deck-window.vala
+++ b/src/toolkit/toolkit-deck-window.vala
@@ -7,13 +7,11 @@
 namespace California.Toolkit {
 
 /**
- * A GtkDialog with no visible action area.
+ * A GtkDialog with no visible action area that holds { link Deck}s.
  *
  * This is designed for UI panes that want to control their own interaction with the user (in
  * particular, button placement) but need all the benefits interaction-wise of GtkDialog.
  *
- * It's expected this will go away when we move to GTK+ 3.12 and can use GtkPopovers for these
- * interactions.
  */
 
 public class DeckWindow : Gtk.Dialog {
diff --git a/src/toolkit/toolkit-deck.vala b/src/toolkit/toolkit-deck.vala
index 6592690..c640b7b 100644
--- a/src/toolkit/toolkit-deck.vala
+++ b/src/toolkit/toolkit-deck.vala
@@ -44,6 +44,16 @@ public class Deck : Gtk.Stack {
     private Gee.HashMap<string, Card> names = new Gee.HashMap<string, Card>();
     
     /**
+     * Fired before { link Card}s are added or removed.
+     */
+    public signal void adding_removing_cards(Gee.List<Card>? adding, Gee.Collection<Card>? removing);
+    
+    /**
+     * Fired after { link Card}s are added or removed.
+     */
+    public signal void added_removed_cards(Gee.List<Card>? added, Gee.Collection<Card>? removed);
+    
+    /**
      * @see Card.dismiss
      */
     public signal void dismiss(bool user_request, bool final);
@@ -124,6 +134,8 @@ public class Deck : Gtk.Stack {
         if (cards.size == 0)
             return;
         
+        adding_removing_cards(cards, null);
+        
         // if empty, first card is home and should be made visible when added
         bool set_home_visible = size == 0;
         
@@ -153,6 +165,8 @@ public class Deck : Gtk.Stack {
             set_visible_child(home);
             home.jumped_to(null, Card.Jump.HOME, null);
         }
+        
+        added_removed_cards(cards, null);
     }
     
     /**
@@ -161,9 +175,11 @@ public class Deck : Gtk.Stack {
      * If the { link top} card is removed, the Deck will return { link home}, clearing the
      * navigation stack in the process.
      */
-    public void remove_cards(Gee.Iterable<Card> cards) {
+    public void remove_cards(Gee.Collection<Card> cards) {
         bool displaying = top != null;
         
+        adding_removing_cards(null, cards);
+        
         foreach (Card card in cards) {
             if (!names.has_key(card.card_id)) {
                 message("Card %s not found in Deck", card.card_id);
@@ -189,6 +205,8 @@ public class Deck : Gtk.Stack {
             set_visible_child(home);
             home.jumped_to(null, Card.Jump.HOME, null);
         }
+        
+        added_removed_cards(null, cards);
     }
     
     private Value? strip_null_value(Value? message) {
@@ -288,11 +306,12 @@ public class Deck : Gtk.Stack {
     private void on_card_mapped(Gtk.Widget widget) {
         Card card = (Card) widget;
         
-        if (card.default_widget != null && card.default_widget.can_default)
-            card.default_widget.grab_default();
-        
-        if (card.initial_focus != null && card.initial_focus.can_focus)
-            card.initial_focus.grab_focus();
+        if (card.default_widget != null) {
+            if (card.default_widget.can_default)
+                card.default_widget.grab_default();
+            else
+                message("Card %s specifies default widget that cannot be default", card.card_id);
+        }
     }
 }
 
diff --git a/src/toolkit/toolkit-rotating-button-box.vala b/src/toolkit/toolkit-rotating-button-box.vala
index 13778f7..07a5f53 100644
--- a/src/toolkit/toolkit-rotating-button-box.vala
+++ b/src/toolkit/toolkit-rotating-button-box.vala
@@ -33,23 +33,49 @@ public class RotatingButtonBox : Gtk.Stack {
     public string? family { get; set; }
     
     private Gee.HashMap<string, Gtk.ButtonBox> button_boxes = new Gee.HashMap<string, Gtk.ButtonBox>();
+    private Gtk.Popover? parent_popover = null;
+    private bool parent_popover_modal = false;
     
     public RotatingButtonBox() {
         homogeneous = true;
         transition_duration = SLOW_STACK_TRANSITION_DURATION_MSEC;
         transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
         
+        notify["transition-running"].connect(on_transition_running);
+        
         bind_property("visible-child-name", this, PROP_FAMILY,
             BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
     }
     
+    // unfortunately, RotatingButtonBox can cause modal Popovers to close because the focus
+    // changes from one button box to another, triggering a situation in GtkWidget where the
+    // Popover thinks it has lost focus ... this hacks around the problem by setting the popover
+    // to modeless until the transition is complete
+    //
+    // TODO: This is fixed in GTK+ 3.13.6.  When 3.14 is baseline requirement, this code can
+    // be removed.
+    private void on_transition_running() {
+        if (transition_running && parent_popover == null) {
+            // set to modeless to hack around problem
+            parent_popover = get_ancestor(typeof (Gtk.Popover)) as Gtk.Popover;
+            if (parent_popover != null) {
+                parent_popover_modal = parent_popover.modal;
+                parent_popover.modal = false;
+            }
+        } else if (!transition_running && parent_popover != null) {
+            // reset to original mode
+            parent_popover.modal = parent_popover_modal;
+            parent_popover = null;
+        }
+    }
+    
     /**
      * Pack a Gtk.Button at the start of a particular family, creating the family if necessary.
      *
      * See Gtk.Box.pack_start().
      */
-    public void pack_start(string family, Gtk.Button button) {
-        get_family_container(family).pack_start(button);
+    public void pack_start(string family, Gtk.Widget widget) {
+        get_family_container(family).pack_start(widget);
     }
     
     /**
@@ -57,8 +83,8 @@ public class RotatingButtonBox : Gtk.Stack {
      *
      * See Gtk.Box.pack_end().
      */
-    public void pack_end(string family, Gtk.Button button) {
-        get_family_container(family).pack_end(button);
+    public void pack_end(string family, Gtk.Widget widget) {
+        get_family_container(family).pack_end(widget);
     }
     
     /**
@@ -83,6 +109,33 @@ public class RotatingButtonBox : Gtk.Stack {
         
         return button_box;
     }
+    
+    /**
+     * Removes (or adds back) a family from the { link RotatingButtonBox}.
+     *
+     * The family remains under the RotatingButtonBox's control, it's simply removed from the
+     * widget heirarchy.  This is useful for sizing purposes.
+     */
+    public void show_hide_family(string family, bool show) {
+        if (!button_boxes.has_key(family))
+            return;
+        
+        Gtk.ButtonBox button_box = button_boxes.get(family);
+        
+        bool shown = false;
+        foreach (Gtk.Widget widget in  get_children()) {
+            if (widget == button_box) {
+                shown = true;
+                
+                break;
+            }
+        }
+        
+        if (show && !shown)
+            add_named(button_box, family);
+        else if (!show && shown)
+            remove(button_box);
+    }
 }
 
 }
diff --git a/src/toolkit/toolkit.vala b/src/toolkit/toolkit.vala
index 2630ed5..ca38290 100644
--- a/src/toolkit/toolkit.vala
+++ b/src/toolkit/toolkit.vala
@@ -33,19 +33,28 @@ public const bool STOP = true;
 public const bool PROPAGATE = false;
 
 private int init_count = 0;
+private Gee.Set<Gtk.Widget>? dead_pool = null;
+private Scheduled? dead_pool_gc = null;
 
 public void init() throws Error {
     if (!Unit.do_init(ref init_count))
         return;
     
+    dead_pool = new Gee.HashSet<Gtk.Widget>();
+    
     Calendar.init();
+    Collection.init();
 }
 
 public void terminate() {
     if (!Unit.do_terminate(ref init_count))
         return;
     
+    Collection.terminate();
     Calendar.terminate();
+    
+    dead_pool = null;
+    dead_pool_gc = null;
 }
 
 /**
@@ -94,4 +103,24 @@ public void set_unbusy(Gtk.Widget widget, Gdk.Cursor? unbusy_cursor) {
     toplevel.get_window().set_cursor(unbusy_cursor);
 }
 
+/**
+ * Destroy a Gtk.Widget when the event loop is idle.
+ */
+public void destroy_later(Gtk.Widget widget) {
+    if (!dead_pool.add(widget))
+        return;
+    
+    // always reschedule
+    dead_pool_gc = new Scheduled.once_at_idle(() => {
+        // traverse_safely makes a copy of the dead_pool, so filter() won't work to remove destroyed
+        // elements, but that also means we can safely remove elements from it in the iterate()
+        // handler ... need to jump through these hoops because the widget.destroy() signal handlers
+        // may turn around and add more widgets to the dead pool during traversal
+        traverse_safely<Gtk.Widget>(dead_pool).iterate((widget) => {
+            widget.destroy();
+            dead_pool.remove(widget);
+        });
+    }, Priority.LOW);
+}
+
 }
diff --git a/src/view/month/month-controller.vala b/src/view/month/month-controller.vala
index 78a47cf..12463e9 100644
--- a/src/view/month/month-controller.vala
+++ b/src/view/month/month-controller.vala
@@ -66,6 +66,11 @@ public class Controller : BaseObject, View.Controllable {
     public Calendar.Date default_date { get; protected set; }
     
     /**
+     * @inheritDoc
+     */
+    public bool in_transition { get; protected set; }
+    
+    /**
      * { link View.Palette} for the entire view.
      */
     public View.Palette palette { get; private set; }
@@ -92,6 +97,8 @@ public class Controller : BaseObject, View.Controllable {
             Toolkit.StackModel.OrderedTransitionType.SLIDE_LEFT_RIGHT, model_presentation,
             trim_presentation_from_cache, ensure_presentation_in_cache);
         
+        stack.bind_property("transition-running", this, PROP_IN_TRANSITION, BindingFlags.SYNC_CREATE);
+        
         // insert labels for days of the week across top of master grid
         for (int col = 0; col < Grid.COLS; col++) {
             Gtk.Label dow_label = new Gtk.Label(null);
@@ -189,6 +196,15 @@ public class Controller : BaseObject, View.Controllable {
     /**
      * @inheritDoc
      */
+    public Gtk.Widget? get_widget_for_date(Calendar.Date date) {
+        Grid? current_grid = get_current_month_grid();
+        
+        return current_grid != null ? current_grid.get_cell_for_date(date) : null;
+    }
+    
+    /**
+     * @inheritDoc
+     */
     public View.Container get_container() {
         return master_grid;
     }
diff --git a/src/view/view-controllable.vala b/src/view/view-controllable.vala
index 0251c9c..299df7d 100644
--- a/src/view/view-controllable.vala
+++ b/src/view/view-controllable.vala
@@ -18,6 +18,12 @@ public interface Controllable : Object {
     public const string PROP_CURRENT_LABEL = "current-label";
     public const string PROP_IS_VIEWING_TODAY = "is-viewing-today";
     public const string PROP_DEFAULT_DATE = "default-date";
+    public const string PROP_IN_TRANSITION = "in-transition";
+    
+    /**
+     * For { link execute_when_not_in_transition}.
+     */
+    public delegate void Callback(View.Controllable controller);
     
     /**
      * A short string uniquely identifying this view.
@@ -46,6 +52,11 @@ public interface Controllable : Object {
     public abstract bool is_viewing_today { get; protected set; }
     
     /**
+     * Indicates when a transition is running (such as when moving between dates).
+     */
+    public abstract bool in_transition { get; protected set; }
+    
+    /**
      * Signal from the { link Controllable} that a DATE-TIME { link Component.Event} should be
      * created with the specified initial parameters.
      */
@@ -96,6 +107,40 @@ public interface Controllable : Object {
      * when creating or displaying events).
      */
     public abstract void unselect_all();
+    
+    /**
+     * Return the GtkWidget that represents the specified { link Calendar.Date}, if available.
+     */
+    public abstract Gtk.Widget? get_widget_for_date(Calendar.Date date);
+    
+    /**
+     * Execute a { link Callback} only when the { link Controllable} is not performing a transition.
+     *
+     * If the Controllable is not in transition when called, the Callback is executed immediately.
+     * Otherwise, execution is delayed until the transition completes.
+     */
+    public void execute_when_not_transitioning(Callback callback) {
+        if (!in_transition) {
+            callback(this);
+            
+            return;
+        }
+        
+        ulong handler_id = 0;
+        handler_id = notify[PROP_IN_TRANSITION].connect(() => {
+            // watch for spurious notifications
+            if (in_transition)
+                return;
+            
+            // disable this signal
+            if (handler_id > 0)
+                disconnect(handler_id);
+            else
+                debug("Unable to disconnect in-transition notify signal handler");
+            
+            callback(this);
+        });
+    }
 }
 
 }
diff --git a/src/view/view.vala b/src/view/view.vala
index 57e3e19..584bfaa 100644
--- a/src/view/view.vala
+++ b/src/view/view.vala
@@ -18,6 +18,8 @@ public void init() throws Error {
     if (!Unit.do_init(ref init_count))
         return;
     
+    Calendar.init();
+    
     // subunit initialization
     View.Common.init();
     View.Month.init();
@@ -31,6 +33,8 @@ public void terminate() {
     View.Week.terminate();
     View.Month.terminate();
     View.Common.terminate();
+    
+    Calendar.terminate();
 }
 
 }
diff --git a/src/view/week/week-controller.vala b/src/view/week/week-controller.vala
index d3496d6..adbb00d 100644
--- a/src/view/week/week-controller.vala
+++ b/src/view/week/week-controller.vala
@@ -52,6 +52,11 @@ public class Controller : BaseObject, View.Controllable {
     public bool is_viewing_today { get; protected set; }
     
     /**
+     * @inheritDoc
+     */
+    public bool in_transition { get; protected set; }
+    
+    /**
      * { link View.Palette} for the entire hosted view.
      */
     public View.Palette palette { get; private set; }
@@ -66,6 +71,7 @@ public class Controller : BaseObject, View.Controllable {
         stack = new ViewContainer(this);
         stack.homogeneous = true;
         stack.transition_duration = Toolkit.SLOW_STACK_TRANSITION_DURATION_MSEC;
+        stack.bind_property("transition-running", this, PROP_IN_TRANSITION, BindingFlags.SYNC_CREATE);
         
         stack_model = new Toolkit.StackModel<Calendar.Week>(stack,
             Toolkit.StackModel.OrderedTransitionType.SLIDE_LEFT_RIGHT, model_presentation,
@@ -122,6 +128,15 @@ public class Controller : BaseObject, View.Controllable {
             current.unselect_all();
     }
     
+    /**
+     * @inheritDoc
+     */
+    public Gtk.Widget? get_widget_for_date(Calendar.Date date) {
+        Grid? current_grid = get_current_grid();
+        
+        return current_grid != null ? current_grid.get_all_day_cell_for_date(date) : null;
+    }
+    
     private Grid? get_current_grid() {
         return stack.get_visible_child() as Grid;
     }
diff --git a/src/view/week/week-day-pane.vala b/src/view/week/week-day-pane.vala
index e71790a..0298618 100644
--- a/src/view/week/week-day-pane.vala
+++ b/src/view/week/week-day-pane.vala
@@ -38,6 +38,11 @@ internal class DayPane : Pane, Common.InstanceContainer {
     public Calendar.WallTime? selection_end { get; private set; }
     
     /**
+     * The center point of the current selection.
+     */
+    public Gdk.Point? selection_point { get; private set; default = null; }
+    
+    /**
      * @inheritDoc
      */
     public int event_count { get { return days_events.size; } }
@@ -306,6 +311,12 @@ internal class DayPane : Pane, Common.InstanceContainer {
             ctx.rectangle(0, y, get_allocated_width(), height);
             Gdk.cairo_set_source_rgba(ctx, palette.selection);
             ctx.fill();
+            
+            selection_point = Gdk.Point();
+            selection_point.x = get_allocated_width() / 2;
+            selection_point.y = y + (height / 2);
+        } else {
+            selection_point = null;
         }
         
         return true;
diff --git a/src/view/week/week-grid.vala b/src/view/week/week-grid.vala
index 6556a02..c2141f4 100644
--- a/src/view/week/week-grid.vala
+++ b/src/view/week/week-grid.vala
@@ -383,10 +383,10 @@ internal class Grid : Gtk.Box {
         DayPane day_pane = (DayPane) widget;
         
         Calendar.ExactTimeSpan? selection_span = day_pane.get_selection_span();
-        if (selection_span == null)
+        if (selection_span == null || day_pane.selection_point == null)
             return Toolkit.PROPAGATE;
         
-        owner.request_create_timed_event(selection_span, widget, point);
+        owner.request_create_timed_event(selection_span, widget, day_pane.selection_point);
         
         return Toolkit.STOP;
     }



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