[Dev] Selecting time in event detail view
Davor Cubranic
cubranic at cs.ubc.ca
Tue Nov 1 13:45:52 PST 2005
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.
-------------- next part --------------
A non-text attachment was scrubbed...
Name: ecco-event-dialog.png
Type: image/png
Size: 14704 bytes
Desc: not available
Url : http://lists.osafoundation.org/pipermail/dev/attachments/20051101/fe8bc5ba/ecco-event-dialog.png
-------------- next part --------------
A non-text attachment was scrubbed...
Name: ecco-event-dropdown.png
Type: image/png
Size: 15660 bytes
Desc: not available
Url : http://lists.osafoundation.org/pipermail/dev/attachments/20051101/fe8bc5ba/ecco-event-dropdown.png
-------------- next part --------------
A non-text attachment was scrubbed...
Name: python-clock-hours.png
Type: image/png
Size: 5150 bytes
Desc: not available
Url : http://lists.osafoundation.org/pipermail/dev/attachments/20051101/fe8bc5ba/python-clock-hours.png
-------------- next part --------------
A non-text attachment was scrubbed...
Name: python-clock-minutes.png
Type: image/png
Size: 5102 bytes
Desc: not available
Url : http://lists.osafoundation.org/pipermail/dev/attachments/20051101/fe8bc5ba/python-clock-minutes.png
-------------- next part --------------
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()
More information about the Dev
mailing list