Tuesday, July 28, 2009

Highlevel Mouse Event Converter (wxPython)
















I really needed to make a good one (not a school assignment :) that convers lowlevel mouse events to highlevel ones. When the user left double clicks on a window, the window gets leftdown, leftup, leftdoubleclick, leftup events in this order. It is too low level. In this case all we want is a single double click event.

- A double click event doesn't come with friends.
- Whenever a mouse down event comes, the application can except a following mouse up event (it may be after dragging event stuff or enter/leave window event).
- A series of dragging events are always wrapped by a startdragging and an enddragging event.
- Capture mouse automatically. (See the comment in onButtonDown() method)
- Can set threshold in pixels so that anybody can double click with his trembling hand (e.g. my dad's)

BUG
While dragging, EnterWindow/LeaveWindow occurs when the mouse enter/leaves the parent window on GTK+.

UPDATES
Jul 29: Passing proper event object to mouse down/up handlers.
Jul 31: Get the right mouse position while mouse captured.
Aug 5 : Fixed missing double click detection failure.
Aug 8 : Modified to Register popup menu functions.


"""Low level -> high level mouse event converter."""
import wx

NORMAL = 0
FIRSTWAIT = 1
SECONDWAIT = 2
AFTERDCLICK = 3
MOUSEDOWN = 4
DRAGGING = 5

ID_FIRSTTIMER = wx.NewId()
ID_SECONDTIMER = wx.NewId()

class HighLevMouseEventConv(object):
def __init__(self, eventWindow, t1 = 200, t2 = 100, dragThreshPixel = 3):
"""HighLevelEvent(eventWindow, t1 = 200, t2 = 100, dragThreshPixel = 3)

eventWindow: The window that receives mouse events.
t1, t2: How long it waits to detect is a mouse down is a double click in mili seconds.
dragThreshPixel: Drag moves within this thresh do not produce drag related events.

When you want a popup menu associated with the window create a func to show the menu, register
it using set(Left, Middle, Right)PopupMenuFunc(). These functions should never return value.
"""
self.t1 = t1
self.t2 = t2
self.dragThreshPixel = dragThreshPixel
self.state = NORMAL
self.evwin = eventWindow

self.firstTimer = wx.Timer(eventWindow, ID_FIRSTTIMER)
self.secondTimer = wx.Timer(eventWindow, ID_SECONDTIMER)

dh = self.defaultHandler
l, r, m = wx.MOUSE_BTN_LEFT, wx.MOUSE_BTN_RIGHT, wx.MOUSE_BTN_MIDDLE

self.onHLVMouseDown = {l:dh, r:dh, m:dh}
self.onHLVMouseUp = {l:dh, r:dh, m:dh}
self.onHLVMouseDClick = {l:dh, r:dh, m:dh}
self.onHLVMouseStartDragging = {l:dh, r:dh, m:dh}
self.onHLVMouseDragging = {l:dh, r:dh, m:dh}
self.onHLVMouseEndDragging = {l:dh, r:dh, m:dh}
self.popupMenuFunc = {l:dh, r:dh, m:dh}

self.onHLVMotion = dh
self.onHLVEnterWindow = dh
self.onHLVLeaveWindow = dh

eventWindow.Bind(wx.EVT_MOUSE_EVENTS, self.onMouseEvents)
eventWindow.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onMouseCaptureLost)
eventWindow.Bind(wx.EVT_TIMER, self.onFirstTimer, id = ID_FIRSTTIMER)
eventWindow.Bind(wx.EVT_TIMER, self.onSecondTimer, id = ID_SECONDTIMER)


def setOnHLVLeftDown(self, handler):
self.onHLVMouseDown[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVLeftUp(self, handler):
self.onHLVMouseUp[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVLeftDClick(self, handler):
self.onHLVMouseDClick[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVLeftStartDragging(self, handler):
self.onHLVMouseStartDragging[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVLeftDragging(self, handler):
self.onHLVMouseDragging[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVLeftEndDragging(self, handler):
self.onHLVMouseEndDragging[wx.MOUSE_BTN_LEFT] = handler

def setOnHLVMiddleDown(self, handler):
self.onHLVMouseDown[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVMiddleUp(self, handler):
self.onHLVMouseUp[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVMiddleDClick(self, handler):
self.onHLVMouseDClick[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVMiddleStartDragging(self, handler):
self.onHLVMouseStartDragging[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVMiddleDragging(self, handler):
self.onHLVMouseDragging[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVMiddleEndDragging(self, handler):
self.onHLVMouseEndDragging[wx.MOUSE_BTN_MIDDLE] = handler

def setOnHLVRightDown(self, handler):
self.onHLVMouseDown[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVRightUp(self, handler):
self.onHLVMouseUp[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVRightDClick(self, handler):
self.onHLVMouseDClick[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVRightStartDragging(self, handler):
self.onHLVMouseStartDragging[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVRightDragging(self, handler):
self.onHLVMouseDragging[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVRightEndDragging(self, handler):
self.onHLVMouseEndDragging[wx.MOUSE_BTN_RIGHT] = handler

def setOnHLVMotion(self, handler):
self.onHLVMotion = handler

def setOnHLVEnterWindow(self, handler):
"""It can detect the mouse enter/leaves from, to the parent window when the mouse is dragging.
There's no clear way but detecting mouse motion event to enter/leave the window itself
while the mouse is captured to the parent"""
self.onHLVEnterWindow = handler

def setOnHLVLeaveWindow(self, handler):
"""See setOnHLVLeaveWindow doc."""
self.onHLVLeaveWindow = handler

#popupMenu
def setLeftPopupMenuFunc(self, func):
self.popupMenuFunc[wx.MOUSE_BTN_LEFT] = func

def setMiddlePopupMenuFunc(self, func):
self.popupMenuFunc[wx.MOUSE_BTN_MIDDLE] = func

def setRightPopupMenuFunc(self, func):
self.popupMenuFunc[wx.MOUSE_BTN_RIGHT] = func


def defaultHandler(self, ev):
return True

def onButtonDown(self, ev):
if not self.popupMenuFunc[ev.GetButton()](ev):
return

if self.state == AFTERDCLICK:
self.state = NORMAL
self.evwin.CaptureMouse()

#On GTK, I knoticed if the window is a control, its parent gets mouse events when the window
#captures mouse events with CaptureMouse(). To call ReleaseMouse() properly this object receives
#the mouse leftup events sent to the parent window temporarily.
parent = self.evwin.GetParent()
if parent:
parent.Bind(wx.EVT_LEFT_UP, self.onParentButtonUp)
parent.Bind(wx.EVT_MIDDLE_UP, self.onParentButtonUp)
parent.Bind(wx.EVT_RIGHT_UP, self.onParentButtonUp)
parent.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onMouseCaptureLost)
parent.Bind(wx.EVT_MOTION, self.onParentMouseEvents)
parent.Bind(wx.EVT_ENTER_WINDOW, self.onParentEntering)
parent.Bind(wx.EVT_LEAVE_WINDOW, self.onParentLeaving)

self.secondTimer.Stop()
state = self.state
if state == NORMAL:
self.button = ev.GetButton()
self.state = FIRSTWAIT
self.posx, self.posy = ev.GetPosition()
self.downevattrs = self._getEventAttrs(ev)
self.firstTimer.Start(self.t1, True)
elif state == SECONDWAIT:
self.state = AFTERDCLICK
self.onHLVMouseDClick[self.button](ev)

def onButtonUp(self, ev):
evwin = self.evwin
if evwin.HasCapture():
evwin.ReleaseMouse()
parent = evwin.GetParent()
if parent:
parent.Unbind(wx.EVT_LEFT_UP)
parent.Unbind(wx.EVT_MIDDLE_UP)
parent.Unbind(wx.EVT_RIGHT_UP)
parent.Unbind(wx.EVT_MOUSE_CAPTURE_LOST)
parent.Unbind(wx.EVT_MOTION)
parent.Unbind(wx.EVT_ENTER_WINDOW)
parent.Unbind(wx.EVT_LEAVE_WINDOW)

self.firstTimer.Stop()
state = self.state
if state == FIRSTWAIT:
self.state = SECONDWAIT
self.upevattrs = self._getEventAttrs(ev)
self.secondTimer.Start(self.t2, True)
elif state == AFTERDCLICK:
self.state = NORMAL
elif state == MOUSEDOWN:
self.state = NORMAL
self.onHLVMouseUp[self.button](ev)
elif state == DRAGGING:
self.state = NORMAL
self.onHLVMouseEndDragging[self.button](ev)
self.onHLVMouseUp[self.button](ev)

def onButtonDClick(self, ev):
self.secondTimer.Stop()
state = self.state
if state == NORMAL:
self.onButtonDown(ev)
elif state == SECONDWAIT:
self.state = AFTERDCLICK
self.onHLVMouseDClick[self.button](ev)

def onMoving(self, ev):
if self.state == NORMAL:
self.onHLVMotion(ev)

def onDragging(self, ev):
x, y = ev.GetPosition()
t = self.dragThreshPixel
state = self.state
if state == FIRSTWAIT:
if -t < x - self.posx < t and -t < y - self.posy < t:
return
else:
self.state = DRAGGING
self.firstTimer.Stop()
self.onHLVMouseDown[self.button](ev)
self.onHLVMouseStartDragging[self.button](ev)
elif state == MOUSEDOWN:
if -t < x - self.posx < t and -t < y - self.posy < t:
return
else:
self.state = DRAGGING
self.onHLVMouseStartDragging[self.button](ev)
elif state == DRAGGING:
self.onHLVMouseDragging[self.button](ev)

def onEntering(self, ev):
self.onHLVEnterWindow(ev)

def onLeaving(self, ev):
self.onHLVLeaveWindow(ev)


def onMouseEvents(self, ev):
ev.Skip()
if ev.Moving():
return self.onMoving(ev)
elif ev.Dragging():
return self.onDragging(ev)
elif ev.ButtonDown():
return self.onButtonDown(ev)
elif ev.ButtonUp():
return self.onButtonUp(ev)
elif ev.ButtonDClick():
return self.onButtonDClick(ev)
elif ev.Entering():
return self.onEntering(ev)
elif ev.Leaving():
return self.onLeaving(ev)

def onMouseCaptureLost(self, ev):
self.evwin.ReleaseMouse()
ev.Skip()

def onFirstTimer(self, ev):
self.state = MOUSEDOWN
downev = self._createMouseEvent(self.downevattrs)
self.onHLVMouseDown[self.button](downev)

def onSecondTimer(self, ev):
self.state = NORMAL
downev = self._createMouseEvent(self.downevattrs)
upev = self._createMouseEvent(self.upevattrs)
self.onHLVMouseDown[self.button](downev)
self.onHLVMouseUp[self.button](upev)

def onParentButtonUp(self, ev):
self._convetMouseEvent(ev)
self.onButtonUp(ev)
def onParentMouseEvents(self, ev):
self._convetMouseEvent(ev)
self.onMouseEvents(ev)
def onParentEntering(self, ev):
self._convetMouseEvent(ev)
self.onEntering(ev)
def onParentLeaving(self, ev):
self._convetMouseEvent(ev)
self.onLeaving(ev)

def _convetMouseEvent(self, ev):
evwin = self.evwin
parent = evwin.GetParent()
x, y = ev.GetPosition()
offsetx, offsety = evwin.GetPosition()
ev.m_x = x - offsetx
ev.m_y = y - offsety
ev.SetEventObject(evwin)

def _getEventAttrs(self, ev):
return [
ev.GetEventObject(),
ev.GetEventType(),
ev.GetId(),
ev.GetTimestamp(),
ev.m_altDown,
ev.m_controlDown,
ev.m_leftDown,
ev.m_middleDown,
ev.m_rightDown,
ev.m_metaDown,
ev.m_shiftDown,
ev.m_x,
ev.m_y,
ev.m_wheelRotation,
ev.m_wheelDelta,
ev.m_linesPerAction]

def _createMouseEvent(self, eventAttrs):
obj, typ, id, time, alt, ctrl, l, m, r, meta, shift, x, y, wr, wd, lpa = eventAttrs
ev = wx.MouseEvent(typ)
ev.SetEventObject(obj)
ev.GetEventObject()
ev.SetEventType(typ)
ev.SetId(id)
ev.SetTimestamp(time)
ev.m_altDown = alt
ev.m_controlDown = ctrl
ev.m_leftDown = l
ev.m_middleDown = m
ev.m_rightDown = r
ev.m_metaDown = meta
ev.m_shiftDown = shift
ev.m_x = x
ev.m_y = y
ev.m_wheelRotation = wr
ev.m_wheelDelta = wd
ev.m_linesPerAction = lpa
return ev


if __name__ == '__main__':
class TestWindow(wx.Button):
def __init__(self, parent):
super(TestWindow, self).__init__(parent, -1, pos = (50, 50), size = (150, 150))

hlev = HighLevMouseEventConv(self, dragThreshPixel = 15)

hlev.setOnHLVLeftDown(self.onHLVLeftDown)
hlev.setOnHLVLeftUp(self.onHLVLeftUp)
hlev.setOnHLVLeftDClick(self.onHLVLeftDClick)
hlev.setOnHLVLeftStartDragging(self.onHLVLeftStartDragging)
hlev.setOnHLVLeftDragging(self.onHLVLeftDragging)
hlev.setOnHLVLeftEndDragging(self.onHLVLeftEndDragging)
hlev.setOnHLVMiddleDown(self.onHLVMiddleDown)
hlev.setOnHLVMiddleUp(self.onHLVMiddleUp)
hlev.setOnHLVMiddleDClick(self.onHLVMiddleDClick)
hlev.setOnHLVMiddleStartDragging(self.onHLVMiddleStartDragging)
hlev.setOnHLVMiddleDragging(self.onHLVMiddleDragging)
hlev.setOnHLVMiddleEndDragging(self.onHLVMiddleEndDragging)
hlev.setOnHLVRightDown(self.onHLVRightDown)
hlev.setOnHLVRightUp(self.onHLVRightUp)
hlev.setOnHLVRightDClick(self.onHLVRightDClick)
hlev.setOnHLVRightStartDragging(self.onHLVRightStartDragging)
hlev.setOnHLVRightDragging(self.onHLVRightDragging)
hlev.setOnHLVRightEndDragging(self.onHLVRightEndDragging)
hlev.setOnHLVMotion(self.onHLVMotion)
hlev.setOnHLVEnterWindow(self.onHLVEnterWindow)
hlev.setOnHLVLeaveWindow(self.onHLVLeaveWindow)

self.lup = True
self.dragstate = 'normal'

def onHLVLeftDown(self, ev):
print 'onHLVLeftDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
if not self.lup: print 'ERROR ' * 5
self.lup = False
def onHLVLeftUp(self, ev):
print 'onHLVLeftUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
self.lup = True
def onHLVLeftDClick(self, ev):
print 'onHLVLeftDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVLeftStartDragging(self, ev):
print 'onHLVLeftStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
if self.dragstate != 'normal': print 'ERROR ' * 5
self.dragstate = 'started'
def onHLVLeftDragging(self, ev):
print 'onHLVLeftDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
if not self.dragstate in ('started', 'dragging'): print 'ERROR ' * 5
self.dragstate = 'dragging'
def onHLVLeftEndDragging(self, ev):
print 'onHLVLeftEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
if not self.dragstate in ('started', 'dragging'): print 'ERROR ' * 5
self.dragstate = 'normal'
def onHLVMiddleDown(self, ev):
print 'onHLVMiddleDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMiddleUp(self, ev):
print 'onHLVMiddleUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMiddleDClick(self, ev):
print 'onHLVMiddleDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMiddleStartDragging(self, ev):
print 'onHLVMiddleStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMiddleDragging(self, ev):
print 'onHLVMiddleDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMiddleEndDragging(self, ev):
print 'onHLVMiddleEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightDown(self, ev):
print 'onHLVRightDown', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightUp(self, ev):
print 'onHLVRightUp', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightDClick(self, ev):
print 'onHLVRightDClick', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightStartDragging(self, ev):
print 'onHLVRightStartDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightDragging(self, ev):
print 'onHLVRightDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVRightEndDragging(self, ev):
print 'onHLVRightEndDragging', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVMotion(self, ev):
print 'onHLVMotion', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVEnterWindow(self, ev):
print 'onHLVEnterWindow', ev.GetPosition(), type(ev.GetEventObject()).__name__
def onHLVLeaveWindow(self, ev):
print 'onHLVLeaveWindow', ev.GetPosition(), type(ev.GetEventObject()).__name__


app = wx.App()
frame = wx.Frame(None, -1, "Title", size = (300, 400))
panel = wx.Panel(frame, -1)
testwin = TestWindow(panel)
frame.Show()
app.MainLoop()

Saturday, July 25, 2009

Click selecting a curve

I wanted a nice connection arrow for my app, not just a concatenation of lines like











Recently wxWidgets got cairo and wxPython got it accordingly. Using wxcairo (wx.lib.wxcairo) I could draw a nice line of connection like this.











But there was one problem, in my app, the user needs to be able to select the connection by clicking it. How can I detect if the mouse point is on a connection? I need to calculate the length between the line and the point. cairo uses 3rd order bezier curve. To get the parameter t of the bezier curve that gives the closest point on the curve to the mouse point, I need to differentiate the distance and solve the equation of t (or I can get the condition that on the closest point the tangent vector and the vector towards the mouse point makes the right angle. It should produce the same result). The problem is, the equation is 5th order and I cannot solve it. I thought if I should use numerical calculation but still the calculation cost is too expensive. So I decided to use linear interpolation. It's good as long as the user doesn't recognize the error. I can use bounding box and it won't be time consuming. Finally I decided to divide the curve where t = 0, 0.05, 0.1, 0.15, 0.3, 0.5, 0.7, 0.85, 0.9, 0.95, 1. It should be enough just for a GUI program.


These are several tests.

Divide the curve in 3, 5, 8, and 10













Close up














Final result

Wednesday, July 22, 2009

Evolution Reel

Evolution Reel

From the site:
Most people want to see how a professional animator struggles... well here's the reel for you. A playblast of my progress on a shot at the end of each production day.


From the video:
ICE AGE DAWN OF THE DINOSAURS is property of Fox and Blue Sky Studios
The following is for educational purposes only. The reel demonstrates the entire process of completing a production shot from the rough poses to blocking, splining, and polish. The shot was created working 7 days a week during "crunch." Some days represent 16 hour workdays while others are only 3 hours on a Sunday.

Tuesday, July 21, 2009

Eclipse




















Wishing for a clear sky.

Task Dependency Diagram

When I make a relatively large scale system (here relatively large means something that takes more than one month to develop), I make a task dependency diagram like this.













Blue ones are tasks that has been done, whites are not, greens are milestones, usually tests. And I put a small icon that indicates what task I'm currently doing.












During the last project I drew it on an A3 paper and put it on the wall so that everybody could see what he was doing in the overall project picture and how one's work was related to others (each had an animal shaped magnet, I was monky ;). I did it so that nobody would feel as if he was a cog by showing the goal and having people recognize it was a team work but it was also good for a communication tool.

This task (pointing at a task) needs some try and error and he may need more time than expected, so after you have done your current work you jump here (pointing at a task) to prepare for it before he finishes this one (pointing at a task) and comes here (pointing at a task).

It really works.

Friday, July 17, 2009

A module file can be loaded twice as two different modules (in Python 2.x)

I'll show you a case where one file (mymodule.py) gets loaded twice as two different modules. I'm sure this is trivial to some people, if you know how to do this, you don't have to read the rest of this entry. I will write it anyway hoping it will help somebody since it could bring a huge confusion if he doesn't know why it happens (huge confusion, according to my experience).

Modules and packages in directories found in sys.path are called toplevel. Those which are not in the directories found in sys.path but in a package are still accessible but not toplevel.

Say you have a package named mypackage in a directory you can find in the PYTHONPATH, and it has a module mymodule. My package is a toplevel and mymodule is not.

$ pwd
/home/kotamura/mytest
$ echo $PYTHONPATH
/home/kotamura/mytest/mytoplevel
$ tree mytoplevel/
mytoplevel/
`-- mypackage
|-- __init__.py
`-- mymodule.py
Let's run Python interactively.
$ python
Python 2.6 (r26:66714, Jun  8 2009, 16:07:26)
[GCC 4.4.0 20090506 (Red Hat 4.4.0-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import mypackage.mymodule as mm
>>> mm.__name__
'mypackage.mymodule'
You can see the module is not a toplevel from its name (it's under mypackage.)
Go down the directories and do it again.
$ python
Python 2.6 (r26:66714, Jun  8 2009, 16:07:26)
[GCC 4.4.0 20090506 (Red Hat 4.4.0-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import mypackage.mymodule as mm
>>> mm.__name__
'mypackage.mymodule'

It's not surprising at all. But you can also import mymodule
>>> import mymodule as m
>>> m.__name__
'mymodule'

Now mymodule is toplevel!, This is because if you run Python interactively Python adds the current directory to sys.path. What is important is that mm and m are different objects even if it is created from the same file mymodule.py
>>> mm.a = 1
>>> m.a = 2
>>> mm.a
1
>>> mm is m
False

It's not the case you will see only when you use interactive console. The same thing happens when you import a module from another module without specifying it from the toplevel.
$ cat weirdImporter.py
import mypackage.mymodule as mm
import mymodule as m
print mm.__name__
print m.__name__

You don't have to be in the directory to run weirdImporter.py. You can run it from anywhere
$ python /home/kotamura/mytest/mytoplevel/mypackage/weirdImporter.py
mypackage.mymodule
mymodule

Again mymodule.pygot imported twice.
import mymodule as m

Python lets weirdImporter import mymodule because it is in the same directory, and since weirdImporter runs in the __main__ module (not mypackage.weirdImporter I mean), it imports mymodule as a toplevel. While
import mypackage.mymodule as mm

imports the mymoduel.py as mypackage.mymodule. So they are different.

A solution many people recommend is that you always import a package/module specicfying the path from the toplevel. It will solve every problem. You can also use "relative import" first introduced in Python2.5 but it brings another confusion until you get used to it.

Finally, Python3.x doesn't let weirdImporter import mymoduel.py only because it's in the same directory. But you still need to be careful not to have the same file be imported more than once from multiple toplevels. Don't forget executing 'python /path/to/foo.py' adds /path/to directory to sys.path.

Tuesday, July 14, 2009

wx.MiniPanel

While I was browsing wxPython demo, I found a really nifty window called wx.MiniFrame.


















I thought it was nice for my app but soon realized it's wx.Frame. I wanted it inside my application to draw a graph. I didn't want one of my nodes to travel outside my application window and join Firefox, so I decided to make a similar one as a panel.


















Can you see which one is MiniFrame and which is MiniPanel?











One that cannot' be outside the application window is MiniPanel!
(well, anybody talked about close but something? I don't know... (works on Windows))















I haven't finished minor tunings, e.g. the title is embedded in the code, but it's already resizable.

Jul. 15 added:
I tested on (Python2.6, wxPython-2.8.10.1, Fedora11) and (Python 2.6.2, wxPython 2.8.10.1, WindowsXP). Not tested on Mac.


import wx
import wx.lib.resizewidget as resizewidget
import cStringIO as StringIO

batten = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x11\x00\x00\x00\x10\x08\x02\x00\x00\x00\x7fS\x03\x08\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00`IDAT8OcdX\xf9\x9f\x81d\x00\xd4C*B\xd6\xf0\x1f7@1\x17\xce\x01\xaa\xc7c!\x8a,\xa6\x1edi8\x1b\x9f\x1e\xb8\xeb\x80f\xa1\xb1\x11\xae\xc0j\x0f\xb2\xbf \n\x08\xb8\r\xcd\x06\xa2\xf4\xa0\x05\x1ea=\xe4\xfb\x07\x7f`2P\x14?\xc8^\xc7L\x0f\xd8\xd3\x01\t\xa9\x8e\x04\xa5\xf0\xa4L\x86\x1e\x00\xbc\xf4\xfe\xa6\xcaO\x9a\x16\x00\x00\x00\x00IEND\xaeB`\x82'

bmpstream = StringIO.StringIO(batten)
try:
image = wx.ImageFromStream(bmpstream)
finally:
bmpstream.close()

class _TitleBarPanel(wx.Panel):
def __init__(self, parent, titleFGColour, titleBGColour, barWidth, fontSize, title):
wx.Panel.__init__(self, parent, pos = (0, 0), size = (0, barWidth))
self.SetBackgroundColour(titleBGColour)
self.SetMaxSize((100000, barWidth))
self.parent = parent
self.gparent = parent.GetParent()
self.restwin = None

#Title static text.
self.titlest = titlest = wx.StaticText(self, -1, title, size = (100000, barWidth))
titlest.SetForegroundColour(titleFGColour)
font = titlest.GetFont()
font.SetPointSize(fontSize)
titlest.SetFont(font)

#Close bitmap button.
bfi = wx.BitmapFromImage(image)
self.closeb = b = wx.BitmapButton(self, -1, bfi, (0, 0), (barWidth, barWidth), style = wx.BU_EXACTFIT)
b.SetToolTipString("Delete this node.")

hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(titlest, 1, wx.EXPAND | wx.CENTER)
hsizer.Add(b, 0, wx.RIGHT)
self.SetSizer(hsizer)

self.titlest.Bind(wx.EVT_BUTTON, self.onClose, b)
self.Bind(wx.EVT_BUTTON, self.onClose, b)
self.titlest.Bind(wx.EVT_LEFT_DOWN, self.onTitleMouseLeftDown)
self.titlest.Bind(wx.EVT_LEFT_UP, self.onTitleMouseLeftUp)
self.Bind(wx.EVT_LEFT_UP, self.onTitleMouseLeftUp)
self.titlest.Bind(wx.EVT_MOTION, self.onTitleMouseMotion)
self.Bind(wx.EVT_MOTION, self.onTitleMouseMotion)
self.titlest.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onTitleMouseCaptureLost)
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.onTitleMouseCaptureLost)

def onClose(self, ev):
self.parent.Close()

def onTitleMouseLeftDown(self, ev):
self.titlest.CaptureMouse()
x, y = ev.GetPosition()
sx, sy = self.titlest.GetScreenPosition()
gx, gy = self.gparent.GetPosition()
self.px, self.py = sx + x, sy + y
self.gx, self.gy = gx, gy

def onTitleMouseLeftUp(self, ev):
if self.titlest.HasCapture():
self.titlest.ReleaseMouse()

def onTitleMouseMotion(self, ev):
if ev.Dragging():
x, y = ev.GetPosition()
sx, sy = self.titlest.GetScreenPosition()
px, py = sx + x, sy + y
if self.restwin:
fsx, fsy = self.restwin.GetScreenPosition()
fw, fh = self.restwin.GetSize()
margin = self.margin
if px < fsx + margin or py < fsy + margin or px > fsx + fw - margin or py > fsy + fh - margin:
return
self.gparent.SetPosition((self.gx + px - self.px, self.gy + py - self.py))

def onTitleMouseCaptureLost(self, ev):
self.titlest.ReleaseMouse()

def setDragRestrictWindow(self, window, margin):
self.restwin = window
self.margin = margin

class NodePanel(wx.Panel):
def __init__(self, parent, id = wx.ID_ANY, pos=wx.DefaultPosition, size = wx.DefaultSize,
style = wx.TAB_TRAVERSAL|wx.NO_BORDER, name=wx.PanelNameStr,
borderColour = wx.BLACK, minSize = (0, 0), maxSize = (-1, -1),
bgColour = wx.NullColour, titleFGColour = wx.WHITE, titleBGColour = wx.Colour(80, 167, 255),
barWidth = 13, fontSize = 8, title = 'MiniPanel'):
wx.Panel.__init__(self, parent, id, pos, size, style | wx.FULL_REPAINT_ON_RESIZE, name)
self.borderColour = borderColour

rw = resizewidget.ResizeWidget(parent, pos = pos, style = wx.FULL_REPAINT_ON_RESIZE)
self.SetMinSize(minSize)
self.SetMaxSize(maxSize)
rw.SetManagedChild(self)

self.tb = tb = _TitleBarPanel(self, titleFGColour, titleBGColour, barWidth, fontSize, title)
self.clientPanel = clientPanel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(tb, 0, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border = 3)
sizer.Add(clientPanel, 1, wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, border = 3)

self.SetSizer(sizer)
self.SetBackgroundColour(bgColour)
clientPanel.SetBackgroundColour(bgColour)

rw.SetSize(size)

self.Bind(wx.EVT_PAINT,self.OnPaint)
self.Bind(wx.EVT_IDLE,self.OnPaintOnIdle)

def OnPaintOnIdle(self, ev):
#self._onPaint(wx.ClientDC(self))
pass

def OnPaint(self, ev):
self._onPaint(wx.PaintDC(self))

def _onPaint(self, dc):
#Use double buffering as you like.
#(wxPython-demo dir)/samples/pySketch/pySketch.py has a good example of it.
dc.SetPen(wx.Pen(self.borderColour))
x, y = self.GetSize()
dc.DrawLine(0, 0, x-1, 0)
dc.DrawLine(0, 0, 0, y-1)
dc.DrawLine(x-1, 0, x-1, y-1)
dc.DrawLine(0, y-1, x-1, y-1)

def setDragRestrictWindow(self, window, margin = 0):
#If you don't set this, the Panel will go outside the window. Is there a nice way? :(
self.tb.setDragRestrictWindow(window, margin)

def getClientPanel(self):
return self.clientPanel

def getTitleStaticText(self):
return self.tb.titlest

def Destroy(self):
#The resize handle is the parent of the Panel(reparented). We want to destroy
#the handle too. We'll override Destroy() to make sure it is destroyed.
self.GetParent().Destroy()


if __name__ == '__main__':
myapp=wx.PySimpleApp()
frame = wx.Frame(None, -1, "Title", size = wx.Size(300, 400))
scroll = wx.ScrolledWindow(frame, -1)
scroll.SetScrollbars(50,50,10,10)
np = NodePanel(scroll, pos = (30, 30), size = (200, 200), borderColour = (128, 128, 128))

np.setDragRestrictWindow(scroll, 15)

print np.getTitleStaticText().GetLabel()

def OnClose(ev):
if ev.CanVeto():
dlg = wx.MessageDialog(None, 'Are you sure?', 'msgb', wx.YES_NO)
if dlg.ShowModal() == wx.ID_YES:
np.GetParent().Destroy()
dlg.Destroy()

else:
np.Destroy()


np.Bind(wx.EVT_CLOSE, OnClose, np)


def CloseIt(ev):
np.Close()
clientAreaCloseButton = wx.Button(np.getClientPanel(), -1, "Close Me", pos = (20, 20))
np.Bind(wx.EVT_BUTTON, CloseIt, clientAreaCloseButton)

frame.Show()
myapp.MainLoop()

Thursday, July 9, 2009

Wednesday, July 8, 2009

Thread Manager

Last update Jun 10

import thread, threading

class _WorkerThread(threading.Thread):
    def __init__(self, sem, job, *args, **kargs):
        self._workSem = sem
        super(_WorkerThread, self).__init__()
        self.job = job
        self.args = args
        self.kargs = kargs
        self.isCanceled = False
        self.hasStarted = False
        self._cancelLock = threading.Lock()

    def run(self):
        if self._workSem:
            with self._workSem:
                with self._cancelLock:
                    if self.isCanceled:
                        return
                    self.hasStarted = True
                self.job(*self.args, **self.kargs)
        else:
            self.job(*self.args, **self.kargs)

class JobManager(object):
    def __init__(self, maxnumthreads = 1):
        """Set maxnumthreads to specify the max number of threads which runs concurrently."""
        self._workers = {}
        self._unmanagedWorkers = []
        self._workSem = threading.Semaphore(maxnumthreads)
        self.maxnumthreads = maxnumthreads
        self._lock = threading.RLock()
        self._nextJobIdCounter = 1

    def postJob(self, job, *args, **kargs):
        """job must be a callable, args and kargs are arguments passed to it."""
        self._gc()
        wt = _WorkerThread(self._workSem, job, *args, **kargs)
        wt.start()
        id = self._nextJobIdCounter
        self._workers[id] = wt
        self._nextJobIdCounter += 1
        return id

    def cancelJob(self, jobid):
        """Cancel a posted job. jobid must be an object returned by postJob().
        It returns True if the job gets canceled, False it it has started."""
        self._gc()
        worker = self._workers.get(jobid, None)
        if not worker: #It doesn't exist because it finished execution and removed from the _workers
            return False
        with worker._cancelLock:
            worker.isCanceled = True
            return not worker.hasStarted

    def waitOnIdle(self):
        """Blocks until the every worker thread terminates."""
        self._gc()
        while self._workers or self._unmanagedWorkers:
            wt = (self._workers.values() + self._unmanagedWorkers).pop()
            wt.join()
            self._gc()

    def getNumWaitingJobs(self):
        """Returns the number of jobs waiting. It includes threads currently running."""
        self._gc()
        return len(self._workers)

    def forceExecuteOnWorkerThread(self, job, *args, **kargs):
        """Execute the job immediately on a thread. It is not queued."""
        wt = _WorkerThread(None, job, *args, **kargs)
        wt.start()
        self._unmanagedWorkers.append(wt)

    def executeWhenNoWorkerThreadsRunning(self, job, *args, **kargs):
        """The calling thread execute the job (callable), ensuring no worker threads running.
        It blocks when a thread is running.It DOESN'T mean job is executed after the every
        worker threads has been terminated.
        (Though it looks the current Python implementation awakens a thread which called acquire()
        earlier.)
        It doesn't take jobs launched by forceExecuteOnWorkerThread() into account."""
        for i in range(self.maxnumthreads):
            self._workSem.acquire()
        try:
            job(*args, **kargs)
        finally:
            for i in range(self.maxnumthreads):
                self._workSem.release()

    def _gc(self):
        with self._lock:
            self._workers = dict([w for w in self._workers.items() if w[1].isAlive()])
            self._unmanagedWorkers = [wt for wt in self._unmanagedWorkers if wt.isAlive()]


if __name__ == '__main__':
    import time
    def somejob(i, wait = 0.1):
        time.sleep(wait)
        print 'somejob', i
        time.sleep(1)
    def endMessage(msg):
        print msg

    jm = JobManager()
    jobIds = []
    for i in range(5):
        id = jm.postJob(somejob, i + 1)
        jobIds.append(id)
        print 'posted', id
    time.sleep(2)
    print "cancel", jobIds[0], jm.cancelJob(jobIds[0])
    print "cancel", jobIds[-1], jm.cancelJob(jobIds[-1])
    #print jm.getNumWaitingJobs()
    jm.forceExecuteOnWorkerThread(somejob, 'unmanaged1', 4)
    jm.waitOnIdle() #It waits for unmanaged1 termination
    jm.forceExecuteOnWorkerThread(somejob, 'unmanaged2')
    jm.executeWhenNoWorkerThreadsRunning(endMessage, 'done') #It doesn't wait for unmanaged2

Thursday, July 2, 2009

Writing a technical document

When I write a technical document, I try to be careful to keep three things.

1) Do not use the term that hasn't been explained in the earlier pages.
I think this is a basic rule that every document must keep, but lots of them break it. I once read a document about a Python framework which uses a term "jelly" (is that case, it used the term in method names) with no explanation beforehand. It took me a long time until I know its meaning. I hoped if the writer had added just one line saying "jelly means serialize". (although the framework itself is very useful. Lots of its terms are named after a sandwich. You guess what it is? ;).

2) Do not try to explain two things at a time.
Human brains are not designed to think about two things at a time. When you explain recursive function call, don't take mandelbrot as an example. Think about an average reader and take an example that most reader is supposed to know.

3) "Make a list of stuffs to write, think about the explanation flow of the document".
I usually make a graph for 1) and 3), which has a set of circles, each circle contains one element of the list , and dependencies as arrows between the circles.