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.
------------------------------------------------------------------------
------------------------------------------------------------------------
------------------------------------------------------------------------
------------------------------------------------------------------------
------------------------------------------------------------------------
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