diff --git a/bufferedcanvas.py b/bufferedcanvas.py new file mode 100644 index 0000000..5e0868e --- /dev/null +++ b/bufferedcanvas.py @@ -0,0 +1,117 @@ +""" +BufferedCanvas -- flicker-free canvas widget +Copyright (C) 2005, 2006 Daniel Keep, 2011 Duane Johnson + +To use this widget, just override or replace the draw method. +This will be called whenever the widget size changes, or when +the update method is explicitly called. + +Please submit any improvements/bugfixes/ideas to the following +url: + + http://wiki.wxpython.org/index.cgi/BufferedCanvas + +2006-04-29: Added bugfix for a crash on Mac provided by Marc Jans. +""" + +# Hint: try removing '.sp4msux0rz' +__author__ = 'Daniel Keep ' + +__license__ = """ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public License as +published by the Free Software Foundation; either version 2.1 of the +License, or (at your option) any later version. + +As a special exception, the copyright holders of this library +hereby recind Section 3 of the GNU Lesser General Public License. This +means that you MAY NOT apply the terms of the ordinary GNU General +Public License instead of this License to any given copy of the +Library. This has been done to prevent users of the Library from being +denied access or the ability to use future improvements. + +This library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +__all__ = ['BufferedCanvas'] + +import wx + +class BufferedCanvas(wx.Panel): + """ + Implements a flicker-free canvas widget. + + Standard usage is to subclass this class, and override the + draw method. The draw method is passed a device context, which + should be used to do your drawing. + + If you want to force a redraw (for whatever reason), you should + call the update method. This is because the draw method is never + called as a result of an EVT_PAINT event. + """ + + # These are our two buffers. Just be aware that when the buffers + # are flipped, the REFERENCES are swapped. So I wouldn't want to + # try holding onto explicit references to one or the other ;) + buffer = None + backbuffer = None + + def __init__(self, + parent, + ID=-1, + pos=wx.DefaultPosition, + size=wx.DefaultSize, + style=wx.NO_FULL_REPAINT_ON_RESIZE|wx.WANTS_CHARS): + wx.Panel.__init__(self,parent,ID,pos,size,style) + + # Bind events + self.Bind(wx.EVT_PAINT, self.onPaint) + + # Disable background erasing (flicker-licious) + def disable_event(*pargs,**kwargs): + pass # the sauce, please + self.Bind(wx.EVT_ERASE_BACKGROUND, disable_event) + + ## + ## General methods + ## + + def draw(self,dc): + """ + Stub: called when the canvas needs to be re-drawn. + """ + pass + + def update(self): + """ + Causes the canvas to be updated. + """ + self.Refresh() + + def getWidthHeight(self): + width,height = self.GetClientSizeTuple() + if width == 0: + width = 1 + if height == 0: + height = 1 + return (width, height) + + ## + ## Event handlers + ## + + def onPaint(self, event): + # Blit the front buffer to the screen + w, h = self.GetClientSizeTuple() + if not w or not h: + return + else: + dc = wx.BufferedPaintDC(self) + self.draw(dc, w, h) diff --git a/images/arrow_keys.png b/images/arrow_keys.png new file mode 100644 index 0000000..192871a Binary files /dev/null and b/images/arrow_keys.png differ diff --git a/images/control_xy.png b/images/control_xy.png new file mode 100644 index 0000000..12dcda7 Binary files /dev/null and b/images/control_xy.png differ diff --git a/images/control_z.png b/images/control_z.png new file mode 100644 index 0000000..56553d7 Binary files /dev/null and b/images/control_z.png differ diff --git a/pronterface.py b/pronterface.py index 71ee80c..f2aee6b 100755 --- a/pronterface.py +++ b/pronterface.py @@ -30,6 +30,8 @@ if os.name=="nt": pass +from xybuttons import XYButtons +from zbuttons import ZButtons import pronsole def dosify(name): @@ -65,6 +67,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): wx.Frame.__init__(self,None,title=_("Printer Interface"),size=size); self.SetIcon(wx.Icon("P-face.ico",wx.BITMAP_TYPE_ICO)) self.panel=wx.Panel(self,-1,size=size) + self.panel.SetBackgroundColour("white") self.statuscheck=False self.tempreport="" self.monitor=0 @@ -74,36 +77,10 @@ class PronterWindow(wx.Frame,pronsole.pronsole): ycol=(180,180,255) zcol=(180,255,180) self.cpbuttons=[ - [_("X+100"),("move X 100"),(2,0),xcol,(1,3)], - [_("X+10"),("move X 10"),(3,0),xcol,(1,3)], - [_("X+1"),("move X 1"),(4,0),xcol,(1,3)], - [_("X+0.1"),("move X 0.1"),(5,0),xcol,(1,3)], - [_("HomeX"),("home X"),(6,0),(205,205,78),(1,3)], - [_("X-0.1"),("move X -0.1"),(7,0),xcol,(1,3)], - [_("X-1"),("move X -1"),(8,0),xcol,(1,3)], - [_("X-10"),("move X -10"),(9,0),xcol,(1,3)], - [_("X-100"),("move X -100"),(10,0),xcol,(1,3)], - [_("Y+100"),("move Y 100"),(2,3),ycol,(1,3)], - [_("Y+10"),("move Y 10"),(3,3),ycol,(1,3)], - [_("Y+1"),("move Y 1"),(4,3),ycol,(1,3)], - [_("Y+0.1"),("move Y 0.1"),(5,3),ycol,(1,3)], - [_("HomeY"),("home Y"),(6,3),(150,150,205),(1,3)], - [_("Y-0.1"),("move Y -0.1"),(7,3),ycol,(1,3)], - [_("Y-1"),("move Y -1"),(8,3),ycol,(1,3)], - [_("Y-10"),("move Y -10"),(9,3),ycol,(1,3)], - [_("Y-100"),("move Y -100"),(10,3),ycol,(1,3)], - [_("Motors off"),("M84"),(2,6),(250,250,250),(1,3)], - [_("Z+10"),("move Z 10"),(3,6),zcol,(1,3)], - [_("Z+1"),("move Z 1"),(4,6),zcol,(1,3)], - [_("Z+0.1"),("move Z 0.1"),(5,6),zcol,(1,3)], - [_("HomeZ"),("home Z"),(6,6),(150,205,150),(1,3)], - [_("Z-0.1"),("move Z -0.1"),(7,6),zcol,(1,3)], - [_("Z-1"),("move Z -1"),(8,6),zcol,(1,3)], - [_("Z-10"),("move Z -10"),(9,6),zcol,(1,3)], - [_("Home"),("home"),(10,6),(250,250,250),(1,3)], - [_("Check temp"),("M105"),(11,6),(225,200,200),(1,3)], - [_("Extrude"),("extrude"),(13,0),(225,200,200),(1,2)], - [_("Reverse"),("reverse"),(14,0),(225,200,200),(1,2)], + [_("Motors off"),("M84"),(1,0),(250,250,250),(1,2)], + [_("Check temp"),("M105"),(3,5),(225,200,200),(1,3)], + [_("Extrude"),("extrude"),(5,0),(225,200,200),(1,2)], + [_("Reverse"),("reverse"),(6,0),(225,200,200),(1,2)], ] self.custombuttons=[] self.btndict={} @@ -158,6 +135,11 @@ class PronterWindow(wx.Frame,pronsole.pronsole): wx.CallAfter(self.connectbtn.Disable) for i in self.printerControls: wx.CallAfter(i.Enable) + + # Enable XYButtons and ZButtons + self.xyb.enable() + self.zb.enable() + if self.filename: wx.CallAfter(self.printbtn.Enable) @@ -417,7 +399,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): portslist += [self.settings.port] self.serialport = wx.ComboBox(self.panel, -1, choices=portslist, - style=wx.CB_DROPDOWN|wx.CB_SORT, pos=(50,0)) + style=wx.CB_DROPDOWN|wx.CB_SORT|wx.CB_READONLY, pos=(50,0)) try: if self.settings.port in scan: self.serialport.SetValue(self.settings.port) @@ -428,8 +410,8 @@ class PronterWindow(wx.Frame,pronsole.pronsole): uts.Add(self.serialport) uts.Add(wx.StaticText(self.panel,-1,"@",pos=(250,5)),wx.RIGHT,5) self.baud = wx.ComboBox(self.panel, -1, - choices=["2400", "9600", "19200", "38400", "57600", "115200"], - style=wx.CB_DROPDOWN|wx.CB_SORT, size=(110,30),pos=(275,0)) + choices=["2400", "9600", "19200", "38400", "57600", "115200", "250000"], + style=wx.CB_DROPDOWN|wx.CB_SORT|wx.CB_READONLY, size=(110,30),pos=(275,0)) try: self.baud.SetValue("115200") self.baud.SetValue(str(self.settings.baudrate)) @@ -510,14 +492,20 @@ class PronterWindow(wx.Frame,pronsole.pronsole): lls=self.lowerlsizer=wx.GridBagSizer() lls.Add(wx.StaticText(self.panel,-1,_("mm/min"),pos=(60,69)),pos=(0,4),span=(1,4)) self.xyfeedc=wx.SpinCtrl(self.panel,-1,str(self.settings.xy_feedrate),min=0,max=50000,size=(70,25),pos=(25,83)) - lls.Add(wx.StaticText(self.panel,-1,_("XY:"),pos=(2,90-2)),pos=(1,0),span=(1,2)) - lls.Add(self.xyfeedc,pos=(1,2),span=(1,4)) - lls.Add(wx.StaticText(self.panel,-1,_("Z:"),pos=(90,90-2)),pos=(1,6),span=(1,2)) + lls.Add(wx.StaticText(self.panel,-1,_("XY:"),pos=(2,90-2)),pos=(1,3),span=(1,1), flag=wx.ALIGN_CENTER) + lls.Add(self.xyfeedc,pos=(1,4),span=(1,2)) + lls.Add(wx.StaticText(self.panel,-1,_("Z:"),pos=(90,90-2)),pos=(1,6),span=(1,1), flag=wx.ALIGN_CENTER) self.zfeedc=wx.SpinCtrl(self.panel,-1,str(self.settings.z_feedrate),min=0,max=50000,size=(70,25),pos=(105,83)) - lls.Add(self.zfeedc,pos=(1,8),span=(1,4)) + lls.Add(self.zfeedc,pos=(1,7),span=(1,3)) #lls.Add((200,375)) + self.xyb = XYButtons(self.panel, self.moveXY, self.homeButtonClicked) + lls.Add(self.xyb, pos=(2,0), span=(1,6), flag=wx.ALIGN_CENTER) + self.zb = ZButtons(self.panel, self.moveZ) + lls.Add(self.zb, pos=(2,7), span=(1,2), flag=wx.ALIGN_CENTER) + wx.CallAfter(self.xyb.SetFocus) + for i in self.cpbuttons: btn=wx.Button(self.panel,-1,i[0])#,size=(60,-1)) btn.SetBackgroundColour(i[3]) @@ -529,45 +517,45 @@ class PronterWindow(wx.Frame,pronsole.pronsole): lls.Add(btn,pos=i[2],span=i[4]) - lls.Add(wx.StaticText(self.panel,-1,_("Heater:"),pos=(0,343)),pos=(11,0),span=(1,1)) + lls.Add(wx.StaticText(self.panel,-1,_("Heater:"),pos=(0,343)),pos=(3,0),span=(1,1),flag=wx.ALIGN_CENTER) htemp_choices=[self.temps[i]+" ("+i+")" for i in sorted(self.temps.keys(),key=lambda x:self.temps[x])] self.settoff=wx.Button(self.panel,-1,_("Off"),size=(36,-1),pos=(45,335)) self.settoff.Bind(wx.EVT_BUTTON,lambda e:self.do_settemp("off")) self.printerControls.append(self.settoff) - lls.Add(self.settoff,pos=(11,1),span=(1,1)) + lls.Add(self.settoff,pos=(3,1),span=(1,1)) if self.settings.last_temperature not in map(float,self.temps.values()): htemp_choices = [str(self.settings.last_temperature)] + htemp_choices self.htemp=wx.ComboBox(self.panel, -1, choices=htemp_choices,style=wx.CB_DROPDOWN, size=(60,25),pos=(45,337)) self.htemp.Bind(wx.EVT_COMBOBOX,self.htemp_change) - lls.Add(self.htemp,pos=(11,2),span=(1,2)) - + + lls.Add(self.htemp,pos=(3,2),span=(1,2)) self.settbtn=wx.Button(self.panel,-1,_("Set"),size=(36,-1),pos=(125,335)) self.settbtn.Bind(wx.EVT_BUTTON,self.do_settemp) self.printerControls.append(self.settbtn) - lls.Add(self.settbtn,pos=(11,4),span=(1,2)) + lls.Add(self.settbtn,pos=(3,4),span=(1,1)) - lls.Add(wx.StaticText(self.panel,-1,_("Bed:"),pos=(0,343)),pos=(12,0),span=(1,1)) + lls.Add(wx.StaticText(self.panel,-1,_("Bed:"),pos=(0,343)),pos=(4,0),span=(1,1),flag=wx.ALIGN_CENTER) btemp_choices=[self.bedtemps[i]+" ("+i+")" for i in sorted(self.bedtemps.keys(),key=lambda x:self.temps[x])] self.setboff=wx.Button(self.panel,-1,_("Off"),size=(36,-1),pos=(135,335)) self.setboff.Bind(wx.EVT_BUTTON,lambda e:self.do_bedtemp("off")) self.printerControls.append(self.setboff) - lls.Add(self.setboff,pos=(12,1),span=(1,1)) + lls.Add(self.setboff,pos=(4,1),span=(1,1)) if self.settings.last_bed_temperature not in map(float,self.bedtemps.values()): btemp_choices = [str(self.settings.last_bed_temperature)] + btemp_choices self.btemp=wx.ComboBox(self.panel, -1, choices=btemp_choices,style=wx.CB_DROPDOWN, size=(60,25),pos=(135,367)) self.btemp.Bind(wx.EVT_COMBOBOX,self.btemp_change) - lls.Add(self.btemp,pos=(12,2),span=(1,2)) + lls.Add(self.btemp,pos=(4,2),span=(1,2)) self.setbbtn=wx.Button(self.panel,-1,_("Set"),size=(38,-1),pos=(135,365)) self.setbbtn.Bind(wx.EVT_BUTTON,self.do_bedtemp) self.printerControls.append(self.setbbtn) - lls.Add(self.setbbtn,pos=(12,4),span=(1,2)) + lls.Add(self.setbbtn,pos=(4,4),span=(1,2)) self.btemp.SetValue(str(self.settings.last_bed_temperature)) self.htemp.SetValue(str(self.settings.last_temperature)) @@ -588,25 +576,27 @@ class PronterWindow(wx.Frame,pronsole.pronsole): if( '(' not in self.htemp.Value): self.htemp.SetValue(self.htemp.Value + ' (user)') + #lls.Add(self.btemp,pos=(4,1),span=(1,3)) + #lls.Add(self.setbbtn,pos=(4,4),span=(1,2)) self.tempdisp=wx.StaticText(self.panel,-1,"") - lls.Add(self.tempdisp,pos=(12,6),span=(1,3)) + lls.Add(self.tempdisp,pos=(4,6),span=(1,3)) self.edist=wx.SpinCtrl(self.panel,-1,"5",min=0,max=1000,size=(60,25),pos=(70,398)) self.edist.SetBackgroundColour((225,200,200)) self.edist.SetForegroundColour("black") - lls.Add(self.edist,pos=(13,3),span=(1,2)) - lls.Add(wx.StaticText(self.panel,-1,_("mm"),pos=(130,407)),pos=(13,5),span=(1,2)) + lls.Add(self.edist,pos=(5,2),span=(1,1)) + lls.Add(wx.StaticText(self.panel,-1,_("mm"),pos=(130,407)),pos=(5,3),span=(1,2)) self.efeedc=wx.SpinCtrl(self.panel,-1,str(self.settings.e_feedrate),min=0,max=50000,size=(60,25),pos=(70,397+28)) self.efeedc.SetBackgroundColour((225,200,200)) self.efeedc.SetForegroundColour("black") self.efeedc.Bind(wx.EVT_SPINCTRL,self.setfeeds) - lls.Add(self.efeedc,pos=(14,3),span=(1,2)) - lls.Add(wx.StaticText(self.panel,-1,_("mm/min"),pos=(130,407+27)),pos=(14,5),span=(1,2)) + lls.Add(self.efeedc,pos=(6,2),span=(1,1)) + lls.Add(wx.StaticText(self.panel,-1,_("mm/min"),pos=(130,407+27)),pos=(6,3),span=(1,2)) self.xyfeedc.Bind(wx.EVT_SPINCTRL,self.setfeeds) self.zfeedc.Bind(wx.EVT_SPINCTRL,self.setfeeds) self.zfeedc.SetBackgroundColour((180,255,180)) self.zfeedc.SetForegroundColour("black") - lls.Add((10,0),pos=(0,11),span=(1,1)) + # lls.Add((10,0),pos=(0,11),span=(1,1)) self.gviz=gviz.gviz(self.panel,(300,300), bedsize=(self.settings.bed_size_x,self.settings.bed_size_y), grid=(self.settings.preview_grid_step1,self.settings.preview_grid_step2), @@ -620,7 +610,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.gwindow.Bind(wx.EVT_CLOSE,lambda x:self.gwindow.Hide()) cs=self.centersizer=wx.GridBagSizer() cs.Add(self.gviz,pos=(0,0),span=(1,3)) - lls.Add(cs,pos=(0,10),span=(15,1)) + lls.Add(cs,pos=(0,10),span=(8,1)) self.uppersizer=wx.BoxSizer(wx.VERTICAL) self.uppersizer.Add(self.uppertopsizer) @@ -973,6 +963,26 @@ class PronterWindow(wx.Frame,pronsole.pronsole): else: e.Skip() + def homeButtonClicked(self, corner): + if corner == 0: # upper-left + self.onecmd('home X') + if corner == 1: # upper-right + self.onecmd('home Y') + if corner == 2: # lower-right + self.onecmd('home Z') + if corner == 3: # lower-left + self.onecmd('home') + + def moveXY(self, x, y): + if x != 0: + self.onecmd('move X %s' % x) + if y != 0: + self.onecmd('move Y %s' % y) + + def moveZ(self, z): + if z != 0: + self.onecmd('move Z %s' % z) + def procbutton(self,e): try: if hasattr(e.GetEventObject(),"custombutton"): @@ -1379,6 +1389,10 @@ class PronterWindow(wx.Frame,pronsole.pronsole): wx.CallAfter(self.pausebtn.Disable); for i in self.printerControls: wx.CallAfter(i.Disable) + + # Disable XYButtons and ZButtons + self.xyb.disable() + self.zb.disable() if self.paused: self.p.paused=0 diff --git a/test.py b/test.py new file mode 100644 index 0000000..a5ab9f7 --- /dev/null +++ b/test.py @@ -0,0 +1,34 @@ +import wx, os, math +from bufferedcanvas import * + +from xybuttons import XYButtons +from zbuttons import ZButtons + +class MyFrame(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition, wx.Size(600, 400)) + sizer = wx.BoxSizer() + self.xy = XYButtons(self, moveCallback=self.moveXY) + sizer.Add(self.xy, flag=wx.ALIGN_CENTER) + self.z = ZButtons(self, moveCallback=self.moveZ) + sizer.Add(self.z, flag=wx.ALIGN_CENTER) + + self.SetSizer(sizer) + self.SetBackgroundColour("white") + + def moveXY(self, x, y): + print "got x", x, 'y', y + + def moveZ(self, z): + print "got z", z + + +class MyApp(wx.App): + def OnInit(self): + frame = MyFrame(None, -1, 'test.py') + frame.Show(True) + frame.Centre() + return True + +app = MyApp(0) +app.MainLoop() \ No newline at end of file diff --git a/xybuttons.py b/xybuttons.py new file mode 100644 index 0000000..50b5575 --- /dev/null +++ b/xybuttons.py @@ -0,0 +1,330 @@ +import wx, os, math +from bufferedcanvas import * + +def imagefile(filename): + return os.path.join(os.path.dirname(__file__), "images", filename) + +def sign(n): + if n < 0: return -1 + elif n > 0: return 1 + else: return 0 + +class XYButtons(BufferedCanvas): + keypad_positions = { + 0: (105, 102), + 1: (86, 83), + 2: (68, 65), + 3: (53, 50) + } + corner_size = (49, 49) + corner_inset = (8, 6) + label_overlay_positions = { + 0: (142, 105, 11), + 1: (160, 85, 13), + 2: (179, 65, 15), + 3: (201, 42, 16) + } + concentric_circle_radii = [11, 45, 69, 94, 115] + center = (124, 121) + spacer = 7 + + def __init__(self, parent, moveCallback=None, cornerCallback=None, ID=-1): + self.bg_bmp = wx.Image(imagefile("control_xy.png"),wx.BITMAP_TYPE_PNG).ConvertToBitmap() + self.keypad_bmp = wx.Image(imagefile("arrow_keys.png"),wx.BITMAP_TYPE_PNG).ConvertToBitmap() + self.keypad_idx = -1 + self.quadrant = None + self.concentric = None + self.corner = None + self.moveCallback = moveCallback + self.cornerCallback = cornerCallback + self.enabled = False + + BufferedCanvas.__init__(self, parent, ID) + + self.SetSize(self.bg_bmp.GetSize()) + + # Set up mouse and keyboard event capture + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) + self.Bind(wx.EVT_KEY_UP, self.OnKey) + wx.GetTopLevelParent(self).Bind(wx.EVT_CHAR_HOOK, self.OnTopLevelKey) + + def disable(self): + self.enabled = False + self.update() + + def enable(self): + self.enabled = True + self.update() + + def distanceToLine(self, pos, x1, y1, x2, y2): + xlen = x2 - x1 + ylen = y2 - y1 + pxlen = x1 - pos.x + pylen = y1 - pos.y + return abs(xlen*pylen-ylen*pxlen)/math.sqrt(xlen**2+ylen**2) + + def distanceToPoint(self, x1, y1, x2, y2): + return math.sqrt((x1-x2)**2 + (y1-y2)**2) + + def cycleKeypadIndex(self): + idx = self.keypad_idx + 1 + if idx > 2: idx = 0 + return idx + + def setKeypadIndex(self, idx): + self.keypad_idx = idx + self.update() + + def getMovement(self): + xdir = [1, 0, -1, 0][self.quadrant] + ydir = [0, 1, 0, -1][self.quadrant] + magnitude = math.pow(10, self.concentric-1) + return (magnitude * xdir, magnitude * ydir) + + def lookupConcentric(self, radius): + idx = 0 + for r in XYButtons.concentric_circle_radii[1:]: + if radius < r: + return idx + idx += 1 + return len(XYButtons.concentric_circle_radii) + + def getQuadrantConcentricFromPosition(self, pos): + rel_x = pos[0] - XYButtons.center[0] + rel_y = pos[1] - XYButtons.center[1] + radius = math.sqrt(rel_x**2 + rel_y**2) + if rel_x > rel_y and rel_x > -rel_y: + quadrant = 0 # Right + elif rel_x <= rel_y and rel_x > -rel_y: + quadrant = 3 # Down + elif rel_x > rel_y and rel_x < -rel_y: + quadrant = 1 # Up + else: + quadrant = 2 # Left + + idx = self.lookupConcentric(radius) + return (quadrant, idx) + + def mouseOverKeypad(self, mpos): + for idx, kpos in XYButtons.keypad_positions.items(): + radius = self.distanceToPoint(mpos[0], mpos[1], kpos[0], kpos[1]) + if radius < 9: + return idx + return None + + def drawPartialPie(self, gc, center, r1, r2, angle1, angle2): + p1 = wx.Point(center.x + r1*math.cos(angle1), center.y + r1*math.sin(angle1)) + + path = gc.CreatePath() + path.MoveToPoint(p1.x, p1.y) + path.AddArc(center.x, center.y, r1, angle1, angle2, True) + path.AddArc(center.x, center.y, r2, angle2, angle1, False) + path.AddLineToPoint(p1.x, p1.y) + gc.DrawPath(path) + + def highlightQuadrant(self, gc, quadrant, concentric): + assert(quadrant >= 0 and quadrant <= 3) + assert(concentric >= 0 and concentric <= 3) + + inner_ring_radius = XYButtons.concentric_circle_radii[0] + # fudge = math.pi*0.002 + fudge = -0.02 + center = wx.Point(XYButtons.center[0], XYButtons.center[1]) + if quadrant == 0: + a1, a2 = (-math.pi*0.25, math.pi*0.25) + center.x += inner_ring_radius + elif quadrant == 1: + a1, a2 = (math.pi*1.25, math.pi*1.75) + center.y -= inner_ring_radius + elif quadrant == 2: + a1, a2 = (math.pi*0.75, math.pi*1.25) + center.x -= inner_ring_radius + elif quadrant == 3: + a1, a2 = (math.pi*0.25, math.pi*0.75) + center.y += inner_ring_radius + + r1 = XYButtons.concentric_circle_radii[concentric] + r2 = XYButtons.concentric_circle_radii[concentric+1] + + self.drawPartialPie(gc, center, r1-inner_ring_radius, r2-inner_ring_radius, a1+fudge, a2-fudge) + + def drawCorner(self, gc, x, y, angle=0.0): + w, h = XYButtons.corner_size + + gc.PushState() + gc.Translate(x, y) + gc.Rotate(angle) + path = gc.CreatePath() + path.MoveToPoint(-w/2, -h/2) + path.AddLineToPoint(w/2, -h/2) + path.AddLineToPoint(w/2, -h/2+h/3) + path.AddLineToPoint(-w/2+w/3, h/2) + path.AddLineToPoint(-w/2, h/2) + path.AddLineToPoint(-w/2, -h/2) + gc.DrawPath(path) + gc.PopState() + + def highlightCorner(self, gc, corner=0): + w, h = XYButtons.corner_size + cx, cy = XYButtons.center + ww, wh = self.GetSizeTuple() + + inset = 10 + if corner == 0: + x, y = (cx - ww/2 + inset, cy - wh/2 + inset) + self.drawCorner(gc, x+w/2, y+h/2, 0) + elif corner == 1: + x, y = (cx + ww/2 - inset, cy - wh/2 + inset) + self.drawCorner(gc, x-w/2, y+h/2, math.pi/2) + elif corner == 2: + x, y = (cx + ww/2 - inset, cy + wh/2 - inset) + self.drawCorner(gc, x-w/2, y-h/2, math.pi) + elif corner == 3: + x, y = (cx - ww/2 + inset, cy + wh/2 - inset) + self.drawCorner(gc, x+w/2, y-h/2, math.pi*3/2) + + + def draw(self, dc, w, h): + dc.Clear() + gc = wx.GraphicsContext.Create(dc) + + center = wx.Point(XYButtons.center[0], XYButtons.center[1]) + w, h = (self.bg_bmp.GetWidth(), self.bg_bmp.GetHeight()) + gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) + + if self.enabled: + # Brush and pen for grey overlay when mouse hovers over + gc.SetPen(wx.Pen(wx.Colour(100,100,100,172), 4)) + gc.SetBrush(wx.Brush(wx.Colour(0,0,0,128))) + + if self.concentric != None: + if self.concentric < len(XYButtons.concentric_circle_radii): + if self.quadrant != None: + self.highlightQuadrant(gc, self.quadrant, self.concentric) + elif self.corner != None: + self.highlightCorner(gc, self.corner) + + if self.keypad_idx >= 0: + padw, padh = (self.keypad_bmp.GetWidth(), self.keypad_bmp.GetHeight()) + pos = XYButtons.keypad_positions[self.keypad_idx] + pos = (pos[0] - padw/2 - 3, pos[1] - padh/2 - 3) + gc.DrawBitmap(self.keypad_bmp, pos[0], pos[1], padw, padh) + + # Draw label overlays + gc.SetPen(wx.Pen(wx.Colour(255,255,255,128), 1)) + gc.SetBrush(wx.Brush(wx.Colour(255,255,255,128+64))) + for idx, kpos in XYButtons.label_overlay_positions.items(): + if idx != self.concentric: + r = kpos[2] + gc.DrawEllipse(kpos[0]-r, kpos[1]-r, r*2, r*2) + else: + gc.SetPen(wx.Pen(wx.Colour(255,255,255,0), 4)) + gc.SetBrush(wx.Brush(wx.Colour(255,255,255,128))) + gc.DrawRectangle(0, 0, w, h) + + + # Used to check exact position of keypad dots, should we ever resize the bg image + # for idx, kpos in XYButtons.label_overlay_positions.items(): + # dc.DrawCircle(kpos[0], kpos[1], kpos[2]) + + ## ------ ## + ## Events ## + ## ------ ## + + def OnTopLevelKey(self, evt): + # Let user press escape on any control, and return focus here + if evt.GetKeyCode() == wx.WXK_ESCAPE: + self.SetFocus() + evt.Skip() + + def OnKey(self, evt): + if not self.enabled: + return + if self.keypad_idx >= 0: + if evt.GetKeyCode() == wx.WXK_TAB: + self.setKeypadIndex(self.cycleKeypadIndex()) + elif evt.GetKeyCode() == wx.WXK_UP: + self.quadrant = 1 + elif evt.GetKeyCode() == wx.WXK_DOWN: + self.quadrant = 3 + elif evt.GetKeyCode() == wx.WXK_LEFT: + self.quadrant = 2 + elif evt.GetKeyCode() == wx.WXK_RIGHT: + self.quadrant = 0 + else: + evt.Skip() + return + + if self.moveCallback: + self.concentric = self.keypad_idx + x, y = self.getMovement() + self.moveCallback(x, y) + + def OnMotion(self, event): + if not self.enabled: + return + + oldcorner = self.corner + oldq, oldc = self.quadrant, self.concentric + + mpos = event.GetPosition() + idx = self.mouseOverKeypad(mpos) + self.quadrant = None + self.concentric = None + if idx == None: + center = wx.Point(XYButtons.center[0], XYButtons.center[1]) + riseDist = self.distanceToLine(mpos, center.x-1, center.y-1, center.x+1, center.y+1) + fallDist = self.distanceToLine(mpos, center.x-1, center.y+1, center.x+1, center.y-1) + self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) + + # If mouse hovers in space between quadrants, don't commit to a quadrant + if riseDist <= XYButtons.spacer or fallDist <= XYButtons.spacer: + self.quadrant = None + + cx, cy = XYButtons.center + if mpos.x < cx and mpos.y < cy: + self.corner = 0 + if mpos.x >= cx and mpos.y < cy: + self.corner = 1 + if mpos.x >= cx and mpos.y >= cy: + self.corner = 2 + if mpos.x < cx and mpos.y >= cy: + self.corner = 3 + + if oldq != self.quadrant or oldc != self.concentric or oldcorner != self.corner: + self.update() + + def OnLeftDown(self, event): + if not self.enabled: + return + + # Take focus when clicked so that arrow keys can control movement + self.SetFocus() + + mpos = event.GetPosition() + + idx = self.mouseOverKeypad(mpos) + if idx == None: + self.quadrant, self.concentric = self.getQuadrantConcentricFromPosition(mpos) + if self.concentric != None: + if self.concentric < len(XYButtons.concentric_circle_radii): + if self.quadrant != None: + x, y = self.getMovement() + if self.moveCallback: + self.moveCallback(x, y) + elif self.corner != None: + if self.cornerCallback: + self.cornerCallback(self.corner) + else: + if self.keypad_idx == idx: + self.setKeypadIndex(-1) + else: + self.setKeypadIndex(idx) + + def OnLeaveWindow(self, evt): + self.quadrant = None + self.concentric = None + self.update() \ No newline at end of file diff --git a/zbuttons.py b/zbuttons.py new file mode 100644 index 0000000..d268a89 --- /dev/null +++ b/zbuttons.py @@ -0,0 +1,131 @@ +import wx, os, math +from bufferedcanvas import * + +def imagefile(filename): + return os.path.join(os.path.dirname(__file__), "images", filename) + +def sign(n): + if n < 0: return -1 + elif n > 0: return 1 + else: return 0 + +class ZButtons(BufferedCanvas): + button_ydistances = [7, 30, 55, 83, 112] + center = (30, 118) + label_overlay_positions = { + 0: (1, 18, 11), + 1: (1, 41, 13), + 2: (1, 67, 15), + 3: None + } + + def __init__(self, parent, moveCallback=None, ID=-1): + self.bg_bmp = wx.Image(imagefile("control_z.png"),wx.BITMAP_TYPE_PNG).ConvertToBitmap() + self.range = None + self.direction = None + self.orderOfMagnitudeIdx = 0 # 0 means '1', 1 means '10', 2 means '100', etc. + self.moveCallback = moveCallback + self.enabled = False + + BufferedCanvas.__init__(self, parent, ID) + + self.SetSize(wx.Size(59, 244)) + + # Set up mouse and keyboard event capture + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) + + def disable(self): + self.enabled = False + self.update() + + def enable(self): + self.enabled = True + self.update() + + def lookupRange(self, ydist): + idx = -1 + for d in ZButtons.button_ydistances: + if ydist < d: + return idx + idx += 1 + return None + + def highlight(self, gc, rng, dir): + assert(rng >= -1 and rng <= 3) + assert(dir >= -1 and dir <= 1) + + fudge = 11 + x = 0 + fudge + w = 59 - fudge*2 + if rng >= 0: + k = 1 if dir > 0 else 0 + y = ZButtons.center[1] - (dir * ZButtons.button_ydistances[rng+k]) + h = ZButtons.button_ydistances[rng+1] - ZButtons.button_ydistances[rng] + gc.DrawRoundedRectangle(x, y, w, h, 4) + # gc.DrawRectangle(x, y, w, h) + # self.drawPartialPie(dc, center, r1-inner_ring_radius, r2-inner_ring_radius, a1+fudge, a2-fudge) + + def getRangeDir(self, pos): + ydelta = ZButtons.center[1] - pos[1] + return (self.lookupRange(abs(ydelta)), sign(ydelta)) + + def draw(self, dc, w, h): + dc.Clear() + gc = wx.GraphicsContext.Create(dc) + w, h = (self.bg_bmp.GetWidth(), self.bg_bmp.GetHeight()) + + gc.DrawBitmap(self.bg_bmp, 0, 0, w, h) + + if self.enabled: + # Draw label overlays + gc.SetPen(wx.Pen(wx.Colour(255,255,255,128), 1)) + gc.SetBrush(wx.Brush(wx.Colour(255,255,255,128+64))) + for idx, kpos in ZButtons.label_overlay_positions.items(): + if kpos and idx != self.range: + r = kpos[2] + gc.DrawEllipse(ZButtons.center[0]-kpos[0]-r, ZButtons.center[1]-kpos[1]-r, r*2, r*2) + + # Top 'layer' is the mouse-over highlights + gc.SetPen(wx.Pen(wx.Colour(100,100,100,172), 4)) + gc.SetBrush(wx.Brush(wx.Colour(0,0,0,128))) + if self.range != None and self.direction != None: + self.highlight(gc, self.range, self.direction) + else: + gc.SetPen(wx.Pen(wx.Colour(255,255,255,0), 4)) + gc.SetBrush(wx.Brush(wx.Colour(255,255,255,128))) + gc.DrawRectangle(0, 0, w, h) + + ## ------ ## + ## Events ## + ## ------ ## + + def OnMotion(self, event): + if not self.enabled: + return + + oldr, oldd = self.range, self.direction + + mpos = event.GetPosition() + self.range, self.direction = self.getRangeDir(mpos) + + if oldr != self.range or oldd != self.direction: + self.update() + + def OnLeftDown(self, event): + if not self.enabled: + return + + mpos = event.GetPosition() + r, d = self.getRangeDir(mpos) + if r >= 0: + value = math.pow(10, self.orderOfMagnitudeIdx) * math.pow(10, r - 1) * d + if self.moveCallback: + self.moveCallback(value) + + def OnLeaveWindow(self, evt): + self.range = None + self.direction = None + self.update()