]> git.saurik.com Git - wxWidgets.git/blob - wxPython/samples/roses/wxroses.py
fixed wxVsnprintf() to write as much as it can if the output buffer is too short
[wxWidgets.git] / wxPython / samples / roses / wxroses.py
1 #----------------------------------------------------------------------------
2 # Name: wxroses.py
3 # Purpose: wxPython GUI using clroses.py to display a classic graphics
4 # hack.
5 #
6 # Author: Ric Werme, Robin Dunn.
7 # WWW: http://WermeNH.com/roses
8 #
9 # Created: June 2007
10 # CVS-ID: $Id$
11 # Copyright: Public Domain, please give credit where credit is due.
12 # License: Sorry, no EULA.
13 #----------------------------------------------------------------------------
14
15 # This module is responsible for everything involving GUI usage
16 # as clroses knows nothing about wxpython, tkintr, etc.
17
18 # There are some notes about how the Roses algorithm works in clroses.py,
19 # but the best reference should be at http://WermeNH.com/roses/index.html .
20
21 # There are a number of enhancements that could be done to wxRoses, and
22 # contributions are welcome as long as you don't destory the general
23 # structure, flavor, and all that. The following list is in the order
24 # I'd like to see done. Some are easy, some aren't, some are easy if
25 # you have experience in the right parts of external code.
26
27 # Brighter crossing points.
28 # Where many vectors cross, the display becomes washed out as a solid shape
29 # of light. On (antique) refresh vector graphics systems, crossing points
30 # are brighter because the electron beam paints the pixels multiple times.
31 # This gives complex patterns a lacy feel to some, and a 3-D sense to
32 # fluted shapes where vectors lie tangent to some curve. It would be
33 # nice to do the same in a bitmap buffer, the code to draw a vector is
34 # pretty simple, adding something that adds brightness to it via math or
35 # a lookup table ought to be a simple addition.
36
37 # Monochrome is so 20th century.
38 # There are a number of things that could be done with color. The simplest
39 # is to step through colors in a color list, better things to do would be
40 # for clroses.py to determine the length of an interesting "generator pattern,"
41 # e.g. the square in the opening display. Then it could change colors either
42 # every four vectors or cycle through the first four colors in the list.
43
44 # Bookmark that Rose!
45 # As you play with wxRoses, you'll come across some patterns that are
46 # "keepers." A bookmark mechanism would be handy.
47
48 # Save that Rose!
49 # It would be nice to have a Menu-bar/File/Save-as dialog to save a pattern
50 # as a jpg/png/gif file.
51
52 # Themes
53 # A pulldown option to select various themes is worthwhile. E.g.:
54 # Start an interesting animation,
55 # Select complex, lacy Roses,
56 # Select the author's favorites,
57 # Return to the initial Rose.
58 # Actually, all that's necessary are some pre-loaded bookmarks.
59
60 # Help text
61 # Standard fare, or:
62
63 # Slide show
64 # At CMU I created an interactive slide show that walked people through
65 # all the options and made suggestions about how to choose Style and Petal.
66 # I forget exactly what I said and may not have listings for it. At any rate,
67 # making the help mechanism start one of several "lessons" where it could
68 # control the display (without blocking the user's control) would be pretty
69 # straightforward.
70
71 import wx
72 import clroses
73 import wx.lib.colourselect as cs
74
75
76 # Class SpinPanel creates a control that includes both a StaticText widget
77 # which holds the the name of a parameter and a SpinCtrl widget which
78 # displays the current value. Values are set at initialization and can
79 # change via the SpinCtrl widget or by the program. So that the program
80 # can easily access the SpinCtrl, the SpinPanel handles are saved in the
81 # spin_panels dictionary.
82 class SpinPanel(wx.Panel):
83 def __init__(self, parent, name, min_value, value, max_value, callback):
84 wx.Panel.__init__(self, parent, -1)
85 if "wxMac" in wx.PlatformInfo:
86 self.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
87 self.st = wx.StaticText(self, -1, name)
88 self.sc = wx.SpinCtrl(self, -1, "", size = (70, -1))
89 self.sc.SetRange(min_value, max_value)
90 self.sc.SetValue(value)
91 self.sc.Bind(wx.EVT_SPINCTRL, self.OnSpin)
92 self.callback = callback
93
94 sizer = wx.BoxSizer(wx.HORIZONTAL)
95 sizer.Add(self.st, 0, wx.ALIGN_CENTER_VERTICAL)
96 sizer.Add((1,1), 1)
97 sizer.Add(self.sc)
98 self.SetSizer(sizer)
99
100 global spin_panels
101 spin_panels[name] = self
102
103 # Called (generally through spin_panels{}) to set the SpinCtrl value.
104 def SetValue(self, value):
105 self.sc.SetValue(value)
106
107 # Called when user changes the SpinCtrl value.
108 def OnSpin(self, event):
109 name = self.st.GetLabel()
110 value = self.sc.GetValue()
111 if verbose:
112 print 'OnSpin', name, '=', value
113 self.callback(name, value) # Call MyFrame.OnSpinback to call clroses
114
115
116 # This class is used to display the current rose diagram. It keeps a
117 # buffer bitmap of the current display, which it uses to refresh the
118 # screen with when needed. When it is told to draw some lines it does
119 # so to the buffer in order for it to always be up to date.
120 class RosePanel(wx.Panel):
121 def __init__(self, *args, **kw):
122 wx.Panel.__init__(self, *args, **kw)
123 self.InitBuffer()
124 self.resizeNeeded = False
125 self.useGCDC = False
126 self.useBuffer = True
127
128 # set default colors
129 self.SetBackgroundColour((51, 51, 51)) # gray20
130 self.SetForegroundColour((164, 211, 238)) # lightskyblue2
131
132 # connect the size and paint events to handlers
133 self.Bind(wx.EVT_SIZE, self.OnSize)
134 self.Bind(wx.EVT_PAINT, self.OnPaint)
135 self.Bind(wx.EVT_IDLE, self.OnIdle)
136
137
138 def InitBuffer(self):
139 size = self.GetClientSize()
140 self.buffer = wx.EmptyBitmap(max(1, size.width),
141 max(1, size.height))
142
143 def Clear(self):
144 dc = self.useBuffer and wx.MemoryDC(self.buffer) or wx.ClientDC(self)
145 dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
146 dc.Clear()
147 if self.useBuffer:
148 self.Refresh(False)
149
150 def DrawLines(self, lines):
151 if len(lines) <= 1:
152 return
153 dc = self.useBuffer and wx.MemoryDC(self.buffer) or wx.ClientDC(self)
154 if self.useGCDC:
155 dc = wx.GCDC(dc)
156 dc.SetPen(wx.Pen(self.GetForegroundColour(), 1))
157 dc.DrawLines(lines)
158 if self.useBuffer:
159 self.Refresh(False)
160
161 def TriggerResize(self):
162 self.GetParent().TriggerResize(self.buffer.GetSize())
163
164 def TriggerRedraw(self):
165 self.GetParent().TriggerRedraw()
166
167 def OnSize(self, evt):
168 self.resizeNeeded = True
169
170 def OnIdle(self, evt):
171 if self.resizeNeeded:
172 self.InitBuffer()
173 self.TriggerResize()
174 if self.useBuffer:
175 self.Refresh()
176 self.resizeNeeded = False
177
178 def OnPaint(self, evt):
179 dc = wx.PaintDC(self)
180 if self.useBuffer:
181 dc.DrawBitmap(self.buffer, 0,0)
182 else:
183 self.TriggerRedraw()
184
185
186 # A panel used to collect options on how the rose is drawn
187 class OptionsPanel(wx.Panel):
188 def __init__(self, parent, rose):
189 wx.Panel.__init__(self, parent)
190 self.rose = rose
191 sizer = wx.StaticBoxSizer(wx.StaticBox(self, label='Options'),
192 wx.VERTICAL)
193 self.useGCDC = wx.CheckBox(self, label="Use GCDC")
194 sizer.Add(self.useGCDC, 0, wx.BOTTOM|wx.LEFT, 2)
195 self.useBuffer = wx.CheckBox(self, label="Use buffering")
196 sizer.Add(self.useBuffer, 0, wx.BOTTOM|wx.LEFT, 2)
197
198 def makeCButton(label):
199 btn = cs.ColourSelect(self, size=(20,22))
200 lbl = wx.StaticText(self, -1, label)
201 sizer = wx.BoxSizer(wx.HORIZONTAL)
202 sizer.Add(btn)
203 sizer.Add((4,4))
204 sizer.Add(lbl, 0, wx.ALIGN_CENTER_VERTICAL)
205 return sizer, btn
206
207 s, self.fg = makeCButton('Foreground')
208 sizer.Add(s)
209 s, self.bg = makeCButton('Background')
210 sizer.Add(s)
211 self.SetSizer(sizer)
212
213 self.Bind(wx.EVT_CHECKBOX, self.OnUseGCDC, self.useGCDC)
214 self.Bind(wx.EVT_CHECKBOX, self.OnUseBuffer, self.useBuffer)
215 self.Bind(wx.EVT_IDLE, self.OnIdle)
216 self.Bind(cs.EVT_COLOURSELECT, self.OnSetFG, self.fg)
217 self.Bind(cs.EVT_COLOURSELECT, self.OnSetBG, self.bg)
218
219 def OnIdle(self, evt):
220 if self.useGCDC.GetValue() != self.rose.useGCDC:
221 self.useGCDC.SetValue(self.rose.useGCDC)
222 if self.useBuffer.GetValue() != self.rose.useBuffer:
223 self.useBuffer.SetValue(self.rose.useBuffer)
224 if self.fg.GetValue() != self.rose.GetForegroundColour():
225 self.fg.SetValue(self.rose.GetForegroundColour())
226 if self.bg.GetValue() != self.rose.GetBackgroundColour():
227 self.bg.SetValue(self.rose.GetBackgroundColour())
228
229 def OnUseGCDC(self, evt):
230 self.rose.useGCDC = evt.IsChecked()
231 self.rose.TriggerRedraw()
232
233 def OnUseBuffer(self, evt):
234 self.rose.useBuffer = evt.IsChecked()
235 self.rose.TriggerRedraw()
236
237 def OnSetFG(self, evt):
238 self.rose.SetForegroundColour(evt.GetValue())
239 self.rose.TriggerRedraw()
240
241 def OnSetBG(self, evt):
242 self.rose.SetBackgroundColour(evt.GetValue())
243 self.rose.TriggerRedraw()
244
245
246 # MyFrame is the traditional class name to create and populate the
247 # application's frame. The general GUI has control/status panels on
248 # the right side and a panel on the left side that draws the rose
249 #
250 # This class also derives from clroses.rose so it can implement the
251 # required interfaces to connect the GUI to the rose engine.
252 class MyFrame(wx.Frame, clroses.rose):
253 # Color matching dictionary, convert label name to color:
254 # Stop and Go ala traffic lights,
255 # Skip and Forward look ahead to the purple mountain majesties (really bluish),
256 # Reverse and Backward look morosely behind to maroon memories,
257 # Redraw looks at the brown earth right below your feet.
258 # Yeah, so it's lame. All I really wanted was to color Stop and Go.
259 labelColours = {
260 'Go': 'dark green', 'Stop': 'red',
261 'Redraw': 'brown', 'Skip': 'dark slate blue',
262 'Backward': 'maroon', 'Forward': 'dark slate blue', 'Reverse': 'maroon'
263 }
264
265 def __init__(self):
266 def makeSP(name, labels, statictexts = None):
267 panel = wx.Panel(self.side_panel, -1)
268 box = wx.StaticBox(panel, -1, name)
269 sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
270 for name, min_value, value, max_value in labels:
271 sp = SpinPanel(panel, name, min_value, value, max_value, self.OnSpinback)
272 sizer.Add(sp, 0, wx.EXPAND)
273 if statictexts:
274 for name, text in statictexts:
275 st = wx.StaticText(panel, -1, text)
276 spin_panels[name] = st # Supposed to be a SpinPanel....
277 sizer.Add(st, 0, wx.EXPAND)
278 panel.SetSizer(sizer)
279 return panel
280
281 wx.Frame.__init__(self, None, title="Roses in wxPython")
282
283 self.rose_panel = RosePanel(self)
284 self.side_panel = wx.Panel(self)
285
286 # The cmd panel is four buttons whose names and foreground colors
287 # change. Plop them in a StaticBox like the SpinPanels. Use
288 # a 2x2 grid, but StaticBoxSizer can't handle that. Therefore,
289 # create a sub panel, layout the buttons there, then give that to
290 # a higher panel that has the static box stuff.
291 self.cmd_panel = wx.Panel(self.side_panel, -1)
292 self.sub_panel = wx.Panel(self.cmd_panel, -1)
293 sizer = wx.GridSizer(rows = 2, cols = 2)
294 global ctrl_buttons
295 border = 'wxMac' in wx.PlatformInfo and 3 or 1
296 for name, handler in (
297 ('Go', self.OnGoStop),
298 ('Redraw', self.OnRedraw),
299 ('Backward', self.OnBackward),
300 ('Forward', self.OnForward)):
301 button = wx.Button(self.sub_panel, -1, name)
302 button.SetForegroundColour(self.labelColours[name])
303 ctrl_buttons[name] = button
304 button.Bind(wx.EVT_BUTTON, handler)
305 sizer.Add(button, 0, wx.EXPAND|wx.ALL, border)
306 self.sub_panel.SetSizer(sizer)
307
308 # Set up cmd_panel with StaticBox stuff
309 box = wx.StaticBox(self.cmd_panel, -1, 'Command')
310 sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
311 sizer.Add(self.sub_panel)
312 self.cmd_panel.SetSizer(sizer)
313
314 # Now make the rest of the control panels...
315 # The order of creation of SpinCtrls and Buttons is the order that
316 # the tab key will step through, so the order of panel creation is
317 # important.
318 # In the SpinPanel data (name, min, value, max), value will be
319 # overridden by clroses.py defaults.
320 self.coe_panel = makeSP('Coefficient',
321 (('Style', 0, 100, 3600),
322 ('Sincr', -3600, -1, 3600),
323 ('Petal', 0, 2, 3600),
324 ('Pincr', -3600, 1, 3600)))
325
326 self.vec_panel = makeSP('Vector',
327 (('Vectors' , 1, 399, 3600),
328 ('Minimum' , 1, 1, 3600),
329 ('Maximum' , 1, 3600, 3600),
330 ('Skip first', 0, 0, 3600),
331 ('Draw only' , 1, 3600, 3600)),
332 (('Takes', 'Takes 0000 vectors'), ))
333
334 self.tim_panel = makeSP('Timing',
335 (('Vec/tick' , 1, 20, 3600),
336 ('msec/tick', 1, 50, 1000),
337 ('Delay' , 1, 2000, 9999)))
338
339 self.opt_panel = OptionsPanel(self.side_panel, self.rose_panel)
340
341 # put them all on in a sizer attached to the side_panel
342 panelSizer = wx.BoxSizer(wx.VERTICAL)
343 panelSizer.Add(self.cmd_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5)
344 panelSizer.Add(self.coe_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5)
345 panelSizer.Add(self.vec_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5)
346 panelSizer.Add(self.tim_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5)
347 panelSizer.Add(self.opt_panel, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5)
348 self.side_panel.SetSizer(panelSizer)
349
350 # and now arrange the two main panels in another sizer for the frame
351 mainSizer = wx.BoxSizer(wx.HORIZONTAL)
352 mainSizer.Add(self.rose_panel, 1, wx.EXPAND)
353 mainSizer.Add(self.side_panel, 0, wx.EXPAND)
354 self.SetSizer(mainSizer)
355
356 # bind event handlers
357 self.timer = wx.Timer(self)
358 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
359
360 # Determine appropriate image size.
361 # At this point, the rose_panel and side_panel will both report
362 # size (20, 20). After mainSizer.Fit(self) they will report the
363 # same, but the Frame size, self.GetSize(), will report the desired
364 # side panel dimensions plus an extra 20 on the width. That lets
365 # us determine the frame size that will display the side panel and
366 # a square space for the diagram. Only after Show() will the two
367 # panels report the accurate sizes.
368 mainSizer.Fit(self)
369 rw, rh = self.rose_panel.GetSize()
370 sw, sh = self.side_panel.GetSize()
371 fw, fh = self.GetSize()
372 h = max(600, fh) # Change 600 to desired minimum size
373 w = h + fw - rw
374 if verbose:
375 print 'rose panel size', (rw, rh)
376 print 'side panel size', (sw, sh)
377 print ' frame size', (fw, fh)
378 print 'Want size', (w,h)
379 self.SetSize((w, h))
380 self.SupplyControlValues() # Ask clroses to tell us all the defaults
381 self.Show()
382
383 # Command button event handlers. These are relabled when changing between auto
384 # and manual modes. They simply reflect the call to a method in the base class.
385 #
386 # Go/Stop button
387 def OnGoStop(self, event):
388 if verbose:
389 print 'OnGoStop'
390 self.cmd_go_stop()
391
392 # Redraw/Redraw
393 def OnRedraw(self, event):
394 if verbose:
395 print 'OnRedraw'
396 self.cmd_redraw()
397
398 # Backward/Reverse
399 def OnBackward(self, event):
400 if verbose:
401 print 'OnBackward'
402 self.cmd_backward()
403
404 # Forward/Skip
405 def OnForward(self, event):
406 if verbose:
407 print 'OnForward'
408 self.cmd_step()
409
410
411 # The clroses.roses class expects to have methods available that
412 # implement the missing parts of the functionality needed to do
413 # the actual work of getting the diagram to the screen and etc.
414 # Those are implemented here as the App* methods.
415
416 def AppClear(self):
417 if verbose:
418 print 'AppClear: clear screen'
419 self.rose_panel.Clear()
420
421 def AppCreateLine(self, line):
422 # print 'AppCreateLine, len', len(line), 'next', self.nextpt
423 self.rose_panel.DrawLines(line)
424
425 # Here when clroses has set a new style and/or petal value, update
426 # strings on display.
427 def AppSetParam(self, style, petals, vectors):
428 spin_panels['Style'].SetValue(style)
429 spin_panels['Petal'].SetValue(petals)
430 spin_panels['Vectors'].SetValue(vectors)
431
432 def AppSetIncrs(self, sincr, pincr):
433 spin_panels['Sincr'].SetValue(sincr)
434 spin_panels['Pincr'].SetValue(pincr)
435
436 def AppSetVectors(self, vectors, minvec, maxvec, skipvec, drawvec):
437 spin_panels['Vectors'].SetValue(vectors)
438 spin_panels['Minimum'].SetValue(minvec)
439 spin_panels['Maximum'].SetValue(maxvec)
440 spin_panels['Skip first'].SetValue(skipvec)
441 spin_panels['Draw only'].SetValue(drawvec)
442
443 def AppSetTakesVec(self, takes):
444 spin_panels['Takes'].SetLabel('Takes %d vectors' % takes)
445
446 # clroses doesn't change this data, so it's not telling us something
447 # we don't already know.
448 def AppSetTiming(self, vecPtick, msecPtick, delay):
449 spin_panels['Vec/tick'].SetValue(vecPtick)
450 spin_panels['msec/tick'].SetValue(msecPtick)
451 spin_panels['Delay'].SetValue(delay)
452
453 # Command buttons change their names based on the whether we're in auto
454 # or manual mode.
455 def AppCmdLabels(self, labels):
456 for name, label in map(None, ('Go', 'Redraw', 'Backward', 'Forward'), labels):
457 ctrl_buttons[name].SetLabel(label)
458 ctrl_buttons[name].SetForegroundColour(self.labelColours[label])
459
460
461 # Timer methods. The paranoia about checking up on the callers is
462 # primarily because it's easier to check here. We expect that calls to
463 # AppAfter and OnTimer alternate, but don't verify that AppCancelTimer()
464 # is canceling anything as callers of that may be uncertain about what's
465 # happening.
466
467 # Method to provide a single callback after some amount of time.
468 def AppAfter(self, msec, callback):
469 if self.timer_callback:
470 print 'AppAfter: timer_callback already set!',
471 # print 'AppAfter:', callback
472 self.timer_callback = callback
473 self.timer.Start(msec, True)
474
475 # Method to cancel something we might be waiting for but have lost
476 # interest in.
477 def AppCancelTimer(self):
478 self.timer.Stop()
479 # print 'AppCancelTimer'
480 self.timer_callback = None
481
482 # When the timer happens, we come here and jump off to clroses internal code.
483 def OnTimer(self, evt):
484 callback = self.timer_callback
485 self.timer_callback = None
486 # print 'OnTimer,', callback
487 if callback:
488 callback() # Often calls AppAfter() and sets the callback
489 else:
490 print 'OnTimer: no callback!'
491
492
493 resize_delay = 300
494
495 def TriggerResize(self, size):
496 self.resize(size, self.resize_delay)
497 self.resize_delay = 100
498
499 def TriggerRedraw(self):
500 self.repaint(10)
501
502 # Called when data in spin boxes changes.
503 def OnSpinback(self, name, value):
504 if verbose:
505 print 'OnSpinback', name, value
506 if name == 'Style':
507 self.SetStyle(value)
508 elif name == 'Sincr':
509 self.SetSincr(value)
510 elif name == 'Petal':
511 self.SetPetals(value)
512 elif name == 'Pincr':
513 self.SetPincr(value)
514
515 elif name == 'Vectors':
516 self.SetVectors(value)
517 elif name == 'Minimum':
518 self.SetMinVec(value)
519 elif name == 'Maximum':
520 self.SetMaxVec(value)
521 elif name == 'Skip first':
522 self.SetSkipFirst(value)
523 elif name == 'Draw only':
524 self.SetDrawOnly(value)
525
526 elif name == 'Vec/tick':
527 self.SetStep(value)
528 elif name == 'msec/tick':
529 self.SetDrawDelay(value)
530 elif name == 'Delay':
531 self.SetWaitDelay(value)
532 else:
533 print 'OnSpinback: Don\'t recognize', name
534
535 verbose = 0 # Need some command line options...
536 spin_panels = {} # Hooks to get from rose to panel labels
537 ctrl_buttons = {} # Button widgets for command (NE) panel
538
539 app = wx.App(False)
540 MyFrame()
541 if verbose:
542 print 'spin_panels', spin_panels.keys()
543 print 'ctrl_buttons', ctrl_buttons.keys()
544 app.MainLoop()