Hi Davor,

Thanks for the code! I tried running it on Mac and Linux. It works fine on Linux, but on the Mac I get this:

Traceback (most recent call last):
  File "/Users/twl/davor.py", line 33, in OnShowClock
    win = ClockPopup(self, wx.SIMPLE_BORDER)
  File "/Users/twl/davor.py", line 55, in __init__
    wx.PopupTransientWindow.__init__(self, parent, style)
File "/Users/twl/work/osaf/binaries/release/Library/Frameworks/ Python.framework/Versions/2.4/lib/python2.4/site-packages/wx/ _windows.py", line 1814, in __init__
    newobj = _windows_.new_PopupTransientWindow(*args, **kwargs)
NotImplementedError

Ted

On Nov 1, 2005, at 1:45 PM, Davor Cubranic wrote:

I'm a big fan of Ecco PIM/outliner, and one of very useful features in its UI is the way it allows selecting time by clicking in an analog clock dropdown. (See attachment for a screenshot of Ecco's event properties dialog, both with and without the clock dropdown.) So having to enter times in Chandler by typing them in a text control felt like a step backward and I tried to recreate Ecco's clock dropdown in wxPython. I'm attaching the code with this email, in case you guys are interested in adding it to Chandler at some point. The core functionality is in class ClockPopup, which also uses CancelButton and ClockLabel. ClockDemoPanel illustrates how to set up the popup and hook to its selection notification. The rest is just the required stuff to run a wx application.

To use the popup, run the application and click on the button with the clock icon to the right of the text control (apologies for the ugly icon, it's just for illustration). The clock popup will show up, and you can left-click on any of the hours labels to select the hours value. The popup will go away and the text control will show the selected time. If you want to set a time value that isn't on the hour, *right-click* on the hour label. The hour value will be selected and the clock face will change to minutes -- left-click on the minute label now and the popup will again go away and the text control updated with the selected time. Click outside the popup or on the red "X" button in the middle of the clock face to cancel the selection at any time.

I tried to recreate Ecco's look and functionality as closely as I could. The only difference is that in my version both hours an minutes can be selected from a single popup using the right-click mechanism. I think it saves a bit of time compared to Ecco's separate dropdowns for hours and minutes. The drawback of my version is that the user now has to remember the left/right-click distinction, but I find it to be very natural and not inappropriate to such a frequently used feature.

Davor

P.S. I'm sorry if this is not the right place to post this suggestion. I sent it to the dev list rather than design because I also included working code. I also considered posting on Bugzilla, but the mailing list allows more people to discuss the proposal. I can open an enhancement request in Bugzilla if that's the preferred way.
P.P.S. The code has been tested on Windows and wxPython 2.6.1.0.
<ecco-event-dialog.png>
<ecco-event-dropdown.png>
<python-clock-hours.png>
<python-clock-minutes.png>
import wx
import wx.lib.buttons as buttons
import wx.lib.masked as masked

class ClockDemoPanel(wx.Panel):
    """
A demo panel illustrating how to set up the popup and hook it up to a display, like a text control
    """
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, -1)

        l = wx.StaticText(self, -1, "Select time")
        tc = wx.TextCtrl(self, -1, "12:00", size=(50, -1))
        wx.CallAfter(tc.SetInsertionPoint, 0)
        self.tc = tc

        clockimage = wx.BitmapFromImage(wx.ImageFromStream(stream))
        clockmask = wx.Mask(clockimage, wx.BLUE)
        clockimage.SetMask(clockmask)
b = wx.BitmapButton(self, -1, clockimage, None, (clockimage.GetWidth()+8, clockimage.GetHeight()+8))
        self.Bind(wx.EVT_BUTTON, self.OnShowClock, b)

        space=5
        sizer = wx.FlexGridSizer(cols=3, hgap=space, vgap=space)
        sizer.AddMany([ l, tc, b,
                        ])
        border = wx.BoxSizer(wx.VERTICAL)
        border.Add(sizer, 0, wx.ALL, 25)
        self.SetSizer(border)


    def OnShowClock(self, evt):
        win = ClockPopup(self, wx.SIMPLE_BORDER)
        self.Bind(masked.EVT_TIMEUPDATE, self.OnTimeSelected, win)

        # Show the popup right below or above the button
        # depending on available screen space...
        btn = evt.GetEventObject()
        pos = btn.ClientToScreen( (0,0) )
        sz =  btn.GetSize()
        win.Position(pos, (0, sz[1]))

        win.Popup()

    def OnTimeSelected(self, evt):
        self.tc.SetValue('%02d:%02d' % evt.GetValue())

####################

class ClockPopup(wx.PopupTransientWindow):
    """
    A popup to select a time value using a clock-like interface.
    """
    def __init__(self, parent, style):
        wx.PopupTransientWindow.__init__(self, parent, style)
        self.hours = self.minutes = None
        self.SetBackgroundColour('white')
        self.makeClockFace(self)
        self.SetSize( (77, 77) )

    def GetValue(self):
"""Returns the popup's selected time as a hour-minutes tuple."""
        return (self.hours, self.minutes)

    def OnTimeSelected(self, evt):
"""Fires a TIMEUPDATED event to notify listeners a time value has been selected in the popup."""
        if not self.hours:
            self.hours = int(evt.GetEventObject().text)
            self.minutes = 0
        else:
            self.minutes = int(evt.GetEventObject().text)
evt = masked.TimeUpdatedEvent(self.GetId(), (self.hours, self.minutes))
        self.GetEventHandler().ProcessEvent(evt)

        self.Show(False)
        self.Destroy()

    def OnClockRightClick(self, evt):
"""Remembers the selected hour value and switches to selecting minutes.""" if self.hours: return # No right click if showing minutes
        self.hours = int(evt.GetEventObject().text)
hoursLabel = wx.StaticText(self, -1, str(self.hours), (25, 16), (23, 14), style=wx.ALIGN_CENTER_HORIZONTAL)
        hoursLabel.SetBackgroundColour(wx.Colour(200, 200, 200))
it = ['55', '00', '05', '50', '10', '45', '15', '40', '20', '35', '30', '25'].__iter__()
        for label in self.labels:
            label.SetLabel(it.next())

    def OnClockCancel(self, evt):
        """Closes the popup without selecting a time value."""
        self.Show(False)
        self.Destroy()

    def makeClockFace(self, win):
"""A helper function that creates the contents of the popup window and arranges them into the UI."""

        self.labels = [
            ClockLabel(win, '11', (2, 2), wx.ALIGN_RIGHT),
ClockLabel(win, '12', (25, 2), wx.ALIGN_CENTER_HORIZONTAL),
            ClockLabel(win, ' 1', (48, 2)),
            ClockLabel(win, '10', (2, 16), wx.ALIGN_CENTER),
            ClockLabel(win, ' 2', (48, 16), wx.ALIGN_CENTER),
            ClockLabel(win, ' 9', (2, 30), wx.ALIGN_LEFT),
            ClockLabel(win, ' 3', (48, 30), wx.ALIGN_RIGHT),
            ClockLabel(win, ' 8', (2, 44), wx.ALIGN_CENTER),
            ClockLabel(win, ' 4', (48, 44), wx.ALIGN_CENTER),
            ClockLabel(win, ' 7', (2, 58), wx.ALIGN_RIGHT),
            ClockLabel(win, ' 6', (25, 58), wx.ALIGN_CENTER),
            ClockLabel(win, ' 5', (48, 58)),
        ]
        CancelButton(win, (28, 30))


class CancelButton(buttons.GenButton):
    """
Button for cancelling time selection (red X in the middle of the clock face)
    """
    def __init__(self, parent, pos):
buttons.GenButton.__init__(self, parent, -1, 'x', pos=pos, style=wx.SIMPLE_BORDER)
        self.SetFont(wx.Font(9, wx.SWISS, wx.NORMAL, wx.BOLD, False))
        self.SetSize((17, 16))
        self.SetForegroundColour(wx.RED)
        self.Bind(wx.EVT_LEFT_UP, parent.OnClockCancel)


class ClockLabel(wx.PyWindow):
    """
    A simple window that is used as sizer items in the tests below to
    show how the various sizers work.
    """
def __init__(self, parent, text, pos=wx.DefaultPosition, style=wx.NO_BORDER, size=wx.DefaultSize):
        wx.PyWindow.__init__(self, parent, pos=pos, style=style)

        self.text = text
        if size != wx.DefaultSize:
            self.size = size
        else:
            self.size = (23, 14)
        self.SetSize(self.size)
        self.style = style
        self.selected = False

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
        self.Bind(wx.EVT_LEFT_UP, parent.OnTimeSelected)
        self.Bind(wx.EVT_RIGHT_UP, parent.OnClockRightClick)

    def OnLeave(self, evt):
        self.selected = False
        self.Refresh()

    def OnEnter(self, evt):
        self.selected = True
        self.Refresh()

    def OnPaint(self, evt):
        sz = self.GetSize()
        dc = wx.PaintDC(self)
        font = dc.GetFont()
        font.SetPointSize(8)
        if int(self.text) % 3 != 0:
            font.SetWeight(wx.FONTWEIGHT_NORMAL)
            dc.SetTextForeground(wx.BLACK)
        else:
            dc.SetTextForeground(wx.RED)
            font.SetWeight(wx.FONTWEIGHT_BOLD)
        dc.SetFont(font)
        if self.selected:
            background = wx.BLUE_BRUSH
            dc.SetTextForeground(wx.WHITE)
        else:
            background = wx.WHITE_BRUSH
        dc.SetBackground(background)
        dc.Clear()
        w,h = dc.GetTextExtent(self.text)
        if self.style & wx.ALIGN_RIGHT:
            x = sz.width - w
        elif self.style & wx.ALIGN_CENTER_HORIZONTAL:
            x = (sz.width-w)/2
        else:
            x = 0
        dc.DrawText(self.text, x, (sz.height-h)/2)

    def GetLabel(self):
        return self.text

    def SetLabel(self, text):
        self.text = text
        self.Refresh()

####################################3


class ClockDemoFrame(wx.Frame):
    def __init__(self, parent, title, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.CLOSE_BOX|wx.CAPTION| wx.SYSTEM_MENU):
        wx.Frame.__init__(self, parent, -1, title, pos, size, style)
        panel = ClockDemoPanel(self)


class ClockDemoApp(wx.App):
"""Represents the timer application to the wx framework. Its only task is to create
    the window and enter the event loop."""
    def OnInit(self):
        frame = ClockDemoFrame(None, "timer")
        self.SetTopWindow(frame)
        frame.Show()

        return True


import cStringIO
clockbmpdata = \
'\x42\x4d\xf6\x00\x00\x00\x00\x00\x00\x00\x76\x00\x00\x00\x28\x00\
\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x01\x00\x04\x00\x00\x00\
\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x80\
\x00\x00\x00\x80\x80\x00\x80\x00\x00\x00\x80\x00\x80\x00\x80\x80\
\x00\x00\x80\x80\x80\x00\xc0\xc0\xc0\x00\x00\x00\xff\x00\x00\xff\
\x00\x00\x00\xff\xff\x00\xff\x00\x00\x00\xff\x00\xff\x00\xff\xff\
\x00\x00\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\
\x00\x00\x00\x0f\xff\xff\xff\x00\x00\x00\x00\x00\x0f\xff\xf0\x00\
\x0f\xff\xff\x00\x00\xff\xf0\x0f\xff\xff\xff\xff\x00\xff\x00\xff\
\xff\xff\xff\x0f\xf0\x0f\x00\xff\xff\xff\xf0\xff\xf0\x0f\x00\xff\
\xff\xff\x0f\xff\xf0\x0f\x00\xff\xff\xf0\xff\xff\xf0\x0f\x00\xff\
\xff\xf0\xff\xff\xf0\x0f\x00\xff\xff\xf0\xff\xff\xf0\x0f\x00\xff\
\xff\xf0\xff\xff\xf0\x0f\xf0\x0f\xff\xff\xff\xff\x00\xff\xf0\x00\
\x0f\xff\xff\x00\x00\xff\xff\x00\x00\x00\x00\x00\x0f\xff\xff\xff\
\x00\x00\x00\x0f\xff\xff'
stream = cStringIO.StringIO(clockbmpdata)
clockimage = None
clockmask = None

def main():
    app = ClockDemoApp(False)
    app.MainLoop()

if __name__ == '__main__':
    __name__ = 'Main'
    main()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

----
Ted Leung                 Open Source Applications Foundation (OSAF)
PGP Fingerprint: 1003 7870 251F FA71 A59A  CEE3 BEBA 2B87 F5FC 4B42


_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

Reply via email to