1#!/usr/bin/env python3
2
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.
17  The (syntax colored) source code appears in the left
18  source code window. IT CANNOT BE EDITED, but ONLY VIEWED!
19
20  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
22  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.
25
26  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.
32
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"""
88import sys
89import os
90
91from tkinter import *
92from idlelib.colorizer import ColorDelegator, color_config
93from idlelib.percolator import Percolator
94from idlelib.textview import view_text
95from turtledemo import __doc__ as about_turtledemo
96
97import turtle
98
99demo_dir = os.path.dirname(os.path.abspath(__file__))
100darwin = sys.platform == 'darwin'
101
102STARTUP = 1
103READY = 2
104RUNNING = 3
105DONE = 4
106EVENTDRIVEN = 5
107
108menufont = ("Arial", 12, NORMAL)
109btnfont = ("Arial", 12, 'bold')
110txtfont = ['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]
115
116def getExampleEntries():
117    return [entry[:-3] for entry in os.listdir(demo_dir) if
118            entry.endswith(".py") and entry[0] != '_']
119
120help_entries = (  # (help_label,  help_doc)
121    ('Turtledemo help', __doc__),
122    ('About turtledemo', about_turtledemo),
123    ('About turtle module', turtle.__doc__),
124    )
125
126
127class DemoWindow(object):
128
129    def __init__(self, filename=None):
130        self.root = root = turtle._root = Tk()
131        root.title('Python turtle-graphics examples')
132        root.wm_protocol("WM_DELETE_WINDOW", self._destroy)
133
134        if darwin:
135            import subprocess
136            # Make sure we are the currently activated OS X application
137            # so that our menu bar appears.
138            subprocess.run(
139                    [
140                        'osascript',
141                        '-e', 'tell application "System Events"',
142                        '-e', 'set frontmost of the first process whose '
143                              'unix id is {} to true'.format(os.getpid()),
144                        '-e', 'end tell',
145                    ],
146                    stderr=subprocess.DEVNULL,
147                    stdout=subprocess.DEVNULL,)
148
149        root.grid_rowconfigure(0, weight=1)
150        root.grid_columnconfigure(0, weight=1)
151        root.grid_columnconfigure(1, minsize=90, weight=1)
152        root.grid_columnconfigure(2, minsize=90, weight=1)
153        root.grid_columnconfigure(3, minsize=90, weight=1)
154
155        self.mBar = Menu(root, relief=RAISED, borderwidth=2)
156        self.mBar.add_cascade(menu=self.makeLoadDemoMenu(self.mBar),
157                              label='Examples', underline=0)
158        self.mBar.add_cascade(menu=self.makeFontMenu(self.mBar),
159                              label='Fontsize', underline=0)
160        self.mBar.add_cascade(menu=self.makeHelpMenu(self.mBar),
161                              label='Help', underline=0)
162        root['menu'] = self.mBar
163
164        pane = PanedWindow(orient=HORIZONTAL, sashwidth=5,
165                           sashrelief=SOLID, bg='#ddd')
166        pane.add(self.makeTextFrame(pane))
167        pane.add(self.makeGraphFrame(pane))
168        pane.grid(row=0, columnspan=4, sticky='news')
169
170        self.output_lbl = Label(root, height= 1, text=" --- ", bg="#ddf",
171                                font=("Arial", 16, 'normal'), borderwidth=2,
172                                relief=RIDGE)
173        if darwin:  # Leave Mac button colors alone - #44254.
174            self.start_btn = Button(root, text=" START ", font=btnfont,
175                                    fg='#00cc22', command=self.startDemo)
176            self.stop_btn = Button(root, text=" STOP ", font=btnfont,
177                                   fg='#00cc22', command=self.stopIt)
178            self.clear_btn = Button(root, text=" CLEAR ", font=btnfont,
179                                    fg='#00cc22', command = self.clearCanvas)
180        else:
181            self.start_btn = Button(root, text=" START ", font=btnfont,
182                                    fg="white", disabledforeground = "#fed",
183                                    command=self.startDemo)
184            self.stop_btn = Button(root, text=" STOP ", font=btnfont,
185                                   fg="white", disabledforeground = "#fed",
186                                   command=self.stopIt)
187            self.clear_btn = Button(root, text=" CLEAR ", font=btnfont,
188                                    fg="white", disabledforeground="#fed",
189                                    command = self.clearCanvas)
190        self.output_lbl.grid(row=1, column=0, sticky='news', padx=(0,5))
191        self.start_btn.grid(row=1, column=1, sticky='ew')
192        self.stop_btn.grid(row=1, column=2, sticky='ew')
193        self.clear_btn.grid(row=1, column=3, sticky='ew')
194
195        Percolator(self.text).insertfilter(ColorDelegator())
196        self.dirty = False
197        self.exitflag = False
198        if filename:
199            self.loadfile(filename)
200        self.configGUI(DISABLED, DISABLED, DISABLED,
201                       "Choose example from menu", "black")
202        self.state = STARTUP
203
204
205    def onResize(self, event):
206        cwidth = self.canvas.winfo_width()
207        cheight = self.canvas.winfo_height()
208        self.canvas.xview_moveto(0.5*(self.canvwidth-cwidth)/self.canvwidth)
209        self.canvas.yview_moveto(0.5*(self.canvheight-cheight)/self.canvheight)
210
211    def makeTextFrame(self, root):
212        self.text_frame = text_frame = Frame(root)
213        self.text = text = Text(text_frame, name='text', padx=5,
214                                wrap='none', width=45)
215        color_config(text)
216
217        self.vbar = vbar = Scrollbar(text_frame, name='vbar')
218        vbar['command'] = text.yview
219        vbar.pack(side=LEFT, fill=Y)
220        self.hbar = hbar = Scrollbar(text_frame, name='hbar', orient=HORIZONTAL)
221        hbar['command'] = text.xview
222        hbar.pack(side=BOTTOM, fill=X)
223        text['yscrollcommand'] = vbar.set
224        text['xscrollcommand'] = hbar.set
225
226        text['font'] = tuple(txtfont)
227        shortcut = 'Command' if darwin else 'Control'
228        text.bind_all('<%s-minus>' % shortcut, self.decrease_size)
229        text.bind_all('<%s-underscore>' % shortcut, self.decrease_size)
230        text.bind_all('<%s-equal>' % shortcut, self.increase_size)
231        text.bind_all('<%s-plus>' % shortcut, self.increase_size)
232        text.bind('<Control-MouseWheel>', self.update_mousewheel)
233        text.bind('<Control-Button-4>', self.increase_size)
234        text.bind('<Control-Button-5>', self.decrease_size)
235
236        text.pack(side=LEFT, fill=BOTH, expand=1)
237        return text_frame
238
239    def makeGraphFrame(self, root):
240        # t._Screen is a singleton class instantiated or retrieved
241        # by calling Screen.  Since tdemo canvas needs a different
242        # configuration, we manually set class attributes before
243        # calling Screen and manually call superclass init after.
244        turtle._Screen._root = root
245
246        self.canvwidth = 1000
247        self.canvheight = 800
248        turtle._Screen._canvas = self.canvas = canvas = turtle.ScrolledCanvas(
249                root, 800, 600, self.canvwidth, self.canvheight)
250        canvas.adjustScrolls()
251        canvas._rootwindow.bind('<Configure>', self.onResize)
252        canvas._canvas['borderwidth'] = 0
253
254        self.screen = screen = turtle.Screen()
255        turtle.TurtleScreen.__init__(screen, canvas)
256        turtle.RawTurtle.screens = [screen]
257        return canvas
258
259    def set_txtsize(self, size):
260        txtfont[1] = size
261        self.text['font'] = tuple(txtfont)
262        self.output_lbl['text'] = 'Font size %d' % size
263
264    def decrease_size(self, dummy=None):
265        self.set_txtsize(max(txtfont[1] - 1, MINIMUM_FONT_SIZE))
266        return 'break'
267
268    def increase_size(self, dummy=None):
269        self.set_txtsize(min(txtfont[1] + 1, MAXIMUM_FONT_SIZE))
270        return 'break'
271
272    def update_mousewheel(self, event):
273        # For wheel up, event.delta = 120 on Windows, -1 on darwin.
274        # X-11 sends Control-Button-4 event instead.
275        if (event.delta < 0) == (not darwin):
276            return self.decrease_size()
277        else:
278            return self.increase_size()
279
280    def configGUI(self, start, stop, clear, txt="", color="blue"):
281        if darwin:  # Leave Mac button colors alone - #44254.
282            self.start_btn.config(state=start)
283            self.stop_btn.config(state=stop)
284            self.clear_btn.config(state=clear)
285        else:
286            self.start_btn.config(state=start,
287                                  bg="#d00" if start == NORMAL else "#fca")
288            self.stop_btn.config(state=stop,
289                                 bg="#d00" if stop == NORMAL else "#fca")
290            self.clear_btn.config(state=clear,
291                                  bg="#d00" if clear == NORMAL else "#fca")
292        self.output_lbl.config(text=txt, fg=color)
293
294    def makeLoadDemoMenu(self, master):
295        menu = Menu(master)
296
297        for entry in getExampleEntries():
298            def load(entry=entry):
299                self.loadfile(entry)
300            menu.add_command(label=entry, underline=0,
301                             font=menufont, command=load)
302        return menu
303
304    def makeFontMenu(self, master):
305        menu = Menu(master)
306        menu.add_command(label="Decrease (C-'-')", command=self.decrease_size,
307                         font=menufont)
308        menu.add_command(label="Increase (C-'+')", command=self.increase_size,
309                         font=menufont)
310        menu.add_separator()
311
312        for size in font_sizes:
313            def resize(size=size):
314                self.set_txtsize(size)
315            menu.add_command(label=str(size), underline=0,
316                             font=menufont, command=resize)
317        return menu
318
319    def makeHelpMenu(self, master):
320        menu = Menu(master)
321
322        for help_label, help_file in help_entries:
323            def show(help_label=help_label, help_file=help_file):
324                view_text(self.root, help_label, help_file)
325            menu.add_command(label=help_label, font=menufont, command=show)
326        return menu
327
328    def refreshCanvas(self):
329        if self.dirty:
330            self.screen.clear()
331            self.dirty=False
332
333    def loadfile(self, filename):
334        self.clearCanvas()
335        turtle.TurtleScreen._RUNNING = False
336        modname = 'turtledemo.' + filename
337        __import__(modname)
338        self.module = sys.modules[modname]
339        with open(self.module.__file__, 'r') as f:
340            chars = f.read()
341        self.text.delete("1.0", "end")
342        self.text.insert("1.0", chars)
343        self.root.title(filename + " - a Python turtle graphics example")
344        self.configGUI(NORMAL, DISABLED, DISABLED,
345                       "Press start button", "red")
346        self.state = READY
347
348    def startDemo(self):
349        self.refreshCanvas()
350        self.dirty = True
351        turtle.TurtleScreen._RUNNING = True
352        self.configGUI(DISABLED, NORMAL, DISABLED,
353                       "demo running...", "black")
354        self.screen.clear()
355        self.screen.mode("standard")
356        self.state = RUNNING
357
358        try:
359            result = self.module.main()
360            if result == "EVENTLOOP":
361                self.state = EVENTDRIVEN
362            else:
363                self.state = DONE
364        except turtle.Terminator:
365            if self.root is None:
366                return
367            self.state = DONE
368            result = "stopped!"
369        if self.state == DONE:
370            self.configGUI(NORMAL, DISABLED, NORMAL,
371                           result)
372        elif self.state == EVENTDRIVEN:
373            self.exitflag = True
374            self.configGUI(DISABLED, NORMAL, DISABLED,
375                           "use mouse/keys or STOP", "red")
376
377    def clearCanvas(self):
378        self.refreshCanvas()
379        self.screen._delete("all")
380        self.canvas.config(cursor="")
381        self.configGUI(NORMAL, DISABLED, DISABLED)
382
383    def stopIt(self):
384        if self.exitflag:
385            self.clearCanvas()
386            self.exitflag = False
387            self.configGUI(NORMAL, DISABLED, DISABLED,
388                           "STOPPED!", "red")
389        turtle.TurtleScreen._RUNNING = False
390
391    def _destroy(self):
392        turtle.TurtleScreen._RUNNING = False
393        self.root.destroy()
394        self.root = None
395
396
397def main():
398    demo = DemoWindow()
399    demo.root.mainloop()
400
401if __name__ == '__main__':
402    main()
403