├── README.md ├── docs └── platypus_logo.gif ├── guided_tour.py ├── requirements.txt ├── setup.py ├── tkui ├── __init__.py ├── functions.py └── generated_tree.py ├── tour_functions.py └── tour_text.py /README.md: -------------------------------------------------------------------------------- 1 | # tkui - a visual introspective GUI maker with live editing of the GUI and its editor at the same time 2 | 3 | ## Installation and running 4 | 5 | pip install -r requirements.txt 6 | 7 | which installs the dependencies [uielem](https://github.com/asrp/uielem) and [undoable](https://github.com/asrp/undoable). Run 8 | 9 | python guided_tour.py 10 | 11 | to start the guided tour which gives an idea of how the pieces work. 12 | 13 | To start the default editor instead: 14 | 15 | python tkui.py 16 | 17 | Note that both need to be started from the `tkui` directory. 18 | 19 | ## Rationale and intended use 20 | 21 | Its easier to make a GUI if you can visually see the result immediately and there are a few editors that allows this. But why should the GUI editor itself be off limits? What if you suddenly want to add many new objects of a certain type to your GUI? This is something that would need adding a feature to the editor. tkui lets you do this with no need to recompile, no need to even restart the program. 22 | 23 | The guided tour explains this by showing it: The intended way to use tkui is to start `python tkui.py`, create a root `tk.Frame` or `tk.Toplevel` somewhere (anywhere) where the new UI you're making will be. Then add to that root element through, save everything under the root using the `gencode` function and then use the generated code for UI layout in your program. 24 | 25 | For things with callbacks, edit `functions.py` (`tour_functions.py` in the guided tour) and reload it (using the "Reload" button or `execfile("functions")`). This way you can see right away if the new function works or not. 26 | 27 | Some widgets are available for wrapping Python values directly (`BoxedList`, `BoxedBool`, `BoxedDict`). 28 | 29 | ## History 30 | 31 | `tkui` is the result of a bad joke gone too far. Just wanted to see how self-referential I could make it. After that, a guided tour instead of documentation seemed self-referentially appropriate. 32 | 33 | ## Todo 34 | 35 | - Finish explaining what everything does in the guided tour. 36 | - Think of other visual representations of `list` and `dict`, especially `dict. 37 | - Repackage a lot of the global state (into classes or other). 38 | - Determine what works and doesn't work with undo/redo and make more things work. 39 | -------------------------------------------------------------------------------- /docs/platypus_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrp/tkui/47e7a65e22c60098e4f8772fecf01ee373d96334/docs/platypus_logo.gif -------------------------------------------------------------------------------- /guided_tour.py: -------------------------------------------------------------------------------- 1 | import Tkinter as tk 2 | from Tkinter import Tk, Frame, Button 3 | import tkMessageBox, tkFont 4 | import tkui 5 | from tkui import Entry, BoxedList, BoxedTree, BoxedBool, ScrolledText, gencode 6 | from uielem import uidict, UI, bindlist 7 | from undoable import observed_list, observed_tree 8 | import ttk 9 | import re 10 | import sys 11 | import os 12 | import logging 13 | 14 | class TkGuide: 15 | def __init__(self, terp): 16 | self.step_index = 0 17 | self.hint_index = 0 18 | self.executed = set() 19 | self.terp = terp 20 | 21 | def start_step(self): 22 | uidict["guide"].delete(1.0, "end") 23 | # Hack! Should be elsewhere 24 | if self.step_index == 0: 25 | try: 26 | self.image = tk.PhotoImage(file=os.path.join("docs", "platypus_logo.gif")).subsample(2) 27 | except tk.TclError: 28 | self.image = tk.PhotoImage() # fallback 29 | uidict["guide"].image_create('end', image=self.image) 30 | uidict["guide"].insert('end', "\n") 31 | add_tagged(uidict["guide"], limited_markdown(self.step.message.strip())) 32 | self.hint_index = 0 33 | if self.hint_index < len(self.step.hint): 34 | uidict["hint terp"].text = self.step.hint[self.hint_index] 35 | 36 | @property 37 | def step(self): 38 | return steps[self.step_index] 39 | 40 | def next_hint(self, event): 41 | self.hint_index += 1 42 | self.terp.sendexec(event) 43 | if self.hint_index < len(self.step.hint): 44 | uidict["hint terp"].text = self.step.hint[self.hint_index] 45 | 46 | def tree_change(self, widget, event, *args, **kwargs): 47 | # Hack! Should be elsewhere 48 | logging.info("Tree change", widget, event, args, kwargs) 49 | if self.step.condition == "uilist": 50 | if event in ["append", "insert"] and args[-1] == uilist: 51 | uiroot.elem.after(50, self.next) 52 | elif self.step.condition == "history": 53 | if event in ["insert", "append"] and args[-1].kwargs.get("name") == "history": 54 | uiroot.elem.after(50, self.next) 55 | 56 | def next(self): 57 | if self.step.post and self.step_index not in self.executed: 58 | self.step.post(uiroot) 59 | self.executed.add(self.step_index) 60 | self.step_index += 1 61 | self.start_step() 62 | 63 | def prev(self): 64 | self.step_index = max(0, self.step_index - 1) 65 | self.start_step() 66 | 67 | def jump_to(self, index): 68 | self.step_index = index 69 | self.start_step() 70 | 71 | def setfonts(): 72 | for fontname in tkFont.names(): 73 | default_font = tkFont.nametofont(fontname) 74 | default_font.configure(size=14) 75 | 76 | def select_callback(event): 77 | selection = event.widget.wselection() 78 | #if uidict["autoparam"].value and selection: 79 | # widget = selection[0][1] 80 | # uidict["params"]._dict.replace(widget.kwargs) 81 | 82 | def hint_toggle(name, op, value): 83 | if value: 84 | uidict["hint terp"].ui.parent.repack() 85 | else: 86 | uidict["hint terp"].pack_forget() 87 | 88 | def limited_markdown(text): 89 | text = re.sub(" (.*?)\n", " \\1\n", text) 90 | text = re.sub("`(.*?)`", "\\1", text) 91 | text = re.sub("\*{2}(.+)\*{2}", "\\1", text) 92 | text = re.sub("\*(.+)\*", "\\1", text) 93 | tagged = [""] + re.split("(<.*?>)", text) 94 | return zip(tagged[::2], tagged[1::2]) 95 | 96 | def add_tagged(widget, tagged): 97 | for tag, text in tagged: 98 | widget.insert('end', text, (tag,)) 99 | 100 | histfile = "tour_guide_history" 101 | terp = tkui.TkTerp(histfile, globals()) 102 | tkguide = TkGuide(terp) 103 | execfile("tour_functions.py") 104 | execfile("tour_text.py") 105 | 106 | if len(sys.argv) > 1: 107 | execfile(sys.argv[1]) 108 | else: 109 | uiroot = UI(Tk, packanchor='n', name='root', title='TkUI Guided Tour', children=[ 110 | UI(Frame, packside='left', children=[ 111 | UI(Frame, packside='top', children=[ 112 | UI(tkui.ScrolledText, name='guide', height=8, width=40, font="Verdana 14", wrap=tk.WORD), 113 | UI(Button, text='Embark', name="embark", command=tkguide.next), 114 | UI(Frame, packside='left', name="hint frame", children=[ 115 | UI(tkui.BoxedBool, text='Hint', name='hint'), 116 | UI(tkui.Entry, text='', name='hint terp')]), 117 | ])]), 118 | UI(Frame, packside='left', name="hidden", children=[ 119 | UI(tkui.Entry, text='', name='tkterp'), 120 | UI(Frame, packside='top', name='uilistframe', children=[ 121 | UI(tkui.BoxedList, width=12, name='uilist'), 122 | UI(Frame, packside='left', children=[ 123 | UI(tkui.Entry, width=10, name='child params'), 124 | UI(Button, text='+', command=addwidget), 125 | UI(Button, text='+c', command=addchild), 126 | UI(Button, text='-', command=delwidget)])]), 127 | UI(tkui.BoxedTree, name="tree"), 128 | ])]) 129 | 130 | uiroot.makeelem() 131 | setfonts() 132 | terp.bind_keys(uidict["tkterp"]) 133 | uidict['hint terp'].bind('', tkguide.next_hint) 134 | uidict['hint terp'].bind('', tkguide.next_hint) 135 | tkuilist = observed_list(["Entry"]) 136 | 137 | treeelem = uidict["tree"] 138 | treeelem._tree.append(uiroot) 139 | treeelem._tree.elem = None 140 | treeelem.redraw() 141 | style = ttk.Style(uidict["root"]) 142 | style.configure('Treeview', rowheight=14*2) 143 | treeelem.column("#0", minwidth=0, width=800) 144 | uitree = tkui.UITree(uiroot, treeelem) 145 | uidict["tree"].bind('<>', select_callback) 146 | #uidict["params"]._dict.callbacks.append(update_param) 147 | tkuitree = treeelem.ui 148 | uilist = uidict["uilistframe"].ui 149 | 150 | undolog = tkui.UndoLog() 151 | undolog.add(uiroot) 152 | 153 | uidict["root"].bind("", tkui.click) 154 | 155 | uiroot.callbacks.append(tkguide.tree_change) 156 | tkguide.start_step() 157 | uidict["hint"].callbacks.append(hint_toggle) 158 | uidict["hidden"].pack_forget() 159 | uidict["hint terp"].pack_forget() 160 | if len(sys.argv) == 1: 161 | uidict["hint frame"].pack_forget() 162 | uidict["guide"].tag_configure("", font="Verdana 15 bold") 163 | uidict["guide"].tag_configure("", font="Verdana 14") 164 | uidict["guide"].tag_configure("", font="Monospace 13") 165 | uidict["guide"].tag_configure("", font="Monospace 13") 166 | uidict["root"].mainloop() 167 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e git://github.com/asrp/uielem#egg=uielem 2 | -e git://github.com/asrp/undoable#egg=undoable 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup(name='tkui', 3 | version='0.9', 4 | description='A visual introspective GUI maker with live editing of the GUI and its editor at the same time', 5 | url='https://github.com/asrp/tkui', 6 | author='asrp', 7 | author_email='asrp@email.com', 8 | packages=['tkui'], 9 | keywords='gui editor introspection') 10 | -------------------------------------------------------------------------------- /tkui/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | import sys, pdb 4 | import Tkinter as tk 5 | from uielem import uidict, UI, bindlist 6 | from ScrolledText import ScrolledText as TkScrolledText 7 | import tkMessageBox, tkFont 8 | import ttk 9 | from undoable import observed_list, observed_dict, observed_tree, UndoLog 10 | import time 11 | import os 12 | 13 | class Entry(tk.Entry, object): 14 | def __init__(self, *args, **kwargs): 15 | text = kwargs.pop("defaulttext", "") 16 | self.autosize = kwargs.pop("autosize", False) 17 | tk.Entry.__init__(self, *args, **kwargs) 18 | self.text = text 19 | #self.bind('', terp.sendexec) 20 | self.bind('', self.select_all) 21 | 22 | def select_all(self, event): 23 | self.selection_range(0, 'end') 24 | return "break" 25 | 26 | def get_text(self): 27 | return self.get() 28 | 29 | def set_text(self, text=""): 30 | self.delete(0, tk.END) 31 | self.insert(tk.END, text) 32 | if self.autosize: 33 | self["width"] = len(text) 34 | 35 | text = property(get_text, set_text) 36 | 37 | class ScrolledText(TkScrolledText, object): 38 | def __init__(self, *args, **kwargs): 39 | text = kwargs.pop("defaulttext", "") 40 | self.autosize = kwargs.pop("autosize", False) 41 | TkScrolledText.__init__(self, *args, **kwargs) 42 | self.text = text 43 | self.bind('', self.select_all) 44 | 45 | def select_all(self, event): 46 | event.widget.tag_add("sel", "1.0", "end") 47 | return "break" 48 | 49 | def get_text(self): 50 | return self.get(1.0, 'end') 51 | 52 | def set_text(self, text=""): 53 | self.delete(1.0, "end") 54 | self.insert("end", text) 55 | if self.autosize: 56 | self["width"] = len(text) 57 | 58 | text = property(get_text, set_text) 59 | 60 | class BoxedBool(tk.Checkbutton, object): 61 | def __init__(self, *args, **kwargs): 62 | self.var = kwargs["variable"] = tk.IntVar() 63 | self.var.set(kwargs.pop("value", 0)) 64 | self.var.trace("w", self.callback) 65 | self.callbacks = [] 66 | tk.Checkbutton.__init__(self, *args, **kwargs) 67 | 68 | def callback(self, name, index, op): 69 | logging.debug("Bool callback %s %s %s", name, op, self.value) 70 | for callback in self.callbacks: 71 | callback(name, op, self.value) 72 | 73 | def get_value(self): 74 | return bool(self.var.get()) 75 | 76 | def set_value(self, value): 77 | self.var.set(value) 78 | if self.value: 79 | self.select() 80 | else: 81 | self.deselect() 82 | 83 | value = property(get_value, set_value) 84 | 85 | class BoxedList(tk.Listbox): 86 | def __init__(self, *args, **kwargs): 87 | logging.debug("Creating BoxedList with %s %s", args, kwargs) 88 | self._list = kwargs.pop("_list", observed_list()) 89 | self._list.callbacks.append(self.callback) 90 | tk.Listbox.__init__(self, *args, **kwargs) 91 | self.redraw() 92 | 93 | def reset(self, newlist): 94 | self._list.callbacks.remove(self.callback) 95 | self._list = newlist 96 | self._list.callbacks.append(self.callback) 97 | self.redraw() 98 | 99 | def callback(self, _list, event, *args): 100 | if event == "append": 101 | value, = args 102 | self.insert(tk.END, value) 103 | elif event == "pop": 104 | index, = args 105 | if index == -1: 106 | index = len(self._list) 107 | self.delete(index) 108 | elif event == "replace": 109 | self.redraw() 110 | elif event == "extend": 111 | self.redraw() 112 | elif event == "__setitem__": 113 | index, value = args 114 | if index < 0: 115 | index = len(_list) + index 116 | self.delete(index) 117 | self.insert(index, _list[index]) 118 | self.see(index) 119 | 120 | def redraw(self): 121 | self.delete(0, tk.END) 122 | for item in self._list: 123 | self.insert(tk.END, item) 124 | 125 | def append(self, item): 126 | self.insert(tk.END, item) 127 | 128 | def lselection(self): 129 | return [self._list[int(index)] for index in self.curselection()] 130 | 131 | class BoxedDict(tk.Frame): 132 | def __init__(self, *args, **kwargs): 133 | logging.debug("Creating BoxedDict with %s %s", args, kwargs) 134 | self._dict = kwargs.pop("_dict", observed_dict()) 135 | self._dict.callbacks.append(self.callback) 136 | self.width = kwargs.pop("ewidth", 5) 137 | tk.Frame.__init__(self, *args, **kwargs) 138 | self.entries = {} 139 | 140 | def reset(self, newdict): 141 | self._dict.callbacks.remove(self.callback) 142 | self._dict = newdict 143 | self._dict.callbacks.append(self.callback) 144 | self.redraw() 145 | 146 | def addkeys(self, widget, entrytype, key): 147 | for k in ['', '', '']: 148 | widget.bind(k, lambda e, key=key: self.keypress(e, entrytype, key)) 149 | 150 | def keypress(self, event, entrytype, key): 151 | logging.debug("Pressed %s: %s" % (entrytype, key)) 152 | if event.keysym == "Return": 153 | newkey = self.entries[key][0].elem.text 154 | logging.debug("New key %s", newkey) 155 | if newkey != key: 156 | value = self.entries[key][1].elem.text 157 | if key != "": 158 | del self._dict[key] 159 | if newkey != "": 160 | self._dict[newkey] = value 161 | self.entries[newkey][0].elem.focus() 162 | if key == "" and newkey != "": 163 | self.entries[key][0].elem.text = "" 164 | self.entries[key][1].elem.text = "" 165 | self.entries[key][0].elem.lift() 166 | self.entries[key][1].elem.lift() 167 | elif entrytype == "value": 168 | self._dict[key] = self.entries[key][1].elem.text 169 | elif event.keysym == "Down": 170 | newwidget = event.widget.tk_focusNext().tk_focusNext() 171 | if newwidget in [s for w in self.pack_slaves() for s in w.pack_slaves()]: 172 | newwidget.focus() 173 | elif event.keysym == "Up": 174 | newwidget = event.widget.tk_focusPrev().tk_focusPrev() 175 | if newwidget in [s for w in self.pack_slaves() for s in w.pack_slaves()]: 176 | newwidget.focus() 177 | 178 | def callback(self, _dict, event, *args): 179 | logging.debug("Event %s", event) 180 | if event == "__setitem__": 181 | key, value = args 182 | # TODO: Bad! Fix using undocallback? 183 | if key not in self.entries: 184 | logging.debug("Adding to row %s", len(self._dict)) 185 | self.addentry(key, value, len(self.ui) - 1) 186 | else: 187 | self.entries[key][1].elem.text = self._dict[key] 188 | elif event == "pop" or event == "__delitem__": 189 | key = args[0] 190 | self.ui.remove(self.entries[key]) 191 | self.entries[key].elem.destroy() 192 | del self.entries[key] 193 | elif event == "clear": 194 | for entry in self.entries.values(): 195 | self.ui.remove(entry) 196 | self.entries.clear() 197 | elif event == "replace": 198 | self.redraw() 199 | 200 | def addentry(self, key, value, index="end"): 201 | frame = UI(tk.Frame, packside=tk.LEFT, children=[ 202 | UI(Entry, width=self.width, defaulttext=key), 203 | UI(Entry, width=self.width, defaulttext=value)]) 204 | self.ui.add(frame, index) 205 | self.entries[key] = frame 206 | self.addkeys(frame[0].elem, "key", key) 207 | self.addkeys(frame[1].elem, "value", key) 208 | 209 | def redraw(self): 210 | for child in self.pack_slaves(): 211 | child.destroy() 212 | self.key_entries = {} 213 | self.value_entries = {} 214 | for i, key in enumerate(sorted(self._dict.keys())): 215 | self.addentry(key, uneval(self._dict[key])) 216 | self.addentry("", "") 217 | self.ui.repack() 218 | 219 | class TkTerp: 220 | def __init__(self, histfile=None, globs=None): 221 | self.histfile = histfile 222 | if histfile and os.path.isfile(histfile): 223 | self.history = observed_list(open(histfile).read().splitlines()) 224 | else: 225 | self.history = observed_list() 226 | self.histindex = len(self.history) 227 | self.globs = globs if globs is not None else {} 228 | self.history.append("") 229 | 230 | def sendexec(self, event): 231 | command = event.widget.text 232 | print ">>> %s" % command 233 | globs = self.globs if self.globs else globals() 234 | try: 235 | co = compile(command, "", "single") 236 | exec co in globs 237 | except: 238 | self.last_tb = sys.exc_traceback 239 | traceback.print_exc() 240 | if len(self.history) > 1 and command == self.history[-2]: 241 | self.history[-1] = "" 242 | self.histindex = len(self.history) - 1 243 | else: 244 | self.history[-1] = command 245 | self.histindex = len(self.history) 246 | self.history.append("") 247 | event.widget.text = "" 248 | 249 | def pm(self): 250 | pdb.post_mortem(self.last_tb) 251 | 252 | def hist(self, event): 253 | if event.keysym == "Up": 254 | if self.histindex == len(self.history) - 1: 255 | self.history[-1] = event.widget.text 256 | self.histindex = max(0, self.histindex - 1) 257 | elif event.keysym == "Down": 258 | self.histindex = min(len(self.history) - 1, self.histindex + 1) 259 | event.widget.text = self.history[self.histindex] 260 | 261 | def save(self): 262 | if self.histfile: 263 | open(self.histfile, "w").write("\n".join(self.history[:-1])) 264 | 265 | def bind_keys(self, elem): 266 | elem.bind('', self.sendexec) 267 | elem.bind('', self.sendexec) 268 | elem.bind('', self.hist) 269 | elem.bind('', self.hist) 270 | 271 | class BoxedTree(ttk.Treeview): 272 | def __init__(self, *args, **kwargs): 273 | logging.debug("Creating BoxedTree with %s %s", args, kwargs) 274 | self._tree = kwargs.pop("_tree", observed_tree()) 275 | self._tree.undocallbacks.append(self.callback) 276 | ttk.Treeview.__init__(self, *args, **kwargs) 277 | self.redraw() 278 | 279 | def reset(self, newtree): 280 | self._tree.undocallbacks.remove(self.callback) 281 | self._tree = newtree 282 | self._tree.undocallbacks.append(self.callback) 283 | self.redraw() 284 | 285 | def callback(self, _tree, undo, redo): 286 | event, args = redo[0], redo[1:] 287 | logging.debug("** BoxedTree callback %s %s %s", event, _tree, args) 288 | if event == "remove": 289 | child, = args 290 | logging.debug("treeindex %s", _tree.treeindex) 291 | #if hasattr(child, "treeindex"): 292 | # print child.treeindex 293 | # Not sure why it stopped working or why this works now! 294 | if hasattr(child, "treeindex"): 295 | self.detach(child.treeindex) 296 | elif event == "pop": 297 | index, = args 298 | oldvalue = undo[1] 299 | # TODO: Fix (should maybe call remove but then needs to not remove twice from list). This is the old fixed point problem. 300 | if hasattr(oldvalue, "elem"): 301 | oldvalue.elem.pack_forget() 302 | self.detach(oldvalue.treeindex) 303 | del self.widget[oldvalue.treeindex] 304 | oldvalue.treeindex = None 305 | #_tree.remove(oldvalue) 306 | #self.detach(_tree.children[index].treeindex) 307 | elif event == "reparent": 308 | newparent, = args 309 | # Hack TODO: Fix. 310 | if hasattr(_tree, "treeindex") and _tree.treeindex != None: 311 | self.reattach(_tree.treeindex, newparent.treeindex, "end") 312 | else: 313 | self.addtreeelem(_tree, parent=newparent.treeindex) 314 | elif event == "insert": 315 | index, child = args 316 | #print child.treeindex, _tree.treeindex, index 317 | # Hack TODO: Fix. 318 | if hasattr(child, "treeindex") and child.treeindex != None: 319 | self.reattach(child.treeindex, _tree.treeindex, index) 320 | else: 321 | self.addtreeelem(child, parent=_tree.treeindex, index=index) 322 | elif event == "append": 323 | child, = args 324 | index = len(child.parent) 325 | if hasattr(child, "treeindex") and child.treeindex != None: 326 | self.reattach(child.treeindex, _tree.treeindex, index) 327 | else: 328 | self.addtreeelem(child, parent=_tree.treeindex, index=index) 329 | 330 | def redraw(self): 331 | map(self.delete, self.get_children()) 332 | self.widget = {} 333 | self.addtreeelem(self._tree) 334 | 335 | def addtreeelem(self, elem, parent="", index="end"): 336 | index = self.insert(parent, index, text=str(elem), open=True) 337 | self.widget[index] = elem 338 | elem.treeindex = index 339 | for subelem in elem: 340 | self.addtreeelem(subelem, index) 341 | 342 | def selectiondict(self): 343 | return {index: self.widget[index] for index in self.selection()} 344 | 345 | def wselection(self): 346 | return [self.widget[index] for index in self.selection()] 347 | 348 | def selection_set(self, *args, **kwargs): 349 | return ttk.Treeview.selection_set(self, *args, **kwargs) 350 | 351 | def selection_set_by_items(self, wrappers): 352 | keys = [k for k, v in self.widget.items() if v in wrappers] 353 | if keys: 354 | self.selection_set(keys[0]) 355 | for key in keys[1:]: 356 | self.selection_add(key) 357 | #return self.selection_set(keys) 358 | 359 | class UITree: 360 | def __init__(self, root, tree): 361 | self.root = root 362 | self.tree = tree 363 | self.indexof = {} 364 | 365 | def addwidget(self, location="sibling", kwargs=None, *args): 366 | if kwargs is None: 367 | kwargs = {} 368 | widget = self.tree.wselection()[0] 369 | uiname = uidict["uilist"]._list[int(uidict["uilist"].curselection()[0])] 370 | logging.debug("Adding %s %s %s %s %s", widget, kwargs, uiname, eval(uiname), uidict["child params"].text.split(",")) 371 | # Need to think of something safer than eval. 372 | if location == "child": 373 | newelem = UI(eval(uiname), **kwargs) 374 | widget.add(newelem, 0) 375 | elif widget.parent != self.tree._tree: 376 | newelem = UI(eval(uiname), **kwargs) 377 | newindex = widget.parent.index(widget) + 1 378 | widget.parent.add(newelem, newindex) 379 | else: 380 | # Make sure to have a name to retrieve this one! 381 | logging.debug("Adding new toplevel") 382 | newelem = UI(eval(uiname), **kwargs) 383 | fakeroot.append(newelem) 384 | newelem.makeelem() 385 | newelem.elem.bind("", click) 386 | newelem.elem.bind("", drag) 387 | 388 | def delwidget(self, *args): 389 | widget = self.tree.wselection()[0] 390 | widget.parent.remove(widget) 391 | 392 | def move_by(self, diff, *args): 393 | widget = self.tree.wselection()[0] 394 | logging.debug("Moving %s by %s" % (widget, diff)) 395 | newindex = widget.parent.move_by(widget, diff) 396 | 397 | def reparent(self, direction, *args): 398 | widget = self.tree.wselection()[0] 399 | logging.debug("reparenting %s", widget) 400 | if direction == "left": 401 | newparent = widget.parent.parent 402 | newindex = newparent.index(widget.parent) + 1 403 | elif direction == "right": 404 | parent = widget.parent 405 | newparent = sibling = parent[parent.index(widget) - 1] 406 | newindex = len(widget.parent) 407 | widget.setparent(newparent, newindex) 408 | recursive_lift(widget) 409 | 410 | def gencode(root=None, filename=None, prefix="uiroot = "): 411 | root = uiroot if root is None else root 412 | if filename is None: 413 | filename = os.path.join(os.path.dirname(__file__), "generated_tree.py") 414 | code = "%s%s" % (prefix, root.code(len(prefix)).strip()) 415 | open(filename, "w").write(code) 416 | 417 | def uneval(v): 418 | if type(v) in [int, float, str]: 419 | return repr(v) 420 | elif hasattr(v, "__call__"): 421 | return v.__name__ 422 | else: 423 | return "%s()" % v.__class__.__name__ 424 | 425 | def update_param(event, _dict, *args): 426 | logging.debug("update %s %s %s", event, _dict, args) 427 | if event == "set": 428 | key, oldvalue = args 429 | widget = uidict["tree"].wselection()[0] 430 | widget.update(key, eval(_dict[key])) 431 | elif event == "create": 432 | key, value, oldvalue = args 433 | widget = uidict["tree"].wselection()[0] 434 | widget.update(key, eval(_dict[key])) 435 | 436 | def select_callback(event): 437 | selection = event.widget.wselection() 438 | if uidict["autoparam"].value and selection: 439 | widget = selection[0] 440 | uidict["params"]._dict.replace(widget.kwargs) 441 | 442 | def selection(): 443 | return uidict["tree"].wselection() 444 | 445 | def recursive_lift(widget): 446 | if widget.elemtype.__name__ == "Canvas": 447 | tk.Misc.lift(widget.elem) 448 | else: 449 | widget.elem.lift() 450 | for child in widget: 451 | recursive_lift(child) 452 | 453 | def click(event): 454 | global startdragtime 455 | widget = event.widget 456 | logging.debug("clicked at %s %s on top of %s", event.x, event.y, widget) 457 | logging.debug("%s %s", type(widget), widget.__class__) 458 | key = [k for k, v in uidict["tree"].widget.items() 459 | if getattr(v, "elem", None) == widget][0] 460 | uidict["tree"].selection_set(key) 461 | #recursive_lift(widget.ui) 462 | startdragtime = time.time() 463 | return "break" 464 | 465 | startdragtime = 0 466 | dragdelay = 0.5 467 | 468 | def xy(event, widget=None): 469 | if widget == None: 470 | widget = event.widget 471 | x = event.x_root - widget.ui.toplevel.winfo_rootx() 472 | y = event.y_root - widget.ui.toplevel.winfo_rooty() 473 | return (x, y) 474 | 475 | def drag(event): 476 | if time.time() - startdragtime > dragdelay: 477 | widget = uidict["tree"].wselection()[0] 478 | #widget.elem.pack_forget() 479 | x, y = xy(event, widget.elem) 480 | recursive_lift(widget) 481 | widget.elem.place(x=x, y=y) 482 | 483 | def mark(event): 484 | x, y = xy(event) 485 | logging.debug("mark %s %s", x, y) 486 | marker = UI(Entry, defaulttext = str(len(uidict["markers"].ui)), 487 | bg="blue", fg="white", autosize=True) 488 | uidict["markers"].ui.add(marker) 489 | marker.elem.place(x=x, y=y) 490 | 491 | def simplify(selection): 492 | if type(selection) == dict: 493 | selection = selection.values() 494 | if len(selection) == 1: 495 | return selection[0] 496 | return selection 497 | 498 | def markereval(marker): 499 | xy = (marker.winfo_rootx(), marker.winfo_rooty()) 500 | marker.lower() 501 | widget = marker.winfo_containing(*xy) 502 | marker.lift() 503 | if widget is None: 504 | return None 505 | if isinstance(widget, BoxedList): 506 | return simplify(widget.lselection()) 507 | elif isinstance(widget, Entry): 508 | # Should this be evaluated? 509 | return entry.text 510 | elif isinstance(widget, BoxedTree): 511 | return simplify(widget.wselection()) 512 | 513 | def markerargs(): 514 | kwargs = {} 515 | for marker in uidict["markers"].ui: 516 | kwargs[marker.elem.text] = markereval(marker.elem) 517 | numkeys = [int(key) for key in kwargs if key.isdigit()] 518 | args = [None for i in xrange(max(numkeys + [-1]) + 1)] 519 | for key in numkeys: 520 | args[key] = kwargs.pop(str(key)) 521 | return args, kwargs 522 | 523 | def marker_info(event): 524 | x = event.x_root - event.widget.ui.toplevel.winfo_rootx() 525 | y = event.y_root - event.widget.ui.toplevel.winfo_rooty() 526 | marker = uidict["markers"].ui[0].elem 527 | xy = (marker.winfo_rootx() - marker.ui.toplevel.winfo_rootx(), 528 | marker.winfo_rooty() - marker.ui.toplevel.winfo_rooty()) 529 | widget = marker.winfo_containing(*xy) 530 | logging.debug("marker info: %s", (marker.winfo_geometry(), marker.winfo_ismapped(), marker.winfo_manager())) 531 | 532 | def setfonts(): 533 | for fontname in tkFont.names(): 534 | default_font = tkFont.nametofont(fontname) 535 | default_font.configure(size=14) 536 | 537 | # Get all widgets from Tkinter 538 | tkuilist = ["tk.%s" % e for e in dir(tk) if e.capitalize() == e and 539 | len(e) > 2 and not e.startswith("_")] 540 | tkuilist += ["Entry", "BoxedBool", "BoxedList", "BoxedDict", "BoxedTree"] 541 | tkuilist = observed_list(tkuilist) 542 | 543 | if __name__ == "__main__": 544 | #logging.basicConfig(level=logging.DEBUG) 545 | 546 | from Tkinter import Tk, Label, Frame, Button, Canvas 547 | histfile = "tkui_history" 548 | terp = TkTerp(histfile) 549 | 550 | def quit(): 551 | logging.info("Shutting down...") 552 | terp.save() 553 | uidict["root"].destroy() 554 | 555 | execfile(os.path.join(os.path.dirname(__file__), "functions.py")) 556 | execfile(os.path.join(os.path.dirname(__file__), "generated_tree.py")) 557 | uiroot.makeelem() 558 | 559 | uidict["uilist"].reset(tkuilist) 560 | paramdict = observed_dict({"test": 3, "foo": "bar"}) 561 | uidict["params"].reset(paramdict) 562 | uidict["bind"].reset(bindlist) 563 | uidict["history"].reset(terp.history) 564 | 565 | fakeroot = observed_tree() 566 | fakeroot.append(uiroot) 567 | fakeroot.elem = None 568 | uidict["tree"].reset(fakeroot) 569 | #uidict["tree"].reset(uiroot) 570 | tree = uidict["tree"] 571 | style = ttk.Style(uidict["root"]) 572 | style.configure('Treeview', rowheight=14*2) 573 | tree.column("#0", minwidth=0, width=800) 574 | uitree = UITree(uiroot, tree) 575 | uidict["tree"].bind('<>', select_callback) 576 | uidict["params"]._dict.callbacks.append(update_param) 577 | 578 | for marker in uidict["markers"].ui: 579 | marker.elem.place_forget() 580 | 581 | undolog = UndoLog() 582 | undolog.add(uiroot) 583 | uidict["undolist"].reset(undolog.root) 584 | 585 | setfonts() 586 | 587 | terp.bind_keys(uidict['exec']) 588 | uidict["root"].protocol("WM_DELETE_WINDOW", quit) 589 | uidict["root"].bind("", click) 590 | uidict["root"].bind("", drag) 591 | uidict['root'].bind('', mark) 592 | uidict["root"].bind("", startrect) 593 | uidict["root"].bind("", moverect) 594 | uidict["root"].bind("", endrect) 595 | #uidict["root"].bind("", releasecb) 596 | #print "BINDINGS", uidict["root"].event_info('<>') 597 | #Cannot get list of bound events, have to track them ourselves. 598 | #uiroot[1][1][1].elem.config(command = lambda e: None) 599 | uidict["root"].mainloop() 600 | -------------------------------------------------------------------------------- /tkui/functions.py: -------------------------------------------------------------------------------- 1 | def addwidget(*args): 2 | uitree.addwidget(*args, kwargs=eval(uidict["child params"].text or "{}")) 3 | 4 | def addchild(*args): 5 | uitree.addwidget(location="child", *args) 6 | 7 | def delwidget(*args): 8 | uitree.delwidget(*args) 9 | 10 | def move_up(*args): 11 | undolog.start_group("move") 12 | uitree.move_by(-1, *args) 13 | undolog.end_group("move") 14 | 15 | def move_down(*args): 16 | undolog.start_group("move") 17 | uitree.move_by(1, *args) 18 | undolog.end_group("move") 19 | 20 | def move_left(*args): 21 | undolog.start_group("move") 22 | uitree.reparent("left", *args) 23 | undolog.end_group("move") 24 | 25 | def move_right(*args): 26 | undolog.start_group("move") 27 | uitree.reparent("right", *args) 28 | undolog.end_group("move") 29 | 30 | def undo(*args): 31 | undolog.undo() 32 | 33 | def redo(*args): 34 | undolog.redo() 35 | 36 | def reload(): 37 | execfile(os.path.join(os.path.dirname(__file__), "functions.py"), globals()) 38 | 39 | def printmarker(): 40 | args, kwargs = markerargs() 41 | kwargstr = ["%s = %s" % (k, v) for k, v in kwargs.items()] 42 | print "f(%s)" % ", ".join(map(str, args + kwargstr)) 43 | 44 | def startrect(event): 45 | global startxy 46 | x, y = xy(event) 47 | startxy = x, y 48 | uidict["rectangle"].place(x=x, y=y, anchor="nw") 49 | tk.Misc.lift(uidict["rectangle"]) 50 | uidict["rectangle"]["width"] = 0 51 | uidict["rectangle"]["height"] = 0 52 | 53 | def moverect(event): 54 | x, y = xy(event) 55 | uidict["rectangle"]["width"] = x - startxy[0] 56 | uidict["rectangle"]["height"] = y - startxy[1] 57 | 58 | def box(widget): 59 | return (widget.winfo_x(), widget.winfo_y(), 60 | widget.winfo_width(), widget.winfo_height()) 61 | 62 | def contains(box1, box2): 63 | x1, y1, w1, h1 = box1 64 | x2, y2, w2, h2 = box2 65 | return x1 <= x2 <= x2 + w2 <= x1 + w1 and y1 <= y2 <= y2 + h2 <= y1 + h1 66 | 67 | def endrect(event): 68 | tk.Misc.lower(uidict["rectangle"]) 69 | uidict["tree"].selection_set_by_items(rectselection()) 70 | 71 | def rectselection(): 72 | rbox = box(uidict["rectangle"]) 73 | toplevel = uidict["rectangle"].ui.toplevel.ui 74 | def condition(widget): 75 | return contains(rbox, box(widget.elem)) 76 | return [elem for elem in toplevel.tops(condition) if not elem == uidict["rectangle"].ui] 77 | -------------------------------------------------------------------------------- /tkui/generated_tree.py: -------------------------------------------------------------------------------- 1 | uiroot = UI(Tk, packanchor='n', name='root', title='TkUIMaker', children=[ 2 | UI(Frame, packside='left', children=[ 3 | UI(Frame, packside='top', children=[ 4 | UI(BoxedList, name='history'), 5 | UI(Frame, packside='left', children=[ 6 | UI(Label, text='Exec: '), 7 | UI(Entry, defaulttext='', name='exec')]), 8 | UI(Frame, packside='left', children=[ 9 | UI(Button, text='Generate UI code', command=gencode), 10 | UI(Button, text='Reload', command=reload), 11 | UI(Button, text='Printmarker', command=printmarker)])]), 12 | UI(Frame, packside='top', children=[ 13 | UI(BoxedList, width=12, name='uilist'), 14 | UI(Frame, packside='left', children=[ 15 | UI(Entry, width=10, name='child params'), 16 | UI(Button, text='+', command=addwidget), 17 | UI(Button, text='+c', command=addchild), 18 | UI(Button, text='-', command=delwidget)])]), 19 | UI(Frame, packside='top', children=[ 20 | UI(BoxedList, width=12, name='undolist'), 21 | UI(Frame, packside='left', children=[ 22 | UI(Button, text='undo', command=undo), 23 | UI(Button, text='redo', command=redo)])])]), 24 | UI(Frame, packside='left', children=[ 25 | UI(BoxedTree, name='tree'), 26 | UI(Frame, packside='top', children=[ 27 | UI(Frame, packside='left', children=[ 28 | UI(Button, text='^', command=move_up), 29 | UI(Button, text='v', command=move_down), 30 | UI(Button, text='<', command=move_left), 31 | UI(Button, text='>', command=move_right)]), 32 | UI(BoxedBool, text='Auto update', name='autoparam'), 33 | UI(BoxedDict, name='params'), 34 | UI(BoxedList, name='bind')])]), 35 | UI(Frame, geometry='place', name='markers'), 36 | UI(Frame, geometry='place', name='overlay', children=[ 37 | UI(Canvas, width=0, bg='red', name='rectangle', height=0)])]) -------------------------------------------------------------------------------- /tour_functions.py: -------------------------------------------------------------------------------- 1 | def addwidget(*args): 2 | uitree.addwidget(*args, kwargs=eval(uidict["child params"].text or "{}")) 3 | 4 | def addchild(*args): 5 | uitree.addwidget(location="child", *args) 6 | 7 | def delwidget(*args): 8 | uitree.delwidget(*args) 9 | 10 | def move_up(*args): 11 | undolog.start_group("move") 12 | uitree.move_by(-1, *args) 13 | undolog.end_group("move") 14 | 15 | def move_down(*args): 16 | undolog.start_group("move") 17 | uitree.move_by(1, *args) 18 | undolog.end_group("move") 19 | 20 | def move_left(*args): 21 | undolog.start_group("move") 22 | uitree.reparent("left", *args) 23 | undolog.end_group("move") 24 | 25 | def move_right(*args): 26 | undolog.start_group("move") 27 | uitree.reparent("right", *args) 28 | undolog.end_group("move") 29 | 30 | def undo(*args): 31 | undolog.undo() 32 | 33 | def redo(*args): 34 | undolog.redo() 35 | 36 | def reload(): 37 | execfile("functions.py", globals()) 38 | 39 | def printmarker(): 40 | args, kwargs = markerargs() 41 | kwargstr = ["%s = %s" % (k, v) for k, v in kwargs.items()] 42 | print "f(%s)" % ", ".join(map(str, args + kwargstr)) 43 | 44 | def startrect(event): 45 | global startxy 46 | x, y = xy(event) 47 | startxy = x, y 48 | uidict["rectangle"].place(x=x, y=y, anchor="nw") 49 | tk.Misc.lift(uidict["rectangle"]) 50 | uidict["rectangle"]["width"] = 0 51 | uidict["rectangle"]["height"] = 0 52 | 53 | def moverect(event): 54 | x, y = xy(event) 55 | uidict["rectangle"]["width"] = x - startxy[0] 56 | uidict["rectangle"]["height"] = y - startxy[1] 57 | 58 | def box(widget): 59 | return (widget.winfo_x(), widget.winfo_y(), 60 | widget.winfo_width(), widget.winfo_height()) 61 | 62 | def contains(box1, box2): 63 | x1, y1, w1, h1 = box1 64 | x2, y2, w2, h2 = box2 65 | return x1 <= x2 <= x2 + w2 <= x1 + w1 and y1 <= y2 <= y2 + h2 <= y1 + h1 66 | 67 | def endrect(event): 68 | tk.Misc.lower(uidict["rectangle"]) 69 | uidict["tree"].selection_set_by_items(rectselection()) 70 | 71 | def rectselection(): 72 | rbox = box(uidict["rectangle"]) 73 | toplevel = uidict["rectangle"].ui.toplevel.ui 74 | def condition(widget): 75 | return contains(rbox, box(widget.elem)) 76 | return [elem for elem in toplevel.tops(condition) if not elem == uidict["rectangle"].ui] 77 | 78 | def save(): 79 | gencode(uiroot, "my_tour.py") 80 | -------------------------------------------------------------------------------- /tour_text.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Step = namedtuple("Step", "message condition post hint") 4 | 5 | def step1(uiroot): 6 | uidict["embark"].ui.parent.remove(uidict["embark"].ui) 7 | uiroot[0][0].append(uidict['tkterp'].ui) 8 | uidict["hint frame"].ui.parent.repack() 9 | 10 | def step2(uiroot): 11 | uidict["uilist"].reset(tkuilist) 12 | uidict["tree"].selection_set_by_items([uidict["uilistframe"].ui]) 13 | 14 | def step3(uiroot): 15 | uidict["child params"].text = '{"name": "history"}' 16 | 17 | def step4(uiroot): 18 | uidict["child params"].text = '' 19 | 20 | steps = [Step(""" 21 | Welcome to the Inquisitive Platypus! I will be your guide today on your passage to Tkui. Once everyone is in and comfortably seated, we can begin our journey. 22 | """, None, step1, []), 23 | Step(""" 24 | Before we arrive on Tkui, let's review some safety precautions. 25 | 26 | I will give each of you a portable interpreter, a "TkTerp" in the local dialect. If you ever get lost or get into a bind, you can always type Python commands in there to be evaluated. 27 | 28 | Try typing 29 | 30 | 1+1 31 | 32 | in there now and pressing enter. You should see the output in your console or terminal. 33 | 34 | You can use the arrow keys in the TkTerp to browse your command history. 35 | 36 | Once you're done familiarizing yourself with your TkTerp, type 37 | 38 | tkguide.next() 39 | 40 | in it. `tkguide.prev()` lets you go back a step. 41 | 42 | You will use your TkTerp a lot in the beginning but less and less as we see more places. 43 | 44 | [If you need a hint, check the "Hint" checkbox to see a hint of what to type next. Press enter inside the Hint's TkTerp to run the hinted command and see the next command.] 45 | """, None, None, ["1+1", "tkguide.next()"]), 46 | 47 | Step(""" 48 | First up is the uilist. Any UI maker will want to add and remove elements from their UI and that is all done here. Um...hmm...I'm sure it was supposed to be here. Well, we can't be far. 49 | 50 | [If you think the uilist should **east**, type 51 | 52 | uiroot[0].append(uilist) 53 | 54 | in your TkTerp and if you think its **west** of here, type 55 | 56 | uiroot[0].insert(0, uilist) 57 | 58 | in your TkTerp.] 59 | """, "uilist", step2, ["uiroot[0].append(uilist)"]), 60 | 61 | Step(""" 62 | Right, here we are. So as I was saying. 63 | 64 | To add an element, select its type from the list and click on the "+" button. Try adding an Entry now. 65 | 66 | New elements are added as the next sibling of the **currently selected** element. To select an element, right click on it. 67 | 68 | To delete an element, right click to select it and click the "-" button. 69 | 70 | You can also use your TkTerp to do the same. Just treat element containers like tk.Frame and tk.Toplevel like lists. 71 | 72 | To add, 73 | 74 | uiroot[0].append(UI(Entry, defaulttext='Hello world')) 75 | 76 | To remove 77 | 78 | uiroot[0].pop() 79 | 80 | We will come back to the language of the Tkuis eventually. 81 | 82 | Type `tkguide.next()` when you are done. 83 | """, None, step3, 84 | """uiroot[0].append(UI(Entry, defaulttext='Hello world')) 85 | uiroot[0].pop() 86 | tkguide.next()""".split("\n")), 87 | 88 | Step(""" 89 | Take a moment to notice the structure which houses the uilist. Its a modern-era tkui.BoxedList. 90 | 91 | Any changes to the list this element is reflected visually. Try adding an element to it using your TkTerp now. 92 | 93 | uidict["uilist"]._list.append("BoxedList") 94 | 95 | Then try removing and putting it back. 96 | 97 | uidict["uilist"]._list.pop() 98 | 99 | For easier reference, let's name this list 100 | 101 | tkuilist = uidict["uilist"]._list 102 | tkuilist.append("BoxedList") 103 | 104 | and make our first BoxedList. Select it from the uilist, right click an element you want the list to be placed next to and then click "+". 105 | 106 | The element created get passed the dictionnary to the left of the "+" button. Here we are setting its name to "history" for reference by uidict later. 107 | """, "history", step4, 108 | """uidict["uilist"]._list.append("BoxedList") 109 | uiroot[0].insert(1, UI(tkui.BoxedList, name="history"))""".split("\n")), 110 | 111 | Step(""" 112 | Let's set it to track your TkTerp's command history. 113 | 114 | uidict["history"].reset(terp.history) 115 | 116 | You can now add the full list of available elements by typing 117 | 118 | uidict["uilist"]._list.extend(tkui.tkuilist) 119 | 120 | Type `tkguide.next()` when you are done. 121 | 122 | If you are tired of typing `tkguide.next()` (or finding it in your history), you can add a button to call that function. Add a Button with these parameters 123 | 124 | {"command": tkguide.next, "text": "next"} 125 | 126 | *or* create the button and add it through the interpreter. 127 | 128 | UI(Button, command=tkguide.next, text="next") 129 | 130 | (Either `append` or `insert` this to some node. A node can be reference either by multiple indices from `uiroot` or `uielem`.) 131 | """, None, None, 132 | """uidict["history"].reset(terp.history) 133 | uidict["uilist"]._list.extend(tkui.tkuilist) 134 | tkguide.next()""".split("\n")), 135 | 136 | Step(""" 137 | Up ahead is the Tkuian forest. 138 | 139 | [Add the `tkuitree` anywhere, such as with] 140 | 141 | uiroot[0].append(tkuitree) 142 | 143 | As you can see in front of you is a BoxedTree. Within it, we can see all elements. As an everlight, it highlights the currently selected element all year round. 144 | 145 | Try right clicking on some elements and observe how its highlight changes. The tree is tall so you might have to look up and down by scrolling the scroll wheel inside the tree. You can also left click an item in the tree to select them. 146 | 147 | Type `tkguide.next()` when you are done. 148 | """, None, None, 149 | """uiroot[0].append(tkuitree) 150 | tkguide.next() 151 | """.split("\n")), 152 | 153 | Step(""" 154 | Now's the perfect time to take a lunch break and record our journey. 155 | 156 | gencode(uiroot, "my_tour.py") 157 | 158 | which can be later recalled using 159 | 160 | python guided_tour.py my_tour.py 161 | 162 | instead of `python guided_tour.py`. This only loads the layout of the UI, their content need to be filled again with 163 | 164 | tkguide.jump_to(7) 165 | uidict["history"].reset(terp.history) 166 | uidict["uilist"]._list.extend(tkui.tkuilist) 167 | 168 | (Remember the first line to be able to get to these instructions to follow them later.) 169 | 170 | For convenience, add a button to do that. 171 | 172 | UI(tk.Button, text="save", command=save) 173 | 174 | Use a tex editor to edit (or add) the function `save` in `tour_functions.py` to 175 | 176 | def save(): 177 | gencode(uiroot, "my_tour.py") 178 | 179 | Save `tour_functions.py` and reload it 180 | 181 | execfile("tour_functions.py") 182 | 183 | Type `tkguide.next()` when you are done. 184 | """, None, None, 185 | """gencode(uiroot, "my_tour.py") 186 | uiroot[0][0].append(UI(tk.Button, text="save", command=save)) 187 | execfile("tour_functions.py") 188 | tkguide.next()""".split("\n")), 189 | 190 | Step(""" 191 | Now I know there are many UI enthousiats that come to Tkui and for the ones among us today, so while you see if you like the Tkui cuisine, let me say a few words about that. 192 | 193 | The idea of Tkui is to make the UI *and* the UI maker at the same time. Or rather, just make a single UI with the parts intended users of your application contained in a tk.Frame or tk.Toplevel and when you are done, generate the code for that tk.Frame or tk.Toplevel (using `gencode(elem, "some_file.py")`). 194 | 195 | But honestly, if you ask me, why not keep the entire editor but keep it invisible until the user clicks some "edit" button? What if the user just wants to change the button layout a bit? Why ask them to recompile or even restart your program? 196 | 197 | How about we all add a canvas for an UI? 198 | 199 | UI(tk.Frame, name="user root") 200 | 201 | Even if you are not interested in UI making, you could put some souvenirs in there to bring back. 202 | """, None, None, 203 | """uiroot[0].insert(0, UI(tk.Frame, name="user root")) 204 | tkguide.next()""".split("\n")), 205 | 206 | Step(""" 207 | We're approaching the center of Tkui's jungle. To your right is a forest rea-ranger's cabin. Four buttons attached to the `move_up`, `move_down`, `move_left` and `move_right` functions lets them easily rearrange elements in the UI tree. 208 | 209 | Add these buttons now. Feel free to pick different text and add a Frame around them. 210 | 211 | UI(Button, text='^', command=move_up) 212 | UI(Button, text='v', command=move_down) 213 | UI(Button, text='<', command=move_left) 214 | UI(Button, text='>', command=move_right) 215 | 216 | Select an element (using either right-click or the UI tree) and click the up (`^`) or down (`v`) button. 217 | 218 | Left "dedents" the selected element in the tree and right (`>`) "indents" it. 219 | 220 | Rearrange the tree as you like and then `tkguide.next()` to move forward. 221 | """, None, None, 222 | """uiroot[0][1].append(UI(Frame, name="rearrange", packside="left")) 223 | uidict["rearrange"].ui.append(UI(Button, text='^', command=move_up)) 224 | uidict["rearrange"].ui.append(UI(Button, text='v', command=move_down)) 225 | uidict["rearrange"].ui.append(UI(Button, text='<', command=move_left)) 226 | uidict["rearrange"].ui.append(UI(Button, text='>', command=move_right)) 227 | tkguide.next()""".split("\n")), 228 | 229 | Step(""" 230 | This brings us to the abrupt end of our tour. There are still many parts of tkui we haven't seen. If you'd like to know when our evening and other routes will be available, please send and e-mail to 231 | 232 | asrp email com (with an @ and . added between) 233 | 234 | to be added to our newsletter. 235 | """, None, None, [""]) 236 | ] 237 | 238 | [""" 239 | The massive contraption facing us is a water clock and they have quite a bit of history. 240 | 241 | [Author's note: Undo/redo very much not fully tested.] 242 | """] 243 | --------------------------------------------------------------------------------