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