diff --git a/pymlac/Display.py b/pymlac/Display.py index 4e64819..40b58fb 100644 --- a/pymlac/Display.py +++ b/pymlac/Display.py @@ -1,48 +1,334 @@ -#!/usr/bin/python +#!/usr/bin/python +# -*- coding: utf-8 -*- """ The Imlac display. """ -import pygame -from pygame.locals import * +import wx from Globals import * +# if we don't have log.py, don't crash +try: + import log + log = log.Log('test.log', log.Log.DEBUG) +except ImportError: + def log(*args, **kwargs): + pass -class Display(object): +# the widget version +__version__ = '0.0' + +# type of events +DisplayStop = 1 + + +###### +# Base class for the widget canvas - buffered and flicker-free. +###### + +class _BufferedCanvas(wx.Panel): + """Implements a buffered, flicker-free canvas widget. + + This class is based on: + http://wiki.wxpython.org/index.cgi/BufferedCanvas + """ + + # The backing buffer + buffer = None + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.NO_FULL_REPAINT_ON_RESIZE): + """Initialise the canvas. + + parent reference to 'parent' widget + id the unique widget ID (NB: shadows builtin 'id') + pos canvas position + size canvas size + style wxPython style + """ + + wx.Panel.__init__(self, parent, id, pos, size, style) + + # Bind events + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_SIZE, self.OnSize) + + # Disable background erasing (flicker-licious) + def disable_event(*args, **kwargs): + pass # the sauce, please + self.Bind(wx.EVT_ERASE_BACKGROUND, disable_event) + + # set callback upon onSize event + self.onSizeCallback = None + + self.dc = None + + def Draw(self, dc): + """Stub: called when the canvas needs to be re-drawn.""" + + # set display scale + scaleX = self.view_width*1.0 / self.ScaleMaxX + scaleY = self.view_height*1.0 / self.ScaleMaxY + dc.SetUserScale(min(scaleX,scaleY), min(scaleX,scaleY)) + + # don't REPLACE this method, EXTEND it! + + def Update(self): + """Causes the canvas to be updated.""" + + dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) + dc.BeginDrawing() + dc.Clear() # because maybe view size > map size + self.Draw(dc) + dc.EndDrawing() + self.dc = dc + + def OnPaint(self, event): + """Paint the canvas to the screen.""" + + # Blit the front buffer to the screen + wx.BufferedPaintDC(self, self.buffer) + + def OnSize(self, event=None): + """Create a new off-screen buffer to hold drawn data.""" + + (width, height) = self.GetClientSizeTuple() + + minsize = min(width, height) + self.SetSize((minsize, minsize)) + + self.view_width = minsize + self.view_height = minsize + + # new off-screen buffer + self.buffer = wx.EmptyBitmap(minsize, minsize) + + # call onSize callback, if registered + if self.onSizeCallback: + self.onSizeCallback() + + # Now update the screen + self.Update() + + +class Display(_BufferedCanvas): + + BackgroundColour = 'black' + DrawColour = '#ffff00' + SYNC_HZ = 40 SYNC_40HZ_CYCLE_COUNT = int(CYCLES_PER_SECOND / SYNC_HZ) - def __init__(self, screen): - self.screen = screen + # max coorcinates of pymlac display + ScaleMaxX = 1024 + ScaleMaxY = 1024 + + + + def __init__(self, parent, **kwargs): + # create and initialise the base panel + _BufferedCanvas.__init__(self, parent=parent, **kwargs) + self.SetBackgroundColour(self.BackgroundColour) + + # set internal state self.running = 0 self.cycle_count = 0 - self.display = pygame.Surface((1024, 1024)) self.Sync40hz = 1 - self.display.fill((0, 0, 0)) + self.drawlist = [] - def draw(self, dotted, oldx, oldy, x, y): - oldx %= 1024 - oldy %= 1024 - x %= 1024 - y %= 1024 - pygame.draw.line(self.display, YELLOW, (oldx, 1024 - oldy), (x, 1024 - y)) + self.OnSize() - def flip(self): - self.screen.blit(self.display, (0, 0)) - pygame.display.flip() - self.display.fill((0, 0, 0)) + def draw(self, x1, y1, x2, y2): + """Draw a line on the screen. + + x1, y1 start coordinates + x2, y2 end coordinates + """ + + self.drawlist.append((x1, y1, x2, y2)) + self.Update() + + def Draw(self, dc): + """Update the display on the widget screen.""" + + # there's some code in super.Draw() we need + super(Display, self).Draw(dc) + + if self.drawlist: + dc.SetPen(wx.Pen(self.DrawColour)) + dc.DrawLineList(self.drawlist) def syncclear(self): self.Sync40hz = 0 self.cycle_count = self.SYNC_40HZ_CYCLE_COUNT + @property def ready(self): return self.Sync40hz def tick(self, cycles): + """Advance the internal state by given cycles.""" + self.cycle_count -= cycles if self.cycle_count <= 0: self.Sync40hz = 1 + self.cycle_count = 0 + +############################################################################### +# Define the events that are raised by the display widget. +############################################################################### + +# display stop +_myEVT_DISPLAY_STOP = wx.NewEventType() +EVT_DISPLAY_STOP = wx.PyEventBinder(_myEVT_DISPLAY_STOP, 1) + +class _DisplayEvent(wx.PyCommandEvent): + """Event sent from the display widget.""" + + def __init__(self, eventType, id): + """Construct a display event. + + eventType type of event + id unique event number + + Event will be adorned with attributes by raising code. + """ + + wx.PyCommandEvent.__init__(self, eventType, id) + +############################################################################### +# The display widget proper +############################################################################### + +class PymlacDisplay(_BufferedCanvas): + """A widget to display pymlac draw lists.""" + + # line colour + DrawColour = '#ffff00' + + # panel background colour + BackgroundColour = '#000000' + + # max coorcinates of pymlac display + ScaleMaxX = 1024 + ScaleMaxY = 1024 + + def __init__(self, parent, **kwargs): + """Initialise a display instance. + + parent reference to parent object + **kwargs keyword args for Panel + """ + + # create and initialise the base panel + _BufferedCanvas.__init__(self, parent=parent, **kwargs) + self.SetBackgroundColour(PymlacDisplay.BackgroundColour) + + # set some internal state + self.default_cursor = wx.CURSOR_DEFAULT + self.drawlist = None # list of (x1,y1,x2,y2) + + # set callback when parent resizes +# self.onSizeCallback = self.ResizeCallback + + # force a resize, which sets up the rest of the state + # eventually calls ResizeCallback() + self.OnSize() + + ###### + # GUI stuff + ###### + + def OnKeyDown(self, event): + pass + + def OnKeyUp(self, event): + pass + + def OnLeftUp(self, event): + """Left mouse button up. + """ + + pass + + def OnMiddleUp(self, event): + """Middle mouse button up. Do nothing in this version.""" + + pass + + def OnRightUp(self, event): + """Right mouse button up. + + Note that when we iterate through the layer_z_order list we must + iterate on a *copy* as the user select process can modify + self.layer_z_order. + + THIS CODE HASN'T BEEN LOOKED AT IN A LONG, LONG TIME. + """ + + pass + + def Draw(self, dc): + """Do actual widget drawing. + Overrides the _BufferedCanvas.draw() method. + + dc device context to draw on + + Draws the current drawlist to the screen. + """ + + scaleX = self.view_width*1.0 / self.ScaleMaxX + scaleY = self.view_height*1.0 / self.ScaleMaxY + dc.SetUserScale(min(scaleX,scaleY), min(scaleX,scaleY)) + + if self.drawlist: + dc.SetPen(wx.Pen(self.DrawColour)) + dc.DrawLineList(self.drawlist) + + ###### + # Change the drawlist + ###### + + def Drawlist(self, dl): + self.drawlist = dl + self.Update() + + ###### + # Routines for display events + ###### + + def RaiseEventStop(self): + """Raise a display stop event.""" + + event = _DisplayEvent(_myEVT_DISPLAY_STOP, self.GetId()) + self.GetEventHandler().ProcessEvent(event) + + def info(self, msg): + """Display an information message, log and graphically.""" + + log_msg = '# ' + msg + length = len(log_msg) + prefix = '#### Information ' + banner = prefix + '#'*(80 - len(log_msg) - len(prefix)) + log(banner) + log(log_msg) + log(banner) + + wx.MessageBox(msg, 'Warning', wx.OK | wx.ICON_INFORMATION) + + def warn(self, msg): + """Display a warning message, log and graphically.""" + + log_msg = '# ' + msg + length = len(log_msg) + prefix = '#### Warning ' + banner = prefix + '#'*(80 - len(log_msg) - len(prefix)) + log(banner) + log(log_msg) + log(banner) + + wx.MessageBox(msg, 'Warning', wx.OK | wx.ICON_ERROR) + diff --git a/pymlac/test_Display.py b/pymlac/test_Display.py new file mode 100755 index 0000000..bc0aff7 --- /dev/null +++ b/pymlac/test_Display.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test the pymlac display widget. + +Usage: test_pymlac_display.py [-h] +""" + + +import wx +import Display + +# if we don't have log.py, don't crash +try: + import log + log = log.Log('test.log', log.Log.DEBUG) +except ImportError: + def log(*args, **kwargs): + pass + + +###### +# Various demo constants +###### + +WindowTitleHeight = 22 +DefaultAppSize = (600, 600+WindowTitleHeight) + +################################################################################ +# The main application frame +################################################################################ + +class TestFrame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, size=DefaultAppSize, + title=('Test pymlac display - %s' + % Display.__version__)) + self.SetMinSize(DefaultAppSize) + self.panel = wx.Panel(self, wx.ID_ANY) + self.panel.ClearBackground() + + # build the GUI + box = wx.BoxSizer(wx.VERTICAL) + self.display = Display.Display(self.panel) + box.Add(self.display, proportion=1, border=1, flag=wx.EXPAND) + self.panel.SetSizer(box) + self.panel.Layout() + self.Centre() + self.Show(True) + + self.Bind(wx.EVT_SIZE, self.OnSize) + + self.lock = False + + self.Refresh() + + self.display.draw(0, 0, 1023, 1023) + self.display.draw(1023, 0, 0, 1023) + + def OnSize(self, event): + """Maintain square window.""" + + if not self.lock: + self.lock = True + (w, h) = event.GetSize() + size = min(w, h) + self.SetSize((size-WindowTitleHeight, size)) + self.lock = False + + event.Skip() + +################################################################################ + +if __name__ == '__main__': + import sys + import getopt + import traceback + + # print some usage information + def usage(msg=None): + if msg: + print(msg+'\n') + print(__doc__) # module docstring used + + # our own handler for uncaught exceptions + def excepthook(type, value, tb): + msg = '\n' + '=' * 80 + msg += '\nUncaught exception:\n' + msg += ''.join(traceback.format_exception(type, value, tb)) + msg += '=' * 80 + '\n' + print msg + sys.exit(1) + + # plug our handler into the python system + sys.excepthook = excepthook + + # decide which tiles to use, default is GMT + argv = sys.argv[1:] + + try: + (opts, args) = getopt.getopt(argv, 'h', ['help']) + except getopt.error: + usage() + sys.exit(1) + + for (opt, param) in opts: + if opt in ['-h', '--help']: + usage() + sys.exit(0) + + # start wxPython app + app = wx.App() + TestFrame().Show() + app.MainLoop() + sys.exit(0) +