blob: 0a58332a6664007ff111e0008c49b80619872f8e [file] [log] [blame]
Georg Brandl076cbae2010-12-30 22:31:10 +00001#!/usr/bin/env python3
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -04002
3"""
4 ----------------------------------------------
5 turtleDemo - Help
6 ----------------------------------------------
7
8 This document has two sections:
9
10 (1) How to use the demo viewer
11 (2) How to add your own demos to the demo repository
12
13
14 (1) How to use the demo viewer.
15
16 Select a demoscript from the example menu.
Terry Jan Reedy86757992014-10-09 18:44:32 -040017 The (syntax colored) source code appears in the left
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -040018 source code window. IT CANNOT BE EDITED, but ONLY VIEWED!
19
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -040020 The demo viewer windows can be resized. The divider between text
21 and canvas can be moved by grabbing it with the mouse. The text font
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -040022 size can be changed from the menu and with Control/Command '-'/'+'.
23 It can also be changed on most systems with Control-mousewheel
24 when the mouse is over the text.
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -040025
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -040026 Press START button to start the demo.
27 Stop execution by pressing the STOP button.
28 Clear screen by pressing the CLEAR button.
29 Restart by pressing the START button again.
30
31 SPECIAL demos, such as clock.py are those which run EVENTDRIVEN.
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -040032
33 Press START button to start the demo.
34
35 - Until the EVENTLOOP is entered everything works
36 as in an ordinary demo script.
37
38 - When the EVENTLOOP is entered, you control the
39 application by using the mouse and/or keys (or it's
40 controlled by some timer events)
41 To stop it you can and must press the STOP button.
42
43 While the EVENTLOOP is running, the examples menu is disabled.
44
45 - Only after having pressed the STOP button, you may
46 restart it or choose another example script.
47
48 * * * * * * * *
49 In some rare situations there may occur interferences/conflicts
50 between events concerning the demo script and those concerning the
51 demo-viewer. (They run in the same process.) Strange behaviour may be
52 the consequence and in the worst case you must close and restart the
53 viewer.
54 * * * * * * * *
55
56
57 (2) How to add your own demos to the demo repository
58
59 - Place the file in the same directory as turtledemo/__main__.py
60 IMPORTANT! When imported, the demo should not modify the system
61 by calling functions in other modules, such as sys, tkinter, or
62 turtle. Global variables should be initialized in main().
63
64 - The code must contain a main() function which will
65 be executed by the viewer (see provided example scripts).
66 It may return a string which will be displayed in the Label below
67 the source code window (when execution has finished.)
68
69 - In order to run mydemo.py by itself, such as during development,
70 add the following at the end of the file:
71
72 if __name__ == '__main__':
73 main()
74 mainloop() # keep window open
75
76 python -m turtledemo.mydemo # will then run it
77
78 - If the demo is EVENT DRIVEN, main must return the string
79 "EVENTLOOP". This informs the demo viewer that the script is
80 still running and must be stopped by the user!
81
82 If an "EVENTLOOP" demo runs by itself, as with clock, which uses
83 ontimer, or minimal_hanoi, which loops by recursion, then the
84 code should catch the turtle.Terminator exception that will be
85 raised when the user presses the STOP button. (Paint is not such
86 a demo; it only acts in response to mouse clicks and movements.)
87"""
Georg Brandl076cbae2010-12-30 22:31:10 +000088import sys
89import os
90
91from tkinter import *
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -040092from idlelib.colorizer import ColorDelegator, color_config
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040093from idlelib.percolator import Percolator
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040094from idlelib.textview import view_text
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -040095from turtledemo import __doc__ as about_turtledemo
Georg Brandl076cbae2010-12-30 22:31:10 +000096
97import turtle
Georg Brandl076cbae2010-12-30 22:31:10 +000098
99demo_dir = os.path.dirname(os.path.abspath(__file__))
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400100darwin = sys.platform == 'darwin'
Georg Brandl076cbae2010-12-30 22:31:10 +0000101
102STARTUP = 1
103READY = 2
104RUNNING = 3
105DONE = 4
106EVENTDRIVEN = 5
107
108menufont = ("Arial", 12, NORMAL)
109btnfont = ("Arial", 12, 'bold')
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400110txtfont = ['Lucida Console', 10, 'normal']
111
112MINIMUM_FONT_SIZE = 6
113MAXIMUM_FONT_SIZE = 100
114font_sizes = [8, 9, 10, 11, 12, 14, 18, 20, 22, 24, 30]
Georg Brandl076cbae2010-12-30 22:31:10 +0000115
116def getExampleEntries():
117 return [entry[:-3] for entry in os.listdir(demo_dir) if
118 entry.endswith(".py") and entry[0] != '_']
119
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -0400120help_entries = ( # (help_label, help_doc)
121 ('Turtledemo help', __doc__),
122 ('About turtledemo', about_turtledemo),
123 ('About turtle module', turtle.__doc__),
Terry Jan Reedy280aace2014-07-25 01:56:24 -0400124 )
Georg Brandl076cbae2010-12-30 22:31:10 +0000125
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -0400126
127
Georg Brandl076cbae2010-12-30 22:31:10 +0000128class DemoWindow(object):
129
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400130 def __init__(self, filename=None):
Georg Brandl076cbae2010-12-30 22:31:10 +0000131 self.root = root = turtle._root = Tk()
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400132 root.title('Python turtle-graphics examples')
Georg Brandl076cbae2010-12-30 22:31:10 +0000133 root.wm_protocol("WM_DELETE_WINDOW", self._destroy)
134
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400135 if darwin:
Terry Jan Reedy7e55db22014-07-28 22:23:59 -0400136 import subprocess
137 # Make sure we are the currently activated OS X application
138 # so that our menu bar appears.
139 p = subprocess.Popen(
140 [
141 'osascript',
142 '-e', 'tell application "System Events"',
143 '-e', 'set frontmost of the first process whose '
144 'unix id is {} to true'.format(os.getpid()),
145 '-e', 'end tell',
146 ],
147 stderr=subprocess.DEVNULL,
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400148 stdout=subprocess.DEVNULL,)
Terry Jan Reedy7e55db22014-07-28 22:23:59 -0400149
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400150 root.grid_rowconfigure(0, weight=1)
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400151 root.grid_columnconfigure(0, weight=1)
152 root.grid_columnconfigure(1, minsize=90, weight=1)
153 root.grid_columnconfigure(2, minsize=90, weight=1)
154 root.grid_columnconfigure(3, minsize=90, weight=1)
Georg Brandl076cbae2010-12-30 22:31:10 +0000155
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400156 self.mBar = Menu(root, relief=RAISED, borderwidth=2)
157 self.mBar.add_cascade(menu=self.makeLoadDemoMenu(self.mBar),
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400158 label='Examples', underline=0)
159 self.mBar.add_cascade(menu=self.makeFontMenu(self.mBar),
160 label='Fontsize', underline=0)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400161 self.mBar.add_cascade(menu=self.makeHelpMenu(self.mBar),
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400162 label='Help', underline=0)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400163 root['menu'] = self.mBar
Georg Brandl076cbae2010-12-30 22:31:10 +0000164
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400165 pane = PanedWindow(orient=HORIZONTAL, sashwidth=5,
166 sashrelief=SOLID, bg='#ddd')
167 pane.add(self.makeTextFrame(pane))
168 pane.add(self.makeGraphFrame(pane))
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400169 pane.grid(row=0, columnspan=4, sticky='news')
Georg Brandl076cbae2010-12-30 22:31:10 +0000170
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400171 self.output_lbl = Label(root, height= 1, text=" --- ", bg="#ddf",
172 font=("Arial", 16, 'normal'), borderwidth=2,
173 relief=RIDGE)
174 self.start_btn = Button(root, text=" START ", font=btnfont,
175 fg="white", disabledforeground = "#fed",
176 command=self.startDemo)
177 self.stop_btn = Button(root, text=" STOP ", font=btnfont,
178 fg="white", disabledforeground = "#fed",
179 command=self.stopIt)
180 self.clear_btn = Button(root, text=" CLEAR ", font=btnfont,
181 fg="white", disabledforeground="#fed",
182 command = self.clearCanvas)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400183 self.output_lbl.grid(row=1, column=0, sticky='news', padx=(0,5))
184 self.start_btn.grid(row=1, column=1, sticky='ew')
185 self.stop_btn.grid(row=1, column=2, sticky='ew')
186 self.clear_btn.grid(row=1, column=3, sticky='ew')
Georg Brandl076cbae2010-12-30 22:31:10 +0000187
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400188 Percolator(self.text).insertfilter(ColorDelegator())
Georg Brandl076cbae2010-12-30 22:31:10 +0000189 self.dirty = False
190 self.exitflag = False
191 if filename:
192 self.loadfile(filename)
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400193 self.configGUI(DISABLED, DISABLED, DISABLED,
Georg Brandl076cbae2010-12-30 22:31:10 +0000194 "Choose example from menu", "black")
195 self.state = STARTUP
196
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400197
198 def onResize(self, event):
199 cwidth = self._canvas.winfo_width()
200 cheight = self._canvas.winfo_height()
201 self._canvas.xview_moveto(0.5*(self.canvwidth-cwidth)/self.canvwidth)
202 self._canvas.yview_moveto(0.5*(self.canvheight-cheight)/self.canvheight)
203
204 def makeTextFrame(self, root):
205 self.text_frame = text_frame = Frame(root)
206 self.text = text = Text(text_frame, name='text', padx=5,
207 wrap='none', width=45)
Terry Jan Reedy2bac3b72016-05-29 01:40:22 -0400208 color_config(text)
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400209
210 self.vbar = vbar = Scrollbar(text_frame, name='vbar')
211 vbar['command'] = text.yview
212 vbar.pack(side=LEFT, fill=Y)
213 self.hbar = hbar = Scrollbar(text_frame, name='hbar', orient=HORIZONTAL)
214 hbar['command'] = text.xview
215 hbar.pack(side=BOTTOM, fill=X)
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400216 text['yscrollcommand'] = vbar.set
217 text['xscrollcommand'] = hbar.set
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -0400218
219 text['font'] = tuple(txtfont)
220 shortcut = 'Command' if darwin else 'Control'
221 text.bind_all('<%s-minus>' % shortcut, self.decrease_size)
222 text.bind_all('<%s-underscore>' % shortcut, self.decrease_size)
223 text.bind_all('<%s-equal>' % shortcut, self.increase_size)
224 text.bind_all('<%s-plus>' % shortcut, self.increase_size)
225 text.bind('<Control-MouseWheel>', self.update_mousewheel)
226 text.bind('<Control-Button-4>', self.increase_size)
227 text.bind('<Control-Button-5>', self.decrease_size)
228
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400229 text.pack(side=LEFT, fill=BOTH, expand=1)
230 return text_frame
231
232 def makeGraphFrame(self, root):
233 turtle._Screen._root = root
234 self.canvwidth = 1000
235 self.canvheight = 800
236 turtle._Screen._canvas = self._canvas = canvas = turtle.ScrolledCanvas(
237 root, 800, 600, self.canvwidth, self.canvheight)
238 canvas.adjustScrolls()
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -0400239 canvas._rootwindow.bind('<Configure>', self.onResize)
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400240 canvas._canvas['borderwidth'] = 0
241
242 self.screen = _s_ = turtle.Screen()
243 turtle.TurtleScreen.__init__(_s_, _s_._canvas)
244 self.scanvas = _s_._canvas
245 turtle.RawTurtle.screens = [_s_]
246 return canvas
Georg Brandl076cbae2010-12-30 22:31:10 +0000247
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400248 def set_txtsize(self, size):
249 txtfont[1] = size
250 self.text['font'] = tuple(txtfont)
251 self.output_lbl['text'] = 'Font size %d' % size
252
253 def decrease_size(self, dummy=None):
254 self.set_txtsize(max(txtfont[1] - 1, MINIMUM_FONT_SIZE))
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -0400255 return 'break'
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400256
257 def increase_size(self, dummy=None):
258 self.set_txtsize(min(txtfont[1] + 1, MAXIMUM_FONT_SIZE))
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -0400259 return 'break'
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400260
261 def update_mousewheel(self, event):
262 # For wheel up, event.delte = 120 on Windows, -1 on darwin.
263 # X-11 sends Control-Button-4 event instead.
Terry Jan Reedyd43ffc92014-09-04 20:59:41 -0400264 if (event.delta < 0) == (not darwin):
265 return self.decrease_size()
266 else:
267 return self.increase_size()
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400268
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400269 def configGUI(self, start, stop, clear, txt="", color="blue"):
Terry Jan Reedy8b95d5e2014-07-27 03:01:13 -0400270 self.start_btn.config(state=start,
271 bg="#d00" if start == NORMAL else "#fca")
272 self.stop_btn.config(state=stop,
273 bg="#d00" if stop == NORMAL else "#fca")
274 self.clear_btn.config(state=clear,
275 bg="#d00" if clear == NORMAL else"#fca")
Georg Brandl076cbae2010-12-30 22:31:10 +0000276 self.output_lbl.config(text=txt, fg=color)
277
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400278 def makeLoadDemoMenu(self, master):
279 menu = Menu(master)
Georg Brandl076cbae2010-12-30 22:31:10 +0000280
281 for entry in getExampleEntries():
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400282 def load(entry=entry):
283 self.loadfile(entry)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400284 menu.add_command(label=entry, underline=0,
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400285 font=menufont, command=load)
286 return menu
Georg Brandl076cbae2010-12-30 22:31:10 +0000287
Terry Jan Reedyc6a1c022014-09-03 16:17:41 -0400288 def makeFontMenu(self, master):
289 menu = Menu(master)
290 menu.add_command(label="Decrease (C-'-')", command=self.decrease_size,
291 font=menufont)
292 menu.add_command(label="Increase (C-'+')", command=self.increase_size,
293 font=menufont)
294 menu.add_separator()
295
296 for size in font_sizes:
297 def resize(size=size):
298 self.set_txtsize(size)
299 menu.add_command(label=str(size), underline=0,
300 font=menufont, command=resize)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400301 return menu
Georg Brandl076cbae2010-12-30 22:31:10 +0000302
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400303 def makeHelpMenu(self, master):
304 menu = Menu(master)
Georg Brandl076cbae2010-12-30 22:31:10 +0000305
Terry Jan Reedy280aace2014-07-25 01:56:24 -0400306 for help_label, help_file in help_entries:
307 def show(help_label=help_label, help_file=help_file):
Terry Jan Reedy94ee51e2014-08-14 23:59:32 -0400308 view_text(self.root, help_label, help_file)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400309 menu.add_command(label=help_label, font=menufont, command=show)
Terry Jan Reedyf819ef72014-08-15 01:23:02 -0400310 return menu
Georg Brandl076cbae2010-12-30 22:31:10 +0000311
312 def refreshCanvas(self):
Terry Jan Reedy8450c532014-08-27 01:43:50 -0400313 if self.dirty:
314 self.screen.clear()
315 self.dirty=False
Georg Brandl076cbae2010-12-30 22:31:10 +0000316
317 def loadfile(self, filename):
Terry Jan Reedy8450c532014-08-27 01:43:50 -0400318 self.clearCanvas()
319 turtle.TurtleScreen._RUNNING = False
Georg Brandl076cbae2010-12-30 22:31:10 +0000320 modname = 'turtledemo.' + filename
321 __import__(modname)
322 self.module = sys.modules[modname]
323 with open(self.module.__file__, 'r') as f:
324 chars = f.read()
325 self.text.delete("1.0", "end")
326 self.text.insert("1.0", chars)
327 self.root.title(filename + " - a Python turtle graphics example")
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400328 self.configGUI(NORMAL, DISABLED, DISABLED,
Georg Brandl076cbae2010-12-30 22:31:10 +0000329 "Press start button", "red")
330 self.state = READY
331
332 def startDemo(self):
333 self.refreshCanvas()
334 self.dirty = True
335 turtle.TurtleScreen._RUNNING = True
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400336 self.configGUI(DISABLED, NORMAL, DISABLED,
Georg Brandl076cbae2010-12-30 22:31:10 +0000337 "demo running...", "black")
338 self.screen.clear()
339 self.screen.mode("standard")
340 self.state = RUNNING
341
342 try:
343 result = self.module.main()
344 if result == "EVENTLOOP":
345 self.state = EVENTDRIVEN
346 else:
347 self.state = DONE
348 except turtle.Terminator:
Serhiy Storchaka80a18032015-02-22 17:25:33 +0200349 if self.root is None:
350 return
Georg Brandl076cbae2010-12-30 22:31:10 +0000351 self.state = DONE
352 result = "stopped!"
353 if self.state == DONE:
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400354 self.configGUI(NORMAL, DISABLED, NORMAL,
Georg Brandl076cbae2010-12-30 22:31:10 +0000355 result)
356 elif self.state == EVENTDRIVEN:
357 self.exitflag = True
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400358 self.configGUI(DISABLED, NORMAL, DISABLED,
Georg Brandl076cbae2010-12-30 22:31:10 +0000359 "use mouse/keys or STOP", "red")
360
361 def clearCanvas(self):
362 self.refreshCanvas()
363 self.screen._delete("all")
364 self.scanvas.config(cursor="")
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400365 self.configGUI(NORMAL, DISABLED, DISABLED)
Georg Brandl076cbae2010-12-30 22:31:10 +0000366
367 def stopIt(self):
368 if self.exitflag:
369 self.clearCanvas()
370 self.exitflag = False
Terry Jan Reedy2ced87f2014-08-27 01:58:40 -0400371 self.configGUI(NORMAL, DISABLED, DISABLED,
Georg Brandl076cbae2010-12-30 22:31:10 +0000372 "STOPPED!", "red")
Terry Jan Reedy8450c532014-08-27 01:43:50 -0400373 turtle.TurtleScreen._RUNNING = False
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400374
375 def _destroy(self):
Serhiy Storchaka80a18032015-02-22 17:25:33 +0200376 turtle.TurtleScreen._RUNNING = False
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400377 self.root.destroy()
Serhiy Storchaka80a18032015-02-22 17:25:33 +0200378 self.root = None
Terry Jan Reedyb03f0422014-07-23 15:01:12 -0400379
Georg Brandl076cbae2010-12-30 22:31:10 +0000380
Terry Jan Reedyb8352e72014-07-23 17:27:57 -0400381def main():
382 demo = DemoWindow()
383 demo.root.mainloop()
384
Georg Brandl076cbae2010-12-30 22:31:10 +0000385if __name__ == '__main__':
Terry Jan Reedyb8352e72014-07-23 17:27:57 -0400386 main()