[california/wip/725767-week] Introducing EventConnectors and first impl of ButtonConnector
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [california/wip/725767-week] Introducing EventConnectors and first impl of ButtonConnector
- Date: Fri, 16 May 2014 03:12:34 +0000 (UTC)
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]