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

Gary Martin garycmartin at googlemail.com
Sun Jan 1 06:16:28 EST 2012


Hi Manuel,

Just wanted to ping say thanks for all the effort on Clock, and apologise for not moving forward on the patches. I don't yet have a suitable Sugar development environment to test any of this new gtk3 dependent work, and I have a minimal network connection for at least a week or two so no chance of downloading/building new test environments.

Regards, and a Happy New Year!
--Gary

On 30 Dec 2011, at 15:00, Manuel Quiñones wrote:

> 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