initial commit down a path for a possible core python tkinter gui
This commit is contained in:
parent
c0ce07404f
commit
70ec532703
6 changed files with 450 additions and 0 deletions
15
coretk/Pipfile
Normal file
15
coretk/Pipfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
flake8 = "*"
|
||||
isort = "*"
|
||||
black = "==19.3b0"
|
||||
|
||||
[packages]
|
||||
pillow = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
134
coretk/Pipfile.lock
generated
Normal file
134
coretk/Pipfile.lock
generated
Normal file
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c719ca1ace4a33dfe01fe579162d4557e8711819c4e7b4f8e799dd25b9cde596"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de",
|
||||
"sha256:0ab7c5b5d04691bcbd570658667dd1e21ca311c62dcfd315ad2255b1cd37f64f",
|
||||
"sha256:0b3e6cf3ea1f8cecd625f1420b931c83ce74f00c29a0ff1ce4385f99900ac7c4",
|
||||
"sha256:365c06a45712cd723ec16fa4ceb32ce46ad201eb7bbf6d3c16b063c72b61a3ed",
|
||||
"sha256:38301fbc0af865baa4752ddae1bb3cbb24b3d8f221bf2850aad96b243306fa03",
|
||||
"sha256:3aef1af1a91798536bbab35d70d35750bd2884f0832c88aeb2499aa2d1ed4992",
|
||||
"sha256:3fe0ab49537d9330c9bba7f16a5f8b02da615b5c809cdf7124f356a0f182eccd",
|
||||
"sha256:45a619d5c1915957449264c81c008934452e3fd3604e36809212300b2a4dab68",
|
||||
"sha256:49f90f147883a0c3778fd29d3eb169d56416f25758d0f66775db9184debc8010",
|
||||
"sha256:571b5a758baf1cb6a04233fb23d6cf1ca60b31f9f641b1700bfaab1194020555",
|
||||
"sha256:5ac381e8b1259925287ccc5a87d9cf6322a2dc88ae28a97fe3e196385288413f",
|
||||
"sha256:6153db744a743c0c8c91b8e3b9d40e0b13a5d31dbf8a12748c6d9bfd3ddc01ad",
|
||||
"sha256:6fd63afd14a16f5d6b408f623cc2142917a1f92855f0df997e09a49f0341be8a",
|
||||
"sha256:70acbcaba2a638923c2d337e0edea210505708d7859b87c2bd81e8f9902ae826",
|
||||
"sha256:70b1594d56ed32d56ed21a7fbb2a5c6fd7446cdb7b21e749c9791eac3a64d9e4",
|
||||
"sha256:76638865c83b1bb33bcac2a61ce4d13c17dba2204969dedb9ab60ef62bede686",
|
||||
"sha256:7b2ec162c87fc496aa568258ac88631a2ce0acfe681a9af40842fc55deaedc99",
|
||||
"sha256:7cee2cef07c8d76894ebefc54e4bb707dfc7f258ad155bd61d87f6cd487a70ff",
|
||||
"sha256:7d16d4498f8b374fc625c4037742fbdd7f9ac383fd50b06f4df00c81ef60e829",
|
||||
"sha256:b50bc1780681b127e28f0075dfb81d6135c3a293e0c1d0211133c75e2179b6c0",
|
||||
"sha256:bd0582f831ad5bcad6ca001deba4568573a4675437db17c4031939156ff339fa",
|
||||
"sha256:cfd40d8a4b59f7567620410f966bb1f32dc555b2b19f82a91b147fac296f645c",
|
||||
"sha256:e3ae410089de680e8f84c68b755b42bc42c0ceb8c03dbea88a5099747091d38e",
|
||||
"sha256:e9046e559c299b395b39ac7dbf16005308821c2f24a63cae2ab173bd6aa11616",
|
||||
"sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808",
|
||||
"sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.1.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
|
||||
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.3b0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
|
||||
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.8"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
|
||||
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
|
||||
],
|
||||
"version": "==2.5.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
"sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
|
||||
],
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
}
|
||||
}
|
||||
}
|
0
coretk/coretk/__init__.py
Normal file
0
coretk/coretk/__init__.py
Normal file
286
coretk/coretk/graph.py
Normal file
286
coretk/coretk/graph.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
import enum
|
||||
import tkinter as tk
|
||||
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
|
||||
class GraphMode(enum.Enum):
|
||||
SELECT = 0
|
||||
EDGE = 1
|
||||
NODE = 2
|
||||
|
||||
|
||||
class CanvasGraph(tk.Canvas):
|
||||
images = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, name, file_path):
|
||||
image = Image.open(file_path)
|
||||
tk_image = ImageTk.PhotoImage(image)
|
||||
cls.images[name] = tk_image
|
||||
|
||||
def __init__(self, master=None, cnf=None, **kwargs):
|
||||
if cnf is None:
|
||||
cnf = {}
|
||||
kwargs["highlightthickness"] = 0
|
||||
super().__init__(master, cnf, **kwargs)
|
||||
self.mode = GraphMode.SELECT
|
||||
self.selected = None
|
||||
self.node_context = None
|
||||
self.nodes = {}
|
||||
self.edges = {}
|
||||
self.drawing_edge = None
|
||||
self.setup_menus()
|
||||
self.setup_bindings()
|
||||
|
||||
def setup_menus(self):
|
||||
self.node_context = tk.Menu(self.master)
|
||||
self.node_context.add_command(label="One")
|
||||
self.node_context.add_command(label="Two")
|
||||
self.node_context.add_command(label="Three")
|
||||
|
||||
def setup_bindings(self):
|
||||
self.bind("<ButtonPress-1>", self.click_press)
|
||||
self.bind("<ButtonRelease-1>", self.click_release)
|
||||
self.bind("<B1-Motion>", self.click_motion)
|
||||
self.bind("<Button-3>", self.context)
|
||||
self.bind("e", self.set_mode)
|
||||
self.bind("s", self.set_mode)
|
||||
self.bind("n", self.set_mode)
|
||||
|
||||
def canvas_xy(self, event):
|
||||
x = self.canvasx(event.x)
|
||||
y = self.canvasy(event.y)
|
||||
return x, y
|
||||
|
||||
def get_selected(self, event):
|
||||
overlapping = self.find_overlapping(event.x, event.y, event.x, event.y)
|
||||
nodes = set(self.find_withtag("node"))
|
||||
selected = None
|
||||
for _id in overlapping:
|
||||
if self.drawing_edge and self.drawing_edge.id == _id:
|
||||
continue
|
||||
|
||||
if _id in nodes:
|
||||
selected = _id
|
||||
break
|
||||
|
||||
if selected is None:
|
||||
selected = _id
|
||||
|
||||
return selected
|
||||
|
||||
def click_release(self, event):
|
||||
self.focus_set()
|
||||
self.selected = self.get_selected(event)
|
||||
print(f"click release selected: {self.selected}")
|
||||
if self.mode == GraphMode.EDGE:
|
||||
self.handle_edge_release(event)
|
||||
elif self.mode == GraphMode.NODE:
|
||||
x, y = self.canvas_xy(event)
|
||||
self.add_node(x, y, "Node", "switch")
|
||||
|
||||
def handle_edge_release(self, event):
|
||||
edge = self.drawing_edge
|
||||
self.drawing_edge = None
|
||||
|
||||
# not drawing edge return
|
||||
if edge is None:
|
||||
return
|
||||
|
||||
# edge dst must be a node
|
||||
print(f"current selected: {self.selected}")
|
||||
print(f"current nodes: {self.find_withtag('node')}")
|
||||
is_node = self.selected in self.find_withtag("node")
|
||||
if not is_node:
|
||||
edge.delete()
|
||||
return
|
||||
|
||||
# edge dst is same as src, delete edge
|
||||
if edge.src == self.selected:
|
||||
edge.delete()
|
||||
|
||||
# set dst node and snap edge to center
|
||||
x, y = self.coords(self.selected)
|
||||
edge.complete(self.selected, x, y)
|
||||
print(f"drawing edge token: {edge.token}")
|
||||
if edge.token in self.edges:
|
||||
edge.delete()
|
||||
else:
|
||||
self.edges[edge.token] = edge
|
||||
node_src = self.nodes[edge.src]
|
||||
node_src.edges.add(edge)
|
||||
node_dst = self.nodes[edge.dst]
|
||||
node_dst.edges.add(edge)
|
||||
|
||||
print(f"edges: {self.find_withtag('edge')}")
|
||||
|
||||
def click_press(self, event):
|
||||
print(f"click press: {event}")
|
||||
selected = self.get_selected(event)
|
||||
is_node = selected in self.find_withtag("node")
|
||||
if self.mode == GraphMode.EDGE and is_node:
|
||||
x, y = self.coords(selected)
|
||||
self.drawing_edge = CanvasEdge(x, y, x, y, selected, self)
|
||||
|
||||
def click_motion(self, event):
|
||||
if self.mode == GraphMode.EDGE and self.drawing_edge is not None:
|
||||
x2, y2 = self.canvas_xy(event)
|
||||
x1, y1, _, _ = self.coords(self.drawing_edge.id)
|
||||
self.coords(self.drawing_edge.id, x1, y1, x2, y2)
|
||||
|
||||
def context(self, event):
|
||||
selected = self.get_selected(event)
|
||||
nodes = self.find_withtag("node")
|
||||
if selected in nodes:
|
||||
print(f"node context: {selected}")
|
||||
self.node_context.post(event.x_root, event.y_root)
|
||||
|
||||
def set_mode(self, event):
|
||||
print(f"mode event: {event}")
|
||||
if event.char == "e":
|
||||
self.mode = GraphMode.EDGE
|
||||
elif event.char == "s":
|
||||
self.mode = GraphMode.SELECT
|
||||
elif event.char == "n":
|
||||
self.mode = GraphMode.NODE
|
||||
print(f"graph mode: {self.mode}")
|
||||
|
||||
def add_node(self, x, y, name, image_name):
|
||||
image = self.images[image_name]
|
||||
node = CanvasNode(x, y, name, image, self)
|
||||
self.nodes[node.id] = node
|
||||
return node
|
||||
|
||||
|
||||
class CanvasEdge:
|
||||
width = 3
|
||||
|
||||
def __init__(self, x1, y1, x2, y2, src, canvas):
|
||||
self.src = src
|
||||
self.dst = None
|
||||
self.canvas = canvas
|
||||
self.id = self.canvas.create_line(x1, y1, x2, y2, tags="edge", width=self.width)
|
||||
self.token = None
|
||||
self.canvas.tag_lower(self.id)
|
||||
|
||||
def complete(self, dst, x, y):
|
||||
self.dst = dst
|
||||
self.token = tuple(sorted((self.src, self.dst)))
|
||||
x1, y1, _, _ = self.canvas.coords(self.id)
|
||||
self.canvas.coords(self.id, x1, y1, x, y)
|
||||
|
||||
def delete(self):
|
||||
self.canvas.delete(self.id)
|
||||
|
||||
|
||||
class CanvasNode:
|
||||
def __init__(self, x, y, name, image, canvas):
|
||||
self.name = name
|
||||
self.image = image
|
||||
self.canvas = canvas
|
||||
self.id = self.canvas.create_image(
|
||||
x, y, anchor=tk.CENTER, image=self.image, tags="node"
|
||||
)
|
||||
self.text_id = self.canvas.create_text(x, y + 20, text=self.name)
|
||||
self.canvas.tag_bind(self.id, "<ButtonPress-1>", self.click_press)
|
||||
self.canvas.tag_bind(self.id, "<ButtonRelease-1>", self.click_release)
|
||||
self.canvas.tag_bind(self.id, "<B1-Motion>", self.motion)
|
||||
self.canvas.tag_bind(self.id, "<Button-3>", self.context)
|
||||
self.edges = set()
|
||||
self.moving = None
|
||||
|
||||
def click_press(self, event):
|
||||
print(f"click press {self.name}: {event}")
|
||||
self.moving = self.canvas.canvas_xy(event)
|
||||
|
||||
def click_release(self, event):
|
||||
print(f"click release {self.name}: {event}")
|
||||
self.moving = None
|
||||
|
||||
def motion(self, event):
|
||||
if self.canvas.mode == GraphMode.EDGE:
|
||||
return
|
||||
x, y = self.canvas.canvas_xy(event)
|
||||
moving_x, moving_y = self.moving
|
||||
offset_x, offset_y = x - moving_x, y - moving_y
|
||||
self.moving = x, y
|
||||
|
||||
old_x, old_y = self.canvas.coords(self.id)
|
||||
self.canvas.move(self.id, offset_x, offset_y)
|
||||
self.canvas.move(self.text_id, offset_x, offset_y)
|
||||
new_x, new_y = self.canvas.coords(self.id)
|
||||
for edge in self.edges:
|
||||
x1, y1, x2, y2 = self.canvas.coords(edge.id)
|
||||
if x1 == old_x and y1 == old_y:
|
||||
self.canvas.coords(edge.id, new_x, new_y, x2, y2)
|
||||
else:
|
||||
self.canvas.coords(edge.id, x1, y1, new_x, new_y)
|
||||
|
||||
def context(self, event):
|
||||
print(f"context click {self.name}: {event}")
|
||||
|
||||
|
||||
class Application(tk.Frame):
|
||||
def __init__(self, master=None):
|
||||
super().__init__(master)
|
||||
self.pack(fill=tk.BOTH, expand=1)
|
||||
self.images = []
|
||||
self.menubar = None
|
||||
self.create_menu()
|
||||
self.create_widgets()
|
||||
|
||||
def create_menu(self):
|
||||
self.master.option_add("*tearOff", tk.FALSE)
|
||||
self.menubar = tk.Menu(self.master)
|
||||
file_menu = tk.Menu(self.menubar)
|
||||
file_menu.add_command(label="Open")
|
||||
file_menu.add_command(label="Exit", command=root.quit)
|
||||
self.menubar.add_cascade(label="File", menu=file_menu)
|
||||
help_menu = tk.Menu(self.menubar)
|
||||
self.menubar.add_cascade(label="Help", menu=help_menu)
|
||||
self.master.config(menu=self.menubar)
|
||||
|
||||
def create_widgets(self):
|
||||
edit_frame = tk.Frame(self)
|
||||
edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2)
|
||||
b = tk.Button(edit_frame, text="Button 1")
|
||||
b.pack(side=tk.TOP, pady=1)
|
||||
b = tk.Button(edit_frame, text="Button 2")
|
||||
b.pack(side=tk.TOP, pady=1)
|
||||
b = tk.Button(edit_frame, text="Button 3")
|
||||
b.pack(side=tk.TOP, pady=1)
|
||||
|
||||
self.canvas = CanvasGraph(
|
||||
self, background="#cccccc", scrollregion=(0, 0, 1000, 1000)
|
||||
)
|
||||
self.canvas.load("switch", "switch.png")
|
||||
self.canvas.add_node(50, 50, "Node 1", "switch")
|
||||
self.canvas.add_node(50, 100, "Node 2", "switch")
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
scroll_x = tk.Scrollbar(
|
||||
self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview
|
||||
)
|
||||
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
scroll_y = tk.Scrollbar(self.canvas, command=self.canvas.yview)
|
||||
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.canvas.configure(xscrollcommand=scroll_x.set)
|
||||
self.canvas.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
status_bar = tk.Frame(self)
|
||||
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
b = tk.Button(status_bar, text="Button 1")
|
||||
b.pack(side=tk.LEFT, padx=1)
|
||||
b = tk.Button(status_bar, text="Button 2")
|
||||
b.pack(side=tk.LEFT, padx=1)
|
||||
b = tk.Button(status_bar, text="Button 3")
|
||||
b.pack(side=tk.LEFT, padx=1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
root.title("Graph Canvas")
|
||||
root.geometry("800x600")
|
||||
root.state("zoomed")
|
||||
app = Application(master=root)
|
||||
app.mainloop()
|
BIN
coretk/coretk/switch.png
Normal file
BIN
coretk/coretk/switch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
15
coretk/setup.cfg
Normal file
15
coretk/setup.cfg
Normal file
|
@ -0,0 +1,15 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[isort]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
|
||||
[flake8]
|
||||
ignore=E501,W503,E203
|
||||
max-line-length=100
|
||||
max-complexity=26
|
||||
select=B,C,E,F,W,T4
|
Loading…
Reference in a new issue