I'm trying to write a clock widget.  I want it to look nice and to
behave in a friendly manner.

* It should show colons between the hours, minutes, and seconds
* typing the time should "skip" over the colons

I've tried to implement this by 5 sequential text entry widgets, each
without a frame so they look like they're part of the same widget, laid
out like this:

+----+---+----+---+----+
| hh | : | mm | : | ss |
+----+---+----+---+----+

if the time is 10:20, I want focus to default to the hours, and the user
can type in "1020".

The best way I can think of doing this is by detecting that on the
keypress for "2", the number is out of bounds, then blocking the signals
for the minute text entry, and setting it's value to 2.

However, sometimes when I try to block the signals, I get the message 

 Warning: gsignal.c:1695: instance `0xaf6800' has no handler with id `115'

Which doesn't make sense to me, because I 115 definitely exists, as I
stored it previously.

I've attached the code which will exhibit the behaviour.  Just type
"python clock.py"

Any suggestions?  Anyone know why signal 115 disappears?

-sjbrown
#! /usr/bin/python
'''
module docstring goes here.
'''
import gtk
from gtk import gdk
import gtk.glade
import re

#-----------------------------------------------------------------------------
def SignalLoopProtected( widgetName, signalName ):
    '''This decorator ensures that a loop won't be created from a callback
    continually calling itself in a loop, or a loop between a pair of callbacks
    that call each other.  It takes the callback, "origFunc" and wraps it
    in calls to handler_block and handler_unblock.

    Requirements:
    a handler ID has to be created and stored in the instance.  It has to be
    accessible by calling GetHandlerID(widgetName, signalName)
    '''
    def Decorated( origFunc ):
        def NewFunc( self, widget, *args ):
            assert hasattr(widget, 'handler_block') \
               and hasattr(self,'GetHandlerID'), \
               'SignalLoopProtected can only decorate GTK callback funcs that'+\
               ' can be retrieved from self.GetHandlerID(widgetName,signalName)'
            handlerID = self.GetHandlerID(widgetName, signalName)
            print "++ Blocking signal %s id: %d" % (signalName, handlerID)
            widget.handler_block( handlerID )
            rc = origFunc( self, widget, *args )
            print '-- Unblocking signal %s id: %d' % (signalName, handlerID)
            widget.handler_unblock( handlerID )
            return rc
        NewFunc.func_name = origFunc.func_name
        return NewFunc
    return Decorated

#-----------------------------------------------------------------------------
class GladeInspectingAutoconnecter(object):
    '''Base class that inspects Glade XML and autoconnects
    matching names to the instance

    Provides Autoconnect(), which should be called at the end of __init__,
    and GetHandlerID, which is useful for the above decorator, 
    SignalLoopProtected.
    '''
    # Regular expression to match handler method names patterns
    # On_widget__signal and After_widget__signal.  Note that we use two
    # underscores between the Glade widget name and the signal name.
    handler_re = re.compile(r'(On|After)_(.*)__(.*)', re.IGNORECASE)
    def Autoconnect(self, xml):
        """Connects signal handling methods to Glade widgets.
        Methods named like On_widget__signal or After_widget__signal
        are connected to the appropriate widgets and signals.
        """
        self._handlerIDs = {}
        for attr in dir(self):
            match = self.handler_re.match(attr)
            if not match:
                continue
            when, widgetName, signalName = match.groups()
            method = getattr(self, attr)
            assert callable(method), 'Instance attr looks like method name'
            widget = xml.get_widget(widgetName)
            if not widget:
                #print 'Widget %s was not found' % widgetName
                continue
            handlerID = widget.connect(signalName.replace('_', '-'), method)
            self._handlerIDs[(widgetName, signalName)] = handlerID

        print 'All handlers found:'
        for k,v in self._handlerIDs.items():
            print "%40s : %5s" % (k,v)

    def GetHandlerID( self, widgetName, signalName ):
        return self._handlerIDs[(widgetName, signalName)]
            



class Clock(GladeInspectingAutoconnecter):
    def __init__(self, gladeXML):
        #self.xml = gtk.glade.XML("clock.glade")
        self.xml = gladeXML

        #self.xml.signal_autoconnect(self)
        print ' calling Autoconnect'

        self.hours = self.xml.get_widget("clock_hours")
        self.minutes = self.xml.get_widget("clock_minutes")
        self.seconds = self.xml.get_widget("clock_seconds")
        self.clockSpin = self.xml.get_widget("clockspin")

        self.colon1 = self.xml.get_widget("clock_colon1")
        self.colon2 = self.xml.get_widget("clock_colon2")

        self.maxValues = {
                          self.hours: 23,
                          self.minutes: 59,
                          self.seconds: 59,
                         }

        self.Autoconnect(self.xml)

        #skip over the colons when tabbing
        self.clockBox = self.xml.get_widget("clock_hbox")
        self.clockBox.set_focus_chain((self.hours, 
                                       self.minutes, 
                                       self.seconds, 
                                       self.clockSpin))

        #each focusPasser implicitly passes focus to the next item when 
        #text is input beyond maxLength or when special chars are input
        self.entryFocusPassTable = {
                                     self.hours: self.minutes,
                                     self.minutes: self.seconds
                                   }

        self.lastFocusedClockEntry = self.hours
        self.pendingChangeToClockSpin = None


    def getTimeList(self):
        return [ int(x.get_text()) 
                 for x in [self.hours, self.minutes, self.seconds] ]

    def _passFocus(self, currentWidget):
        try:
            nextWidget = self.entryFocusPassTable[currentWidget]
            nextWidget.grab_focus()
            return nextWidget
        except IndexError:
            return None
        

    def on_clockentry__focus(self, widget, *args):
        print '\n  on_clockentry_focus'
        self.lastFocusedClockEntry = widget
        # set up the clock spin widget...
        digits = widget.get_text() #order is important. get text before set_range.
        self.clockSpin.set_range(0, self.maxValues[widget])
        if digits:
            print '  last focused text', digits
            self.clockSpin.set_value(int(digits))

    on_clock_seconds__focus = on_clockentry__focus
    on_clock_minutes__focus = on_clockentry__focus
    on_clock_hours__focus = on_clockentry__focus


    def on_clockentry__insert_at_cursor(self, widget, *args):
        print '\n  on_clockentry_insert_at_cursor'
        print widget, args

    on_clock_seconds__insert_at_cursor = on_clockentry__insert_at_cursor
    on_clock_minutes__insert_at_cursor = on_clockentry__insert_at_cursor
    on_clock_hours__insert_at_cursor = on_clockentry__insert_at_cursor


    def on_clockentry__backspace(self, widget, *args):
        print '\n  on_clockentry_backspace'
        print widget, args

    on_clock_seconds__backspace = on_clockentry__backspace
    on_clock_minutes__backspace = on_clockentry__backspace
    on_clock_hours__backspace = on_clockentry__backspace


    @SignalLoopProtected('clockspin', 'change_value')
    def on_clockentry__insert_text(self, widget, newText, *args):
        #NOTE: changed happens BEFORE clockentry_changed
        print '\n  on_clockentry_insert_text.  Widget:', widget, '!'
        print '\n  on_clockentry_insert_text.  Widget:', widget.get_name(), '!'
        text = widget.get_text()

        print '  cur text', text, 'newText:', newText

        if newText == ':':
            print '  got colon blow!'
            self._passFocus(widget)
        elif widget.get_position() >= widget.get_max_length():
            print '  overflow!'
            nextWidget = self._passFocus(widget)
            if not nextWidget:
                return
            nextWidget.set_text(newText)
            nextWidget.set_position(1)
        else:
            fullText = text+newText
            print '  fulltext is ', fullText
            try:
                self.pendingChangeToClockSpin = int(fullText)
            except ValueError:
                pass
        
        print '  RETURN on_clockentry_insert_text.  '
        return False

    on_clock_seconds__insert_text = on_clockentry__insert_text
    on_clock_minutes__insert_text = on_clockentry__insert_text
    on_clock_hours__insert_text = on_clockentry__insert_text


    def on_clockentry__delete_text(self, widget, *args):
        print '\n  on_clockentry_delete_text'
        print '  ', widget, args
        text = widget.get_text()
        print '  cur text', text, 'args:', args

    on_clock_seconds__delete_text = on_clockentry__delete_text
    on_clock_minutes__delete_text = on_clockentry__delete_text
    on_clock_hours__delete_text = on_clockentry__delete_text


    def on_clockentry__changed(self, widget, *args):
        #NOTE: changed happens AFTER insert_text
        print '\n  on_clockentry_changed'
        print '  widget:', widget, 'args:', args
        text = widget.get_text()
        print '  input text:', text
        #allow only numeric characters
        justDigits = filter(str.isdigit, text)
        if not justDigits:
            widget.set_text('')
            return

        if int(justDigits) > self.maxValues[widget]:
            justDigits = str(self.maxValues[widget])

        widget.set_text(justDigits)

        if self.pendingChangeToClockSpin:
            self.clockSpin.set_value(self.pendingChangeToClockSpin)
            self.pendingChangeToClockSpin = None

    on_clock_seconds__changed = on_clockentry__changed
    on_clock_minutes__changed = on_clockentry__changed
    on_clock_hours__changed = on_clockentry__changed


    @SignalLoopProtected('clock_seconds', 'changed')
    def on_clockspin__value_changed(self, *args):
        print '\n  on_clockspin__value_changed', args
        #print 'current spin value', self.clockSpin.get_value_as_int()
        strValue = str(int(self.clockSpin.get_value_as_int()))
        self.lastFocusedClockEntry.set_text(strValue)
        #print '---on_clockspin__value_changed out....'
        return False


    def on_clockspin__change_value(self, *args):
        print '\n  on_clockspin_change_value (KEYBOARD input)', args
        return False

#-------------------------------------------------------------------------------
example_xml = '''<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd";>

<glade-interface>

<widget class="GtkWindow" id="window1">
  <property name="width_request">200</property>
  <property name="height_request">200</property>
  <property name="visible">True</property>
  <signal name="destroy_event" handler="on_window1_destroy_event" />
  <signal name="delete_event" handler="on_window1_delete_event" />

  <child>
    <widget class="GtkAlignment" id="alignment2">
      <property name="visible">True</property>
      <property name="xalign">0.5</property>
      <property name="yalign">0.5</property>
      <property name="xscale">0</property>
      <property name="yscale">0</property>
      <property name="top_padding">0</property>
      <property name="bottom_padding">0</property>
      <property name="left_padding">0</property>
      <property name="right_padding">0</property>

      <child>
        <widget class="GtkFrame" id="clock_frame">
          <property name="visible">True</property>
          <property name="label_xalign">0</property>
          <property name="label_yalign">0</property>
          <property name="shadow_type">GTK_SHADOW_IN</property>

          <child>
            <widget class="GtkAlignment" id="clock_align">
              <property name="border_width">2</property>
              <property name="visible">True</property>
              <property name="xalign">0</property>
              <property name="yalign">0</property>
              <property name="xscale">0</property>
              <property name="yscale">0</property>
              <property name="top_padding">0</property>
              <property name="bottom_padding">0</property>
              <property name="left_padding">0</property>
              <property name="right_padding">0</property>

              <child>
                <widget class="GtkHBox" id="clock_hbox">
                  <property name="visible">True</property>
                  <property name="homogeneous">False</property>
                  <property name="spacing">0</property>

                  <child>
                    <widget class="GtkEntry" id="clock_hours">
                      <property name="visible">True</property>
                      <property name="can_focus">True</property>
                      <property name="events">GDK_FOCUS_CHANGE_MASK</property>
                      <property name="editable">True</property>
                      <property name="visibility">True</property>
                      <property name="max_length">2</property>
                      <property name="text" translatable="yes">00</property>
                      <property name="has_frame">False</property>
                      <property name="activates_default">False</property>
                      <property name="width_chars">2</property>
                      <signal name="activate" handler="on_clockentry_activate" />
                      <signal name="backspace" handler="on_clockentry_backspace" />
                      <signal name="move_cursor" handler="on_clockentry_move_cursor" />
                      <signal name="changed" handler="on_clockentry_changed" />
                      <signal name="insert_text" handler="on_clockentry_insert_text" />
                      <signal name="focus_in_event" handler="on_clockentry_focus" />
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">True</property>
                      <property name="fill">True</property>
                    </packing>
                  </child>

                  <child>
                    <widget class="GtkEntry" id="clock_colon1">
                      <property name="visible">True</property>
                      <property name="editable">False</property>
                      <property name="visibility">True</property>
                      <property name="max_length">1</property>
                      <property name="text" translatable="yes">:</property>
                      <property name="has_frame">False</property>
                      <property name="activates_default">False</property>
                      <property name="width_chars">1</property>
                      <signal name="focus_in_event" handler="on_colon_focus" />
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">False</property>
                      <property name="fill">False</property>
                    </packing>
                  </child>

                  <child>
                    <widget class="GtkEntry" id="clock_minutes">
                      <property name="visible">True</property>
                      <property name="can_focus">True</property>
                      <property name="editable">True</property>
                      <property name="visibility">True</property>
                      <property name="max_length">2</property>
                      <property name="text" translatable="yes">00</property>
                      <property name="has_frame">False</property>
                      <property name="activates_default">False</property>
                      <property name="width_chars">2</property>
                      <signal name="activate" handler="on_clockentry_activate" />
                      <signal name="backspace" handler="on_clockentry_backspace" />
                      <signal name="move_cursor" handler="on_clockentry_move_cursor" />
                      <signal name="changed" handler="on_clockentry_changed" />
                      <signal name="insert_text" handler="on_clockentry_insert_text" />
                      <signal name="focus_in_event" handler="on_clockentry_focus" />
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">True</property>
                      <property name="fill">True</property>
                    </packing>
                  </child>

                  <child>
                    <widget class="GtkEntry" id="clock_colon2">
                      <property name="visible">True</property>
                      <property name="editable">False</property>
                      <property name="visibility">True</property>
                      <property name="max_length">1</property>
                      <property name="text" translatable="yes">:</property>
                      <property name="has_frame">False</property>
                      <property name="activates_default">False</property>
                      <property name="width_chars">1</property>
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">True</property>
                      <property name="fill">True</property>
                    </packing>
                  </child>

                  <child>
                    <widget class="GtkEntry" id="clock_seconds">
                      <property name="visible">True</property>
                      <property name="can_focus">True</property>
                      <property name="editable">True</property>
                      <property name="visibility">True</property>
                      <property name="max_length">2</property>
                      <property name="text" translatable="yes">00</property>
                      <property name="has_frame">False</property>
                      <property name="activates_default">False</property>
                      <property name="width_chars">2</property>
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">True</property>
                      <property name="fill">True</property>
                    </packing>
                  </child>

                  <child>
                    <widget class="GtkSpinButton" id="clockspin">
                      <property name="width_request">17</property>
                      <property name="visible">True</property>
                      <property name="climb_rate">1</property>
                      <property name="digits">0</property>
                      <property name="numeric">False</property>
                      <property name="update_policy">GTK_UPDATE_ALWAYS</property>
                      <property name="snap_to_ticks">False</property>
                      <property name="wrap">True</property>
                      <property name="adjustment">0 0 1 1 1 1</property>
                      <signal name="input" handler="on_clockspin_input" />
                      <signal name="value_changed" handler="on_clockspin__value_changed" />
                    </widget>
                    <packing>
                      <property name="padding">0</property>
                      <property name="expand">True</property>
                      <property name="fill">True</property>
                    </packing>
                  </child>
                </widget>
              </child>
            </widget>
          </child>
        </widget>
      </child>
    </widget>
  </child>
</widget>

</glade-interface>
'''

class TestingClock(Clock):
    def __init__(self):
        xml = gtk.glade.xml_new_from_buffer(example_xml, len(example_xml))
        Clock.__init__(self, xml)
        self.xml.signal_autoconnect(self)

    def on_window1_delete_event(self, *args):
        print 'delete!'
        gtk.main_quit()

    def on_window1_destroy_event(self, *args):
        print 'destroy!'
        gtk.main_quit()


def test_init():
    c = TestingClock()
    print c.getTimeList()
    tl = c.getTimeList()
    assert tl == [0,0,0]

def test_focus():
    c = TestingClock()
    print 'hours is focus', c.hours.is_focus()
    assert c.hours.is_focus()

def test_spinup():
    c = TestingClock()
def test_spindown():
    c = TestingClock()

def test_boundaries():
    c = TestingClock()
    print c.getTimeList()
    c.hours.set_text('23')
    tl = c.getTimeList()
    assert tl == [23,0,0]
    c.hours.set_text('0')
    tl = c.getTimeList()
    assert tl == [0,0,0]
    c.hours.set_text('24')
    tl = c.getTimeList()
    assert tl == [23,0,0]
    c.hours.set_text('29')
    tl = c.getTimeList()
    assert tl == [23,0,0]
    c.minutes.set_text('0')
    tl = c.getTimeList()
    assert tl == [23,0,0]
    c.minutes.set_text('59')
    tl = c.getTimeList()
    assert tl == [23,59,0]
    c.minutes.set_text('60')
    tl = c.getTimeList()
    assert tl == [23,59,0]
    c.minutes.set_text('66')
    tl = c.getTimeList()
    assert tl == [23,59,0]
    c.seconds.set_text('0')
    tl = c.getTimeList()
    assert tl == [23,59,0]
    c.seconds.set_text('59')
    tl = c.getTimeList()
    assert tl == [23,59,59]
    c.seconds.set_text('60')
    tl = c.getTimeList()
    assert tl == [23,59,59]
    c.seconds.set_text('66')
    tl = c.getTimeList()
    assert tl == [23,59,59]

def test_badinput():
    c = TestingClock()
    tl = c.getTimeList()
    assert tl == [0,0,0]
    c.seconds.set_text('-1')
    tl = c.getTimeList()
    assert tl == [0,0,1]
    c.seconds.set_text('a')
    tl = c.getTimeList()
    assert tl == [0,0,0]
    c.seconds.set_text('*')
    tl = c.getTimeList()
    assert tl == [0,0,0]

def test_backspace():
    c = TestingClock()

if __name__ == '__main__':
    c = TestingClock()
    gtk.main()
_______________________________________________
pygtk mailing list   pygtk@daa.com.au
http://www.daa.com.au/mailman/listinfo/pygtk
Read the PyGTK FAQ: http://www.async.com.br/faq/pygtk/

Reply via email to