Ticket #1959: 0002-New-feature-grab-the-hands-of-the-clock.patch

File 0002-New-feature-grab-the-hands-of-the-clock.patch, 13.8 KB (added by manuq, 12 years ago)

Patch that adds the feature.

  • clock.py

    From df2fbf83dc0d5032d6bf174ce29cae44dac08399 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= <manuq@laptop.org>
    Date: Fri, 30 Dec 2011 11:58:45 -0300
    Subject: [PATCH clock 2/2] New feature: grab the hands of the clock
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    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@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 b class ClockActivity(activity.Activity): 
    309309        button.connect("toggled", self._speak_time_clicked_cb)
    310310        display_toolbar.insert(button, -1)
    311311
     312        # Button to toggle drag & drop
     313        button = ToggleToolButton("grab")
     314        button.set_tooltip(_("Toolbar", "Grab the hands"))
     315        button.connect("toggled", self._dragdrop_clicked_cb)
     316        display_toolbar.insert(button, -1)
     317
    312318    def _make_display(self):
    313319        """Prepare the display of the clock.
    314320
    class ClockActivity(activity.Activity): 
    358364        or digital).
    359365        """
    360366        self._clock.set_display_mode(display_mode)
     367        self._clock.queue_draw()
    361368
    362369    def _write_time_clicked_cb(self, button):
    363370        """The user clicked on the "write time" button to print the
    class ClockActivity(activity.Activity): 
    378385        if self._speak_time:
    379386            self._write_and_speak(True)
    380387
     388    def _dragdrop_clicked_cb(self, button):
     389        self._clock.change_grab_hands_mode(button.get_active())
     390
    381391    def _minutes_changed_cb(self, clock):
    382392        """Minutes have changed on the clock face: we have to update
    383393        the display of the time in full letters if the user has chosen
    class ClockFace(gtk.DrawingArea): 
    481491        self._mode = _MODE_SIMPLE_CLOCK
    482492
    483493        # SVG Background handle
    484         self._svg_handle = None
     494        self._svg_handle = rsvg.Handle(file="clock.svg")
    485495
     496        # This are calculated on widget resize
     497        self._center_x = None
     498        self._center_y = None
     499        self._width = None
     500        self._height = None
    486501        self._radius = -1
    487502        self._line_width = 2
     503        self._hour_size = None
     504        self._minutes_size = None
     505        self._seconds_size = None
     506
     507        self._hour_angle = None
     508        self._minutes_angle = None
     509        self._seconds_angle = None
     510
     511        # When dragging a hand, this is the name of the hand.  If
     512        # None, it means that no hand is being drag.
     513        self._dragging_hand = None
     514
     515        # In grab hands mode, the clock is not updated each second.
     516        # Instead, it is stopped and the child can drag the hands.
     517        self._grab_hands_mode = False
     518
     519        # Tolerance for dragging hands, in radians.  Try to change
     520        # this value to improve dragging:
     521        self._angle_e = 0.3
    488522
    489523        # Color codes (approved colors for XO screen:
    490524        # http://wiki.laptop.org/go/XO_colors)
    class ClockFace(gtk.DrawingArea): 
    509543        self.connect("size-allocate", self._size_allocate_cb)
    510544
    511545        # The masks to capture the events we are interested in
    512         self.add_events(gdk.EXPOSURE_MASK | gdk.VISIBILITY_NOTIFY_MASK)
     546        self.add_events(gdk.EXPOSURE_MASK | gdk.VISIBILITY_NOTIFY_MASK
     547            | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK
     548            | gtk.gdk.BUTTON1_MOTION_MASK)
    513549
    514550        # Define a new signal to notify the application when minutes
    515551        # change.  If the user wants to display the time in full
    class ClockFace(gtk.DrawingArea): 
    518554        gobject.signal_new("time_minute", ClockFace,
    519555          gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
    520556
     557        # Event handlers for drag & drop:
     558        self._press_id = None
     559        self._motion_id = None
     560        self._release_id = None
     561
    521562    def set_display_mode(self, mode):
    522563        """Set the type of clock to display (simple, nice, digital).
    523564        'mode' is one of MODE_XXX_CLOCK constants.
    524565        """
    525566        self._mode = mode
    526567
     568    def change_grab_hands_mode(self, start_dragging):
     569        """Connect or disconnect the callbacks for doing drag & drop
     570        of the hands of the clock.
     571        """
     572        if start_dragging:
     573            self._grab_hands_mode = True
     574            self._press_id = self.connect("button-press-event",
     575                                          self._press_cb)
     576            self._motion_id = self.connect("motion-notify-event",
     577                                           self._motion_cb)
     578            self._release_id = self.connect("button-release-event",
     579                                        self._release_cb)
     580        else:
     581            self._grab_hands_mode = False
     582            self.disconnect(self._press_id)
     583            self.disconnect(self._motion_id)
     584            self.disconnect(self._release_id)
     585
     586            # Update again the clock every seconds.
     587            gobject.timeout_add(1000, self._update_cb)
     588
    527589    def _size_allocate_cb(self, widget, allocation):
    528590        """We know the size of the widget on the screen, so we keep
    529591        the parameters which are important for our rendering (center
    class ClockFace(gtk.DrawingArea): 
    537599        self._width = allocation.width
    538600        self._height = allocation.height
    539601        self._line_width = int(self._radius / 150)
    540 
    541         # Reload the svg handle
    542         self._svg_handle = rsvg.Handle(file="clock.svg")
     602        self._hour_size = self._radius * 0.5
     603        self._minutes_size = self._radius * 0.8
     604        self._seconds_size = self._radius * 0.7
    543605
    544606        self.initialized = True
    545607
    class ClockFace(gtk.DrawingArea): 
    729791    def _draw_hands(self):
    730792        """Draw the hands of the analog clocks.
    731793        """
    732         hours = self._time.hour
    733         minutes = self._time.minute
    734         seconds = self._time.second
    735 
    736794        cr = self.window.cairo_create()
    737795        cr.set_line_cap(cairo.LINE_CAP_ROUND)
    738796
    class ClockFace(gtk.DrawingArea): 
    742800        cr.set_source_rgba(*style.Color(self._COLOR_HOURS).get_rgba())
    743801        cr.set_line_width(8 * self._line_width)
    744802        cr.move_to(self._center_x, self._center_y)
    745         cr.line_to(int(self._center_x + self._radius * 0.5 *
    746             math.sin(math.pi / 6 * hours + math.pi / 360 * minutes)),
    747             int(self._center_y + self._radius * 0.5 *
    748             - math.cos(math.pi / 6 * hours + math.pi / 360 * minutes)))
     803        sin, cos = math.sin(self._hour_angle), math.cos(self._hour_angle)
     804        cr.line_to(
     805            int(self._center_x + self._hour_size * sin),
     806            int(self._center_y - self._hour_size * cos))
    749807        cr.stroke()
    750808
    751809        # Minute hand:
    class ClockFace(gtk.DrawingArea): 
    753811        cr.set_source_rgba(*style.Color(self._COLOR_MINUTES).get_rgba())
    754812        cr.set_line_width(6 * self._line_width)
    755813        cr.move_to(self._center_x, self._center_y)
    756         cr.line_to(int(self._center_x + self._radius * 0.8 *
    757                 math.sin(math.pi / 30 * minutes)),
    758                    int(self._center_y + self._radius * 0.8 *
    759                 - math.cos(math.pi / 30 * minutes)))
     814        sin, cos = math.sin(self._minutes_angle), math.cos(self._minutes_angle)
     815        cr.line_to(
     816            int(self._center_x + self._minutes_size * sin),
     817            int(self._center_y - self._minutes_size * cos))
    760818        cr.stroke()
    761819
    762820        # Seconds hand:
    class ClockFace(gtk.DrawingArea): 
    764822        cr.set_source_rgba(*style.Color(self._COLOR_SECONDS).get_rgba())
    765823        cr.set_line_width(2 * self._line_width)
    766824        cr.move_to(self._center_x, self._center_y)
    767         cr.line_to(int(self._center_x + self._radius * 0.7 *
    768                 math.sin(math.pi / 30 * seconds)),
    769                 int(self._center_y + self._radius * 0.7 *
    770                 - math.cos(math.pi / 30 * seconds)))
     825        sin, cos = math.sin(self._seconds_angle), math.cos(self._seconds_angle)
     826        cr.line_to(
     827            int(self._center_x + self._seconds_size * sin),
     828            int(self._center_y - self._seconds_size * cos))
    771829        cr.stroke()
    772830
    773831    def _draw_numbers(self):
    class ClockFace(gtk.DrawingArea): 
    803861            self.queue_draw()
    804862            self.window.process_updates(True)
    805863
     864    def _press_cb(self, widget, event):
     865        mouse_x, mouse_y, state = event.window.get_pointer()
     866        if not (state & gtk.gdk.BUTTON1_MASK):
     867            return
     868
     869        # Calculate the angle from the center of the clock to the
     870        # mouse pointer:
     871        adjacent = mouse_x - self._center_x
     872        opposite = -1 * (mouse_y - self._center_y)
     873        pointer_angle = math.atan2(adjacent, opposite)
     874
     875        # If the angle is negative, convert it to the equal angle
     876        # between 0 and 2*pi:
     877        if pointer_angle < 0:
     878            pointer_angle += math.pi * 2
     879
     880        def normalize(angle):
     881            """Return the equal angle that is minor than 2*pi."""
     882            return angle - (math.pi * 2) * int(angle / (math.pi * 2))
     883
     884        def are_near(hand_angle, angle):
     885            """Return True if the angles are similar in the unit circle."""
     886            return (normalize(hand_angle) >= angle - self._angle_e and
     887                    normalize(hand_angle) < angle + self._angle_e)
     888
     889        def smaller_size(hand_size, adjacent, opposite):
     890            """Return True if the distance is smaller than the hand size."""
     891            return math.hypot(adjacent, opposite) <= hand_size
     892
     893        # Check if we can start dragging a hand of the clock:
     894        if are_near(self._hour_angle, pointer_angle):
     895            if smaller_size(self._hour_size, adjacent, opposite):
     896                self._dragging_hand = 'hour'
     897        elif are_near(self._minutes_angle, pointer_angle):
     898            if smaller_size(self._minutes_size, adjacent, opposite):
     899                self._dragging_hand = 'minutes'
     900        elif are_near(self._seconds_angle, pointer_angle):
     901            if smaller_size(self._seconds_size, adjacent, opposite):
     902                self._dragging_hand = 'seconds'
     903
     904    def _motion_cb(self, widget, event):
     905        if self._dragging_hand is None:
     906            return
     907
     908        if event.is_hint:
     909            mouse_x, mouse_y, state = event.window.get_pointer()
     910        else:
     911            mouse_x = event.x
     912            mouse_y = event.y
     913            state = event.state
     914
     915        if not state & gtk.gdk.BUTTON1_MASK:
     916            return
     917
     918        # Calculate the angle from the center of the clock to the
     919        # mouse pointer:
     920        adjacent = mouse_x - self._center_x
     921        opposite = -1 * (mouse_y - self._center_y)
     922        pointer_angle = math.atan2(adjacent, opposite)
     923
     924        # If the angle is negative, convert it to the equal angle
     925        # between 0 and 2*pi:
     926        if pointer_angle < 0:
     927            pointer_angle += math.pi * 2
     928
     929        # Update the angle of the hand being drag:
     930        if self._dragging_hand == 'hour':
     931            self._hour_angle = pointer_angle
     932        elif self._dragging_hand == 'minutes':
     933            self._minutes_angle = pointer_angle
     934        elif self._dragging_hand == 'seconds':
     935            self._seconds_angle = pointer_angle
     936
     937        # Force redraw of the clock:
     938        self.queue_draw()
     939
     940    def _release_cb(self, widget, event):
     941        self._dragging_hand = None
     942        self.queue_draw()
     943
    806944    def _update_cb(self):
    807945        """Called every seconds to update the time value.
    808946        """
    809947        # update the time and force a redraw of the clock
    810948        self._time = datetime.now()
    811949
     950        hours = self._time.hour
     951        minutes = self._time.minute
     952        seconds = self._time.second
     953
     954        self._hour_angle = math.pi / 6 * hours + math.pi / 360 * minutes
     955        self._minutes_angle = math.pi / 30 * minutes
     956        self._seconds_angle = math.pi / 30 * seconds
     957
    812958        gobject.idle_add(self._redraw_canvas)
    813959
    814960        # When the minutes change, we raise the 'time_minute'
    class ClockFace(gtk.DrawingArea): 
    820966            self._old_minute = self._time.minute
    821967
    822968        # Keep running this timer as long as the clock is active
    823         # (ie. visible)
    824         return self._active
     969        # (ie. visible) or the mode changes to dragging the hands of
     970        # the clock.
     971        return self._active and not self._grab_hands_mode
    825972
    826973    def get_time(self):
    827974        """Public access to the time member of the clock face.
  • new file icons/grab.svg

    diff --git a/icons/grab.svg b/icons/grab.svg
    new file mode 100644
    index 0000000..9ca34fb
    - +  
     1<?xml version="1.0" encoding="UTF-8"?>
     2<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
     3  <!ENTITY fill_color "#FFFFFF">
     4  <!ENTITY stroke_color "#FFFFFF">
     5]>
     6<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">
     7<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>