[Sugar-devel] [PATCH clock 2/2] New feature: grab the hands of the clock

Manuel Quiñones manuq at laptop.org
Fri Dec 30 10:00:54 EST 2011


This is a feature often requested by pedagogues.  Moving the hands of
the clock is very useful for the children to learn the relation of
hours, minutes, and seconds.

This patch needs the previously sent patch for porting to Cairo:
http://lists.sugarlabs.org/archive/sugar-devel/2011-December/034993.html

Adds a new toggle button in the toolbar, with the graphic of a hand.
When active, the hands of the clock can be grabbed.  When deactivated,
the clock returns to the actual hour.

In the future, it would be good to update the displayed time in the
label with the customized time, and use the talking button to say the
customized time.

Signed-off-by: Manuel Quiñones <manuq at laptop.org>
---
 clock.py       |  193 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 icons/grab.svg |    7 ++
 2 files changed, 177 insertions(+), 23 deletions(-)
 create mode 100644 icons/grab.svg

diff --git a/clock.py b/clock.py
index 7635b2e..0bae06c 100755
--- a/clock.py
+++ b/clock.py
@@ -309,6 +309,12 @@ class ClockActivity(activity.Activity):
         button.connect("toggled", self._speak_time_clicked_cb)
         display_toolbar.insert(button, -1)
 
+        # Button to toggle drag & drop
+        button = ToggleToolButton("grab")
+        button.set_tooltip(_("Toolbar", "Grab the hands"))
+        button.connect("toggled", self._dragdrop_clicked_cb)
+        display_toolbar.insert(button, -1)
+
     def _make_display(self):
         """Prepare the display of the clock.
 
@@ -358,6 +364,7 @@ class ClockActivity(activity.Activity):
         or digital).
         """
         self._clock.set_display_mode(display_mode)
+        self._clock.queue_draw()
 
     def _write_time_clicked_cb(self, button):
         """The user clicked on the "write time" button to print the
@@ -378,6 +385,9 @@ class ClockActivity(activity.Activity):
         if self._speak_time:
             self._write_and_speak(True)
 
+    def _dragdrop_clicked_cb(self, button):
+        self._clock.change_grab_hands_mode(button.get_active())
+
     def _minutes_changed_cb(self, clock):
         """Minutes have changed on the clock face: we have to update
         the display of the time in full letters if the user has chosen
@@ -481,10 +491,34 @@ class ClockFace(gtk.DrawingArea):
         self._mode = _MODE_SIMPLE_CLOCK
 
         # SVG Background handle
-        self._svg_handle = None
+        self._svg_handle = rsvg.Handle(file="clock.svg")
 
+        # This are calculated on widget resize
+        self._center_x = None
+        self._center_y = None
+        self._width = None
+        self._height = None
         self._radius = -1
         self._line_width = 2
+        self._hour_size = None
+        self._minutes_size = None
+        self._seconds_size = None
+
+        self._hour_angle = None
+        self._minutes_angle = None
+        self._seconds_angle = None
+
+        # When dragging a hand, this is the name of the hand.  If
+        # None, it means that no hand is being drag.
+        self._dragging_hand = None
+
+        # In grab hands mode, the clock is not updated each second.
+        # Instead, it is stopped and the child can drag the hands.
+        self._grab_hands_mode = False
+
+        # Tolerance for dragging hands, in radians.  Try to change
+        # this value to improve dragging:
+        self._angle_e = 0.3
 
         # Color codes (approved colors for XO screen:
         # http://wiki.laptop.org/go/XO_colors)
@@ -509,7 +543,9 @@ class ClockFace(gtk.DrawingArea):
         self.connect("size-allocate", self._size_allocate_cb)
 
         # The masks to capture the events we are interested in
-        self.add_events(gdk.EXPOSURE_MASK | gdk.VISIBILITY_NOTIFY_MASK)
+        self.add_events(gdk.EXPOSURE_MASK | gdk.VISIBILITY_NOTIFY_MASK
+            | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
+            | gtk.gdk.BUTTON1_MOTION_MASK)
 
         # Define a new signal to notify the application when minutes
         # change.  If the user wants to display the time in full
@@ -518,12 +554,38 @@ class ClockFace(gtk.DrawingArea):
         gobject.signal_new("time_minute", ClockFace,
           gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
 
+        # Event handlers for drag & drop:
+        self._press_id = None
+        self._motion_id = None
+        self._release_id = None
+
     def set_display_mode(self, mode):
         """Set the type of clock to display (simple, nice, digital).
         'mode' is one of MODE_XXX_CLOCK constants.
         """
         self._mode = mode
 
+    def change_grab_hands_mode(self, start_dragging):
+        """Connect or disconnect the callbacks for doing drag & drop
+        of the hands of the clock.
+        """
+        if start_dragging:
+            self._grab_hands_mode = True
+            self._press_id = self.connect("button-press-event",
+                                          self._press_cb)
+            self._motion_id = self.connect("motion-notify-event",
+                                           self._motion_cb)
+            self._release_id = self.connect("button-release-event",
+                                        self._release_cb)
+        else:
+            self._grab_hands_mode = False
+            self.disconnect(self._press_id)
+            self.disconnect(self._motion_id)
+            self.disconnect(self._release_id)
+
+            # Update again the clock every seconds.
+            gobject.timeout_add(1000, self._update_cb)
+
     def _size_allocate_cb(self, widget, allocation):
         """We know the size of the widget on the screen, so we keep
         the parameters which are important for our rendering (center
@@ -537,9 +599,9 @@ class ClockFace(gtk.DrawingArea):
         self._width = allocation.width
         self._height = allocation.height
         self._line_width = int(self._radius / 150)
-
-        # Reload the svg handle
-        self._svg_handle = rsvg.Handle(file="clock.svg")
+        self._hour_size = self._radius * 0.5
+        self._minutes_size = self._radius * 0.8
+        self._seconds_size = self._radius * 0.7
 
         self.initialized = True
 
@@ -729,10 +791,6 @@ class ClockFace(gtk.DrawingArea):
     def _draw_hands(self):
         """Draw the hands of the analog clocks.
         """
-        hours = self._time.hour
-        minutes = self._time.minute
-        seconds = self._time.second
-
         cr = self.window.cairo_create()
         cr.set_line_cap(cairo.LINE_CAP_ROUND)
 
@@ -742,10 +800,10 @@ class ClockFace(gtk.DrawingArea):
         cr.set_source_rgba(*style.Color(self._COLOR_HOURS).get_rgba())
         cr.set_line_width(8 * self._line_width)
         cr.move_to(self._center_x, self._center_y)
-        cr.line_to(int(self._center_x + self._radius * 0.5 *
-            math.sin(math.pi / 6 * hours + math.pi / 360 * minutes)),
-            int(self._center_y + self._radius * 0.5 *
-            - math.cos(math.pi / 6 * hours + math.pi / 360 * minutes)))
+        sin, cos = math.sin(self._hour_angle), math.cos(self._hour_angle)
+        cr.line_to(
+            int(self._center_x + self._hour_size * sin),
+            int(self._center_y - self._hour_size * cos))
         cr.stroke()
 
         # Minute hand:
@@ -753,10 +811,10 @@ class ClockFace(gtk.DrawingArea):
         cr.set_source_rgba(*style.Color(self._COLOR_MINUTES).get_rgba())
         cr.set_line_width(6 * self._line_width)
         cr.move_to(self._center_x, self._center_y)
-        cr.line_to(int(self._center_x + self._radius * 0.8 *
-                math.sin(math.pi / 30 * minutes)),
-                   int(self._center_y + self._radius * 0.8 *
-                - math.cos(math.pi / 30 * minutes)))
+        sin, cos = math.sin(self._minutes_angle), math.cos(self._minutes_angle)
+        cr.line_to(
+            int(self._center_x + self._minutes_size * sin),
+            int(self._center_y - self._minutes_size * cos))
         cr.stroke()
 
         # Seconds hand:
@@ -764,10 +822,10 @@ class ClockFace(gtk.DrawingArea):
         cr.set_source_rgba(*style.Color(self._COLOR_SECONDS).get_rgba())
         cr.set_line_width(2 * self._line_width)
         cr.move_to(self._center_x, self._center_y)
-        cr.line_to(int(self._center_x + self._radius * 0.7 *
-                math.sin(math.pi / 30 * seconds)),
-                int(self._center_y + self._radius * 0.7 *
-                - math.cos(math.pi / 30 * seconds)))
+        sin, cos = math.sin(self._seconds_angle), math.cos(self._seconds_angle)
+        cr.line_to(
+            int(self._center_x + self._seconds_size * sin),
+            int(self._center_y - self._seconds_size * cos))
         cr.stroke()
 
     def _draw_numbers(self):
@@ -803,12 +861,100 @@ class ClockFace(gtk.DrawingArea):
             self.queue_draw()
             self.window.process_updates(True)
 
+    def _press_cb(self, widget, event):
+        mouse_x, mouse_y, state = event.window.get_pointer()
+        if not (state & gtk.gdk.BUTTON1_MASK):
+            return
+
+        # Calculate the angle from the center of the clock to the
+        # mouse pointer:
+        adjacent = mouse_x - self._center_x
+        opposite = -1 * (mouse_y - self._center_y)
+        pointer_angle = math.atan2(adjacent, opposite)
+
+        # If the angle is negative, convert it to the equal angle
+        # between 0 and 2*pi:
+        if pointer_angle < 0:
+            pointer_angle += math.pi * 2
+
+        def normalize(angle):
+            """Return the equal angle that is minor than 2*pi."""
+            return angle - (math.pi * 2) * int(angle / (math.pi * 2))
+
+        def are_near(hand_angle, angle):
+            """Return True if the angles are similar in the unit circle."""
+            return (normalize(hand_angle) >= angle - self._angle_e and
+                    normalize(hand_angle) < angle + self._angle_e)
+
+        def smaller_size(hand_size, adjacent, opposite):
+            """Return True if the distance is smaller than the hand size."""
+            return math.hypot(adjacent, opposite) <= hand_size
+
+        # Check if we can start dragging a hand of the clock:
+        if are_near(self._hour_angle, pointer_angle):
+            if smaller_size(self._hour_size, adjacent, opposite):
+                self._dragging_hand = 'hour'
+        elif are_near(self._minutes_angle, pointer_angle):
+            if smaller_size(self._minutes_size, adjacent, opposite):
+                self._dragging_hand = 'minutes'
+        elif are_near(self._seconds_angle, pointer_angle):
+            if smaller_size(self._seconds_size, adjacent, opposite):
+                self._dragging_hand = 'seconds'
+
+    def _motion_cb(self, widget, event):
+        if self._dragging_hand is None:
+            return
+
+        if event.is_hint:
+            mouse_x, mouse_y, state = event.window.get_pointer()
+        else:
+            mouse_x = event.x
+            mouse_y = event.y
+            state = event.state
+
+        if not state & gtk.gdk.BUTTON1_MASK:
+            return
+
+        # Calculate the angle from the center of the clock to the
+        # mouse pointer:
+        adjacent = mouse_x - self._center_x
+        opposite = -1 * (mouse_y - self._center_y)
+        pointer_angle = math.atan2(adjacent, opposite)
+
+        # If the angle is negative, convert it to the equal angle
+        # between 0 and 2*pi:
+        if pointer_angle < 0:
+            pointer_angle += math.pi * 2
+
+        # Update the angle of the hand being drag:
+        if self._dragging_hand == 'hour':
+            self._hour_angle = pointer_angle
+        elif self._dragging_hand == 'minutes':
+            self._minutes_angle = pointer_angle
+        elif self._dragging_hand == 'seconds':
+            self._seconds_angle = pointer_angle
+
+        # Force redraw of the clock:
+        self.queue_draw()
+
+    def _release_cb(self, widget, event):
+        self._dragging_hand = None
+        self.queue_draw()
+
     def _update_cb(self):
         """Called every seconds to update the time value.
         """
         # update the time and force a redraw of the clock
         self._time = datetime.now()
 
+        hours = self._time.hour
+        minutes = self._time.minute
+        seconds = self._time.second
+
+        self._hour_angle = math.pi / 6 * hours + math.pi / 360 * minutes
+        self._minutes_angle = math.pi / 30 * minutes
+        self._seconds_angle = math.pi / 30 * seconds
+
         gobject.idle_add(self._redraw_canvas)
 
         # When the minutes change, we raise the 'time_minute'
@@ -820,8 +966,9 @@ class ClockFace(gtk.DrawingArea):
             self._old_minute = self._time.minute
 
         # Keep running this timer as long as the clock is active
-        # (ie. visible)
-        return self._active
+        # (ie. visible) or the mode changes to dragging the hands of
+        # the clock.
+        return self._active and not self._grab_hands_mode
 
     def get_time(self):
         """Public access to the time member of the clock face.
diff --git a/icons/grab.svg b/icons/grab.svg
new file mode 100644
index 0000000..9ca34fb
--- /dev/null
+++ b/icons/grab.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+  <!ENTITY fill_color "#FFFFFF">
+  <!ENTITY stroke_color "#FFFFFF">
+]>
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">
+<path d="M11,18 Q13,16 15,19 L15,31 Q16,33 17,31 L17,7 Q19,5 21,7 L21,25 Q22,27 23,25 L23,7 Q25,5 27,7 L27,25 Q28,27 29,25 L29,7 Q31,5 33,7 L33,25 Q34,27 35,25 L35,12 Q37,10 39,12 L39,37 Q39,44 33,45 L17,45 Q11,45 11,37" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:.3"/></svg>
-- 
1.7.7.4



More information about the Sugar-devel mailing list