initial commit down a path for a possible core python tkinter gui

This commit is contained in:
Blake Harnden 2019-09-15 15:20:00 -07:00
parent c0ce07404f
commit 70ec532703
6 changed files with 450 additions and 0 deletions

15
coretk/Pipfile Normal file
View 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
View 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"
}
}
}

View file

286
coretk/coretk/graph.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

15
coretk/setup.cfg Normal file
View 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