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
127
128class DemoWindow(object):
129
130    def __init__(self, filename=None):
131        self.root = root = turtle._root = Tk()
132        root.title('Python turtle-graphics examples')
133        root.wm_protocol("WM_DELETE_WINDOW", self._destroy)
134
135        if darwin:
136            import subprocess
137            # Make sure we are the currently activated OS X application
138            # so that our menu bar appears.
139            subprocess.run(
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,
148                    stdout=subprocess.DEVNULL,)
149
150        root.grid_rowconfigure(0, weight=1)
151        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)
155
156        self.mBar = Menu(root, relief=RAISED, borderwidth=2)
157        self.mBar.add_cascade(menu=self.makeLoadDemoMenu(self.mBar),
158                              label='Examples', underline=0)
159        self.mBar.add_cascade(menu=self.makeFontMenu(self.mBar),
160                              label='Fontsize', underline=0)
161        self.mBar.add_cascade(menu=self.makeHelpMenu(self.mBar),
162                              label='Help', underline=0)
163        root['menu'] = self.mBar
164
165        pane = PanedWindow(orient=HORIZONTAL, sashwidth=5,
166                           sashrelief=SOLID, bg='#ddd')
167        pane.add(self.makeTextFrame(pane))
168        pane.add(self.makeGraphFrame(pane))
169        pane.grid(row=0, columnspan=4, sticky='news')
170
171        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)
183        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')
187
188        Percolator(self.text).insertfilter(ColorDelegator())
189        self.dirty = False
190        self.exitflag = False
191        if filename:
192            self.loadfile(filename)
193        self.configGUI(DISABLED, DISABLED, DISABLED,
194                       "Choose example from menu", "black")
195        self.state = STARTUP
196
197
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)
208        color_config(text)
209
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)
216        text['yscrollcommand'] = vbar.set
217        text['xscrollcommand'] = hbar.set
218
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
229        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()
239        canvas._rootwindow.bind('<Configure>', self.onResize)
240        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
247
248    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))
255        return 'break'
256
257    def increase_size(self, dummy=None):
258        self.set_txtsize(min(txtfont[1] + 1, MAXIMUM_FONT_SIZE))
259        return 'break'
260
261    def update_mousewheel(self, event):
262        # For wheel up, event.delta = 120 on Windows, -1 on darwin.
263        # X-11 sends Control-Button-4 event instead.
264        if (event.delta < 0) == (not darwin):
265            return self.decrease_size()
266        else:
267            return self.increase_size()
268
269    def configGUI(self, start, stop, clear, txt="", color="blue"):
270        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")
276        self.output_lbl.config(text=txt, fg=color)
277
278    def makeLoadDemoMenu(self, master):
279        menu = Menu(master)
280
281        for entry in getExampleEntries():
282            def load(entry=entry):
283                self.loadfile(entry)
284            menu.add_command(label=entry, underline=0,
285                             font=menufont, command=load)
286        return menu
287
288    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)
301        return menu
302
303    def makeHelpMenu(self, master):
304        menu = Menu(master)
305
306        for help_label, help_file in help_entries:
307            def show(help_label=help_label, help_file=help_file):
308                view_text(self.root, help_label, help_file)
309            menu.add_command(label=help_label, font=menufont, command=show)
310        return menu
311
312    def refreshCanvas(self):
313        if self.dirty:
314            self.screen.clear()
315            self.dirty=False
316
317    def loadfile(self, filename):
318        self.clearCanvas()
319        turtle.TurtleScreen._RUNNING = False
320        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")
328        self.configGUI(NORMAL, DISABLED, DISABLED,
329                       "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
336        self.configGUI(DISABLED, NORMAL, DISABLED,
337                       "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:
349            if self.root is None:
350                return
351            self.state = DONE
352            result = "stopped!"
353        if self.state == DONE:
354            self.configGUI(NORMAL, DISABLED, NORMAL,
355                           result)
356        elif self.state == EVENTDRIVEN:
357            self.exitflag = True
358            self.configGUI(DISABLED, NORMAL, DISABLED,
359                           "use mouse/keys or STOP", "red")
360
361    def clearCanvas(self):
362        self.refreshCanvas()
363        self.screen._delete("all")
364        self.scanvas.config(cursor="")
365        self.configGUI(NORMAL, DISABLED, DISABLED)
366
367    def stopIt(self):
368        if self.exitflag:
369            self.clearCanvas()
370            self.exitflag = False
371            self.configGUI(NORMAL, DISABLED, DISABLED,
372                           "STOPPED!", "red")
373        turtle.TurtleScreen._RUNNING = False
374
375    def _destroy(self):
376        turtle.TurtleScreen._RUNNING = False
377        self.root.destroy()
378        self.root = None
379
380
381def main():
382    demo = DemoWindow()
383    demo.root.mainloop()
384
385if __name__ == '__main__':
386    main()
387