[california/wip/725767-week] Introducing EventConnectors and first impl of ButtonConnector



commit af79f55ce9b41055a691a0c0c0a26a4a705d76c6
Author: Jim Nelson <jim yorba org>
Date:   Thu May 15 19:31:41 2014 -0700

    Introducing EventConnectors and first impl of ButtonConnector

 src/Makefile.am                           |    3 +
 src/toolkit/toolkit-button-connector.vala |  298 +++++++++++++++++++++++++++++
 src/toolkit/toolkit-button-event.vala     |   75 +++++++
 src/toolkit/toolkit-event-connector.vala  |   93 +++++++++
 src/toolkit/toolkit.vala                  |   31 +++
 src/view/week/week-grid.vala              |   28 +++
 6 files changed, 528 insertions(+), 0 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 079ceca..84d5874 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -108,12 +108,15 @@ california_VALASOURCES = \
        tests/tests-quick-add.vala \
        \
        toolkit/toolkit.vala \
+       toolkit/toolkit-button-connector.vala \
+       toolkit/toolkit-button-event.vala \
        toolkit/toolkit-calendar-popup.vala \
        toolkit/toolkit-card.vala \
        toolkit/toolkit-combo-box-text-model.vala \
        toolkit/toolkit-deck.vala \
        toolkit/toolkit-deck-window.vala \
        toolkit/toolkit-editable-label.vala \
+       toolkit/toolkit-event-connector.vala \
        toolkit/toolkit-listbox-model.vala \
        toolkit/toolkit-mutable-widget.vala \
        toolkit/toolkit-popup.vala \
diff --git a/src/toolkit/toolkit-button-connector.vala b/src/toolkit/toolkit-button-connector.vala
new file mode 100644
index 0000000..9c62e70
--- /dev/null
+++ b/src/toolkit/toolkit-button-connector.vala
@@ -0,0 +1,298 @@
+/* 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 { link EventConnector} for (mouse) button events.
+ *
+ * "Raw" GDK events may be trapped by subscribing to { link pressed} and { link released}.  These
+ * signals also provide Cancellables; if set (cancelled), the event will not propagate further.
+ *
+ * Otherwise, ButtonConnector will continue monitoring raw events and convert them into friendlier
+ * signals: { link clicked}, { link double_clicked}, and { link triple_clicked}.  A complete set
+ * of press/release events are effectively translated into a single clicked event.  This relieves
+ * the application of the problem of receving a clicked event and having to wait to determine if
+ * a double-click will follow.
+ */
+
+public class ButtonConnector : EventConnector {
+    // GDK reports 250ms is used to determine if a click is a double-click (and another 250ms for
+    // triple-click), so pause just a little more than that to determine if all the clicking is
+    // done
+    private const int CLICK_DETERMINATION_DELAY_MSEC = 250;
+    
+    // The actual ButtonEvent, with some useful functionality for release timeouts
+    private class InternalButtonEvent : ButtonEvent {
+        private uint timeout_id = 0;
+        
+        public signal void release_timeout();
+        
+        public InternalButtonEvent(Gtk.Widget widget, Gdk.EventButton event) {
+            base (widget, event);
+        }
+        
+        ~InternalButtonEvent() {
+            cancel_timeout();
+        }
+        
+        private void cancel_timeout() {
+            if (timeout_id == 0)
+                return;
+            
+            Source.remove(timeout_id);
+            timeout_id = 0;
+        }
+        
+        public override void update_press(Gtk.Widget widget, Gdk.EventButton press_event) {
+            base.update_press(widget, press_event);
+            
+            cancel_timeout();
+        }
+        
+        public override void update_release(Gtk.Widget widget, Gdk.EventButton release_event) {
+            base.update_release(widget, release_event);
+            
+            cancel_timeout();
+            timeout_id = Timeout.add(CLICK_DETERMINATION_DELAY_MSEC, on_timeout);
+        }
+        
+        private bool on_timeout() {
+            timeout_id = 0;
+            
+            release_timeout();
+            
+            return false;
+        }
+    }
+    
+    private Gee.HashMap<Gtk.Widget, InternalButtonEvent> primary_states = new Gee.HashMap<
+        Gtk.Widget, InternalButtonEvent>();
+    private Gee.HashMap<Gtk.Widget, InternalButtonEvent> secondary_states = new Gee.HashMap<
+        Gtk.Widget, InternalButtonEvent>();
+    private Gee.HashMap<Gtk.Widget, InternalButtonEvent> tertiary_states = new Gee.HashMap<
+        Gtk.Widget, InternalButtonEvent>();
+    private Cancellable cancellable = new Cancellable();
+    
+    /**
+     * The "raw" "button-pressed" signal received by { link ButtonConnector}.
+     *
+     * Signal subscribers should cancel the Cancellable to prevent propagation of the event.
+     */
+    public signal void pressed(Gtk.Widget widget, Gdk.EventButton event, Cancellable cancellable);
+    
+    /**
+     * The "raw" "button-released" signal received by { link ButtonConnector}.
+     *
+     * Signal subscribers should cancel the Cancellable to prevent propagation of the event.
+     */
+    public signal void released(Gtk.Widget widget, Gdk.EventButton event, Cancellable cancellable);
+    
+    /**
+     * Fired when a button is pressed and released once.
+     */
+    public signal void clicked(ButtonEvent details);
+    
+    /**
+     * Fired when a button is pressed and released twice in succession.
+     */
+    public signal void double_clicked(ButtonEvent details);
+    
+    /**
+     * Fired when a button is pressed and released thrice in succession.
+     */
+    public signal void triple_clicked(ButtonEvent details);
+    
+    /**
+     * Create a new { link ButtonConnector} for monitoring (mouse) button events from Gtk.Widgets.
+     */
+    public ButtonConnector() {
+        base (Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK);
+    }
+    
+    /**
+     * Subclasses may override this method to hook into this event before or after the signal
+     * has fired.
+     *
+     * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
+     */
+    protected virtual bool notify_pressed(Gtk.Widget widget, Gdk.EventButton event) {
+        pressed(widget, event, cancellable);
+        
+        return stop_propagation();
+    }
+    
+    /**
+     * Subclasses may override this method to hook into this event before or after the signal
+     * has fired.
+     *
+     * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
+     */
+    protected virtual bool notify_released(Gtk.Widget widget, Gdk.EventButton event) {
+        released(widget, event, cancellable);
+        
+        return stop_propagation();
+    }
+    
+    /**
+     * Subclasses may override this method to hook into this event before or after the signal
+     * has fired.
+     */
+    protected virtual void notify_clicked(ButtonEvent details) {
+        clicked(details);
+    }
+    
+    /**
+     * Subclasses may override this method to hook into this event before or after the signal
+     * has fired.
+     */
+    protected virtual void notify_double_clicked(ButtonEvent details) {
+        double_clicked(details);
+    }
+    
+    /**
+     * Subclasses may override this method to hook into this event before or after the signal
+     * has fired.
+     */
+    protected virtual void notify_triple_clicked(ButtonEvent details) {
+        triple_clicked(details);
+    }
+    
+    protected override void connect_signals(Gtk.Widget widget) {
+        // clear this, just in case something was lingering
+        clear_widget(widget);
+        
+        widget.button_press_event.connect(on_button_event);
+        widget.button_release_event.connect(on_button_event);
+    }
+    
+    protected override void disconnect_signals(Gtk.Widget widget) {
+        clear_widget(widget);
+        
+        widget.button_press_event.disconnect(on_button_event);
+        widget.button_release_event.disconnect(on_button_event);
+    }
+    
+    private void clear_widget(Gtk.Widget widget) {
+        primary_states.unset(widget);
+        secondary_states.unset(widget);
+        tertiary_states.unset(widget);
+    }
+    
+    // Checks if the Cancellable has been cancelled, in which case return EVENT_STOP and replaces
+    // the Cancellable
+    private bool stop_propagation() {
+        if (!cancellable.is_cancelled())
+            return EVENT_PROPAGATE;
+        
+        cancellable = new Cancellable();
+        
+        return EVENT_STOP;
+    }
+    
+    private Gee.HashMap<Gtk.Widget, InternalButtonEvent>? get_states_map(Button button) {
+        switch (button) {
+            case Button.PRIMARY:
+                return primary_states;
+            
+            case Button.SECONDARY:
+                return secondary_states;
+            
+            case Button.TERTIARY:
+                return tertiary_states;
+            
+            case Button.OTHER:
+                return null;
+            
+            default:
+                assert_not_reached();
+        }
+    }
+    
+    private bool on_button_event(Gtk.Widget widget, Gdk.EventButton event) {
+        Button button = Button.from_event(event);
+        
+        return process_button_event(widget, event, button, get_states_map(button));
+    }
+    
+    private bool process_button_event(Gtk.Widget widget, Gdk.EventButton event,
+        Button button, Gee.HashMap<Gtk.Widget, InternalButtonEvent>? button_states) {
+        switch(event.type) {
+            case Gdk.EventType.BUTTON_PRESS:
+            case Gdk.EventType.2BUTTON_PRESS:
+            case Gdk.EventType.3BUTTON_PRESS:
+                // notify of raw event
+                if (notify_pressed(widget, event) == EVENT_STOP) {
+                    // drop any lingering state
+                    if (button_states != null)
+                        button_states.unset(widget);
+                    
+                    return EVENT_STOP;
+                }
+                
+                // save state for the release event, potentially updating existing state from
+                // previous press (possible for multiple press events to arrive back-to-back
+                // when double- and triple-clicking)
+                if (button_states != null) {
+                    InternalButtonEvent? details = button_states.get(widget);
+                    if (details == null) {
+                        details = new InternalButtonEvent(widget, event);
+                        details.release_timeout.connect(on_release_timeout);
+                        button_states.set(widget, details);
+                    } else {
+                        details.update_press(widget, event);
+                    }
+                }
+            break;
+            
+            case Gdk.EventType.BUTTON_RELEASE:
+                // notify of raw event
+                if (notify_released(widget, event) == EVENT_STOP) {
+                    // release lingering state
+                    if (button_states != null)
+                        button_states.unset(widget);
+                    
+                    return EVENT_STOP;
+                }
+                
+                // update saved state (if any) with release info and start timer
+                if (button_states != null) {
+                    InternalButtonEvent? details = button_states.get(widget);
+                    if (details != null)
+                        details.update_release(widget, event);
+                }
+            break;
+        }
+        
+        return EVENT_PROPAGATE;
+    }
+    
+    private void on_release_timeout(InternalButtonEvent details) {
+        // release button timed-out, meaning it's time to evaluate where the sequence stands and
+        // notify subscribers
+        switch (details.press_type) {
+            case Gdk.EventType.BUTTON_PRESS:
+                notify_clicked(details);
+            break;
+            
+            case Gdk.EventType.2BUTTON_PRESS:
+                notify_double_clicked(details);
+            break;
+            
+            case Gdk.EventType.3BUTTON_PRESS:
+                notify_triple_clicked(details);
+            break;
+        }
+        
+        // drop state, now finished with it
+        Gee.HashMap<Gtk.Widget, InternalButtonEvent>? states_map = get_states_map(details.button);
+        if (states_map != null)
+            states_map.unset(details.widget);
+    }
+}
+
+}
+
diff --git a/src/toolkit/toolkit-button-event.vala b/src/toolkit/toolkit-button-event.vala
new file mode 100644
index 0000000..1aa3f80
--- /dev/null
+++ b/src/toolkit/toolkit-button-event.vala
@@ -0,0 +1,75 @@
+/* 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 {
+
+/**
+ * Details of a (mouse) button event as reported by { link ButtonConnector}.
+ */
+
+public class ButtonEvent : BaseObject {
+    /**
+     * The Gtk.Widget the button press occurred on.
+     *
+     * Even if the button is released over a different widget, this widget is always reported
+     * by GTK and all coordinates are relative to it.
+     */
+    public Gtk.Widget widget { get; private set; }
+    
+    /**
+     * The { link Button} the event originated from.
+     */
+    public Button button { get; private set; }
+    
+    /**
+     * The last-seen button press type.
+     */
+    public Gdk.EventType press_type { get; private set; }
+    
+    /**
+     * The x,y coordinates (in { link widget}'s coordinate system} the last press occurred.
+     */
+    public Gdk.Point press_point { get; private set; }
+    
+    /**
+     * The x,y coordinates (in { link widget}'s coordinate system} the last release occurred.
+     */
+    public Gdk.Point release_point { get; private set; }
+    
+    internal ButtonEvent(Gtk.Widget widget, Gdk.EventButton press_event) {
+        this.widget = widget;
+        button = Button.from_event(press_event);
+        press_type = press_event.type;
+        press_point.x = (int) press_event.x;
+        press_point.y = (int) press_event.y;
+    }
+    
+    // Update state with the next button press
+    internal virtual void update_press(Gtk.Widget widget, Gdk.EventButton press_event) {
+        assert(this.widget == widget);
+        assert(Button.from_event(press_event) == button);
+        
+        press_type = press_event.type;
+        press_point.x = (int) press_event.x;
+        press_point.y = (int) press_event.y;
+    }
+    
+    // Update state with the next button release and start the release timer
+    internal virtual void update_release(Gtk.Widget widget, Gdk.EventButton release_event) {
+        assert(this.widget == widget);
+        assert(Button.from_event(release_event) == button);
+        
+        release_point.x = (int) release_event.x;
+        release_point.y = (int) release_event.y;
+    }
+    
+    public override string to_string() {
+        return "EventDetails: button=%s press_type=%s".printf(button.to_string(), press_type.to_string());
+    }
+}
+
+}
+
diff --git a/src/toolkit/toolkit-event-connector.vala b/src/toolkit/toolkit-event-connector.vala
new file mode 100644
index 0000000..66fa695
--- /dev/null
+++ b/src/toolkit/toolkit-event-connector.vala
@@ -0,0 +1,93 @@
+/* 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 {
+
+/**
+ * An EventConnector is a type of signalling mechanism for specific user-input events.
+ *
+ * Gtk.Widgets are connected to EventConnector via { link connect_to}.  EventConnector signals can
+ * then monitored for specific events originating from all the connected widgets.  This promotes
+ * reuse of code, as EventConenctor objects may be shared among disparate widgets, or separate
+ * instances for each, with each EventConnector (or a custom subclass) able to maintain its own
+ * state rather than having to pollute a container widget's space with its own concerns.
+ *
+ * In general, EventConnectors will not work with NO_WINDOW widgets.  Place them in a Gtk.EventBox
+ * and connect this object to that.
+ */
+
+public abstract class EventConnector : BaseObject {
+    // helper consts for subclasses
+    protected const bool EVENT_PROPAGATE = false;
+    protected const bool EVENT_STOP = true;
+    
+    private Gdk.EventMask event_mask;
+    private Gee.HashSet<Gtk.Widget> widgets = new Gee.HashSet<Gtk.Widget>();
+    
+    protected EventConnector(Gdk.EventMask event_mask) {
+        this.event_mask = event_mask;
+    }
+    
+    ~EventConnector() {
+        // use to_array() to avoid iterator issues as widgets are removed
+        foreach (Gtk.Widget widget in widgets.to_array())
+            disconnect_from(widget);
+    }
+    
+    /**
+     * Have this { link EventConnector} monitor the widget for the connector's specific events.
+     */
+    public void connect_to(Gtk.Widget widget) {
+        // don't continue if already connected
+        if (!widgets.add(widget))
+            return;
+        
+        widget.add_events(event_mask);
+        connect_signals(widget);
+        widget.destroy.connect(on_widget_destroy);
+    }
+    
+    /**
+     * Have this { link EventConnector} stop monitoring the widget for the connector's specific
+     * events.
+     *
+     * If the widget is destroyed, EventConnector will automatically stop monitoring it.
+     */
+    public void disconnect_from(Gtk.Widget widget) {
+        // don't disconnect if not connected
+        if (!widgets.remove(widget))
+            return;
+        
+        // can't remove event mask safely, so just don't
+        disconnect_signals(widget);
+        widget.destroy.disconnect(on_widget_destroy);
+    }
+    
+    private void on_widget_destroy(Gtk.Widget widget) {
+        disconnect_from(widget);
+    }
+    
+    /**
+     * Subclasses should use this method to connect to their appropriate signals.
+     *
+     * The event mask is updated automatically, so that's not necessary.
+     */
+    protected abstract void connect_signals(Gtk.Widget widget);
+    
+    /**
+     * Subclasses should use this method to disconnect the signals they connected to.
+     *
+     * This is also a good time to clean up any lingering state.
+     */
+    protected abstract void disconnect_signals(Gtk.Widget widget);
+    
+    public override string to_string() {
+        return get_class().get_type().name();
+    }
+}
+
+}
+
diff --git a/src/toolkit/toolkit.vala b/src/toolkit/toolkit.vala
index ec84471..5661dd8 100644
--- a/src/toolkit/toolkit.vala
+++ b/src/toolkit/toolkit.vala
@@ -22,6 +22,37 @@ public const int DEFAULT_STACK_TRANSITION_DURATION_MSEC = 300;
  */
 public const int SLOW_STACK_TRANSITION_DURATION_MSEC = 500;
 
+/**
+ * Enumeration for (mouse) buttons.
+ *
+ * @see ButtonConnector
+ */
+public enum Button {
+    PRIMARY,
+    SECONDARY,
+    TERTIARY,
+    OTHER;
+    
+    /**
+     * Converts the button field of a Gdk.EventButton to a { link Button} enumeration.
+     */
+    public static Button from_event(Gdk.EventButton event) {
+        switch (event.button) {
+            case 1:
+                return PRIMARY;
+            
+            case 3:
+                return SECONDARY;
+            
+            case 2:
+                return TERTIARY;
+            
+            default:
+                return OTHER;
+        }
+    }
+}
+
 private int init_count = 0;
 
 public void init() throws Error {
diff --git a/src/view/week/week-grid.vala b/src/view/week/week-grid.vala
index ffeb93a..799ac61 100644
--- a/src/view/week/week-grid.vala
+++ b/src/view/week/week-grid.vala
@@ -41,6 +41,7 @@ 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 Toolkit.ButtonConnector day_pane_button_connector = new Toolkit.ButtonConnector();
     
     public Grid(Controller owner, Calendar.Week week) {
         Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
@@ -82,6 +83,7 @@ internal class Grid : Gtk.Box {
             
             DayPane pane = new DayPane(this, date);
             pane.expand = true;
+            day_pane_button_connector.connect_to(pane);
             pane_grid.attach(pane, col, 0, 1, 1);
             
             date_to_panes.set(date, pane);
@@ -96,6 +98,11 @@ internal class Grid : Gtk.Box {
         scrolled_panes.add(pane_grid);
         pack_end(scrolled_panes, true, true, 0);
         
+        // 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);
+        day_pane_button_connector.triple_clicked.connect(on_day_pane_triple_clicked);
+        
         // set up calendar subscriptions for the week
         subscriptions = new Backing.CalendarSubscriptionManager(
             new Calendar.ExactTimeSpan.from_span(week, Calendar.Timezone.local));
@@ -164,6 +171,27 @@ internal class Grid : Gtk.Box {
                 day_pane.remove_event(event);
         }
     }
+    
+    private void on_day_pane_clicked(Toolkit.ButtonEvent details) {
+        if (details.button != Toolkit.Button.PRIMARY)
+            return;
+        
+        debug("clicked");
+    }
+    
+    private void on_day_pane_double_clicked(Toolkit.ButtonEvent details) {
+        if (details.button != Toolkit.Button.PRIMARY)
+            return;
+        
+        debug("double clicked");
+    }
+    
+    private void on_day_pane_triple_clicked(Toolkit.ButtonEvent details) {
+        if (details.button != Toolkit.Button.PRIMARY)
+            return;
+        
+        debug("triple clicked");
+    }
 }
 
 }


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