import tkinter as tk import time import os import copy from tkinter import colorchooser as coldialogue from tkinter import simpledialog as dialogue from tkinter import filedialog as filedialogue from tkinter import messagebox import tkinter.font as tkf from math import atan2, pi class node: def __init__(self, s, w, h, co = "#000000", ch = -999, pa = -999): self.text = s self.textsize = (h, w) self.colour = co if ch == -999: ch = set() self.children = ch if pa == -999: pa = set() self.parents = pa def change(self, text = -999, textsize = -999, colour = -999, children = -999, parents = -999): if text != -999: self.text = text if textsize == -999: self.textsize = measure_text(text) if textsize != -999: self.textsize = textsize if colour != -999: self.colour = colour if children != -999: self.children = children if parents != -999: self.parents = parents return self def copy(self): return node(self.text, self.textsize[1], self.textsize[0], self.colour, copy.copy(self.children), copy.copy(self.parents)) def __str__(self): return ("node(" + self.text + ", size = " + str(self.textsize) + ", c = " + self.colour + ", ch = " + str(self.children) + ", pa " + str(self.parents) + ")") def __repr__(self): return self.__str__() def idmap(n): return { a: a for a in range(0, n) } def start(): global win, winw, winh, ca, text, ptext, waiting, defdim, defsz, clip, edge, pad, hs, vs, disp, undos, redos, savedas print("\n\nnew run") win = tk.Tk() win.bind("", left_click) win.bind("", repeat_macro_cb) win.bind("", control_down_cb) win.bind("", control_left_cb) win.bind("", control_right_cb) win.bind("", control_up_cb) win.bind("", down_cb) win.bind("", cancel_click) win.bind("", left_cb) win.bind("", right_cb) win.bind("", shift_down_cb) win.bind("", shift_left_cb) win.bind("", shift_right_cb) win.bind("", shift_up_cb) win.bind("", up_cb) win.bind("A", reread_macro_cb) win.bind("a", obey_macro_cb) win.bind("b", list_nodes_cb) win.bind("c", copy_subtree_cb) win.bind("d", delnode_cb) win.bind("e", remove_columns_rows_cb) win.bind("f", fake_macro_cb) win.bind("H", show_macro_help_cb) win.bind("h", show_help_cb) win.bind("i", insert_cb) win.bind("j", join_nodes_cb) win.bind("k", cut_subtree_cb) win.bind("l", split_node_cb) win.bind("m", move_subtree_cb) win.bind("n", move_node_cb) win.bind("o", read_file_cb) win.bind("p", paste_subtree_cb) win.bind("q", query_cb) win.bind("r", change_colour_cb) win.bind("s", write_file_cb) win.bind("t", change_text_cb) win.bind("u", unjoin_nodes_cb) win.bind("x", swap_cb) win.bind("y", redo_cb) win.bind("z", undo_cb) win.protocol("WM_DELETE_WINDOW", close_window_cb) winw = win.winfo_screenwidth() - 50 winh = win.winfo_screenheight() - 100 win.geometry(str(winw) + "x" + str(winh) + "+20+20") text = "h: help, t: change text, r: colour, j: join, u: unjoin, i: insert c/r, e: erase c/r, c: copy, d: del node, l: split sub, k: cut, p: paste, m: move sub, n: move node, x: swap, a: macro, z: undo, y: redo, o: open, s: save" ptext = "h: help, e: erase c/r, a: macro, z: undo, y: redo, o: open, s: save" disp = tk.StringVar(value = ptext) tlab = tk.Label(win, textvariable = disp, anchor = "w") tlab.grid(row = 0, column = 0, sticky = "ew") ca = tk.Canvas(win, width = winw - 40, height = winh - 40, background = "white") ca.grid(row = 1, column = 0) hs = tk.Scrollbar(win, orient = "horizontal", width = 20, command = ca.xview) hs.grid(row = 2, column = 0, sticky = "ew") vs = tk.Scrollbar(win, orient = "vertical", width = 20, command = ca.yview) vs.grid(row = 1, column = 1, sticky = "ns") ca.configure(xscrollcommand = hs.set) ca.configure(yscrollcommand = vs.set) win.after(1, lambda: win.focus_force()) win.update() edge = 20 defsz = 10 waiting = None clip = [] pad = [5, 5] defdim = (winh // (defsz + 2 * pad[0]) - 3, winw // (defsz + 2 * pad[1]) - 2) undos = [] redos = [] savedas = None clear() def clear(): global nodes, active, vpositions, edge, defdim, defsz, sizes, independents, haschanged, title, filename global everchanged, origin, showing_help, undoing, swapping, macros, last_macro sizes = [[ defsz ] * defdim[0], [ defsz ] * defdim[1]] nodes = {} vpositions = [[set() for i in range(0, defdim[0])], [set() for i in range(0, defdim[1])]] active = origin = (-1, -1) independents = node("***all***", 0, 0) nodes[(-2, -2)] = independents filename = "" haschanged = False everchanged = False showing_help = False title = "Tree Experiments" undoing = False swapping = [] macros = {} last_macro = None win.title(title) def error(* a): set_text() m = "".join([ w if type(w) == str else str(w) for w in a ]) messagebox.showerror("Trouble!", m) def close_window_cb(): global win, savedas if savedas != None: os.remove(savedas) win.destroy() win = None def special_split(s): pts = [] pos = 0 cur = "" end = len(s) s += " " while pos <= end: while pos <= end: if s[pos] == " ": pos += 1 else: break if pos > end: break if s[pos] == "\"": pos += 1 while pos <= end: if s[pos] == "\"": pts.append(cur) cur = "" pos += 1 break else: cur += s[pos] pos += 1 else: cur = s[pos] pos += 1 while pos <= end: if s[pos] == " ": pts.append(cur) cur = "" pos += 1 break else: cur += s[pos] pos += 1 return pts def getrc(r, c = -1): if c == -1: return (r[0], r[1], tuple(r)) else: return (r, c, (r, c)) def numin(ind, which): global vpositions ps = vpositions[ind] if which < 0 or which >= len(ps): return 0 return len(ps[which]) def add_new_sizes(ind, which, d): global sizes sz = sizes[ind] sz[which] = max(sz[which], d, defsz) def changed(hasit = True): global win, filename, title, haschanged, everchanged if hasit: if not haschanged: win.title(title + " (CHANGES NOT SAVED) " + filename) haschanged = True everchanged = True elif not hasit: if everchanged: win.title(title + " (saved) " + filename) else: win.title(title + filename) haschanged = False def recalc_sizes(ind, which): global sizes, vpositions, nodes, defsz sz = sizes[ind] ps = vpositions[ind] if which >= len(sz): error("recalc sizes ", (ind, which), " index >= size ", len(sz)) return big = defsz for i in ps[which]: if ind == 0: pos = (which, i) else: pos = (i, which) if pos not in nodes: error("recalc sizes ", (ind, which), " non-existent node ", pos) return n = nodes[pos] big = max(big, n.textsize[ind]) if sz[which] != big: sz[which] = big def set_text(s = -9999): global text, ptext, disp, active if s == -9999: s = text if active[0] < 0: s = ptext else: s += ", active (" + str(active[0]) + "," + str(active[1]) + ")" disp.set(s) def measure_text(s): global ca t = ca.create_text(10, 10, text = s, anchor = "nw", fill = "red", font = ("courier new", -20, "bold")) b = ca.bbox(t) h = b[3] - b[1] if h & 1: h += 1 w = b[2] - b[0] if w & 1: w += 1 ca.delete(t) return (h, w) def make_node(normal, r, c, s, col = "#000000", ch = -999, pa = -999): global ca, nodes, vpositions, independents if ch == -999: ch = set() if pa == -999: pa = set() (h, w) = measure_text(s) arc = (r, c) if arc in nodes: del nodes[arc] n = node(s, w, h, col, ch, pa) if normal: nodes[arc] = n add_new_sizes(1, c, w) add_new_sizes(0, r, h) vpositions[0][r].add(c) vpositions[1][c].add(r) if len(pa) == 0: independents.children.add((r, c)) n.change(parents = {(-2, -2)}) changed() return n def killnode(r, c = -1): global nodes (r, c, rc) = getrc(r, c) if rc in nodes: n = nodes[rc] del nodes[rc] for rci in n.children: nodes[rci].parents.discard(rc) for rci in n.parents: nodes[rci].children.discard(rc) vpositions[0][r].discard(c) vpositions[1][c].discard(r) changed() def execute_swap(sw): un = [] s = nodes[sw[0]].text un.append(("t", sw[0], s)) for i in range(1, len(sw)): ns = nodes[sw[i]].text set_node_text(sw[i], s) un.append(("t", sw[i], ns)) s = ns set_node_text(sw[0], s) swapping = [] waiting = None return un def swap_cb(e): global active, waiting, swapping, undos if waiting == "swap": un = execute_swap(swapping) undos.append(un) set_text() changed() draw("swap") return if tuple(active) not in nodes: return swapping = [ tuple(active) ] set_text("swapping " + str(tuple(active)) + " click more or press x when done") waiting = "swap" def text_in_box(n, box): global ca, pad col = n.colour (x0, y0, x1, y1) = box midx = (x0 + x1) // 2 midy = (y0 + y1) // 2 adjx = (n.textsize[1] + 1) // 2 adjy = (n.textsize[0] + 1) // 2 lx = midx - adjx ty = midy - adjy rx = midx + adjx by = midy + adjy try: it = ca.create_line(lx, ty, rx, ty, width = 2, fill = col) ca.delete(it) except BaseException: print("invalid colour", col, "changed to black") col = "black" n.colour = col ca.create_text(midx, midy, text = n.text, anchor = "center", fill = col, font = ("courier new", -20, "bold")) ca.create_line(lx, ty, rx, ty, width = 2, fill = col) ca.create_line(lx, by, rx, by, width = 2, fill = col) ca.create_line(lx, ty, lx, by, width = 2, fill = col) ca.create_line(rx, ty, rx, by, width = 2, fill = col) return (lx, ty, rx, by) drawcount = 0 def bigger(a, b): global origin chra = a[0] - origin[0] chca = a[1] - origin[1] chrb = b[0] - origin[0] chcb = b[1] - origin[1] if chra * chcb < chrb * chca: return True else: return False def heapsort(st, fn): hp = [] if len(st) < 2: return list(st) for t in st: pos = len(hp) hp.append(t) while pos > 0: pa = (pos - 1) // 2 if fn(t, hp[pa]): (hp[pos], hp[pa]) = (hp[pa], hp[pos]) else: break pos = pa end = len(hp) - 1 while end != 0: (hp[0], hp[end]) = (hp[end], hp[0]) pos = 0 while True: ch1 = pos * 2 + 1 ch2 = ch1 + 1 if ch1 >= end: break if ch2 >= end: if fn(hp[ch1], hp[pos]): (hp[pos], hp[ch1]) = (hp[ch1], hp[pos]) break if fn(hp[ch2], hp[ch1], ): if fn(hp[pos], hp[ch2]): break (hp[ch2], hp[pos]) = (hp[pos], hp[ch2]) pos = ch2 else: if fn(hp[pos], hp[ch1]): break (hp[ch1], hp[pos]) = (hp[pos], hp[ch1]) pos = ch1 end -= 1 return hp def draw(s): global win, ca, sizes, active, pad, drawcount, origin, undos, vpositions drawcount += 1 for i in ca.find_all(): ca.delete(i) grey = "#D0D0D0" rowheight = sizes[0] colwidth = sizes[1] maxw = sum(colwidth) + edge + 2 * len(colwidth) * pad[1] maxh = sum(rowheight) + edge + 2 * len(rowheight) * pad[0] ca.config(scrollregion = (0, 0, maxw + 10, maxh + 10)) ca.create_line(edge, edge, maxw, edge, width = 1, fill = grey) y = edge for r in range(0, len(sizes[0])): ca.create_text(edge // 2, y + rowheight[r] // 2 + pad[0], text = str(r), anchor = "center", fill = grey) y += rowheight[r] + 2 * pad[0] ca.create_line(edge, y, maxw, y, width = 1, fill = grey) ca.create_line(edge, edge, edge, maxh, width = 1, fill = grey) x = edge for c in range(0, len(sizes[1])): ca.create_text(x + colwidth[c] // 2 + pad[1], edge // 2, text = str(c), anchor = "center", fill = grey) x += colwidth[c] + 2 * pad[1] ca.create_line(x, edge, x, maxh, width = 1, fill = grey) if active[1] != -1 and active[0] != -1: (x0, y0, x1, y1) = node_box(active) ca.create_rectangle(x0, y0, x1, y1, fill = "yellow") for rc in nodes: if rc == (-2, -2): continue origin = rc box = node_box(rc) (x0, y0, x1, y1) = box n = nodes[rc] (lx, ty, rx, by) = text_in_box(n, box) ch = heapsort(n.children, bigger) if len(ch) == 1: sep = 0 xpos = (lx + rx) // 2 else: sep = (rx - lx) / (len(ch) - 1) xpos = lx for drc in ch: (dxl, dyt, dxr, dyb) = node_box(drc) if drc in nodes: col = nodes[drc].colour dy = (dyt + dyb - n.textsize[0]) // 2 else: col = "#000000" dy = dyt try: ca.create_line(int(xpos + 0.5), by, (dxl + dxr) // 2, dy, width = 3, fill = col) except BaseException: print("invalid colour", col, "changed to black") col = "black" if drc in nodes: nodes[drc].colour = col ca.create_line(int(xpos + 0.5), by, (dxl + dxr) // 2, dy, width = 3, fill = col) xpos += sep set_text(str(drawcount) + ": " + (text if active[0] >= 0 else ptext)) def split_node_cb(e): global nodes, active, origin, undos tua = tuple(active) if active[0] < 0 or tua not in nodes: error("attempt to split (l) with no active node") return None t0 = nodes[tua].text t1 = t0 t2 = None t3 = t0 parts = nodes[tua].text.split("|") if len(parts) > 1: t1 = parts[0].strip() if len(parts) == 3: t2 = parts[1].strip() t3 = parts[2].strip() else: t3 = parts[1].strip() un = [] on = nodes[tua] set_node_text(tua, t1) un.append(("t", tua, t0)) if (active[0], active[1] + 1) in nodes: shove_up(1, active[1] + 1, 2) un.append(("su", 1, active[1] + 1, 2)) elif (active[0], active[1] + 2) in nodes: shove_up(1, active[1] + 1, 1) un.append(("su", 1, active[1] + 1, 1)) nt = (active[0], active[1] + 2) nn = make_node(True, nt[0], nt[1], t3, on.colour) un.append(("cr", nt)) if t2: mt = (active[0], active[1] + 1) mn = make_node(True, mt[0], mt[1] , t2, on.colour) un.append(("cr", mt)) nn.parents = copy.copy(on.parents) un.append(("op", nt, set())) ocs = {} ops = {} for pt in nn.parents: un.append(("oc", pt, copy.copy(nodes[pt].children))) nodes[pt].children.add(nt) origin = tua ch = heapsort(on.children, bigger) halfway = (len(on.children) + 1) // 2 un.append(("oc", tua, copy.copy(on.children))) on.children = set(ch[0:halfway]) un.append(("oc", (nt[0], nt[1]), copy.copy(nn.children))) nn.children = set(ch[halfway:]) for ct in nn.children: un.append(("op", ct, copy.copy(nodes[ct].parents))) nodes[ct].parents.discard(tua) nodes[ct].parents.add(nt) changed() draw("split node") if e != -999: undos.append(un) return [] return un gen_help = [ "Press ESC to exit from help", "", "Most actions are based on the active grid cell or node, to make a grid cell active click on it,", "to have no active cell click outside the grid. The active cell is always shown in yellow.", "When there is no active cell, only a very limited number of commands are available.", "Commands are given by typing single keys. Left, right, up, down keys change the active cell", "Single node moved by ctrl+arrow, whole subtree moved by shift+arrow", "", "s: save the diagram, the save_as dialogue appears", "o: open (read and display) a saved diagram, the open file dialogue appears", "t: set/change text of the active cell", " t and r create a node if the cell was empty", "r: set colour of active cell. press ENTER alone to toggle between red and black", "j: join (connect with a line) active cell to another cell, click on the other cell (*)", "u: unjoin (remove connection) between active cell and another cell, click on the other cell (*)", " (join and unjoin can connect/disconnect children and parents)", "d: delete a single node (the active one) children become independent trees", "k: (kut) the entire subtree starting at the active node is removed, can be pasted back", "l: split the active node, divide children equally between two parts; if its text contains two", " vertical bars \"|\" the text is split there between the two resultant nodes; if three", " e.g. ab|cd|ef then ab goes to the left part, ef to the right, and cd detached between them", "c: copy - the entire subtree starting at the active node is copied, and can be pasted elsewhere", "p: the last cut or moved subtree is pasted back with its root at the active cell", "m: move - same as cut followed by paste but subtree still visible when choosing new place (*)", "n: move mode - same as m (move) above, but only the active node moves, children stay still (*)", "i: insert rows or columns in the grid. In the dialogue box type l (left of), r (right of),", " a (above), or b (below the active cell) and number of rows/columns to be added, default 1", "e: erase columns and/or rows. In the dialogue box type comma-separated list of r (row) or c", " (column) follwed by range of or single row/column number. e.g. r12-20, c7, c8-10", "x: exchange - select a sequence of nodes and swap their contents", "a: obey macro, ctrl-a: re-obey last macro, shift-A: re-read macro after editing file xxx.mcaro", "f: (fake macro) type macro commands, semicolon separated, in dialogue, obeyed as normal macros", "H: (capital) show help for macro definitions", "q: (query) show details of the active cell", "z: undo - only one file read can be undone", "y: redo", "h: show this help screen again, ESC to go back to diagram", "", "Commands marked (*) may be abandoned before selecting another cell by pressing ESC" ] mac_help = [ "Press ESC to exit from help", "This describes macros, use lower case h instead of capital H for general help", "Macros are defined by creating text files, a macro named abc is defined by the file abc.macro", "commands are on separate lines, their parts are (generally) space separated", "", "outside of a macro the 'a' command opens a dialogue to ask for the name of the macro to", "be run. Optional parameters are space separated and follow the name, double quotes keep spaces", "if a macro file is edited during a session, the capital 'A' command will read the new definition,", "little 'a' uses the old one. control-'a' repeats the last macro that was run.", "undo and redo apply to whole macros, not their individual commands.", "", "inside macros $1, $2, $3 etc are replaced by the first, second, third, etc parameter", "in the following, X1, X2 represent movements, u, d, l, r, c, or p with optional number attached", "meaning up down left right by n cells or nth child or parent (c and p count from 1 not 0)", "", "commands marked (*) used alone operate on the active cell, they may be followed by X1 X2 ...", "movements in which case the cell that is X1 X2 ... away from the active cell is used instead", "", "ac R C: change active cell to be the cell in row R column C", "ac X1 X2 ...: change the active cell by moving from current one", "t T: change the text in the active cell to T. T is the rest of the line, spaces kept", "r C: change the colour of the active cell, C = \"blue\", \"pink\", \"#RRGGBB\", etc", "j X1 X2 ...: join the active node to the one at the position X1 X2 relative to it", " e.g. j d4 l: if active node is row 10 col 20 it is connected to cell at row 14 col 21", "u X1 X2 ...: disconnect nodes, undoes the effect of j", "c: copy the subtree rooted at the active node; no visible change, use p to paste (*)", "k: copy the subtree rooted at the active node; use p to paste it anywhere (*)", "p: the last subtree to have been copied, cut, or moved is pasted back, root at active node (*)", "l: split the active node, new node appears to right, same text, same parents, but children divided", "d: delete the active node, children become independent trees", "i Y1 Y2 ...: add new rows/columns around active cell, a above, b below, l to left, r to right,", " with optional number attached", "m X1 X2 ... (by|to) X3 X4 ...: move entire subtree, X1 X2 ... optional say which subtree to move", " X3 X4 ... say where to move it, \"to\" means relative to active, \"by\" relative to its root", "n X1 X2 ... (by|to) X3 X4 ...: same as m but only one node moved, children and parents kept", "x: followed by comma separated X1 X2 sequences - the texts and colours of the indicated nodes", " are exchanged in a circular swap", "e: followed by e.g c23 c20-25 r9, remove entire rows and/or columns", "o filename: read the previously saved arrangement from named file, current arrangement discarded", "s filename: save current arrangement; if no filename the most recently read file is overwritten", "a N P1 P2 ...: run the macro named N; P1 P2 are optional parameters. 'a' alone repeats last", "A N P1 P2 ...: same as 'a' but rereads macro definition first", "z: undo last action", "y: redo last undone action" ] def add_undo_select(oldactive): global active, undos nact = ("ac", tuple(oldactive)) if len(undos) > 0: if undos[-1][0] == "ac": return [] return [nact] def process_macro_line(s): it = [] for p in s.split(): p = p.strip() if p != "": it.append(p) if len(it) == 0: return it if it[0] == "t" or it[0] == "e": txt = s.lstrip()[1:].lstrip().removesuffix("\n") return [it[0], txt] items = [] for i in it: n = safestrtoint(i) if n == None: items.append(i) else: items.append(n) return items def read_macro(s): global macros try: f = open(s + ".macro") except BaseException: error("Trouble!", "could not open the file \"" + s + ".macro\"") return None all = [] for line in f: it = process_macro_line(line) if len(it) != 0: all.append(it) f.close() macros[s] = all return [] def accumulate_changes(lst): global active, origin, nodes, bigger origin = tuple(active) pos = 0 dr = 0 dc = 0 hadd = hadc = hadp = 0 while True: if pos >= len(lst): if hadd + hadc + hadp != 1: error("can't combine direction child parent") return (0,0) if hadc + hadp > 0: return (origin[0] - active[0], origin[1] - active[1]) else: return (dr, dc) if lst[pos] in ["l", "r", "d", "u", "p", "c"]: seenext = True np = "1" d = lst[pos][0] elif lst[pos][0] in ["l", "r", "d", "u", "p", "c"]: seenext = False d = lst[pos][0] np = lst[pos][1:] elif lst[pos][-1] in ["l", "r", "d", "u", "p", "c"]: seenext = False d = lst[pos][-1] np = lst[pos][0:-1] else: error("bad movement in macro:\n", lst[pos]) return (0, 0) n = safestrtoint(np) if n == None: error("bad movement in macro:\n", lst[pos]) return (0, 0) pos += 1 if seenext and pos < len(lst) and type(lst[pos]) == int: n = lst[pos] pos += 1 if d == "u": hadd = 1 dr -= n elif d == "d": hadd = 1 dr += n elif d == "l": hadd = 1 dc -= n elif d == "r": hadd = 1 dc += n elif d == "c": hadc = 1 n -= 1 if origin not in nodes: error("can't specify child when not at node ", origin) return (0, 0) no = nodes[origin] chs = heapsort(no.children, bigger) if n < 0 or n >= len(chs): error("accessing child ", n, " out of ", len(chs)) return (0, 0) origin = chs[n] elif d == "p": hadp = 1 n -= 1 if origin not in nodes: error("can't specify child when not at node ", origin) return (0, 0) no = nodes[origin] pas = heapsort(no.parents, bigger) if n < 0 or n >= len(pas): error("accessing parent ", n, " out of ", len(pas)) return (0, 0) origin = pas[len(pas) - n - 1] def grow_to_accommodate(r, c): global sizes if r < 0: shove_up(0, 0, - r) elif r >= len(sizes[0]): expand_edge(0, r + 1 - len(sizes[0])) elif c < 0: shove_up(1, 0, - c) elif c >= len(sizes[1]): expand_edge(1, c + 1 - len(sizes[1])) def adjust_active(dr, dc): global active a0 = active[0] + dr if a0 < 0: a0 = 0 a1 = active[1] + dc if a1 < 0: a1 = 0 return [a0, a1] def with_temp_active(parts, f, a): global active oa = active (dr, dc) = accumulate_changes(parts) active = adjust_active(dr, dc) if a == None: k = f() else: k = f(a) active = oa return k def split_parts(parts, wanted): n = len(parts) a = [] b = None c = [] pos = 0 while pos < n: if parts[pos] in wanted: b = parts[pos] pos += 1 break else: a.append(parts[pos]) pos += 1 while pos < n: c.append(parts[pos]) pos += 1 if b == None: return [a] else: return [a, b, c] def obey_macro(lines, params): global active, nodes, undos, last_macro, filename uns = [] for line0 in lines: tua = tuple(active) line = [] for i in line0: if len(i) > 1 and i[0] == "$": n = safestrtoint(i[1:]) if n != None: if n < 1: error("using parameter ", i, ", they start at 1") return None elif n > len(params): error("using parameter ", i, " when ", len(params), " were provided") return None else: i = params[n - 1] line.append(i) c = line[0] k = [] if c == "ac": k = add_undo_select(tua) if len(line) == 3 and type(line[1]) == int and type(line[2]) == int: active = [line[1], line[2]] else: (dr, dc) = accumulate_changes(line[1:]) grow_to_accommodate(active[0] + dr, active[1] + dc) active = adjust_active(dr, dc) elif c == "t": if active[0] < 0: error("set text (t) with no active cell") k = None if tua in nodes: k = [("t", tua, nodes[tua].text)] set_node_text(active, line[1]) else: k = [("cr", tua, None)] make_node(True, active[0], active[1], line[1]) elif c == "r": if active[0] < 0: error("set colour (r) with no active cell") k = None if tua in nodes: k = [("r", tua, nodes[tua].colour)] nodes[tua].colour = line[1] else: n = make_node(True, active[0], active[1], "", line[1], set(), set()) k = [("cr", tua, n)] elif c == "j": (dr, dc) = accumulate_changes(line[1:]) [r, c] = adjust_active(dr, dc) k = join_nodes_part_two(r, c) elif c == "u": (dr, dc) = accumulate_changes(line[1:]) [r, c] = adjust_active(dr, dc) k = unjoin_nodes_part_two(r, c) elif c == "c": k = with_temp_active(line[1:], copy_subtree, None) elif c == "k": k = with_temp_active(line[1:], cut_subtree_cb, -999) elif c == "p": k = with_temp_active(line[1:], paste_subtree_cb, -999) elif c == "l": k = with_temp_active(line[1:], split_node_cb, -999) elif c == "q": k = with_temp_active(line[1:], query_cb, -999) elif c == "d": k = with_temp_active(line[1:], delnode_cb, -999) elif c == "i": k = do_insert(line[1:]) elif c == "m" or c == "n": ps = split_parts(line[1:], {"to", "by"}) if len(ps) == 1: (dr, dc) = accumulate_changes(ps[0]) [rw, cl] = adjust_active(dr, dc) if c == "m": k = move_subtree_part_two(rw, cl) else: k = move_node_part_two(rw, cl) else: oa = active (dr, dc) = accumulate_changes(ps[2]) (dra, dca) = accumulate_changes(ps[0]) if ps[1] == "to": [dr, dc] = adjust_active(dr, dc) active = adjust_active(dra, dca) else: active = adjust_active(dra, dca) [dr, dc] = adjust_active(dr, dc) if c == "m": k = move_subtree_part_two(dr, dc) else: k = move_node_part_two(dr, dc) if dra == 0 and dca == 0: active = [dr, dc] else: active = oa elif c == "x": pts = [] for p1 in line[1:]: if type(p1) == int: pts.append(str(p1)) elif p1 == ",": if len(pts) > 0 and pts[-1] != ",": pts.append(p1) elif "," not in p1: pts.append(p1) else: frs = p1.split(",") for p2 in frs: if p2 != "": pts.append(p2) pts.append(",") while len(pts) > 0 and pts[-1] == ",": pts = pts[0:-1] mvs = [] mv = [] for i in pts: if i == ",": mvs.append(mv) mv = [] else: mv.append(i) mvs.append(mv) chgs = [] for mv in mvs: chgs.append(accumulate_changes(mv)) k = execute_swap(chgs) elif c == "e": k = remove_columns_rows_cb(-999, line[1]) elif c == "o": if len(line) < 2 and not filename: error("filename needed when reading in macro") k = None else: if len(line) >= 2: filename = line[1] k = read_file(filename) elif c == "s": if len(line) < 2 and not filename: error("filename needed when saving in macro") k = None else: if len(line) >= 2: filename = line[1] k = write_file(filename) elif c == "a" or c == "A": if len(line) > 1: s = line[1] p = line[2:] elif last_macro != None: s = last_macro[0] p = last_macro[1] else: error("no macro file named ", s, ".macro") return None if type(s) == list: k = obey_macro(s, []) else: if c == "A" or s not in macros: read_macro(s) if s not in macros: error("no macro file named ", s, ".macro") return None k = obey_macro(macros[s], p) last_macro = [s, p] elif c == "y": k = redo_cb(1) elif c == "z": k = undo_cb(1) else: error("bad command in macro: ", str(line)) return None if k == None: if len(uns) > 0: undos.append(uns) return None if len(k) > 0: uns = uns + k changed() draw("obey macro") return uns def general_obey_macro(s, force = False): global macros, last_macro pts = special_split(s) if len(pts) == 0: return if force or pts[0] not in macros: read_macro(pts[0]) if pts[0] not in macros: return k = obey_macro(macros[pts[0]], pts[1:]) if k != None: undos.append(k) last_macro = [pts[0], pts[1:]] def obey_macro_cb(e): s = dialogue.askstring("Choose macro\t\t\t\t\t", "enter its name, xxx will run the macro in the file xxx.macro\nparameters allowed use \" to include space", parent = win) if s == None or s=="": return general_obey_macro(s) def reread_macro_cb(e): s = dialogue.askstring("Choose macro\t\t\t\t\t", "enter its name, xxx will run the macro in the file xxx.macro", parent = win) if s == None or s=="": return general_obey_macro(s, True) def fake_macro_cb(e): global last_macro, undos s = dialogue.askstring("enter macro\t\t\t\t\t", "commands separated by semicolons", parent = win) if s == None or s=="": return m = s.split(";") mac = [] for s in m: items = process_macro_line(s) if len(items) != 0: mac.append(items) u = obey_macro(mac, []) if u: undos.append(u) last_macro = [mac, []] def repeat_macro_cb(e): global macros, last_macro if last_macro == None: messagebox.showerror("Trouble!", "nothing to repeat, no macros have been used yet") return if type(last_macro[0]) == str: u = obey_macro(macros[last_macro[0]], last_macro[1]) else: u = obey_macro(last_macro[0], last_macro[1]) if u: undos.append(u) def list_nodes_cb(e): for rc in nodes: n = nodes[rc] print(rc, n.text, n.colour, n.parents, n.children) print() bigfont = None smallfont = None def show_help(which): global showing_help, ca, bigfont, smallfont if bigfont == None: bigfont = tkf.Font(family = "courier new", size = 12, weight = "bold") smallfont = tkf.Font(family = "helvetica", size = 10) (h, w) = measure_text("X") h = 3 * h // 4 for i in ca.find_all(): ca.delete(i) pos = 20 + h for ln in which: if ln == "": pass elif ln[0] == " ": ca.create_text(60, pos, text = ln.strip(), font = smallfont, anchor = "sw", fill = "black") elif ln[1] == ":": ca.create_text(20, pos, text = ln[0], font = bigfont, anchor = "sw", fill = "black") ca.create_text(40, pos, text = ln[2:].strip(), font = smallfont, anchor = "sw", fill = "black") else: ca.create_text(20, pos, text = ln.strip(), font = smallfont, anchor = "sw", fill = "black") pos += h showing_help = True def show_help_cb(e): global gen_help show_help(gen_help) def show_macro_help_cb(e): global mac_help show_help(mac_help) def identify_node(e, f = -9999999): global sizes, edge, pad if f == -9999999: xp = e.x yp = e.y else: xp = e yp = f if xp < edge or yp < edge: return (-1, -1) rowheight = sizes[0] colwidth = sizes[1] x = edge c = 0 p = 2 * pad[1] while c < len(colwidth): nx = x + colwidth[c] + p if xp < nx: break x = nx c += 1 y = edge r = 0 p = 2 * pad[0] while r < len(rowheight): ny = y + rowheight[r] + p if yp < ny: break y = ny r += 1 if c >= len(colwidth) or r >= len(rowheight): return (-1, -1) return (r, c) def node_box(r, c = -1): global sizes if c == -1: (r, c) = r rowheight = sizes[0] colwidth = sizes[1] pr = 2 * pad[0] pc = 2 * pad[1] y = edge for i in range(0, min(r, len(sizes[0]))): y += rowheight[i] + pr x = edge for i in range(0, min(c, len(sizes[1]))): x += colwidth[i] + pc return (x, y, x + colwidth[c] + pc, y + rowheight[r] + pr) def move_active(dr, dc, dodraw = True): global active, sizes if active[0] < 1: return nr = active[0] + dr nc = active[1] + dc if nr < 0 or nc < 0 or nr >= len(sizes[0]) or nc >= len(sizes[1]): return add_undo_select(active) active = [nr, nc] if dodraw: changed() draw("arrow key") def left_cb(e): move_active(0, -1) def right_cb(e): move_active(0, 1) def up_cb(e): move_active(-1, 0) def down_cb(e): move_active(1, 0) def left_click(e): global active, waiting, nodes, hs, vs, ca, win, undos, swapping ctr = win.winfo_containing(e.x_root, e.y_root) if ctr == hs or ctr == vs: return u = None old = active if ctr == ca: (r, c) = identify_node(ca.canvasx(e.x), ca.canvasy(e.y)) if waiting == "join": u = join_nodes_part_two(r, c) draw("join click") elif waiting == "unjoin": u = unjoin_nodes_part_two(r, c) draw("unjoin click") elif waiting == "move": u = move_subtree_part_two(r, c) draw("move click") elif waiting == "movenode": u = move_node_part_two(r, c) elif waiting == "swap": if (r, c) not in nodes: error("empty nodes can not be involved in a swap") else: swapping.append((r, c)) s = [str(x) for x in swapping] set_text("swapping " + " ".join(s) + " click more or press x when done") else: active = [r, c] if old != active: u = add_undo_select(old) draw("select click") else: active = [-1, -1] if old[0] >= 0: u = add_undo_select(old) draw("deselect click") if u: undos.append(u) def cancel_click(e): global waiting, showing_help waiting = None set_text() if showing_help: showing_help = False draw("esc from help") def get_read_file_name(): global filename if filename == "": p = "." e = ".txt" else: (p, f) = os.path.split(filename) (x, e) = os.path.splitext(f) s = filedialogue.askopenfilename(title = "save as ...", defaultextension = e, initialdir = p) if s == "": return None else: return s def read_file(fname): global active, filename, sizes, nodes, clip, vpositions, savedas clear() filename = fname nodes = {} f = None try: f = open(filename, "r") except BaseException: pass if f == None: try: tfn = filename + ".txt" f = open(tfn, "r") filename = tfn except BaseException: pass if f == None: try: tfn = os.getcwd() + os.sep + filename f = open(tfn, "r") filename = tfn except BaseException: pass if f == None: try: tfn = os.getcwd() + os.sep + filename + ".txt" f = open(tfn, "r") filename = tfn except BaseException: pass if f == None: error("can't open file \"", filename, "\"") return None fnw = None if len(nodes) > 0: if savedas != None: try: os.remove(savedas) except BaseException: error("failed to delete file \"", savedas, "\"") fnw = os.getcwd() + os.sep + "tsaved" + str(int(time.time())) + ".txt" write_file(fnw) savedas = fnw s = f.readline().removesuffix("\n") p = s.split() active = [int(p[0]), int(p[1])] s = f.readline().removesuffix("\n") sizes[0] = [ int(v) for v in s.split() ] s = f.readline().removesuffix("\n") sizes[1] = [ int(v) for v in s.split() ] vpositions = [[set() for i in range(0, len(sizes[0]))], [set() for i in range(0, len(sizes[1]))]] clip = [] while True: s = f.readline().removesuffix("\n") if s == "": break (n, r, c) = s.split() r = int(r) c = int(c) t = f.readline()[2 : -1] s = f.readline().removesuffix("\n") (w, h, col) = s.split() w = int(w) h = int(h) s = f.readline().removesuffix("\n") vals = s.split() chs = set() for i in range(0, len(vals), 2): chs.add((int(vals[i]), int(vals[i + 1]))) s = f.readline().removesuffix("\n") vals = s.split() pas = set() for i in range(0, len(vals), 2): pas.add((int(vals[i]), int(vals[i + 1]))) if n == "node": d = make_node(True, r, c, t, col, chs, pas) else: d = make_node(False, r, c, t, col, chs, pas) clip.append((r, c, d)) changed(False) f.close() return [("rf", filename, fnw)] def read_file_cb(x): global undos fnr = get_read_file_name() if fnr == None: return u = read_file(fnr) if u: undos.append(u) changed() draw("read file") def write_node(n, f): print(" ", n.text, file = f) print(" ", n.textsize[1], n.textsize[0], n.colour, file = f) print(end = " ", file = f) for (ri, ci) in n.children: print(ri, ci, end = " ", file = f) print(file = f) print(end = " ", file = f) for (ri, ci) in n.parents: print(ri, ci, end = " ", file = f) print(file = f) def write_file_given_name(fn): global filename filename = fn f = None try: f = open(filename, "w") except BaseException: pass if f == None: try: tfn = os.getcwd() + os.sep + filename f = open(tfn, "w") filename = tfn except BaseException: error("can't create file \"", filename, "\"") return None print(active[0], active[1], file = f) print(" ".join([str(n) for n in sizes[0]]), file = f) print(" ".join([str(n) for n in sizes[1]]), file = f) for rc in nodes: n = nodes[rc] print("node", rc[0], rc[1], file = f) write_node(n, f) for (r, c, n) in clip: print("clip", r, c, file = f) write_node(n, f) f.close() changed(False) return [] def write_file(fn = None): global active, filename, sizes, nodes if fn != None: s = fn else: if filename == "": p = "." f = "" e = ".txt" else: (p, f) = os.path.split(filename) (x, e) = os.path.splitext(f) s = filedialogue.asksaveasfilename(title = "save as ...", initialfile = f, defaultextension = e, initialdir = p) if s == "": return None return write_file_given_name(s) def write_file_cb(e): write_file() def unjoin_nodes_cb(e): global active, waiting if active[0] == -1: return set_text("now click on node to be disconnected (ESC to cancel)") waiting = "unjoin" def actbigger(a, b): if a[0] != b[0]: return a[0] > b[0] return a[1] > b[1] def order_for_t(r, c, t = -999): if t == -999: t = c (r, c) = r if t == 0: return (r, c) else: return (c, r) def change_for_t(r, c, t, d = False): if d == False: d = t t = c (r, c) = r if t == 0: return (r + d, c) else: return (r, c + d) def remove_columns_rows_cb(e, s = None): global sizes, active, vpositions, nodes if e != -999: s = dialogue.askstring("erase columns or rows\t\t\t\t\t", "enter c then column range or r then row range" + "any number comma separated, range can be single" + "number or number-number", parent = win) if s == None or s=="": return rgs = s.split(",") rgs = [ s.strip() for s in rgs ] acts = [[], []] for rg in rgs: if rg == "": continue if rg[0] == "r": t = 0 elif rg[0] == "c": t = 1 else: error("erase rows/cols (e) ", rg[0], " should be r or c") return None ns = rg[1:].split("-") n1 = safestrtoint(ns[0]) if n1 == None: error("erase rows/cols (e) ", ns[0], " is not a number") return None if len(ns) == 1: n2 = n1 elif len(ns) == 2: n2 = safestrtoint(ns[1]) if n2 == None: error("erase rows/cols (e) ", ns[1], " is not a number") return None if n1 > n2: (n1, n2) = (n2, n1) else: error("erase rows/cols (e) \"", rg[1:], "\": too many -s in range") return None if n1 < 0 or n2 < 0: error("erase rows/cols (e) negative row and column numbers ", n1, ", ", n2, " are not possible") return None acts[t].append((n1, n2)) erase = [set(), set()] for t in range(0, 2): for (a, b) in acts[t]: nw = set(range(a, b + 1)) erase[t] |= nw erase[0] = sorted(erase[0]) erase[1] = sorted(erase[1]) occupied = [[], []] for t in range(0, 2): for p in erase[t]: if len(vpositions[t][p]) != 0: occupied[t].append(p) for orc in vpositions[t][p]: (r, c) = order_for_t(p, orc, t) (x0, y0, x1, y1) = node_box(r, c) ctrx = (x0 + x1) // 2 ctry = (y0 + y1) // 2 ca.create_line(ctrx - 50, ctry - 50, ctrx + 50, ctry + 50, width = 5, fill = "orange") ca.create_line(ctrx + 50, ctry - 50, ctrx - 50, ctry + 50, width = 5, fill = "orange") s = "" if len(occupied[0]) != 0 or len(occupied[1]) != 0: if len(occupied[0]) != 0: s = "rows " + ", ".join((str(x) for x in occupied[0])) if len(occupied[1]) != 0: if s != "": s += "\nand " s += "columns " + ", ".join((str(x) for x in occupied[1])) s += "\nare not empty. remove anyway?" ok = messagebox.askyesno("Decide Please", s) if not ok: changed() draw("erase r/c canc") return None u = actually_remove_columns_rows(erase) changed() draw("erase r/c done") if e == -999: return u else: undos.append(u) def actually_remove_columns_rows(erase): global nodes, vpositions, active, sizes un = [] for t in range(0, 2): cmin = -12 cmax = -11 e = [] for p in erase[t]: if p <= cmax + 1: cmax = p else: if cmin >= 0: e.append((cmin, cmax)) cmin = p cmax = p if cmin >= 0: e.append((cmin, cmax)) erase[t] = e for t in range(0, 2): eras = erase[t] for i in range(len(eras) - 1, -1, -1): (rmin, rmax) = eras[i] rchg = rmax - rmin + 1 vpos = vpositions[t] for j in range(rmax, rmin - 1, -1): for p in list(vpos[j]): (r, c) = order_for_t(j, p, t) nn = copy.copy(nodes[(r, c)]) nn.children = copy.copy(nn.children) nn.parents = copy.copy(nn.parents) un.append(("de", (r, c), nn)) killnode(r, c) nns = {} for rici in nodes: n = nodes[rici] n.parents = { rpcp if rpcp[t] <= rmax else change_for_t(rpcp, t, - rchg) for rpcp in n.parents } n.children = { rccc if rccc[t] <= rmax else change_for_t(rccc, t, - rchg) for rccc in n.children } if rici[t] > rmax: nns[change_for_t(rici, t, - rchg)] = n else: nns[rici] = n nodes = nns vpositions[t] = vpos[0 : rmin] + vpos[rmax + 1:] un.append(("rm", t, rmin, rmax)) vpos = vpositions[1 - t] for j in range(0, len(vpos)): vpos[j] = { pos - rchg if pos > rmax else pos for pos in vpos[j] } vpositions[1 - t] = vpos if active[t] > rmax: active[t] -= rchg rowhts = [ defsz ] * len(sizes[0]) colwds = [ defsz ] * len(sizes[1]) for rc in nodes: n = nodes[rc] (r, c) = rc ts = n.textsize if ts[0] > rowhts[r]: rowhts[r] = ts[0] if ts[1] > colwds[c]: colwds[c] = ts[1] sizes = [rowhts, colwds] return un def unremove(t, rmin, rmax): global nodes, vpositions, active, sizes rchg = rmax - rmin + 1 nns = {} for rici in nodes: n = nodes[rici] n.parents = { rpcp if rpcp[t] < rmin else change_for_t(rpcp, t, rchg) for rpcp in n.parents } n.children = { rccc if rccc[t] < rmin else change_for_t(rccc, t, rchg) for rccc in n.children } if rici[t] >= rmin: nns[change_for_t(rici, t, rchg)] = n else: nns[rici] = n nodes = nns vpos = vpositions[t] vpositions[t] = vpos[0 : rmin] + [set() for i in range(0, rchg)] + vpos[rmin:] vpos = vpositions[1 - t] for j in range(0, len(vpos)): vpos[j] = { pos + rchg if pos >= rmin else pos for pos in vpos[j] } vpositions[1 - t] = vpos if active[t] >= rmin: active[t] += rchg rowhts = [ defsz ] * len(sizes[0]) colwds = [ defsz ] * len(sizes[1]) for rc in nodes: n = nodes[rc] (r, c) = rc ts = n.textsize if ts[0] > rowhts[r]: rowhts[r] = ts[0] if ts[1] > colwds[c]: colwds[c] = ts[1] sizes = [rowhts, colwds] def unjoin_nodes_part_two(r, c = -1): global active, waiting, sizes, nodes, text (r, c, rc) = getrc(r,c) ac = tuple(active) waiting = None if rc == ac: error("unjoin (u) node ", rc, " from itself") return None if ac[0] < 0 or ac not in nodes: error("unjoin (u) with no active node") return None if rc not in nodes: return [] an = nodes[ac] rn = nodes[rc] if rc in an.parents: u = ("u", rc, ac) rn.children.discard(ac) an.parents.discard(rc) if len(an.parents) == 0: independents.children.add(ac) an.parents.add((-2, -2)) elif rc in an.children: u = ("u", ac, rc) rn.parents.discard(ac) an.children.discard(rc) if len(rn.parents) == 0: independents.children.add(rc) rn.parents.add((-2, -2)) changed() draw("unjoin nodes cb") return u def set_node_text(r, c, s = -999): global nodes if s == -999: s = c if type(r) == list: rc = tuple(r) else: rc = r (r, c) = rc else: rc = (r, c) if rc not in nodes: make_node(True, r, c, s) else: n = nodes[rc] n.text = s hw = measure_text(s) n.textsize = hw recalc_sizes(0, r) recalc_sizes(1, c) changed() def change_text(): global active, win if active[0] == -1: return tua = tuple(active) iv = "" if tua in nodes: iv = nodes[tua].text s = dialogue.askstring("text for cell r" + str(active[0]) + "c" + str(active[1]), "text\t\t\t\t\t", initialvalue = iv, parent = win) if s == None: return if type(s) == str: if tua in nodes: u = ("t", tua, nodes[tua].text) else: u = ("cr", tua, None) set_node_text(active, s) changed() return u def change_text_cb(e): global undos u = change_text() if u: undos.append(u) draw("change text cb") def change_colour(): global active, win, nodes if active[0] == -1: return tua = tuple(active) if tua in nodes: n = nodes[tua] oc = n.colour if oc == "#ff0000": dc = "#000000" else: dc = "#ff0000" else: dc = "#0000ff" (x, c) = coldialogue.askcolor(dc, title = "colour for cell r" + str(active[0]) + "c" + str(active[1])) if x == None: return if tua in nodes: ret = ("r", tua, nodes[tua].colour) nodes[tua].colour = c else: n = make_node(True, tua[0], tua[1], "", c, set(), set()) ret = ("cr", tua, n) changed() return ret def change_colour_cb(e): global undos u = change_colour() if u: undos.append(u) draw("change colour cb") def find_container_cb(e): global active, clip, nodes, waiting if active[0] == -1: return set_text("now click on other extreme (ESC to cancel)") waiting = "find" def move_node_cb(e): global active, clip, nodes, waiting if active[0] == -1: return if tuple(active) not in nodes: return set_text("now click on new location (ESC to cancel)") waiting = "movenode" def move_subtree_cb(e): global active, clip, nodes, waiting if active[0] == -1: return if tuple(active) not in nodes: error("trying to move non-existent node ", active) return set_text("now click on new location (ESC to cancel)") waiting = "move" def find_container_part_two(r, c): global waiting, active waiting = None minc = min(active[1], c) maxc = max(active[1], c) x = find_container(minc, maxc) def move_node_part_two(r, c = -1): global active, waiting, nodes, clip, vpositions un = [] eun = [] (r, c, rc) = getrc(r, c) waiting = None ac = tuple(active) if ac == rc: return [] if ac not in nodes: error("move node (n) when no active node") return None n = nodes[ac] on = None if rc in nodes: on = nodes[rc] eun.append(("op", ac, copy.copy(n.parents))) n.parents |= on.parents eun.append(("oc", ac, copy.copy(n.children))) n.children |= on.children n.parents.discard(ac) n.children.discard(ac) eun.append(("de", rc, copy.copy(on))) for prc in n.parents: pn = nodes[prc] un.append([("oc", prc, copy.copy(pn.children))]) pn.children = {rc if orc == ac else orc for orc in pn.children} for crc in n.children: cn = nodes[crc] un.append([("op", crc, copy.copy(cn.parents))]) cn.parents = {rc if orc == ac else orc for orc in cn.parents} un += eun un.append(("n", rc, ac)) vpositions[0][active[0]].discard(active[1]) vpositions[1][active[1]].discard(active[0]) nodes[rc] = n del nodes[ac] vpositions[0][r].add(c) vpositions[1][c].add(r) recalc_sizes(0, active[0]) recalc_sizes(1, active[1]) active = [r, c] recalc_sizes(0, r) recalc_sizes(1, c) changed() draw("movenode") return un def shift_left_cb(e): if active[1] == 0: shove_up(1, 0, 1) move_subtree_part_two(active[0], active[1] - 1) changed() draw("shift left") def shift_right_cb(e): if active[1] == len(sizes[1]) - 1: expand_edge(1, 1) move_subtree_part_two(active[0], active[1] + 1) changed() draw("shift right") def shift_up_cb(e): if active[0] == 0: shove_up(0, 0, 1) move_subtree_part_two(active[0] - 1, active[1]) changed() draw("shift up") def shift_down_cb(e): if active[0] == len(sizes[0]) - 1: expand_edge(0, 1) move_subtree_part_two(active[0] + 1, active[1]) changed() draw("shift down") def control_left_cb(e): global undos, active un = [] if active[1] == 0: shove_up(1, 0, 1) un.append(("su", 1, 0, 1)) u = move_node_part_two(active[0], active[1] - 1) un += u undos.append(un) changed() draw("ctrl left") def control_right_cb(e): global undos, active un = [] if active[1] == len(sizes[1]) - 1: expand_edge(1, 1) un.append(("ee", 1, 1)) u = move_node_part_two(active[0], active[1] + 1) un += u undos.append(un) changed() draw("ctrl right") def control_up_cb(e): global undos, active un = [] if active[0] == 0: shove_up(0, 0, 1) un.append(("su", 0, 0, 1)) u = move_node_part_two(active[0] - 1, active[1]) un += u undos.append(un) changed() draw("ctrl up") def control_down_cb(e): un = [] global undos, active if active[0] == len(sizes[0]) - 1: expand_edge(0, 1) un.append(("ee", 0, 1)) u = move_node_part_two(active[0] + 1, active[1]) un += u undos.append(un) changed() draw("ctrl down") def move_subtree_part_two(r, c = -1): global active, waiting, nodes, text, clip, undos (r, c, rc) = getrc(r, c) waiting = None oa = tuple(active) n = nodes[oa] och = {} for prc in n.parents: och[prc] = copy.copy(nodes[prc].children) pas = copy.copy(n.parents) os = copy.deepcopy(sizes) u1 = cut_subtree(True) if u1 == None: return None set_text() active = [r, c] u2 = paste_subtree(oa, os) if u2 == None: return None ac = tuple(active) if u2: u1.append(u2) else: (r, c) = ac nodes[ac].parents = pas for prc in pas: pnc = nodes[prc].children u1.append(("oc", prc, och[prc])) pnc.add(ac) pnc.discard(oa) changed() return u1 def copy_subtree(): tu = tuple(active) if tu not in nodes: error("attempt to copy (c) non-existent node ", tu) return None n = nodes[tu] u = cut_subtree(False) nodes[tu] = n return u def copy_subtree_cb(e): copy_subtree() def cut_subtree_cb(e): global undos u = cut_subtree(True) if u: undos.append(u) draw("cut subtree cb") def delnode_cb(e): global undos tua = tuple(active) if tuple(active) not in nodes: error("attempting to delete (d) non-existent node ", tue) return None n = nodes[tua] killnode(active) un = [("de", tua, n)] recalc_sizes(0, active[0]) recalc_sizes(1, active[1]) draw("delnode") if e != -999: undos.append(un) un = [] return un def cut_subtree(remove = True): global active, clip, nodes, sizes, gone tua = tuple(active) if tua not in nodes: error("attempting to remove (k) non-existent node ", tua) return None n = nodes[tua] if remove: for pa in n.parents: if pa in nodes: pn = nodes[pa] pn.children.discard(tua) clip = [] gone = [set(), set()] recrem(active[0], active[1], remove) if clip: pn = clip[0][2] pn.parents = set() u = [("k", tua, clip, n.parents)] changed() return u def recrem(r, c, remove, depth = 1): global nodes, clip, active, gone, independents (r, c, rc) = getrc(r, c) if rc not in nodes: messagebox.showerror("Trouble!", "Trying to remove non-existent node ", rc) return (ar, ac) = active n = nodes[rc] ch = n.children pa = n.parents nch = {(ri - ar, ci - ac) for (ri, ci) in ch} npa = {(ri - ar, ci - ac) for (ri, ci) in pa} clip.append([r - ar, c - ac, n.copy().change(children = nch, parents = npa)]) for (cr, cc) in list(ch): recrem(cr, cc, remove, depth + 1) if remove: del nodes[rc] vpositions[0][r].discard(c) if numin(0, r) == 0: gone[0].add(r) vpositions[1][c].discard(r) if numin(1, c) == 0: gone[1].add(c) recalc_sizes(0, r) recalc_sizes(1, c) def paste_subtree_cb(e): global undos u = paste_subtree() if u: undos.append(u) draw("paste subtree cb") def draw_cb(e): draw("draw cb") def find_clip_overwrites(): global nodes, clip, active (ar, ac) = active overs = [] for (r, c, n) in clip: if (r + ar, c + ac) in nodes: overs.append((r + ar, c + ac)) return overs def query_cb(e): global active, undos, vpositions, nodes tua = tuple(active) lines = [] if tua in nodes: lines.append("active " + str(tua)) else: lines.append("active " + str(tua) + " (empty cell)") nb = node_box(tua) lines.append(" box " + str(nb)) rc = identify_node((nb[0] + nb[2]) // 2, (nb[1] + nb[3]) // 2) lines.append(" centre maps back to " + str(rc)) if tua in nodes: n = nodes[tua] lines.append(" text " + n.text + " h,w " + str(n.textsize) + " colour " + n.colour) lines.append(" children " + str(n.children)) lines.append(" parents " + str(n.parents)) lines.append(" in row " + str(active[0]) + ", cols " + str(sorted(vpositions[0][active[0]])) + " occupied") lines.append(" in col " + str(active[1]) + ", rows " + str(sorted(vpositions[1][active[1]])) + " occupied") messagebox.showinfo("cell " + str(tuple(active)), "\n".join(lines)) return [] def paste_subtree(oa = None, os = None): global active, clip, nodes, sizes, gone, ca un = [] if active[0] == -1: error("paste (p) with no active cell") return None if len(clip) == 0: return [] arc = tuple(active) if arc in nodes: on = nodes[arc] op = on.parents.copy() oc = clip og = gone cut_subtree() cut = True clip = oc gone = og else: on = None op = [] cut = False overs = find_clip_overwrites() if len(overs) != 0: if os != None: (sizes, os) = (os, sizes) for (r, c) in overs: (x0, y0, x1, y1) = node_box(r, c) ctrx = (x0 + x1) // 2 ctry = (y0 + y1) // 2 ca.create_line(ctrx - 50, ctry - 50, ctrx + 50, ctry + 50, width = 5, fill = "orange") ca.create_line(ctrx + 50, ctry - 50, ctrx - 50, ctry + 50, width = 5, fill = "orange") if os != None: sizes = os msg = "Nodes as shown will be overwritten\nDo you want to continue anyway?" if oa == None: msg = msg + "\n(if no, you can still paste somewhere else)" ok = messagebox.askyesno("Decide Please", msg) if not ok: if oa != None: active = [oa[0], oa[1]] paste_subtree() changed() return None for rc in overs: un.append(("de", rc, copy.copy(nodes[rc]))) minc = minr = maxc = maxr = None for (r, c, n) in clip: if minc == None or c < minc: minc = c if maxc == None or c > maxc: maxc = c if minr == None or r < minr: minr = r if maxr == None or r > maxr: maxr = r defect = arc[1] + minc if defect < 0: shove_up(1, 0, - defect) un.append(("su", 1, 0, - defect)) arc = (arc[0], arc[1] - defect) defect = len(sizes[1]) - arc[1] - maxc - 1 if defect < 0: expand_edge(1, - defect) un.append(("ee", 1, - defect)) defect = len(sizes[0]) - arc[0] - maxr - 1 if defect < 0: expand_edge(0, - defect) un.append(("ee", 0, - defect)) active = [arc[0], arc[1]] for (r, c, n) in clip: (ar, ac) = active r += ar c += ac rc = (r, c) if rc in nodes: oldn = nodes[rc] oldps = oldn.parents oldch = oldn.children else: oldps = set() oldch = set() nn = copy.copy(n) nn.children = {(ri + ar, ci + ac) for (ri, ci) in n.children} nn.parents = {(ri + ar, ci + ac) for (ri, ci) in n.parents} un.append(("cr", rc, nn)) nodes[(r, c)] = nn vpositions[0][r].add(c) vpositions[1][c].add(r) sizes[0][r] = max(n.textsize[0], sizes[0][r]) sizes[1][c] = max(n.textsize[1], sizes[1][c]) nn.children = {(cr + active[0], cc + active[1]) for (cr, cc) in n.children} | oldch nn.parents = {(cr + active[0], cc + active[1]) for (cr, cc) in n.parents} | oldps if on != None: for prc in op: nodes[prc].children.add(prc) (r, c, n) = clip[0] un.append(("op", tuple(active), copy.copy(n.parents))) n.parents = n.parents | op changed() return un def join_nodes_cb(e): global active, waiting if active[0] == -1: return set_text("now click on node to be connected (ESC to cancel)") waiting = "join" def join_nodes_part_two(r, c = -1): global active, waiting, sizes, nodes, text (r, c, rc) = getrc(r,c) waiting = None tua = tuple(active) if rc == tua: error("connecting (j) ", tua, " to itself") return None if rc not in nodes: error("connecting (j) ", tua, " to non-existent node ", rc) return None if tua not in nodes: error("connecting (j) non-existent node ", tua, " to ", rc) return None swapped = False if r < active[0]: (r, active[0], c, active[1]) = (active[0], r, active[1], c) rc = (r, c) swapped = True tua = tuple(active) n = nodes[tua] rn = nodes[rc] u = ("j", tua, rc) rn.parents.discard((-2, -2)) independents.children.discard(rc) rn.parents.add(tua) n.children.add(rc) if swapped: (r, active[0], c, active[1]) = (active[0], r, active[1], c) changed() return u def insert_cb(e): insert() draw("insert cb") def insert(): global active, undos if active[0] == -1: return q = "a = row above, b = row below,\nl = col left, r = col right,\noptional number\t\t\t\t" r = dialogue.askstring("insert new colomns or rows", q, parent = win) if r == None or len(r) == 0: return u = do_insert(r) if u != None: undos.append(un) def do_insert(rs): global active, win, undos, vpositions un = [] if type(rs) == str: rs = rs.split() for r in rs: invalid = None s = "" if r[0] in "ablr": t = r[0] s = r[1:] elif r[-1] in "ablr": t = r[-1] s = r[0:-1] else: invalid = r if s == "": s = "1" if invalid == None: num = safestrtoint(s) if num == None: invalid = r if invalid != None: error("bad insertion (i) detail \"" + r + "\"") return None if t == "l": shove_up(1, active[1], num) un.append(("su", 1, active[1], num)) active[1] += num elif t == "r": shove_up(1, active[1] + 1, num) un.append(("su", 1, active[1] + 1, num)) elif t == "a": shove_up(0, active[0], num) un.append(("su", 0, active[0], num)) active[0] += num elif t == "b": shove_up(0, active[0] + 1, num) un.append(("su", 0, active[0] + 1, num)) changed() return un def undo(act): global nodes, active, clip, savedas if type(act) == list: ret = [] for i in range(len(act) - 1, -1, -1): u = undo(act[i]) ret.append(u) return ret ak = act[0] a1 = n = None r = c = -1 if len(act) > 1: a1 = act[1] if type(a1) == list: a1 = tuple(a1) if type(a1) == tuple: (r, c) = a1 rc = a1 if rc in nodes: n = nodes[rc] if ak == "ac": nact = ("ac", tuple(active)) active = [r, c] return nact elif ak == "t": oldt = n.text set_node_text(r, c, act[2]) return ("t", rc, oldt) elif ak == "r": oldc = n.colour n.colour = act[2] return ("r", rc, oldc) elif ak == "cr": n = nodes[rc] del nodes[rc] vpositions[0][r].discard(c) vpositions[1][c].discard(r) recalc_sizes(0, r) recalc_sizes(1, c) return ("cr", rc, n) elif ak == "de": n = act[2] nn = make_node(True, r, c, n.text, n.colour, n.children, n.parents) for prc in nn.parents: if prc[0] >= 0: nodes[prc].children.add(rc) for crc in nn.children: nodes[crc].parents.add(rc) return ("de", rc) elif ak == "j": if rc not in nodes or act[2] not in nodes: return act nodes[rc].children.discard(act[2]) nodes[act[2]].parents.discard(rc) return act elif ak == "u": if rc not in nodes or act[2] not in nodes: return act nodes[rc].children.add(act[2]) nodes[act[2]].parents.add(rc) return act elif ak == "oc": oc = copy.copy(n.children) n.children = act[2] return (ak, rc, oc) elif ak == "op": op = copy.copy(n.parents) n.parents = act[2] return (ak, rc, op) elif ak == "n": active = rc move_node_part_two(act[2]) return act elif ak == "k": active = list(rc) clip = copy.copy(act[2]) paste_subtree() op = copy.copy(act[3]) for prc in op: nodes[prc].children.add(rc) nodes[rc].parents = op return act elif ak == "rf": if savedas == None: messagebox.showerror("Not Possible", "Only one read-file can be undone") read_file(act[2]) savedas = None return act elif ak == "su": reverse_shove_up(act[1], act[2], act[3]) if active[act[1]] >= act[2]: active[act[1]] -= act[3] return act elif ak == "ee": reduce_edge(act[1], act[2]) return act elif ak == "rm": unremove(act[1], act[2], act[3]) return act else: error("action " + str(ak) + " can't be undone") def redo(act): global nodes, active, clip, savedas if type(act) == list: ret = [] for i in range(len(act) - 1, -1, -1): ret.append(redo(act[i])) return ret ak = act[0] a1 = n = None r = c = -1 if len(act) > 1: a1 = act[1] if type(a1) == list: a1 = tuple(a1) if type(a1) == tuple: (r, c) = a1 rc = a1 if rc in nodes: n = nodes[rc] if ak == "ac": nact = ("ac", tuple(active)) active = [r, c] return nact elif ak == "t": oldt = n.text set_node_text(r, c, act[2]) return ("t", rc, oldt) elif ak == "r": oldc = n.colour n.colour = act[2] return ("r", rc, oldc) elif ak == "cr": n = act[2] make_node(True, r, c, n.text, n.colour, n.children, n.parents) return ("cr", rc) elif ak == "de": n = copy.copy(nodes[rc]) n.children = copy.copy(n.children) n.parents = copy.copy(n.parents) killnode(r, c) recalc_sizes(0, r) recalc_sizes(1, c) return ("de", rc, n) elif ak == "j": if rc not in nodes or act[2] not in nodes: return act nodes[rc].children.add(act[2]) nodes[act[2]].parents.add(rc) return act elif ak == "u": if rc not in nodes or act[2] not in nodes: return act nodes[rc].children.discard(act[2]) nodes[act[2]].parents.discard(rc) return act elif ak == "oc": oc = copy.copy(n.children) n.children = act[2] return (ak, rc, oc) elif ak == "op": op = copy.copy(n.parents) n.parents = act[2] return (ak, rc, op) elif ak == "n": active = act[2] move_node_part_two(rc) return act elif ak == "k": active = list(rc) cut_subtree() return act elif ak == "rf": read_file(act[1]) savedas = act[2] return act elif ak == "su": shove_up(act[1], act[2], act[3]) if active[act[1]] >= act[2]: active[act[1]] += act[3] return act elif ak == "ee": expand_edge(act[1], act[2]) return act elif ak == "rm": lot = list(range(act[2], act[3] + 1)) if act[1] == 0: eras = [lot, []] else: eras = [[], lot] actually_remove_columns_rows(eras) return act else: error("action " + str(ak) + " can't be redone") def undo_cb(e): global undos, undoing if len(undos) == 0: error("undo buffer is empty") return action = undos.pop() undoing = True nact = undo(action) undoing = False redos.append(nact) changed() draw("undo_cb") def redo_cb(e): global undos, undoing if len(redos) == 0: error("redo buffer is empty") return action = redos.pop() undoing = True nact = redo(action) undoing = False undos.append(nact) changed() draw("redo_cb") def shove_positions_up(st, ind, pos, by): nst = set() for (r, c) in st: if ind == 0: if r >= pos: nst.add((r + by, c)) else: nst.add((r, c)) elif ind == 1: if c >= pos: nst.add((r, c + by)) else: nst.add((r, c)) return nst def expand_edge(ind, by): global sizes, defsz, vpositions sizes[ind] += [defsz] * by vpositions[ind] += [set() for i in range(0, by)] def reduce_edge(ind, by): global sizes, vpositions sizes[ind] = sizes[ind][0 : - by] vpositions[ind] = vpositions[ind][0 : - by] def shove_up(ind, pos, by): global nodes, sizes, active, defsz, vpositions, independents sz = sizes[ind] oldsz = len(sz) ps = vpositions[ind] ops = vpositions[1 - ind] independents.children = shove_positions_up(independents.children, ind, pos, by) for i in range(0, len(ops)): ops[i] = { v + by if v >= pos else v for v in ops[i] } sz = sz[0 : pos] + [ defsz ] * by + sz[pos:] ps = ps[0 : pos] + [ set() for i in range(0, by) ] + ps[pos:] sizes[ind] = sz vpositions[ind] = ps nn = {} for nrc in nodes: n = nodes[nrc] n.children = shove_positions_up(n.children, ind, pos, by) n.parents = shove_positions_up(n.parents, ind, pos, by) if nrc[ind] >= pos: newnrc = (nrc[0] + (by if ind == 0 else 0), nrc[1] + (by if ind == 1 else 0)) nn[newnrc] = n else: nn[nrc] = n nodes = nn def reverse_shove_up(ind, pos, by): global nodes, sizes, active, defsz, vpositions, independents sz = sizes[ind] oldsz = len(sz) ps = vpositions[ind] ops = vpositions[1 - ind] independents.children = shove_positions_up(independents.children, ind, pos, - by) for i in range(0, len(ops)): ops[i] = { v - by if v >= pos else v for v in ops[i] } sz = sz[0:pos] + sz[pos + by:] ps = ps[0:pos] + ps[pos + by:] sizes[ind] = sz vpositions[ind] = ps nn = {} for nrc in nodes: n = nodes[nrc] n.children = shove_positions_up(n.children, ind, pos, - by) n.parents = shove_positions_up(n.parents, ind, pos, - by) if nrc[ind] >= pos: newnrc = (nrc[0] - (by if ind == 0 else 0), nrc[1] - (by if ind == 1 else 0)) nn[newnrc] = n else: nn[nrc] = n active[0] = max(min(active[0], len(sizes[0]) - 1), 0) active[1] = max(min(active[1], len(sizes[1]) - 1), 0) nodes = nn def safestrtoint(s): try: return int(s) except BaseException: return None try: win except NameError: print("-----------------------------------\nnew start") else: print("-----------------------------------\nrestart") if win: win.destroy() start() draw("startup")