[california/wip/730601-week-drag] First code for adding click+drag to week view



commit 69ad83f865b49ec9b0ed59225d19ab949fe67fbb
Author: Jim Nelson <jim yorba org>
Date:   Fri May 23 19:25:28 2014 -0700

    First code for adding click+drag to week view

 src/Makefile.am                           |    2 +
 src/toolkit/toolkit-button-connector.vala |   19 +++---
 src/toolkit/toolkit-button-event.vala     |   33 ++++++++-
 src/toolkit/toolkit-motion-connector.vala |  107 +++++++++++++++++++++++++++++
 src/toolkit/toolkit-motion-event.vala     |   69 ++++++++++++++++++
 src/view/week/week-day-pane.vala          |   66 ++++++++++++++++--
 src/view/week/week-grid.vala              |   43 +++++++++++-
 7 files changed, 317 insertions(+), 22 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 0999a5c..67612c2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -118,6 +118,8 @@ california_VALASOURCES = \
        toolkit/toolkit-editable-label.vala \
        toolkit/toolkit-event-connector.vala \
        toolkit/toolkit-listbox-model.vala \
+       toolkit/toolkit-motion-connector.vala \
+       toolkit/toolkit-motion-event.vala \
        toolkit/toolkit-mutable-widget.vala \
        toolkit/toolkit-popup.vala \
        toolkit/toolkit-stack-model.vala \
diff --git a/src/toolkit/toolkit-button-connector.vala b/src/toolkit/toolkit-button-connector.vala
index 915134d..eac3765 100644
--- a/src/toolkit/toolkit-button-connector.vala
+++ b/src/toolkit/toolkit-button-connector.vala
@@ -80,10 +80,11 @@ public class ButtonConnector : EventConnector {
     /**
      * The "raw" "button-pressed" signal received by { link ButtonConnector}.
      *
+     * TODO:
      * Signal subscribers should cancel the Cancellable to prevent propagation of the event.
      * This will prevent the various "clicked" signals from firing.
      */
-    public signal void pressed(Gtk.Widget widget, Gdk.EventButton event, Cancellable cancellable);
+    public signal void pressed(ButtonEvent details);
     
     /**
      * The "raw" "button-released" signal received by { link ButtonConnector}.
@@ -91,7 +92,7 @@ public class ButtonConnector : EventConnector {
      * Signal subscribers should cancel the Cancellable to prevent propagation of the event.
      * This will prevent the various "clicked" signals from firing.
      */
-    public signal void released(Gtk.Widget widget, Gdk.EventButton event, Cancellable cancellable);
+    public signal void released(ButtonEvent details);
     
     /**
      * Fired when a button is pressed and released once.
@@ -137,8 +138,8 @@ public class ButtonConnector : EventConnector {
      *
      * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
      */
-    protected virtual bool notify_pressed(Gtk.Widget widget, Gdk.EventButton event) {
-        pressed(widget, event, cancellable);
+    protected virtual bool notify_pressed(ButtonEvent details) {
+        pressed(details);
         
         return stop_propagation();
     }
@@ -149,8 +150,8 @@ public class ButtonConnector : EventConnector {
      *
      * @return { link EVENT_STOP} or { link EVENT_PROPAGATE}.
      */
-    protected virtual bool notify_released(Gtk.Widget widget, Gdk.EventButton event) {
-        released(widget, event, cancellable);
+    protected virtual bool notify_released(ButtonEvent details) {
+        released(details);
         
         return stop_propagation();
     }
@@ -231,7 +232,7 @@ public class ButtonConnector : EventConnector {
     }
     
     private bool on_button_event(Gtk.Widget widget, Gdk.EventButton event) {
-        Button button = Button.from_event(event);
+        Button button = Button.from_button_event(event);
         
         return process_button_event(widget, event, button, get_states_map(button));
     }
@@ -243,7 +244,7 @@ public class ButtonConnector : EventConnector {
             case Gdk.EventType.2BUTTON_PRESS:
             case Gdk.EventType.3BUTTON_PRESS:
                 // notify of raw event
-                if (notify_pressed(widget, event) == EVENT_STOP) {
+                if (notify_pressed(new ButtonEvent(widget, event)) == EVENT_STOP) {
                     // drop any lingering state
                     if (button_states != null)
                         button_states.unset(widget);
@@ -268,7 +269,7 @@ public class ButtonConnector : EventConnector {
             
             case Gdk.EventType.BUTTON_RELEASE:
                 // notify of raw event
-                if (notify_released(widget, event) == EVENT_STOP) {
+                if (notify_released(new ButtonEvent(widget, event)) == EVENT_STOP) {
                     // release lingering state
                     if (button_states != null)
                         button_states.unset(widget);
diff --git a/src/toolkit/toolkit-button-event.vala b/src/toolkit/toolkit-button-event.vala
index 917a54f..775267e 100644
--- a/src/toolkit/toolkit-button-event.vala
+++ b/src/toolkit/toolkit-button-event.vala
@@ -20,7 +20,7 @@ public enum Button {
     /**
      * Converts the button field of a Gdk.EventButton to a { link Button} enumeration.
      */
-    public static Button from_event(Gdk.EventButton event) {
+    public static Button from_button_event(Gdk.EventButton event) {
         switch (event.button) {
             case 1:
                 return PRIMARY;
@@ -35,6 +35,31 @@ public enum Button {
                 return OTHER;
         }
     }
+    
+    /**
+     * Returns the Gdk.ModifierType corresponding to this { link Button}.
+     *
+     * { link OTHER} merely means any button not { link PRIMARY}, { link SECONDARY}, or
+     * { link TERTIARY}.
+     */
+    public Gdk.ModifierType get_modifier_mask() {
+        switch (this) {
+            case PRIMARY:
+                return Gdk.ModifierType.BUTTON1_MASK;
+            
+            case SECONDARY:
+                return Gdk.ModifierType.BUTTON2_MASK;
+            
+            case TERTIARY:
+                return Gdk.ModifierType.BUTTON3_MASK;
+            
+            case OTHER:
+                return Gdk.ModifierType.BUTTON4_MASK | Gdk.ModifierType.BUTTON5_MASK;
+            
+            default:
+                assert_not_reached();
+        }
+    }
 }
 
 /**
@@ -74,7 +99,7 @@ public class ButtonEvent : BaseObject {
     
     internal ButtonEvent(Gtk.Widget widget, Gdk.EventButton press_event) {
         this.widget = widget;
-        button = Button.from_event(press_event);
+        button = Button.from_button_event(press_event);
         press_type = press_event.type;
         _press_point.x = (int) press_event.x;
         _press_point.y = (int) press_event.y;
@@ -83,7 +108,7 @@ public class ButtonEvent : BaseObject {
     // 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);
+        assert(Button.from_button_event(press_event) == button);
         
         press_type = press_event.type;
         _press_point.x = (int) press_event.x;
@@ -93,7 +118,7 @@ public class ButtonEvent : BaseObject {
     // 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);
+        assert(Button.from_button_event(release_event) == button);
         
         _release_point.x = (int) release_event.x;
         _release_point.y = (int) release_event.y;
diff --git a/src/toolkit/toolkit-motion-connector.vala b/src/toolkit/toolkit-motion-connector.vala
new file mode 100644
index 0000000..515dfcb
--- /dev/null
+++ b/src/toolkit/toolkit-motion-connector.vala
@@ -0,0 +1,107 @@
+/* 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 pointer (mouse) motion events, including the pointer entering and
+ * exiting the widget's space.
+ */
+
+public class MotionConnector : EventConnector {
+    /**
+     * Fired when the pointer (mouse cursor) enters the Gtk.Widget.
+     */
+    public signal void entered(MotionEvent event);
+    
+    /**
+     * Fired when the pointer (mouse cursor) leaves the Gtk.Widget.
+     */
+    public signal void exited(MotionEvent event);
+    
+    /**
+     * Fired when the pointer (mouse cursor) moves across the Gtk.Widget.
+     *
+     * @see button_motion
+     */
+    public signal void motion(MotionEvent event);
+    
+    /**
+     * Fired when the pointer (mouse cursor) moves across the Gtk.Widget while a button is pressed.
+     *
+     * @see motion
+     */
+    public signal void button_motion(MotionEvent event);
+    
+    public MotionConnector() {
+        base (Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.ENTER_NOTIFY_MASK | 
Gdk.EventMask.LEAVE_NOTIFY_MASK);
+    }
+    
+    /**
+     * Subclasses may override this call to update state before or after the signal fires.
+     */
+    protected void notify_entered(MotionEvent event) {
+        entered(event);
+    }
+    
+    /**
+     * Subclasses may override this call to update state before or after the signal fires.
+     */
+    protected void notify_exited(MotionEvent event) {
+        exited(event);
+    }
+    
+    /**
+     * Subclasses may override this call to update state before or after the signal fires.
+     */
+    protected void notify_motion(MotionEvent event) {
+        motion(event);
+    }
+    
+    /**
+     * Subclasses may override this call to update state before or after the signal fires.
+     */
+    protected void notify_button_motion(MotionEvent event) {
+        button_motion(event);
+    }
+    
+    protected override void connect_signals(Gtk.Widget widget) {
+        widget.motion_notify_event.connect(on_motion_notify_event);
+        widget.enter_notify_event.connect(on_enter_notify_event);
+        widget.leave_notify_event.connect(on_leave_notify_event);
+    }
+    
+    protected override void disconnect_signals(Gtk.Widget widget) {
+        widget.motion_notify_event.disconnect(on_motion_notify_event);
+        widget.enter_notify_event.disconnect(on_enter_notify_event);
+        widget.leave_notify_event.disconnect(on_leave_notify_event);
+    }
+    
+    private bool on_motion_notify_event(Gtk.Widget widget, Gdk.EventMotion event) {
+        MotionEvent motion_event = new MotionEvent(widget, event);
+        
+        notify_motion(motion_event);
+        if (motion_event.is_any_button_pressed())
+            notify_button_motion(motion_event);
+        
+        return EVENT_PROPAGATE;
+    }
+    
+    private bool on_enter_notify_event(Gtk.Widget widget, Gdk.EventCrossing event) {
+        notify_entered(new MotionEvent.for_crossing(widget, event));
+        
+        return EVENT_PROPAGATE;
+    }
+    
+    private bool on_leave_notify_event(Gtk.Widget widget, Gdk.EventCrossing event) {
+        notify_entered(new MotionEvent.for_crossing(widget, event));
+        
+        return EVENT_PROPAGATE;
+    }
+}
+
+}
+
diff --git a/src/toolkit/toolkit-motion-event.vala b/src/toolkit/toolkit-motion-event.vala
new file mode 100644
index 0000000..0142fbd
--- /dev/null
+++ b/src/toolkit/toolkit-motion-event.vala
@@ -0,0 +1,69 @@
+/* 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 pointer (mouse) motion event, including entering and leaving a widget.
+ */
+
+public class MotionEvent : BaseObject {
+    /**
+     * The Gtk.Widget in question.
+     */
+    public Gtk.Widget widget { get; private set; }
+    
+    /**
+     * The pointer location at the time of the event.
+     */
+    private Gdk.Point _location = Gdk.Point();
+    public Gdk.Point location { get { return _location; } }
+    
+    /**
+     * The state of the modifier keys at the time of the event.
+     */
+    public Gdk.ModifierType modifiers { get; private set; }
+    
+    internal MotionEvent(Gtk.Widget widget, Gdk.EventMotion event) {
+        this.widget = widget;
+        _location.x = (int) event.x;
+        _location.y = (int) event.y;
+        modifiers = event.state;
+    }
+    
+    internal MotionEvent.for_crossing(Gtk.Widget widget, Gdk.EventCrossing event) {
+        this.widget = widget;
+        _location.x = (int) event.x;
+        _location.y = (int) event.y;
+        modifiers = event.state;
+    }
+    
+    /**
+     * Returns true if the { link Button} is pressed at the time of this event.
+     */
+    public bool is_button_pressed(Button button) {
+        return (modifiers & button.get_modifier_mask()) != 0;
+    }
+    
+    /**
+     * Returns true if any button is pressed at the time of this event.
+     */
+    public bool is_any_button_pressed() {
+        return (modifiers &
+            (Gdk.ModifierType.BUTTON1_MASK
+            | Gdk.ModifierType.BUTTON1_MASK
+            | Gdk.ModifierType.BUTTON1_MASK
+            | Gdk.ModifierType.BUTTON1_MASK
+            | Gdk.ModifierType.BUTTON1_MASK)) != 0;
+    }
+    
+    public override string to_string() {
+        return "MotionEvent %d,%d".printf(location.x, location.y);
+    }
+}
+
+}
+
diff --git a/src/view/week/week-day-pane.vala b/src/view/week/week-day-pane.vala
index 6a29494..82be367 100644
--- a/src/view/week/week-day-pane.vala
+++ b/src/view/week/week-day-pane.vala
@@ -16,7 +16,8 @@ namespace California.View.Week {
 internal class DayPane : Pane, Common.InstanceContainer {
     public const string PROP_OWNER = "owner";
     public const string PROP_DATE = "date";
-    public const string PROP_SELECTED = "selected";
+    public const string PROP_SELECTION_STATE = "selection-start";
+    public const string PROP_SELECTION_END = "selection-end";
     
     // No matter how wide the event is in the day, always leave a little peeking out so the hour/min
     // lines are visible
@@ -24,7 +25,15 @@ internal class DayPane : Pane, Common.InstanceContainer {
     
     public Calendar.Date date { get; set; }
     
-    public bool selected { get; set; default = false; }
+    /**
+     * Where the current selection starts, if any.
+     */
+    public Calendar.WallTime? selection_start { get; private set; }
+    
+    /**
+     * Where the current selection ends, if any.
+     */
+    public Calendar.WallTime? selection_end { get; private set; }
     
     /**
      * @inheritDoc
@@ -45,7 +54,7 @@ internal class DayPane : Pane, Common.InstanceContainer {
         this.date = date;
         
         notify[PROP_DATE].connect(queue_draw);
-        notify[PROP_SELECTED].connect(queue_draw);
+        
         Calendar.System.instance.is_24hr_changed.connect(queue_draw);
         Calendar.System.instance.today_changed.connect(on_today_changed);
         
@@ -136,14 +145,42 @@ internal class DayPane : Pane, Common.InstanceContainer {
         return null;
     }
     
+    public void update_selection(Calendar.WallTime wall_time) {
+        if (selection_start == null) {
+            selection_start = wall_time;
+            selection_end = null;
+        } else {
+            selection_end = wall_time;
+        }
+        
+        queue_draw();
+    }
+    
+    public Calendar.ExactTimeSpan? get_selection_span() {
+        if (selection_start == null || selection_end == null)
+            return null;
+        
+        return new Calendar.ExactTimeSpan(
+            new Calendar.ExactTime(Calendar.Timezone.local, date, selection_start),
+            new Calendar.ExactTime(Calendar.Timezone.local, date, selection_end)
+        );
+    }
+    
+    public void clear_selection() {
+        if (selection_start == null && selection_end == null)
+            return;
+        
+        selection_start = null;
+        selection_end = null;
+        
+        queue_draw();
+    }
+    
     // note that a painter's algorithm should be used here: background should be painted before
     // calling base method, and foreground afterward
     protected override bool on_draw(Cairo.Context ctx) {
-        // shade background color if this is current day or selected
-        if (selected) {
-            Gdk.cairo_set_source_rgba(ctx, Palette.instance.selection);
-            ctx.paint();
-        } else if (date.equal_to(Calendar.System.today)) {
+        // shade background color if this is current day
+        if (date.equal_to(Calendar.System.today)) {
             Gdk.cairo_set_source_rgba(ctx, Palette.instance.current_day);
             ctx.paint();
         }
@@ -211,6 +248,19 @@ internal class DayPane : Pane, Common.InstanceContainer {
             ctx.stroke();
         }
         
+        // draw selection rectangle
+        if (selection_start != null && selection_end != null) {
+            int start_y = get_line_y(selection_start);
+            int end_y = get_line_y(selection_end);
+            
+            int y = int.min(start_y, end_y);
+            int height = int.max(start_y, end_y) - y;
+            
+            ctx.rectangle(0, y, get_allocated_width(), height);
+            Gdk.cairo_set_source_rgba(ctx, Palette.instance.selection);
+            ctx.fill();
+        }
+        
         return true;
     }
     
diff --git a/src/view/week/week-grid.vala b/src/view/week/week-grid.vala
index bf546f1..45359de 100644
--- a/src/view/week/week-grid.vala
+++ b/src/view/week/week-grid.vala
@@ -44,6 +44,8 @@ internal class Grid : Gtk.Box {
     private Gee.HashMap<Calendar.Date, AllDayCell> date_to_all_day = new Gee.HashMap<Calendar.Date,
         AllDayCell>();
     private Toolkit.ButtonConnector instance_container_button_connector = new Toolkit.ButtonConnector();
+    private Toolkit.MotionConnector day_pane_motion_connector = new Toolkit.MotionConnector();
+    private Toolkit.MotionConnector all_day_cell_motion_connector = new Toolkit.MotionConnector();
     private Gtk.ScrolledWindow scrolled_panes;
     private Gtk.Widget right_spacer;
     private bool vadj_init = false;
@@ -104,6 +106,7 @@ internal class Grid : Gtk.Box {
             // label and the day panes
             AllDayCell all_day_cell = new AllDayCell(this, date);
             instance_container_button_connector.connect_to(all_day_cell);
+            all_day_cell_motion_connector.connect_to(all_day_cell);
             top_grid.attach(all_day_cell, col, 1, 1, 1);
             
             // save mapping
@@ -112,6 +115,7 @@ internal class Grid : Gtk.Box {
             DayPane pane = new DayPane(this, date);
             pane.expand = true;
             instance_container_button_connector.connect_to(pane);
+            day_pane_motion_connector.connect_to(pane);
             pane_grid.attach(pane, col, 1, 1, 1);
             
             // save mapping
@@ -133,10 +137,15 @@ internal class Grid : Gtk.Box {
         scrolled_panes.get_vscrollbar().realize.connect(on_realloc_right_spacer);
         scrolled_panes.get_vscrollbar().size_allocate.connect(on_realloc_right_spacer);
         
-        // connect panes' event signal handlers
+        // connect panes' button event signal handlers
+        instance_container_button_connector.released.connect(on_instance_container_button_released);
         instance_container_button_connector.clicked.connect(on_instance_container_clicked);
         instance_container_button_connector.double_clicked.connect(on_instance_container_double_clicked);
         
+        // connect to motion event handlers
+        all_day_cell_motion_connector.button_motion.connect(on_all_day_cell_button_motion);
+        day_pane_motion_connector.motion.connect(on_day_pane_motion);
+        
         // set up calendar subscriptions for the week
         subscriptions = new Backing.CalendarSubscriptionManager(
             new Calendar.ExactTimeSpan.from_span(week, Calendar.Timezone.local));
@@ -338,6 +347,38 @@ internal class Grid : Gtk.Box {
         owner.request_create_all_day_event(instance_container.contained_span, instance_container,
             details.press_point);
     }
+    
+    private void on_instance_container_button_released(Toolkit.ButtonEvent details) {
+        DayPane? day_pane = details.widget as DayPane;
+        if (day_pane == null)
+            return;
+        
+        Calendar.ExactTimeSpan? selection_span = day_pane.get_selection_span();
+        if (selection_span == null)
+            return;
+        
+        // clear selection on button release, always
+        day_pane.clear_selection();
+        
+        owner.request_create_timed_event(selection_span, details.widget, details.release_point);
+    }
+    
+    private void on_all_day_cell_button_motion(Toolkit.MotionEvent details) {
+        if (!details.is_button_pressed(Toolkit.Button.PRIMARY))
+            return;
+        
+        debug("cell");
+    }
+    
+    private void on_day_pane_motion(Toolkit.MotionEvent details) {
+        DayPane day_pane = (DayPane) details.widget;
+        
+        // only update selection as long as 
+        if (details.is_button_pressed(Toolkit.Button.PRIMARY))
+            day_pane.update_selection(day_pane.get_wall_time(details.location.y));
+        else
+            day_pane.clear_selection();
+    }
 }
 
 }


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