~nch/glue

e5a1dfa7581b7addd66491ce5f2a9a9fc0447d2f — nc 1 year, 4 months ago f7b75a2
clean up node editor code
2 files changed, 69 insertions(+), 62 deletions(-)

M glue.py
M table.py
M glue.py => glue.py +67 -62
@@ 1,96 1,101 @@
import tkinter as tk
import tkinter.dnd as dnd # type: ignore
from dataclasses import dataclass
from typing import Dict
from typing import Dict, Tuple
import table

def _cursor_pos(canvas, event):
    # where the corner of the canvas is relative to the screen:
    x_org = canvas.winfo_rootx()
    y_org = canvas.winfo_rooty()
    # XXX UTTER INSANITY -- x/y_root are relative to the *MONITOR* x/y mouse positions!
    #     That's why we're getting the offset here:
    return event.x_root - x_org, event.y_root - y_org

class DraggedNode:
    def __init__(self, orig_node):
        x1, y1, x2, y2 = orig_node.canvas.bbox(orig_node.id)
        self.orig_node = orig_node
        self.id = orig_node.canvas.create_rectangle(x1, y1, x2, y2)

    def move(self, x, y):
        x1, y1, _1, _2 = self.orig_node.canvas.bbox(self.id)
        self.orig_node.canvas.move(self.id, x-x1, y-y1)

    def dnd_end(self, target, event):
        self.orig_node.canvas.delete(self.id)
        self.orig_node.move(*_cursor_pos(self.orig_node.canvas, event))

class DraggedEdge:
    def __init__(self, start_node, x, y):
        self.ox, self.oy = x, y
        self.start_node = start_node
        self.id = start_node.canvas.create_line(x, y, x, y)

    def move(self, x, y):
        self.start_node.canvas.coords(self.id, self.ox, self.oy, x, y)

    def dnd_end(self, target, event):
        self.start_node.canvas.delete(self.id)
        if isinstance(target, Node): # FIXME
            self.start_node.add_child(target)
            print('add child', target)

class Node:
    def __init__(self, canvas, x=10, y=10):
        self.frame = tk.Frame(canvas, borderwidth=2, bg='#aaa', width=100, height=100, pady=20, padx=10)
        self.id = canvas.create_window(x, y, window=self.frame, anchor='nw')
        self.canvas = canvas
        self.edge = None
        self.frame.bind('<Button-1>', self.press)
        self.frame.bind('<Button-2>', self.create_edge)

    def create_edge(self, event):
        if self.edge: # HACK
            self.canvas.delete(self.edge)
        self.edge = self.canvas.create_line(0, 0, 10, 10, fill='black', tag='drag_edge')
        self.press(event)

    def attach(self, canvas, x, y):
        if self.edge:
            self.canvas.delete(self.edge)
            self.edge = None
        else:
            self.canvas.coords(self.id, x, y)

    def press(self, event):
        if dnd.dnd_start(self, event):
            self.x_off = event.x
            self.y_off = event.y

            self.x_orig, self.y_orig = self.canvas.coords(self.id)

    def where(self, canvas, event):
        # where the corner of the canvas is relative to the screen:
        x_org = canvas.winfo_rootx()
        y_org = canvas.winfo_rooty()
        # where the pointer is relative to the canvas widget:
        x = event.x_root - x_org
        y = event.y_root - y_org
        # compensate for initial pointer offset
        return x - self.x_off, y - self.y_off

    def dnd_end(self, target, event):
        pass
        # FIXME I really don't like that it's possible to add a child without adding a node if this array is appended to instead of calling add_child().
        #       Figure out a better way of enforcing this.
        self.children = []
        self.frame.bind('<Button-1>', lambda ev: dnd.dnd_start(DraggedNode(self), ev))
        self.frame.bind('<Button-3>', lambda ev: dnd.dnd_start(DraggedEdge(self, *_cursor_pos(self.canvas, ev)), ev))

import numpy as np # type: ignore
nodes = table.Table({'id': np.array(dtype='int'), 'gui_handle': np.array(dtype='object')})
    def add_child(self, child):
        self.children.append(child)
        # TODO self.canvas.create_line()

    def move(self, x, y):
        self.canvas.coords(self.id, x, y)

class NodeCanvas:
    def __init__(self, root):
        self.top = root
        self.canvas = tk.Canvas(self.top, width=800, height=600, background='#444')
        self.canvas = tk.Canvas(root, width=800, height=600, background='#444')
        self.canvas.pack(fill='both', expand=1)
        self.canvas.dnd_accept = self.dnd_accept

    def dnd_accept(self, source, event):
        return self

    def dnd_enter(self, source, event):
        x, y = source.where(self.canvas, event)
        x1, y1, x2, y2 = source.canvas.bbox(source.id)

        dx, dy = x2-x1, y2-y1
        self.dndid = self.canvas.create_rectangle(x, y, x+dx, y+dx)
        self.dnd_motion(source, event)

    def dnd_motion(self, source, event):
        x, y = source.where(self.canvas, event)
        x1, y1, x2, y2 = self.canvas.bbox(self.dndid)
        self.canvas.move(self.dndid, x-x1, y-y1)
        x, y = _cursor_pos(self.canvas, event)
        source.move(x, y)

    ### unused ###
    def dnd_enter(self, source, event):
        pass

    ### unused ###
    def dnd_leave(self, source, event):
        self.canvas.delete(self.dndid)
        self.dndid = None
        pass

    ### unused ###
    def dnd_commit(self, source, event):
        self.dnd_leave(source, event)
        x, y = source.where(self.canvas, event)
        source.attach(self.canvas, x, y)

    def create_node(self):
        n = Node(self.canvas)
        return n.frame
        pass

import numpy as np # type: ignore
# TODO: make initialization notation easier...
nodes = table.Table({'id': np.array([], dtype='int'), 'gui_handle': np.array([], dtype='object')})
edges = table.Table({'n1_id': np.array([], dtype='int'), 'n2_id': np.array([], dtype='int')})

root = tk.Tk()
root.geometry("+1+1")
canvas = NodeCanvas(root)
n = canvas.create_node()
n = Node(canvas.canvas).frame
tk.Label(n, text='test').pack()
n2 = canvas.create_node()
n2 = Node(canvas.canvas).frame
tk.Label(n2, text='test2').pack()
root.mainloop()


M table.py => table.py +2 -0
@@ 5,6 5,8 @@ from collections.abc import Iterable

# TODO: implement pure relational alebra version
# TODO: implement tuple calculus version
# TODO: add referential integrity...
# TODO: add serialization/deserialization

def _ensure_columns_match(a, b):
    if set(a) != set(b):