From 70ec532703c45e04d7c3f00b3ddcad52b58d5edd Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 15:20:00 -0700 Subject: [PATCH 001/305] initial commit down a path for a possible core python tkinter gui --- coretk/Pipfile | 15 ++ coretk/Pipfile.lock | 134 ++++++++++++++++++ coretk/coretk/__init__.py | 0 coretk/coretk/graph.py | 286 ++++++++++++++++++++++++++++++++++++++ coretk/coretk/switch.png | Bin 0 -> 5286 bytes coretk/setup.cfg | 15 ++ 6 files changed, 450 insertions(+) create mode 100644 coretk/Pipfile create mode 100644 coretk/Pipfile.lock create mode 100644 coretk/coretk/__init__.py create mode 100644 coretk/coretk/graph.py create mode 100644 coretk/coretk/switch.png create mode 100644 coretk/setup.cfg diff --git a/coretk/Pipfile b/coretk/Pipfile new file mode 100644 index 00000000..ff97aa2f --- /dev/null +++ b/coretk/Pipfile @@ -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" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock new file mode 100644 index 00000000..3900d09b --- /dev/null +++ b/coretk/Pipfile.lock @@ -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" + } + } +} diff --git a/coretk/coretk/__init__.py b/coretk/coretk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py new file mode 100644 index 00000000..8de6ef81 --- /dev/null +++ b/coretk/coretk/graph.py @@ -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("", self.click_press) + self.bind("", self.click_release) + self.bind("", self.click_motion) + self.bind("", 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, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + self.canvas.tag_bind(self.id, "", 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() diff --git a/coretk/coretk/switch.png b/coretk/coretk/switch.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c852947639b68084a32c96f156ec1be99829e2 GIT binary patch literal 5286 zcmZ{ocQ72>*TevdH;B4-kINfX72sWoH^e)XYSlG_r&V!Y2Kw^p#T5?ceSAE2DgO%JIF|G z=NH=znztaa(a}`D<*l8oeF?t>xhK@b=a&By3V1x|qV%uR&r{Xmf1P82d&B?$eT9~~ zs$sy7?X19L^Y5X%%IiOagR^(?I7SofA0zIIlDXjs30cXbW|>HBBZYe_d&YK*3N4`K zdItt5n1G)RYrkJYlvjY?lZSy86#ZOvQGj76mjuMI9|DC;COmF%Ds|Y-TiEd)3k>(g z@>^3SN}N<)znoWS_Bh_iJIq`>J-y6Dz$^ZX-PH0fq=p|C=@vBEdd(5?`w;&FR!q#5 zB%=2?4O(mrLq>FK^}Gsyv$qg82)A4bH|tGVU!G6%0(FA*&kkwOj1$4FU(mjNhlZqXkQ(X2)u}mYMYO zS$`}{`@84MUubjcRx6RB+UMC8ASzoPEA<`c!2afK(-v#3ImM6S?pH9gp%~7NkVvB- zQZhVXE_LK-kBLG@-pPdm4~oH-vbWs_Mt>Qk*@vPo*7PHF2v$I^f8bN{eNeFAF4n=U zGNF9M+q;~yci~?(6XIkxtZ9}Sd$%8ZH}+%erC^Jgwo@cmXMQUlU`-VZeV1XFN?dMA zmr^Iz>K-JW9W11nGB1%c_}BZbW>Uk}zQ~~wNsDixVuexeOezA z=)b1zK00x&+IJY(I7y_vOiG{%1d&MPhvusY@t}04ke|t;p}jonuKj`~l4zU#^w^MP z^b@fgS0~BvH#Ix7spObK&YQS9rggCy%9pSDF1`)w3TK3P>ORM4u2?&m>>9@ux+ba! zO}PS{WeV==!wsh1u(=8eqB)4CF|QTVUcM2D&q`LCLaJs%1`qWZ%$pJV-p#=^{t*!C z2v1|=R-oX?%^v_=_|dbG*l*s)?6PicZPjbAc!R<0_;yZh2cMR74N`c8dLoa$s$C8x zR7gXTj-Sz6Fi?pPi`WQCGyhm*hCT%w(pYJ!_SAcckoN_sE}gc7?b*-}VFgPpdp6vQ z*H##;<`5qA1=QDk%T28YN* zJJDO0X(61`!buIP>ENUgq5?Ndk!*RbK*JK2idkE}mF4B?nPUgE0 zW-?l&Lf0yxtFp6&tB6p;jaS{pjj;Nz1jqE>r6{s=E)Da#I!T*QFuC;yhxQU7ogc-woTcxzbfl1RGyjE1_P82i#ZM@3@0%J+NqTQpC9JH8LXZt_x%-L z7xjZ3Idvm>qO}4}koklg4VL+>9sniYC5|_apg!}n?L+Gq2dYy@lPvA}c61)Y@#`r~ z43ztwyiE0=oWVQ!alFd%i;C>Bdx6sk4=`rWHjztToh@5k@Xcggsu8k4IX(X&x`OUm z&HC#3wQu@SR758=nW4Og(<{_UV4ZYdg4Ku)bHg2s9F{n&{7_X{lRdQu5^%M zOi~n@qAm<2b(h(`sGLy83i^i4lpbW>9KhWyW~u(rAjU31^+naX@cS~tkdi@0ogDUT zE3luH{h04-P9xF`b`I)JYyCfFc4lVul zjEayDrV_!&E01*$H0qqrQs;eg*=CwW=LJFO4xl_;Df{a^UB84_26EK1LlQM+P~`G5 z-2L3Pb2AyycC&J*_il6PyK{1=D7QJ|Nyw0iPhKkx?HflmPX-8{(en|ciWXu8YlKmp zvrq&k*gfE(wUv%TvNxvLb=MA89MC-g=(@N?DjRo1p4hhKyi2#4q-GXI6KNyf^Xq3J zqA)qkEL|z7cE>`vX6e+!I|mlPD?M&r>;5jJTM>7et8NFIMh$0?;bsX%jphQ%i!Hm3 zbHrIMin969_1|@=Z8iBW%5naOKo?9#6Ylh?NhV`E#%=g!`@2r8ajK$XlyBlw{c$Tz z#5>1(ZnGP4x)x6l=|T8wK4Ft*mYM}12!#kAGQH;|qqY#gw%DVwBd@wsHdCPt*)fC+ zi#5>b(@td?POxM+78k+SnUea>jkd|H8>L~WUEDa{x8$OB?{?|h_NIT5R>)h8HLn_` zlB$h(2{ObBR=;9(N>2qLm#!+sZC0kpA!veY{MTK(U6m}RV3G|MRty830Hc~Z4^f7O z)hEhf=HD6*@C&VLRnDK)SfkPiHD`NnY=kOx^SPF6dU)o~y(L8y(^wjdV7uqMLcmw)aKq=KZejxDc`@^aMI%ZdH z=G=kX(@*xS&WjC8KZy-g+AQQIX;naDaJ;LGLZGgp(dk17mMi$SKn_-dVSc)J>uy@G z(0*f~4BJYSi@5zfKIllqvnbU?L`XiDV|5u(ws9Xj{^_01R=wZLscAaL_e_GtX~sj{ z85Cye*km@l<+->>VP+4rudp|H1{h*KIilVl*q2IRh^umSUm9D}F{Vjc zF85BXx1Ehd8Jro|xKIj{F^k{Z%==XA;+r#J-tI~O>#SFG2GSw!5|;?t)*tEF#wsxC z#?BMCJ&&GH^I@~+*%?LMgDgc<@OsksvMhIhpLd;d z{2tQH-V4pYh#@g$LU|o+4F6r6@4ED8$#?&@-GrNXjs8fg#+S@4>h zl$xyob}5BF5ta4U3QfKEOP0O(HD%By_G=loe(OwVj4G+HY1t|$VcP*~ZNk`r4w3K| zvwQ;n3y{J!l&EW~j#10WNRIi;-}$@AUM%ndQEZ3~r8ziW9yu3LrAVN;7n*-P!uxgA zgs{3iX*xOK@zB~L(g`{>p2=aoJ17>inC}$k08oT^4d=$0U)R-FY4(Ggs-F+$vERX@ zh{2|ts!cde8=M*F*GbNBYSdn9>AQXPuBK*YPEKxiiwjJ%`%@qA%k0faWC^@po_dja}f;=3jO$KH^>T@d1zyBOUeIQiPE|W%ej|W-i~pMPKAp+rXU6 zj)kc9PP_?r$ZB3iLLXVc$0u6Ot2;<^*%~wxT0~zmm6NMl0uNz?U36I$i)5DaTKuqw4Vq?Asc<+VMAv$=F>7e+VQ%%4$gRd_b`@yW) z`{wEt5xq2f^|p>t%zkeF8`~x;FEA#(dbG;&=VLPr_~KB*&zIVhS8=F#Yt;5NY$&%? zX59mfT@)J$Di!}J+>XOKYmA`KZrj=yn*B3}&I0^oD#Qi%h*ZxF{s=X;l5)3(1BkIwFk_ze^y!#E^gVv?E0KME%cq1UwKh_{*;@i7sjotd0TUZazK ziAP6=&NoX>rbYmh8%tjVkB!JA-c0UU-kj=k{7`Nt^-PFSQsT9zNz!`@*GR=I6AxIE;e5WR}4O>TT9VAI|x0sc75wWef6f1 zel1!mlxmHl$Hhh(B%{Ks9X^P7lnpD|uozEk7us?fO>H%;dG({O#^}oi+mom~LaDog zD&^s79$bBHX~&-@n0r+w>n!g&tUv($H7SGr~T`pi4Yqj z_8fa7<&f#I)!Tn>60D!5molAdH<8eA~6`^8}J4UM_~vq!8;_<_jYRmXI+#T=ZAN)s@b1*7Oq=QKj)Bj-9hl5m`h{_H#+#*Ov6(~jQx4< zD<^Z*eg}Ddkw{;;sH`d8`lZ!=we8VxovL$N+~l}iegIF`1m^Ae#A!!Ar$?B$Y<2sm zXb|mVf%>+P(zm`rpZJhu(;}vwe4_7rx~&i3rObxcm92zB8H~G@UlY=gg|1hZhN+(Z z{#2guO;L{nrWA#<>_o+dTvpbi8!f2{W`lOL&i1LTroZvkbbK8)#-T!L>PlRES9(Jq zWQJI_)tqbuC<(c&NZu#+8SN9^Pza6F@ zE<7=FOMp*E{FA$5@9UfPVvxn%$|1JQ&`P|JI8W$g^6j_%1C2+W%mraK4kl`3;v`q)3Y($NXuPYeE(KcqV@p zcr=)hW zA}e_!2OOATv#>g_j7Uh(_1v#7sWNHES@+*wPuT@)=*V@y-umbn0@f6H(Z4@Pc>D2t z@m-qvTfD?+@~|RA#Qy9fwykjU`hZ{*-o&7Q>Z&Qb;~ugo-+p4HS!azwAc+0@g;zgB zv2Vs2f2{rU!${0a%;F~`7#7#HpkyH*l;x=VW+WiPQLg7~hDi9OXi@+30rfa3VkT_< zoegPE*~$fEx;f*{%@k|iirJxT+|8@2d7~~AF`qxjhUGy1 zXn=XXA3GlGM^?hXc{XkzCjG3(WPGg96+Q^@sxVWEG4Q9SP<&cWbaS*X3re@tRcSbG zKK=M~H>%)1Y(ccRMRCn|*TgX%#n|k)DfM+=gxAQjAni*l}sOSv%Mn*jd{KdJWovZ-W3@ M8hYwA5Ua@l0JEh!?f?J) literal 0 HcmV?d00001 diff --git a/coretk/setup.cfg b/coretk/setup.cfg new file mode 100644 index 00000000..d9228b5f --- /dev/null +++ b/coretk/setup.cfg @@ -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 From 28e1e7d796d45f3e265c1225f7889523500b043f Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 16:00:01 -0700 Subject: [PATCH 002/305] separated graph module from app for coretk --- coretk/coretk/app.py | 68 ++++++++++++++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 65 ---------------------------------------- 2 files changed, 68 insertions(+), 65 deletions(-) create mode 100644 coretk/coretk/app.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py new file mode 100644 index 00000000..cc664923 --- /dev/null +++ b/coretk/coretk/app.py @@ -0,0 +1,68 @@ +import tkinter as tk + +from coretk.graph import CanvasGraph + + +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() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 8de6ef81..d6e73e3c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -219,68 +219,3 @@ class CanvasNode: 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() From bcb7bf4a10a632032043e9d7805b08fc6835518a Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 16:38:12 -0700 Subject: [PATCH 003/305] coretk change to set a core-icon --- coretk/coretk/app.py | 18 ++++++++++++------ coretk/coretk/core-icon.png | Bin 0 -> 2931 bytes 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/core-icon.png diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index cc664923..5957054a 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,4 +1,5 @@ import tkinter as tk +from PIL import Image, ImageTk from coretk.graph import CanvasGraph @@ -6,18 +7,27 @@ from coretk.graph import CanvasGraph class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + self.master.title("CORE") + self.master.geometry("800x600") + self.master.state("zoomed") + self.set_icon() self.pack(fill=tk.BOTH, expand=1) self.images = [] self.menubar = None self.create_menu() self.create_widgets() + def set_icon(self): + image = Image.open("core-icon.png") + tk_image = ImageTk.PhotoImage(image) + self.master.tk.call("wm", "iconphoto", self.master._w, tk_image) + 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) + file_menu.add_command(label="Exit", command=self.master.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) @@ -60,9 +70,5 @@ class Application(tk.Frame): if __name__ == "__main__": - root = tk.Tk() - root.title("Graph Canvas") - root.geometry("800x600") - root.state("zoomed") - app = Application(master=root) + app = Application() app.mainloop() diff --git a/coretk/coretk/core-icon.png b/coretk/coretk/core-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0ff5aa3a51f71807691032f516d7a15d7da896 GIT binary patch literal 2931 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 literal 0 HcmV?d00001 From 6f916995d7fe36a892dda237c1b82e4df2b759f6 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 23:16:08 -0700 Subject: [PATCH 004/305] setup coretk left side buttons as radio buttons --- .gitignore | 3 +++ coretk/coretk/app.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 038146aa..700cbf17 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ ns3/setup.py # ignore corefx build corefx/target + +# python +__pycache__ diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5957054a..e8b559a5 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -36,11 +36,16 @@ class Application(tk.Frame): 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") + radio_value = tk.IntVar() + b = tk.Radiobutton(edit_frame, text="Button 1", indicatoron=False, variable=radio_value, value=1) b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 2") + b = tk.Radiobutton(edit_frame, text="Button 2", indicatoron=False, variable=radio_value, value=2) b.pack(side=tk.TOP, pady=1) - b = tk.Button(edit_frame, text="Button 3") + b = tk.Radiobutton(edit_frame, text="Button 3", indicatoron=False, variable=radio_value, value=3) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton(edit_frame, text="Button 4", indicatoron=False, variable=radio_value, value=4) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton(edit_frame, text="Button 5", indicatoron=False, variable=radio_value, value=5) b.pack(side=tk.TOP, pady=1) self.canvas = CanvasGraph( From d88ea50ad232a5f8ab2ed11ddc9801abaf1864c6 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Sun, 15 Sep 2019 23:45:13 -0700 Subject: [PATCH 005/305] coretk - changes to have app load all images, and some small cleanup --- coretk/coretk/app.py | 49 ++++++++++++++++++++++++++---------------- coretk/coretk/graph.py | 22 ++++++------------- coretk/coretk/icons.py | 15 +++++++++++++ 3 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 coretk/coretk/icons.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e8b559a5..9294b523 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,26 +1,29 @@ import tkinter as tk -from PIL import Image, ImageTk from coretk.graph import CanvasGraph +from coretk.icons import Images class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - self.master.title("CORE") - self.master.geometry("800x600") - self.master.state("zoomed") - self.set_icon() - self.pack(fill=tk.BOTH, expand=1) - self.images = [] + self.load_images() + self.setup_app() self.menubar = None self.create_menu() self.create_widgets() - def set_icon(self): - image = Image.open("core-icon.png") - tk_image = ImageTk.PhotoImage(image) - self.master.tk.call("wm", "iconphoto", self.master._w, tk_image) + def load_images(self): + Images.load("switch", "switch.png") + Images.load("core", "core-icon.png") + + def setup_app(self): + self.master.title("CORE") + self.master.geometry("800x600") + self.master.state("zoomed") + image = Images.get("core") + self.master.tk.call("wm", "iconphoto", self.master._w, image) + self.pack(fill=tk.BOTH, expand=True) def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) @@ -34,26 +37,34 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def create_widgets(self): + image = Images.get("switch") edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) radio_value = tk.IntVar() - b = tk.Radiobutton(edit_frame, text="Button 1", indicatoron=False, variable=radio_value, value=1) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=1, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 2", indicatoron=False, variable=radio_value, value=2) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=2, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 3", indicatoron=False, variable=radio_value, value=3) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=3, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 4", indicatoron=False, variable=radio_value, value=4) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=4, image=image + ) b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton(edit_frame, text="Button 5", indicatoron=False, variable=radio_value, value=5) + b = tk.Radiobutton( + edit_frame, indicatoron=False, variable=radio_value, value=5, image=image + ) 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 diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d6e73e3c..4e5cef74 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,7 +1,7 @@ import enum import tkinter as tk -from PIL import Image, ImageTk +from coretk.icons import Images class GraphMode(enum.Enum): @@ -11,14 +11,6 @@ class GraphMode(enum.Enum): 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 = {} @@ -78,7 +70,7 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, "Node", "switch") + self.add_node(x, y, "switch") def handle_edge_release(self, event): edge = self.drawing_edge @@ -146,9 +138,9 @@ class CanvasGraph(tk.Canvas): 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) + def add_node(self, x, y, image_name): + image = Images.get(image_name) + node = CanvasNode(x, y, image, self) self.nodes[node.id] = node return node @@ -175,13 +167,13 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, name, image, canvas): - self.name = name + def __init__(self, x, y, image, canvas): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + self.name = f"Node {self.id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) diff --git a/coretk/coretk/icons.py b/coretk/coretk/icons.py new file mode 100644 index 00000000..94517a14 --- /dev/null +++ b/coretk/coretk/icons.py @@ -0,0 +1,15 @@ +from PIL import Image, ImageTk + + +class Images: + images = {} + + @classmethod + def load(cls, name, file_path): + image = Image.open(file_path) + tk_image = ImageTk.PhotoImage(image) + cls.images[name] = tk_image + + @classmethod + def get(cls, name): + return cls.images[name] From 00a37cbb6fcd9018916a294e5085460574a4b065 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Tue, 17 Sep 2019 13:18:07 -0700 Subject: [PATCH 006/305] coretk - removed python3.7 hard requirement --- coretk/Pipfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index ff97aa2f..d5344207 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -10,6 +10,3 @@ black = "==19.3b0" [packages] pillow = "*" - -[requires] -python_version = "3.7" From 8682f01fdce59559678f48e4492c16d8161eef1b Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:20:22 -0700 Subject: [PATCH 007/305] coretk - updated pipfile to install as editable, added convenient run script, updated images to load assuming local path --- coretk/Pipfile | 5 ++++- coretk/Pipfile.lock | 11 ++++++----- coretk/coretk/app.py | 1 - coretk/coretk/icons.py | 4 ++++ coretk/setup.py | 14 ++++++++++++++ 5 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 coretk/setup.py diff --git a/coretk/Pipfile b/coretk/Pipfile index d5344207..3f8edcc3 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -3,10 +3,13 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true +[scripts] +coretk = "python coretk/app.py" + [dev-packages] flake8 = "*" isort = "*" black = "==19.3b0" [packages] -pillow = "*" +coretk = {editable = true,path = "."} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 3900d09b..1ed92ea4 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,12 +1,10 @@ { "_meta": { "hash": { - "sha256": "c719ca1ace4a33dfe01fe579162d4557e8711819c4e7b4f8e799dd25b9cde596" + "sha256": "f5abf95b09f7c7431b5d2143b43b6873cf9cf7d90dfb2eaad08d2446368a0a05" }, "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, + "requires": {}, "sources": [ { "name": "pypi", @@ -16,6 +14,10 @@ ] }, "default": { + "coretk": { + "editable": true, + "path": "." + }, "pillow": { "hashes": [ "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", @@ -45,7 +47,6 @@ "sha256:ef6be704ae2bc8ad0ebc5cb850ee9139493b0fc4e81abcc240fb392a63ebc808", "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" ], - "index": "pypi", "version": "==6.1.0" } }, diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9294b523..3beac831 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -20,7 +20,6 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("800x600") - self.master.state("zoomed") image = Images.get("core") self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/icons.py b/coretk/coretk/icons.py index 94517a14..8aa0ae28 100644 --- a/coretk/coretk/icons.py +++ b/coretk/coretk/icons.py @@ -1,11 +1,15 @@ +import os from PIL import Image, ImageTk +PATH = os.path.abspath(os.path.dirname(__file__)) + class Images: images = {} @classmethod def load(cls, name, file_path): + file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image diff --git a/coretk/setup.py b/coretk/setup.py new file mode 100644 index 00000000..d68365ff --- /dev/null +++ b/coretk/setup.py @@ -0,0 +1,14 @@ +from setuptools import find_packages, setup + +setup( + name="coretk", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "pillow", + ], + description="CORE GUI", + url="https://github.com/coreemu/core", + author="Boeing Research & Technology", + license="BSD", +) From 4a9e6febe59cb04d95b10c822153f820e1e37a84 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:21:41 -0700 Subject: [PATCH 008/305] coretk - changes icons.py to images.py --- coretk/coretk/app.py | 2 +- coretk/coretk/graph.py | 2 +- coretk/coretk/{icons.py => images.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename coretk/coretk/{icons.py => images.py} (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 3beac831..5ca0067c 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import tkinter as tk from coretk.graph import CanvasGraph -from coretk.icons import Images +from coretk.images import Images class Application(tk.Frame): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4e5cef74..23bc41bd 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,7 +1,7 @@ import enum import tkinter as tk -from coretk.icons import Images +from coretk.images import Images class GraphMode(enum.Enum): diff --git a/coretk/coretk/icons.py b/coretk/coretk/images.py similarity index 100% rename from coretk/coretk/icons.py rename to coretk/coretk/images.py From 17d1830176bdb7502fe2b359e48749a1deb5a831 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:25:33 -0700 Subject: [PATCH 009/305] coretk changed prints to use logging --- coretk/coretk/app.py | 2 ++ coretk/coretk/graph.py | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5ca0067c..163c8a17 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from coretk.graph import CanvasGraph @@ -85,5 +86,6 @@ class Application(tk.Frame): if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 23bc41bd..9bd599e9 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,4 +1,5 @@ import enum +import logging import tkinter as tk from coretk.images import Images @@ -65,7 +66,7 @@ class CanvasGraph(tk.Canvas): def click_release(self, event): self.focus_set() self.selected = self.get_selected(event) - print(f"click release selected: {self.selected}") + logging.debug(f"click release selected: {self.selected}") if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -81,8 +82,8 @@ class CanvasGraph(tk.Canvas): return # edge dst must be a node - print(f"current selected: {self.selected}") - print(f"current nodes: {self.find_withtag('node')}") + logging.debug(f"current selected: {self.selected}") + logging.debug(f"current nodes: {self.find_withtag('node')}") is_node = self.selected in self.find_withtag("node") if not is_node: edge.delete() @@ -95,7 +96,7 @@ class CanvasGraph(tk.Canvas): # 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}") + logging.debug(f"drawing edge token: {edge.token}") if edge.token in self.edges: edge.delete() else: @@ -105,10 +106,10 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - print(f"edges: {self.find_withtag('edge')}") + logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): - print(f"click press: {event}") + logging.debug(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: @@ -125,18 +126,18 @@ class CanvasGraph(tk.Canvas): selected = self.get_selected(event) nodes = self.find_withtag("node") if selected in nodes: - print(f"node context: {selected}") + logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) def set_mode(self, event): - print(f"mode event: {event}") + logging.debug(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}") + logging.debug(f"graph mode: {self.mode}") def add_node(self, x, y, image_name): image = Images.get(image_name) @@ -183,11 +184,11 @@ class CanvasNode: self.moving = None def click_press(self, event): - print(f"click press {self.name}: {event}") + logging.debug(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}") + logging.debug(f"click release {self.name}: {event}") self.moving = None def motion(self, event): @@ -210,4 +211,4 @@ class CanvasNode: self.canvas.coords(edge.id, x1, y1, new_x, new_y) def context(self, event): - print(f"context click {self.name}: {event}") + logging.debug(f"context click {self.name}: {event}") From 2f8935b4c99e9da8c2d035a7367e6bf767c22566 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:26:26 -0700 Subject: [PATCH 010/305] coretk - formatting cleanup --- coretk/coretk/images.py | 1 + coretk/setup.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 8aa0ae28..4ca2b5ac 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,4 +1,5 @@ import os + from PIL import Image, ImageTk PATH = os.path.abspath(os.path.dirname(__file__)) diff --git a/coretk/setup.py b/coretk/setup.py index d68365ff..8b8ce2d3 100644 --- a/coretk/setup.py +++ b/coretk/setup.py @@ -4,9 +4,7 @@ setup( name="coretk", version="0.1.0", packages=find_packages(), - install_requires=[ - "pillow", - ], + install_requires=["pillow"], description="CORE GUI", url="https://github.com/coreemu/core", author="Boeing Research & Technology", From 372a690af95df3ebd51cf90a1b0252b19f1116d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden Date: Wed, 18 Sep 2019 11:39:48 -0700 Subject: [PATCH 011/305] coretk - added to pre-commit --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 98 +++++++++++++++++++++++++++++++++- daemon/.pre-commit-config.yaml | 21 ++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index 3f8edcc3..720b1c45 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -10,6 +10,7 @@ coretk = "python coretk/app.py" flake8 = "*" isort = "*" black = "==19.3b0" +pre-commit = "*" [packages] coretk = {editable = true,path = "."} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 1ed92ea4..a08ee17f 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f5abf95b09f7c7431b5d2143b43b6873cf9cf7d90dfb2eaad08d2446368a0a05" + "sha256": "52de2a0b7a80abe39564a3943879fe75305b0cb8243c0fce78ef43689bee77c0" }, "pipfile-spec": 6, "requires": {}, @@ -58,6 +58,13 @@ ], "version": "==1.4.3" }, + "aspy.yaml": { + "hashes": [ + "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", + "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" + ], + "version": "==1.3.0" + }, "attrs": { "hashes": [ "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", @@ -73,6 +80,13 @@ "index": "pypi", "version": "==19.3b0" }, + "cfgv": { + "hashes": [ + "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144", + "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289" + ], + "version": "==2.0.1" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -95,6 +109,28 @@ "index": "pypi", "version": "==3.7.8" }, + "identify": { + "hashes": [ + "sha256:4f1fe9a59df4e80fcb0213086fcf502bc1765a01ea4fe8be48da3b65afd2a017", + "sha256:d8919589bd2a5f99c66302fec0ef9027b12ae150b0b0213999ad3f695fc7296e" + ], + "version": "==1.4.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "version": "==0.23" + }, + "importlib-resources": { + "hashes": [ + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + ], + "markers": "python_version < '3.7'", + "version": "==1.0.2" + }, "isort": { "hashes": [ "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", @@ -110,6 +146,27 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "pre-commit": { + "hashes": [ + "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", + "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" + ], + "index": "pypi", + "version": "==1.18.3" + }, "pycodestyle": { "hashes": [ "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", @@ -124,12 +181,51 @@ ], "version": "==2.1.1" }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" ], "version": "==0.10.0" + }, + "virtualenv": { + "hashes": [ + "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", + "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + ], + "version": "==16.7.5" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" } } } diff --git a/daemon/.pre-commit-config.yaml b/daemon/.pre-commit-config.yaml index 73566c9d..ac6bc80b 100644 --- a/daemon/.pre-commit-config.yaml +++ b/daemon/.pre-commit-config.yaml @@ -21,3 +21,24 @@ repos: language: system entry: bash -c 'cd daemon && pipenv run flake8' types: [python] + + - id: isort-tk + name: coretk-isort + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run isort --atomic -y' + types: [python] + + - id: black-tk + name: coretk-black + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run black .' + types: [python] + + - id: flake8-tk + name: coretk-flake8 + stages: [commit] + language: system + entry: bash -c 'cd coretk && pipenv run flake8' + types: [python] From 3268c110cd1bc31a844ae6db390e51409cab814e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 19 Sep 2019 10:35:35 -0700 Subject: [PATCH 012/305] gh action - adding coretk checks --- .github/workflows/coretk-checks.yml | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/coretk-checks.yml diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml new file mode 100644 index 00000000..324babfd --- /dev/null +++ b/.github/workflows/coretk-checks.yml @@ -0,0 +1,31 @@ +name: CORE Tk Checks + +on: [push] + +jobs: + build: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pipenv + run: | + python -m pip install --upgrade pip + pip install pipenv + cd coretk + pipenv install --dev + - name: isort + run: | + cd coretk + pipenv run isort -c + - name: black + run: | + cd coretk + pipenv run black --check . + - name: flake8 + run: | + cd coretk + pipenv run flake8 From 5297286b7ad06cc842dbc02c96547952b59436f4 Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Thu, 19 Sep 2019 16:24:21 -0700 Subject: [PATCH 013/305] working on menubar --- coretk/coretk/app.py | 583 +++++++++++++++++++++++++++++++++++- coretk/coretk/menuaction.py | 334 +++++++++++++++++++++ 2 files changed, 911 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/menuaction.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 163c8a17..8e023cac 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.menuaction as action from coretk.graph import CanvasGraph from coretk.images import Images @@ -25,16 +26,586 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def create_file_menu(self): + """ + Create file menu + + :return: nothing + """ + file_menu = tk.Menu(self.master) + file_menu.add_command( + label="New", command=action.file_new, accelerator="Ctrl+N" + ) + file_menu.add_command( + label="Open...", command=action.file_open, accelerator="Ctrl+O" + ) + file_menu.add_command(label="Reload", command=action.file_reload) + file_menu.add_command( + label="Save", command=action.file_save, accelerator="Ctrl+S" + ) + file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + + file_menu.add_separator() + + file_menu.add_command( + label="Export Python script...", command=action.file_export_python_script + ) + file_menu.add_command( + label="Execute XML or Python script...", + command=action.file_execute_xml_or_python_script, + ) + file_menu.add_command( + label="Execute Python script with options...", + command=action.file_execute_python_script_with_options, + ) + + file_menu.add_separator() + + file_menu.add_command( + label="Open current file in editor...", + command=action.file_open_current_file_in_editor, + ) + file_menu.add_command(label="Print...", command=action.file_print) + file_menu.add_command( + label="Save screenshot...", command=action.file_save_screenshot + ) + + file_menu.add_separator() + + file_menu.add_command( + label="/home/ncs/.core/configs/sample1.imn", + command=action.file_example_link, + ) + + file_menu.add_separator() + + file_menu.add_command(label="Quit", command=self.master.quit) + self.menubar.add_cascade(label="File", menu=file_menu) + + def create_edit_menu(self): + """ + Create edit menu + + :return: nothing + """ + edit_menu = tk.Menu(self.master) + edit_menu.add_command( + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" + ) + edit_menu.add_command( + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y" + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Cut", command=action.edit_cut, accelerator="Ctrl+X" + ) + edit_menu.add_command( + label="Copy", command=action.edit_copy, accelerator="Ctrl+C" + ) + edit_menu.add_command( + label="Paste", command=action.edit_paste, accelerator="Ctrl+V" + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" + ) + edit_menu.add_command( + label="Select Adjacent", + command=action.edit_select_adjacent, + accelerator="Ctrl+J", + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Find...", command=action.edit_find, accelerator="Ctrl+F" + ) + edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) + edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + + self.menubar.add_cascade(label="Edit", menu=edit_menu) + + def create_canvas_menu(self): + """ + Create canvas menu + + :return: nothing + """ + canvas_menu = tk.Menu(self.master) + canvas_menu.add_command(label="New", command=action.canvas_new) + canvas_menu.add_command(label="Manage...", command=action.canvas_manage) + canvas_menu.add_command(label="Delete", command=action.canvas_delete) + + canvas_menu.add_separator() + + canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + + canvas_menu.add_separator() + + canvas_menu.add_command( + label="Previous", command=action.canvas_previous, accelerator="PgUp" + ) + canvas_menu.add_command( + label="Next", command=action.canvas_next, accelerator="PgDown" + ) + canvas_menu.add_command( + label="First", command=action.canvas_first, accelerator="Home" + ) + canvas_menu.add_command( + label="Last", command=action.canvas_last, accelerator="End" + ) + + self.menubar.add_cascade(label="Canvas", menu=canvas_menu) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + show_menu = tk.Menu(self.master) + show_menu.add_command(label="All") + show_menu.add_command(label="None") + show_menu.add_separator() + show_menu.add_command(label="Interface Names") + show_menu.add_command(label="IPv4 Addresses") + show_menu.add_command(label="IPv6 Addresses") + show_menu.add_command(label="Node Labels") + show_menu.add_command(label="Annotations") + show_menu.add_command(label="Grid") + show_menu.add_command(label="API Messages") + + view_menu.add_cascade(label="Show", menu=show_menu) + + def create_view_menu(self): + """ + Create view menu + + :return: nothing + """ + view_menu = tk.Menu(self.master) + self.create_show_menu(view_menu) + view_menu.add_command( + label="Show hidden nodes", command=action.view_show_hidden_nodes + ) + view_menu.add_command(label="Locked", command=action.view_locked) + view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + + view_menu.add_separator() + + view_menu.add_command( + label="Zoom in", command=action.view_zoom_in, accelerator="+" + ) + view_menu.add_command( + label="Zoom out", command=action.view_zoom_out, accelerator="-" + ) + + self.menubar.add_cascade(label="View", menu=view_menu) + + def create_experimental_menu(self, tools_menu): + experimental_menu = tk.Menu(self.master) + experimental_menu.add_command(label="Plugins...") + experimental_menu.add_command(label="ns2immunes converter...") + experimental_menu.add_command(label="Topology partitioning...") + + tools_menu.add_cascade(label="Experimental", menu=experimental_menu) + + def create_random_menu(self, topology_generator_menu): + """ + Create random menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + random_menu = tk.Menu(self.master) + random_menu.add_command(label="R(1)") + random_menu.add_command(label="R(5)") + random_menu.add_command(label="R(10)") + random_menu.add_command(label="R(15)") + random_menu.add_command(label="R(20)") + random_menu.add_command(label="R(30)") + random_menu.add_command(label="R(40)") + random_menu.add_command(label="R(50)") + random_menu.add_command(label="R(75)") + random_menu.add_command(label="R(100)") + + topology_generator_menu.add_cascade(label="Random", menu=random_menu) + + def create_grid_menu(self, topology_generator_menu): + """ + Create grid menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + grid_menu = tk.Menu(self.master) + grid_menu.add_command(label="G(1)") + grid_menu.add_command(label="G(5)") + grid_menu.add_command(label="G(10)") + grid_menu.add_command(label="G(15)") + grid_menu.add_command(label="G(20)") + grid_menu.add_command(label="G(25)") + grid_menu.add_command(label="G(30)") + grid_menu.add_command(label="G(35)") + grid_menu.add_command(label="G(40)") + grid_menu.add_command(label="G(50)") + grid_menu.add_command(label="G(60)") + grid_menu.add_command(label="G(70)") + grid_menu.add_command(label="G(80)") + grid_menu.add_command(label="G(90)") + grid_menu.add_command(label="G(100)") + + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) + + # TODO do later + def create_connected_grid_menu(self, topology_generator_menu): + """ + Create connected grid menu items and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + return + + def create_chain_menu(self, topology_generator_menu): + """ + Create chain menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + chain_menu = tk.Menu(self.master) + chain_menu.add_command(label="P(2)") + chain_menu.add_command(label="P(3)") + chain_menu.add_command(label="P(4)") + chain_menu.add_command(label="P(5)") + chain_menu.add_command(label="P(6)") + chain_menu.add_command(label="P(7)") + chain_menu.add_command(label="P(8)") + chain_menu.add_command(label="P(9)") + chain_menu.add_command(label="P(10)") + chain_menu.add_command(label="P(11)") + chain_menu.add_command(label="P(12)") + chain_menu.add_command(label="P(13)") + chain_menu.add_command(label="P(14)") + chain_menu.add_command(label="P(15)") + chain_menu.add_command(label="P(16)") + chain_menu.add_command(label="P(17)") + chain_menu.add_command(label="P(18)") + chain_menu.add_command(label="P(19)") + chain_menu.add_command(label="P(20)") + chain_menu.add_command(label="P(21)") + chain_menu.add_command(label="P(22)") + chain_menu.add_command(label="P(23)") + chain_menu.add_command(label="P(24)") + chain_menu.add_command(label="P(32)") + chain_menu.add_command(label="P(64)") + chain_menu.add_command(label="P(128)") + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) + + def create_star_menu(self, topology_generator_menu): + """ + Create star menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + star_menu = tk.Menu(self.master) + for i in range(3, 26, 1): + the_label = "C(" + str(i) + ")" + star_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Star", menu=star_menu) + + def create_cycle_menu(self, topology_generator_menu): + """ + Create cycle menu item and the sub items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cycle_menu = tk.Menu(self.master) + for i in range(3, 25, 1): + the_label = "C(" + str(i) + ")" + cycle_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) + + def create_wheel_menu(self, topology_generator_menu): + """ + Create wheel menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + wheel_menu = tk.Menu(self.master) + for i in range(4, 26, 1): + the_label = "W(" + str(i) + ")" + wheel_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) + + def create_cube_menu(self, topology_generator_menu): + """ + Create cube menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cube_menu = tk.Menu(self.master) + for i in range(2, 7, 1): + the_label = "Q(" + str(i) + ")" + cube_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) + + def create_clique_menu(self, topology_generator_menu): + """ + Create clique menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + clique_menu = tk.Menu(self.master) + for i in range(3, 25, 1): + the_label = "K(" + str(i) + ")" + clique_menu.add_command(label=the_label) + + topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) + + # TODO do later + def create_bipartite_menu(self, topology_generator_menu): + """ + Create bipartite menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + # bipartite_menu = tk.Menu(self.master) + return + + def create_topology_generator_menu(self, tools_menu): + """ + Create topology menu item and its sub menu items + + :param tkinter.Menu tools_menu: tools menu + + :return: nothing + """ + topology_generator_menu = tk.Menu(self.master) + # topology_generator_menu.add_command(label="Random") + self.create_random_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Grid") + self.create_grid_menu(topology_generator_menu) + topology_generator_menu.add_command(label="Connected Grid") + self.create_chain_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Chain") + # topology_generator_menu.add_command(label="Star") + self.create_star_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Cycle") + self.create_cycle_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Wheel") + self.create_wheel_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Cube") + self.create_cube_menu(topology_generator_menu) + # topology_generator_menu.add_command(label="Clique") + self.create_clique_menu(topology_generator_menu) + topology_generator_menu.add_command(label="Bipartite") + + tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) + + def create_tools_menu(self): + """ + Create tools menu + + :return: nothing + """ + + tools_menu = tk.Menu(self.master) + tools_menu.add_command( + label="Auto rearrange all", command=action.tools_auto_rearrange_all + ) + tools_menu.add_command( + label="Auto rearrange selected", + command=action.tools_auto_rearrange_selected, + ) + tools_menu.add_separator() + + tools_menu.add_command( + label="Align to grid", command=action.tools_align_to_grid + ) + + tools_menu.add_separator() + + tools_menu.add_command(label="Traffic...", command=action.tools_traffic) + tools_menu.add_command( + label="IP addresses...", command=action.tools_ip_addresses + ) + tools_menu.add_command( + label="MAC addresses...", command=action.tools_mac_addresses + ) + tools_menu.add_command( + label="Build hosts file...", command=action.tools_build_hosts_file + ) + tools_menu.add_command( + label="Renumber nodes...", command=action.tools_renumber_nodes + ) + self.create_experimental_menu(tools_menu) + self.create_topology_generator_menu(tools_menu) + tools_menu.add_command(label="Debugger...", command=action.tools_debugger) + + self.menubar.add_cascade(label="Tools", menu=tools_menu) + + def create_observer_widgets_menu(self, widget_menu): + """ + Create observer widget menu item and create the sub menu items inside + + :param tkinter.Menu widget_menu: widget_menu + :return: nothing + """ + observer_widget_menu = tk.Menu(self.master) + observer_widget_menu.add_command(label="None") + observer_widget_menu.add_command(label="processes") + observer_widget_menu.add_command(label="ifconfig") + observer_widget_menu.add_command(label="IPv4 routes") + observer_widget_menu.add_command(label="IPv6 routes") + observer_widget_menu.add_command(label="OSPFv2 neighbors") + observer_widget_menu.add_command(label="OSPFv3 neighbors") + observer_widget_menu.add_command(label="Listening sockets") + observer_widget_menu.add_command(label="IPv4 MFC entries") + observer_widget_menu.add_command(label="IPv6 MFC entries") + observer_widget_menu.add_command(label="firewall rules") + observer_widget_menu.add_command(label="IPsec policies") + observer_widget_menu.add_command(label="docker logs") + observer_widget_menu.add_command(label="OSPFv3 MDR level") + observer_widget_menu.add_command(label="PIM neighbors") + observer_widget_menu.add_command(label="Edit...") + + widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + + def create_adjacency_menu(self, widget_menu): + """ + Create adjacency menu item and the sub menu items inside + + :param tkinter.Menu widget_menu: widget menu + :return: nothing + """ + adjacency_menu = tk.Menu(self.master) + adjacency_menu.add_command(label="OSPFv2") + adjacency_menu.add_command(label="OSPFv3") + adjacency_menu.add_command(label="OSLR") + adjacency_menu.add_command(label="OSLRv2") + + widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + + def create_widgets_menu(self): + """ + Create widget menu + + :return: nothing + """ + widget_menu = tk.Menu(self.master) + self.create_observer_widgets_menu(widget_menu) + self.create_adjacency_menu(widget_menu) + widget_menu.add_command(label="Throughput", command=action.widgets_throughput) + + widget_menu.add_separator() + + widget_menu.add_command( + label="Configure Adjacency...", command=action.widgets_configure_adjacency + ) + widget_menu.add_command( + label="Configure Throughput...", command=action.widgets_configure_throughput + ) + + self.menubar.add_cascade(label="Widgets", menu=widget_menu) + + def create_session_menu(self): + """ + Create session menu + + :return: nothing + """ + session_menu = tk.Menu(self.master) + session_menu.add_command(label="Start", command=action.session_start) + session_menu.add_command( + label="Change sessions...", command=action.session_change_sessions + ) + + session_menu.add_separator() + + session_menu.add_command( + label="Node types...", command=action.session_node_types + ) + session_menu.add_command(label="Comments...", command=action.session_comments) + session_menu.add_command(label="Hooks...", command=action.session_hooks) + session_menu.add_command( + label="Reset node positions", command=action.session_reset_node_positions + ) + session_menu.add_command( + label="Emulation servers...", command=action.session_emulation_servers + ) + session_menu.add_command(label="Options...", command=action.session_options) + + self.menubar.add_cascade(label="Session", menu=session_menu) + + def create_help_menu(self): + """ + Create help menu + + :return: nothing + """ + help_menu = tk.Menu(self.master) + help_menu.add_command( + label="Core Github (www)", command=action.help_core_github + ) + help_menu.add_command( + label="Core Documentation (www)", command=action.help_core_documentation + ) + help_menu.add_command(label="About", command=action.help_about) + + self.menubar.add_cascade(label="Help", menu=help_menu) + + def bind_menubar_shortcut(self): + self.bind_all("", action.file_new_shortcut) + self.bind_all("", action.file_open_shortcut) + self.bind_all("", action.file_save_shortcut) + self.bind_all("", action.edit_undo_shortcut) + self.bind_all("", action.edit_redo_shortcut) + self.bind_all("", action.edit_cut_shortcut) + self.bind_all("", action.edit_copy_shortcut) + self.bind_all("", action.edit_paste_shortcut) + self.bind_all("", action.edit_select_all_shortcut) + self.bind_all("", action.edit_select_adjacent_shortcut) + self.bind_all("", action.edit_find_shortcut) + self.bind_all("", action.canvas_previous_shortcut) + self.bind_all("", action.canvas_next_shortcut) + self.bind_all("", action.canvas_first_shortcut) + self.bind_all("", action.canvas_last_shortcut) + self.bind_all("", action.view_zoom_in_shortcut) + self.bind_all("", action.view_zoom_out_shortcut) + 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=self.master.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.create_file_menu() + self.create_edit_menu() + self.create_canvas_menu() + self.create_view_menu() + self.create_tools_menu() + self.create_widgets_menu() + self.create_session_menu() + self.create_help_menu() + self.master.config(menu=self.menubar) + self.bind_menubar_shortcut() def create_widgets(self): image = Images.get("switch") diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py new file mode 100644 index 00000000..005776f9 --- /dev/null +++ b/coretk/coretk/menuaction.py @@ -0,0 +1,334 @@ +""" +The actions taken when each menubar option is clicked +""" + +import logging +import webbrowser + + +def file_new(): + logging.debug("Click file New") + + +def file_new_shortcut(event): + logging.debug("Shortcut for file new shortcut") + + +def file_open(): + logging.debug("Click file Open") + + +def file_open_shortcut(event): + logging.debug("Shortcut for file open") + + +def file_reload(): + logging.debug("Click file Reload") + + +def file_save(): + logging.debug("Click file save") + + +def file_save_shortcut(event): + logging.debug("Shortcut for file save") + + +def file_save_as_xml(): + logging.debug("Click save as XML") + + +def file_save_as_imn(): + logging.debug("Click save as imn") + + +def file_export_python_script(): + logging.debug("Click file export python script") + + +def file_execute_xml_or_python_script(): + logging.debug("Execute XML or Python script") + + +def file_execute_python_script_with_options(): + logging.debug("Click execute Python script with options") + + +def file_open_current_file_in_editor(): + logging.debug("Click file open current in editor") + + +def file_print(): + logging.debug("Click file Print") + + +def file_save_screenshot(): + logging.debug("Click file save screenshot") + + +def file_example_link(): + logging.debug("Click file example link") + + +def edit_undo(): + logging.debug("Click edit undo") + + +def edit_undo_shortcut(event): + logging.debug("Shortcut for edit undo") + + +def edit_redo(): + logging.debug("Click edit redo") + + +def edit_redo_shortcut(event): + logging.debug("Shortcut for edit redo") + + +def edit_cut(): + logging.debug("Click edit cut") + + +def edit_cut_shortcut(event): + logging.debug("Shortcut for edit cut") + + +def edit_copy(): + logging.debug("Click edit copy") + + +def edit_copy_shortcut(event): + logging.debug("Shortcut for edit copy") + + +def edit_paste(): + logging.debug("Click edit paste") + + +def edit_paste_shortcut(event): + logging.debug("Shortcut for edit paste") + + +def edit_select_all(): + logging.debug("Click edit select all") + + +def edit_select_all_shortcut(event): + logging.debug("Shortcut for edit select all") + + +def edit_select_adjacent(): + logging.debug("Click edit select adjacent") + + +def edit_select_adjacent_shortcut(event): + logging.debug("Shortcut for edit select adjacent") + + +def edit_find(): + logging.debug("CLick edit find") + + +def edit_find_shortcut(event): + logging.debug("Shortcut for edit find") + + +def edit_clear_marker(): + logging.debug("Click edit clear marker") + + +def edit_preferences(): + logging.debug("Click preferences") + + +def canvas_new(): + logging.debug("Click canvas new") + + +def canvas_manage(): + logging.debug("Click canvas manage") + + +def canvas_delete(): + logging.debug("Click canvas delete") + + +def canvas_size_scale(): + logging.debug("Click canvas size/scale") + + +def canvas_wallpaper(): + logging.debug("CLick canvas wallpaper") + + +def canvas_previous(): + logging.debug("Click canvas previous") + + +def canvas_previous_shortcut(event): + logging.debug("Shortcut for canvas previous") + + +def canvas_next(): + logging.debug("Click canvas next") + + +def canvas_next_shortcut(event): + logging.debug("Shortcut for canvas next") + + +def canvas_first(): + logging.debug("CLick canvas first") + + +def canvas_first_shortcut(event): + logging.debug("Shortcut for canvas first") + + +def canvas_last(): + logging.debug("CLick canvas last") + + +def canvas_last_shortcut(event): + logging.debug("Shortcut canvas last") + + +def view_show(): + logging.debug("Click view show") + + +def view_show_hidden_nodes(): + logging.debug("Click view show hidden nodes") + + +def view_locked(): + logging.debug("Click view locked") + + +def view_3d_gui(): + logging.debug("CLick view 3D GUI") + + +def view_zoom_in(): + logging.debug("Click view zoom in") + + +def view_zoom_in_shortcut(event): + logging.debug("Shortcut view zoom in") + + +def view_zoom_out(): + logging.debug("Click view zoom out") + + +def view_zoom_out_shortcut(event): + logging.debug("Shortcut view zoom out") + + +def tools_auto_rearrange_all(): + logging.debug("Click tools, auto rearrange all") + + +def tools_auto_rearrange_selected(): + logging.debug("CLick tools auto rearrange selected") + + +def tools_align_to_grid(): + logging.debug("Click tools align to grid") + + +def tools_traffic(): + logging.debug("Click tools traffic") + + +def tools_ip_addresses(): + logging.debug("Click tools ip addresses") + + +def tools_mac_addresses(): + logging.debug("Click tools mac addresses") + + +def tools_build_hosts_file(): + logging.debug("Click tools build hosts file") + + +def tools_renumber_nodes(): + logging.debug("Click tools renumber nodes") + + +def tools_experimental(): + logging.debug("Click tools experimental") + + +def tools_topology_generator(): + logging.debug("Click tools topology generator") + + +def tools_debugger(): + logging.debug("Click tools debugger") + + +def widgets_observer_widgets(): + logging.debug("Click widgets observer widgets") + + +def widgets_adjacency(): + logging.debug("Click widgets adjacency") + + +def widgets_throughput(): + logging.debug("Click widgets throughput") + + +def widgets_configure_adjacency(): + logging.debug("Click widgets configure adjacency") + + +def widgets_configure_throughput(): + logging.debug("Click widgets configure throughput") + + +def session_start(): + logging.debug("Click session start") + + +def session_change_sessions(): + logging.debug("Click session change sessions") + + +def session_node_types(): + logging.debug("Click session node types") + + +def session_comments(): + logging.debug("Click session comments") + + +def session_hooks(): + logging.debug("Click session hooks") + + +def session_reset_node_positions(): + logging.debug("Click session reset node positions") + + +def session_emulation_servers(): + logging.debug("Click session emulation servers") + + +def session_options(): + logging.debug("Click session options") + + +def help_core_github(): + webbrowser.open_new("https://github.com/coreemu/core") + + +def help_core_documentation(): + webbrowser.open_new("http://coreemu.github.io/core/") + + +def help_about(): + logging.debug("Click help About") From 303e96cdd6c59ddf844bc2aadf29e10e9ac433b0 Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Fri, 20 Sep 2019 10:45:35 -0700 Subject: [PATCH 014/305] finish up menubar --- coretk/coretk/app.py | 266 +++++++++++++++++++----------------- coretk/coretk/menuaction.py | 4 + 2 files changed, 146 insertions(+), 124 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 8e023cac..bd0fd680 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -32,7 +32,7 @@ class Application(tk.Frame): :return: nothing """ - file_menu = tk.Menu(self.master) + file_menu = tk.Menu(self.menubar) file_menu.add_command( label="New", command=action.file_new, accelerator="Ctrl+N" ) @@ -89,7 +89,7 @@ class Application(tk.Frame): :return: nothing """ - edit_menu = tk.Menu(self.master) + edit_menu = tk.Menu(self.menubar) edit_menu.add_command( label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" ) @@ -136,7 +136,7 @@ class Application(tk.Frame): :return: nothing """ - canvas_menu = tk.Menu(self.master) + canvas_menu = tk.Menu(self.menubar) canvas_menu.add_command(label="New", command=action.canvas_new) canvas_menu.add_command(label="Manage...", command=action.canvas_manage) canvas_menu.add_command(label="Delete", command=action.canvas_delete) @@ -170,17 +170,17 @@ class Application(tk.Frame): :param tkinter.Menu view_menu: the view menu :return: nothing """ - show_menu = tk.Menu(self.master) - show_menu.add_command(label="All") - show_menu.add_command(label="None") + show_menu = tk.Menu(view_menu) + show_menu.add_command(label="All", command=action.sub_menu_items) + show_menu.add_command(label="None", command=action.sub_menu_items) show_menu.add_separator() - show_menu.add_command(label="Interface Names") - show_menu.add_command(label="IPv4 Addresses") - show_menu.add_command(label="IPv6 Addresses") - show_menu.add_command(label="Node Labels") - show_menu.add_command(label="Annotations") - show_menu.add_command(label="Grid") - show_menu.add_command(label="API Messages") + show_menu.add_command(label="Interface Names", command=action.sub_menu_items) + show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="Node Labels", command=action.sub_menu_items) + show_menu.add_command(label="Annotations", command=action.sub_menu_items) + show_menu.add_command(label="Grid", command=action.sub_menu_items) + show_menu.add_command(label="API Messages", command=action.sub_menu_items) view_menu.add_cascade(label="Show", menu=show_menu) @@ -190,7 +190,7 @@ class Application(tk.Frame): :return: nothing """ - view_menu = tk.Menu(self.master) + view_menu = tk.Menu(self.menubar, tearoff=True) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes @@ -210,10 +210,20 @@ class Application(tk.Frame): self.menubar.add_cascade(label="View", menu=view_menu) def create_experimental_menu(self, tools_menu): - experimental_menu = tk.Menu(self.master) - experimental_menu.add_command(label="Plugins...") - experimental_menu.add_command(label="ns2immunes converter...") - experimental_menu.add_command(label="Topology partitioning...") + """ + Create experimental menu item and the sub menu items inside + + :param tkinter.Menu tools_menu: tools menu + :return: nothing + """ + experimental_menu = tk.Menu(tools_menu, tearoff=True) + experimental_menu.add_command(label="Plugins...", command=action.sub_menu_items) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items + ) + experimental_menu.add_command( + label="Topology partitioning...", command=action.sub_menu_items + ) tools_menu.add_cascade(label="Experimental", menu=experimental_menu) @@ -224,17 +234,12 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - random_menu = tk.Menu(self.master) - random_menu.add_command(label="R(1)") - random_menu.add_command(label="R(5)") - random_menu.add_command(label="R(10)") - random_menu.add_command(label="R(15)") - random_menu.add_command(label="R(20)") - random_menu.add_command(label="R(30)") - random_menu.add_command(label="R(40)") - random_menu.add_command(label="R(50)") - random_menu.add_command(label="R(75)") - random_menu.add_command(label="R(100)") + random_menu = tk.Menu(topology_generator_menu) + # list of number of random nodes to create + nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] + for i in nums: + the_label = "R(" + str(i) + ")" + random_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Random", menu=random_menu) @@ -245,26 +250,17 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - grid_menu = tk.Menu(self.master) - grid_menu.add_command(label="G(1)") - grid_menu.add_command(label="G(5)") - grid_menu.add_command(label="G(10)") - grid_menu.add_command(label="G(15)") - grid_menu.add_command(label="G(20)") - grid_menu.add_command(label="G(25)") - grid_menu.add_command(label="G(30)") - grid_menu.add_command(label="G(35)") - grid_menu.add_command(label="G(40)") - grid_menu.add_command(label="G(50)") - grid_menu.add_command(label="G(60)") - grid_menu.add_command(label="G(70)") - grid_menu.add_command(label="G(80)") - grid_menu.add_command(label="G(90)") - grid_menu.add_command(label="G(100)") + grid_menu = tk.Menu(topology_generator_menu) + + # list of number of nodes to create + nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] + + for i in nums: + the_label = "G(" + str(i) + ")" + grid_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) - # TODO do later def create_connected_grid_menu(self, topology_generator_menu): """ Create connected grid menu items and the sub menu items inside @@ -272,7 +268,15 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - return + grid_menu = tk.Menu(topology_generator_menu) + for i in range(1, 11, 1): + i_n_menu = tk.Menu(grid_menu) + for j in range(1, 11, 1): + i_j_label = str(i) + " X " + str(j) + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = str(i) + " X N" + grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) + topology_generator_menu.add_cascade(label="Connected Grid", menu=grid_menu) def create_chain_menu(self, topology_generator_menu): """ @@ -281,33 +285,13 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - chain_menu = tk.Menu(self.master) - chain_menu.add_command(label="P(2)") - chain_menu.add_command(label="P(3)") - chain_menu.add_command(label="P(4)") - chain_menu.add_command(label="P(5)") - chain_menu.add_command(label="P(6)") - chain_menu.add_command(label="P(7)") - chain_menu.add_command(label="P(8)") - chain_menu.add_command(label="P(9)") - chain_menu.add_command(label="P(10)") - chain_menu.add_command(label="P(11)") - chain_menu.add_command(label="P(12)") - chain_menu.add_command(label="P(13)") - chain_menu.add_command(label="P(14)") - chain_menu.add_command(label="P(15)") - chain_menu.add_command(label="P(16)") - chain_menu.add_command(label="P(17)") - chain_menu.add_command(label="P(18)") - chain_menu.add_command(label="P(19)") - chain_menu.add_command(label="P(20)") - chain_menu.add_command(label="P(21)") - chain_menu.add_command(label="P(22)") - chain_menu.add_command(label="P(23)") - chain_menu.add_command(label="P(24)") - chain_menu.add_command(label="P(32)") - chain_menu.add_command(label="P(64)") - chain_menu.add_command(label="P(128)") + chain_menu = tk.Menu(topology_generator_menu) + # number of nodes to create + nums = list(range(2, 25, 1)) + [32, 64, 128] + for i in nums: + the_label = "P(" + str(i) + ")" + chain_menu.add_command(label=the_label, command=action.sub_menu_items) + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) def create_star_menu(self, topology_generator_menu): @@ -317,10 +301,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - star_menu = tk.Menu(self.master) + star_menu = tk.Menu(topology_generator_menu) for i in range(3, 26, 1): the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label) + star_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Star", menu=star_menu) @@ -331,10 +315,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cycle_menu = tk.Menu(self.master) + cycle_menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label) + cycle_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) @@ -345,10 +329,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - wheel_menu = tk.Menu(self.master) + wheel_menu = tk.Menu(topology_generator_menu) for i in range(4, 26, 1): the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label) + wheel_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) @@ -359,10 +343,10 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cube_menu = tk.Menu(self.master) + cube_menu = tk.Menu(topology_generator_menu) for i in range(2, 7, 1): the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label) + cube_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) @@ -373,14 +357,13 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - clique_menu = tk.Menu(self.master) + clique_menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label) + clique_menu.add_command(label=the_label, command=action.sub_menu_items) topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) - # TODO do later def create_bipartite_menu(self, topology_generator_menu): """ Create bipartite menu item and the sub menu items inside @@ -388,8 +371,17 @@ class Application(tk.Frame): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - # bipartite_menu = tk.Menu(self.master) - return + bipartite_menu = tk.Menu(topology_generator_menu) + temp = 24 + for i in range(1, 13, 1): + i_n_menu = tk.Menu(bipartite_menu) + for j in range(i, temp, 1): + i_j_label = "K(" + str(i) + " X " + str(j) + ")" + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = "K(" + str(i) + " X N)" + bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + temp = temp - 1 + topology_generator_menu.add_cascade(label="Bipartite", menu=bipartite_menu) def create_topology_generator_menu(self, tools_menu): """ @@ -399,25 +391,18 @@ class Application(tk.Frame): :return: nothing """ - topology_generator_menu = tk.Menu(self.master) - # topology_generator_menu.add_command(label="Random") + topology_generator_menu = tk.Menu(tools_menu, tearoff=True) + self.create_random_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Grid") self.create_grid_menu(topology_generator_menu) - topology_generator_menu.add_command(label="Connected Grid") + self.create_connected_grid_menu(topology_generator_menu) self.create_chain_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Chain") - # topology_generator_menu.add_command(label="Star") self.create_star_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Cycle") self.create_cycle_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Wheel") self.create_wheel_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Cube") self.create_cube_menu(topology_generator_menu) - # topology_generator_menu.add_command(label="Clique") self.create_clique_menu(topology_generator_menu) - topology_generator_menu.add_command(label="Bipartite") + self.create_bipartite_menu(topology_generator_menu) tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) @@ -428,7 +413,7 @@ class Application(tk.Frame): :return: nothing """ - tools_menu = tk.Menu(self.master) + tools_menu = tk.Menu(self.menubar) tools_menu.add_command( label="Auto rearrange all", command=action.tools_auto_rearrange_all ) @@ -470,23 +455,51 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(self.master) - observer_widget_menu.add_command(label="None") - observer_widget_menu.add_command(label="processes") - observer_widget_menu.add_command(label="ifconfig") - observer_widget_menu.add_command(label="IPv4 routes") - observer_widget_menu.add_command(label="IPv6 routes") - observer_widget_menu.add_command(label="OSPFv2 neighbors") - observer_widget_menu.add_command(label="OSPFv3 neighbors") - observer_widget_menu.add_command(label="Listening sockets") - observer_widget_menu.add_command(label="IPv4 MFC entries") - observer_widget_menu.add_command(label="IPv6 MFC entries") - observer_widget_menu.add_command(label="firewall rules") - observer_widget_menu.add_command(label="IPsec policies") - observer_widget_menu.add_command(label="docker logs") - observer_widget_menu.add_command(label="OSPFv3 MDR level") - observer_widget_menu.add_command(label="PIM neighbors") - observer_widget_menu.add_command(label="Edit...") + observer_widget_menu = tk.Menu(widget_menu, tearoff=True) + observer_widget_menu.add_command(label="None", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="processes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="ifconfig", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv2 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="Listening sockets", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="firewall rules", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPsec policies", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="docker logs", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 MDR level", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="PIM neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) @@ -497,11 +510,11 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(self.master) - adjacency_menu.add_command(label="OSPFv2") - adjacency_menu.add_command(label="OSPFv3") - adjacency_menu.add_command(label="OSLR") - adjacency_menu.add_command(label="OSLRv2") + adjacency_menu = tk.Menu(widget_menu, tearoff=True) + adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) @@ -511,7 +524,7 @@ class Application(tk.Frame): :return: nothing """ - widget_menu = tk.Menu(self.master) + widget_menu = tk.Menu(self.menubar, tearoff=True) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) @@ -533,7 +546,7 @@ class Application(tk.Frame): :return: nothing """ - session_menu = tk.Menu(self.master) + session_menu = tk.Menu(self.menubar, tearoff=True) session_menu.add_command(label="Start", command=action.session_start) session_menu.add_command( label="Change sessions...", command=action.session_change_sessions @@ -562,7 +575,7 @@ class Application(tk.Frame): :return: nothing """ - help_menu = tk.Menu(self.master) + help_menu = tk.Menu(self.menubar) help_menu.add_command( label="Core Github (www)", command=action.help_core_github ) @@ -574,6 +587,11 @@ class Application(tk.Frame): self.menubar.add_cascade(label="Help", menu=help_menu) def bind_menubar_shortcut(self): + """ + Bind hot keys to matching command + + :return: nothing + """ self.bind_all("", action.file_new_shortcut) self.bind_all("", action.file_open_shortcut) self.bind_all("", action.file_save_shortcut) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 005776f9..2e1427da 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -6,6 +6,10 @@ import logging import webbrowser +def sub_menu_items(): + logging.debug("Click on sub menu items") + + def file_new(): logging.debug("Click file New") From aa1fb621829b7999b57eb40d4596a0319906546b Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Fri, 20 Sep 2019 11:58:15 -0700 Subject: [PATCH 015/305] start working on sidebar --- coretk/coretk/app.py | 54 +++++++++++++++++++++++++++++++----- coretk/coretk/lanswitch.gif | Bin 0 -> 744 bytes coretk/coretk/link.gif | Bin 0 -> 86 bytes coretk/coretk/marker.gif | Bin 0 -> 375 bytes coretk/coretk/router.gif | Bin 0 -> 1152 bytes coretk/coretk/select.gif | Bin 0 -> 925 bytes coretk/coretk/start.gif | Bin 0 -> 1131 bytes 7 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 coretk/coretk/lanswitch.gif create mode 100644 coretk/coretk/link.gif create mode 100644 coretk/coretk/marker.gif create mode 100644 coretk/coretk/router.gif create mode 100644 coretk/coretk/select.gif create mode 100644 coretk/coretk/start.gif diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 163c8a17..22d927e6 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,8 +15,14 @@ class Application(tk.Frame): self.create_widgets() def load_images(self): - Images.load("switch", "switch.png") + # Images.load("switch", "switch.png") Images.load("core", "core-icon.png") + Images.load("start", "start.gif") + Images.load("switch", "lanswitch.gif") + Images.load("marker", "marker.gif") + Images.load("router", "router.gif") + Images.load("select", "select.gif") + Images.load("link", "link.gif") def setup_app(self): self.master.title("CORE") @@ -37,28 +43,62 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def create_widgets(self): - image = Images.get("switch") + select_image = Images.get("select") + start_image = Images.get("start") + link_image = Images.get("link") + router_image = Images.get("router") + switch_image = Images.get("switch") + marker_image = Images.get("marker") + edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) radio_value = tk.IntVar() b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=1, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=1, + image=select_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=2, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=2, + image=start_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=3, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=3, + image=link_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=4, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=4, + image=router_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( - edit_frame, indicatoron=False, variable=radio_value, value=5, image=image + edit_frame, + indicatoron=False, + variable=radio_value, + value=5, + image=switch_image, + ) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton( + edit_frame, + indicatoron=False, + variable=radio_value, + value=6, + image=marker_image, ) b.pack(side=tk.TOP, pady=1) diff --git a/coretk/coretk/lanswitch.gif b/coretk/coretk/lanswitch.gif new file mode 100644 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/link.gif b/coretk/coretk/link.gif new file mode 100644 index 0000000000000000000000000000000000000000..55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea GIT binary patch literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v literal 0 HcmV?d00001 diff --git a/coretk/coretk/router.gif b/coretk/coretk/router.gif new file mode 100644 index 0000000000000000000000000000000000000000..eaf145ebc86c1b81600571705902866550da416d GIT binary patch literal 1152 zcmZ?wbhEHblxGlSc;3Sx<6SA|U!xFIuNd5*6w;s++Nc!Tq#D_(8r7;6)1e;Ip#enk z-J0>;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 literal 0 HcmV?d00001 diff --git a/coretk/coretk/select.gif b/coretk/coretk/select.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb7e128c878317f6287564514a302e5e3c40c10e GIT binary patch literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^ Date: Fri, 20 Sep 2019 13:33:42 -0700 Subject: [PATCH 016/305] finish up menubar --- coretk/coretk/app.py | 131 +++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bd0fd680..f1593662 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -34,14 +34,14 @@ class Application(tk.Frame): """ file_menu = tk.Menu(self.menubar) file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N" + label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 ) file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O" + label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 ) - file_menu.add_command(label="Reload", command=action.file_reload) + file_menu.add_command(label="Reload", command=action.file_reload, underline=0) file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S" + label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 ) file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) @@ -66,7 +66,7 @@ class Application(tk.Frame): label="Open current file in editor...", command=action.file_open_current_file_in_editor, ) - file_menu.add_command(label="Print...", command=action.file_print) + file_menu.add_command(label="Print...", command=action.file_print, underline=0) file_menu.add_command( label="Save screenshot...", command=action.file_save_screenshot ) @@ -80,8 +80,8 @@ class Application(tk.Frame): file_menu.add_separator() - file_menu.add_command(label="Quit", command=self.master.quit) - self.menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Quit", command=self.master.quit, underline=0) + self.menubar.add_cascade(label="File", menu=file_menu, underline=0) def create_edit_menu(self): """ @@ -91,22 +91,22 @@ class Application(tk.Frame): """ edit_menu = tk.Menu(self.menubar) edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z" + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 ) edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y" + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 ) edit_menu.add_separator() edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X" + label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 ) edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C" + label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 ) edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V" + label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 ) edit_menu.add_separator() @@ -123,12 +123,12 @@ class Application(tk.Frame): edit_menu.add_separator() edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F" + label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 ) edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - self.menubar.add_cascade(label="Edit", menu=edit_menu) + self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) def create_canvas_menu(self): """ @@ -161,7 +161,7 @@ class Application(tk.Frame): label="Last", command=action.canvas_last, accelerator="End" ) - self.menubar.add_cascade(label="Canvas", menu=canvas_menu) + self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) def create_show_menu(self, view_menu): """ @@ -207,7 +207,7 @@ class Application(tk.Frame): label="Zoom out", command=action.view_zoom_out, accelerator="-" ) - self.menubar.add_cascade(label="View", menu=view_menu) + self.menubar.add_cascade(label="View", menu=view_menu, underline=0) def create_experimental_menu(self, tools_menu): """ @@ -217,15 +217,19 @@ class Application(tk.Frame): :return: nothing """ experimental_menu = tk.Menu(tools_menu, tearoff=True) - experimental_menu.add_command(label="Plugins...", command=action.sub_menu_items) experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items + label="Plugins...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items, underline=0 ) experimental_menu.add_command( label="Topology partitioning...", command=action.sub_menu_items ) - tools_menu.add_cascade(label="Experimental", menu=experimental_menu) + tools_menu.add_cascade( + label="Experimental", menu=experimental_menu, underline=0 + ) def create_random_menu(self, topology_generator_menu): """ @@ -241,7 +245,9 @@ class Application(tk.Frame): the_label = "R(" + str(i) + ")" random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Random", menu=random_menu) + topology_generator_menu.add_cascade( + label="Random", menu=random_menu, underline=0 + ) def create_grid_menu(self, topology_generator_menu): """ @@ -259,7 +265,7 @@ class Application(tk.Frame): the_label = "G(" + str(i) + ")" grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu) + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) def create_connected_grid_menu(self, topology_generator_menu): """ @@ -276,7 +282,9 @@ class Application(tk.Frame): i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) i_n_label = str(i) + " X N" grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade(label="Connected Grid", menu=grid_menu) + topology_generator_menu.add_cascade( + label="Connected Grid", menu=grid_menu, underline=0 + ) def create_chain_menu(self, topology_generator_menu): """ @@ -292,7 +300,7 @@ class Application(tk.Frame): the_label = "P(" + str(i) + ")" chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu) + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) def create_star_menu(self, topology_generator_menu): """ @@ -306,7 +314,7 @@ class Application(tk.Frame): the_label = "C(" + str(i) + ")" star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu) + topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) def create_cycle_menu(self, topology_generator_menu): """ @@ -320,7 +328,7 @@ class Application(tk.Frame): the_label = "C(" + str(i) + ")" cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu) + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) def create_wheel_menu(self, topology_generator_menu): """ @@ -334,7 +342,7 @@ class Application(tk.Frame): the_label = "W(" + str(i) + ")" wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu) + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) def create_cube_menu(self, topology_generator_menu): """ @@ -348,7 +356,7 @@ class Application(tk.Frame): the_label = "Q(" + str(i) + ")" cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu) + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) def create_clique_menu(self, topology_generator_menu): """ @@ -362,7 +370,9 @@ class Application(tk.Frame): the_label = "K(" + str(i) + ")" clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Clique", menu=clique_menu) + topology_generator_menu.add_cascade( + label="Clique", menu=clique_menu, underline=0 + ) def create_bipartite_menu(self, topology_generator_menu): """ @@ -381,7 +391,9 @@ class Application(tk.Frame): i_n_label = "K(" + str(i) + " X N)" bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) temp = temp - 1 - topology_generator_menu.add_cascade(label="Bipartite", menu=bipartite_menu) + topology_generator_menu.add_cascade( + label="Bipartite", menu=bipartite_menu, underline=0 + ) def create_topology_generator_menu(self, tools_menu): """ @@ -404,7 +416,9 @@ class Application(tk.Frame): self.create_clique_menu(topology_generator_menu) self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade(label="Topology generator", menu=topology_generator_menu) + tools_menu.add_cascade( + label="Topology generator", menu=topology_generator_menu, underline=0 + ) def create_tools_menu(self): """ @@ -415,38 +429,43 @@ class Application(tk.Frame): tools_menu = tk.Menu(self.menubar) tools_menu.add_command( - label="Auto rearrange all", command=action.tools_auto_rearrange_all + label="Auto rearrange all", + command=action.tools_auto_rearrange_all, + underline=0, ) tools_menu.add_command( label="Auto rearrange selected", command=action.tools_auto_rearrange_selected, + underline=0, ) tools_menu.add_separator() tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid + label="Align to grid", command=action.tools_align_to_grid, underline=0 ) tools_menu.add_separator() tools_menu.add_command(label="Traffic...", command=action.tools_traffic) tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses + label="IP addresses...", command=action.tools_ip_addresses, underline=0 ) tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses + label="MAC addresses...", command=action.tools_mac_addresses, underline=0 ) tools_menu.add_command( - label="Build hosts file...", command=action.tools_build_hosts_file + label="Build hosts file...", + command=action.tools_build_hosts_file, + underline=0, ) tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes + label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 ) self.create_experimental_menu(tools_menu) self.create_topology_generator_menu(tools_menu) tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - self.menubar.add_cascade(label="Tools", menu=tools_menu) + self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) def create_observer_widgets_menu(self, widget_menu): """ @@ -538,7 +557,7 @@ class Application(tk.Frame): label="Configure Throughput...", command=action.widgets_configure_throughput ) - self.menubar.add_cascade(label="Widgets", menu=widget_menu) + self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) def create_session_menu(self): """ @@ -547,27 +566,41 @@ class Application(tk.Frame): :return: nothing """ session_menu = tk.Menu(self.menubar, tearoff=True) - session_menu.add_command(label="Start", command=action.session_start) session_menu.add_command( - label="Change sessions...", command=action.session_change_sessions + label="Start", command=action.session_start, underline=0 + ) + session_menu.add_command( + label="Change sessions...", + command=action.session_change_sessions, + underline=0, ) session_menu.add_separator() session_menu.add_command( - label="Node types...", command=action.session_node_types - ) - session_menu.add_command(label="Comments...", command=action.session_comments) - session_menu.add_command(label="Hooks...", command=action.session_hooks) - session_menu.add_command( - label="Reset node positions", command=action.session_reset_node_positions + label="Node types...", command=action.session_node_types, underline=0 ) session_menu.add_command( - label="Emulation servers...", command=action.session_emulation_servers + label="Comments...", command=action.session_comments, underline=0 + ) + session_menu.add_command( + label="Hooks...", command=action.session_hooks, underline=0 + ) + session_menu.add_command( + label="Reset node positions", + command=action.session_reset_node_positions, + underline=0, + ) + session_menu.add_command( + label="Emulation servers...", + command=action.session_emulation_servers, + underline=0, + ) + session_menu.add_command( + label="Options...", command=action.session_options, underline=0 ) - session_menu.add_command(label="Options...", command=action.session_options) - self.menubar.add_cascade(label="Session", menu=session_menu) + self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) def create_help_menu(self): """ From 1ebe33cabbb323bf16f5ca5fdfc92f4a7cffdfa9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 20 Sep 2019 13:51:01 -0700 Subject: [PATCH 017/305] get rid all the dash lines in menubar --- coretk/coretk/app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f1593662..01d79d17 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -190,7 +190,7 @@ class Application(tk.Frame): :return: nothing """ - view_menu = tk.Menu(self.menubar, tearoff=True) + view_menu = tk.Menu(self.menubar) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes @@ -216,7 +216,7 @@ class Application(tk.Frame): :param tkinter.Menu tools_menu: tools menu :return: nothing """ - experimental_menu = tk.Menu(tools_menu, tearoff=True) + experimental_menu = tk.Menu(tools_menu) experimental_menu.add_command( label="Plugins...", command=action.sub_menu_items, underline=0 ) @@ -403,7 +403,7 @@ class Application(tk.Frame): :return: nothing """ - topology_generator_menu = tk.Menu(tools_menu, tearoff=True) + topology_generator_menu = tk.Menu(tools_menu) self.create_random_menu(topology_generator_menu) self.create_grid_menu(topology_generator_menu) @@ -474,7 +474,7 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(widget_menu, tearoff=True) + observer_widget_menu = tk.Menu(widget_menu) observer_widget_menu.add_command(label="None", command=action.sub_menu_items) observer_widget_menu.add_command( label="processes", command=action.sub_menu_items @@ -529,7 +529,7 @@ class Application(tk.Frame): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(widget_menu, tearoff=True) + adjacency_menu = tk.Menu(widget_menu) adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) @@ -543,7 +543,7 @@ class Application(tk.Frame): :return: nothing """ - widget_menu = tk.Menu(self.menubar, tearoff=True) + widget_menu = tk.Menu(self.menubar) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) @@ -565,7 +565,7 @@ class Application(tk.Frame): :return: nothing """ - session_menu = tk.Menu(self.menubar, tearoff=True) + session_menu = tk.Menu(self.menubar) session_menu.add_command( label="Start", command=action.session_start, underline=0 ) From f1698de74b9f19c0d475b98b9a8c51c3c030ae14 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 23 Sep 2019 11:43:13 -0700 Subject: [PATCH 018/305] toolbar --- coretk/coretk/app.py | 43 ++++++++++++++++++++++++++++++++++++++- coretk/coretk/hub.gif | Bin 0 -> 719 bytes coretk/coretk/tooltip.py | 0 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/hub.gif create mode 100644 coretk/coretk/tooltip.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 22d927e6..5ed74956 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -23,6 +23,7 @@ class Application(tk.Frame): Images.load("router", "router.gif") Images.load("select", "select.gif") Images.load("link", "link.gif") + Images.load("hub", "hub.gif") def setup_app(self): self.master.title("CORE") @@ -42,11 +43,35 @@ class Application(tk.Frame): self.menubar.add_cascade(label="Help", menu=help_menu) self.master.config(menu=self.menubar) + def create_network_layer_node( + self, edit_frame, radio_value, hub_image, switch_image + ): + menu_button = tk.Menubutton( + edit_frame, + direction=tk.RIGHT, + image=hub_image, + width=32, + height=32, + relief=tk.RAISED, + ) + # menu_button.grid() + menu_button.menu = tk.Menu(menu_button) + menu_button["menu"] = menu_button.menu + + menu_button.menu.add_radiobutton( + image=hub_image, variable=radio_value, value=7, indicatoron=False + ) + menu_button.menu.add_radiobutton( + image=switch_image, variable=radio_value, value=8, indicatoron=False + ) + menu_button.pack(side=tk.TOP, pady=1) + def create_widgets(self): select_image = Images.get("select") start_image = Images.get("start") link_image = Images.get("link") router_image = Images.get("router") + hub_image = Images.get("hub") switch_image = Images.get("switch") marker_image = Images.get("marker") @@ -58,6 +83,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=1, + width=32, + height=32, image=select_image, ) b.pack(side=tk.TOP, pady=1) @@ -66,6 +93,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=2, + width=32, + height=32, image=start_image, ) b.pack(side=tk.TOP, pady=1) @@ -74,6 +103,8 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=3, + width=32, + height=32, image=link_image, ) b.pack(side=tk.TOP, pady=1) @@ -82,15 +113,21 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=4, + width=32, + height=32, image=router_image, ) + b.pack(side=tk.TOP, pady=1) + b = tk.Radiobutton( edit_frame, indicatoron=False, variable=radio_value, value=5, - image=switch_image, + width=32, + height=32, + image=hub_image, ) b.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( @@ -98,10 +135,14 @@ class Application(tk.Frame): indicatoron=False, variable=radio_value, value=6, + width=32, + height=32, image=marker_image, ) b.pack(side=tk.TOP, pady=1) + self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) + self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/hub.gif b/coretk/coretk/hub.gif new file mode 100644 index 0000000000000000000000000000000000000000..17f7c4d3ef726f744da685423057366093df33f8 GIT binary patch literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvD Date: Wed, 25 Sep 2019 08:29:34 -0700 Subject: [PATCH 019/305] Create a class for menubar and start working on toolbar --- coretk/coretk/app.py | 710 ++++------------------------------- coretk/coretk/coremenubar.py | 654 ++++++++++++++++++++++++++++++++ coretk/coretk/coretoolbar.py | 421 +++++++++++++++++++++ coretk/coretk/oval.gif | Bin 0 -> 174 bytes coretk/coretk/rectangle.gif | Bin 0 -> 160 bytes coretk/coretk/rj45.gif | Bin 0 -> 755 bytes coretk/coretk/text.gif | Bin 0 -> 127 bytes coretk/coretk/tunnel.gif | Bin 0 -> 799 bytes coretk/coretk/wlan.gif | Bin 0 -> 146 bytes 9 files changed, 1153 insertions(+), 632 deletions(-) create mode 100644 coretk/coretk/coremenubar.py create mode 100644 coretk/coretk/coretoolbar.py create mode 100644 coretk/coretk/oval.gif create mode 100644 coretk/coretk/rectangle.gif create mode 100644 coretk/coretk/rj45.gif create mode 100644 coretk/coretk/text.gif create mode 100644 coretk/coretk/tunnel.gif create mode 100644 coretk/coretk/wlan.gif diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e0eb138e..37e51aff 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,8 @@ import logging import tkinter as tk -import coretk.menuaction as action +from coretk.coremenubar import CoreMenubar +from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph from coretk.images import Images @@ -33,638 +34,14 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) - def create_file_menu(self): - """ - Create file menu - - :return: nothing - """ - file_menu = tk.Menu(self.menubar) - file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 - ) - file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 - ) - file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - ) - file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) - - file_menu.add_separator() - - file_menu.add_command( - label="Export Python script...", command=action.file_export_python_script - ) - file_menu.add_command( - label="Execute XML or Python script...", - command=action.file_execute_xml_or_python_script, - ) - file_menu.add_command( - label="Execute Python script with options...", - command=action.file_execute_python_script_with_options, - ) - - file_menu.add_separator() - - file_menu.add_command( - label="Open current file in editor...", - command=action.file_open_current_file_in_editor, - ) - file_menu.add_command(label="Print...", command=action.file_print, underline=0) - file_menu.add_command( - label="Save screenshot...", command=action.file_save_screenshot - ) - - file_menu.add_separator() - - file_menu.add_command( - label="/home/ncs/.core/configs/sample1.imn", - command=action.file_example_link, - ) - - file_menu.add_separator() - - file_menu.add_command(label="Quit", command=self.master.quit, underline=0) - self.menubar.add_cascade(label="File", menu=file_menu, underline=0) - - def create_edit_menu(self): - """ - Create edit menu - - :return: nothing - """ - edit_menu = tk.Menu(self.menubar) - edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 - ) - edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 - ) - edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 - ) - edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" - ) - edit_menu.add_command( - label="Select Adjacent", - command=action.edit_select_adjacent, - accelerator="Ctrl+J", - ) - - edit_menu.add_separator() - - edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 - ) - edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) - edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - - self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) - - def create_canvas_menu(self): - """ - Create canvas menu - - :return: nothing - """ - canvas_menu = tk.Menu(self.menubar) - canvas_menu.add_command(label="New", command=action.canvas_new) - canvas_menu.add_command(label="Manage...", command=action.canvas_manage) - canvas_menu.add_command(label="Delete", command=action.canvas_delete) - - canvas_menu.add_separator() - - canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) - canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) - - canvas_menu.add_separator() - - canvas_menu.add_command( - label="Previous", command=action.canvas_previous, accelerator="PgUp" - ) - canvas_menu.add_command( - label="Next", command=action.canvas_next, accelerator="PgDown" - ) - canvas_menu.add_command( - label="First", command=action.canvas_first, accelerator="Home" - ) - canvas_menu.add_command( - label="Last", command=action.canvas_last, accelerator="End" - ) - - self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) - - def create_show_menu(self, view_menu): - """ - Create the menu items in View/Show - - :param tkinter.Menu view_menu: the view menu - :return: nothing - """ - show_menu = tk.Menu(view_menu) - show_menu.add_command(label="All", command=action.sub_menu_items) - show_menu.add_command(label="None", command=action.sub_menu_items) - show_menu.add_separator() - show_menu.add_command(label="Interface Names", command=action.sub_menu_items) - show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="Node Labels", command=action.sub_menu_items) - show_menu.add_command(label="Annotations", command=action.sub_menu_items) - show_menu.add_command(label="Grid", command=action.sub_menu_items) - show_menu.add_command(label="API Messages", command=action.sub_menu_items) - - view_menu.add_cascade(label="Show", menu=show_menu) - - def create_view_menu(self): - """ - Create view menu - - :return: nothing - """ - view_menu = tk.Menu(self.menubar) - self.create_show_menu(view_menu) - view_menu.add_command( - label="Show hidden nodes", command=action.view_show_hidden_nodes - ) - view_menu.add_command(label="Locked", command=action.view_locked) - view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) - - view_menu.add_separator() - - view_menu.add_command( - label="Zoom in", command=action.view_zoom_in, accelerator="+" - ) - view_menu.add_command( - label="Zoom out", command=action.view_zoom_out, accelerator="-" - ) - - self.menubar.add_cascade(label="View", menu=view_menu, underline=0) - - def create_experimental_menu(self, tools_menu): - """ - Create experimental menu item and the sub menu items inside - - :param tkinter.Menu tools_menu: tools menu - :return: nothing - """ - experimental_menu = tk.Menu(tools_menu) - experimental_menu.add_command( - label="Plugins...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="Topology partitioning...", command=action.sub_menu_items - ) - - tools_menu.add_cascade( - label="Experimental", menu=experimental_menu, underline=0 - ) - - def create_random_menu(self, topology_generator_menu): - """ - Create random menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - random_menu = tk.Menu(topology_generator_menu) - # list of number of random nodes to create - nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] - for i in nums: - the_label = "R(" + str(i) + ")" - random_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade( - label="Random", menu=random_menu, underline=0 - ) - - def create_grid_menu(self, topology_generator_menu): - """ - Create grid menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology_generator_menu - :return: nothing - """ - grid_menu = tk.Menu(topology_generator_menu) - - # list of number of nodes to create - nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] - - for i in nums: - the_label = "G(" + str(i) + ")" - grid_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) - - def create_connected_grid_menu(self, topology_generator_menu): - """ - Create connected grid menu items and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - grid_menu = tk.Menu(topology_generator_menu) - for i in range(1, 11, 1): - i_n_menu = tk.Menu(grid_menu) - for j in range(1, 11, 1): - i_j_label = str(i) + " X " + str(j) - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = str(i) + " X N" - grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade( - label="Connected Grid", menu=grid_menu, underline=0 - ) - - def create_chain_menu(self, topology_generator_menu): - """ - Create chain menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - chain_menu = tk.Menu(topology_generator_menu) - # number of nodes to create - nums = list(range(2, 25, 1)) + [32, 64, 128] - for i in nums: - the_label = "P(" + str(i) + ")" - chain_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) - - def create_star_menu(self, topology_generator_menu): - """ - Create star menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - star_menu = tk.Menu(topology_generator_menu) - for i in range(3, 26, 1): - the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) - - def create_cycle_menu(self, topology_generator_menu): - """ - Create cycle menu item and the sub items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - cycle_menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) - - def create_wheel_menu(self, topology_generator_menu): - """ - Create wheel menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - wheel_menu = tk.Menu(topology_generator_menu) - for i in range(4, 26, 1): - the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) - - def create_cube_menu(self, topology_generator_menu): - """ - Create cube menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - cube_menu = tk.Menu(topology_generator_menu) - for i in range(2, 7, 1): - the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) - - def create_clique_menu(self, topology_generator_menu): - """ - Create clique menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology generator menu - :return: nothing - """ - clique_menu = tk.Menu(topology_generator_menu) - for i in range(3, 25, 1): - the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label, command=action.sub_menu_items) - - topology_generator_menu.add_cascade( - label="Clique", menu=clique_menu, underline=0 - ) - - def create_bipartite_menu(self, topology_generator_menu): - """ - Create bipartite menu item and the sub menu items inside - - :param tkinter.Menu topology_generator_menu: topology_generator_menu - :return: nothing - """ - bipartite_menu = tk.Menu(topology_generator_menu) - temp = 24 - for i in range(1, 13, 1): - i_n_menu = tk.Menu(bipartite_menu) - for j in range(i, temp, 1): - i_j_label = "K(" + str(i) + " X " + str(j) + ")" - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = "K(" + str(i) + " X N)" - bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) - temp = temp - 1 - topology_generator_menu.add_cascade( - label="Bipartite", menu=bipartite_menu, underline=0 - ) - - def create_topology_generator_menu(self, tools_menu): - """ - Create topology menu item and its sub menu items - - :param tkinter.Menu tools_menu: tools menu - - :return: nothing - """ - topology_generator_menu = tk.Menu(tools_menu) - - self.create_random_menu(topology_generator_menu) - self.create_grid_menu(topology_generator_menu) - self.create_connected_grid_menu(topology_generator_menu) - self.create_chain_menu(topology_generator_menu) - self.create_star_menu(topology_generator_menu) - self.create_cycle_menu(topology_generator_menu) - self.create_wheel_menu(topology_generator_menu) - self.create_cube_menu(topology_generator_menu) - self.create_clique_menu(topology_generator_menu) - self.create_bipartite_menu(topology_generator_menu) - - tools_menu.add_cascade( - label="Topology generator", menu=topology_generator_menu, underline=0 - ) - - def create_tools_menu(self): - """ - Create tools menu - - :return: nothing - """ - - tools_menu = tk.Menu(self.menubar) - tools_menu.add_command( - label="Auto rearrange all", - command=action.tools_auto_rearrange_all, - underline=0, - ) - tools_menu.add_command( - label="Auto rearrange selected", - command=action.tools_auto_rearrange_selected, - underline=0, - ) - tools_menu.add_separator() - - tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid, underline=0 - ) - - tools_menu.add_separator() - - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) - tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses, underline=0 - ) - tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses, underline=0 - ) - tools_menu.add_command( - label="Build hosts file...", - command=action.tools_build_hosts_file, - underline=0, - ) - tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 - ) - self.create_experimental_menu(tools_menu) - self.create_topology_generator_menu(tools_menu) - tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - - self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) - - def create_observer_widgets_menu(self, widget_menu): - """ - Create observer widget menu item and create the sub menu items inside - - :param tkinter.Menu widget_menu: widget_menu - :return: nothing - """ - observer_widget_menu = tk.Menu(widget_menu) - observer_widget_menu.add_command(label="None", command=action.sub_menu_items) - observer_widget_menu.add_command( - label="processes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="ifconfig", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv2 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Listening sockets", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="firewall rules", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPsec policies", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="docker logs", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 MDR level", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="PIM neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) - - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) - - def create_adjacency_menu(self, widget_menu): - """ - Create adjacency menu item and the sub menu items inside - - :param tkinter.Menu widget_menu: widget menu - :return: nothing - """ - adjacency_menu = tk.Menu(widget_menu) - adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) - - def create_widgets_menu(self): - """ - Create widget menu - - :return: nothing - """ - widget_menu = tk.Menu(self.menubar) - self.create_observer_widgets_menu(widget_menu) - self.create_adjacency_menu(widget_menu) - widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - - widget_menu.add_separator() - - widget_menu.add_command( - label="Configure Adjacency...", command=action.widgets_configure_adjacency - ) - widget_menu.add_command( - label="Configure Throughput...", command=action.widgets_configure_throughput - ) - - self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) - - def create_session_menu(self): - """ - Create session menu - - :return: nothing - """ - session_menu = tk.Menu(self.menubar) - session_menu.add_command( - label="Start", command=action.session_start, underline=0 - ) - session_menu.add_command( - label="Change sessions...", - command=action.session_change_sessions, - underline=0, - ) - - session_menu.add_separator() - - session_menu.add_command( - label="Node types...", command=action.session_node_types, underline=0 - ) - session_menu.add_command( - label="Comments...", command=action.session_comments, underline=0 - ) - session_menu.add_command( - label="Hooks...", command=action.session_hooks, underline=0 - ) - session_menu.add_command( - label="Reset node positions", - command=action.session_reset_node_positions, - underline=0, - ) - session_menu.add_command( - label="Emulation servers...", - command=action.session_emulation_servers, - underline=0, - ) - session_menu.add_command( - label="Options...", command=action.session_options, underline=0 - ) - - self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) - - def create_help_menu(self): - """ - Create help menu - - :return: nothing - """ - help_menu = tk.Menu(self.menubar) - help_menu.add_command( - label="Core Github (www)", command=action.help_core_github - ) - help_menu.add_command( - label="Core Documentation (www)", command=action.help_core_documentation - ) - help_menu.add_command(label="About", command=action.help_about) - - self.menubar.add_cascade(label="Help", menu=help_menu) - - def bind_menubar_shortcut(self): - """ - Bind hot keys to matching command - - :return: nothing - """ - self.bind_all("", action.file_new_shortcut) - self.bind_all("", action.file_open_shortcut) - self.bind_all("", action.file_save_shortcut) - self.bind_all("", action.edit_undo_shortcut) - self.bind_all("", action.edit_redo_shortcut) - self.bind_all("", action.edit_cut_shortcut) - self.bind_all("", action.edit_copy_shortcut) - self.bind_all("", action.edit_paste_shortcut) - self.bind_all("", action.edit_select_all_shortcut) - self.bind_all("", action.edit_select_adjacent_shortcut) - self.bind_all("", action.edit_find_shortcut) - self.bind_all("", action.canvas_previous_shortcut) - self.bind_all("", action.canvas_next_shortcut) - self.bind_all("", action.canvas_first_shortcut) - self.bind_all("", action.canvas_last_shortcut) - self.bind_all("", action.view_zoom_in_shortcut) - self.bind_all("", action.view_zoom_out_shortcut) - def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) - self.create_file_menu() - self.create_edit_menu() - self.create_canvas_menu() - self.create_view_menu() - self.create_tools_menu() - self.create_widgets_menu() - self.create_session_menu() - self.create_help_menu() - + core_menu = CoreMenubar(self, self.master, self.menubar) + core_menu.create_core_menubar() self.master.config(menu=self.menubar) - self.bind_menubar_shortcut() + # TODO clean up this code def create_network_layer_node( self, edit_frame, radio_value, hub_image, switch_image ): @@ -687,8 +64,72 @@ class Application(tk.Frame): image=switch_image, variable=radio_value, value=8, indicatoron=False ) menu_button.pack(side=tk.TOP, pady=1) + self.master.update() + print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) + # print(menu_button.winfo_width(), menu_button.winfo_height()) + # print(self.master.winfo_height()) + option_frame = tk.Frame(self.master) + + switch_button = tk.Button(option_frame, image=switch_image, width=32, height=32) + switch_button.pack(side=tk.LEFT, pady=1) + hub_button = tk.Button(option_frame, image=hub_image, width=32, height=32) + hub_button.pack(side=tk.LEFT, pady=1) + print("Place the button") + print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) + option_frame.place( + x=menu_button.winfo_rootx() + 33, y=menu_button.winfo_rooty() - 117 + ) + self.update() + + print("option frame: " + str(option_frame.winfo_rooty())) + print("option frame x: " + str(option_frame.winfo_rootx())) + + print("frame dimension: " + str(option_frame.winfo_height())) + print("button height: " + str(hub_button.winfo_rooty())) + + # TODO switch 177 into the rooty of the selection tool, retrieve image in here + def draw_options(self, main_button, radio_value): + hub_image = Images.get("hub") + switch_image = Images.get("switch") + option_frame = tk.Frame(self.master) + + switch_button = tk.Radiobutton( + option_frame, + image=switch_image, + width=32, + height=32, + variable=radio_value, + value=7, + indicatoron=False, + ) + switch_button.pack(side=tk.LEFT, pady=1) + hub_button = tk.Radiobutton( + option_frame, + image=hub_image, + width=32, + height=32, + variable=radio_value, + value=8, + indicatoron=False, + ) + hub_button.pack(side=tk.LEFT, pady=1) + self.master.update() + option_frame.place( + x=main_button.winfo_rootx() + 35 - self.selection_button.winfo_rootx(), + y=main_button.winfo_rooty() - self.selection_button.winfo_rooty(), + ) + + def create_network_layer_node_attempt2(self, edit_frame, radio_value): + hub_image = Images.get("hub") + main_button = tk.Radiobutton( + edit_frame, image=hub_image, width=32, height=32, indicatoron=False + ) + main_button.pack(side=tk.TOP, pady=1) + self.draw_options(main_button, radio_value) def create_widgets(self): + + """ select_image = Images.get("select") start_image = Images.get("start") link_image = Images.get("link") @@ -696,11 +137,15 @@ class Application(tk.Frame): hub_image = Images.get("hub") switch_image = Images.get("switch") marker_image = Images.get("marker") + """ edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + core_editbar = CoreToolbar(self.master, edit_frame) + core_editbar.create_toolbar() + """ radio_value = tk.IntVar() - b = tk.Radiobutton( + self.selection_button = tk.Radiobutton( edit_frame, indicatoron=False, variable=radio_value, @@ -709,7 +154,7 @@ class Application(tk.Frame): height=32, image=select_image, ) - b.pack(side=tk.TOP, pady=1) + self.selection_button.pack(side=tk.TOP, pady=1) b = tk.Radiobutton( edit_frame, indicatoron=False, @@ -763,8 +208,9 @@ class Application(tk.Frame): ) b.pack(side=tk.TOP, pady=1) - self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) - + #self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) + self.create_network_layer_node_attempt2(edit_frame, radio_value) + """ self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py new file mode 100644 index 00000000..f4ba51f5 --- /dev/null +++ b/coretk/coretk/coremenubar.py @@ -0,0 +1,654 @@ +import tkinter as tk + +import coretk.menuaction as action + + +class CoreMenubar(object): + """ + Core menubar + """ + + def __init__(self, application, master, menubar): + """ + Create a CoreMenubar instance + + :param master: + :param tkinter.Menu menubar: menubar object + :param coretk.app.Application application: application object + """ + self.menubar = menubar + self.master = master + self.application = application + + def create_file_menu(self): + """ + Create file menu + + :return: nothing + """ + file_menu = tk.Menu(self.menubar) + file_menu.add_command( + label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 + ) + file_menu.add_command( + label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 + ) + file_menu.add_command(label="Reload", command=action.file_reload, underline=0) + file_menu.add_command( + label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 + ) + file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + + file_menu.add_separator() + + file_menu.add_command( + label="Export Python script...", command=action.file_export_python_script + ) + file_menu.add_command( + label="Execute XML or Python script...", + command=action.file_execute_xml_or_python_script, + ) + file_menu.add_command( + label="Execute Python script with options...", + command=action.file_execute_python_script_with_options, + ) + + file_menu.add_separator() + + file_menu.add_command( + label="Open current file in editor...", + command=action.file_open_current_file_in_editor, + ) + file_menu.add_command(label="Print...", command=action.file_print, underline=0) + file_menu.add_command( + label="Save screenshot...", command=action.file_save_screenshot + ) + + file_menu.add_separator() + + file_menu.add_command( + label="/home/ncs/.core/configs/sample1.imn", + command=action.file_example_link, + ) + + file_menu.add_separator() + + file_menu.add_command(label="Quit", command=self.master.quit, underline=0) + self.menubar.add_cascade(label="File", menu=file_menu, underline=0) + + def create_edit_menu(self): + """ + Create edit menu + + :return: nothing + """ + edit_menu = tk.Menu(self.menubar) + edit_menu.add_command( + label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 + ) + edit_menu.add_command( + label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 + ) + edit_menu.add_command( + label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 + ) + edit_menu.add_command( + label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" + ) + edit_menu.add_command( + label="Select Adjacent", + command=action.edit_select_adjacent, + accelerator="Ctrl+J", + ) + + edit_menu.add_separator() + + edit_menu.add_command( + label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 + ) + edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) + edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + + self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) + + def create_canvas_menu(self): + """ + Create canvas menu + + :return: nothing + """ + canvas_menu = tk.Menu(self.menubar) + canvas_menu.add_command(label="New", command=action.canvas_new) + canvas_menu.add_command(label="Manage...", command=action.canvas_manage) + canvas_menu.add_command(label="Delete", command=action.canvas_delete) + + canvas_menu.add_separator() + + canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + + canvas_menu.add_separator() + + canvas_menu.add_command( + label="Previous", command=action.canvas_previous, accelerator="PgUp" + ) + canvas_menu.add_command( + label="Next", command=action.canvas_next, accelerator="PgDown" + ) + canvas_menu.add_command( + label="First", command=action.canvas_first, accelerator="Home" + ) + canvas_menu.add_command( + label="Last", command=action.canvas_last, accelerator="End" + ) + + self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + show_menu = tk.Menu(view_menu) + show_menu.add_command(label="All", command=action.sub_menu_items) + show_menu.add_command(label="None", command=action.sub_menu_items) + show_menu.add_separator() + show_menu.add_command(label="Interface Names", command=action.sub_menu_items) + show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) + show_menu.add_command(label="Node Labels", command=action.sub_menu_items) + show_menu.add_command(label="Annotations", command=action.sub_menu_items) + show_menu.add_command(label="Grid", command=action.sub_menu_items) + show_menu.add_command(label="API Messages", command=action.sub_menu_items) + + view_menu.add_cascade(label="Show", menu=show_menu) + + def create_view_menu(self): + """ + Create view menu + + :return: nothing + """ + view_menu = tk.Menu(self.menubar) + self.create_show_menu(view_menu) + view_menu.add_command( + label="Show hidden nodes", command=action.view_show_hidden_nodes + ) + view_menu.add_command(label="Locked", command=action.view_locked) + view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + + view_menu.add_separator() + + view_menu.add_command( + label="Zoom in", command=action.view_zoom_in, accelerator="+" + ) + view_menu.add_command( + label="Zoom out", command=action.view_zoom_out, accelerator="-" + ) + + self.menubar.add_cascade(label="View", menu=view_menu, underline=0) + + def create_experimental_menu(self, tools_menu): + """ + Create experimental menu item and the sub menu items inside + + :param tkinter.Menu tools_menu: tools menu + :return: nothing + """ + experimental_menu = tk.Menu(tools_menu) + experimental_menu.add_command( + label="Plugins...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="ns2immunes converter...", command=action.sub_menu_items, underline=0 + ) + experimental_menu.add_command( + label="Topology partitioning...", command=action.sub_menu_items + ) + + tools_menu.add_cascade( + label="Experimental", menu=experimental_menu, underline=0 + ) + + def create_random_menu(self, topology_generator_menu): + """ + Create random menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + random_menu = tk.Menu(topology_generator_menu) + # list of number of random nodes to create + nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] + for i in nums: + the_label = "R(" + str(i) + ")" + random_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade( + label="Random", menu=random_menu, underline=0 + ) + + def create_grid_menu(self, topology_generator_menu): + """ + Create grid menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + grid_menu = tk.Menu(topology_generator_menu) + + # list of number of nodes to create + nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] + + for i in nums: + the_label = "G(" + str(i) + ")" + grid_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) + + def create_connected_grid_menu(self, topology_generator_menu): + """ + Create connected grid menu items and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + grid_menu = tk.Menu(topology_generator_menu) + for i in range(1, 11, 1): + i_n_menu = tk.Menu(grid_menu) + for j in range(1, 11, 1): + i_j_label = str(i) + " X " + str(j) + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = str(i) + " X N" + grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) + topology_generator_menu.add_cascade( + label="Connected Grid", menu=grid_menu, underline=0 + ) + + def create_chain_menu(self, topology_generator_menu): + """ + Create chain menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + chain_menu = tk.Menu(topology_generator_menu) + # number of nodes to create + nums = list(range(2, 25, 1)) + [32, 64, 128] + for i in nums: + the_label = "P(" + str(i) + ")" + chain_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) + + def create_star_menu(self, topology_generator_menu): + """ + Create star menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + star_menu = tk.Menu(topology_generator_menu) + for i in range(3, 26, 1): + the_label = "C(" + str(i) + ")" + star_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) + + def create_cycle_menu(self, topology_generator_menu): + """ + Create cycle menu item and the sub items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cycle_menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + the_label = "C(" + str(i) + ")" + cycle_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) + + def create_wheel_menu(self, topology_generator_menu): + """ + Create wheel menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + wheel_menu = tk.Menu(topology_generator_menu) + for i in range(4, 26, 1): + the_label = "W(" + str(i) + ")" + wheel_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) + + def create_cube_menu(self, topology_generator_menu): + """ + Create cube menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + cube_menu = tk.Menu(topology_generator_menu) + for i in range(2, 7, 1): + the_label = "Q(" + str(i) + ")" + cube_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) + + def create_clique_menu(self, topology_generator_menu): + """ + Create clique menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology generator menu + :return: nothing + """ + clique_menu = tk.Menu(topology_generator_menu) + for i in range(3, 25, 1): + the_label = "K(" + str(i) + ")" + clique_menu.add_command(label=the_label, command=action.sub_menu_items) + + topology_generator_menu.add_cascade( + label="Clique", menu=clique_menu, underline=0 + ) + + def create_bipartite_menu(self, topology_generator_menu): + """ + Create bipartite menu item and the sub menu items inside + + :param tkinter.Menu topology_generator_menu: topology_generator_menu + :return: nothing + """ + bipartite_menu = tk.Menu(topology_generator_menu) + temp = 24 + for i in range(1, 13, 1): + i_n_menu = tk.Menu(bipartite_menu) + for j in range(i, temp, 1): + i_j_label = "K(" + str(i) + " X " + str(j) + ")" + i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) + i_n_label = "K(" + str(i) + " X N)" + bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + temp = temp - 1 + topology_generator_menu.add_cascade( + label="Bipartite", menu=bipartite_menu, underline=0 + ) + + def create_topology_generator_menu(self, tools_menu): + """ + Create topology menu item and its sub menu items + + :param tkinter.Menu tools_menu: tools menu + + :return: nothing + """ + topology_generator_menu = tk.Menu(tools_menu) + + self.create_random_menu(topology_generator_menu) + self.create_grid_menu(topology_generator_menu) + self.create_connected_grid_menu(topology_generator_menu) + self.create_chain_menu(topology_generator_menu) + self.create_star_menu(topology_generator_menu) + self.create_cycle_menu(topology_generator_menu) + self.create_wheel_menu(topology_generator_menu) + self.create_cube_menu(topology_generator_menu) + self.create_clique_menu(topology_generator_menu) + self.create_bipartite_menu(topology_generator_menu) + + tools_menu.add_cascade( + label="Topology generator", menu=topology_generator_menu, underline=0 + ) + + def create_tools_menu(self): + """ + Create tools menu + + :return: nothing + """ + + tools_menu = tk.Menu(self.menubar) + tools_menu.add_command( + label="Auto rearrange all", + command=action.tools_auto_rearrange_all, + underline=0, + ) + tools_menu.add_command( + label="Auto rearrange selected", + command=action.tools_auto_rearrange_selected, + underline=0, + ) + tools_menu.add_separator() + + tools_menu.add_command( + label="Align to grid", command=action.tools_align_to_grid, underline=0 + ) + + tools_menu.add_separator() + + tools_menu.add_command(label="Traffic...", command=action.tools_traffic) + tools_menu.add_command( + label="IP addresses...", command=action.tools_ip_addresses, underline=0 + ) + tools_menu.add_command( + label="MAC addresses...", command=action.tools_mac_addresses, underline=0 + ) + tools_menu.add_command( + label="Build hosts file...", + command=action.tools_build_hosts_file, + underline=0, + ) + tools_menu.add_command( + label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 + ) + self.create_experimental_menu(tools_menu) + self.create_topology_generator_menu(tools_menu) + tools_menu.add_command(label="Debugger...", command=action.tools_debugger) + + self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) + + def create_observer_widgets_menu(self, widget_menu): + """ + Create observer widget menu item and create the sub menu items inside + + :param tkinter.Menu widget_menu: widget_menu + :return: nothing + """ + observer_widget_menu = tk.Menu(widget_menu) + observer_widget_menu.add_command(label="None", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="processes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="ifconfig", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 routes", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv2 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="Listening sockets", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv4 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPv6 MFC entries", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="firewall rules", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="IPsec policies", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="docker logs", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="OSPFv3 MDR level", command=action.sub_menu_items + ) + observer_widget_menu.add_command( + label="PIM neighbors", command=action.sub_menu_items + ) + observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) + + widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + + def create_adjacency_menu(self, widget_menu): + """ + Create adjacency menu item and the sub menu items inside + + :param tkinter.Menu widget_menu: widget menu + :return: nothing + """ + adjacency_menu = tk.Menu(widget_menu) + adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) + adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) + + widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + + def create_widgets_menu(self): + """ + Create widget menu + + :return: nothing + """ + widget_menu = tk.Menu(self.menubar) + self.create_observer_widgets_menu(widget_menu) + self.create_adjacency_menu(widget_menu) + widget_menu.add_command(label="Throughput", command=action.widgets_throughput) + + widget_menu.add_separator() + + widget_menu.add_command( + label="Configure Adjacency...", command=action.widgets_configure_adjacency + ) + widget_menu.add_command( + label="Configure Throughput...", command=action.widgets_configure_throughput + ) + + self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) + + def create_session_menu(self): + """ + Create session menu + + :return: nothing + """ + session_menu = tk.Menu(self.menubar) + session_menu.add_command( + label="Start", command=action.session_start, underline=0 + ) + session_menu.add_command( + label="Change sessions...", + command=action.session_change_sessions, + underline=0, + ) + + session_menu.add_separator() + + session_menu.add_command( + label="Node types...", command=action.session_node_types, underline=0 + ) + session_menu.add_command( + label="Comments...", command=action.session_comments, underline=0 + ) + session_menu.add_command( + label="Hooks...", command=action.session_hooks, underline=0 + ) + session_menu.add_command( + label="Reset node positions", + command=action.session_reset_node_positions, + underline=0, + ) + session_menu.add_command( + label="Emulation servers...", + command=action.session_emulation_servers, + underline=0, + ) + session_menu.add_command( + label="Options...", command=action.session_options, underline=0 + ) + + self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) + + def create_help_menu(self): + """ + Create help menu + + :return: nothing + """ + help_menu = tk.Menu(self.menubar) + help_menu.add_command( + label="Core Github (www)", command=action.help_core_github + ) + help_menu.add_command( + label="Core Documentation (www)", command=action.help_core_documentation + ) + help_menu.add_command(label="About", command=action.help_about) + + self.menubar.add_cascade(label="Help", menu=help_menu) + + def bind_menubar_shortcut(self): + """ + Bind hot keys to matching command + + :return: nothing + """ + self.application.bind_all("", action.file_new_shortcut) + self.application.bind_all("", action.file_open_shortcut) + self.application.bind_all("", action.file_save_shortcut) + self.application.bind_all("", action.edit_undo_shortcut) + self.application.bind_all("", action.edit_redo_shortcut) + self.application.bind_all("", action.edit_cut_shortcut) + self.application.bind_all("", action.edit_copy_shortcut) + self.application.bind_all("", action.edit_paste_shortcut) + self.application.bind_all("", action.edit_select_all_shortcut) + self.application.bind_all("", action.edit_select_adjacent_shortcut) + self.application.bind_all("", action.edit_find_shortcut) + self.application.bind_all("", action.canvas_previous_shortcut) + self.application.bind_all("", action.canvas_next_shortcut) + self.application.bind_all("", action.canvas_first_shortcut) + self.application.bind_all("", action.canvas_last_shortcut) + self.application.bind_all("", action.view_zoom_in_shortcut) + self.application.bind_all("", action.view_zoom_out_shortcut) + + def create_core_menubar(self): + """ + Create core menubar and bind the hot keys to their matching command + + :return: nothing + """ + self.create_file_menu() + self.create_edit_menu() + self.create_canvas_menu() + self.create_view_menu() + self.create_tools_menu() + self.create_widgets_menu() + self.create_session_menu() + self.create_help_menu() + self.bind_menubar_shortcut() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py new file mode 100644 index 00000000..e66bbcc6 --- /dev/null +++ b/coretk/coretk/coretoolbar.py @@ -0,0 +1,421 @@ +import logging +import tkinter as tk + +from coretk.images import Images + + +class CoreToolbar(object): + """ + Core toolbar class + """ + + # TODO Temporarily have a radio_value instance here, might have to include the run frame + def __init__(self, master, edit_frame): + """ + Create a CoreToolbar instance + + :param tkinter.Frame edit_frame: edit frame + """ + self.master = master + self.edit_frame = edit_frame + self.radio_value = tk.IntVar() + + # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node + self.selection_tool_button = None + # Reference to the option menus + self.link_layer_option_menu = None + self.marker_option_menu = None + self.network_layer_option_menu = None + + def load_toolbar_images(self): + """ + Load the images that appear in core toolbar + + :return: nothing + """ + Images.load("core", "core-icon.png") + Images.load("start", "start.gif") + Images.load("switch", "lanswitch.gif") + Images.load("marker", "marker.gif") + Images.load("router", "router.gif") + Images.load("select", "select.gif") + Images.load("link", "link.gif") + Images.load("hub", "hub.gif") + Images.load("wlan", "wlan.gif") + Images.load("rj45", "rj45.gif") + Images.load("tunnel", "tunnel.gif") + Images.load("oval", "oval.gif") + Images.load("rectangle", "rectangle.gif") + Images.load("text", "text.gif") + + def hide_all_option_menu_frames(self): + """ + Hide any option menu frame that is displayed on screen so that when a new option menu frame is drawn, only + one frame is displayed + + :return: nothing + """ + if self.marker_option_menu: + self.marker_option_menu.place_forget() + if self.link_layer_option_menu: + self.link_layer_option_menu.place_forget() + if self.network_layer_option_menu: + self.network_layer_option_menu.place_forget() + + def click_selection_tool(self): + logging.debug("Click selection tool") + + def create_selection_tool_button(self): + """ + Create selection tool button + + :return: nothing + """ + selection_tool_image = Images.get("select") + self.selection_tool_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=1, + width=32, + height=32, + image=selection_tool_image, + command=self.click_selection_tool, + ) + self.selection_tool_button.pack(side=tk.TOP, pady=1) + + def create_start_stop_session_button(self): + """ + Create start stop session button + + :return: nothing + """ + start_image = Images.get("start") + start_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=2, + width=32, + height=32, + image=start_image, + ) + start_button.pack(side=tk.TOP, pady=1) + + def create_link_tool_button(self): + """ + Create link tool button + + :return: nothing + """ + link_image = Images.get("link") + link_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=3, + width=32, + height=32, + image=link_image, + ) + link_button.pack(side=tk.TOP, pady=1) + + def create_network_layer_button(self): + """ + Create network layer button + + :return: nothing + """ + router_image = Images.get("router") + network_layer_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=4, + width=32, + height=32, + image=router_image, + ) + network_layer_button.pack(side=tk.TOP, pady=1) + + def pick_hub(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("hub")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node HUB") + + def pick_switch(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("switch")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node SWITCH") + + def pick_wlan(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("wlan")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node WLAN") + + def pick_rj45(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("rj45")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node RJ45") + + def pick_tunnel(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("tunnel")) + if self.radio_value.get() != 5: + self.radio_value.set(5) + logging.debug("Pick link-layer node TUNNEL") + + def draw_link_layer_options(self, link_layer_button): + # TODO if other buttons are press or nothing is pressed or the button is pressed but frame is forgotten/hidden + option_frame = tk.Frame(self.master) + current_choice = self.radio_value.get() + self.hide_all_option_menu_frames() + if ( + current_choice == 0 + or current_choice != 5 + or (current_choice == 5 and not option_frame.winfo_manager()) + ): + hub_image = Images.get("hub") + switch_image = Images.get("switch") + wlan_image = Images.get("wlan") + rj45_image = Images.get("rj45") + tunnel_image = Images.get("tunnel") + + hub_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=7, + width=32, + height=32, + image=hub_image, + command=lambda: self.pick_hub( + frame=option_frame, main_button=link_layer_button + ), + ) + hub_button.pack(side=tk.LEFT, pady=1) + switch_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=8, + width=32, + height=32, + image=switch_image, + command=lambda: self.pick_switch( + frame=option_frame, main_button=link_layer_button + ), + ) + switch_button.pack(side=tk.LEFT, pady=1) + wlan_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=9, + width=32, + height=32, + image=wlan_image, + command=lambda: self.pick_wlan( + frame=option_frame, main_button=link_layer_button + ), + ) + wlan_button.pack(side=tk.LEFT, pady=1) + rj45_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=10, + width=32, + height=32, + image=rj45_image, + command=lambda: self.pick_rj45( + frame=option_frame, main_button=link_layer_button + ), + ) + rj45_button.pack(side=tk.LEFT, pady=1) + tunnel_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=11, + width=32, + height=32, + image=tunnel_image, + command=lambda: self.pick_tunnel( + frame=option_frame, main_button=link_layer_button + ), + ) + tunnel_button.pack(side=tk.LEFT, pady=1) + + _x = ( + link_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 33 + ) + _y = ( + link_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + ) + option_frame.place(x=_x, y=_y) + self.link_layer_option_menu = option_frame + + def create_link_layer_button(self): + """ + Create link-layer node button and the options that represent different link-layer node types + + :return: nothing + """ + hub_image = Images.get("hub") + link_layer_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=5, + width=32, + height=32, + image=hub_image, + command=lambda: self.draw_link_layer_options(link_layer_button), + ) + link_layer_button.pack(side=tk.TOP, pady=1) + + def pick_marker(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("marker")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick marker") + + def pick_oval(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("oval")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick frame") + + def pick_rectangle(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("rectangle")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick rectangle") + + def pick_text(self, frame, main_button): + frame.place_forget() + main_button.configure(image=Images.get("text")) + if self.radio_value.get() != 6: + self.radio_value.set(6) + logging.debug("Pick text") + + def draw_marker_options(self, main_button): + # TODO if no button pressed, or other buttons being pressed, or marker button is being pressed but no frame is drawn + option_frame = tk.Frame(self.master) + current_choice = self.radio_value.get() + self.hide_all_option_menu_frames() + # TODO might need to find better way to write this, or might not + if ( + current_choice == 0 + or current_choice != 6 + or (current_choice == 6 and not option_frame.winfo_manager()) + ): + marker_image = Images.get("marker") + oval_image = Images.get("oval") + rectangle_image = Images.get("rectangle") + text_image = Images.get("text") + + marker_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=12, + width=32, + height=32, + image=marker_image, + command=lambda: self.pick_marker( + frame=option_frame, main_button=main_button + ), + ) + marker_button.pack(side=tk.LEFT, pady=1) + oval_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=13, + width=32, + height=32, + image=oval_image, + command=lambda: self.pick_oval( + frame=option_frame, main_button=main_button + ), + ) + oval_button.pack(side=tk.LEFT, pady=1) + rectangle_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=14, + width=32, + height=32, + image=rectangle_image, + command=lambda: self.pick_rectangle( + frame=option_frame, main_button=main_button + ), + ) + rectangle_button.pack(side=tk.LEFT, pady=1) + text_button = tk.Radiobutton( + option_frame, + indicatoron=False, + variable=self.radio_value, + value=15, + width=32, + height=32, + image=text_image, + command=lambda: self.pick_text( + frame=option_frame, main_button=main_button + ), + ) + text_button.pack(side=tk.LEFT, pady=1) + self.master.update() + _x = ( + main_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 33 + ) + _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() + option_frame.place(x=_x, y=_y) + self.marker_option_menu = option_frame + + def create_marker_button(self): + """ + Create marker button and options that represent different marker types + + :return: nothing + """ + marker_image = Images.get("marker") + marker_main_button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=6, + width=32, + height=32, + image=marker_image, + command=lambda: self.draw_marker_options(marker_main_button), + ) + marker_main_button.pack(side=tk.TOP, pady=1) + + def create_toolbar(self): + self.load_toolbar_images() + self.create_selection_tool_button() + self.create_start_stop_session_button() + self.create_link_tool_button() + self.create_network_layer_button() + self.create_link_layer_button() + self.create_marker_button() diff --git a/coretk/coretk/oval.gif b/coretk/coretk/oval.gif new file mode 100644 index 0000000000000000000000000000000000000000..4b3124d4c9f45c45764c282af9fa277fd20ccc6a GIT binary patch literal 174 zcmZ?wbhEHbRA5kGSjfoG(9rPl?Fk4{{3orEtf^pRU|_CLl98(5l%JZJm#*NPpIeZa zSIMCGlZBCsfr&wf0SG|a8JH5L^shYqmVdFyf+%Zojy>fQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aG|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP literal 0 HcmV?d00001 diff --git a/coretk/coretk/rj45.gif b/coretk/coretk/rj45.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ab7ac56dba8946e37182cf05ca160683f53e2e9 GIT binary patch literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPYjEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% literal 0 HcmV?d00001 diff --git a/coretk/coretk/tunnel.gif b/coretk/coretk/tunnel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d574147f535122637516f4887d931779c5903ead GIT binary patch literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/wlan.gif b/coretk/coretk/wlan.gif new file mode 100644 index 0000000000000000000000000000000000000000..d72fe9c3f8db0c7013424313bc826eca7022c19a GIT binary patch literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A literal 0 HcmV?d00001 From b1bac1dda07ff3118dabf105f736b1c180681ea7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 26 Sep 2019 16:05:57 -0700 Subject: [PATCH 020/305] progress on core toolbar --- coretk/coretk/OVS.gif | Bin 0 -> 744 bytes coretk/coretk/app.py | 176 +------- coretk/coretk/coretoolbar.py | 602 ++++++++++++++++---------- coretk/coretk/document-properties.gif | Bin 0 -> 635 bytes coretk/coretk/host.gif | Bin 0 -> 1189 bytes coretk/coretk/mdr.gif | Bin 0 -> 1276 bytes coretk/coretk/observe.gif | Bin 0 -> 1149 bytes coretk/coretk/pc.gif | Bin 0 -> 1300 bytes coretk/coretk/plot.gif | Bin 0 -> 265 bytes coretk/coretk/router_green.gif | Bin 0 -> 753 bytes coretk/coretk/run.gif | Bin 0 -> 324 bytes coretk/coretk/stop.gif | Bin 0 -> 1204 bytes coretk/coretk/toolbaraction.py | 17 + coretk/coretk/twonode.gif | Bin 0 -> 220 bytes 14 files changed, 393 insertions(+), 402 deletions(-) create mode 100755 coretk/coretk/OVS.gif create mode 100644 coretk/coretk/document-properties.gif create mode 100644 coretk/coretk/host.gif create mode 100644 coretk/coretk/mdr.gif create mode 100644 coretk/coretk/observe.gif create mode 100644 coretk/coretk/pc.gif create mode 100644 coretk/coretk/plot.gif create mode 100644 coretk/coretk/router_green.gif create mode 100644 coretk/coretk/run.gif create mode 100644 coretk/coretk/stop.gif create mode 100644 coretk/coretk/toolbaraction.py create mode 100644 coretk/coretk/twonode.gif diff --git a/coretk/coretk/OVS.gif b/coretk/coretk/OVS.gif new file mode 100755 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 37e51aff..43273462 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -17,15 +17,7 @@ class Application(tk.Frame): self.create_widgets() def load_images(self): - # Images.load("switch", "switch.png") Images.load("core", "core-icon.png") - Images.load("start", "start.gif") - Images.load("switch", "lanswitch.gif") - Images.load("marker", "marker.gif") - Images.load("router", "router.gif") - Images.load("select", "select.gif") - Images.load("link", "link.gif") - Images.load("hub", "hub.gif") def setup_app(self): self.master.title("CORE") @@ -41,176 +33,14 @@ class Application(tk.Frame): core_menu.create_core_menubar() self.master.config(menu=self.menubar) - # TODO clean up this code - def create_network_layer_node( - self, edit_frame, radio_value, hub_image, switch_image - ): - menu_button = tk.Menubutton( - edit_frame, - direction=tk.RIGHT, - image=hub_image, - width=32, - height=32, - relief=tk.RAISED, - ) - # menu_button.grid() - menu_button.menu = tk.Menu(menu_button) - menu_button["menu"] = menu_button.menu - - menu_button.menu.add_radiobutton( - image=hub_image, variable=radio_value, value=7, indicatoron=False - ) - menu_button.menu.add_radiobutton( - image=switch_image, variable=radio_value, value=8, indicatoron=False - ) - menu_button.pack(side=tk.TOP, pady=1) - self.master.update() - print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) - # print(menu_button.winfo_width(), menu_button.winfo_height()) - # print(self.master.winfo_height()) - option_frame = tk.Frame(self.master) - - switch_button = tk.Button(option_frame, image=switch_image, width=32, height=32) - switch_button.pack(side=tk.LEFT, pady=1) - hub_button = tk.Button(option_frame, image=hub_image, width=32, height=32) - hub_button.pack(side=tk.LEFT, pady=1) - print("Place the button") - print(menu_button.winfo_rootx(), menu_button.winfo_rooty()) - option_frame.place( - x=menu_button.winfo_rootx() + 33, y=menu_button.winfo_rooty() - 117 - ) - self.update() - - print("option frame: " + str(option_frame.winfo_rooty())) - print("option frame x: " + str(option_frame.winfo_rootx())) - - print("frame dimension: " + str(option_frame.winfo_height())) - print("button height: " + str(hub_button.winfo_rooty())) - - # TODO switch 177 into the rooty of the selection tool, retrieve image in here - def draw_options(self, main_button, radio_value): - hub_image = Images.get("hub") - switch_image = Images.get("switch") - option_frame = tk.Frame(self.master) - - switch_button = tk.Radiobutton( - option_frame, - image=switch_image, - width=32, - height=32, - variable=radio_value, - value=7, - indicatoron=False, - ) - switch_button.pack(side=tk.LEFT, pady=1) - hub_button = tk.Radiobutton( - option_frame, - image=hub_image, - width=32, - height=32, - variable=radio_value, - value=8, - indicatoron=False, - ) - hub_button.pack(side=tk.LEFT, pady=1) - self.master.update() - option_frame.place( - x=main_button.winfo_rootx() + 35 - self.selection_button.winfo_rootx(), - y=main_button.winfo_rooty() - self.selection_button.winfo_rooty(), - ) - - def create_network_layer_node_attempt2(self, edit_frame, radio_value): - hub_image = Images.get("hub") - main_button = tk.Radiobutton( - edit_frame, image=hub_image, width=32, height=32, indicatoron=False - ) - main_button.pack(side=tk.TOP, pady=1) - self.draw_options(main_button, radio_value) - def create_widgets(self): - - """ - select_image = Images.get("select") - start_image = Images.get("start") - link_image = Images.get("link") - router_image = Images.get("router") - hub_image = Images.get("hub") - switch_image = Images.get("switch") - marker_image = Images.get("marker") - """ - edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - core_editbar = CoreToolbar(self.master, edit_frame) + exec_frame = tk.Frame(edit_frame) + exec_frame.pack(side=tk.TOP) + core_editbar = CoreToolbar(self.master, edit_frame, exec_frame, self.menubar) core_editbar.create_toolbar() - """ - radio_value = tk.IntVar() - self.selection_button = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=1, - width=32, - height=32, - image=select_image, - ) - self.selection_button.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=2, - width=32, - height=32, - image=start_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=3, - width=32, - height=32, - image=link_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=4, - width=32, - height=32, - image=router_image, - ) - b.pack(side=tk.TOP, pady=1) - - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=5, - width=32, - height=32, - image=hub_image, - ) - b.pack(side=tk.TOP, pady=1) - b = tk.Radiobutton( - edit_frame, - indicatoron=False, - variable=radio_value, - value=6, - width=32, - height=32, - image=marker_image, - ) - b.pack(side=tk.TOP, pady=1) - - #self.create_network_layer_node(edit_frame, radio_value, hub_image, switch_image) - self.create_network_layer_node_attempt2(edit_frame, radio_value) - """ self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index e66bbcc6..44b4f472 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.toolbaraction as tbaction from coretk.images import Images @@ -9,8 +10,7 @@ class CoreToolbar(object): Core toolbar class """ - # TODO Temporarily have a radio_value instance here, might have to include the run frame - def __init__(self, master, edit_frame): + def __init__(self, master, edit_frame, exec_frame, menubar): """ Create a CoreToolbar instance @@ -18,15 +18,25 @@ class CoreToolbar(object): """ self.master = master self.edit_frame = edit_frame + self.execution_frame = exec_frame + self.menubar = menubar self.radio_value = tk.IntVar() + self.exec_radio_value = tk.IntVar() + + # button dimension + self.width = 32 + self.height = 32 # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node self.selection_tool_button = None + # Reference to the option menus self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None + self.execution_frame = None + def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -47,23 +57,70 @@ class CoreToolbar(object): Images.load("oval", "oval.gif") Images.load("rectangle", "rectangle.gif") Images.load("text", "text.gif") + Images.load("host", "host.gif") + Images.load("pc", "pc.gif") + Images.load("mdr", "mdr.gif") + Images.load("prouter", "router_green.gif") + Images.load("ovs", "OVS.gif") + Images.load("editnode", "document-properties.gif") + Images.load("run", "run.gif") + Images.load("plot", "plot.gif") + Images.load("twonode", "twonode.gif") + Images.load("stop", "stop.gif") + Images.load("observe", "observe.gif") - def hide_all_option_menu_frames(self): + def destroy_previous_frame(self): """ - Hide any option menu frame that is displayed on screen so that when a new option menu frame is drawn, only - one frame is displayed + Destroy any extra frame from previous before drawing a new one :return: nothing """ - if self.marker_option_menu: - self.marker_option_menu.place_forget() - if self.link_layer_option_menu: - self.link_layer_option_menu.place_forget() - if self.network_layer_option_menu: - self.network_layer_option_menu.place_forget() + if ( + self.network_layer_option_menu + and self.network_layer_option_menu.winfo_exists() + ): + self.network_layer_option_menu.destroy() + if self.link_layer_option_menu and self.link_layer_option_menu.winfo_exists(): + self.link_layer_option_menu.destroy() + if self.marker_option_menu and self.marker_option_menu.winfo_exists(): + self.marker_option_menu.destroy() + + def create_buttons(self, img, func, frame, main_button): + """ + Create button and put it on the frame + + :param PIL.Image img: button image + :param func: the command that is executed when button is clicked + :param tkinter.Frame frame: frame that contains the button + :param tkinter.Radiobutton main_button: main button + :return: nothing + """ + button = tk.Button(frame, width=self.width, height=self.height, image=img) + button.pack(side=tk.LEFT, pady=1) + button.bind("", lambda mb: func(main_button)) + + def bind_widgets_before_frame_hide(self, frame): + """ + Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before + any further action is performed + + :param tkinter.Frame frame: the frame to be destroyed + :return: nothing + """ + self.menubar.bind("", lambda e: frame.destroy()) + self.master.bind("", lambda e: frame.destroy()) + + def unbind_widgets_after_frame_hide(self): + """ + Unbind the widgets to make sure everything works normally again after the menu option frame is destroyed + + :return: nothing + """ + self.master.unbind("") + self.menubar.unbind("Button-1>") def click_selection_tool(self): - logging.debug("Click selection tool") + logging.debug("Click SELECTION TOOL") def create_selection_tool_button(self): """ @@ -77,13 +134,19 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=1, - width=32, - height=32, + width=self.width, + height=self.height, image=selection_tool_image, - command=self.click_selection_tool, + command=lambda: self.click_selection_tool(), ) self.selection_tool_button.pack(side=tk.TOP, pady=1) + def click_start_stop_session_tool(self): + logging.debug("Click START STOP SESSION button") + for i in self.edit_frame.winfo_children(): + i.destroy() + self.create_runtime_tool_bar() + def create_start_stop_session_button(self): """ Create start stop session button @@ -96,9 +159,10 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=2, - width=32, - height=32, + width=self.width, + height=self.height, image=start_image, + command=lambda: self.click_start_stop_session_tool(), ) start_button.pack(side=tk.TOP, pady=1) @@ -114,12 +178,100 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=3, - width=32, - height=32, + width=self.width, + height=self.height, image=link_image, + command=lambda: tbaction.click_link_tool(), ) link_button.pack(side=tk.TOP, pady=1) + def pick_router(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("router")) + logging.debug("Pick router option") + + def pick_host(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("host")) + logging.debug("Pick host option") + + def pick_pc(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("pc")) + logging.debug("Pick PC option") + + def pick_mdr(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("mdr")) + logging.debug("Pick MDR option") + + def pick_prouter(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("prouter")) + logging.debug("Pick prouter option") + + def pick_ovs(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("ovs")) + logging.debug("Pick OVS option") + + def pick_editnode(self, main_button): + self.network_layer_option_menu.destroy() + main_button.configure(image=Images.get("editnode")) + logging.debug("Pick editnode option") + + def draw_network_layer_options(self, network_layer_button): + """ + Draw the options for network-layer button + + :param tkinter.Radiobutton network_layer_button: network-layer button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() + option_frame = tk.Frame(self.master) + img_list = [ + Images.get("router"), + Images.get("host"), + Images.get("pc"), + Images.get("mdr"), + Images.get("prouter"), + Images.get("ovs"), + Images.get("editnode"), + ] + func_list = [ + self.pick_router, + self.pick_host, + self.pick_pc, + self.pick_mdr, + self.pick_prouter, + self.pick_ovs, + self.pick_editnode, + ] + for i in range(len(img_list)): + self.create_buttons( + img_list[i], func_list[i], option_frame, network_layer_button + ) + + # place frame at a calculated position as well as keep a reference of that frame + _x = ( + network_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 40 + ) + _y = ( + network_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + - 1 + ) + option_frame.place(x=_x, y=_y) + self.network_layer_option_menu = option_frame + + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() + def create_network_layer_button(self): """ Create network layer button @@ -132,140 +284,86 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=4, - width=32, - height=32, + width=self.width, + height=self.height, image=router_image, + command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) - def pick_hub(self, frame, main_button): - frame.place_forget() + def pick_hub(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node HUB") - def pick_switch(self, frame, main_button): - frame.place_forget() + def pick_switch(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node SWITCH") - def pick_wlan(self, frame, main_button): - frame.place_forget() + def pick_wlan(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node WLAN") - def pick_rj45(self, frame, main_button): - frame.place_forget() + def pick_rj45(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node RJ45") - def pick_tunnel(self, frame, main_button): - frame.place_forget() + def pick_tunnel(self, main_button): + self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - if self.radio_value.get() != 5: - self.radio_value.set(5) logging.debug("Pick link-layer node TUNNEL") def draw_link_layer_options(self, link_layer_button): - # TODO if other buttons are press or nothing is pressed or the button is pressed but frame is forgotten/hidden + """ + Draw the options for link-layer button + + :param tkinter.RadioButton link_layer_button: link-layer button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() option_frame = tk.Frame(self.master) - current_choice = self.radio_value.get() - self.hide_all_option_menu_frames() - if ( - current_choice == 0 - or current_choice != 5 - or (current_choice == 5 and not option_frame.winfo_manager()) - ): - hub_image = Images.get("hub") - switch_image = Images.get("switch") - wlan_image = Images.get("wlan") - rj45_image = Images.get("rj45") - tunnel_image = Images.get("tunnel") + img_list = [ + Images.get("hub"), + Images.get("switch"), + Images.get("wlan"), + Images.get("rj45"), + Images.get("tunnel"), + ] + func_list = [ + self.pick_hub, + self.pick_switch, + self.pick_wlan, + self.pick_rj45, + self.pick_tunnel, + ] + for i in range(len(img_list)): + self.create_buttons( + img_list[i], func_list[i], option_frame, link_layer_button + ) - hub_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=7, - width=32, - height=32, - image=hub_image, - command=lambda: self.pick_hub( - frame=option_frame, main_button=link_layer_button - ), - ) - hub_button.pack(side=tk.LEFT, pady=1) - switch_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=8, - width=32, - height=32, - image=switch_image, - command=lambda: self.pick_switch( - frame=option_frame, main_button=link_layer_button - ), - ) - switch_button.pack(side=tk.LEFT, pady=1) - wlan_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=9, - width=32, - height=32, - image=wlan_image, - command=lambda: self.pick_wlan( - frame=option_frame, main_button=link_layer_button - ), - ) - wlan_button.pack(side=tk.LEFT, pady=1) - rj45_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=10, - width=32, - height=32, - image=rj45_image, - command=lambda: self.pick_rj45( - frame=option_frame, main_button=link_layer_button - ), - ) - rj45_button.pack(side=tk.LEFT, pady=1) - tunnel_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=11, - width=32, - height=32, - image=tunnel_image, - command=lambda: self.pick_tunnel( - frame=option_frame, main_button=link_layer_button - ), - ) - tunnel_button.pack(side=tk.LEFT, pady=1) + # place frame at a calculated position as well as keep a reference of the frame + _x = ( + link_layer_button.winfo_rootx() + - self.selection_tool_button.winfo_rootx() + + 40 + ) + _y = ( + link_layer_button.winfo_rooty() + - self.selection_tool_button.winfo_rooty() + - 1 + ) + option_frame.place(x=_x, y=_y) + self.master.update() + self.link_layer_option_menu = option_frame - _x = ( - link_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 33 - ) - _y = ( - link_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - ) - option_frame.place(x=_x, y=_y) - self.link_layer_option_menu = option_frame + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() def create_link_layer_button(self): """ @@ -279,118 +377,70 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=5, - width=32, - height=32, + width=self.width, + height=self.height, image=hub_image, command=lambda: self.draw_link_layer_options(link_layer_button), ) link_layer_button.pack(side=tk.TOP, pady=1) - def pick_marker(self, frame, main_button): - frame.place_forget() + def pick_marker(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("marker")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick marker") + logging.debug("Pick MARKER") + return "break" - def pick_oval(self, frame, main_button): - frame.place_forget() + def pick_oval(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("oval")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick frame") + logging.debug("Pick OVAL") - def pick_rectangle(self, frame, main_button): - frame.place_forget() + def pick_rectangle(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("rectangle")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick rectangle") + logging.debug("Pick RECTANGLE") - def pick_text(self, frame, main_button): - frame.place_forget() + def pick_text(self, main_button): + self.marker_option_menu.destroy() main_button.configure(image=Images.get("text")) - if self.radio_value.get() != 6: - self.radio_value.set(6) - logging.debug("Pick text") + logging.debug("Pick TEXT") def draw_marker_options(self, main_button): - # TODO if no button pressed, or other buttons being pressed, or marker button is being pressed but no frame is drawn - option_frame = tk.Frame(self.master) - current_choice = self.radio_value.get() - self.hide_all_option_menu_frames() - # TODO might need to find better way to write this, or might not - if ( - current_choice == 0 - or current_choice != 6 - or (current_choice == 6 and not option_frame.winfo_manager()) - ): - marker_image = Images.get("marker") - oval_image = Images.get("oval") - rectangle_image = Images.get("rectangle") - text_image = Images.get("text") + """ + Draw the options for marker button - marker_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=12, - width=32, - height=32, - image=marker_image, - command=lambda: self.pick_marker( - frame=option_frame, main_button=main_button - ), - ) - marker_button.pack(side=tk.LEFT, pady=1) - oval_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=13, - width=32, - height=32, - image=oval_image, - command=lambda: self.pick_oval( - frame=option_frame, main_button=main_button - ), - ) - oval_button.pack(side=tk.LEFT, pady=1) - rectangle_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=14, - width=32, - height=32, - image=rectangle_image, - command=lambda: self.pick_rectangle( - frame=option_frame, main_button=main_button - ), - ) - rectangle_button.pack(side=tk.LEFT, pady=1) - text_button = tk.Radiobutton( - option_frame, - indicatoron=False, - variable=self.radio_value, - value=15, - width=32, - height=32, - image=text_image, - command=lambda: self.pick_text( - frame=option_frame, main_button=main_button - ), - ) - text_button.pack(side=tk.LEFT, pady=1) - self.master.update() - _x = ( - main_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 33 - ) - _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - option_frame.place(x=_x, y=_y) - self.marker_option_menu = option_frame + :param tkinter.Radiobutton main_button: the main button + :return: nothing + """ + # create a frame and add buttons to it + self.destroy_previous_frame() + option_frame = tk.Frame(self.master) + img_list = [ + Images.get("marker"), + Images.get("oval"), + Images.get("rectangle"), + Images.get("text"), + ] + func_list = [ + self.pick_marker, + self.pick_oval, + self.pick_rectangle, + self.pick_text, + ] + for i in range(len(img_list)): + self.create_buttons(img_list[i], func_list[i], option_frame, main_button) + + # place the frame at a calculated position as well as keep a reference of that frame + _x = main_button.winfo_rootx() - self.selection_tool_button.winfo_rootx() + 40 + _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - 1 + option_frame.place(x=_x, y=_y) + self.master.update() + self.marker_option_menu = option_frame + + # destroy the frame before any further actions on other widgets + self.bind_widgets_before_frame_hide(option_frame) + option_frame.wait_window(option_frame) + self.unbind_widgets_after_frame_hide() def create_marker_button(self): """ @@ -404,13 +454,30 @@ class CoreToolbar(object): indicatoron=False, variable=self.radio_value, value=6, - width=32, - height=32, + width=self.width, + height=self.height, image=marker_image, command=lambda: self.draw_marker_options(marker_main_button), ) marker_main_button.pack(side=tk.TOP, pady=1) + def create_selection_tool_button_for_exec(self): + for i in self.edit_frame.winfo_children(): + i.destroy() + selection_tool_image = Images.get("select") + for i in range(7): + button = tk.Radiobutton( + self.edit_frame, + indicatoron=False, + variable=self.radio_value, + value=1, + width=self.width, + height=self.height, + image=selection_tool_image, + command=lambda: tbaction.click_selection_tool(), + ) + button.pack(side=tk.TOP, pady=1) + def create_toolbar(self): self.load_toolbar_images() self.create_selection_tool_button() @@ -419,3 +486,80 @@ class CoreToolbar(object): self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() + + def create_radio_button(self, frame, image, func, value): + button = tk.Radiobutton( + frame, + indicatoron=False, + width=self.width, + height=self.height, + image=image, + value=value, + variable=self.exec_radio_value, + ) + button.pack(side=tk.TOP, pady=1) + + def create_regular_button(self, frame, image, func): + button = tk.Button( + frame, width=self.width, height=self.height, image=image, command=func + ) + button.pack(side=tk.TOP, pady=1) + + def create_observe_button(self): + menu_button = tk.Menubutton( + self.edit_frame, + image=Images.get("observe"), + width=self.width, + height=self.height, + direction=tk.RIGHT, + ) + menu_button.menu = tk.Menu(menu_button, tearoff=0) + menu_button["menu"] = menu_button.menu + menu_button.pack(side=tk.TOP, pady=1) + + menu_button.menu.add_command(label="None") + menu_button.menu.add_command(label="processes") + menu_button.menu.add_command(label="ifconfig") + menu_button.menu.add_command(label="IPv4 routes") + menu_button.menu.add_command(label="IPv6 routes") + menu_button.menu.add_command(label="OSPFv2 neighbors") + menu_button.menu.add_command(label="OSPFv3 neighbors") + menu_button.menu.add_command(label="Listening sockets") + menu_button.menu.add_command(label="IPv4 MFC entries") + menu_button.menu.add_command(label="IPv6 MFC entries") + menu_button.menu.add_command(label="firewall rules") + menu_button.menu.add_command(label="IPSec policies") + menu_button.menu.add_command(label="docker logs") + menu_button.menu.add_command(label="OSPFv3 MDR level") + menu_button.menu.add_command(label="PIM neighbors") + menu_button.menu.add_command(label="Edit...") + + def click_run_button(self): + logging.debug("Click on RUN button") + + def click_stop_button(self): + logging.debug("Click on STOP button ") + for i in self.edit_frame.winfo_children(): + i.destroy() + self.create_toolbar() + + def create_runtime_tool_bar(self): + self.create_radio_button( + self.edit_frame, Images.get("select"), self.click_selection_tool, 1 + ) + self.create_regular_button( + self.edit_frame, Images.get("stop"), self.click_stop_button + ) + self.create_observe_button() + self.create_radio_button( + self.edit_frame, Images.get("plot"), self.click_selection_tool, 2 + ) + self.create_radio_button( + self.edit_frame, Images.get("marker"), self.click_selection_tool, 3 + ) + self.create_radio_button( + self.edit_frame, Images.get("twonode"), self.click_selection_tool, 4 + ) + self.create_regular_button( + self.edit_frame, Images.get("run"), self.click_run_button + ) diff --git a/coretk/coretk/document-properties.gif b/coretk/coretk/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/coretk/coretk/host.gif b/coretk/coretk/host.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bd60ae3d34d9bcd8be206ba2a8a701b19aea319 GIT binary patch literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs literal 0 HcmV?d00001 diff --git a/coretk/coretk/mdr.gif b/coretk/coretk/mdr.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6762f6500828ead06fb73c7a9061165ccdef85b GIT binary patch literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; literal 0 HcmV?d00001 diff --git a/coretk/coretk/observe.gif b/coretk/coretk/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/plot.gif b/coretk/coretk/plot.gif new file mode 100644 index 0000000000000000000000000000000000000000..3924adbf821d1cfeb34a0b9efc7e9d79041fd1df GIT binary patch literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> literal 0 HcmV?d00001 diff --git a/coretk/coretk/router_green.gif b/coretk/coretk/router_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e3ecd57c59ec99767f5641e701f908f09f0258 GIT binary patch literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu literal 0 HcmV?d00001 diff --git a/coretk/coretk/run.gif b/coretk/coretk/run.gif new file mode 100644 index 0000000000000000000000000000000000000000..71dcc67eddc7b829b3221ba4823a04e93c317ba9 GIT binary patch literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S literal 0 HcmV?d00001 diff --git a/coretk/coretk/stop.gif b/coretk/coretk/stop.gif new file mode 100644 index 0000000000000000000000000000000000000000..02c2866864fb955827b6af4d434a8ea03508b5a6 GIT binary patch literal 1204 zcmb```A?Gv9LMo*>GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob literal 0 HcmV?d00001 From 5ce340b8b0dab3ac5e7b3a84a08897c29201c5c1 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 14:19:48 -0700 Subject: [PATCH 021/305] progress on tool bar --- coretk/coretk/app.py | 7 +- coretk/coretk/coretoolbar.py | 269 +++++++++++++++-------------------- 2 files changed, 122 insertions(+), 154 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 43273462..a3fa4e34 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -36,15 +36,16 @@ class Application(tk.Frame): def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - exec_frame = tk.Frame(edit_frame) - exec_frame.pack(side=tk.TOP) - core_editbar = CoreToolbar(self.master, edit_frame, exec_frame, self.menubar) + core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) core_editbar.create_toolbar() self.canvas = CanvasGraph( self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) + + # self.canvas.create_line(0, 0, 10, 10) + scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 44b4f472..83debab5 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,7 +1,6 @@ import logging import tkinter as tk -import coretk.toolbaraction as tbaction from coretk.images import Images @@ -10,7 +9,7 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, master, edit_frame, exec_frame, menubar): + def __init__(self, master, edit_frame, menubar): """ Create a CoreToolbar instance @@ -18,7 +17,6 @@ class CoreToolbar(object): """ self.master = master self.edit_frame = edit_frame - self.execution_frame = exec_frame self.menubar = menubar self.radio_value = tk.IntVar() self.exec_radio_value = tk.IntVar() @@ -35,8 +33,6 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None - self.execution_frame = None - def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -85,7 +81,19 @@ class CoreToolbar(object): if self.marker_option_menu and self.marker_option_menu.winfo_exists(): self.marker_option_menu.destroy() - def create_buttons(self, img, func, frame, main_button): + def destroy_children_widgets(self, parent): + """ + Destroy all children of a parent widget + + :param tkinter.Frame parent: parent frame + :return: nothing + """ + + for i in parent.winfo_children(): + if i.winfo_name() != "!frame": + i.destroy() + + def create_button(self, img, func, frame, main_button): """ Create button and put it on the frame @@ -99,6 +107,40 @@ class CoreToolbar(object): button.pack(side=tk.LEFT, pady=1) button.bind("", lambda mb: func(main_button)) + def create_radio_button(self, frame, image, func, variable, value): + button = tk.Radiobutton( + frame, + indicatoron=False, + width=self.width, + height=self.height, + image=image, + value=value, + variable=variable, + command=func, + ) + button.pack(side=tk.TOP, pady=1) + + def create_regular_button(self, frame, image, func): + button = tk.Button( + frame, width=self.width, height=self.height, image=image, command=func + ) + button.pack(side=tk.TOP, pady=1) + + def draw_button_menu_frame(self, edit_frame, option_frame, main_button): + """ + Draw option menu frame right next to the main button + + :param tkinter.Frame edit_frame: parent frame of the main button + :param tkinter.Frame option_frame: option frame to draw + :param tkinter.Radiobutton main_button: the main button + :return: nothing + """ + + first_button = edit_frame.winfo_children()[0] + _x = main_button.winfo_rootx() - first_button.winfo_rootx() + 40 + _y = main_button.winfo_rooty() - first_button.winfo_rooty() - 1 + option_frame.place(x=_x, y=_y) + def bind_widgets_before_frame_hide(self, frame): """ Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before @@ -122,68 +164,13 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") - def create_selection_tool_button(self): - """ - Create selection tool button - - :return: nothing - """ - selection_tool_image = Images.get("select") - self.selection_tool_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=1, - width=self.width, - height=self.height, - image=selection_tool_image, - command=lambda: self.click_selection_tool(), - ) - self.selection_tool_button.pack(side=tk.TOP, pady=1) - def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") - for i in self.edit_frame.winfo_children(): - i.destroy() - self.create_runtime_tool_bar() + self.destroy_children_widgets(self.edit_frame) + self.create_runtime_toolbar() - def create_start_stop_session_button(self): - """ - Create start stop session button - - :return: nothing - """ - start_image = Images.get("start") - start_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=2, - width=self.width, - height=self.height, - image=start_image, - command=lambda: self.click_start_stop_session_tool(), - ) - start_button.pack(side=tk.TOP, pady=1) - - def create_link_tool_button(self): - """ - Create link tool button - - :return: nothing - """ - link_image = Images.get("link") - link_button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=3, - width=self.width, - height=self.height, - image=link_image, - command=lambda: tbaction.click_link_tool(), - ) - link_button.pack(side=tk.TOP, pady=1) + def click_link_tool(self): + logging.debug("Click LINK button") def pick_router(self, main_button): self.network_layer_option_menu.destroy() @@ -229,7 +216,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("router"), Images.get("host"), @@ -249,22 +236,12 @@ class CoreToolbar(object): self.pick_editnode, ] for i in range(len(img_list)): - self.create_buttons( + self.create_button( img_list[i], func_list[i], option_frame, network_layer_button ) # place frame at a calculated position as well as keep a reference of that frame - _x = ( - network_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 40 - ) - _y = ( - network_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - - 1 - ) - option_frame.place(x=_x, y=_y) + self.draw_button_menu_frame(self.edit_frame, option_frame, network_layer_button) self.network_layer_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -283,7 +260,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=4, + value=3, width=self.width, height=self.height, image=router_image, @@ -325,7 +302,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("hub"), Images.get("switch"), @@ -341,23 +318,12 @@ class CoreToolbar(object): self.pick_tunnel, ] for i in range(len(img_list)): - self.create_buttons( + self.create_button( img_list[i], func_list[i], option_frame, link_layer_button ) # place frame at a calculated position as well as keep a reference of the frame - _x = ( - link_layer_button.winfo_rootx() - - self.selection_tool_button.winfo_rootx() - + 40 - ) - _y = ( - link_layer_button.winfo_rooty() - - self.selection_tool_button.winfo_rooty() - - 1 - ) - option_frame.place(x=_x, y=_y) - self.master.update() + self.draw_button_menu_frame(self.edit_frame, option_frame, link_layer_button) self.link_layer_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -376,7 +342,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=5, + value=4, width=self.width, height=self.height, image=hub_image, @@ -414,7 +380,7 @@ class CoreToolbar(object): """ # create a frame and add buttons to it self.destroy_previous_frame() - option_frame = tk.Frame(self.master) + option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ Images.get("marker"), Images.get("oval"), @@ -428,13 +394,10 @@ class CoreToolbar(object): self.pick_text, ] for i in range(len(img_list)): - self.create_buttons(img_list[i], func_list[i], option_frame, main_button) + self.create_button(img_list[i], func_list[i], option_frame, main_button) # place the frame at a calculated position as well as keep a reference of that frame - _x = main_button.winfo_rootx() - self.selection_tool_button.winfo_rootx() + 40 - _y = main_button.winfo_rooty() - self.selection_tool_button.winfo_rooty() - 1 - option_frame.place(x=_x, y=_y) - self.master.update() + self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) self.marker_option_menu = option_frame # destroy the frame before any further actions on other widgets @@ -453,7 +416,7 @@ class CoreToolbar(object): self.edit_frame, indicatoron=False, variable=self.radio_value, - value=6, + value=5, width=self.width, height=self.height, image=marker_image, @@ -461,50 +424,29 @@ class CoreToolbar(object): ) marker_main_button.pack(side=tk.TOP, pady=1) - def create_selection_tool_button_for_exec(self): - for i in self.edit_frame.winfo_children(): - i.destroy() - selection_tool_image = Images.get("select") - for i in range(7): - button = tk.Radiobutton( - self.edit_frame, - indicatoron=False, - variable=self.radio_value, - value=1, - width=self.width, - height=self.height, - image=selection_tool_image, - command=lambda: tbaction.click_selection_tool(), - ) - button.pack(side=tk.TOP, pady=1) - def create_toolbar(self): self.load_toolbar_images() - self.create_selection_tool_button() - self.create_start_stop_session_button() - self.create_link_tool_button() + self.create_regular_button( + self.edit_frame, Images.get("start"), self.click_start_stop_session_tool + ) + self.create_radio_button( + self.edit_frame, + Images.get("select"), + self.click_selection_tool, + self.radio_value, + 1, + ) + self.create_radio_button( + self.edit_frame, + Images.get("link"), + self.click_link_tool, + self.radio_value, + 2, + ) self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() - def create_radio_button(self, frame, image, func, value): - button = tk.Radiobutton( - frame, - indicatoron=False, - width=self.width, - height=self.height, - image=image, - value=value, - variable=self.exec_radio_value, - ) - button.pack(side=tk.TOP, pady=1) - - def create_regular_button(self, frame, image, func): - button = tk.Button( - frame, width=self.width, height=self.height, image=image, command=func - ) - button.pack(side=tk.TOP, pady=1) - def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, @@ -512,6 +454,7 @@ class CoreToolbar(object): width=self.width, height=self.height, direction=tk.RIGHT, + relief=tk.RAISED, ) menu_button.menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu_button.menu @@ -534,31 +477,55 @@ class CoreToolbar(object): menu_button.menu.add_command(label="PIM neighbors") menu_button.menu.add_command(label="Edit...") + def click_stop_button(self): + logging.debug("Click on STOP button ") + self.destroy_children_widgets(self.edit_frame) + self.create_toolbar() + def click_run_button(self): logging.debug("Click on RUN button") - def click_stop_button(self): - logging.debug("Click on STOP button ") - for i in self.edit_frame.winfo_children(): - i.destroy() - self.create_toolbar() + def click_plot_button(self): + logging.debug("Click on plot button") - def create_runtime_tool_bar(self): - self.create_radio_button( - self.edit_frame, Images.get("select"), self.click_selection_tool, 1 - ) + def click_marker_button(self): + logging.debug("Click on marker button") + + def click_two_node_button(self): + logging.debug("Click TWONODE button") + + def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, Images.get("stop"), self.click_stop_button ) + self.create_radio_button( + self.edit_frame, + Images.get("select"), + self.click_selection_tool, + self.exec_radio_value, + 1, + ) self.create_observe_button() self.create_radio_button( - self.edit_frame, Images.get("plot"), self.click_selection_tool, 2 + self.edit_frame, + Images.get("plot"), + self.click_plot_button, + self.exec_radio_value, + 2, ) self.create_radio_button( - self.edit_frame, Images.get("marker"), self.click_selection_tool, 3 + self.edit_frame, + Images.get("marker"), + self.click_marker_button, + self.exec_radio_value, + 3, ) self.create_radio_button( - self.edit_frame, Images.get("twonode"), self.click_selection_tool, 4 + self.edit_frame, + Images.get("twonode"), + self.click_two_node_button, + self.exec_radio_value, + 4, ) self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button From a1af60688e6da6f8eeb64fc498bb0f10c6fe530e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 15:28:51 -0700 Subject: [PATCH 022/305] add tool tip class to draw tool tip box for the buttons --- coretk/coretk/coretoolbar.py | 3 +++ coretk/coretk/tooltip.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 83debab5..69379aa0 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,6 +3,8 @@ import tkinter as tk from coretk.images import Images +# from coretk.tooltip import CreateToolTip + class CoreToolbar(object): """ @@ -267,6 +269,7 @@ class CoreToolbar(object): command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) + # network_layer_btt = CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): self.link_layer_option_menu.destroy() diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index e69de29b..226b0ed8 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -0,0 +1,38 @@ +import tkinter as tk + + +class CreateToolTip(object): + """ + Create tool tip for a given widget + """ + + def __init__(self, widget, text="widget info"): + self.widget = widget + self.text = text + self.widget.bind("", self.enter) + self.widget.bind("", self.close) + self.tw = None + + def enter(self, event=None): + x = 0 + y = 0 + x, y, cx, cy = self.widget.bbox("insert") + x += self.widget.winfo_rootx() + y += self.widget.winfo_rooty() + 32 + + self.tw = tk.Toplevel(self.widget) + self.tw.wm_overrideredirect(True) + self.tw.wm_geometry("+%d+%d" % (x, y)) + label = tk.Label( + self.tw, + text=self.text, + justify=tk.LEFT, + background="yellow", + relief="solid", + borderwidth=1, + ) + label.pack(ipadx=1) + + def close(self, event=None): + if self.tw: + self.tw.destroy() From 2e007935ee77eac7005f8a5b6c5fa005566dc59b Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 16:00:38 -0700 Subject: [PATCH 023/305] toolbar --- coretk/coretk/coretoolbar.py | 16 ++++++++++++---- coretk/coretk/tooltip.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 69379aa0..74d7979b 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -2,8 +2,7 @@ import logging import tkinter as tk from coretk.images import Images - -# from coretk.tooltip import CreateToolTip +from coretk.tooltip import CreateToolTip class CoreToolbar(object): @@ -109,7 +108,7 @@ class CoreToolbar(object): button.pack(side=tk.LEFT, pady=1) button.bind("", lambda mb: func(main_button)) - def create_radio_button(self, frame, image, func, variable, value): + def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( frame, indicatoron=False, @@ -121,6 +120,7 @@ class CoreToolbar(object): command=func, ) button.pack(side=tk.TOP, pady=1) + CreateToolTip(button, tooltip_msg) def create_regular_button(self, frame, image, func): button = tk.Button( @@ -269,7 +269,7 @@ class CoreToolbar(object): command=lambda: self.draw_network_layer_options(network_layer_button), ) network_layer_button.pack(side=tk.TOP, pady=1) - # network_layer_btt = CreateToolTip(network_layer_button, "Network-layer virtual nodes") + CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): self.link_layer_option_menu.destroy() @@ -352,6 +352,7 @@ class CoreToolbar(object): command=lambda: self.draw_link_layer_options(link_layer_button), ) link_layer_button.pack(side=tk.TOP, pady=1) + CreateToolTip(link_layer_button, "link-layer nodes") def pick_marker(self, main_button): self.marker_option_menu.destroy() @@ -426,6 +427,7 @@ class CoreToolbar(object): command=lambda: self.draw_marker_options(marker_main_button), ) marker_main_button.pack(side=tk.TOP, pady=1) + CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): self.load_toolbar_images() @@ -438,6 +440,7 @@ class CoreToolbar(object): self.click_selection_tool, self.radio_value, 1, + "selection tool", ) self.create_radio_button( self.edit_frame, @@ -445,6 +448,7 @@ class CoreToolbar(object): self.click_link_tool, self.radio_value, 2, + "link tool", ) self.create_network_layer_button() self.create_link_layer_button() @@ -507,6 +511,7 @@ class CoreToolbar(object): self.click_selection_tool, self.exec_radio_value, 1, + "selection tool", ) self.create_observe_button() self.create_radio_button( @@ -515,6 +520,7 @@ class CoreToolbar(object): self.click_plot_button, self.exec_radio_value, 2, + "plot", ) self.create_radio_button( self.edit_frame, @@ -522,6 +528,7 @@ class CoreToolbar(object): self.click_marker_button, self.exec_radio_value, 3, + "marker", ) self.create_radio_button( self.edit_frame, @@ -529,6 +536,7 @@ class CoreToolbar(object): self.click_two_node_button, self.exec_radio_value, 4, + "run command from one node to another", ) self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 226b0ed8..beb68ba3 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -27,7 +27,7 @@ class CreateToolTip(object): self.tw, text=self.text, justify=tk.LEFT, - background="yellow", + background="#ffffe6", relief="solid", borderwidth=1, ) From 4bf08af88606c84ca470d1af373c4872c8bcc1dd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 27 Sep 2019 16:18:30 -0700 Subject: [PATCH 024/305] added core to coretk dependencies --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 111 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index 720b1c45..da8c8df8 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -14,3 +14,4 @@ pre-commit = "*" [packages] coretk = {editable = true,path = "."} +core = {editable = true,path = "./../daemon"} diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index a08ee17f..d3547813 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "52de2a0b7a80abe39564a3943879fe75305b0cb8243c0fce78ef43689bee77c0" + "sha256": "2cb6b23bfb8b9bebb5ece1016de468cc57fe46cf4df08a61b1861aee4bed1028" }, "pipfile-spec": 6, "requires": {}, @@ -14,10 +14,91 @@ ] }, "default": { + "configparser": { + "hashes": [ + "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", + "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df" + ], + "version": "==4.0.2" + }, + "core": { + "editable": true, + "path": "./../daemon" + }, "coretk": { "editable": true, "path": "." }, + "future": { + "hashes": [ + "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + ], + "version": "==0.17.1" + }, + "grpcio": { + "hashes": [ + "sha256:0337debec20fe385bcd49048d6917270efbc17a5119857466559b4db91f8995b", + "sha256:164f82a99e08797ea786283b66b45ebe76772d321577d1674ba6fe0200155892", + "sha256:172dfba8d9621048c2cbc1d1cf7a02244e9a9a8cff5bb79bb30bcb0c13c7fd31", + "sha256:18f4b536d8a9cfa15b3214e0bb628071def94160699e91798f0a954c3b2db88d", + "sha256:2283b56bda49b068b0f08d006fffc7dd46eae72322f1a5dec87fc9c218f1dc2d", + "sha256:26b33f488a955bf49262d2ce3423d3a8174108506d8f819e8150aca21bdd3b99", + "sha256:31cc9b6f70bdd0d9ff53df2d563ea1fb278601d5c625932d8a82d03b08ff3de0", + "sha256:37dd8684fbc2bc00766ae6784bcbd7f874bc96527636a341411db811d04ff650", + "sha256:424c01189ef51a808669f020368b01204e0f1fa0bf2adab7c8d0d13166f92e9e", + "sha256:431c099f20a1f1d97def98f87bb74fa752e8819c2bab23d79089353aed1acc9b", + "sha256:4c2f1d0b27bcef301e5d5c1da05ffd7d174f807f61889c006b8e708b16bc978e", + "sha256:59b8d738867b59c5daaff5df242b5f3f9c58b47862f603c6ee530964b897b69b", + "sha256:8d4f1ee2a67cf8f792d4fc9b8c7bb2148174e838d935f175653aec234752828b", + "sha256:97ab9e35b47bda0441332204960f95c1169c55ec8e989381bedd32bdb9f78b05", + "sha256:9cf93e185507bfdaa7ed45a90049bd3f1ed3f6357ad3772b31e993ff723cf67d", + "sha256:a5a81472c0ca6181492b9291c316ff60c6c94dd3f21c1e8c481f21923d899af0", + "sha256:aaa1feb0fdd094af6db0a16cbd446ed94285a50e320aede5971152d9ea022df8", + "sha256:b36bf4408f8400ee9ab13ff129e71f2e4c72ce2d8886b744aeab77ce50a55cf6", + "sha256:bb345f7e98b38a2c1ef33ff1145687234f78dfeedf308b41b3e41f4b42eba099", + "sha256:c13ae15695e0eb4ba2db920d6a197171d2398c675afa4f27460b6381d20a6884", + "sha256:c4a233a00cc5b64543e97733902151bc6738396931b3c166aad03a3aaadbd479", + "sha256:c521c5f8a95baabba69c68dd0f5e34f37c8adf1a9691f9884dba3eab4ebadc29", + "sha256:c772edd094fe3e54d6d54fdecb90c51cb8d07b55e9c1cda2d33e9615e33d07e8", + "sha256:cebfba6542855403b29e4bc95bbcd5ab444f21137b440f2fb7c7925ca0e55bfd", + "sha256:d7490e013c4bad3e8db804fc6483b47125dc8df0ebcfd6e419bd25df35025301", + "sha256:dfb6063619f297cbd22c67530d7465d98348b35d0424bbc1756b36c5ef9f99d4", + "sha256:e80a15b48a66f35c7c33db2a7df4034a533b362269d0e60e0036e23f14bac7b5", + "sha256:ea444fa1c1ec4f8d2ce965bb01e06148ef9ceb398fb2f627511d50f137eac35b", + "sha256:ec986cbf8837a49f9612cc1cfc2a8ccb54875cfce5355a121279de35124ea1db", + "sha256:fb641df6de8c4a55c784c24d334d53096954d9b30679d3ce5eb6a4d25c1020a3", + "sha256:fb88bd791c8efbcb36de12f0aa519ceec0b7806d3decff16e412e097d4725d44", + "sha256:ffa1be3d566a9cbd21a5f2d95fd9262ec6c337c499291bfeb51547b8de18942e" + ], + "version": "==1.24.0" + }, + "lxml": { + "hashes": [ + "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", + "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", + "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", + "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", + "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", + "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", + "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", + "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", + "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", + "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", + "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", + "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", + "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", + "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", + "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", + "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", + "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", + "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" + ], + "version": "==4.4.1" + }, "pillow": { "hashes": [ "sha256:0804f77cb1e9b6dbd37601cee11283bba39a8d44b9ddb053400c58e0c0d7d9de", @@ -48,6 +129,34 @@ "sha256:f8dc19d92896558f9c4317ee365729ead9d7bbcf2052a9a19a3ef17abbb8ac5b" ], "version": "==6.1.0" + }, + "protobuf": { + "hashes": [ + "sha256:26c0d756c7ad6823fccbc3b5f84c619b9cc7ac281496fe0a9d78e32023c45034", + "sha256:3200046e4d4f6c42ed66257dbe15e2e5dc76072c280e9b3d69dc8f3a4fa3fbbc", + "sha256:368f1bae6dd22d04fd2254d30cd301863408a96ff604422e3ddd8ab601f095a4", + "sha256:3902fa1920b4ef9f710797496b309efc5ccd0faeba44dc82ed6a711a244764a0", + "sha256:3a7a8925ba6481b9241cdb5d69cd0b0700f23efed6bb691dc9543faa4aa25d6f", + "sha256:4bc33d49f43c6e9916fb56b7377cb4478cbf25824b4d2bedfb8a4e3df31c12ca", + "sha256:568b434a36e31ed30d60d600b2227666ce150b8b5275948f50411481a4575d6d", + "sha256:5c393cd665d03ce6b29561edd6b0cc4bcb3fb8e2a7843e8f223d693f07f61b40", + "sha256:80072e9ba36c73cf89c01f669c7b123733fc2de1780b428082a850f53cc7865f", + "sha256:843f498e98ad1469ad54ecb4a7ccf48605a1c5d2bd26ae799c7a2cddab4a37ec", + "sha256:aa45443035651cbfae74c8deb53358ba660d8e7a5fbab3fc4beb33fb3e3ca4be", + "sha256:aaab817d9d038dd5f56a6fb2b2e8ae68caf1fd28cc6a963c755fa73268495c13", + "sha256:e6f68b9979dc8f75299293d682f67fecb72d78f98652da2eeb85c85edef1ca94", + "sha256:e7366cabddff3441d583fdc0176ab42eba4ee7090ef857d50c4dd59ad124003a", + "sha256:f0144ad97cd28bfdda0567b9278d25061ada5ad2b545b538cd3577697b32bda3", + "sha256:f655338491481f482042f19016647e50365ab41b75b486e0df56e0dcc425abf4" + ], + "version": "==3.9.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" } }, "develop": { From 269d7f8f9252801d1ed694f6f633a4f9b68e91fd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 30 Sep 2019 10:11:29 -0700 Subject: [PATCH 025/305] create grids --- coretk/coretk/app.py | 6 +-- coretk/coretk/coretoolbar.py | 76 +++++++++++++++++++++++++---- coretk/coretk/graph.py | 93 +++++++++++++++++++++++++++++++++++- 3 files changed, 162 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index a3fa4e34..1b27587f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -21,7 +21,7 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") - self.master.geometry("800x600") + self.master.geometry("1000x800") image = Images.get("core") self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) @@ -40,11 +40,11 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + master=self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - # self.canvas.create_line(0, 0, 10, 10) + # self.canvas.create_rectangle(0, 0, 1000, 750, outline="#000000", fill="#ffffff", width=1) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 74d7979b..df646488 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from coretk.graph import GraphMode from coretk.images import Images from coretk.tooltip import CreateToolTip @@ -34,6 +35,9 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None + # variables used by canvas graph + self.mode = GraphMode.SELECT + def load_toolbar_images(self): """ Load the images that appear in core toolbar @@ -66,6 +70,24 @@ class CoreToolbar(object): Images.load("stop", "stop.gif") Images.load("observe", "observe.gif") + def get_graph_mode(self): + """ + Retrieve current graph mode + + :rtype: int + :return: current graph mode + """ + return self.mode + + def set_graph_mode(self, mode): + """ + Set graph mode + + :param int mode: graph mode + :return: nothing + """ + self.mode = mode + def destroy_previous_frame(self): """ Destroy any extra frame from previous before drawing a new one @@ -94,7 +116,7 @@ class CoreToolbar(object): if i.winfo_name() != "!frame": i.destroy() - def create_button(self, img, func, frame, main_button): + def create_button(self, img, func, frame, main_button, btt_message): """ Create button and put it on the frame @@ -106,6 +128,7 @@ class CoreToolbar(object): """ button = tk.Button(frame, width=self.width, height=self.height, image=img) button.pack(side=tk.LEFT, pady=1) + CreateToolTip(button, btt_message) button.bind("", lambda mb: func(main_button)) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): @@ -122,11 +145,12 @@ class CoreToolbar(object): button.pack(side=tk.TOP, pady=1) CreateToolTip(button, tooltip_msg) - def create_regular_button(self, frame, image, func): + def create_regular_button(self, frame, image, func, btt_message): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) button.pack(side=tk.TOP, pady=1) + CreateToolTip(button, btt_message) def draw_button_menu_frame(self, edit_frame, option_frame, main_button): """ @@ -165,6 +189,7 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") + self.set_graph_mode(GraphMode.SELECT) def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") @@ -173,6 +198,7 @@ class CoreToolbar(object): def click_link_tool(self): logging.debug("Click LINK button") + self.set_graph_mode(GraphMode.EDGE) def pick_router(self, main_button): self.network_layer_option_menu.destroy() @@ -237,9 +263,22 @@ class CoreToolbar(object): self.pick_ovs, self.pick_editnode, ] + tooltip_list = [ + "router", + "host", + "PC", + "mdr", + "prouter", + "OVS", + "edit node types", + ] for i in range(len(img_list)): self.create_button( - img_list[i], func_list[i], option_frame, network_layer_button + img_list[i], + func_list[i], + option_frame, + network_layer_button, + tooltip_list[i], ) # place frame at a calculated position as well as keep a reference of that frame @@ -320,9 +359,20 @@ class CoreToolbar(object): self.pick_rj45, self.pick_tunnel, ] + tooltip_list = [ + "ethernet hub", + "ethernet switch", + "wireless LAN", + "rj45 physical interface tool", + "tunnel tool", + ] for i in range(len(img_list)): self.create_button( - img_list[i], func_list[i], option_frame, link_layer_button + img_list[i], + func_list[i], + option_frame, + link_layer_button, + tooltip_list[i], ) # place frame at a calculated position as well as keep a reference of the frame @@ -358,7 +408,6 @@ class CoreToolbar(object): self.marker_option_menu.destroy() main_button.configure(image=Images.get("marker")) logging.debug("Pick MARKER") - return "break" def pick_oval(self, main_button): self.marker_option_menu.destroy() @@ -397,8 +446,11 @@ class CoreToolbar(object): self.pick_rectangle, self.pick_text, ] + tooltip_list = ["marker", "oval", "rectangle", "text"] for i in range(len(img_list)): - self.create_button(img_list[i], func_list[i], option_frame, main_button) + self.create_button( + img_list[i], func_list[i], option_frame, main_button, tooltip_list[i] + ) # place the frame at a calculated position as well as keep a reference of that frame self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) @@ -432,7 +484,10 @@ class CoreToolbar(object): def create_toolbar(self): self.load_toolbar_images() self.create_regular_button( - self.edit_frame, Images.get("start"), self.click_start_stop_session_tool + self.edit_frame, + Images.get("start"), + self.click_start_stop_session_tool, + "start the session", ) self.create_radio_button( self.edit_frame, @@ -503,7 +558,10 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( - self.edit_frame, Images.get("stop"), self.click_stop_button + self.edit_frame, + Images.get("stop"), + self.click_stop_button, + "stop the session", ) self.create_radio_button( self.edit_frame, @@ -539,5 +597,5 @@ class CoreToolbar(object): "run command from one node to another", ) self.create_regular_button( - self.edit_frame, Images.get("run"), self.click_run_button + self.edit_frame, Images.get("run"), self.click_run_button, "run" ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9bd599e9..bf85b8cb 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,14 +17,42 @@ class CanvasGraph(tk.Canvas): 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() + self.draw_grid() + + def draw_grid(self, width=1000, height=750): + """ + Create grid + + :param int width: the width + :param int height: the height + + :return: nothing + """ + rectangle_id = self.create_rectangle( + 0, + 0, + width, + height, + outline="#000000", + fill="#ffffff", + width=1, + tags="rectangle", + ) + self.tag_lower(rectangle_id) + for i in range(0, width, 27): + self.create_line(i, 0, i, height, dash=(2, 4), tags="grid line") + for i in range(0, height, 27): + self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -33,6 +61,11 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Three") def setup_bindings(self): + """ + Bind any mouse events or hot keys to the matching action + + :return: nothing + """ self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) @@ -42,11 +75,25 @@ class CanvasGraph(tk.Canvas): self.bind("n", self.set_mode) def canvas_xy(self, event): + """ + Convert window coordinate to canvas coordinate + + :param event: + :rtype: (int, int) + :return: x, y canvas coordinate + """ x = self.canvasx(event.x) y = self.canvasy(event.y) return x, y def get_selected(self, event): + """ + Retrieve the item id that is on the mouse position + + :param event: mouse event + :rtype: int + :return: the item that the mouse point to + """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) nodes = set(self.find_withtag("node")) selected = None @@ -64,6 +111,12 @@ class CanvasGraph(tk.Canvas): return selected def click_release(self, event): + """ + Draw a node or finish drawing an edge according to the current graph mode + + :param event: mouse event + :return: nothing + """ self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") @@ -109,6 +162,12 @@ class CanvasGraph(tk.Canvas): logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): + """ + Start drawing an edge if mouse click is on a node + + :param event: mouse event + :return: nothing + """ logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -117,6 +176,12 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) def click_motion(self, event): + """ + Redraw drawing edge according to the current position of the mouse + + :param event: mouse event + :return: nothing + """ 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) @@ -130,6 +195,12 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def set_mode(self, event): + """ + Set canvas mode according to the hot key that has been pressed + + :param event: key event + :return: nothing + """ logging.debug(f"mode event: {event}") if event.char == "e": self.mode = GraphMode.EDGE @@ -147,21 +218,41 @@ class CanvasGraph(tk.Canvas): class CanvasEdge: + """ + Canvas edge class + """ + width = 3 def __init__(self, x1, y1, x2, y2, src, canvas): + """ + Create an instance of canvas edge object + :param int x1: source x-coord + :param int y1: source y-coord + :param int x2: destination x-coord + :param int y2: destination y-coord + :param int src: source id + :param tkinter.Canvas canvas: canvas object + """ 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) + + # TODO resolve this + # 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) + self.canvas.lift(self.src) + self.canvas.lift(self.dst) + # self.canvas.create_line(0,0,10,10) + # print(x1,y1,x,y) + # self.canvas.create_line(x1+1, y1+1, x+1, y+1) def delete(self): self.canvas.delete(self.id) From cbd593eed6034aa72c0b4d73bbfe1d1c60a44a72 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 1 Oct 2019 16:25:26 -0700 Subject: [PATCH 026/305] finish the basics of toolbar and start working on simple grpc --- coretk/coretk/app.py | 17 ++++- coretk/coretk/coregrpc.py | 83 +++++++++++++++++++++++++ coretk/coretk/coretoolbar.py | 116 +++++++++++++++++------------------ coretk/coretk/graph.py | 105 +++++++++++++++++++++---------- coretk/coretk/images.py | 28 +++++++++ 5 files changed, 254 insertions(+), 95 deletions(-) create mode 100644 coretk/coretk/coregrpc.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1b27587f..9102f4d1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,8 @@ import logging import tkinter as tk +import coretk.images as images +from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph @@ -13,11 +15,16 @@ class Application(tk.Frame): self.load_images() self.setup_app() self.menubar = None + self.canvas = None + self.core_grpc = CoreGrpc() self.create_menu() self.create_widgets() def load_images(self): - Images.load("core", "core-icon.png") + images.load_core_images(Images) + + def close_grpc(self): + self.core_grpc.close() def setup_app(self): self.master.title("CORE") @@ -40,11 +47,14 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - master=self, background="#cccccc", scrollregion=(0, 0, 1000, 1000) + grpc=self.core_grpc, + master=self, + background="#cccccc", + scrollregion=(0, 0, 1000, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) - # self.canvas.create_rectangle(0, 0, 1000, 750, outline="#000000", fill="#ffffff", width=1) + core_editbar.update_canvas(self.canvas) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview @@ -69,3 +79,4 @@ if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() + app.close_grpc() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py new file mode 100644 index 00000000..268c3a13 --- /dev/null +++ b/coretk/coretk/coregrpc.py @@ -0,0 +1,83 @@ +""" +Incorporate grpc into python tkinter GUI +""" +import logging + +from core.api.grpc import client, core_pb2 + + +class CoreGrpc: + def __init__(self): + """ + Create a CoreGrpc instance + """ + self.core = client.CoreGrpcClient() + self.session_id = None + self.set_up() + + def log_event(self, event): + logging.info("event: %s", event) + + def set_up(self): + """ + Create session, handle events session may broadcast, change session state + + :return: nothing + """ + self.core.connect() + # create session + response = self.core.create_session() + logging.info("created session: %s", response) + + # handle events session may broadcast + self.session_id = response.session_id + self.core.events(self.session_id, self.log_event) + + # change session state + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.CONFIGURATION + ) + logging.info("set session state: %s", response) + + def get_session_id(self): + return self.session_id + + # TODO add checkings to the function + def add_node(self, x, y, node_name): + link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] + network_layer_nodes = ["default"] + node = None + if node_name in link_layer_nodes: + if node_name == "switch": + node = core_pb2.Node(type=core_pb2.NodeType.SWITCH) + elif node_name == "hub": + node = core_pb2.Node(type=core_pb2.NodeType.HUB) + elif node_name == "wlan": + node = core_pb2.Node(type=core_pb2.NodeType.WIRELESS_LAN) + elif node_name == "rj45": + node = core_pb2.Node(type=core_pb2.NodeType.RJ45) + elif node_name == "tunnel": + node = core_pb2.Node(type=core_pb2.NodeType.TUNNEL) + + elif node_name in network_layer_nodes: + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(position=position) + else: + return + response = self.core.add_node(self.session_id, node) + logging.info("created %s: %s", node_name, response) + return response.node_id + + def edit_node(self, session_id, node_id, x, y): + position = core_pb2.Position(x=x, y=y) + response = self.core.edit_node(session_id, node_id, position) + logging.info("updated node id %s: %s", node_id, response) + + def close(self): + """ + Clean ups when done using grpc + + :return: nothing + """ + logging.debug("Close grpc") + self.core.close() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index df646488..d4009f1b 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -36,57 +36,17 @@ class CoreToolbar(object): self.network_layer_option_menu = None # variables used by canvas graph - self.mode = GraphMode.SELECT + self.image_to_draw = None + self.canvas = None - def load_toolbar_images(self): + def update_canvas(self, canvas): """ - Load the images that appear in core toolbar + Update canvas variable in CoreToolbar class + :param tkinter.Canvas canvas: core canvas :return: nothing """ - Images.load("core", "core-icon.png") - Images.load("start", "start.gif") - Images.load("switch", "lanswitch.gif") - Images.load("marker", "marker.gif") - Images.load("router", "router.gif") - Images.load("select", "select.gif") - Images.load("link", "link.gif") - Images.load("hub", "hub.gif") - Images.load("wlan", "wlan.gif") - Images.load("rj45", "rj45.gif") - Images.load("tunnel", "tunnel.gif") - Images.load("oval", "oval.gif") - Images.load("rectangle", "rectangle.gif") - Images.load("text", "text.gif") - Images.load("host", "host.gif") - Images.load("pc", "pc.gif") - Images.load("mdr", "mdr.gif") - Images.load("prouter", "router_green.gif") - Images.load("ovs", "OVS.gif") - Images.load("editnode", "document-properties.gif") - Images.load("run", "run.gif") - Images.load("plot", "plot.gif") - Images.load("twonode", "twonode.gif") - Images.load("stop", "stop.gif") - Images.load("observe", "observe.gif") - - def get_graph_mode(self): - """ - Retrieve current graph mode - - :rtype: int - :return: current graph mode - """ - return self.mode - - def set_graph_mode(self, mode): - """ - Set graph mode - - :param int mode: graph mode - :return: nothing - """ - self.mode = mode + self.canvas = canvas def destroy_previous_frame(self): """ @@ -189,47 +149,67 @@ class CoreToolbar(object): def click_selection_tool(self): logging.debug("Click SELECTION TOOL") - self.set_graph_mode(GraphMode.SELECT) + self.canvas.mode = GraphMode.SELECT def click_start_stop_session_tool(self): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) + self.canvas.set_canvas_mode(GraphMode.SELECT) self.create_runtime_toolbar() def click_link_tool(self): logging.debug("Click LINK button") - self.set_graph_mode(GraphMode.EDGE) + self.canvas.set_canvas_mode(GraphMode.EDGE) def pick_router(self, main_button): + logging.debug("Pick router option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("router")) - logging.debug("Pick router option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("router")) + self.canvas.set_drawing_name("default") def pick_host(self, main_button): + logging.debug("Pick host option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("host")) - logging.debug("Pick host option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("host")) + self.canvas.set_drawing_name("default") def pick_pc(self, main_button): + logging.debug("Pick PC option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("pc")) - logging.debug("Pick PC option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("pc")) + self.canvas.set_drawing_name("default") def pick_mdr(self, main_button): + logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("mdr")) - logging.debug("Pick MDR option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("mdr")) + self.canvas.set_drawing_name("default") def pick_prouter(self, main_button): + logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("prouter")) - logging.debug("Pick prouter option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("prouter")) + self.canvas.set_drawing_name("default") def pick_ovs(self, main_button): + logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("ovs")) - logging.debug("Pick OVS option") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("ovs")) + self.canvas.set_drawing_name("default") + # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("editnode")) @@ -311,29 +291,43 @@ class CoreToolbar(object): CreateToolTip(network_layer_button, "Network-layer virtual nodes") def pick_hub(self, main_button): + logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - logging.debug("Pick link-layer node HUB") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("hub")) + self.canvas.set_drawing_name("hub") def pick_switch(self, main_button): + logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - logging.debug("Pick link-layer node SWITCH") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("switch")) + self.canvas.set_drawing_name("switch") def pick_wlan(self, main_button): + logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - logging.debug("Pick link-layer node WLAN") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("wlan")) + self.canvas.set_drawing_name("wlan") def pick_rj45(self, main_button): + logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - logging.debug("Pick link-layer node RJ45") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("rj45")) def pick_tunnel(self, main_button): + logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - logging.debug("Pick link-layer node TUNNEL") + self.canvas.set_canvas_mode(GraphMode.PICKNODE) + self.canvas.set_drawing_image(Images.get("tunnel")) + self.canvas.set_drawing_image(Images.get("tunnel")) def draw_link_layer_options(self, link_layer_button): """ @@ -482,7 +476,7 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): - self.load_toolbar_images() + # self.load_toolbar_images() self.create_regular_button( self.edit_frame, Images.get("start"), @@ -508,6 +502,7 @@ class CoreToolbar(object): self.create_network_layer_button() self.create_link_layer_button() self.create_marker_button() + self.radio_value.set(1) def create_observe_button(self): menu_button = tk.Menubutton( @@ -599,3 +594,4 @@ class CoreToolbar(object): self.create_regular_button( self.edit_frame, Images.get("run"), self.click_run_button, "run" ) + self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index bf85b8cb..06f16488 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,23 +2,25 @@ import enum import logging import tkinter as tk -from coretk.images import Images - class GraphMode(enum.Enum): SELECT = 0 EDGE = 1 - NODE = 2 + PICKNODE = 2 + NODE = 3 + OTHER = 4 class CanvasGraph(tk.Canvas): - def __init__(self, master=None, cnf=None, **kwargs): + def __init__(self, grpc=None, master=None, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) - + self.core_grpc = grpc self.mode = GraphMode.SELECT + self.draw_node_image = None + self.draw_node_name = None self.selected = None self.node_context = None self.nodes = {} @@ -29,6 +31,15 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid() + def set_canvas_mode(self, mode): + self.mode = mode + + def set_drawing_image(self, img): + self.draw_node_image = img + + def set_drawing_name(self, name): + self.draw_node_name = name + def draw_grid(self, width=1000, height=750): """ Create grid @@ -70,9 +81,9 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) - self.bind("e", self.set_mode) - self.bind("s", self.set_mode) - self.bind("n", self.set_mode) + # self.bind("e", self.set_mode) + # self.bind("s", self.set_mode) + # self.bind("n", self.set_mode) def canvas_xy(self, event): """ @@ -124,7 +135,9 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, "switch") + self.add_node(x, y, self.draw_node_image, self.draw_node_name) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -194,25 +207,39 @@ class CanvasGraph(tk.Canvas): logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) - def set_mode(self, event): - """ - Set canvas mode according to the hot key that has been pressed + # def set_mode(self, event): + # """ + # Set canvas mode according to the hot key that has been pressed + # + # :param event: key event + # :return: nothing + # """ + # logging.debug(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 + # logging.debug(f"graph mode: {self.mode}") - :param event: key event - :return: nothing - """ - logging.debug(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 - logging.debug(f"graph mode: {self.mode}") + # def add_node(self, x, y, image_name): + # image = Images.get(image_name) + # node = CanvasNode(x, y, image, self) + # self.nodes[node.id] = node + # return node - def add_node(self, x, y, image_name): - image = Images.get(image_name) - node = CanvasNode(x, y, image, self) + def add_node(self, x, y, image, node_name): + core_session_id = self.core_grpc.get_session_id() + core_node_id = self.core_grpc.add_node(int(x), int(y), node_name) + node = CanvasNode( + core_session_id=core_session_id, + core_node_id=core_node_id, + x=x, + y=y, + image=image, + canvas=self, + ) self.nodes[node.id] = node return node @@ -250,22 +277,23 @@ class CanvasEdge: self.canvas.coords(self.id, x1, y1, x, y) self.canvas.lift(self.src) self.canvas.lift(self.dst) - # self.canvas.create_line(0,0,10,10) - # print(x1,y1,x,y) - # self.canvas.create_line(x1+1, y1+1, x+1, y+1) def delete(self): self.canvas.delete(self.id) class CanvasNode: - def __init__(self, x, y, image, canvas): + def __init__(self, core_session_id, core_node_id, x, y, image, canvas): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.name = f"Node {self.id}" + self.x_coord = x + self.y_coord = y + self.core_session_id = core_session_id + self.core_node_id = core_node_id + self.name = f"Node {self.core_node_id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -274,16 +302,29 @@ class CanvasNode: self.edges = set() self.moving = None + def get_coords(self): + return self.x_coord, self.y_coord + + def update_coords(self): + self.x_coord, self.y_coord = self.canvas.coords(self.id) + def click_press(self, event): logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) def click_release(self, event): logging.debug(f"click release {self.name}: {event}") + self.update_coords() + self.canvas.core_grpc.edit_node( + self.core_session_id, + self.core_node_id, + int(self.x_coord), + int(self.y_coord), + ) self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: + if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: return x, y = self.canvas.canvas_xy(event) moving_x, moving_y = self.moving diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 4ca2b5ac..71758cc2 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -18,3 +18,31 @@ class Images: @classmethod def get(cls, name): return cls.images[name] + + +def load_core_images(images): + images.load("core", "core-icon.png") + images.load("start", "start.gif") + images.load("switch", "lanswitch.gif") + images.load("marker", "marker.gif") + images.load("router", "router.gif") + images.load("select", "select.gif") + images.load("link", "link.gif") + images.load("hub", "hub.gif") + images.load("wlan", "wlan.gif") + images.load("rj45", "rj45.gif") + images.load("tunnel", "tunnel.gif") + images.load("oval", "oval.gif") + images.load("rectangle", "rectangle.gif") + images.load("text", "text.gif") + images.load("host", "host.gif") + images.load("pc", "pc.gif") + images.load("mdr", "mdr.gif") + images.load("prouter", "router_green.gif") + images.load("ovs", "OVS.gif") + images.load("editnode", "document-properties.gif") + images.load("run", "run.gif") + images.load("plot", "plot.gif") + images.load("twonode", "twonode.gif") + images.load("stop", "stop.gif") + images.load("observe", "observe.gif") From cb03aa261a5ba5f47ca0c2404ea1210aea6c8dba Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 3 Oct 2019 16:50:49 -0700 Subject: [PATCH 027/305] some work on grpc add nodes and links, some work on query session, redraw nodes --- coretk/coretk/app.py | 13 ++- coretk/coretk/coregrpc.py | 114 +++++++++++++++++------ coretk/coretk/coretoolbar.py | 93 +++++++++++-------- coretk/coretk/graph.py | 109 +++++++++------------- coretk/coretk/grpcmanagement.py | 157 ++++++++++++++++++++++++++++++++ coretk/coretk/images.py | 38 ++++++++ 6 files changed, 387 insertions(+), 137 deletions(-) create mode 100644 coretk/coretk/grpcmanagement.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9102f4d1..9a9506e2 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -16,16 +16,20 @@ class Application(tk.Frame): self.setup_app() self.menubar = None self.canvas = None + + # start grpc self.core_grpc = CoreGrpc() + self.create_menu() self.create_widgets() def load_images(self): + """ + Load core images + :return: + """ images.load_core_images(Images) - def close_grpc(self): - self.core_grpc.close() - def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") @@ -47,8 +51,8 @@ class Application(tk.Frame): core_editbar.create_toolbar() self.canvas = CanvasGraph( - grpc=self.core_grpc, master=self, + grpc=self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1000, 1000), ) @@ -79,4 +83,3 @@ if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() app.mainloop() - app.close_grpc() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 268c3a13..8596366c 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -14,18 +14,20 @@ class CoreGrpc: self.core = client.CoreGrpcClient() self.session_id = None self.set_up() + self.interface_helper = None def log_event(self, event): logging.info("event: %s", event) - def set_up(self): + def redraw_canvas(self): + return + + def create_new_session(self): """ - Create session, handle events session may broadcast, change session state + Create a new session :return: nothing """ - self.core.connect() - # create session response = self.core.create_session() logging.info("created session: %s", response) @@ -33,39 +35,67 @@ class CoreGrpc: self.session_id = response.session_id self.core.events(self.session_id, self.log_event) - # change session state + def query_existing_sessions(self, sessions): + """ + Query for existing sessions and prompt to join one + + :param repeated core_pb2.SessionSummary sessions: summaries of all the existing sessions + + :return: nothing + """ + for session in sessions: + logging.info("Session id: %s, Session state: %s", session.id, session.state) + logging.info("Input a session you want to enter from the keyboard:") + usr_input = int(input()) + if usr_input == 0: + self.create_new_session() + else: + response = self.core.get_session(usr_input) + self.session_id = usr_input + # self.core.events(self.session_id, self.log_event) + logging.info("Entering session_id %s.... Result: %s", usr_input, response) + + def set_up(self): + """ + Query sessions, if there exist any, promt whether to join one + + :return: nothing + """ + self.core.connect() + + response = self.core.get_sessions() + logging.info("all sessions: %s", response) + + # if there are no sessions, create a new session, else join a session + sessions = response.sessions + + if len(sessions) == 0: + self.create_new_session() + else: + # self.create_new_session() + self.query_existing_sessions(sessions) + + def set_configuration_state(self): response = self.core.set_session_state( self.session_id, core_pb2.SessionState.CONFIGURATION ) logging.info("set session state: %s", response) + def set_instantiate_state(self): + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.INSTANTIATION + ) + logging.info("set session state: %s", response) + def get_session_id(self): return self.session_id - # TODO add checkings to the function - def add_node(self, x, y, node_name): - link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] - network_layer_nodes = ["default"] - node = None - if node_name in link_layer_nodes: - if node_name == "switch": - node = core_pb2.Node(type=core_pb2.NodeType.SWITCH) - elif node_name == "hub": - node = core_pb2.Node(type=core_pb2.NodeType.HUB) - elif node_name == "wlan": - node = core_pb2.Node(type=core_pb2.NodeType.WIRELESS_LAN) - elif node_name == "rj45": - node = core_pb2.Node(type=core_pb2.NodeType.RJ45) - elif node_name == "tunnel": - node = core_pb2.Node(type=core_pb2.NodeType.TUNNEL) - - elif node_name in network_layer_nodes: - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(position=position) - else: - return + def add_node(self, node_type, model, x, y, name): + logging.info("ADD NODE %s", name) + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(type=node_type, position=position, model=model, image=name) response = self.core.add_node(self.session_id, node) - logging.info("created %s: %s", node_name, response) + logging.info("created node: %s", response) return response.node_id def edit_node(self, session_id, node_id, x, y): @@ -73,6 +103,34 @@ class CoreGrpc: response = self.core.edit_node(session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + # def create_interface_helper(self): + # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + # TODO case for other core_pb2.NodeType + def add_link(self, id1, id2, type1, type2): + """ + Grpc client request add link + + :param int session_id: session id + :param int id1: node 1 core id + :param core_pb2.NodeType type1: node 1 core node type + :param int id2: node 2 core id + :param core_pb2.NodeType type2: node 2 core node type + :return: nothing + """ + if not self.interface_helper: + logging.debug("INTERFACE HELPER NOT CREATED YET, CREATING ONE...") + self.interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + interface1 = None + interface2 = None + if type1 == core_pb2.NodeType.DEFAULT: + interface1 = self.interface_helper.create_interface(id1, 0) + if type2 == core_pb2.NodeType.DEFAULT: + interface2 = self.interface_helper.create_interface(id2, 0) + response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + logging.info("created link: %s", response) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d4009f1b..8af43240 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -27,7 +27,6 @@ class CoreToolbar(object): self.width = 32 self.height = 32 - # Used for drawing the horizontally displayed menu items for network-layer nodes and link-layer node self.selection_tool_button = None # Reference to the option menus @@ -35,8 +34,6 @@ class CoreToolbar(object): self.marker_option_menu = None self.network_layer_option_menu = None - # variables used by canvas graph - self.image_to_draw = None self.canvas = None def update_canvas(self, canvas): @@ -151,63 +148,79 @@ class CoreToolbar(object): logging.debug("Click SELECTION TOOL") self.canvas.mode = GraphMode.SELECT - def click_start_stop_session_tool(self): + def click_start_session_tool(self): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) - self.canvas.set_canvas_mode(GraphMode.SELECT) + self.canvas.mode = GraphMode.SELECT self.create_runtime_toolbar() + # set configuration state + self.canvas.core_grpc.set_configuration_state() + + # grpc client requests creating nodes + for node in self.canvas.grpc_manager.nodes_to_create.values(): + self.canvas.core_grpc.add_node( + node.type, node.model, int(node.x), int(node.y), node.name + ) + self.canvas.grpc_manager.nodes_to_create.clear() + + for edge in self.canvas.grpc_manager.edges_to_create.values(): + self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) + self.canvas.grpc_manager.edges_to_create.clear() + + self.canvas.core_grpc.set_instantiate_state() + def click_link_tool(self): logging.debug("Click LINK button") - self.canvas.set_canvas_mode(GraphMode.EDGE) + self.canvas.mode = GraphMode.EDGE def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("router")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("router")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("router") + self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("host")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("host")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("host") + self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("pc")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("pc")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("pc") + self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("mdr")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("mdr")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("mdr") + self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("prouter")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("prouter")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("prouter") + self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() main_button.configure(image=Images.get("ovs")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("ovs")) - self.canvas.set_drawing_name("default") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("ovs") + self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): @@ -294,40 +307,41 @@ class CoreToolbar(object): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("hub")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("hub")) - self.canvas.set_drawing_name("hub") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("hub") + self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("switch")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("switch")) - self.canvas.set_drawing_name("switch") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("switch") + self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("wlan")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("wlan")) - self.canvas.set_drawing_name("wlan") + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("wlan") + self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("rj45")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("rj45")) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("rj45") + self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() main_button.configure(image=Images.get("tunnel")) - self.canvas.set_canvas_mode(GraphMode.PICKNODE) - self.canvas.set_drawing_image(Images.get("tunnel")) - self.canvas.set_drawing_image(Images.get("tunnel")) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get("tunnel") + self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): """ @@ -476,11 +490,10 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): - # self.load_toolbar_images() self.create_regular_button( self.edit_frame, Images.get("start"), - self.click_start_stop_session_tool, + self.click_start_session_tool, "start the session", ) self.create_radio_button( diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 06f16488..5a25188b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,9 @@ import enum import logging import tkinter as tk +from coretk.grpcmanagement import GrpcManager +from coretk.images import Images + class GraphMode(enum.Enum): SELECT = 0 @@ -12,12 +15,11 @@ class GraphMode(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, grpc=None, master=None, cnf=None, **kwargs): + def __init__(self, master=None, grpc=None, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) - self.core_grpc = grpc self.mode = GraphMode.SELECT self.draw_node_image = None self.draw_node_name = None @@ -26,19 +28,32 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.setup_menus() self.setup_bindings() self.draw_grid() + self.core_grpc = grpc + self.grpc_manager = GrpcManager() + self.draw_existing_node() - def set_canvas_mode(self, mode): - self.mode = mode + 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 set_drawing_image(self, img): - self.draw_node_image = img + def setup_bindings(self): + """ + Bind any mouse events or hot keys to the matching action - def set_drawing_name(self, name): - self.draw_node_name = name + :return: nothing + """ + self.bind("", self.click_press) + self.bind("", self.click_release) + self.bind("", self.click_motion) + self.bind("", self.context) + # self.bind("e", self.set_mode) + # self.bind("s", self.set_mode) + # self.bind("n", self.set_mode) def draw_grid(self, width=1000, height=750): """ @@ -65,25 +80,20 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") - 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): + def draw_existing_node(self): """ - Bind any mouse events or hot keys to the matching action + Draw existing node while updating the grpc manager based on that :return: nothing """ - self.bind("", self.click_press) - self.bind("", self.click_release) - self.bind("", self.click_motion) - self.bind("", self.context) - # self.bind("e", self.set_mode) - # self.bind("s", self.set_mode) - # self.bind("n", self.set_mode) + session_id = self.core_grpc.session_id + response = self.core_grpc.core.get_session(session_id) + nodes = response.session.nodes + if len(nodes) > 0: + for node in nodes: + image = Images.convert_type_and_model_to_image(node.type, node.model) + return image + # n = CanvasNode(node.position.x, node.position.y, image, self, node.id) def canvas_xy(self, event): """ @@ -172,6 +182,10 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) + self.grpc_manager.add_edge( + self.core_grpc.get_session_id(), edge.token, node_src.id, node_dst.id + ) + logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): @@ -207,40 +221,14 @@ class CanvasGraph(tk.Canvas): logging.debug(f"node context: {selected}") self.node_context.post(event.x_root, event.y_root) - # def set_mode(self, event): - # """ - # Set canvas mode according to the hot key that has been pressed - # - # :param event: key event - # :return: nothing - # """ - # logging.debug(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 - # logging.debug(f"graph mode: {self.mode}") - - # def add_node(self, x, y, image_name): - # image = Images.get(image_name) - # node = CanvasNode(x, y, image, self) - # self.nodes[node.id] = node - # return node - def add_node(self, x, y, image, node_name): - core_session_id = self.core_grpc.get_session_id() - core_node_id = self.core_grpc.add_node(int(x), int(y), node_name) node = CanvasNode( - core_session_id=core_session_id, - core_node_id=core_node_id, - x=x, - y=y, - image=image, - canvas=self, + x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() ) self.nodes[node.id] = node + self.grpc_manager.add_node( + self.core_grpc.get_session_id(), node.id, x, y, node_name + ) return node @@ -283,17 +271,16 @@ class CanvasEdge: class CanvasNode: - def __init__(self, core_session_id, core_node_id, x, y, image, canvas): + def __init__(self, x, y, image, canvas, core_id): self.image = image self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + self.core_id = core_id self.x_coord = x self.y_coord = y - self.core_session_id = core_session_id - self.core_node_id = core_node_id - self.name = f"Node {self.core_node_id}" + self.name = f"Node {self.core_id}" self.text_id = self.canvas.create_text(x, y + 20, text=self.name) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -315,12 +302,6 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.edit_node( - self.core_session_id, - self.core_node_id, - int(self.x_coord), - int(self.y_coord), - ) self.moving = None def motion(self, event): diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py new file mode 100644 index 00000000..d090f6d6 --- /dev/null +++ b/coretk/coretk/grpcmanagement.py @@ -0,0 +1,157 @@ +""" +Manage useful informations about the nodes, edges and configuration +that can be useful for grpc, acts like a session class +""" +import logging + +from core.api.grpc import core_pb2 + +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] + + +class Node: + def __init__(self, session_id, node_id, type, model, x, y, name): + """ + Create an instance of a node + + :param int session_id: session id + :param int node_id: node id + :param core_pb2.NodeType type: node type + :param int x: x coordinate + :param int y: coordinate + """ + self.session_id = session_id + self.node_id = node_id + self.type = type + self.x = x + self.y = y + self.model = model + self.name = name + + +class Edge: + def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): + """ + Create an instance of an edge + :param int session_id: session id + :param int node_id_1: node 1 id + :param int node_type_1: node 1 type + :param core_pb2.NodeType node_id_2: node 2 id + :param core_pb2.NodeType node_type_2: node 2 type + """ + self.session_id = session_id + self.id1 = node_id_1 + self.id2 = node_id_2 + self.type1 = node_type_1 + self.type2 = node_type_2 + + +class GrpcManager: + def __init__(self): + self.nodes = {} + self.edges = {} + self.id = 1 + # A list of id for re-use, keep in increasing order + self.reusable = [] + self.nodes_to_create = {} + self.edges_to_create = {} + + def peek_id(self): + """ + Peek the next id to be used + + :return: nothing + """ + if len(self.reusable) == 0: + return self.id + else: + return self.reusable[0] + + def get_id(self): + """ + Get the next node id as well as update id status and reusable ids + + :rtype: int + :return: the next id to be used + """ + if len(self.reusable) == 0: + new_id = self.id + self.id = self.id + 1 + return new_id + else: + return self.reusable.pop(0) + + # TODO figure out the name of ovs node + def add_node(self, session_id, canvas_id, x, y, name): + """ + Add node, with information filled in, to grpc manager + + :param int session_id: session id + :param int canvas_id: node's canvas id + :param int x: x coord + :param int y: y coord + :param str name: node type + :return: nothing + """ + node_type = None + node_model = None + if name in link_layer_nodes: + if name == "switch": + node_type = core_pb2.NodeType.SWITCH + elif name == "hub": + node_type = core_pb2.NodeType.HUB + elif name == "wlan": + node_type = core_pb2.NodeType.WIRELESS_LAN + elif name == "rj45": + node_type = core_pb2.NodeType.RJ45 + elif name == "tunnel": + node_type = core_pb2.NodeType.TUNNEL + elif name in network_layer_nodes: + node_type = core_pb2.NodeType.DEFAULT + # TODO what is the model name for ovs + node_model = name + else: + logging.error("grpcmanagemeny.py INVALID node name") + create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) + self.nodes[canvas_id] = create_node + self.nodes_to_create[canvas_id] = create_node + logging.debug( + "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + session_id, + x, + y, + name, + ) + + def add_preexisting_node(self): + return + + def delete_node(self, canvas_id): + """ + Delete a node from the session + + :param int canvas_id: node's id in the canvas + :return: thing + """ + try: + self.nodes.pop(canvas_id) + self.reuseable.append(canvas_id) + self.reuseable.sort() + except KeyError: + logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: + edge = Edge( + session_id, + self.nodes[canvas_id_1].node_id, + self.nodes[canvas_id_1].type, + self.nodes[canvas_id_2].node_id, + self.nodes[canvas_id_2].type, + ) + self.edges[token] = edge + self.edges_to_create[token] = edge + logging.debug("Adding edge to grpc manager...") + else: + logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 71758cc2..4911efac 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,7 +1,10 @@ +import logging import os from PIL import Image, ImageTk +from core.api.grpc import core_pb2 + PATH = os.path.abspath(os.path.dirname(__file__)) @@ -19,6 +22,41 @@ class Images: def get(cls, name): return cls.images[name] + @classmethod + def convert_type_and_model_to_image(cls, node_type, node_model): + """ + Retrieve image based on type and model + :param core_pb2.NodeType node_type: core node type + :param string node_model: the node model + + :return: the matching image + """ + if node_type == core_pb2.NodeType.SWITCH: + return Images.get("switch") + if node_type == core_pb2.NodeType.HUB: + return Images.get("hub") + if node_type == core_pb2.NodeType.WIRELESS_LAN: + return Images.get("wlan") + if node_type == core_pb2.NodeType.RJ45: + return Images.get("rj45") + if node_type == core_pb2.NodeType.TUNNEL: + return Images.get("tunnel") + if node_type == core_pb2.NodeType.DEFAULT: + if node_model == "router": + return Images.get("router") + if node_model == "host": + return Images.get(("host")) + if node_model == "PC": + return Images.get("pc") + if node_model == "mdr": + return Images.get("mdr") + if node_model == "prouter": + return Images.get("prouter") + if node_model == "OVS": + return Images.get("ovs") + else: + logging.debug("INVALID INPUT OR NOT CONSIDERED YET") + def load_core_images(images): images.load("core", "core-icon.png") From 8611106c93bd79b1bcc1923f5d64a4cd1b09d298 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 4 Oct 2019 16:52:07 -0700 Subject: [PATCH 028/305] basics on redraw components from prev section, work more on start/stop session --- coretk/coretk/coregrpc.py | 79 +++++++++++++++++++++++++++------ coretk/coretk/coretoolbar.py | 19 ++++---- coretk/coretk/graph.py | 46 ++++++++++++++----- coretk/coretk/grpcmanagement.py | 53 +++++++++++++++++----- coretk/coretk/images.py | 33 +++++++------- 5 files changed, 170 insertions(+), 60 deletions(-) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 8596366c..bcd04c96 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -55,6 +55,10 @@ class CoreGrpc: # self.core.events(self.session_id, self.log_event) logging.info("Entering session_id %s.... Result: %s", usr_input, response) + def delete_session(self): + response = self.core.delete_session(self.session_id) + logging.info("Deleted session result: %s", response) + def set_up(self): """ Query sessions, if there exist any, promt whether to join one @@ -75,25 +79,59 @@ class CoreGrpc: # self.create_new_session() self.query_existing_sessions(sessions) - def set_configuration_state(self): - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.CONFIGURATION - ) - logging.info("set session state: %s", response) + def get_session_state(self): + response = self.core.get_session(self.session_id) + logging.info("get sessio: %s", response) + return response.session.state + + def set_session_state(self, state): + """ + Set session state + + :param str state: session state to set + :return: nothing + """ + response = None + if state == "configuration": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.CONFIGURATION + ) + elif state == "instantiation": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.INSTANTIATION + ) + elif state == "datacollect": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.DATACOLLECT + ) + elif state == "shutdown": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.SHUTDOWN + ) + elif state == "runtime": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.RUNTIME + ) + elif state == "definition": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.DEFINITION + ) + elif state == "none": + response = self.core.set_session_state( + self.session_id, core_pb2.SessionState.NONE + ) + else: + logging.error("coregrpc.py: set_session_state: INVALID STATE") - def set_instantiate_state(self): - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.INSTANTIATION - ) logging.info("set session state: %s", response) def get_session_id(self): return self.session_id - def add_node(self, node_type, model, x, y, name): - logging.info("ADD NODE %s", name) + def add_node(self, node_type, model, x, y, name, node_id): + logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(type=node_type, position=position, model=model, image=name) + node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) return response.node_id @@ -103,10 +141,25 @@ class CoreGrpc: response = self.core.edit_node(session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + def delete_nodes(self): + for node in self.core.get_session(self.session_id).session.nodes: + response = self.core.delete_node(self.session_id, node.id) + logging.info("delete node %s", response) + # def create_interface_helper(self): # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") - # TODO case for other core_pb2.NodeType + def delete_links(self): + for link in self.core.get_session(self.session_id).session.links: + response = self.core.delete_link( + self.session_id, + link.node_one_id, + link.node_two_id, + link.interface_one.id, + link.interface_two.id, + ) + logging.info("delete link %s", response) + def add_link(self, id1, id2, type1, type2): """ Grpc client request add link diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 8af43240..c59c4222 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from core.api.grpc import core_pb2 from coretk.graph import GraphMode from coretk.images import Images from coretk.tooltip import CreateToolTip @@ -155,20 +156,19 @@ class CoreToolbar(object): self.create_runtime_toolbar() # set configuration state - self.canvas.core_grpc.set_configuration_state() + if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: + self.canvas.core_grpc.set_session_state("definition") + self.canvas.core_grpc.set_session_state("configuration") - # grpc client requests creating nodes - for node in self.canvas.grpc_manager.nodes_to_create.values(): + for node in self.canvas.grpc_manager.nodes.values(): self.canvas.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name + node.type, node.model, int(node.x), int(node.y), node.name, node.node_id ) - self.canvas.grpc_manager.nodes_to_create.clear() - for edge in self.canvas.grpc_manager.edges_to_create.values(): + for edge in self.canvas.grpc_manager.edges.values(): self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) - self.canvas.grpc_manager.edges_to_create.clear() - self.canvas.core_grpc.set_instantiate_state() + self.canvas.core_grpc.set_session_state("instantiation") def click_link_tool(self): logging.debug("Click LINK button") @@ -551,6 +551,9 @@ class CoreToolbar(object): logging.debug("Click on STOP button ") self.destroy_children_widgets(self.edit_frame) self.create_toolbar() + self.canvas.core_grpc.set_session_state("datacollect") + self.canvas.core_grpc.delete_links() + self.canvas.core_grpc.delete_nodes() def click_run_button(self): logging.debug("Click on RUN button") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5a25188b..3c984557 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,7 @@ import enum import logging import tkinter as tk +from core.api.grpc import core_pb2 from coretk.grpcmanagement import GrpcManager from coretk.images import Images @@ -33,7 +34,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.core_grpc = grpc self.grpc_manager = GrpcManager() - self.draw_existing_node() + self.draw_existing_component() def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -80,20 +81,40 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") - def draw_existing_node(self): + def draw_existing_component(self): """ - Draw existing node while updating the grpc manager based on that + Draw existing node and update the information in grpc manager to match :return: nothing """ + core_id_to_canvas_id = {} + session_id = self.core_grpc.session_id - response = self.core_grpc.core.get_session(session_id) - nodes = response.session.nodes - if len(nodes) > 0: - for node in nodes: - image = Images.convert_type_and_model_to_image(node.type, node.model) - return image - # n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + session = self.core_grpc.core.get_session(session_id).session + # nodes = response.session.nodes + # if len(nodes) > 0: + for node in session.nodes: + # peer to peer node is not drawn on the GUI + if node.type != core_pb2.NodeType.PEER_TO_PEER: + image, name = Images.convert_type_and_model_to_image( + node.type, node.model + ) + n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + self.nodes[n.id] = n + core_id_to_canvas_id[node.id] = n.id + self.grpc_manager.add_preexisting_node(n, session_id, node, name) + self.grpc_manager.update_reusable_id() + # draw existing links + # links = response.session.links + for link in session.links: + n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] + n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] + e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + self.edges[e.token] = e + self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + # lift the nodes so they on top of the links + for i in core_id_to_canvas_id.values(): + self.lift(i) def canvas_xy(self, event): """ @@ -289,8 +310,9 @@ class CanvasNode: self.edges = set() self.moving = None - def get_coords(self): - return self.x_coord, self.y_coord + # + # def get_coords(self): + # return self.x_coord, self.y_coord def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index d090f6d6..afbf8175 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -11,19 +11,20 @@ network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] class Node: - def __init__(self, session_id, node_id, type, model, x, y, name): + def __init__(self, session_id, node_id, node_type, model, x, y, name): """ Create an instance of a node :param int session_id: session id :param int node_id: node id - :param core_pb2.NodeType type: node type + :param core_pb2.NodeType node_type: node type :param int x: x coordinate :param int y: coordinate + :param str name: node name """ self.session_id = session_id self.node_id = node_id - self.type = type + self.type = node_type self.x = x self.y = y self.model = model @@ -54,8 +55,8 @@ class GrpcManager: self.id = 1 # A list of id for re-use, keep in increasing order self.reusable = [] - self.nodes_to_create = {} - self.edges_to_create = {} + + self.preexisting = [] def peek_id(self): """ @@ -82,7 +83,6 @@ class GrpcManager: else: return self.reusable.pop(0) - # TODO figure out the name of ovs node def add_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -109,13 +109,11 @@ class GrpcManager: node_type = core_pb2.NodeType.TUNNEL elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT - # TODO what is the model name for ovs node_model = name else: logging.error("grpcmanagemeny.py INVALID node name") create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) self.nodes[canvas_id] = create_node - self.nodes_to_create[canvas_id] = create_node logging.debug( "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", session_id, @@ -124,8 +122,42 @@ class GrpcManager: name, ) - def add_preexisting_node(self): - return + def add_preexisting_node(self, canvas_node, session_id, core_node, name): + """ + Add preexisting nodes to grpc manager + + :param core_pb2.Node core_node: core node grpc message + :param coretk.graph.CanvasNode canvas_node: canvas node + :param int session_id: session id + :return: nothing + """ + core_id = core_node.id + if core_id >= self.id: + self.id = core_id + 1 + self.preexisting.append(core_id) + n = Node( + session_id, + core_id, + core_node.type, + core_node.model, + canvas_node.x_coord, + canvas_node.y_coord, + name, + ) + self.nodes[canvas_node.id] = n + + def update_reusable_id(self): + """ + Update available id for reuse + + :return: nothing + """ + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) + + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) def delete_node(self, canvas_id): """ @@ -151,7 +183,6 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge - self.edges_to_create[token] = edge logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 4911efac..fc04c7cc 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -25,35 +25,36 @@ class Images: @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): """ - Retrieve image based on type and model - :param core_pb2.NodeType node_type: core node type - :param string node_model: the node model + Retrieve image based on type and model + :param core_pb2.NodeType node_type: core node type + :param string node_model: the node model - :return: the matching image - """ + :rtype: tuple(PhotoImage, str) + :return: the matching image and its name + """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get("switch") + return Images.get("switch"), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get("hub") + return Images.get("hub"), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get("wlan") + return Images.get("wlan"), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get("rj45") + return Images.get("rj45"), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get("tunnel") + return Images.get("tunnel"), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get("router") + return Images.get("router"), "router" if node_model == "host": - return Images.get(("host")) + return Images.get(("host")), "host" if node_model == "PC": - return Images.get("pc") + return Images.get("pc"), "PC" if node_model == "mdr": - return Images.get("mdr") + return Images.get("mdr"), "mdr" if node_model == "prouter": - return Images.get("prouter") + return Images.get("prouter"), "prouter" if node_model == "OVS": - return Images.get("ovs") + return Images.get("ovs"), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") From ec2e959bda3950a18be97ea4d0bfa5fc7f5c2e9c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 10 Oct 2019 17:02:28 -0700 Subject: [PATCH 029/305] move images files, create ip and mac for links, progress on node command and node terminal, progress on save and open xml --- coretk/coretk/app.py | 42 ++++- coretk/coretk/coregrpc.py | 151 +++++++++++---- coretk/coretk/coremenubar.py | 56 ++++-- coretk/coretk/coretoolbar.py | 126 +++++++------ coretk/coretk/graph.py | 177 +++++++++++++++--- coretk/coretk/grpcmanagement.py | 71 +++++++ coretk/coretk/{ => icons}/OVS.gif | Bin coretk/coretk/{ => icons}/core-icon.png | Bin .../{ => icons}/document-properties.gif | Bin coretk/coretk/{ => icons}/host.gif | Bin coretk/coretk/{ => icons}/hub.gif | Bin coretk/coretk/{ => icons}/lanswitch.gif | Bin coretk/coretk/{ => icons}/link.gif | Bin coretk/coretk/{ => icons}/marker.gif | Bin coretk/coretk/{ => icons}/mdr.gif | Bin coretk/coretk/{ => icons}/observe.gif | Bin coretk/coretk/{ => icons}/oval.gif | Bin coretk/coretk/{ => icons}/pc.gif | Bin coretk/coretk/{ => icons}/plot.gif | Bin coretk/coretk/{ => icons}/rectangle.gif | Bin coretk/coretk/{ => icons}/rj45.gif | Bin coretk/coretk/{ => icons}/router.gif | Bin coretk/coretk/{ => icons}/router_green.gif | Bin coretk/coretk/{ => icons}/run.gif | Bin coretk/coretk/{ => icons}/select.gif | Bin coretk/coretk/{ => icons}/start.gif | Bin coretk/coretk/{ => icons}/stop.gif | Bin coretk/coretk/{ => icons}/text.gif | Bin coretk/coretk/{ => icons}/tunnel.gif | Bin coretk/coretk/{ => icons}/twonode.gif | Bin coretk/coretk/{ => icons}/wlan.gif | Bin coretk/coretk/images.py | 83 ++++---- coretk/coretk/interface.py | 43 +++++ coretk/coretk/menuaction.py | 129 ++++++++++--- coretk/coretk/oldimage/OVS.gif | Bin 0 -> 744 bytes coretk/coretk/oldimage/core-icon.png | Bin 0 -> 2931 bytes .../coretk/oldimage/document-properties.gif | Bin 0 -> 635 bytes coretk/coretk/oldimage/host.gif | Bin 0 -> 1189 bytes coretk/coretk/oldimage/hub.gif | Bin 0 -> 719 bytes coretk/coretk/oldimage/lanswitch.gif | Bin 0 -> 744 bytes coretk/coretk/oldimage/link.gif | Bin 0 -> 86 bytes coretk/coretk/oldimage/marker.gif | Bin 0 -> 375 bytes coretk/coretk/oldimage/mdr.gif | Bin 0 -> 1276 bytes coretk/coretk/oldimage/observe.gif | Bin 0 -> 1149 bytes coretk/coretk/oldimage/oval.gif | Bin 0 -> 174 bytes coretk/coretk/oldimage/pc.gif | Bin 0 -> 1300 bytes coretk/coretk/oldimage/plot.gif | Bin 0 -> 265 bytes coretk/coretk/oldimage/rectangle.gif | Bin 0 -> 160 bytes coretk/coretk/oldimage/rj45.gif | Bin 0 -> 755 bytes coretk/coretk/oldimage/router.gif | Bin 0 -> 1152 bytes coretk/coretk/oldimage/router_green.gif | Bin 0 -> 753 bytes coretk/coretk/oldimage/run.gif | Bin 0 -> 324 bytes coretk/coretk/oldimage/select.gif | Bin 0 -> 925 bytes coretk/coretk/oldimage/start.gif | Bin 0 -> 1131 bytes coretk/coretk/oldimage/stop.gif | Bin 0 -> 1204 bytes coretk/coretk/oldimage/text.gif | Bin 0 -> 127 bytes coretk/coretk/oldimage/tunnel.gif | Bin 0 -> 799 bytes coretk/coretk/oldimage/twonode.gif | Bin 0 -> 220 bytes coretk/coretk/oldimage/wlan.gif | Bin 0 -> 146 bytes coretk/coretk/prev_saved_xml.txt | 5 + output.txt | 23 +++ 61 files changed, 703 insertions(+), 203 deletions(-) rename coretk/coretk/{ => icons}/OVS.gif (100%) rename coretk/coretk/{ => icons}/core-icon.png (100%) rename coretk/coretk/{ => icons}/document-properties.gif (100%) rename coretk/coretk/{ => icons}/host.gif (100%) rename coretk/coretk/{ => icons}/hub.gif (100%) rename coretk/coretk/{ => icons}/lanswitch.gif (100%) rename coretk/coretk/{ => icons}/link.gif (100%) rename coretk/coretk/{ => icons}/marker.gif (100%) rename coretk/coretk/{ => icons}/mdr.gif (100%) rename coretk/coretk/{ => icons}/observe.gif (100%) rename coretk/coretk/{ => icons}/oval.gif (100%) rename coretk/coretk/{ => icons}/pc.gif (100%) rename coretk/coretk/{ => icons}/plot.gif (100%) rename coretk/coretk/{ => icons}/rectangle.gif (100%) rename coretk/coretk/{ => icons}/rj45.gif (100%) rename coretk/coretk/{ => icons}/router.gif (100%) rename coretk/coretk/{ => icons}/router_green.gif (100%) rename coretk/coretk/{ => icons}/run.gif (100%) rename coretk/coretk/{ => icons}/select.gif (100%) rename coretk/coretk/{ => icons}/start.gif (100%) rename coretk/coretk/{ => icons}/stop.gif (100%) rename coretk/coretk/{ => icons}/text.gif (100%) rename coretk/coretk/{ => icons}/tunnel.gif (100%) rename coretk/coretk/{ => icons}/twonode.gif (100%) rename coretk/coretk/{ => icons}/wlan.gif (100%) create mode 100644 coretk/coretk/interface.py create mode 100755 coretk/coretk/oldimage/OVS.gif create mode 100644 coretk/coretk/oldimage/core-icon.png create mode 100644 coretk/coretk/oldimage/document-properties.gif create mode 100644 coretk/coretk/oldimage/host.gif create mode 100644 coretk/coretk/oldimage/hub.gif create mode 100644 coretk/coretk/oldimage/lanswitch.gif create mode 100644 coretk/coretk/oldimage/link.gif create mode 100644 coretk/coretk/oldimage/marker.gif create mode 100644 coretk/coretk/oldimage/mdr.gif create mode 100644 coretk/coretk/oldimage/observe.gif create mode 100644 coretk/coretk/oldimage/oval.gif create mode 100644 coretk/coretk/oldimage/pc.gif create mode 100644 coretk/coretk/oldimage/plot.gif create mode 100644 coretk/coretk/oldimage/rectangle.gif create mode 100644 coretk/coretk/oldimage/rj45.gif create mode 100644 coretk/coretk/oldimage/router.gif create mode 100644 coretk/coretk/oldimage/router_green.gif create mode 100644 coretk/coretk/oldimage/run.gif create mode 100644 coretk/coretk/oldimage/select.gif create mode 100644 coretk/coretk/oldimage/start.gif create mode 100644 coretk/coretk/oldimage/stop.gif create mode 100644 coretk/coretk/oldimage/text.gif create mode 100644 coretk/coretk/oldimage/tunnel.gif create mode 100644 coretk/coretk/oldimage/twonode.gif create mode 100644 coretk/coretk/oldimage/wlan.gif create mode 100644 coretk/coretk/prev_saved_xml.txt create mode 100644 output.txt diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9a9506e2..6328b3bc 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -6,7 +6,8 @@ from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph -from coretk.images import Images +from coretk.images import ImageEnum, Images +from coretk.menuaction import MenuAction class Application(tk.Frame): @@ -15,13 +16,15 @@ class Application(tk.Frame): self.load_images() self.setup_app() self.menubar = None + self.core_menu = None self.canvas = None - - # start grpc - self.core_grpc = CoreGrpc() + self.core_editbar = None + self.core_grpc = None self.create_menu() self.create_widgets() + self.draw_canvas() + self.start_grpc() def load_images(self): """ @@ -33,23 +36,24 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") - image = Images.get("core") + image = Images.get(ImageEnum.CORE.value) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) def create_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) - core_menu = CoreMenubar(self, self.master, self.menubar) - core_menu.create_core_menubar() + self.core_menu = CoreMenubar(self, self.master, self.menubar) + self.core_menu.create_core_menubar() 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) - core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) - core_editbar.create_toolbar() + self.core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) + self.core_editbar.create_toolbar() + def draw_canvas(self): self.canvas = CanvasGraph( master=self, grpc=self.core_grpc, @@ -58,7 +62,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - core_editbar.update_canvas(self.canvas) + self.core_editbar.update_canvas(self.canvas) scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview @@ -78,8 +82,26 @@ class Application(tk.Frame): b = tk.Button(status_bar, text="Button 3") b.pack(side=tk.LEFT, padx=1) + def start_grpc(self): + """ + Conect client to grpc, query sessions and prompt use to choose an existing session if there exist any + + :return: nothing + """ + self.master.update() + self.core_grpc = CoreGrpc(self.master) + self.core_grpc.set_up() + self.canvas.core_grpc = self.core_grpc + self.canvas.draw_existing_component() + + def on_closing(self): + menu_action = MenuAction(self, self.master) + menu_action.on_quit() + # self.quit() + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() + app.master.protocol("WM_DELETE_WINDOW", app.on_closing) app.mainloop() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index bcd04c96..abcad792 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -2,26 +2,28 @@ Incorporate grpc into python tkinter GUI """ import logging +import os +import tkinter as tk from core.api.grpc import client, core_pb2 class CoreGrpc: - def __init__(self): + def __init__(self, master): """ Create a CoreGrpc instance """ + print("Create grpc instance") self.core = client.CoreGrpcClient() self.session_id = None - self.set_up() + self.master = master + + # self.set_up() self.interface_helper = None def log_event(self, event): logging.info("event: %s", event) - def redraw_canvas(self): - return - def create_new_session(self): """ Create a new session @@ -35,6 +37,29 @@ class CoreGrpc: self.session_id = response.session_id self.core.events(self.session_id, self.log_event) + def _enter_session(self, session_id, dialog): + """ + enter an existing session + + :return: + """ + dialog.destroy() + response = self.core.get_session(session_id) + self.session_id = session_id + print("set session id: %s", session_id) + logging.info("Entering session_id %s.... Result: %s", session_id, response) + # self.master.canvas.draw_existing_component() + + def _create_session(self, dialog): + """ + create a new session + + :param tkinter.Toplevel dialog: save core session prompt dialog + :return: nothing + """ + dialog.destroy() + self.create_new_session() + def query_existing_sessions(self, sessions): """ Query for existing sessions and prompt to join one @@ -43,17 +68,30 @@ class CoreGrpc: :return: nothing """ + dialog = tk.Toplevel() + dialog.title("CORE sessions") for session in sessions: - logging.info("Session id: %s, Session state: %s", session.id, session.state) - logging.info("Input a session you want to enter from the keyboard:") - usr_input = int(input()) - if usr_input == 0: - self.create_new_session() - else: - response = self.core.get_session(usr_input) - self.session_id = usr_input - # self.core.events(self.session_id, self.log_event) - logging.info("Entering session_id %s.... Result: %s", usr_input, response) + b = tk.Button( + dialog, + text="Session " + str(session.id), + command=lambda sid=session.id: self._enter_session(sid, dialog), + ) + b.pack(side=tk.TOP) + b = tk.Button( + dialog, text="create new", command=lambda: self._create_session(dialog) + ) + b.pack(side=tk.TOP) + dialog.update() + x = ( + self.master.winfo_x() + + (self.master.winfo_width() - dialog.winfo_width()) / 2 + ) + y = ( + self.master.winfo_y() + + (self.master.winfo_height() / 2 - dialog.winfo_height()) / 2 + ) + dialog.geometry(f"+{int(x)}+{int(y)}") + dialog.wait_window() def delete_session(self): response = self.core.delete_session(self.session_id) @@ -61,9 +99,9 @@ class CoreGrpc: def set_up(self): """ - Query sessions, if there exist any, promt whether to join one + Query sessions, if there exist any, prompt whether to join one - :return: nothing + :return: existing sessions """ self.core.connect() @@ -76,12 +114,12 @@ class CoreGrpc: if len(sessions) == 0: self.create_new_session() else: - # self.create_new_session() + self.query_existing_sessions(sessions) def get_session_state(self): response = self.core.get_session(self.session_id) - logging.info("get sessio: %s", response) + logging.info("get session: %s", response) return response.session.state def set_session_state(self, state): @@ -125,9 +163,6 @@ class CoreGrpc: logging.info("set session state: %s", response) - def get_session_id(self): - return self.session_id - def add_node(self, node_type, model, x, y, name, node_id): logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) @@ -136,9 +171,9 @@ class CoreGrpc: logging.info("created node: %s", response) return response.node_id - def edit_node(self, session_id, node_id, x, y): + def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(session_id, node_id, position) + response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) def delete_nodes(self): @@ -146,9 +181,6 @@ class CoreGrpc: response = self.core.delete_node(self.session_id, node.id) logging.info("delete node %s", response) - # def create_interface_helper(self): - # self.interface_helper = self.core.InterfaceHelper(ip4_prefix="10.83.0.0/16") - def delete_links(self): for link in self.core.get_session(self.session_id).session.links: response = self.core.delete_link( @@ -160,7 +192,7 @@ class CoreGrpc: ) logging.info("delete link %s", response) - def add_link(self, id1, id2, type1, type2): + def add_link(self, id1, id2, type1, type2, edge): """ Grpc client request add link @@ -171,19 +203,64 @@ class CoreGrpc: :param core_pb2.NodeType type2: node 2 core node type :return: nothing """ - if not self.interface_helper: - logging.debug("INTERFACE HELPER NOT CREATED YET, CREATING ONE...") - self.interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - - interface1 = None - interface2 = None + if1 = None + if2 = None if type1 == core_pb2.NodeType.DEFAULT: - interface1 = self.interface_helper.create_interface(id1, 0) + interface = edge.interface_1 + if1 = core_pb2.Interface( + id=interface.id, + name=interface.name, + mac=interface.mac, + ip4=interface.ipv4, + ip4mask=interface.ip4prefix, + ) + # if1 = core_pb2.Interface(id=id1, name=edge.interface_1.name, ip4=edge.interface_1.ipv4, ip4mask=edge.interface_1.ip4prefix) + logging.debug("create interface 1 %s", if1) + # interface1 = self.interface_helper.create_interface(id1, 0) + if type2 == core_pb2.NodeType.DEFAULT: - interface2 = self.interface_helper.create_interface(id2, 0) - response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + interface = edge.interface_2 + if2 = core_pb2.Interface( + id=interface.id, + name=interface.name, + mac=interface.mac, + ip4=interface.ipv4, + ip4mask=interface.ip4prefix, + ) + # if2 = core_pb2.Interface(id=id2, name=edge.interface_2.name, ip4=edge.interface_2.ipv4, ip4mask=edge.interface_2.ip4prefix) + logging.debug("create interface 2: %s", if2) + # interface2 = self.interface_helper.create_interface(id2, 0) + + # response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) + response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + def launch_terminal(self, node_id): + response = self.core.get_node_terminal(self.session_id, node_id) + logging.info("get terminal %s", response.terminal) + os.system("xterm -e %s &" % response.terminal) + + def save_xml(self, file_path): + """ + Save core session as to an xml file + + :param str file_path: file path that user pick + :return: nothing + """ + response = self.core.save_xml(self.session_id, file_path) + logging.info("coregrpc.py save xml %s", response) + + def open_xml(self, file_path): + """ + Open core xml + + :param str file_path: file to open + :return: session id + """ + response = self.core.open_xml(file_path) + return response.session_id + # logging.info("coregrpc.py open_xml()", type(response)) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index f4ba51f5..f08d18ed 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -1,6 +1,7 @@ import tkinter as tk import coretk.menuaction as action +from coretk.menuaction import MenuAction class CoreMenubar(object): @@ -19,6 +20,33 @@ class CoreMenubar(object): self.menubar = menubar self.master = master self.application = application + self.menuaction = action.MenuAction(application, master) + self.menu_action = MenuAction(self.application, self.master) + + # def on_quit(self): + # """ + # Prompt use to stop running session before application is closed + # + # :return: nothing + # """ + # state = self.application.core_grpc.get_session_state() + # + # if state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION: + # self.application.core_grpc.delete_session() + # self.application.core_grpc.core.close() + # # self.application.quit() + # else: + # msgbox = tk.messagebox.askyesnocancel("stop", "Stop the running session?") + # + # if msgbox or msgbox == False: + # if msgbox: + # self.application.core_grpc.set_session_state("datacollect") + # self.application.core_grpc.delete_links() + # self.application.core_grpc.delete_nodes() + # self.application.core_grpc.delete_session() + # + # self.application.core_grpc.core.close() + # # self.application.quit() def create_file_menu(self): """ @@ -27,18 +55,24 @@ class CoreMenubar(object): :return: nothing """ file_menu = tk.Menu(self.menubar) + # menu_action = MenuAction(self.application, self.master) file_menu.add_command( label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 ) file_menu.add_command( - label="Open...", command=action.file_open, accelerator="Ctrl+O", underline=0 + label="Open...", + command=self.menu_action.file_open_xml, + accelerator="Ctrl+O", + underline=0, ) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) file_menu.add_command( label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 ) - file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save As imn...", command=action.file_save_as_imn) + # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) + file_menu.add_command( + label="Save As XML...", command=self.menu_action.file_save_as_xml + ) file_menu.add_separator() @@ -68,13 +102,8 @@ class CoreMenubar(object): file_menu.add_separator() file_menu.add_command( - label="/home/ncs/.core/configs/sample1.imn", - command=action.file_example_link, + label="Quit", command=self.menuaction.on_quit, underline=0 ) - - file_menu.add_separator() - - file_menu.add_command(label="Quit", command=self.master.quit, underline=0) self.menubar.add_cascade(label="File", menu=file_menu, underline=0) def create_edit_menu(self): @@ -560,9 +589,7 @@ class CoreMenubar(object): :return: nothing """ session_menu = tk.Menu(self.menubar) - session_menu.add_command( - label="Start", command=action.session_start, underline=0 - ) + session_menu.add_command( label="Change sessions...", command=action.session_change_sessions, @@ -604,10 +631,11 @@ class CoreMenubar(object): """ help_menu = tk.Menu(self.menubar) help_menu.add_command( - label="Core Github (www)", command=action.help_core_github + label="Core Github (www)", command=self.menu_action.help_core_github ) help_menu.add_command( - label="Core Documentation (www)", command=action.help_core_documentation + label="Core Documentation (www)", + command=self.menu_action.help_core_documentation, ) help_menu.add_command(label="About", command=action.help_about) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index c59c4222..6911ebaf 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,7 +3,7 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.graph import GraphMode -from coretk.images import Images +from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip @@ -153,7 +153,6 @@ class CoreToolbar(object): logging.debug("Click START STOP SESSION button") self.destroy_children_widgets(self.edit_frame) self.canvas.mode = GraphMode.SELECT - self.create_runtime_toolbar() # set configuration state if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: @@ -166,10 +165,14 @@ class CoreToolbar(object): ) for edge in self.canvas.grpc_manager.edges.values(): - self.canvas.core_grpc.add_link(edge.id1, edge.id2, edge.type1, edge.type2) + self.canvas.core_grpc.add_link( + edge.id1, edge.id2, edge.type1, edge.type2, edge + ) self.canvas.core_grpc.set_session_state("instantiation") + self.create_runtime_toolbar() + def click_link_tool(self): logging.debug("Click LINK button") self.canvas.mode = GraphMode.EDGE @@ -177,55 +180,55 @@ class CoreToolbar(object): def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("router")) + main_button.configure(image=Images.get(ImageEnum.ROUTER.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("router") + self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER.value) self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("host")) + main_button.configure(image=Images.get(ImageEnum.HOST.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("host") + self.canvas.draw_node_image = Images.get(ImageEnum.HOST.value) self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("pc")) + main_button.configure(image=Images.get(ImageEnum.PC.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("pc") + self.canvas.draw_node_image = Images.get(ImageEnum.PC.value) self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("mdr")) + main_button.configure(image=Images.get(ImageEnum.MDR.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("mdr") + self.canvas.draw_node_image = Images.get(ImageEnum.MD.value) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("prouter")) + main_button.configure(image=Images.get(ImageEnum.PROUTER.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("prouter") + self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER.value) self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("ovs")) + main_button.configure(image=Images.get(ImageEnum.OVS.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("ovs") + self.canvas.draw_node_image = Images.get(ImageEnum.OVS.value) self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get("editnode")) + main_button.configure(image=Images.get(ImageEnum.EDITNODE.value)) logging.debug("Pick editnode option") def draw_network_layer_options(self, network_layer_button): @@ -239,13 +242,13 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("router"), - Images.get("host"), - Images.get("pc"), - Images.get("mdr"), - Images.get("prouter"), - Images.get("ovs"), - Images.get("editnode"), + Images.get(ImageEnum.ROUTER.value), + Images.get(ImageEnum.HOST.value), + Images.get(ImageEnum.PC.value), + Images.get(ImageEnum.MDR.value), + Images.get(ImageEnum.PROUTER.value), + Images.get(ImageEnum.OVS.value), + Images.get(ImageEnum.EDITNODE.value), ] func_list = [ self.pick_router, @@ -289,7 +292,7 @@ class CoreToolbar(object): :return: nothing """ - router_image = Images.get("router") + router_image = Images.get(ImageEnum.ROUTER.value) network_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -306,41 +309,41 @@ class CoreToolbar(object): def pick_hub(self, main_button): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("hub")) + main_button.configure(image=Images.get(ImageEnum.HUB.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("hub") + self.canvas.draw_node_image = Images.get(ImageEnum.HUB.value) self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("switch")) + main_button.configure(image=Images.get(ImageEnum.SWITCH.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("switch") + self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH.value) self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("wlan")) + main_button.configure(image=Images.get(ImageEnum.WLAN.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("wlan") + self.canvas.draw_node_image = Images.get(ImageEnum.WLAN.value) self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("rj45")) + main_button.configure(image=Images.get(ImageEnum.RJ45.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("rj45") + self.canvas.draw_node_image = Images.get(ImageEnum.RJ45.value) self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get("tunnel")) + main_button.configure(image=Images.get(ImageEnum.TUNNEL.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get("tunnel") + self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): @@ -354,11 +357,11 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("hub"), - Images.get("switch"), - Images.get("wlan"), - Images.get("rj45"), - Images.get("tunnel"), + Images.get(ImageEnum.HUB.value), + Images.get(ImageEnum.SWITCH.value), + Images.get(ImageEnum.WLAN.value), + Images.get(ImageEnum.RJ45.value), + Images.get(ImageEnum.TUNNEL.value), ] func_list = [ self.pick_hub, @@ -398,7 +401,7 @@ class CoreToolbar(object): :return: nothing """ - hub_image = Images.get("hub") + hub_image = Images.get(ImageEnum.HUB.value) link_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -414,22 +417,22 @@ class CoreToolbar(object): def pick_marker(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("marker")) + main_button.configure(image=Images.get(ImageEnum.MARKER.value)) logging.debug("Pick MARKER") def pick_oval(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("oval")) + main_button.configure(image=Images.get(ImageEnum.OVAL.value)) logging.debug("Pick OVAL") def pick_rectangle(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("rectangle")) + main_button.configure(image=Images.get(ImageEnum.RECTANGLE.value)) logging.debug("Pick RECTANGLE") def pick_text(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get("text")) + main_button.configure(image=Images.get(ImageEnum.TEXT.value)) logging.debug("Pick TEXT") def draw_marker_options(self, main_button): @@ -443,10 +446,10 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get("marker"), - Images.get("oval"), - Images.get("rectangle"), - Images.get("text"), + Images.get(ImageEnum.MARKER.value), + Images.get(ImageEnum.OVAL.value), + Images.get(ImageEnum.RECTANGLE.value), + Images.get(ImageEnum.TEXT.value), ] func_list = [ self.pick_marker, @@ -475,7 +478,7 @@ class CoreToolbar(object): :return: nothing """ - marker_image = Images.get("marker") + marker_image = Images.get(ImageEnum.MARKER.value) marker_main_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -492,13 +495,13 @@ class CoreToolbar(object): def create_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get("start"), + Images.get(ImageEnum.START.value), self.click_start_session_tool, "start the session", ) self.create_radio_button( self.edit_frame, - Images.get("select"), + Images.get(ImageEnum.SELECT.value), self.click_selection_tool, self.radio_value, 1, @@ -506,7 +509,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("link"), + Images.get(ImageEnum.LINK.value), self.click_link_tool, self.radio_value, 2, @@ -520,7 +523,7 @@ class CoreToolbar(object): def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, - image=Images.get("observe"), + image=Images.get(ImageEnum.OBSERVE.value), width=self.width, height=self.height, direction=tk.RIGHT, @@ -550,10 +553,10 @@ class CoreToolbar(object): def click_stop_button(self): logging.debug("Click on STOP button ") self.destroy_children_widgets(self.edit_frame) - self.create_toolbar() self.canvas.core_grpc.set_session_state("datacollect") self.canvas.core_grpc.delete_links() self.canvas.core_grpc.delete_nodes() + self.create_toolbar() def click_run_button(self): logging.debug("Click on RUN button") @@ -570,13 +573,13 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get("stop"), + Images.get(ImageEnum.STOP.value), self.click_stop_button, "stop the session", ) self.create_radio_button( self.edit_frame, - Images.get("select"), + Images.get(ImageEnum.SELECT.value), self.click_selection_tool, self.exec_radio_value, 1, @@ -585,7 +588,7 @@ class CoreToolbar(object): self.create_observe_button() self.create_radio_button( self.edit_frame, - Images.get("plot"), + Images.get(ImageEnum.PLOT.value), self.click_plot_button, self.exec_radio_value, 2, @@ -593,7 +596,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("marker"), + Images.get(ImageEnum.MARKER.value), self.click_marker_button, self.exec_radio_value, 3, @@ -601,13 +604,16 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get("twonode"), + Images.get(ImageEnum.TWONODE.value), self.click_two_node_button, self.exec_radio_value, 4, "run command from one node to another", ) self.create_regular_button( - self.edit_frame, Images.get("run"), self.click_run_button, "run" + self.edit_frame, + Images.get(ImageEnum.RUN.value), + self.click_run_button, + "run", ) self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3c984557..7e69b224 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,10 +1,12 @@ import enum import logging +import math import tkinter as tk from core.api.grpc import core_pb2 from coretk.grpcmanagement import GrpcManager from coretk.images import Images +from coretk.interface import Interface class GraphMode(enum.Enum): @@ -34,7 +36,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.core_grpc = grpc self.grpc_manager = GrpcManager() - self.draw_existing_component() + # self.draw_existing_component() def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -52,9 +54,6 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) - # self.bind("e", self.set_mode) - # self.bind("s", self.set_mode) - # self.bind("n", self.set_mode) def draw_grid(self, width=1000, height=750): """ @@ -91,8 +90,7 @@ class CanvasGraph(tk.Canvas): session_id = self.core_grpc.session_id session = self.core_grpc.core.get_session(session_id).session - # nodes = response.session.nodes - # if len(nodes) > 0: + # redraw existing nodes for node in session.nodes: # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: @@ -104,18 +102,62 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id self.grpc_manager.add_preexisting_node(n, session_id, node, name) self.grpc_manager.update_reusable_id() + # draw existing links - # links = response.session.links for link in session.links: n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + n1.edges.add(e) + n2.edges.add(e) self.edges[e.token] = e self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + + # TODO add back the link info to grpc manager also redraw + grpc_if1 = link.interface_one + grpc_if2 = link.interface_two + ip4_src = None + ip4_dst = None + ip6_src = None + ip6_dst = None + if grpc_if1 is not None: + ip4_src = grpc_if1.ip4 + ip6_src = grpc_if1.ip6 + if grpc_if2 is not None: + ip4_dst = grpc_if2.ip4 + ip6_dst = grpc_if2.ip6 + e.link_info = LinkInfo( + canvas=self, + edge_id=e.id, + ip4_src=ip4_src, + ip6_src=ip6_src, + ip4_dst=ip4_dst, + ip6_dst=ip6_dst, + throughput=None, + ) + + # TODO will include throughput and ipv6 in the future + if1 = Interface(grpc_if1.name, grpc_if1.ip4) + if2 = Interface(grpc_if2.name, grpc_if2.ip4) + self.grpc_manager.edges[e.token].interface_1 = if1 + self.grpc_manager.edges[e.token].interface_2 = if2 + self.grpc_manager.nodes[ + core_id_to_canvas_id[link.node_one_id] + ].interfaces.append(if1) + self.grpc_manager.nodes[ + core_id_to_canvas_id[link.node_two_id] + ].interfaces.append(if2) + # lift the nodes so they on top of the links for i in core_id_to_canvas_id.values(): self.lift(i) + def delete_components(self): + tags = ["node", "edge", "linkinfo", "nodename"] + for i in tags: + for id in self.find_withtag(i): + self.delete(id) + def canvas_xy(self, event): """ Convert window coordinate to canvas coordinate @@ -171,6 +213,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE def handle_edge_release(self, event): + print("Calling edge release") edge = self.drawing_edge self.drawing_edge = None @@ -204,7 +247,20 @@ class CanvasGraph(tk.Canvas): node_dst.edges.add(edge) self.grpc_manager.add_edge( - self.core_grpc.get_session_id(), edge.token, node_src.id, node_dst.id + self.core_grpc.session_id, edge.token, node_src.id, node_dst.id + ) + + # draw link info on the edge + if1 = self.grpc_manager.edges[edge.token].interface_1 + if2 = self.grpc_manager.edges[edge.token].interface_2 + edge.link_info = LinkInfo( + self, + edge.id, + ip4_src=if1.ip4_and_prefix, + ip6_src=None, + ip4_dst=if2.ip4_and_prefix, + ip6_dst=None, + throughput=None, ) logging.debug(f"edges: {self.find_withtag('edge')}") @@ -216,6 +272,7 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + print("click on the canvas") logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -243,14 +300,15 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def add_node(self, x, y, image, node_name): - node = CanvasNode( - x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() - ) - self.nodes[node.id] = node - self.grpc_manager.add_node( - self.core_grpc.get_session_id(), node.id, x, y, node_name - ) - return node + if self.selected == 1: + node = CanvasNode( + x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() + ) + self.nodes[node.id] = node + self.grpc_manager.add_node( + self.core_grpc.session_id, node.id, x, y, node_name + ) + return node class CanvasEdge: @@ -258,7 +316,7 @@ class CanvasEdge: Canvas edge class """ - width = 3 + width = 1.3 def __init__(self, x1, y1, x2, y2, src, canvas): """ @@ -273,9 +331,13 @@ class CanvasEdge: 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.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) self.token = None + # link info object + self.link_info = None # TODO resolve this # self.canvas.tag_lower(self.id) @@ -301,29 +363,40 @@ class CanvasNode: self.core_id = core_id self.x_coord = x self.y_coord = y - self.name = f"Node {self.core_id}" - self.text_id = self.canvas.create_text(x, y + 20, text=self.name) + self.name = f"N{self.core_id}" + self.text_id = self.canvas.create_text( + x, y + 20, text=self.name, tags="nodename" + ) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) + self.canvas.tag_bind(self.id, "", self.double_click) + self.edges = set() self.moving = None - # - # def get_coords(self): - # return self.x_coord, self.y_coord + def double_click(self, event): + node_id = self.canvas.grpc_manager.nodes[self.id].node_id + state = self.canvas.core_grpc.get_session_state() + if state == core_pb2.SessionState.RUNTIME: + self.canvas.core_grpc.launch_terminal(node_id) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): + print("click on the node") logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) + # return "break" def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() + self.canvas.grpc_manager.update_node_location( + self.id, self.x_coord, self.y_coord + ) self.moving = None def motion(self, event): @@ -344,6 +417,64 @@ class CanvasNode: self.canvas.coords(edge.id, new_x, new_y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) + edge.link_info.recalculate_info() def context(self, event): logging.debug(f"context click {self.name}: {event}") + + +class LinkInfo: + def __init__(self, canvas, edge_id, ip4_src, ip6_src, ip4_dst, ip6_dst, throughput): + self.canvas = canvas + self.edge_id = edge_id + self.radius = 37 + + self.ip4_address_1 = ip4_src + self.ip6_address_1 = ip6_src + self.ip4_address_2 = ip4_dst + self.ip6_address_2 = ip6_dst + self.throughput = throughput + self.id1 = self.create_edge_src_info() + self.id2 = self.create_edge_dst_info() + + def slope_src_dst(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge_id) + return (y2 - y1) / (x2 - x1) + + def create_edge_src_info(self): + x1, y1, x2, _ = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) + print(self.ip4_address_1) + id1 = self.canvas.create_text( + x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + ) + return id1 + + def create_edge_dst_info(self): + x1, _, x2, y2 = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) + id2 = self.canvas.create_text( + x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + ) + return id2 + + def recalculate_info(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge_id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + new_x1 = x1 + distance + new_y1 = y1 + distance * m + new_x2 = x2 - distance + new_y2 = y2 - distance * m + self.canvas.coords(self.id1, new_x1, new_y1) + self.canvas.coords(self.id2, new_x2, new_y2) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index afbf8175..ea1ee947 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -5,6 +5,7 @@ that can be useful for grpc, acts like a session class import logging from core.api.grpc import core_pb2 +from coretk.interface import Interface, InterfaceManager link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -29,6 +30,7 @@ class Node: self.y = y self.model = model self.name = name + self.interfaces = [] class Edge: @@ -46,6 +48,8 @@ class Edge: self.id2 = node_id_2 self.type1 = node_type_1 self.type2 = node_type_2 + self.interface_1 = None + self.interface_2 = None class GrpcManager: @@ -57,6 +61,7 @@ class GrpcManager: self.reusable = [] self.preexisting = [] + self.interfaces_manager = InterfaceManager() def peek_id(self): """ @@ -146,6 +151,18 @@ class GrpcManager: ) self.nodes[canvas_node.id] = n + def update_node_location(self, canvas_id, new_x, new_y): + """ + update node + + :param int canvas_id: canvas id of that node + :param int new_x: new x coord + :param int new_y: new y coord + :return: nothing + """ + self.nodes[canvas_id].x = new_x + self.nodes[canvas_id].y = new_y + def update_reusable_id(self): """ Update available id for reuse @@ -173,7 +190,60 @@ class GrpcManager: except KeyError: logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + def create_interface(self, edge, src_canvas_id, dst_canvas_id): + """ + Create the interface for the two end of an edge, add a copy to node's interfaces + + :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param int src_canvas_id: canvas id for the source node + :param int dst_canvas_id: canvas id for the destination node + :return: nothing + """ + src_interface = None + dst_interface = None + + src_node = self.nodes[src_canvas_id] + if src_node.model in network_layer_nodes: + ifid = len(src_node.interfaces) + name = "eth" + str(ifid) + src_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[src_canvas_id].interfaces.append(src_interface) + logging.debug( + "Create source interface 1... IP: %s, name: %s", + src_interface.ipv4, + src_interface.name, + ) + + dst_node = self.nodes[dst_canvas_id] + if dst_node.model in network_layer_nodes: + ifid = len(dst_node.interfaces) + name = "veth" + str(ifid) + dst_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[dst_canvas_id].interfaces.append(dst_interface) + logging.debug( + "Create destination interface... IP: %s, name: %s", + dst_interface.ipv4, + dst_interface.name, + ) + + edge.interface_1 = src_interface + edge.interface_2 = dst_interface + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + """ + Add an edge to grpc manager + + :param int session_id: core session id + :param tuple(int, int) token: edge's identification in the canvas + :param int canvas_id_1: canvas id of source node + :param int canvas_id_2: canvas_id of destination node + + :return: nothing + """ if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: edge = Edge( session_id, @@ -183,6 +253,7 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge + self.create_interface(edge, canvas_id_1, canvas_id_2) logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/OVS.gif b/coretk/coretk/icons/OVS.gif similarity index 100% rename from coretk/coretk/OVS.gif rename to coretk/coretk/icons/OVS.gif diff --git a/coretk/coretk/core-icon.png b/coretk/coretk/icons/core-icon.png similarity index 100% rename from coretk/coretk/core-icon.png rename to coretk/coretk/icons/core-icon.png diff --git a/coretk/coretk/document-properties.gif b/coretk/coretk/icons/document-properties.gif similarity index 100% rename from coretk/coretk/document-properties.gif rename to coretk/coretk/icons/document-properties.gif diff --git a/coretk/coretk/host.gif b/coretk/coretk/icons/host.gif similarity index 100% rename from coretk/coretk/host.gif rename to coretk/coretk/icons/host.gif diff --git a/coretk/coretk/hub.gif b/coretk/coretk/icons/hub.gif similarity index 100% rename from coretk/coretk/hub.gif rename to coretk/coretk/icons/hub.gif diff --git a/coretk/coretk/lanswitch.gif b/coretk/coretk/icons/lanswitch.gif similarity index 100% rename from coretk/coretk/lanswitch.gif rename to coretk/coretk/icons/lanswitch.gif diff --git a/coretk/coretk/link.gif b/coretk/coretk/icons/link.gif similarity index 100% rename from coretk/coretk/link.gif rename to coretk/coretk/icons/link.gif diff --git a/coretk/coretk/marker.gif b/coretk/coretk/icons/marker.gif similarity index 100% rename from coretk/coretk/marker.gif rename to coretk/coretk/icons/marker.gif diff --git a/coretk/coretk/mdr.gif b/coretk/coretk/icons/mdr.gif similarity index 100% rename from coretk/coretk/mdr.gif rename to coretk/coretk/icons/mdr.gif diff --git a/coretk/coretk/observe.gif b/coretk/coretk/icons/observe.gif similarity index 100% rename from coretk/coretk/observe.gif rename to coretk/coretk/icons/observe.gif diff --git a/coretk/coretk/oval.gif b/coretk/coretk/icons/oval.gif similarity index 100% rename from coretk/coretk/oval.gif rename to coretk/coretk/icons/oval.gif diff --git a/coretk/coretk/pc.gif b/coretk/coretk/icons/pc.gif similarity index 100% rename from coretk/coretk/pc.gif rename to coretk/coretk/icons/pc.gif diff --git a/coretk/coretk/plot.gif b/coretk/coretk/icons/plot.gif similarity index 100% rename from coretk/coretk/plot.gif rename to coretk/coretk/icons/plot.gif diff --git a/coretk/coretk/rectangle.gif b/coretk/coretk/icons/rectangle.gif similarity index 100% rename from coretk/coretk/rectangle.gif rename to coretk/coretk/icons/rectangle.gif diff --git a/coretk/coretk/rj45.gif b/coretk/coretk/icons/rj45.gif similarity index 100% rename from coretk/coretk/rj45.gif rename to coretk/coretk/icons/rj45.gif diff --git a/coretk/coretk/router.gif b/coretk/coretk/icons/router.gif similarity index 100% rename from coretk/coretk/router.gif rename to coretk/coretk/icons/router.gif diff --git a/coretk/coretk/router_green.gif b/coretk/coretk/icons/router_green.gif similarity index 100% rename from coretk/coretk/router_green.gif rename to coretk/coretk/icons/router_green.gif diff --git a/coretk/coretk/run.gif b/coretk/coretk/icons/run.gif similarity index 100% rename from coretk/coretk/run.gif rename to coretk/coretk/icons/run.gif diff --git a/coretk/coretk/select.gif b/coretk/coretk/icons/select.gif similarity index 100% rename from coretk/coretk/select.gif rename to coretk/coretk/icons/select.gif diff --git a/coretk/coretk/start.gif b/coretk/coretk/icons/start.gif similarity index 100% rename from coretk/coretk/start.gif rename to coretk/coretk/icons/start.gif diff --git a/coretk/coretk/stop.gif b/coretk/coretk/icons/stop.gif similarity index 100% rename from coretk/coretk/stop.gif rename to coretk/coretk/icons/stop.gif diff --git a/coretk/coretk/text.gif b/coretk/coretk/icons/text.gif similarity index 100% rename from coretk/coretk/text.gif rename to coretk/coretk/icons/text.gif diff --git a/coretk/coretk/tunnel.gif b/coretk/coretk/icons/tunnel.gif similarity index 100% rename from coretk/coretk/tunnel.gif rename to coretk/coretk/icons/tunnel.gif diff --git a/coretk/coretk/twonode.gif b/coretk/coretk/icons/twonode.gif similarity index 100% rename from coretk/coretk/twonode.gif rename to coretk/coretk/icons/twonode.gif diff --git a/coretk/coretk/wlan.gif b/coretk/coretk/icons/wlan.gif similarity index 100% rename from coretk/coretk/wlan.gif rename to coretk/coretk/icons/wlan.gif diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index fc04c7cc..c5df15bb 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,11 +1,13 @@ import logging import os +from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") class Images: @@ -13,7 +15,7 @@ class Images: @classmethod def load(cls, name, file_path): - file_path = os.path.join(PATH, file_path) + # file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image @@ -33,55 +35,62 @@ class Images: :return: the matching image and its name """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get("switch"), "switch" + return Images.get(ImageEnum.SWITCH.value), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get("hub"), "hub" + return Images.get(ImageEnum.HUB.value), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get("wlan"), "wlan" + return Images.get(ImageEnum.WLAN.value), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get("rj45"), "rj45" + return Images.get(ImageEnum.RJ45.value), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get("tunnel"), "tunnel" + return Images.get(ImageEnum.TUNNEL.value), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get("router"), "router" + return Images.get(ImageEnum.ROUTER.value), "router" if node_model == "host": - return Images.get(("host")), "host" + return Images.get((ImageEnum.HOST.value)), "host" if node_model == "PC": - return Images.get("pc"), "PC" + return Images.get(ImageEnum.PC.value), "PC" if node_model == "mdr": - return Images.get("mdr"), "mdr" + return Images.get(ImageEnum.MDR.value), "mdr" if node_model == "prouter": - return Images.get("prouter"), "prouter" + return Images.get(ImageEnum.PROUTER.value), "prouter" if node_model == "OVS": - return Images.get("ovs"), "ovs" + return Images.get(ImageEnum.OVS.value), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") +class ImageEnum(Enum): + SWITCH = "lanswitch" + CORE = "core-icon" + START = "start" + MARKER = "marker" + ROUTER = "router" + SELECT = "select" + LINK = "link" + HUB = "hub" + WLAN = "wlan" + RJ45 = "rj45" + TUNNEL = "tunnel" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + HOST = "host" + PC = "pc" + MDR = "mdr" + PROUTER = "router_green" + OVS = "OVS" + EDITNODE = "document-properties" + PLOT = "plot" + TWONODE = "twonode" + STOP = "stop" + OBSERVE = "observe" + RUN = "run" + + def load_core_images(images): - images.load("core", "core-icon.png") - images.load("start", "start.gif") - images.load("switch", "lanswitch.gif") - images.load("marker", "marker.gif") - images.load("router", "router.gif") - images.load("select", "select.gif") - images.load("link", "link.gif") - images.load("hub", "hub.gif") - images.load("wlan", "wlan.gif") - images.load("rj45", "rj45.gif") - images.load("tunnel", "tunnel.gif") - images.load("oval", "oval.gif") - images.load("rectangle", "rectangle.gif") - images.load("text", "text.gif") - images.load("host", "host.gif") - images.load("pc", "pc.gif") - images.load("mdr", "mdr.gif") - images.load("prouter", "router_green.gif") - images.load("ovs", "OVS.gif") - images.load("editnode", "document-properties.gif") - images.load("run", "run.gif") - images.load("plot", "plot.gif") - images.load("twonode", "twonode.gif") - images.load("stop", "stop.gif") - images.load("observe", "observe.gif") + for file_name in os.listdir(ICONS_DIR): + file_path = os.path.join(ICONS_DIR, file_name) + name = file_name.split(".")[0] + images.load(name, file_path) diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py new file mode 100644 index 00000000..8538ab1f --- /dev/null +++ b/coretk/coretk/interface.py @@ -0,0 +1,43 @@ +import ipaddress +import random + + +class Interface: + def __init__(self, name, ipv4, ifid=None): + """ + Create an interface instance + + :param str name: interface name + :param str ip4: IPv4 + :param str mac: MAC address + :param int ifid: interface id + """ + self.name = name + self.ipv4 = ipv4 + self.ip4prefix = 24 + self.ip4_and_prefix = ipv4 + "/" + str(self.ip4prefix) + self.mac = self.random_mac_address() + self.id = ifid + + def random_mac_address(self): + return "02:00:00:%02x:%02x:%02x" % ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 225), + ) + + +class InterfaceManager: + def __init__(self): + self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) + self.index = 0 + + def get_address(self): + """ + Retrieve a new ipv4 address + + :return: + """ + i = self.index + self.index = self.index + 1 + return self.addresses[i] diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 2e1427da..6618e532 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,7 +3,14 @@ The actions taken when each menubar option is clicked """ import logging +import tkinter as tk import webbrowser +from tkinter import messagebox + +from core.api.grpc import core_pb2 +from coretk.coregrpc import CoreGrpc + +SAVEDIR = "/home/ncs/Desktop/" def sub_menu_items(): @@ -38,14 +45,6 @@ def file_save_shortcut(event): logging.debug("Shortcut for file save") -def file_save_as_xml(): - logging.debug("Click save as XML") - - -def file_save_as_imn(): - logging.debug("Click save as imn") - - def file_export_python_script(): logging.debug("Click file export python script") @@ -70,10 +69,6 @@ def file_save_screenshot(): logging.debug("Click file save screenshot") -def file_example_link(): - logging.debug("Click file example link") - - def edit_undo(): logging.debug("Click edit undo") @@ -294,10 +289,6 @@ def widgets_configure_throughput(): logging.debug("Click widgets configure throughput") -def session_start(): - logging.debug("Click session start") - - def session_change_sessions(): logging.debug("Click session change sessions") @@ -326,13 +317,107 @@ def session_options(): logging.debug("Click session options") -def help_core_github(): - webbrowser.open_new("https://github.com/coreemu/core") - - -def help_core_documentation(): - webbrowser.open_new("http://coreemu.github.io/core/") +# def help_core_github(): +# webbrowser.open_new("https://github.com/coreemu/core") +# +# +# def help_core_documentation(): +# webbrowser.open_new("http://coreemu.github.io/core/") def help_about(): logging.debug("Click help About") + + +class MenuAction: + """ + Actions performed when choosing menu items + """ + + def __init__(self, application, master): + self.master = master + self.application = application + self.core_grpc = application.core_grpc + + def clean_nodes_links_and_set_configuarations(self): + """ + Prompt use to stop running session before application is closed + + :return: nothing + """ + logging.info( + "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" + ) + grpc = self.application.core_grpc + state = grpc.get_session_state() + + if ( + state == core_pb2.SessionState.SHUTDOWN + or state == core_pb2.SessionState.DEFINITION + ): + grpc.delete_session() + grpc.core.close() + # self.application.quit() + else: + msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") + + if msgbox or msgbox is False: + if msgbox: + grpc.set_session_state("datacollect") + grpc.delete_links() + grpc.delete_nodes() + grpc.delete_session() + + grpc.core.close() + # self.application.quit() + + def on_quit(self): + """ + Prompt user whether so save running session, and then close the application + + :return: nothing + """ + self.clean_nodes_links_and_set_configuarations() + # self.application.core_grpc.close() + self.application.quit() + + def file_save_as_xml(self): + logging.info("menuaction.py file_save_as_xml()") + grpc = self.application.core_grpc + file_path = tk.filedialog.asksaveasfilename( + initialdir=SAVEDIR, + title="Save As", + filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), + defaultextension=".xml", + ) + with open("prev_saved_xml.txt", "a") as file: + file.write(file_path + "\n") + grpc.save_xml(file_path) + + def file_open_xml(self): + logging.info("menuaction.py file_open_xml()") + # grpc = self.application.core_grpc + file_path = tk.filedialog.askopenfilename( + initialdir=SAVEDIR, + title="Open", + filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), + ) + # clean up before openning a new session + # t0 = time.clock() + self.clean_nodes_links_and_set_configuarations() + grpc = CoreGrpc(self.application.master) + grpc.core.connect() + session_id = grpc.open_xml(file_path) + grpc.session_id = session_id + self.application.core_grpc = grpc + self.application.canvas.core_grpc = grpc + self.application.canvas.delete_components() + self.application.canvas.draw_existing_component() + # t1 = time.clock() + # print(t1 - t0) + + def help_core_github(self): + webbrowser.open_new("https://github.com/coreemu/core") + + def help_core_documentation(self): + webbrowser.open_new("http://coreemu.github.io/core/") diff --git a/coretk/coretk/oldimage/OVS.gif b/coretk/coretk/oldimage/OVS.gif new file mode 100755 index 0000000000000000000000000000000000000000..38fcbb2ea684f2cb3724f55bb668d66dd952ee4d GIT binary patch literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/core-icon.png b/coretk/coretk/oldimage/core-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0ff5aa3a51f71807691032f516d7a15d7da896 GIT binary patch literal 2931 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/document-properties.gif b/coretk/coretk/oldimage/document-properties.gif new file mode 100644 index 0000000000000000000000000000000000000000..732d8436455ba607ff30774ef83faa434789f775 GIT binary patch literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/host.gif b/coretk/coretk/oldimage/host.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bd60ae3d34d9bcd8be206ba2a8a701b19aea319 GIT binary patch literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/hub.gif b/coretk/coretk/oldimage/hub.gif new file mode 100644 index 0000000000000000000000000000000000000000..17f7c4d3ef726f744da685423057366093df33f8 GIT binary patch literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/link.gif b/coretk/coretk/oldimage/link.gif new file mode 100644 index 0000000000000000000000000000000000000000..55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea GIT binary patch literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/mdr.gif b/coretk/coretk/oldimage/mdr.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6762f6500828ead06fb73c7a9061165ccdef85b GIT binary patch literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/observe.gif b/coretk/coretk/oldimage/observe.gif new file mode 100644 index 0000000000000000000000000000000000000000..6b66e7305f35484095a2182ed828c6507c72a746 GIT binary patch literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bfQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/plot.gif b/coretk/coretk/oldimage/plot.gif new file mode 100644 index 0000000000000000000000000000000000000000..3924adbf821d1cfeb34a0b9efc7e9d79041fd1df GIT binary patch literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/rectangle.gif b/coretk/coretk/oldimage/rectangle.gif new file mode 100644 index 0000000000000000000000000000000000000000..ed271f5737d0e8c2e35e9f03735de27509996893 GIT binary patch literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/rj45.gif b/coretk/coretk/oldimage/rj45.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ab7ac56dba8946e37182cf05ca160683f53e2e9 GIT binary patch literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/router_green.gif b/coretk/coretk/oldimage/router_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..76e3ecd57c59ec99767f5641e701f908f09f0258 GIT binary patch literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/run.gif b/coretk/coretk/oldimage/run.gif new file mode 100644 index 0000000000000000000000000000000000000000..71dcc67eddc7b829b3221ba4823a04e93c317ba9 GIT binary patch literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/select.gif b/coretk/coretk/oldimage/select.gif new file mode 100644 index 0000000000000000000000000000000000000000..bb7e128c878317f6287564514a302e5e3c40c10e GIT binary patch literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/tunnel.gif b/coretk/coretk/oldimage/tunnel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d574147f535122637516f4887d931779c5903ead GIT binary patch literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/twonode.gif b/coretk/coretk/oldimage/twonode.gif new file mode 100644 index 0000000000000000000000000000000000000000..28e75fac3aadc9286cdb65b1269f3f1355f055d4 GIT binary patch literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob literal 0 HcmV?d00001 diff --git a/coretk/coretk/oldimage/wlan.gif b/coretk/coretk/oldimage/wlan.gif new file mode 100644 index 0000000000000000000000000000000000000000..d72fe9c3f8db0c7013424313bc826eca7022c19a GIT binary patch literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A literal 0 HcmV?d00001 diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt new file mode 100644 index 00000000..3d6d4d91 --- /dev/null +++ b/coretk/coretk/prev_saved_xml.txt @@ -0,0 +1,5 @@ +/home/ncs/Desktop/random.xml/home/ncs/Desktop/untitled.xml/home/ncs/Desktop/test.xml +/home/ncs/Desktop/test1.xml + +/home/ncs/Desktop/untitled.xml +/home/ncs/Desktop/test1.xml diff --git a/output.txt b/output.txt new file mode 100644 index 00000000..ee11dc37 --- /dev/null +++ b/output.txt @@ -0,0 +1,23 @@ +./netns/vcmdmodule.c:static PyObject *VCmd_popen(VCmd *self, PyObject *args, PyObject *kwds) +./netns/vcmdmodule.c: {"popen", (PyCFunction)VCmd_popen, METH_VARARGS | METH_KEYWORDS, +./netns/vcmdmodule.c: "popen(args...) -> (VCmdWait, cmdin, cmdout, cmderr)\n\n" +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: def popen(self, args): +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: Execute a popen command against the node. +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/build/lib.linux-x86_64-2.7/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/tests/test_core.py: p, stdin, stdout, stderr = client.popen(command) +./daemon/examples/netns/ospfmanetmdrtest.py: """ Exceute call to node.popen(). """ +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(self.args) +./daemon/examples/netns/ospfmanetmdrtest.py: self.id, self.stdin, self.out, self.err = self.node.client.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: def popen(self, args): +./daemon/core/nodes/client.py: Execute a popen command against the node. +./daemon/core/nodes/client.py: :return: popen object, stdin, stdout, and stderr +./daemon/core/nodes/client.py: logging.debug("popen: %s", cmd) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./daemon/core/nodes/client.py: p, stdin, stdout, stderr = self.popen(args) +./corefx/src/main/resources/js/leaflet.js:!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";function i(t){var i,e,n,o;for(e=1,n=arguments.length;e=0}function B(t,i,e,n){return"touchstart"===i?O(t,e,n):"touchmove"===i?W(t,e,n):"touchend"===i&&H(t,e,n),this}function I(t,i,e){var n=t["_leaflet_"+i+e];return"touchstart"===i?t.removeEventListener(te,n,!1):"touchmove"===i?t.removeEventListener(ie,n,!1):"touchend"===i&&(t.removeEventListener(ee,n,!1),t.removeEventListener(ne,n,!1)),this}function O(t,i,n){var o=e(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(oe.indexOf(t.target.tagName)<0))return;Pt(t)}j(t,i)});t["_leaflet_touchstart"+n]=o,t.addEventListener(te,o,!1),re||(document.documentElement.addEventListener(te,R,!0),document.documentElement.addEventListener(ie,N,!0),document.documentElement.addEventListener(ee,D,!0),document.documentElement.addEventListener(ne,D,!0),re=!0)}function R(t){se[t.pointerId]=t,ae++}function N(t){se[t.pointerId]&&(se[t.pointerId]=t)}function D(t){delete se[t.pointerId],ae--}function j(t,i){t.touches=[];for(var e in se)t.touches.push(se[e]);t.changedTouches=[t],i(t)}function W(t,i,e){var n=function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&j(t,i)};t["_leaflet_touchmove"+e]=n,t.addEventListener(ie,n,!1)}function H(t,i,e){var n=function(t){j(t,i)};t["_leaflet_touchend"+e]=n,t.addEventListener(ee,n,!1),t.addEventListener(ne,n,!1)}function F(t,i,e){function n(t){var i;if(Vi){if(!bi||"mouse"===t.pointerType)return;i=ae}else i=t.touches.length;if(!(i>1)){var e=Date.now(),n=e-(s||e);r=t.touches?t.touches[0]:t,a=n>0&&n<=h,s=e}}function o(t){if(a&&!r.cancelBubble){if(Vi){if(!bi||"mouse"===t.pointerType)return;var e,n,o={};for(n in r)e=r[n],o[n]=e&&e.bind?e.bind(r):e;r=o}r.type="dblclick",i(r),s=null}}var s,r,a=!1,h=250;return t[le+he+e]=n,t[le+ue+e]=o,t[le+"dblclick"+e]=i,t.addEventListener(he,n,!1),t.addEventListener(ue,o,!1),t.addEventListener("dblclick",i,!1),this}function U(t,i){var e=t[le+he+i],n=t[le+ue+i],o=t[le+"dblclick"+i];return t.removeEventListener(he,e,!1),t.removeEventListener(ue,n,!1),bi||t.removeEventListener("dblclick",o,!1),this}function V(t){return"string"==typeof t?document.getElementById(t):t}function q(t,i){var e=t.style[i]||t.currentStyle&&t.currentStyle[i];if((!e||"auto"===e)&&document.defaultView){var n=document.defaultView.getComputedStyle(t,null);e=n?n[i]:null}return"auto"===e?null:e}function G(t,i,e){var n=document.createElement(t);return n.className=i||"",e&&e.appendChild(n),n}function K(t){var i=t.parentNode;i&&i.removeChild(t)}function Y(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function X(t){var i=t.parentNode;i.lastChild!==t&&i.appendChild(t)}function J(t){var i=t.parentNode;i.firstChild!==t&&i.insertBefore(t,i.firstChild)}function $(t,i){if(void 0!==t.classList)return t.classList.contains(i);var e=et(t);return e.length>0&&new RegExp("(^|\\s)"+i+"(\\s|$)").test(e)}function Q(t,i){if(void 0!==t.classList)for(var e=u(i),n=0,o=e.length;n100&&n<500||t.target._simulatedClick&&!t._simulated?Lt(t):(ge=e,i(t))}function Zt(t,i){if(!i||!t.length)return t.slice();var e=i*i;return t=Bt(t,e),t=kt(t,e)}function Et(t,i,e){return Math.sqrt(Dt(t,i,e,!0))}function kt(t,i){var e=t.length,n=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(e);n[0]=n[e-1]=1,At(t,n,i,0,e-1);var o,s=[];for(o=0;oh&&(s=r,h=a);h>e&&(i[s]=1,At(t,i,e,n,s),At(t,i,e,s,o))}function Bt(t,i){for(var e=[t[0]],n=1,o=0,s=t.length;ni&&(e.push(t[n]),o=n);return oi.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function Nt(t,i){var e=i.x-t.x,n=i.y-t.y;return e*e+n*n}function Dt(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return u>0&&((o=((t.x-s)*a+(t.y-r)*h)/u)>1?(s=e.x,r=e.y):o>0&&(s+=a*o,r+=h*o)),a=t.x-s,h=t.y-r,n?a*a+h*h:new x(s,r)}function jt(t){return!oi(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function Wt(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),jt(t)}function Ht(t,i,e){var n,o,s,r,a,h,u,l,c,_=[1,4,2,8];for(o=0,u=t.length;o0?Math.floor(t):Math.ceil(t)};x.prototype={clone:function(){return new x(this.x,this.y)},add:function(t){return this.clone()._add(w(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(w(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new x(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new x(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=_i(this.x),this.y=_i(this.y),this},distanceTo:function(t){var i=(t=w(t)).x-this.x,e=t.y-this.y;return Math.sqrt(i*i+e*e)},equals:function(t){return(t=w(t)).x===this.x&&t.y===this.y},contains:function(t){return t=w(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+a(this.x)+", "+a(this.y)+")"}},P.prototype={extend:function(t){return t=w(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new x((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new x(this.min.x,this.max.y)},getTopRight:function(){return new x(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var i,e;return(t="number"==typeof t[0]||t instanceof x?w(t):b(t))instanceof P?(i=t.min,e=t.max):i=e=t,i.x>=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=b(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=z(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng1,Xi=!!document.createElement("canvas").getContext,Ji=!(!document.createElementNS||!E("svg").createSVGRect),$i=!Ji&&function(){try{var t=document.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}(),Qi=(Object.freeze||Object)({ie:Pi,ielt9:Li,edge:bi,webkit:Ti,android:zi,android23:Mi,androidStock:Si,opera:Zi,chrome:Ei,gecko:ki,safari:Ai,phantom:Bi,opera12:Ii,win:Oi,ie3d:Ri,webkit3d:Ni,gecko3d:Di,any3d:ji,mobile:Wi,mobileWebkit:Hi,mobileWebkit3d:Fi,msPointer:Ui,pointer:Vi,touch:qi,mobileOpera:Gi,mobileGecko:Ki,retina:Yi,canvas:Xi,svg:Ji,vml:$i}),te=Ui?"MSPointerDown":"pointerdown",ie=Ui?"MSPointerMove":"pointermove",ee=Ui?"MSPointerUp":"pointerup",ne=Ui?"MSPointerCancel":"pointercancel",oe=["INPUT","SELECT","OPTION"],se={},re=!1,ae=0,he=Ui?"MSPointerDown":Vi?"pointerdown":"touchstart",ue=Ui?"MSPointerUp":Vi?"pointerup":"touchend",le="_leaflet_",ce=st(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),_e=st(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===_e||"OTransition"===_e?_e+"End":"transitionend";if("onselectstart"in document)fi=function(){mt(window,"selectstart",Pt)},gi=function(){ft(window,"selectstart",Pt)};else{var pe=st(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]);fi=function(){if(pe){var t=document.documentElement.style;vi=t[pe],t[pe]="none"}},gi=function(){pe&&(document.documentElement.style[pe]=vi,vi=void 0)}}var me,fe,ge,ve=(Object.freeze||Object)({TRANSFORM:ce,TRANSITION:_e,TRANSITION_END:de,get:V,getStyle:q,create:G,remove:K,empty:Y,toFront:X,toBack:J,hasClass:$,addClass:Q,removeClass:tt,setClass:it,getClass:et,setOpacity:nt,testProp:st,setTransform:rt,setPosition:at,getPosition:ht,disableTextSelection:fi,enableTextSelection:gi,disableImageDrag:ut,enableImageDrag:lt,preventOutline:ct,restoreOutline:_t,getSizedParentNode:dt,getScale:pt}),ye="_leaflet_events",xe=Oi&&Ei?2*window.devicePixelRatio:ki?window.devicePixelRatio:1,we={},Pe=(Object.freeze||Object)({on:mt,off:ft,stopPropagation:yt,disableScrollPropagation:xt,disableClickPropagation:wt,preventDefault:Pt,stop:Lt,getMousePosition:bt,getWheelDelta:Tt,fakeStop:zt,skipped:Mt,isExternalTarget:Ct,addListener:mt,removeListener:ft}),Le=ci.extend({run:function(t,i,e,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=e||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=ht(t),this._offset=i.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=f(this._animate,this),this._step()},_step:function(t){var i=+new Date-this._startTime,e=1e3*this._duration;ithis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=new M(t.coords.latitude,t.coords.longitude),e=i.toBounds(2*t.coords.accuracy),n=this._locateOptions;if(n.setView){var o=this.getBoundsZoom(e);this.setView(i,n.maxZoom?Math.min(o,n.maxZoom):o)}var s={latlng:i,bounds:e,timestamp:t.timestamp};for(var r in t.coords)"number"==typeof t.coords[r]&&(s[r]=t.coords[r]);this.fire("locationfound",s)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),K(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(g(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)K(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e=G("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),i||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=ji?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return bt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=V(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");mt(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&ji,Q(t,"leaflet-container"+(qi?" leaflet-touch":"")+(Yi?" leaflet-retina":"")+(Li?" leaflet-oldie":"")+(Ai?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=q(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),at(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Q(t.markerPane,"leaflet-zoom-hide"),Q(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){at(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n,!1)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t,i){return t&&this.fire("zoomstart"),i||this.fire("movestart"),this},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){at(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={},this._targets[n(this._container)]=this;var i=t?ft:mt;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),ji&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!Ct(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!Ct(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!Mt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||ct(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e))).length){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&Pt(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.getLatLng&&(!s._radius||s._radius<=10);r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=ji?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){tt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._trunc();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e))&&(this.panBy(e,i),!0)},_createAnimProxy:function(){var t=this._proxy=G("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=ce,e=this._proxy.style[i];rt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();rt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){K(this._proxy),delete this._proxy},_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(f(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,n,o){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,Q(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&tt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),Te=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return Q(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(K(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),ze=function(t){return new Te(t)};be.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=G("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=G("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)K(this._controlCorners[t]);K(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Me=Te.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),mt(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;s>=0;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;s=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ce=Te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=G("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=G("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),wt(s),mt(s,"click",Lt),mt(s,"click",o,this),mt(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";tt(this._zoomInButton,i),tt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&Q(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&Q(this._zoomInButton,i)}});be.mergeOptions({zoomControl:!0}),be.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ce,this.addControl(this.zoomControl))});var Se=Te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i=G("div","leaflet-control-scale"),e=this.options;return this._addScales(e,"leaflet-control-scale-line",i),t.on(e.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=G("div",i,e)),t.imperial&&(this._iScale=G("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ze=Te.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=G("div","leaflet-control-attribution"),wt(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});be.mergeOptions({attributionControl:!0}),be.addInitHook(function(){this.options.attributionControl&&(new Ze).addTo(this)});Te.Layers=Me,Te.Zoom=Ce,Te.Scale=Se,Te.Attribution=Ze,ze.layers=function(t,i,e){return new Me(t,i,e)},ze.zoom=function(t){return new Ce(t)},ze.scale=function(t){return new Se(t)},ze.attribution=function(t){return new Ze(t)};var Ee=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});Ee.addTo=function(t,i){return t.addHandler(i,this),this};var ke,Ae={Events:li},Be=qi?"touchstart mousedown":"mousedown",Ie={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Oe={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Re=ci.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(mt(this._dragStartTarget,Be,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Re._dragging===this&&this.finishDrag(),ft(this._dragStartTarget,Be,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!$(this._element,"leaflet-zoom-anim")&&!(Re._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Re._dragging=this,this._preventOutline&&ct(this._element),ut(),fi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=dt(this._element);this._startPoint=new x(i.clientX,i.clientY),this._parentScale=pt(e),mt(document,Oe[t.type],this._onMove,this),mt(document,Ie[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&t.touches.length>1)this._moved=!0;else{var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY)._subtract(this._startPoint);(e.x||e.y)&&(Math.abs(e.x)+Math.abs(e.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),a+=u=Math.PI/2-2*Math.atan(r*i)-a;return new M(a*e,t.x*e/n)}},He=(Object.freeze||Object)({LonLat:je,Mercator:We,SphericalMercator:mi}),Fe=i({},pi,{code:"EPSG:3395",projection:We,transformation:function(){var t=.5/(Math.PI*We.R);return Z(t,.5,-t,.5)}()}),Ue=i({},pi,{code:"EPSG:4326",projection:je,transformation:Z(1/180,1,-1/180,.5)}),Ve=i({},di,{projection:je,transformation:Z(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});di.Earth=pi,di.EPSG3395=Fe,di.EPSG3857=yi,di.EPSG900913=xi,di.EPSG4326=Ue,di.Simple=Ve;var qe=ci.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[n(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});be.include({addLayer:function(t){if(!t._layerAdd)throw new Error("The provided object is not a Layer.");var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){for(var i=0,e=(t=t?oi(t)?t:[t]:[]).length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){nn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||nn.prototype._containsPoint.call(this,t,!0)}}),sn=Ke.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=oi(t)?t:t.features;if(o){for(i=0,e=o.length;i0?o:[i.src]}else{oi(this._url)||(this._url=[this._url]),i.autoplay=!!this.options.autoplay,i.loop=!!this.options.loop;for(var a=0;ao?(i.height=o+"px",Q(t,"leaflet-popup-scrolled")):tt(t,"leaflet-popup-scrolled"),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();at(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(q(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(ht(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Lt(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});be.mergeOptions({closePopupOnClick:!0}),be.include({openPopup:function(t,i,e){return t instanceof cn||(t=new cn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),qe.include({bindPopup:function(t,i){return t instanceof cn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new cn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof qe||(i=t,t=this),t instanceof Ke)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Lt(t),i instanceof Qe?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var _n=ln.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){ln.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){ln.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=ln.prototype.getEvents.call(this);return qi&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=G("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)this._setView(t,e);else{for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);if(d.z=this._tileZoom,this._isValidTile(d)){var p=this._tiles[this._tileCoordsToKey(d)];p?p.current=!0:r.push(d)}}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new T(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(K(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){Q(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Li&&this.options.opacity<1&&nt(t,this.options.opacity),zi&&!Mi&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),at(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(nt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(Q(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Li||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),mn=pn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=l(this,i)).detectRetina&&Yi&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),zi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return mt(n,"load",e(this._tileOnLoad,this,i,n)),mt(n,"error",e(this._tileOnError,this,i,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),n.alt="",n.setAttribute("role","presentation"),n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:Yi?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Li?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.getAttribute("src")!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=r,i.onerror=r,i.complete||(i.src=si,K(i),delete this._tiles[t]))},_removeTile:function(t){var i=this._tiles[t];if(i)return Si||i.el.setAttribute("src",si),pn.prototype._removeTile.call(this,t)},_tileReady:function(t,i,e){if(this._map&&(!e||e.getAttribute("src")!==si))return pn.prototype._tileReady.call(this,t,i,e)}}),fn=mn.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);var s=(e=l(this,e)).detectRetina&&Yi?2:1,r=this.getTileSize();n.width=r.x*s,n.height=r.y*s,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,mn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToNwSe(t),e=this._crs,n=b(e.project(i[0]),e.project(i[1])),o=n.min,s=n.max,r=(this._wmsVersion>=1.3&&this._crs===Ue?[o.y,o.x,s.y,s.x]:[o.x,o.y,s.x,s.y]).join(","),a=mn.prototype.getTileUrl.call(this,t);return a+c(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+r},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});mn.WMS=fn,Jt.wms=function(t,i){return new fn(t,i)};var gn=qe.extend({options:{padding:.1,tolerance:0},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&Q(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=ht(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i).subtract(s),a=o.multiplyBy(-e).add(n).add(o).subtract(r);ji?rt(this._container,a,e):at(this._container,a)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),vn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){gn.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");mt(t,"mousemove",o(this._onMouseMove,32,this),this),mt(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),mt(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){g(this._redrawRequest),delete this._ctx,K(this._container),ft(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){this._redrawBounds=null;for(var t in this._layers)this._layers[t]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},gn.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=Yi?2:1;at(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",Yi&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){gn.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,o=i.prev;e?e.prev=o:this._drawLast=o,o?o.next=e:this._drawFirst=e,delete this._drawnLayers[t._leaflet_id],delete t._order,delete this._layers[n(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){var i,e=t.options.dashArray.split(/[, ]+/),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),xn={_initContainer:function(){this._container=G("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(gn.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=yn("shape");Q(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=yn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;K(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=yn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=oi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=yn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){X(t._container)},_bringToBack:function(t){J(t._container)}},wn=$i?yn:E,Pn=gn.extend({getEvents:function(){var t=gn.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=wn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=wn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){K(this._container),ft(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){gn.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),at(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=wn("path");t.options.className&&Q(i,t.options.className),t.options.interactive&&Q(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){K(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){X(t._path)},_bringToBack:function(t){J(t._path)}});$i&&Pn.include(xn),be.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&$t(t)||Qt(t)}});var Ln=on.extend({initialize:function(t,i){on.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Pn.create=wn,Pn.pointsToPath=k,sn.geometryToLayer=Ft,sn.coordsToLatLng=Ut,sn.coordsToLatLngs=Vt,sn.latLngToCoords=qt,sn.latLngsToCoords=Gt,sn.getFeature=Kt,sn.asFeature=Yt,be.mergeOptions({boxZoom:!0});var bn=Ee.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){mt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){ft(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){K(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),fi(),ut(),this._startPoint=this._map.mouseEventToContainerPoint(t),mt(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=G("div","leaflet-zoom-box",this._container),Q(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();at(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(K(this._box),tt(this._container,"leaflet-crosshair")),gi(),lt(),ft(document,{contextmenu:Lt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});be.addInitHook("addHandler","boxZoom",bn),be.mergeOptions({doubleClickZoom:!0});var Tn=Ee.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});be.addInitHook("addHandler","doubleClickZoom",Tn),be.mergeOptions({dragging:!0,inertia:!Mi,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var zn=Ee.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Re(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}Q(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){tt(this._map._container,"leaflet-grab"),tt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;this._positions.length>1&&t-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});be.addInitHook("addHandler","scrollWheelZoom",Cn),be.mergeOptions({tap:!0,tapTolerance:15});var Sn=Ee.extend({addHooks:function(){mt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){ft(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(Pt(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&Q(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),mt(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),ft(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&tt(e,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});qi&&!Vi&&be.addInitHook("addHandler","tap",Sn),be.mergeOptions({touchZoom:qi&&!Mi,bounceAtZoomLimits:!0});var Zn=Ee.extend({addHooks:function(){Q(this._map._container,"leaflet-touch-zoom"),mt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){tt(this._map._container,"leaflet-touch-zoom"),ft(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var e=i.mouseEventToContainerPoint(t.touches[0]),n=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),mt(document,"touchmove",this._onTouchMove,this),mt(document,"touchend",this._onTouchEnd,this),Pt(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0,!1),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),Pt(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),ft(document,"touchmove",this._onTouchMove),ft(document,"touchend",this._onTouchEnd),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});be.addInitHook("addHandler","touchZoom",Zn),be.BoxZoom=bn,be.DoubleClickZoom=Tn,be.Drag=zn,be.Keyboard=Mn,be.ScrollWheelZoom=Cn,be.Tap=Sn,be.TouchZoom=Zn,Object.freeze=ti,t.version="1.3.4+HEAD.0e566b2",t.Control=Te,t.control=ze,t.Browser=Qi,t.Evented=ci,t.Mixin=Ae,t.Util=ui,t.Class=v,t.Handler=Ee,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Pe,t.DomUtil=ve,t.PosAnimation=Le,t.Draggable=Re,t.LineUtil=Ne,t.PolyUtil=De,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=S,t.transformation=Z,t.Projection=He,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=di,t.GeoJSON=sn,t.geoJSON=Xt,t.geoJson=an,t.Layer=qe,t.LayerGroup=Ge,t.layerGroup=function(t,i){return new Ge(t,i)},t.FeatureGroup=Ke,t.featureGroup=function(t){return new Ke(t)},t.ImageOverlay=hn,t.imageOverlay=function(t,i,e){return new hn(t,i,e)},t.VideoOverlay=un,t.videoOverlay=function(t,i,e){return new un(t,i,e)},t.DivOverlay=ln,t.Popup=cn,t.popup=function(t,i){return new cn(t,i)},t.Tooltip=_n,t.tooltip=function(t,i){return new _n(t,i)},t.Icon=Ye,t.icon=function(t){return new Ye(t)},t.DivIcon=dn,t.divIcon=function(t){return new dn(t)},t.Marker=$e,t.marker=function(t,i){return new $e(t,i)},t.TileLayer=mn,t.tileLayer=Jt,t.GridLayer=pn,t.gridLayer=function(t){return new pn(t)},t.SVG=Pn,t.svg=Qt,t.Renderer=gn,t.Canvas=vn,t.canvas=$t,t.Path=Qe,t.CircleMarker=tn,t.circleMarker=function(t,i){return new tn(t,i)},t.Circle=en,t.circle=function(t,i,e){return new en(t,i,e)},t.Polyline=nn,t.polyline=function(t,i){return new nn(t,i)},t.Polygon=on,t.polygon=function(t,i){return new on(t,i)},t.Rectangle=Ln,t.rectangle=function(t,i){return new Ln(t,i)},t.Map=be,t.map=function(t,i){return new be(t,i)};var En=window.L;t.noConflict=function(){return window.L=En,this},window.L=t}); From 38683cb0d0da27e990621c8f658f0169cd7a0db5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 18 Oct 2019 16:42:00 -0700 Subject: [PATCH 030/305] more work on coretk --- coretk/coretk/app.py | 10 +- coretk/coretk/coregrpc.py | 169 ++++++++++++----------- coretk/coretk/coretocanvas.py | 44 ++++++ coretk/coretk/coretoolbar.py | 55 ++++++-- coretk/coretk/graph.py | 189 ++++++++++++++----------- coretk/coretk/graph_helper.py | 198 +++++++++++++++++++++++++++ coretk/coretk/grpcmanagement.py | 46 ++++++- coretk/coretk/icons/antenna.gif | Bin 0 -> 230 bytes coretk/coretk/icons/document-new.gif | Bin 0 -> 1054 bytes coretk/coretk/icons/edit-delete.gif | Bin 0 -> 1006 bytes coretk/coretk/icons/fileopen.gif | Bin 0 -> 1095 bytes coretk/coretk/images.py | 4 + coretk/coretk/interface.py | 5 + coretk/coretk/linkinfo.py | 134 ++++++++++++++++++ coretk/coretk/menuaction.py | 53 ++++--- coretk/coretk/prev_saved_xml.txt | 7 +- coretk/coretk/querysessiondrawing.py | 174 +++++++++++++++++++++++ coretk/coretk/wirelessconnection.py | 53 +++++++ 18 files changed, 939 insertions(+), 202 deletions(-) create mode 100644 coretk/coretk/coretocanvas.py create mode 100644 coretk/coretk/graph_helper.py create mode 100644 coretk/coretk/icons/antenna.gif create mode 100644 coretk/coretk/icons/document-new.gif create mode 100644 coretk/coretk/icons/edit-delete.gif create mode 100644 coretk/coretk/icons/fileopen.gif create mode 100644 coretk/coretk/linkinfo.py create mode 100644 coretk/coretk/querysessiondrawing.py create mode 100644 coretk/coretk/wirelessconnection.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 6328b3bc..e4f632a7 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -25,6 +25,7 @@ class Application(tk.Frame): self.create_widgets() self.draw_canvas() self.start_grpc() + # self.try_make_table() def load_images(self): """ @@ -89,7 +90,7 @@ class Application(tk.Frame): :return: nothing """ self.master.update() - self.core_grpc = CoreGrpc(self.master) + self.core_grpc = CoreGrpc(self) self.core_grpc.set_up() self.canvas.core_grpc = self.core_grpc self.canvas.draw_existing_component() @@ -99,6 +100,13 @@ class Application(tk.Frame): menu_action.on_quit() # self.quit() + def try_make_table(self): + f = tk.Frame(self.master) + for i in range(3): + e = tk.Entry(f) + e.grid(row=0, column=1, stick="nsew") + f.pack(side=tk.TOP) + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index abcad792..095169ed 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -3,26 +3,37 @@ Incorporate grpc into python tkinter GUI """ import logging import os -import tkinter as tk +from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.linkinfo import Throughput +from coretk.querysessiondrawing import SessionTable +from coretk.wirelessconnection import WirelessConnection class CoreGrpc: - def __init__(self, master): + def __init__(self, app, sid=None): """ Create a CoreGrpc instance """ - print("Create grpc instance") self.core = client.CoreGrpcClient() - self.session_id = None - self.master = master + self.session_id = sid + self.master = app.master # self.set_up() self.interface_helper = None + self.throughput_draw = Throughput(app.canvas, self) + self.wireless_draw = WirelessConnection(app.canvas, self) def log_event(self, event): logging.info("event: %s", event) + if event.link_event is not None: + self.wireless_draw.hangle_link_event(event.link_event) + + def log_throughput(self, event): + interface_throughputs = event.interface_throughputs + # bridge_throughputs = event.bridge_throughputs + self.throughput_draw.process_grpc_throughput_event(interface_throughputs) def create_new_session(self): """ @@ -36,29 +47,7 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id self.core.events(self.session_id, self.log_event) - - def _enter_session(self, session_id, dialog): - """ - enter an existing session - - :return: - """ - dialog.destroy() - response = self.core.get_session(session_id) - self.session_id = session_id - print("set session id: %s", session_id) - logging.info("Entering session_id %s.... Result: %s", session_id, response) - # self.master.canvas.draw_existing_component() - - def _create_session(self, dialog): - """ - create a new session - - :param tkinter.Toplevel dialog: save core session prompt dialog - :return: nothing - """ - dialog.destroy() - self.create_new_session() + self.core.throughputs(self.log_throughput) def query_existing_sessions(self, sessions): """ @@ -68,35 +57,29 @@ class CoreGrpc: :return: nothing """ - dialog = tk.Toplevel() - dialog.title("CORE sessions") - for session in sessions: - b = tk.Button( - dialog, - text="Session " + str(session.id), - command=lambda sid=session.id: self._enter_session(sid, dialog), - ) - b.pack(side=tk.TOP) - b = tk.Button( - dialog, text="create new", command=lambda: self._create_session(dialog) - ) - b.pack(side=tk.TOP) - dialog.update() - x = ( - self.master.winfo_x() - + (self.master.winfo_width() - dialog.winfo_width()) / 2 - ) - y = ( - self.master.winfo_y() - + (self.master.winfo_height() / 2 - dialog.winfo_height()) / 2 - ) - dialog.geometry(f"+{int(x)}+{int(y)}") - dialog.wait_window() + SessionTable(self, self.master) - def delete_session(self): - response = self.core.delete_session(self.session_id) + def delete_session(self, custom_sid=None): + if custom_sid is None: + sid = self.session_id + else: + sid = custom_sid + response = self.core.delete_session(sid) logging.info("Deleted session result: %s", response) + def terminate_session(self, custom_sid=None): + if custom_sid is None: + sid = self.session_id + else: + sid = custom_sid + s = self.core.get_session(sid).session + # delete links and nodes from running session + if s.state == core_pb2.SessionState.RUNTIME: + self.set_session_state("datacollect", sid) + self.delete_links(sid) + self.delete_nodes(sid) + self.delete_session(sid) + def set_up(self): """ Query sessions, if there exist any, prompt whether to join one @@ -106,7 +89,7 @@ class CoreGrpc: self.core.connect() response = self.core.get_sessions() - logging.info("all sessions: %s", response) + # logging.info("coregrpc.py: all sessions: %s", response) # if there are no sessions, create a new session, else join a session sessions = response.sessions @@ -119,70 +102,87 @@ class CoreGrpc: def get_session_state(self): response = self.core.get_session(self.session_id) - logging.info("get session: %s", response) + # logging.info("get session: %s", response) return response.session.state - def set_session_state(self, state): + def set_session_state(self, state, custom_session_id=None): """ Set session state :param str state: session state to set :return: nothing """ - response = None + if custom_session_id is None: + sid = self.session_id + else: + sid = custom_session_id + if state == "configuration": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.CONFIGURATION + sid, core_pb2.SessionState.CONFIGURATION ) elif state == "instantiation": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.INSTANTIATION + sid, core_pb2.SessionState.INSTANTIATION ) elif state == "datacollect": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.DATACOLLECT + sid, core_pb2.SessionState.DATACOLLECT ) elif state == "shutdown": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.SHUTDOWN - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) elif state == "runtime": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.RUNTIME - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) elif state == "definition": response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.DEFINITION + sid, core_pb2.SessionState.DEFINITION ) elif state == "none": - response = self.core.set_session_state( - self.session_id, core_pb2.SessionState.NONE - ) + response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) else: logging.error("coregrpc.py: set_session_state: INVALID STATE") logging.info("set session state: %s", response) def add_node(self, node_type, model, x, y, name, node_id): - logging.info("coregrpc.py ADD NODE %s", name) position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) + if node_type == core_pb2.NodeType.WIRELESS_LAN: + d = OrderedDict() + d["basic_range"] = "275" + d["bandwidth"] = "54000000" + d["jitter"] = "0" + d["delay"] = "20000" + d["error"] = "0" + r = self.core.set_wlan_config(self.session_id, node_id, d) + logging.debug("set wlan config %s", r) return response.node_id def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) + # self.core.events(self.session_id, self.log_event) - def delete_nodes(self): - for node in self.core.get_session(self.session_id).session.nodes: + def delete_nodes(self, delete_session=None): + if delete_session is None: + sid = self.session_id + else: + sid = delete_session + for node in self.core.get_session(sid).session.nodes: response = self.core.delete_node(self.session_id, node.id) - logging.info("delete node %s", response) + logging.info("delete nodes %s", response) - def delete_links(self): - for link in self.core.get_session(self.session_id).session.links: + def delete_links(self, delete_session=None): + sid = None + if delete_session is None: + sid = self.session_id + else: + sid = delete_session + + for link in self.core.get_session(sid).session.links: response = self.core.delete_link( self.session_id, link.node_one_id, @@ -190,7 +190,7 @@ class CoreGrpc: link.interface_one.id, link.interface_two.id, ) - logging.info("delete link %s", response) + logging.info("delete links %s", response) def add_link(self, id1, id2, type1, type2, edge): """ @@ -235,6 +235,13 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + # def get_session(self): + # response = self.core.get_session(self.session_id) + # nodes = response.session.nodes + # for node in nodes: + # r = self.core.get_node_links(self.session_id, node.id) + # logging.info(r) + def launch_terminal(self, node_id): response = self.core.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) @@ -249,6 +256,7 @@ class CoreGrpc: """ response = self.core.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) + self.core.events(self.session_id, self.log_event) def open_xml(self, file_path): """ @@ -258,7 +266,10 @@ class CoreGrpc: :return: session id """ response = self.core.open_xml(file_path) - return response.session_id + self.session_id = response.session_id + # print("Sessionz") + # self.core.events(self.session_id, self.log_event) + # return response.session_id # logging.info("coregrpc.py open_xml()", type(response)) def close(self): diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py new file mode 100644 index 00000000..f16c5306 --- /dev/null +++ b/coretk/coretk/coretocanvas.py @@ -0,0 +1,44 @@ +""" +provide mapping from core to canvas +""" +import logging + + +class CoreToCanvasMapping: + def __init__(self): + self.core_id_to_canvas_id = {} + self.core_node_and_interface_to_canvas_edge = {} + + def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): + self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token + + def get_token_from_node_and_interface(self, nid, iid): + key = tuple([nid, iid]) + if key in self.core_node_and_interface_to_canvas_edge: + return self.core_node_and_interface_to_canvas_edge[key] + else: + logging.error("invalid key") + return None + + def map_core_id_to_canvas_id(self, core_nid, canvas_nid): + if core_nid not in self.core_id_to_canvas_id: + self.core_id_to_canvas_id[core_nid] = canvas_nid + else: + logging.debug("key already existed") + + def get_canvas_id_from_core_id(self, core_id): + if core_id in self.core_id_to_canvas_id: + return self.core_id_to_canvas_id[core_id] + else: + logging.debug("invalid key") + return None + + # def add_mapping(self, core_id, canvas_id): + # if core_id not in self.core_id_to_canvas_id: + # self.core_id_to_canvas_id[core_id] = canvas_id + # else: + # logging.error("key already mapped") + # + # def delete_mapping(self, core_id): + # result = self.core_id_to_canvas_id.pop(core_id, None) + # return result diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 6911ebaf..4e263d28 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,11 +1,24 @@ import logging import tkinter as tk +from enum import Enum from core.api.grpc import core_pb2 from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip +# from coretk.graph_helper import WlanConnection + + +class SessionStateEnum(Enum): + NONE = "none" + DEFINITION = "definition" + CONFIGURATION = "configuration" + RUNTIME = "runtime" + DATACOLLECT = "datacollect" + SHUTDOWN = "shutdown" + INSTANTIATION = "instantiation" + class CoreToolbar(object): """ @@ -62,7 +75,7 @@ class CoreToolbar(object): if self.marker_option_menu and self.marker_option_menu.winfo_exists(): self.marker_option_menu.destroy() - def destroy_children_widgets(self, parent): + def destroy_children_widgets(self): """ Destroy all children of a parent widget @@ -70,7 +83,7 @@ class CoreToolbar(object): :return: nothing """ - for i in parent.winfo_children(): + for i in self.edit_frame.winfo_children(): if i.winfo_name() != "!frame": i.destroy() @@ -150,14 +163,23 @@ class CoreToolbar(object): self.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): + """ + Start session handler: redraw buttons, send node and link messages to grpc server + + :return: nothing + """ logging.debug("Click START STOP SESSION button") - self.destroy_children_widgets(self.edit_frame) + # self.destroy_children_widgets(self.edit_frame) + self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT # set configuration state - if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.SHUTDOWN: - self.canvas.core_grpc.set_session_state("definition") - self.canvas.core_grpc.set_session_state("configuration") + state = self.canvas.core_grpc.get_session_state() + + if state == core_pb2.SessionState.SHUTDOWN: + self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + + self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) for node in self.canvas.grpc_manager.nodes.values(): self.canvas.core_grpc.add_node( @@ -169,8 +191,9 @@ class CoreToolbar(object): edge.id1, edge.id2, edge.type1, edge.type2, edge ) - self.canvas.core_grpc.set_session_state("instantiation") + self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) + # self.canvas.core_grpc.get_session() self.create_runtime_toolbar() def click_link_tool(self): @@ -206,7 +229,7 @@ class CoreToolbar(object): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get(ImageEnum.MDR.value)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MD.value) + self.canvas.draw_node_image = Images.get(ImageEnum.MDR.value) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): @@ -493,6 +516,11 @@ class CoreToolbar(object): CreateToolTip(marker_main_button, "background annotation tools") def create_toolbar(self): + """ + Create buttons for toolbar in edit mode + + :return: nothing + """ self.create_regular_button( self.edit_frame, Images.get(ImageEnum.START.value), @@ -551,9 +579,16 @@ class CoreToolbar(object): menu_button.menu.add_command(label="Edit...") def click_stop_button(self): + """ + redraw buttons on the toolbar, send node and link messages to grpc server + + :return: nothing + """ logging.debug("Click on STOP button ") - self.destroy_children_widgets(self.edit_frame) - self.canvas.core_grpc.set_session_state("datacollect") + # self.destroy_children_widgets(self.edit_frame) + self.destroy_children_widgets() + + self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) self.canvas.core_grpc.delete_links() self.canvas.core_grpc.delete_nodes() self.create_toolbar() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7e69b224..7793880f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,12 +1,13 @@ import enum import logging -import math import tkinter as tk from core.api.grpc import core_pb2 +from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface +from coretk.linkinfo import LinkInfo class GraphMode(enum.Enum): @@ -31,11 +32,17 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None + self.setup_menus() self.setup_bindings() self.draw_grid() - self.core_grpc = grpc + self.grpc_manager = GrpcManager() + self.core_grpc = grpc + + self.helper = GraphHelper(self) + # self.core_id_to_canvas_id = {} + # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() def setup_menus(self): @@ -44,6 +51,31 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Two") self.node_context.add_command(label="Three") + def canvas_reset_and_redraw(self, new_grpc): + """ + Reset the private variables CanvasGraph object, redraw nodes given the new grpc client + :param new_grpc: + :return: + """ + # delete any existing drawn items + self.delete_components() + + # set the private variables to default value + self.mode = GraphMode.SELECT + self.draw_node_image = None + self.draw_node_name = None + self.selected = None + self.node_context = None + self.nodes = {} + self.edges = {} + self.drawing_edge = None + self.grpc_manager = GrpcManager() + + # new grpc + self.core_grpc = new_grpc + + self.draw_existing_component() + def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action @@ -97,7 +129,9 @@ class CanvasGraph(tk.Canvas): image, name = Images.convert_type_and_model_to_image( node.type, node.model ) - n = CanvasNode(node.position.x, node.position.y, image, self, node.id) + n = CanvasNode( + node.position.x, node.position.y, image, name, self, node.id + ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id self.grpc_manager.add_preexisting_node(n, session_id, node, name) @@ -107,12 +141,33 @@ class CanvasGraph(tk.Canvas): for link in session.links: n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] - e = CanvasEdge(n1.x_coord, n1.y_coord, n2.x_coord, n2.y_coord, n1.id, self) + if link.type == core_pb2.LinkType.WIRED: + e = CanvasEdge( + n1.x_coord, + n1.y_coord, + n2.x_coord, + n2.y_coord, + n1.id, + self, + is_wired=True, + ) + elif link.type == core_pb2.LinkType.WIRELESS: + e = CanvasEdge( + n1.x_coord, + n1.y_coord, + n2.x_coord, + n2.y_coord, + n1.id, + self, + is_wired=False, + ) n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + self.helper.redraw_antenna(link, n1, n2) + # TODO add back the link info to grpc manager also redraw grpc_if1 = link.interface_one grpc_if2 = link.interface_two @@ -128,12 +183,11 @@ class CanvasGraph(tk.Canvas): ip6_dst = grpc_if2.ip6 e.link_info = LinkInfo( canvas=self, - edge_id=e.id, + edge=e, ip4_src=ip4_src, ip6_src=ip6_src, ip4_dst=ip4_dst, ip6_dst=ip6_dst, - throughput=None, ) # TODO will include throughput and ipv6 in the future @@ -213,7 +267,6 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.NODE def handle_edge_release(self, event): - print("Calling edge release") edge = self.drawing_edge self.drawing_edge = None @@ -253,14 +306,19 @@ class CanvasGraph(tk.Canvas): # draw link info on the edge if1 = self.grpc_manager.edges[edge.token].interface_1 if2 = self.grpc_manager.edges[edge.token].interface_2 + ip4_and_prefix_1 = None + ip4_and_prefix_2 = None + if if1 is not None: + ip4_and_prefix_1 = if1.ip4_and_prefix + if if2 is not None: + ip4_and_prefix_2 = if2.ip4_and_prefix edge.link_info = LinkInfo( self, - edge.id, - ip4_src=if1.ip4_and_prefix, + edge, + ip4_src=ip4_and_prefix_1, ip6_src=None, - ip4_dst=if2.ip4_and_prefix, + ip4_dst=ip4_and_prefix_2, ip6_dst=None, - throughput=None, ) logging.debug(f"edges: {self.find_withtag('edge')}") @@ -272,7 +330,6 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - print("click on the canvas") logging.debug(f"click press: {event}") selected = self.get_selected(event) is_node = selected in self.find_withtag("node") @@ -302,7 +359,12 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y, image, node_name): if self.selected == 1: node = CanvasNode( - x=x, y=y, image=image, canvas=self, core_id=self.grpc_manager.peek_id() + x=x, + y=y, + image=image, + node_type=node_name, + canvas=self, + core_id=self.grpc_manager.peek_id(), ) self.nodes[node.id] = node self.grpc_manager.add_node( @@ -316,9 +378,9 @@ class CanvasEdge: Canvas edge class """ - width = 1.3 + width = 1.4 - def __init__(self, x1, y1, x2, y2, src, canvas): + def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): """ Create an instance of canvas edge object :param int x1: source x-coord @@ -331,13 +393,28 @@ class CanvasEdge: self.src = src self.dst = None self.canvas = canvas - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) + + if is_wired is None or is_wired is True: + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) + else: + self.id = self.canvas.create_line( + x1, + y1, + x2, + y2, + tags="edge", + width=self.width, + fill="#ff0000", + state=tk.HIDDEN, + ) self.token = None # link info object self.link_info = None + self.throughput = None + self.wired = is_wired # TODO resolve this # self.canvas.tag_lower(self.id) @@ -346,6 +423,7 @@ class CanvasEdge: self.token = tuple(sorted((self.src, self.dst))) x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) + self.canvas.helper.draw_wireless_case(self.src, self.dst, self) self.canvas.lift(self.src) self.canvas.lift(self.dst) @@ -354,8 +432,9 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, canvas, core_id): + def __init__(self, x, y, image, node_type, canvas, core_id): self.image = image + self.node_type = node_type self.canvas = canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" @@ -367,6 +446,8 @@ class CanvasNode: self.text_id = self.canvas.create_text( x, y + 20, text=self.name, tags="nodename" ) + self.antenna_draw = WlanAntennaManager(self.canvas, self.id) + self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) @@ -374,6 +455,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.double_click) self.edges = set() + self.wlans = [] self.moving = None def double_click(self, event): @@ -386,7 +468,6 @@ class CanvasNode: self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): - print("click on the node") logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) # return "break" @@ -410,7 +491,13 @@ class CanvasNode: 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) + self.antenna_draw.update_antennas_position(offset_x, offset_y) + new_x, new_y = self.canvas.coords(self.id) + + if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.RUNTIME: + self.canvas.core_grpc.edit_node(self.core_id, int(new_x), int(new_y)) + for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if x1 == old_x and y1 == old_y: @@ -418,63 +505,11 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() + self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) + + self.canvas.helper.update_wlan_connection( + old_x, old_y, new_x, new_y, self.wlans + ) def context(self, event): logging.debug(f"context click {self.name}: {event}") - - -class LinkInfo: - def __init__(self, canvas, edge_id, ip4_src, ip6_src, ip4_dst, ip6_dst, throughput): - self.canvas = canvas - self.edge_id = edge_id - self.radius = 37 - - self.ip4_address_1 = ip4_src - self.ip6_address_1 = ip6_src - self.ip4_address_2 = ip4_dst - self.ip6_address_2 = ip6_dst - self.throughput = throughput - self.id1 = self.create_edge_src_info() - self.id2 = self.create_edge_dst_info() - - def slope_src_dst(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge_id) - return (y2 - y1) / (x2 - x1) - - def create_edge_src_info(self): - x1, y1, x2, _ = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) - print(self.ip4_address_1) - id1 = self.canvas.create_text( - x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" - ) - return id1 - - def create_edge_dst_info(self): - x1, _, x2, y2 = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) - id2 = self.canvas.create_text( - x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" - ) - return id2 - - def recalculate_info(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge_id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - new_x1 = x1 + distance - new_y1 = y1 + distance * m - new_x2 = x2 - distance - new_y2 = y2 - distance * m - self.canvas.coords(self.id1, new_x1, new_y1) - self.canvas.coords(self.id2, new_x2, new_y2) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py new file mode 100644 index 00000000..2fc6e011 --- /dev/null +++ b/coretk/coretk/graph_helper.py @@ -0,0 +1,198 @@ +""" +Some graph helper functions +""" +import logging +import tkinter as tk + +from core.api.grpc import core_pb2 +from coretk.images import ImageEnum, Images + + +class GraphHelper: + def __init__(self, canvas): + """ + create an instance of GraphHelper object + """ + self.canvas = canvas + + def draw_wireless_case(self, src_id, dst_id, edge): + src_node_name = self.canvas.nodes[src_id].node_type + dst_node_name = self.canvas.nodes[dst_id].node_type + + if src_node_name == "wlan" or dst_node_name == "wlan": + self.canvas.itemconfig(edge.id, state=tk.HIDDEN) + edge.wired = False + if edge.token not in self.canvas.edges: + if src_node_name == "wlan" and dst_node_name == "wlan": + self.canvas.nodes[src_id].antenna_draw.add_antenna() + elif src_node_name == "wlan": + self.canvas.nodes[dst_id].antenna_draw.add_antenna() + else: + self.canvas.nodes[src_id].antenna_draw.add_antenna() + + edge.wired = True + + def redraw_antenna(self, link, node_one, node_two): + if link.type == core_pb2.LinkType.WIRELESS: + if node_one.node_type == "wlan" and node_two.node_type == "wlan": + node_one.antenna_draw.add_antenna() + elif node_one.node_type == "wlan" and node_two.node_type != "wlan": + node_two.antenna_draw.add_antenna() + elif node_one.node_type != "wlan" and node_two.node_type == "wlan": + node_one.antenna_draw.add_antenna() + else: + logging.error( + "graph_helper.py WIRELESS link but both nodes are non-wireless node" + ) + + def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): + for eid in edge_ids: + x1, y1, x2, y2 = self.canvas.coords(eid) + if x1 == old_x and y1 == old_y: + self.canvas.coords(eid, new_x, new_y, x2, y2) + else: + self.canvas.coords(eid, x1, y1, new_x, new_y) + + +class WlanAntennaManager: + def __init__(self, canvas, node_id): + """ + crate an instance for AntennaManager + """ + self.canvas = canvas + self.node_id = node_id + self.quantity = 0 + self._max = 5 + self.antennas = [] + + # distance between each antenna + self.offset = 0 + + def add_antenna(self): + """ + add an antenna to a node + + :return: nothing + """ + if self.quantity < 5: + x, y = self.canvas.coords(self.node_id) + self.antennas.append( + self.canvas.create_image( + x - 16 + self.offset, + y - 16, + anchor=tk.CENTER, + image=Images.get(ImageEnum.ANTENNA.value), + tags="antenna", + ) + ) + self.quantity = self.quantity + 1 + self.offset = self.offset + 8 + + def update_antennas_position(self, offset_x, offset_y): + """ + redraw antennas of a node according to the new node position + + :return: nothing + """ + for i in self.antennas: + self.canvas.move(i, offset_x, offset_y) + + def delete_antenna(self, canvas_id): + return + + def delete_antennas(self): + """ + Delete all the antennas of a node + + :return: nothing + """ + for i in self.antennas: + self.canvas.delete(i) + + +# class WlanConnection: +# def __init__(self, canvas, grpc): +# """ +# create in +# :param canvas: +# """ +# self.canvas = canvas +# self.core_grpc = grpc +# self.throughput_on = False +# self.map_node_link = {} +# self.links = [] +# +# def wireless_nodes(self): +# """ +# retrieve all the wireless clouds in the canvas +# +# :return: list(coretk.graph.CanvasNode) +# """ +# wireless_nodes = [] +# for n in self.canvas.nodes.values(): +# if n.node_type == "wlan": +# wireless_nodes.append(n) +# return wireless_nodes +# +# def draw_wireless_link(self, src, dst): +# """ +# draw a line between 2 nodes that are connected to the same wireless cloud +# +# :param coretk.graph.CanvasNode src: source node +# :param coretk.graph.CanvasNode dst: destination node +# :return: nothing +# """ +# cid = self.canvas.create_line(src.x_coord, src.y_coord, dst.x_coord, dst.y_coord, tags="wlanconnection") +# if src.id not in self.map_node_link: +# self.map_node_link[src.id] = [] +# if dst.id not in self.map_node_link: +# self.map_node_link[dst.id] = [] +# self.map_node_link[src.id].append(cid) +# self.map_node_link[dst.id].append(cid) +# self.links.append(cid) +# +# def subnet_wireless_connection(self, wlan_node): +# """ +# retrieve all the non-wireless nodes connected to wireless_node and create line (represent wireless connection) between each pair of nodes +# :param coretk.grpah.CanvasNode wlan_node: wireless node +# +# :return: nothing +# """ +# non_wlan_nodes = [] +# for e in wlan_node.edges: +# src = self.canvas.nodes[e.src] +# dst = self.canvas.nodes[e.dst] +# if src.node_type == "wlan" and dst.node_type != "wlan": +# non_wlan_nodes.append(dst) +# elif src.node_type != "wlan" and dst.node_type == "wlan": +# non_wlan_nodes.append(src) +# +# size = len(non_wlan_nodes) +# for i in range(size): +# for j in range(i+1, size): +# self.draw_wireless_link(non_wlan_nodes[i], non_wlan_nodes[j]) +# +# def session_wireless_connection(self): +# """ +# draw all the wireless connection in the canvas +# +# :return: nothing +# """ +# wlan_nodes = self.wireless_nodes() +# for n in wlan_nodes: +# self.subnet_wireless_connection(n) +# +# def show_links(self): +# """ +# show all the links +# """ +# for l in self.links: +# self.canvas.itemconfig(l, state=tk.NORMAL) +# +# def hide_links(self): +# """ +# hide all the links +# :return: +# """ +# for l in self.links: +# self.canvas.itemconfig(l, state=tk.HIDDEN) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index ea1ee947..20f1a74a 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -5,6 +5,7 @@ that can be useful for grpc, acts like a session class import logging from core.api.grpc import core_pb2 +from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -61,8 +62,13 @@ class GrpcManager: self.reusable = [] self.preexisting = [] + # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() + # map tuple(core_node_id, interface_id) to and edge + # self.node_id_and_interface_to_edge_token = {} + self.core_mapping = CoreToCanvasMapping() + def peek_id(self): """ Peek the next id to be used @@ -117,8 +123,11 @@ class GrpcManager: node_model = name else: logging.error("grpcmanagemeny.py INVALID node name") - create_node = Node(session_id, self.get_id(), node_type, node_model, x, y, name) + nid = self.get_id() + create_node = Node(session_id, nid, node_type, node_model, x, y, name) self.nodes[canvas_id] = create_node + self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) + # self.core_id_to_canvas_id[nid] = canvas_id logging.debug( "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", session_id, @@ -219,7 +228,7 @@ class GrpcManager: dst_node = self.nodes[dst_canvas_id] if dst_node.model in network_layer_nodes: ifid = len(dst_node.interfaces) - name = "veth" + str(ifid) + name = "eth" + str(ifid) dst_interface = Interface( name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) ) @@ -232,6 +241,7 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface + return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): """ @@ -253,7 +263,37 @@ class GrpcManager: self.nodes[canvas_id_2].type, ) self.edges[token] = edge - self.create_interface(edge, canvas_id_1, canvas_id_2) + src_interface, dst_interface = self.create_interface( + edge, canvas_id_1, canvas_id_2 + ) + node_one_id = self.nodes[canvas_id_1].node_id + node_two_id = self.nodes[canvas_id_2].node_id + + # provide a way to get an edge from a core node and an interface id + if src_interface is not None: + # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_one_id, src_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_one_id, + src_interface.id, + token, + ) + + if dst_interface is not None: + # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_two_id, dst_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_two_id, + dst_interface.id, + token, + ) + logging.debug("Adding edge to grpc manager...") else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/icons/antenna.gif b/coretk/coretk/icons/antenna.gif new file mode 100644 index 0000000000000000000000000000000000000000..55814324be26242f061339e6cd85d0ebfa67c8b1 GIT binary patch literal 230 zcmZ?wbhEHb)Me0RIK;{T1Zin$?H&D_H*bFS?Ad=9Q2fWIk*sNCU|^=;l%JZJm(HO0 zlZBCsft^7Iqz9y%fq7=dt~>t>PI<20Yw>z*%LIA$wu$OaDpThjU$rSV+3545te3L4 z4c2bo87$x;EGEftbE0tw!@SeeF1l5zS^GY_bn3O=?wHJw67KH-e4D?=Sg*L9zEOBr z-}0AbT~_ipKg-wE^ENhdx3uO}bToH$b2Cop7bHOy`C3Coz bFfU_Yv1-jSmJO>mux#46e%q>aP6lfLpq*ob literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/document-new.gif b/coretk/coretk/icons/document-new.gif new file mode 100644 index 0000000000000000000000000000000000000000..570b45e690fa2f57a8320af5433809beed01d541 GIT binary patch literal 1054 zcmeIx>q}Ds7{~FmF->Kani;e#Z&XAOg>+ew_Zuc8i1?;bEX!U9zNjpUw3a!uU36|< z(sa|glxjv4lpvU-Xa(l9ZSJ^pKik>PHgBanJ6rRA=w)9%Jb%Csp4|ML(`U*ffk=Rq z0nL)eW{LQ@G^j};dnT1P%UUGzR!K{nw6#sDP|B4msj|IA+1{$^RCIJHySi1~FFShr zx_Z?w)crjgZSSy7G%(mVIHcAM_3MTQUJVZnz1F^Ys~Y?Cz%+C5*SAXA0MG^wZQ#-d z9&HGr=Xvz}HhMmso{w-XL{JMyJjO`3>A=e3zEv=B6--)%&i_G9E?T}Xfum;2f(e{k zK}s>J+6*pGa3ckatxHpjOU5;<#ECrF#Iq6TGiWtftR@E{rx3+De%VXpSRnH{eg%X~ zAY}HExhOn|z|))L1uJYJ(T|L+fWowltq?*?R>b1Kv=9Q?un!Pwflv@eK@9teU{(}| zP#neyBStKu1dI_@jDQK!h?5{j!Z?W%wjhiQvIpU|Ad0Y4B;~MCPP@ZtchZ!TcF-<| z%k6ZzX}84CTk2yI%@(8`A0pD`}lKbW&+-VurqU z*^`kZtp0H|{dnR;UuHs9dt_<4T#$09NfDN;)5fPAl=w6<9e>ZE)I8wb-8o+C!(Gu; zMdO*d%~6kU`lAvur&GhlGOWEG4HakC0U;+iqUo_Paf3mhUNIS-k(?JJT+WIYWfk1x zM@5VBnv!#h>uNXRXJZeS=!>hOuTAexjomNIOZhsZI#xE-7+w)N`{*e@W+Y!zxm|Fo z+B}-Cfcm&OHPD1S>H3a1;jO)KwT-<+-NHK_%=Be&L&M!EFxITuW+zD5+WXsuz1}(w Nm&*gnpz<`}>|gl)4y^zH literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/edit-delete.gif b/coretk/coretk/icons/edit-delete.gif new file mode 100644 index 0000000000000000000000000000000000000000..d23f758c7ddf6f31eb685d8d30930ae6017d500f GIT binary patch literal 1006 zcmZ?wbhEHb6krfwc;3#SqOPv4qo<*(uc>FCZD^!zXsl;qqGw`eXkuh+ZenC%VQOh< zZe?Ll3!6;)m&HC-rC;Y-q_XA*4fqE)7jqL z(bCh|+S}FH+uPj_ME#wUCiG6}pEPyq;;P#E?lx?!SW@GRxDVwV)3f=%U5q$v3C82^&2;=+q`!D*3BEXZP~nI z!{!~Ex9-}yZTIHwyLa#0y?fXG?RyUF-+y5LzJvSsA3S*I=#k^6j+{Ps{Pelgr_Y@| zeg5>B^XJc8IDh`qg^O1%UAlVq($&kCu3frv{p!^l*RI~Ue(mO^Yd0@nyLtWQ?MpXr zU%7Gn`i&~^i_io<3f9L*#8xI~mdi?ak_?%XjbJeE9JC{fBoSKD_<%`Qzs=AHIBf|K-ca|D%9jA)xq^g^`QlKZ6bf5PpV3N^QRbX&vc*H5LFT%M>QaGKnot4Mr zibW8c*9qoImY*Ay)4iuC`K?fR`DxR+c}~qdf=ox$efcB}b=G$6JKk1V5QxKE#`RT&>|v=%>_EGRItEZYX>VOJtMJI25sS(Icmq NtJ&5n@o+F$0|0Y`xn=+W literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/fileopen.gif b/coretk/coretk/icons/fileopen.gif new file mode 100644 index 0000000000000000000000000000000000000000..fb74420765af2473e13624376b818542d7c92349 GIT binary patch literal 1095 zcmZ?wbhEHb6krfw_|C`x1pi3{4Iq#JN1>sicnm11NGhvLEw4(0qRKQNx2PlqmzL6s zP?p&Ld6P1`8wErMb0Z?|^!Gl}2?_9Nd?VRN+rY&1BZOO7JOO{Sqyma!y#gi5; znz(uEio~R#@CdKNM|bSlwR-2SRX}q4j+NVXtlYkR<(6&BK{390$%c)K*Kb_3X59iH z1E@GC$aVJYjx}p$tzA2N-MTsJ*U#IqVgAOA3)ioozjp1MRjXz$Up{^5(#eY!P3Y;V z3Ji4i^K?e0?1hf3koxfet7qfbs+b$3F%}76F3>2S#Rg0hNFa4UWw0 z{0c4;3>uwSxy21qE^KIYX6Ki8Xb~`Ma^VzK4_FegvB{NNTtDH+1jA-GUTO1!Cl@w0 zyYtK2H?RmAwRi|ByH8LF+|=SJtnRfNoSC% z_jH5QOItdF)&1u?vvNv7Rly3yMUo?hD89j+h0zkyZQ ztS7=S`S=9Y;B7sT#_8u5xK1|fjWW%?z9IGUw%%y-{QCz~TZPU0Vl0cFUsxKvy)V|f d{QZNYlg<0%Y^%S2czStzf4qJDe-;)7YXHy9P`m&D literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index c5df15bb..f865c191 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -87,6 +87,10 @@ class ImageEnum(Enum): STOP = "stop" OBSERVE = "observe" RUN = "run" + DOCUMENTNEW = "document-new" + FILEOPEN = "fileopen" + EDITDELETE = "edit-delete" + ANTENNA = "antenna" def load_core_images(images): diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 8538ab1f..4bad6e99 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -20,6 +20,11 @@ class Interface: self.id = ifid def random_mac_address(self): + """ + create a random MAC address for an interface + + :return: nothing + """ return "02:00:00:%02x:%02x:%02x" % ( random.randint(0, 255), random.randint(0, 255), diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py new file mode 100644 index 00000000..7480e004 --- /dev/null +++ b/coretk/coretk/linkinfo.py @@ -0,0 +1,134 @@ +""" +Link information, such as IPv4, IPv6 and throughput drawn in the canvas +""" +import math + + +class LinkInfo: + def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + self.canvas = canvas + self.edge = edge + # self.edge_id = edge.id + self.radius = 37 + self.core_grpc = self.canvas.core_grpc + + self.ip4_address_1 = ip4_src + self.ip6_address_1 = ip6_src + self.ip4_address_2 = ip4_dst + self.ip6_address_2 = ip6_dst + self.id1 = self.create_edge_src_info() + self.id2 = self.create_edge_dst_info() + + def slope_src_dst(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + if x2 - x1 == 0: + return 9999 + else: + return (y2 - y1) / (x2 - x1) + + def create_edge_src_info(self): + x1, y1, x2, _ = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) + id1 = self.canvas.create_text( + x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + ) + return id1 + + def create_edge_dst_info(self): + x1, _, x2, y2 = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) + id2 = self.canvas.create_text( + x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + ) + return id2 + + def recalculate_info(self): + x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + m = self.slope_src_dst() + distance = math.cos(math.atan(m)) * self.radius + if x1 > x2: + distance = -distance + new_x1 = x1 + distance + new_y1 = y1 + distance * m + new_x2 = x2 - distance + new_y2 = y2 - distance * m + self.canvas.coords(self.id1, new_x1, new_y1) + self.canvas.coords(self.id2, new_x2, new_y2) + + # def link_througput(self): + # x1, y1, x2, y2 = self.canvas.coords(self.edge.id) + # x = (x1 + x2) / 2 + # y = (y1 + y2) / 2 + # tid = self.canvas.create_text(x, y, text="place text here") + # return tid + + +class Throughput: + def __init__(self, canvas, grpc): + self.canvas = canvas + self.core_grpc = grpc + self.grpc_manager = canvas.grpc_manager + + # edge canvas id mapped to throughput value + self.tracker = {} + + # map an edge canvas id to a throughput canvas id + self.map = {} + + def load_throughput_info(self, interface_throughputs): + """ + load all interface throughouts from an event + + :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface throughputs + :return: nothing + """ + for t in interface_throughputs: + nid = t.node_id + iid = t.interface_id + tp = t.throughput + # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] + token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( + nid, iid + ) + edge_id = self.canvas.edges[token].id + if edge_id not in self.tracker: + self.tracker[edge_id] = tp + else: + temp = self.tracker[edge_id] + self.tracker[edge_id] = (temp + tp) / 2 + + def draw_throughputs(self): + for edge_id in self.tracker: + x1, y1, x2, y2 = self.canvas.coords(edge_id) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) + self.map[edge_id] = tp_id + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + + def process_grpc_throughput_event(self, interface_throughputs): + self.load_throughput_info(interface_throughputs) + self.draw_throughputs() + + def update_throughtput_location(self, edge): + tp_id = self.map[edge.id] + x1, y1 = self.canvas.coords(edge.src) + x2, y2 = self.canvas.coords(edge.dst) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + self.canvas.coords(tp_id, x, y) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 6618e532..207a1f3a 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,12 +3,10 @@ The actions taken when each menubar option is clicked """ import logging -import tkinter as tk import webbrowser -from tkinter import messagebox +from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 -from coretk.coregrpc import CoreGrpc SAVEDIR = "/home/ncs/Desktop/" @@ -317,14 +315,6 @@ def session_options(): logging.debug("Click session options") -# def help_core_github(): -# webbrowser.open_new("https://github.com/coreemu/core") -# -# -# def help_core_documentation(): -# webbrowser.open_new("http://coreemu.github.io/core/") - - def help_about(): logging.debug("Click help About") @@ -367,7 +357,8 @@ class MenuAction: grpc.delete_links() grpc.delete_nodes() grpc.delete_session() - + # else: + # grpc.set_session_state("definition") grpc.core.close() # self.application.quit() @@ -384,35 +375,43 @@ class MenuAction: def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") grpc = self.application.core_grpc - file_path = tk.filedialog.asksaveasfilename( + file_path = filedialog.asksaveasfilename( initialdir=SAVEDIR, title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - with open("prev_saved_xml.txt", "a") as file: - file.write(file_path + "\n") + # with open("prev_saved_xml.txt", "a") as file: + # file.write(file_path + "\n") grpc.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") - # grpc = self.application.core_grpc - file_path = tk.filedialog.askopenfilename( + file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), ) - # clean up before openning a new session - # t0 = time.clock() + # clean up before opening a new session self.clean_nodes_links_and_set_configuarations() - grpc = CoreGrpc(self.application.master) - grpc.core.connect() - session_id = grpc.open_xml(file_path) - grpc.session_id = session_id - self.application.core_grpc = grpc - self.application.canvas.core_grpc = grpc - self.application.canvas.delete_components() - self.application.canvas.draw_existing_component() + # grpc = CoreGrpc(self.application.master) + # grpc.core.connect() + core_grpc = self.application.core_grpc + core_grpc.core.connect() + # session_id = core_grpc.open_xml(file_path) + # core_grpc.session_id = session_id + + core_grpc.open_xml(file_path) + # print("Print session state") + # print(grpc.get_session_state()) + self.application.canvas.canvas_reset_and_redraw(core_grpc) + + # Todo might not need + self.application.core_grpc = core_grpc + + self.application.core_editbar.destroy_children_widgets() + self.application.core_editbar.create_runtime_toolbar() + # self.application.canvas.draw_existing_component() # t1 = time.clock() # print(t1 - t0) diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt index 3d6d4d91..bfb9d15a 100644 --- a/coretk/coretk/prev_saved_xml.txt +++ b/coretk/coretk/prev_saved_xml.txt @@ -1,5 +1,2 @@ -/home/ncs/Desktop/random.xml/home/ncs/Desktop/untitled.xml/home/ncs/Desktop/test.xml -/home/ncs/Desktop/test1.xml - -/home/ncs/Desktop/untitled.xml -/home/ncs/Desktop/test1.xml +/home/ncs/Desktop/runningtest.xml +/home/ncs/Desktop/notrunning.xml diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py new file mode 100644 index 00000000..639b4ba5 --- /dev/null +++ b/coretk/coretk/querysessiondrawing.py @@ -0,0 +1,174 @@ +import logging +import tkinter as tk +from tkinter.ttk import Scrollbar, Treeview + +from coretk.images import ImageEnum, Images + + +class SessionTable: + def __init__(self, grpc, master): + self.grpc = grpc + self.selected = False + self.selected_sid = None + self.master = master + self.top = tk.Toplevel(self.master) + self.description_definition() + self.top.title("CORE sessions") + + self.tree = Treeview(self.top) + # self.tree.pack(side=tk.TOP) + self.tree.grid(row=1, column=0, columnspan=2) + self.draw_scrollbar() + self.draw() + + def description_definition(self): + lable = tk.Label( + self.top, + text="Below is a list of active CORE sessions. Double-click to " + "\nconnect to an existing session. Usually, only sessions in " + "\nthe RUNTIME state persist in the daemon, except for the " + "\none you might be concurrently editting.", + ) + lable.grid(sticky=tk.W) + + def column_definition(self): + # self.tree["columns"] = ("name", "nodecount", "filename", "date") + self.tree["columns"] = "nodecount" + self.tree.column("#0", width=300, minwidth=30) + # self.tree.column("name", width=72, miwidth=30) + self.tree.column("nodecount", width=300, minwidth=30) + # self.tree.column("filename", width=92, minwidth=30) + # self.tree.column("date", width=170, minwidth=30) + + def draw_scrollbar(self): + yscrollbar = Scrollbar(self.top, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=1, column=3, sticky=tk.N + tk.S + tk.W) + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = Scrollbar(self.top, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=2, columnspan=2, sticky=tk.E + tk.W + tk.S) + self.tree.configure(xscrollcommand=xscrollbar.set) + + def heading_definition(self): + self.tree.heading("#0", text="ID", anchor=tk.W) + # self.tree.heading("name", text="Name", anchor=tk.CENTER) + self.tree.heading("nodecount", text="Node Count", anchor=tk.W) + # self.tree.heading("filename", text="Filename", anchor=tk.CENTER) + # self.tree.heading("date", text="Date", anchor=tk.CENTER) + + def enter_session(self, sid): + self.top.destroy() + response = self.grpc.core.get_session(sid) + self.grpc.session_id = sid + self.grpc.core.events(sid, self.grpc.log_event) + logging.info("Entering session_id %s.... Result: %s", sid, response) + + def new_session(self): + self.top.destroy() + self.grpc.create_new_session() + + def on_selected(self, event): + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.enter_session(sid) + + def click_select(self, event): + # logging.debug("Click on %s ", event) + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.selected = True + self.selected_sid = sid + + def session_definition(self): + response = self.grpc.core.get_sessions() + # logging.info("querysessiondrawing.py Get all sessions %s", response) + index = 1 + for session in response.sessions: + self.tree.insert( + "", index, None, text=str(session.id), values=(str(session.nodes)) + ) + index = index + 1 + self.tree.bind("", self.on_selected) + self.tree.bind("<>", self.click_select) + + def click_connect(self): + """ + if no session is selected yet, create a new one else join that session + + :return: nothing + """ + if self.selected and self.selected_sid is not None: + self.enter_session(self.selected_sid) + elif not self.selected and self.selected_sid is None: + self.new_session() + else: + logging.error("querysessiondrawing.py invalid state") + + def shutdown_session(self, sid): + self.grpc.terminate_session(sid) + self.new_session() + self.top.destroy() + + def click_shutdown(self): + """ + if no session is currently selected create a new session else shut the selected session down + + :return: nothing + """ + if self.selected and self.selected_sid is not None: + self.shutdown_session(self.selected_sid) + elif not self.selected and self.selected_sid is None: + self.new_session() + else: + logging.error("querysessiondrawing.py invalid state") + # if self.selected and self.selected_sid is not None: + + def draw_buttons(self): + f = tk.Frame(self.top) + f.grid(row=3, sticky=tk.W) + + b = tk.Button( + f, + image=Images.get(ImageEnum.DOCUMENTNEW.value), + text="New", + compound=tk.LEFT, + command=self.new_session, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button( + f, + image=Images.get(ImageEnum.FILEOPEN.value), + text="Connect", + compound=tk.LEFT, + command=self.click_connect, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button( + f, + image=Images.get(ImageEnum.EDITDELETE.value), + text="Shutdown", + compound=tk.LEFT, + command=self.click_shutdown, + ) + b.pack(side=tk.LEFT, padx=3, pady=4) + b = tk.Button(f, text="Cancel", command=self.new_session) + b.pack(side=tk.LEFT, padx=3, pady=4) + + def center(self): + window_width = self.master.winfo_width() + window_height = self.master.winfo_height() + self.top.update() + top_level_width = self.top.winfo_width() + top_level_height = self.top.winfo_height() + x = window_width / 2 - top_level_width / 2 + y = window_height / 2 - top_level_height / 2 + + self.top.geometry("+%d+%d" % (x, y)) + + def draw(self): + self.column_definition() + self.heading_definition() + self.session_definition() + self.draw_buttons() + self.center() + self.top.wait_window() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py new file mode 100644 index 00000000..63042e19 --- /dev/null +++ b/coretk/coretk/wirelessconnection.py @@ -0,0 +1,53 @@ +""" +Wireless connection handler +""" +from core.api.grpc import core_pb2 + + +class WirelessConnection: + def __init__(self, canvas, grpc): + self.canvas = canvas + self.core_grpc = grpc + self.core_mapping = canvas.grpc_manager.core_mapping + + # map a (node_one_id, node_two_id) to a wlan canvas id + self.map = {} + + def add_wlan_connection(self, node_one_id, node_two_id): + canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) + canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + key = tuple(sorted((node_one_id, node_two_id))) + + if key not in self.map: + x1, y1 = self.canvas.coords(canvas_id_one) + x2, y2 = self.canvas.coords(canvas_id_two) + wlan_canvas_id = self.canvas.create_line( + x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 + ) + self.map[key] = wlan_canvas_id + self.canvas.nodes[canvas_id_one].wlans.append(wlan_canvas_id) + self.canvas.nodes[canvas_id_two].wlans.append(wlan_canvas_id) + + def delete_wlan_connection(self, node_one_id, node_two_id): + canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) + canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + + key = tuple(sorted((node_one_id, node_two_id))) + wlan_canvas_id = self.map[key] + + self.canvas.nodes[canvas_id_one].wlans.remove(wlan_canvas_id) + self.canvas.nodes[canvas_id_two].wlans.remove(wlan_canvas_id) + + self.canvas.delete(wlan_canvas_id) + self.map.pop(key, None) + + def hangle_link_event(self, link_event): + if link_event.message_type == core_pb2.MessageType.ADD: + self.add_wlan_connection( + link_event.link.node_one_id, link_event.link.node_two_id + ) + + if link_event.message_type == core_pb2.MessageType.DELETE: + self.delete_wlan_connection( + link_event.link.node_one_id, link_event.link.node_two_id + ) From f18d07985ad1abcc55a1d06e77e6d5d4aec8a5b9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 21 Oct 2019 16:33:18 -0700 Subject: [PATCH 031/305] more work on coretk --- coretk/coretk/app.py | 2 + coretk/coretk/coregrpc.py | 12 ++- coretk/coretk/graph.py | 9 +- coretk/coretk/grpcmanagement.py | 26 ++++- coretk/coretk/interface.py | 57 +++++++++- coretk/coretk/nodeconfigtable.py | 151 +++++++++++++++++++++++++++ coretk/coretk/querysessiondrawing.py | 9 ++ 7 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 coretk/coretk/nodeconfigtable.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index e4f632a7..ec22f2fd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -93,6 +93,8 @@ class Application(tk.Frame): self.core_grpc = CoreGrpc(self) self.core_grpc.set_up() self.canvas.core_grpc = self.core_grpc + self.canvas.grpc_manager.core_grpc = self.core_grpc + self.canvas.grpc_manager.update_preexisting_ids() self.canvas.draw_existing_component() def on_closing(self): diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 095169ed..3d2955dc 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -18,6 +18,9 @@ class CoreGrpc: """ self.core = client.CoreGrpcClient() self.session_id = sid + + self.node_ids = [] + self.master = app.master # self.set_up() @@ -32,8 +35,14 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs + throughputs_belong_to_session = [] + for if_tp in interface_throughputs: + if if_tp.node_id in self.node_ids: + throughputs_belong_to_session.append(if_tp) # bridge_throughputs = event.bridge_throughputs - self.throughput_draw.process_grpc_throughput_event(interface_throughputs) + self.throughput_draw.process_grpc_throughput_event( + throughputs_belong_to_session + ) def create_new_session(self): """ @@ -147,6 +156,7 @@ class CoreGrpc: def add_node(self, node_type, model, x, y, name, node_id): position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + self.node_ids.append(node_id) response = self.core.add_node(self.session_id, node) logging.info("created node: %s", response) if node_type == core_pb2.NodeType.WIRELESS_LAN: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7793880f..d88524bc 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -8,6 +8,7 @@ from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo +from coretk.nodeconfigtable import NodeConfig class GraphMode(enum.Enum): @@ -37,8 +38,8 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid() - self.grpc_manager = GrpcManager() self.core_grpc = grpc + self.grpc_manager = GrpcManager(grpc) self.helper = GraphHelper(self) # self.core_id_to_canvas_id = {} @@ -69,7 +70,8 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.grpc_manager = GrpcManager() + + self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc @@ -463,6 +465,9 @@ class CanvasNode: state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) + else: + print("config table show up") + NodeConfig(self, self.image, self.node_type, self.name) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 20f1a74a..2d5a9027 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -54,14 +54,17 @@ class Edge: class GrpcManager: - def __init__(self): + def __init__(self, grpc): self.nodes = {} self.edges = {} - self.id = 1 + self.id = None # A list of id for re-use, keep in increasing order self.reusable = [] self.preexisting = [] + self.core_grpc = None + + # self.update_preexisting_ids() # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() @@ -69,6 +72,23 @@ class GrpcManager: # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() + def update_preexisting_ids(self): + """ + get preexisting node ids + :return: + """ + max_id = 0 + client = self.core_grpc.core + sessions = client.get_sessions().sessions + for session_summary in sessions: + session = client.get_session(session_summary.id).session + for node in session.nodes: + if node.id > max_id: + max_id = node.id + self.preexisting.append(node.id) + self.id = max_id + 1 + self.update_reusable_id() + def peek_id(self): """ Peek the next id to be used @@ -211,6 +231,8 @@ class GrpcManager: src_interface = None dst_interface = None + self.interfaces_manager.new_subnet() + src_node = self.nodes[src_canvas_id] if src_node.model in network_layer_nodes: ifid = len(src_node.interfaces) diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 4bad6e99..94911745 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -32,10 +32,39 @@ class Interface: ) +class SubnetAddresses: + def __init__(self, network, addresses): + self.network = network + self.address = addresses + self.address_index = 0 + + def get_new_ip_address(self): + ipaddr = self.address[self.address_index] + self.address_index = self.address_index + 1 + return ipaddr + + class InterfaceManager: def __init__(self): - self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) - self.index = 0 + # self.prefix = None + self.core_subnets = list( + ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) + ) + self.subnet_index = 0 + self.address_index = None + + # self.network = ipaddress.ip_network("10.0.0.0/24") + # self.addresses = list(self.network.hosts()) + self.network = None + self.addresses = None + # self.start_interface_manager() + + def start_interface_manager(self): + self.subnet_index = 0 + self.network = self.core_subnets[self.subnet_index] + self.subnet_index = self.subnet_index + 1 + self.addresses = list(self.network.hosts()) + self.address_index = 0 def get_address(self): """ @@ -43,6 +72,24 @@ class InterfaceManager: :return: """ - i = self.index - self.index = self.index + 1 - return self.addresses[i] + # i = self.index + # self.address_index = self.index + 1 + # return self.addresses[i] + ipaddr = self.addresses[self.address_index] + self.address_index = self.address_index + 1 + return ipaddr + + def new_subnet(self): + self.network = self.core_subnets[self.subnet_index] + self.subnet_index = self.subnet_index + 1 + self.addresses = list(self.network.hosts()) + self.address_index = 0 + + # def new_subnet(self): + # """ + # retrieve a new subnet + # :return: + # """ + # if self.prefix is None: + # self.prefix = + # self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py new file mode 100644 index 00000000..51b8314d --- /dev/null +++ b/coretk/coretk/nodeconfigtable.py @@ -0,0 +1,151 @@ +""" +Create toplevel for node configuration +""" +import logging +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") + +NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] +DEFAULTNODES = ["router", "host", "PC"] + + +class NodeConfig: + def __init__(self, canvas_node, image, node_type, name): + self.image = image + self.node_type = node_type + self.name = name + self.canvas_node = canvas_node + + self.top = tk.Toplevel() + self.top.title(node_type + " configuration") + self.namevar = tk.StringVar(self.top, value="default name") + self.name_and_image_definition() + self.type_and_service_definition() + self.select_definition() + + def open_icon_dir(self, toplevel, entry_text): + imgfile = filedialog.askopenfilename( + initialdir=ICONS_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if len(imgfile) > 0: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + lb = toplevel.grid_slaves(1, 0)[0] + lb.configure(image=tk_img) + lb.image = tk_img + entry_text.set(imgfile) + + def click_apply(self, toplevel, entry_text): + print("click apply") + imgfile = entry_text.get() + if imgfile: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + lb = self.top.grid_slaves(row=0, column=3)[0] + lb.configure(image=tk_img) + lb.image = tk_img + self.image = tk_img + toplevel.destroy() + + def img_modification(self): + print("image modification") + t = tk.Toplevel() + t.title(self.name + " image") + + f = tk.Frame(t) + entry_text = tk.StringVar() + image_file_label = tk.Label(f, text="Image file: ") + image_file_label.pack(side=tk.LEFT, padx=2, pady=2) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=60) + image_file_entry.pack(side=tk.LEFT, padx=2, pady=2) + image_file_button = tk.Button( + f, text="...", command=lambda: self.open_icon_dir(t, entry_text) + ) + image_file_button.pack(side=tk.LEFT, padx=2, pady=2) + f.grid(sticky=tk.W + tk.E) + + img = tk.Label(t, image=self.image) + img.grid(sticky=tk.W + tk.E) + + f = tk.Frame(t) + apply_button = tk.Button( + f, text="Apply", command=lambda: self.click_apply(t, entry_text) + ) + apply_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") + apply_to_multiple_button.pack(side=tk.LEFT, padx=2, pady=2) + cancel_button = tk.Button(f, text="Cancel", command=t.destroy) + cancel_button.pack(side=tk.LEFT, padx=2, pady=2) + f.grid(sticky=tk.E + tk.W) + + def name_and_image_definition(self): + name_label = tk.Label(self.top, text="Node name: ") + name_label.grid() + name_entry = tk.Entry(self.top, textvariable=self.namevar) + name_entry.grid(row=0, column=1) + + core_button = tk.Button(self.top, text="None") + core_button.grid(row=0, column=2) + img_button = tk.Button( + self.top, + image=self.image, + width=40, + height=40, + command=self.img_modification, + ) + img_button.grid(row=0, column=3) + + def type_and_service_definition(self): + f = tk.Frame(self.top) + type_label = tk.Label(f, text="Type: ") + type_label.pack(side=tk.LEFT) + + type_button = tk.Button(f, text="None") + type_button.pack(side=tk.LEFT) + + service_button = tk.Button(f, text="Services...") + service_button.pack(side=tk.LEFT) + + f.grid(row=1, column=1, columnspan=2, sticky=tk.W) + + def config_apply(self): + """ + modify image of the canvas node + :return: nothing + """ + logging.debug("nodeconfigtable.py configuration apply") + self.canvas_node.image = self.image + self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + self.top.destroy() + + def config_cancel(self): + """ + save chosen image but not modify canvas node + :return: nothing + """ + logging.debug("nodeconfigtable.py configuration cancel") + self.canvas_node.image = self.image + self.top.destroy() + + def select_definition(self): + f = tk.Frame(self.top) + apply_button = tk.Button(f, text="Apply", command=self.config_apply) + apply_button.pack(side=tk.LEFT) + cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) + cancel_button.pack(side=tk.LEFT) + f.grid(row=3, column=1, sticky=tk.W) + + def network_node_config(self): + self.name_and_image_definition() + self.select_definition() diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py index 639b4ba5..84786ba5 100644 --- a/coretk/coretk/querysessiondrawing.py +++ b/coretk/coretk/querysessiondrawing.py @@ -7,6 +7,11 @@ from coretk.images import ImageEnum, Images class SessionTable: def __init__(self, grpc, master): + """ + create session table instance + :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param root.master master: + """ self.grpc = grpc self.selected = False self.selected_sid = None @@ -22,6 +27,10 @@ class SessionTable: self.draw() def description_definition(self): + """ + write a short description + :return: nothing + """ lable = tk.Label( self.top, text="Below is a list of active CORE sessions. Double-click to " From c1ed7f54d8980cea4a576c88ca68a95a308ca36a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 22 Oct 2019 13:17:47 -0700 Subject: [PATCH 032/305] coretk --- coretk/coretk/coregrpc.py | 5 + coretk/coretk/coremenubar.py | 4 +- coretk/coretk/coretocanvas.py | 1 + coretk/coretk/grpcmanagement.py | 4 +- coretk/coretk/linkinfo.py | 174 ++++++++++++++++++++++++++++---- coretk/coretk/menuaction.py | 5 + coretk/coretk/sizeandscale.py | 86 ++++++++++++++++ 7 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 coretk/coretk/sizeandscale.py diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 3d2955dc..e3e98edd 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -35,6 +35,9 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs + # for i in interface_throughputs: + # print(i) + # return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -245,6 +248,8 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) + self.core.get_node_links(self.session_id, id1) + # def get_session(self): # response = self.core.get_session(self.session_id) # nodes = response.session.nodes diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index f08d18ed..353e0c8a 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -166,7 +166,9 @@ class CoreMenubar(object): canvas_menu.add_separator() - canvas_menu.add_command(label="Size/scale...", command=action.canvas_size_scale) + canvas_menu.add_command( + label="Size/scale...", command=self.menu_action.canvas_size_and_scale + ) canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) canvas_menu.add_separator() diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py index f16c5306..e808735f 100644 --- a/coretk/coretk/coretocanvas.py +++ b/coretk/coretk/coretocanvas.py @@ -8,6 +8,7 @@ class CoreToCanvasMapping: def __init__(self): self.core_id_to_canvas_id = {} self.core_node_and_interface_to_canvas_edge = {} + # self.edge_id_to_canvas_token = {} def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 2d5a9027..ebd34f6c 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -230,7 +230,7 @@ class GrpcManager: """ src_interface = None dst_interface = None - + print("create interface") self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] @@ -263,6 +263,8 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface + print(src_interface) + print(dst_interface) return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 7480e004..c21a56b0 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -1,11 +1,23 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ +import logging import math +WIRELESS_DEF = ["mdr", "wlan"] + class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + """ + create an instance of LinkInfo object + :param tkinter.Canvas canvas: canvas object + :param coretk.graph.CanvasEdge edge: canvas edge onject + :param ip4_src: + :param ip6_src: + :param ip4_dst: + :param ip6_dst: + """ self.canvas = canvas self.edge = edge # self.edge_id = edge.id @@ -20,13 +32,23 @@ class LinkInfo: self.id2 = self.create_edge_dst_info() def slope_src_dst(self): + """ + calculate slope of the line connecting source node to destination node + :rtype: float + :return: slope of line + """ x1, y1, x2, y2 = self.canvas.coords(self.edge.id) if x2 - x1 == 0: - return 9999 + return 9999.0 else: return (y2 - y1) / (x2 - x1) def create_edge_src_info(self): + """ + draw the ip address for source node + + :return: nothing + """ x1, y1, x2, _ = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -39,6 +61,11 @@ class LinkInfo: return id1 def create_edge_dst_info(self): + """ + draw the ip address for destination node + + :return: nothing + """ x1, _, x2, y2 = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -51,6 +78,11 @@ class LinkInfo: return id2 def recalculate_info(self): + """ + move the node info when the canvas node move + + :return: nothing + """ x1, y1, x2, y2 = self.canvas.coords(self.edge.id) m = self.slope_src_dst() distance = math.cos(math.atan(m)) * self.radius @@ -73,6 +105,11 @@ class LinkInfo: class Throughput: def __init__(self, canvas, grpc): + """ + create an instance of Throughput object + :param tkinter.Canvas canvas: canvas object + :param coretk.coregrpc,CoreGrpc grpc: grpc object + """ self.canvas = canvas self.core_grpc = grpc self.grpc_manager = canvas.grpc_manager @@ -83,6 +120,8 @@ class Throughput: # map an edge canvas id to a throughput canvas id self.map = {} + self.edge_id_to_token = {} + def load_throughput_info(self, interface_throughputs): """ load all interface throughouts from an event @@ -98,28 +137,119 @@ class Throughput: token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( nid, iid ) + print(token) edge_id = self.canvas.edges[token].id + + self.edge_id_to_token[edge_id] = token + if edge_id not in self.tracker: self.tracker[edge_id] = tp else: temp = self.tracker[edge_id] self.tracker[edge_id] = (temp + tp) / 2 + def edge_is_wired(self, token): + """ + determine whether link is a WIRED link + + :param token: + :return: + """ + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src_node = self.canvas.nodes[canvas_src_id] + dst_node = self.canvas.nodes[canvas_dst_id] + + if src_node.node_type == "wlan": + if dst_node.node_type == "mdr": + return False + else: + logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") + return True + if dst_node.node_type == "wlan": + if src_node.node_type == "mdr": + return False + else: + logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") + return True + return True + + def draw_wired_throughput(self, edge_id): + + x1, y1, x2, y2 = self.canvas.coords(edge_id) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) + self.map[edge_id] = tp_id + + # redraw throughput + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + + def draw_wireless_throughput(self, edge_id): + token = self.edge_id_to_token[edge_id] + canvas_edge = self.canvas.edges[token] + canvas_src_id = canvas_edge.src + canvas_dst_id = canvas_edge.dst + src_node = self.canvas.nodes[canvas_src_id] + dst_node = self.canvas.nodes[canvas_dst_id] + + # non_wlan_node = None + if src_node.node_type == "wlan": + non_wlan_node = dst_node + else: + non_wlan_node = src_node + + x, y = self.canvas.coords(non_wlan_node.id) + if edge_id not in self.map: + tp_id = self.canvas.create_text( + x + 50, + y + 25, + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + self.map[edge_id] = tp_id + + # redraw throughput + else: + self.canvas.itemconfig( + self.map[edge_id], + text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + ) + def draw_throughputs(self): for edge_id in self.tracker: - x1, y1, x2, y2 = self.canvas.coords(edge_id) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - if edge_id not in self.map: - tp_id = self.canvas.create_text( - x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - ) - self.map[edge_id] = tp_id + if self.edge_is_wired(self.edge_id_to_token[edge_id]): + self.draw_wired_throughput(edge_id) else: - self.canvas.itemconfig( - self.map[edge_id], - text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - ) + self.draw_wireless_throughput(edge_id) + # draw wireless throughput + + # x1, y1, x2, y2 = self.canvas.coords(edge_id) + # x = (x1 + x2) / 2 + # y = (y1 + y2) / 2 + # + # print(self.is_wired(self.edge_id_to_token[edge_id])) + # # new throughput + # if edge_id not in self.map: + # tp_id = self.canvas.create_text( + # x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + # ) + # self.map[edge_id] = tp_id + # + # # redraw throughput + # else: + # self.canvas.itemconfig( + # self.map[edge_id], + # text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), + # ) def process_grpc_throughput_event(self, interface_throughputs): self.load_throughput_info(interface_throughputs) @@ -127,8 +257,16 @@ class Throughput: def update_throughtput_location(self, edge): tp_id = self.map[edge.id] - x1, y1 = self.canvas.coords(edge.src) - x2, y2 = self.canvas.coords(edge.dst) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.canvas.coords(tp_id, x, y) + if self.edge_is_wired(self.edge_id_to_token[edge.id]): + x1, y1 = self.canvas.coords(edge.src) + x2, y2 = self.canvas.coords(edge.dst) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + self.canvas.coords(tp_id, x, y) + else: + if self.canvas.nodes[edge.src].node_type == "wlan": + x, y = self.canvas.coords(edge.dst) + self.canvas.coords(tp_id, x + 50, y + 20) + else: + x, y = self.canvas.coords(edge.src) + self.canvas.coords(tp_id, x + 50, y + 25) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 207a1f3a..e7e1aafc 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.sizeandscale import SizeAndScale SAVEDIR = "/home/ncs/Desktop/" @@ -153,6 +154,7 @@ def canvas_delete(): def canvas_size_scale(): logging.debug("Click canvas size/scale") + SizeAndScale() def canvas_wallpaper(): @@ -415,6 +417,9 @@ class MenuAction: # t1 = time.clock() # print(t1 - t0) + def canvas_size_and_scale(self): + SizeAndScale() + def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py new file mode 100644 index 00000000..e9485892 --- /dev/null +++ b/coretk/coretk/sizeandscale.py @@ -0,0 +1,86 @@ +""" +size and scale +""" + +import tkinter as tk + + +class SizeAndScale: + def __init__(self): + self.top = tk.Toplevel() + self.top.title("Canvas Size and Scale") + self.size_chart() + + self.pixel_width_text = None + + def click_scrollbar(self, e1, e2, e3): + print(e1, e2, e3) + + def create_text_label(self, frame, text, row, column): + text_label = tk.Label(frame, text=text) + text_label.grid(row=row, column=column) + + def size_chart(self): + f = tk.Frame(self.top) + t = tk.Label(f, text="Size") + t.grid(row=0, column=0, sticky=tk.W) + + scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1) + e = tk.Entry(f, text="1000", xscrollcommand=scrollbar.set) + e.focus() + e.grid(row=1, column=0) + scrollbar.config(command=self.click_scrollbar) + + # l = tk.Label(f, text="W") + # l.grid(row=1, column=2) + # l = tk.Label(f, text=" X ") + # l.grid(row=1, column=3) + self.create_text_label(f, "W", 1, 2) + self.create_text_label(f, " X ", 1, 3) + + hpixel_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + hpixel_scrollbar.grid(row=1, column=5) + + hpixel_entry = tk.Entry(f, text="750", xscrollcommand=hpixel_scrollbar.set) + hpixel_entry.focus() + hpixel_entry.grid(row=1, column=4) + + h_label = tk.Label(f, text="H") + h_label.grid(row=1, column=6) + + self.create_text_label(f, "pixels", 1, 7) + # pixel_label = tk.Label(f, text="pixels") + # pixel_label.grid(row=1, column=7) + + wmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + wmeter_scrollbar.grid(row=2, column=2) + + wmeter_entry = tk.Entry(f, text="1500.0", xscrollcommand=wmeter_scrollbar.set) + wmeter_entry.focus() + wmeter_entry.grid(row=2, column=0, columnspan=2, sticky=tk.W + tk.E) + + # l = tk.Label(f, text=" X ") + # l.grid(row=2, column=3) + self.create_text_label(f, " X ", row=2, column=3) + + # f1 = tk.Frame(f) + hmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) + hmeter_scrollbar.grid(row=2, column=6) + + hmeter_entry = tk.Entry(f, text="1125.0", xscrollcommand=hmeter_scrollbar.set) + hmeter_entry.focus() + hmeter_entry.grid(row=2, column=4, columnspan=2, sticky=tk.W + tk.E) + + self.create_text_label(f, "pixels", 2, 7) + # pixel_label = tk.Label(f, text="pixels") + # pixel_label.grid(row=2, column=7) + # hmeter_entry.pack(side=tk.LEFT) + # + # hmeter_scrollbar = tk.Scrollbar(hmeter_entry, orient=tk.VERTICAL) + # hmeter_scrollbar.pack(side=tk.LEFT) + # f1.grid(row=2, column=4) + + f.grid() + + # def scale_chart(self): From 5829e3ae429ef295c28b368304862d2ef46fdc95 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 24 Oct 2019 16:50:24 -0700 Subject: [PATCH 033/305] more work on coretk --- coretk/coretk/app.py | 2 +- coretk/coretk/coregrpc.py | 9 +- coretk/coretk/coremenubar.py | 4 +- coretk/coretk/graph.py | 19 +- coretk/coretk/menuaction.py | 6 +- coretk/coretk/nodeconfigtable.py | 9 +- coretk/coretk/setwallpaper.py | 294 +++++++++++++++++++++++++ coretk/coretk/sizeandscale.py | 279 ++++++++++++++++++----- coretk/coretk/wallpaper/sample1-bg.gif | Bin 0 -> 319126 bytes coretk/coretk/wallpaper/sample4-bg.jpg | Bin 0 -> 201030 bytes 10 files changed, 548 insertions(+), 74 deletions(-) create mode 100644 coretk/coretk/setwallpaper.py create mode 100644 coretk/coretk/wallpaper/sample1-bg.gif create mode 100644 coretk/coretk/wallpaper/sample4-bg.jpg diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index ec22f2fd..1f85fb9f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -59,7 +59,7 @@ class Application(tk.Frame): master=self, grpc=self.core_grpc, background="#cccccc", - scrollregion=(0, 0, 1000, 1000), + scrollregion=(0, 0, 1200, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index e3e98edd..047e7c92 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -35,9 +35,9 @@ class CoreGrpc: def log_throughput(self, event): interface_throughputs = event.interface_throughputs - # for i in interface_throughputs: - # print(i) - # return + for i in interface_throughputs: + print("") + return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -58,8 +58,9 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id + self.master.title("CORE Session ID " + str(self.session_id)) self.core.events(self.session_id, self.log_event) - self.core.throughputs(self.log_throughput) + # self.core.throughputs(self.log_throughput) def query_existing_sessions(self, sessions): """ diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 353e0c8a..a9780072 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -169,7 +169,9 @@ class CoreMenubar(object): canvas_menu.add_command( label="Size/scale...", command=self.menu_action.canvas_size_and_scale ) - canvas_menu.add_command(label="Wallpaper...", command=action.canvas_wallpaper) + canvas_menu.add_command( + label="Wallpaper...", command=self.menu_action.canvas_set_wallpaper + ) canvas_menu.add_separator() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d88524bc..9f4b87db 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -34,6 +34,8 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None + self.grid = None + self.meters_per_pixel = 1.5 self.setup_menus() self.setup_bindings() self.draw_grid() @@ -89,7 +91,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_motion) self.bind("", self.context) - def draw_grid(self, width=1000, height=750): + def draw_grid(self, width=1000, height=800): """ Create grid @@ -98,7 +100,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - rectangle_id = self.create_rectangle( + self.grid = self.create_rectangle( 0, 0, width, @@ -108,11 +110,11 @@ class CanvasGraph(tk.Canvas): width=1, tags="rectangle", ) - self.tag_lower(rectangle_id) + self.tag_lower(self.grid) for i in range(0, width, 27): - self.create_line(i, 0, i, height, dash=(2, 4), tags="grid line") + self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): - self.create_line(0, i, width, i, dash=(2, 4), tags="grid line") + self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") def draw_existing_component(self): """ @@ -235,6 +237,7 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + print(overlapping) nodes = set(self.find_withtag("node")) selected = None for _id in overlapping: @@ -260,6 +263,7 @@ class CanvasGraph(tk.Canvas): self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") + print(self.mode) if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -359,7 +363,8 @@ class CanvasGraph(tk.Canvas): self.node_context.post(event.x_root, event.y_root) def add_node(self, x, y, image, node_name): - if self.selected == 1: + plot_id = self.find_all()[0] + if self.selected == plot_id: node = CanvasNode( x=x, y=y, @@ -510,7 +515,7 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() - self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) + # self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) self.canvas.helper.update_wlan_connection( old_x, old_y, new_x, new_y, self.wlans diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index e7e1aafc..4a1332d2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale SAVEDIR = "/home/ncs/Desktop/" @@ -418,7 +419,10 @@ class MenuAction: # print(t1 - t0) def canvas_size_and_scale(self): - SizeAndScale() + SizeAndScale(self.application) + + def canvas_set_wallpaper(self): + CanvasWallpaper(self.application) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py index 51b8314d..76550289 100644 --- a/coretk/coretk/nodeconfigtable.py +++ b/coretk/coretk/nodeconfigtable.py @@ -30,7 +30,7 @@ class NodeConfig: self.select_definition() def open_icon_dir(self, toplevel, entry_text): - imgfile = filedialog.askopenfilename( + filename = filedialog.askopenfilename( initialdir=ICONS_DIR, title="Open", filetypes=( @@ -38,16 +38,15 @@ class NodeConfig: ("All Files", "*"), ), ) - if len(imgfile) > 0: - img = Image.open(imgfile) + if len(filename) > 0: + img = Image.open(filename) tk_img = ImageTk.PhotoImage(img) lb = toplevel.grid_slaves(1, 0)[0] lb.configure(image=tk_img) lb.image = tk_img - entry_text.set(imgfile) + entry_text.set(filename) def click_apply(self, toplevel, entry_text): - print("click apply") imgfile = entry_text.get() if imgfile: img = Image.open(imgfile) diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py new file mode 100644 index 00000000..a4a8c908 --- /dev/null +++ b/coretk/coretk/setwallpaper.py @@ -0,0 +1,294 @@ +""" +set wallpaper +""" +import enum +import logging +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +WALLPAPER_DIR = os.path.join(PATH, "wallpaper") + + +class ScaleOption(enum.Enum): + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + +class CanvasWallpaper: + def __init__(self, application): + self.application = application + self.canvas = self.application.canvas + + self.top = tk.Toplevel() + self.top.title("Set Canvas Wallpaper") + self.radiovar = tk.IntVar() + self.show_grid_var = tk.IntVar() + self.adjust_to_dim_var = tk.IntVar() + self.wallpaper = None + + self.create_image_label() + self.create_text_label() + self.open_image() + self.display_options() + self.additional_options() + self.apply_cancel() + + def create_image_label(self): + image_label = tk.Label( + self.top, text="(image preview)", height=8, width=32, bg="white" + ) + image_label.grid(pady=5) + + def create_text_label(self): + text_label = tk.Label(self.top, text="Image filename: ") + text_label.grid() + + def open_image_link(self): + filename = filedialog.askopenfilename( + initialdir=WALLPAPER_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + + # fill the file name into the file name entry + img_open_frame = self.top.grid_slaves(2, 0)[0] + filename_entry = img_open_frame.grid_slaves(0, 0)[0] + filename_entry.delete(0, tk.END) + filename_entry.insert(tk.END, filename) + + # display that onto the label + img_label = self.top.grid_slaves(0, 0)[0] + if filename: + img = Image.open(filename) + img = img.resize((250, 135), Image.ANTIALIAS) + tk_img = ImageTk.PhotoImage(img) + img_label.config(image=tk_img, width=250, height=135) + img_label.image = tk_img + + def clear_link(self): + # delete entry + img_open_frame = self.top.grid_slaves(2, 0)[0] + filename_entry = img_open_frame.grid_slaves(0, 0)[0] + filename_entry.delete(0, tk.END) + + # delete display image + img_label = self.top.grid_slaves(0, 0)[0] + img_label.config(image="", width=32, height=8) + + def open_image(self): + f = tk.Frame(self.top) + + var = tk.StringVar(f, value="") + e = tk.Entry(f, textvariable=var) + e.focus() + e.grid() + + b = tk.Button(f, text="...", command=self.open_image_link) + b.grid(row=0, column=1) + + b = tk.Button(f, text="Clear", command=self.clear_link) + b.grid(row=0, column=2) + + f.grid() + + def display_options(self): + f = tk.Frame(self.top) + + b1 = tk.Radiobutton(f, text="upper-left", value=1, variable=self.radiovar) + b1.grid(row=0, column=0) + + b2 = tk.Radiobutton(f, text="centered", value=2, variable=self.radiovar) + b2.grid(row=0, column=1) + + b3 = tk.Radiobutton(f, text="scaled", value=3, variable=self.radiovar) + b3.grid(row=0, column=2) + + b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) + b4.grid(row=0, column=3) + + self.radiovar.set(1) + + f.grid() + + def additional_options(self): + b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) + b.grid(sticky=tk.W, padx=5) + b = tk.Checkbutton( + self.top, + text="Adjust canvas size to image dimensions", + variable=self.adjust_to_dim_var, + ) + b.grid(sticky=tk.W, padx=5) + self.show_grid_var.set(1) + self.adjust_to_dim_var.set(0) + + def delete_previous_wallpaper(self): + prev_wallpaper = self.canvas.find_withtag("wallpaper") + if prev_wallpaper: + for i in prev_wallpaper: + self.canvas.delete(i) + + def get_canvas_width_and_height(self): + """ + retrieve canvas width and height in pixels + + :return: nothing + """ + canvas = self.application.canvas + grid = canvas.find_withtag("rectangle")[0] + x0, y0, x1, y1 = canvas.coords(grid) + canvas_w = abs(x0 - x1) + canvas_h = abs(y0 - y1) + return canvas_w, canvas_h + + def determine_cropped_image_dimension(self): + """ + determine the dimension of the image after being cropped + + :return: nothing + """ + return + + def upper_left(self, img): + tk_img = ImageTk.PhotoImage(img) + + # crop image if it is bigger than canvas + canvas_w, canvas_h = self.get_canvas_width_and_height() + + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + + if img_w > canvas_w: + + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + cropped = img.crop((0, 0, cropx, cropy)) + cropped_tk = ImageTk.PhotoImage(cropped) + + # place left corner of image to the left corner of the canvas + self.application.croppedwallpaper = cropped_tk + + self.delete_previous_wallpaper() + + wid = self.canvas.create_image( + (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def center(self, img): + """ + place the image at the center of canvas + + :param Image img: image object + :return: nothing + """ + tk_img = ImageTk.PhotoImage(img) + canvas_w, canvas_h = self.get_canvas_width_and_height() + + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + + # dimension of the cropped image + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + + x0 = (img_w - cropx) / 2 + y0 = (img_h - cropy) / 2 + x1 = x0 + cropx + y1 = y0 + cropy + cropped = img.crop((x0, y0, x1, y1)) + cropped_tk = ImageTk.PhotoImage(cropped) + + # place the center of the image at the center of the canvas + self.application.croppedwallpaper = cropped_tk + self.delete_previous_wallpaper() + wid = self.canvas.create_image( + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def scaled(self, img): + """ + scale image based on canvas dimension + + :param Image img: image object + :return: nothing + """ + canvas_w, canvas_h = self.get_canvas_width_and_height() + resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) + image_tk = ImageTk.PhotoImage(resized_image) + self.application.croppedwallpaper = image_tk + + self.delete_previous_wallpaper() + + wid = self.canvas.create_image( + (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" + ) + self.application.wallpaper_id = wid + + def tiled(self, img): + return + + def show_grid(self): + """ + + :return: nothing + """ + if self.show_grid_var.get() == 0: + for i in self.canvas.find_withtag("gridline"): + self.canvas.itemconfig(i, state=tk.HIDDEN) + elif self.show_grid_var.get() == 1: + for i in self.canvas.find_withtag("gridline"): + self.canvas.itemconfig(i, state=tk.NORMAL) + self.canvas.lift(i) + else: + logging.error("setwallpaper.py show_grid invalid value") + + def click_apply(self): + img_link_frame = self.top.grid_slaves(2, 0)[0] + filename = img_link_frame.grid_slaves(0, 0)[0].get() + if not filename: + self.top.destroy() + return + try: + img = Image.open(filename) + except FileNotFoundError: + print("invalid filename, draw original white plot") + if self.application.wallpaper_id: + self.canvas.delete(self.application.wallpaper_id) + self.top.destroy() + return + if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + self.upper_left(img) + elif self.radiovar.get() == ScaleOption.CENTERED.value: + self.center(img) + elif self.radiovar.get() == ScaleOption.SCALED.value: + self.scaled(img) + elif self.radiovar.get() == ScaleOption.TILED.value: + print("not implemented yet") + + self.show_grid() + self.top.destroy() + + def apply_cancel(self): + f = tk.Frame(self.top) + + b = tk.Button(f, text="Apply", command=self.click_apply) + b.grid(row=0, column=0) + + b = tk.Button(f, text="Cancel", command=self.top.destroy) + b.grid(row=0, column=1) + + f.grid(pady=5) diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index e9485892..44d3adbc 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -1,86 +1,255 @@ """ size and scale """ - import tkinter as tk +from functools import partial + +DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] class SizeAndScale: - def __init__(self): + def __init__(self, application): + """ + create an instance for size and scale object + + :param application: main application + """ + self.application = application self.top = tk.Toplevel() self.top.title("Canvas Size and Scale") + self.meter_per_pixel = self.application.canvas.meters_per_pixel + self.size_chart() + self.scale_chart() + self.reference_point_chart() + self.save_as_default() + self.apply_cancel() - self.pixel_width_text = None + def pixel_scrollbar_command(self, size_frame, entry_row, entry_column, event): + """ + change the value shown based on scrollbar action - def click_scrollbar(self, e1, e2, e3): - print(e1, e2, e3) + :param tkinter.Frame frame: pixel dimension frame + :param int entry_row: row number of entry of the frame + :param int entry_column: column number of entry of the frame + :param event: scrollbar event + :return: nothing + """ + pixel_frame = size_frame.grid_slaves(0, 0)[0] + pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] + val = int(pixel_entry.get()) - def create_text_label(self, frame, text, row, column): + if event == "-1": + new_val = val + 2 + elif event == "1": + new_val = val - 2 + + pixel_entry.delete(0, tk.END) + pixel_entry.insert(tk.END, str(new_val)) + + # change meter dimension + meter_frame = size_frame.grid_slaves(1, 0)[0] + meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] + meter_entry.delete(0, tk.END) + meter_entry.insert(tk.END, str(new_val * self.meter_per_pixel)) + + def meter_scrollbar_command(self, size_frame, entry_row, entry_column, event): + """ + change the value shown based on scrollbar action + + :param tkinter.Frame size_frame: size frame + :param int entry_row: row number of entry in the frame it is contained in + :param int entry_column: column number of entry in the frame in is contained in + :param event: scroolbar event + :return: nothing + """ + meter_frame = size_frame.grid_slaves(1, 0)[0] + meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] + val = float(meter_entry.get()) + + if event == "-1": + val += 100.0 + elif event == "1": + val -= 100.0 + meter_entry.delete(0, tk.END) + meter_entry.insert(tk.END, str(val)) + + # change pixel dimension + pixel_frame = size_frame.grid_slaves(0, 0)[0] + pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] + pixel_entry.delete(0, tk.END) + pixel_entry.insert(tk.END, str(int(val / self.meter_per_pixel))) + + def create_text_label(self, frame, text, row, column, sticky=None): + """ + create text label + :param tkinter.Frame frame: parent frame + :param str text: label text + :param int row: row number + :param int column: column number + :param sticky: sticky value + + :return: nothing + """ text_label = tk.Label(frame, text=text) - text_label.grid(row=row, column=column) + text_label.grid(row=row, column=column, sticky=sticky, padx=3, pady=3) + + def create_entry(self, frame, default_value, row, column, width): + text_var = tk.StringVar(frame, value=str(default_value)) + entry = tk.Entry( + frame, textvariable=text_var, width=width, bg="white", state=tk.NORMAL + ) + entry.focus() + entry.grid(row=row, column=column, padx=3, pady=3) def size_chart(self): - f = tk.Frame(self.top) - t = tk.Label(f, text="Size") - t.grid(row=0, column=0, sticky=tk.W) + label = tk.Label(self.top, text="Size") + label.grid(sticky=tk.W, padx=5) - scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1) - e = tk.Entry(f, text="1000", xscrollcommand=scrollbar.set) - e.focus() - e.grid(row=1, column=0) - scrollbar.config(command=self.click_scrollbar) + canvas = self.application.canvas + plot = canvas.find_withtag("rectangle") + x0, y0, x1, y1 = canvas.bbox(plot[0]) + w = abs(x0 - x1) - 2 + h = abs(y0 - y1) - 2 - # l = tk.Label(f, text="W") - # l.grid(row=1, column=2) - # l = tk.Label(f, text=" X ") - # l.grid(row=1, column=3) - self.create_text_label(f, "W", 1, 2) - self.create_text_label(f, " X ", 1, 3) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) - hpixel_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - hpixel_scrollbar.grid(row=1, column=5) + f1 = tk.Frame(f) + pw_scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) + pw_scrollbar.grid(row=0, column=1) + self.create_entry(f1, w, 0, 0, 6) + pw_scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 0)) - hpixel_entry = tk.Entry(f, text="750", xscrollcommand=hpixel_scrollbar.set) - hpixel_entry.focus() - hpixel_entry.grid(row=1, column=4) + self.create_text_label(f1, " W x ", 0, 2) - h_label = tk.Label(f, text="H") - h_label.grid(row=1, column=6) + scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=4) + self.create_entry(f1, h, 0, 3, 6) + scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 3)) + self.create_text_label(f1, " H pixels ", 0, 7) + f1.grid(sticky=tk.W, pady=3) - self.create_text_label(f, "pixels", 1, 7) - # pixel_label = tk.Label(f, text="pixels") - # pixel_label.grid(row=1, column=7) + f2 = tk.Frame(f) + scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1) + self.create_entry(f2, w * self.meter_per_pixel, 0, 0, 8) + scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 0)) + self.create_text_label(f2, " x ", 0, 2) - wmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - wmeter_scrollbar.grid(row=2, column=2) + scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=4) + self.create_entry(f2, h * self.meter_per_pixel, 0, 3, 8) + scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 3)) + self.create_text_label(f2, " meters ", 0, 5) - wmeter_entry = tk.Entry(f, text="1500.0", xscrollcommand=wmeter_scrollbar.set) - wmeter_entry.focus() - wmeter_entry.grid(row=2, column=0, columnspan=2, sticky=tk.W + tk.E) + f2.grid(sticky=tk.W, pady=3) - # l = tk.Label(f, text=" X ") - # l.grid(row=2, column=3) - self.create_text_label(f, " X ", row=2, column=3) + f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) + def scale_chart(self): + label = tk.Label(self.top, text="Scale") + label.grid(padx=5, sticky=tk.W) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + # self.create_text_label(f, "Scale", 0, 0, tk.W) # f1 = tk.Frame(f) - hmeter_scrollbar = tk.Scrollbar(f, orient=tk.VERTICAL) - hmeter_scrollbar.grid(row=2, column=6) + self.create_text_label(f, "100 pixels = ", 0, 0) + self.create_entry(f, self.meter_per_pixel * 100, 0, 1, 10) + self.create_text_label(f, "meters", 0, 2) + # f1.grid(sticky=tk.W, pady=3) + f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - hmeter_entry = tk.Entry(f, text="1125.0", xscrollcommand=hmeter_scrollbar.set) - hmeter_entry.focus() - hmeter_entry.grid(row=2, column=4, columnspan=2, sticky=tk.W + tk.E) + def reference_point_chart(self): + label = tk.Label(self.top, text="Reference point") + label.grid(padx=5, sticky=tk.W) - self.create_text_label(f, "pixels", 2, 7) - # pixel_label = tk.Label(f, text="pixels") - # pixel_label.grid(row=2, column=7) - # hmeter_entry.pack(side=tk.LEFT) - # - # hmeter_scrollbar = tk.Scrollbar(hmeter_entry, orient=tk.VERTICAL) - # hmeter_scrollbar.pack(side=tk.LEFT) - # f1.grid(row=2, column=4) + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + self.create_text_label( + f, + "The default reference point is (0, 0), the upper left corner of the canvas.", + 1, + 0, + tk.W, + ) + f1 = tk.Frame(f) + self.create_entry(f1, 0, 0, 0, 4) + self.create_text_label(f1, " X, ", 0, 1) + self.create_entry(f1, 0, 0, 2, 4) + self.create_text_label(f1, "Y = ", 0, 3) + self.create_entry(f1, 47.5791667, 0, 4, 13) + self.create_text_label(f1, " lat, ", 0, 5) + self.create_entry(f1, -122.132322, 0, 6, 13) + self.create_text_label(f1, "long", 0, 7) + f1.grid(row=2, column=0, sticky=tk.W, pady=3) - f.grid() + f2 = tk.Frame(f) + self.create_text_label(f2, "Altitude: ", 0, 0) + self.create_entry(f2, 2.0, 0, 1, 11) + self.create_text_label(f2, " meters ", 0, 2) + f2.grid(row=3, column=0, sticky=tk.W, pady=3) - # def scale_chart(self): + f.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) + + def save_as_default(self): + var = tk.IntVar() + button = tk.Checkbutton(self.top, text="Save as default", variable=var) + button.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) + + def redraw_grid(self, pixel_width, pixel_height): + """ + redraw grid with new dimension + + :param int pixel_width: width in pixel + :param int pixel_height: height in pixel + :return: nothing + """ + canvas = self.application.canvas + canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) + + # delete old plot and redraw + for i in canvas.find_withtag("gridline"): + canvas.delete(i) + for i in canvas.find_withtag("rectangle"): + canvas.delete(i) + + canvas.draw_grid(width=pixel_width, height=pixel_height) + # lift anything that is drawn on the plot before + for tag in DRAW_OBJECT_TAGS: + for i in canvas.find_withtag(tag): + canvas.lift(i) + + def click_apply(self): + size_frame = self.top.grid_slaves(1, 0)[0] + pixel_size_frame = size_frame.grid_slaves(0, 0)[0] + + pixel_width = int(pixel_size_frame.grid_slaves(0, 0)[0].get()) + pixel_height = int(pixel_size_frame.grid_slaves(0, 3)[0].get()) + + scale_frame = self.top.grid_slaves(3, 0)[0] + meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 + self.application.canvas.meters_per_pixel = meter_per_pixel + self.redraw_grid(pixel_width, pixel_height) + self.top.destroy() + + def apply_cancel(self): + apply_button = tk.Button(self.top, text="Apply", command=self.click_apply) + apply_button.grid(row=7, column=0, pady=5) + cancel_button = tk.Button(self.top, text="Cancel", command=self.top.destroy) + cancel_button.grid(row=7, column=1, pady=5) diff --git a/coretk/coretk/wallpaper/sample1-bg.gif b/coretk/coretk/wallpaper/sample1-bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..98344744698658e0fdbc19a8be28f9bf065018c5 GIT binary patch literal 319126 zcmWiei9ZvL|Ho&W9oX93=4dm=-1iw}&bg1=RPGQpqUf{(Bjn7@oFQuDrkc%7nyb(? zQt75tx_o}$-}`@fKVI+W>-lzYv^O;irGw&tZ~p^;VFJ?fq7n+`O6pQ7>Nu>roxYih zp{e!}TN5jnXm?K!4fUoA+!Qj?m-IG29% z67^h4`soyUdUna>>qVK2ii<@}>DTKA%Nq-unrdrrRyW)%ZN6DpeY3N!wXm{1v+`D9 zV{2}0TX}g$Zf$#RdHZNhS7GIyp}M=dwH=Sz+6PNICmTD)$~rksT}4b*FSEP2shiQ( zS<%j7wy|#By3<+F$-CLbtm|erv4$JFE9*K(n|ia0yLlaVOKQ7jx>@z*-7`&n1r2?r z6}{}%-qPxxiJIP_mhPUqo{{>#-kW`Gjs2a?gXK5-?zIk;+#ITG8)SA2R<;aRb@Xw& z`&+vD@AUQ8cMg@d47YR+*54Uy8R~0ozhBsTzxw9=hGupd>t4<6dyQ=mYC0a2-D20a zk1*S~l^vWq7N@d%q`qsUm=X<7FIHM!O<0JRSX8LB@2FLhAGhO#*+WTk6*fRrz za~(r7567nm@6SG%n!YzgA;}*^mdur+S^y1*?!c)$Z-l-+d=9elYXZ>@}R_C8jKYq$vc{=m_>4PWFA1uG@T6#I~^x4?s7vrlh*o%Dj z+RKrR7eg=k)2ka(Yp?D<+u*Kk_C4MlUD}*}u`$20xv=r-;j`DXFE&?KUT?hKoZ!Ei zc=C4b^{chb*K;r4&TYI|e){I~^4p&qTT>t2KH1#*u<>z`|8e!*`^C5KUcddY_WI-c z$M@^&pWeU!GXH9K|MQ>C@4vRb@BRG#>&usafB*aMKis}7AzTfsud8c{O^S}hTAzuG zIeijqb0(e~dO8(=`vn#d0{jQi1pIIQe`Nr$eZXR)C)Y8(f*7#L>kQA#<7Oh)Nx>_; z-12KtZQcX@QLdGFSj)lkt>(r`W5Ur2mjMXU)?CHGsaV<|%BlFYBu6|z)a_xFjw@%x z?CZKq#X+0;fb0x}tf|kYoOq%K-=Wy8`o(Tai5t_#Mlb5wWI>axR}5xyrx-`okVME) z23Z2Pt5ndNDbe}9`C3|N#T9@uY9(E`H~5p!Gv?EDkueO5*=*-rVVWkr5EHJV^rS5A zwzuP}$&&7*Ew5Y09}f1!o_Vj5D?P6)8JFC@oH|q;7&P$mlYgeL-(2#k)XAcTAxYPY zvvKPz$I{P#&5YkJcKJO$t~K=Y3;*8!ng6ughkt#0)0w(c|L_uPd;8VQlNZ%j{{7nH zo_+A8I&i}s03qoUjXggv1-*2U*sPKZZf|J!*NyW6`(~<5zx{9 ze%OjnI{YdYC%G7nm3zn1sbA!P$-AC%hIc8VBoq95o`c*m@3E;0XMJv-Pm(#V z`cUUlXz9zX&1ypZg^vRczu#}{O#FWTYy_I4aT^sqlm_7>bxs{A2jgT#y~@ILzoZw{ zq1AX{m(Gv0Z2kSlt1U%n4KNocV4Wm`+|-*Z8~@ZS7e2qbUI+LUPGPPnx|(v*8*+`E zK38KBy~p*PwFhSPol6oAuWsIWNXWDp-pRJz#7XQ=LUDO)wq*A^!NUFt)3Wtk=U|Rl z%5N}XS{3WHw?V7yqTnq?jESaUCkp!QAda0GV%RqCQ)~+Kick8(Bqd@$$J;*eYoT{& zEMotfKgT9E1$O1(v_@5cwMhD2tG!PsfeahHW4fpzr#WghRnHaboxD>uS%>iP;J|OQ zz*5mT;aCqE9D>U@y)%Q$TmcV|1Vr50>rskWd&0OMHZ|E65x@rvb>-i=Y-Afo&M&7K za!5HTkds4V{$6S4^ztk<3HOR*-MquP>b$?Is|=O3kyD6en1o*ljhjD6^>Vv;O=*@d=qo*8I!Zinh6$9g_-(6dp0K>Onk9baH(eNs zBdo1*#Ms;PS|p{d*gV!}h)RRb^4g4tcXG`-8M4LFXof@q(%FBXD*n=+OJWNIP{_pF zPRWcY`7IR1UjR|U0D1e=sTP=yDu(oo>L)@x14AAQ6|*zEjc~$0MLBYGEb8?YCDB?2 zSnaDJ!zYIbezStV{@I(EClZ0AvHk8NTNh}_wwimc(n=cdSWL^p2ND;vM&s(I+^;O) zg+4eEEZ^M8`Q)a8HJEleyir^{HH-L;vBi4>PEh28RJ7|HwTYYp@VXz-;+50L6(a)C zkcd*DcjBYs%6m+$g{c_tn1w_EKapuJb$j%=?hM{9C7ynJ8|s zWPu5+{XrK!+_obd#|}4(j~xFV6>i!W`|eCff$0A0O-kO3kG7Fmb)M}ln*2t(;bAE| zv~dbw5S6?;_f0^JX}MOtnp)qeqKs-#Z=THVDL$=}u3)0whKONMY*LFL_E&%BFojB5 znA!zrZuyf&h)tddd^hrD zqC-V~A!Y2)-vTQe)6hGYEUm@cdD$12Y~-J?AzDZT(~Znou1k?h`;ml|yF@)Tzk3aj zJ;h8J^To@PDZ;(5;^5U#gn8YRUAAmNY1z8px%~JCH~6CNmqnoGOz9G;kr^4xB*7eU zZ6vUhaYm2sCySGi59Uqhlo62yfmG4`5CBLDM#(4ik7Adwj5}Lx>ulE>ct~B&!Oc#| z7h-+BpXh~t5Pe}v6-a*TMv2(nqYwVvb99Hvi9(-u5!L4%ZTDvMZcifK2c_zcg5RJX zT5Kx0|L4fg4}%EP=#ja6uCzA*ZqztZR9UDyFAah*kfO*Ypj8+YxbG*}|@m6^YUKCu=F9 zm3In!pO4m)cdfRFM{V*FXKRd}NF$RL^yJM?-NCGFxfyX}hOQk?;%$evrBgfK9@$%+ zo6Nk z$83f{ukey`hs&i~-p34AlB17oz+ZFWwYsM^^@aMa5WSC;`y`QWUdo?!DdG&%A7qNu z7IKh++$lnSWy-$f9r&B?H|Gm7&;^MUgN;H2e`1lcWynPWd_N1!v6bV;L(ka$LcyV2+C-5OCcJ5Q+n@9S$O`)T~ro5q^dye zKV#A64FVz|STqz8vIKF)q7prwUDI4$LvkDS!sCk|H#}`m^EG5j$vItOIgO5I(jdG^ z86eJm*Y-k3!iD<+rX8KKL&O75Y=|?R*Ke&WyA>*JZJRwvJF@pp7?PU5(kZ{Bi&z(T zTVz{|L$5BC%1`^@W>tY#QCE2;1s&W1zTV}RdY3Y8Twa4>ud&ojY{Kq+W7w)*;e;zH zQTG*;zADTW2&*tbGGB42Dk7q0R}aT1JYWgob%ndxo+e(xHM$PqLL!h#gr!8Y*@)g= z%q$vlH_cbPtc3FhDV>6pX2{BwAzyAIMy*6r@Lb`p zxN(_krm@c}2`oVZe&6a)lPBgjK%|x|d^1t_Ru7oO&;7Y}nph;km^C$}85IB$=Y?h!m=q0Et?lANHOmQLInS3mbt`&fV zC0f}`NI0BJgyn@eylrym{-)g+g1v@)4`)2UJ5;S6<(bZn6+h)Mkd$vsjak&rJM z!#7$Cx%d}$T@&MRnJ1Y38u4c`>c67+owM@HqeYqNLV_{*hk9J6%Bzm{#4e%=6x0rm zcpyg7khpE=TAGNgWAttOLF_N{??ekkc@xqR@r-}aJNa<$UHFXI@u>|*LCMIZqUMor z7S(TJAX$jgAHwhQO$N4wSD1)3Pn_Y`A|4HQWE-@=KWY3`c%PI7Z(yl*zbbw81HNgk zeZDZD!xt6x%8}7IQ$X1^GFx_4vCI?LEXB-husm5ic@GhYyaXQs<39o>=%R z9(99{?Dj+AsPGv$mOmCNzogp77jorm-$)j|#sc=C;8A3?%n)@`9^I~3W9GMQhq&az z7I0t`-MJyrQLLd2faY|;_+5|_1;O{yGbSQCQ0jL7Eu7b2skLx)s$_z+=XK7p;Qy!~ z&s`0=BDhD_xvxdg6pL~8xTDfatYZlo|3p+-9g;5Agml3P0%GxZ6OZV^0kHJ%xI0*hJ78ns}^xsK(18_NiN3xQfIn_AgUBophN^J9RbQZqEIMoQ6bzz&L7A>Yi1ly zl}*Kq7kFN1#0@m!(qJoTB9neik4>WevSTM2U2n)5iwHM6ua>U&z`nLR-seJM&2e$R zuZ-S3`~V9vX9>Mv4^Fe8OdH`vHsY-SLT~EkoB?8i4L!oX@>&3~iB@4gxQWHNH?L#tGDt5}Mx7=!XH?3Ld$Y>E~a;QL-KUs?iP1y=`q%pxc3%*SNkg z0^LyazQqpO6wtJi(B{IQ*9*F$vWzMwXp_zkaBRd#QbcTHFcIQ-O5sFpAE8l;r1eyR)rGrTMA z$A&08PGJpe{RYYIGD|*_5q^ovOfke3s^Rs_0esnk!u&SgxSt(Mg^&Wa$rt*Gy69qk zYm0|mBnQ;+_Di?B9tZ#fDkGtfY^`H>`1?GA2 z%>u;jG5uaWdZ&$Tu!KgP{~xy2agxu}3zipPm-56Ne)!h?cn4Hv0WI1EbMtEn9>Qn& zT226193L{dkP&u4xW?+Nz1(T)LRCEMo-)xO{w(SUw))`Pa>Ngv>yl0dhGOV%mqqRM z6>lV4?IhQJ$i`>XT)Z@)eQWGwn_H6KT~%|Kbs7*l57Yg2PUX~uLFjoWHvRDW35b1| z*NpRC)EU1V=Dv@X&_;q&kQT=_lG$}8uPP&F6ODD6DiP_y5F<@Gr$iK??~8hwRZ;aK zX7Bg6FZ?LG*c)X*zd%$SO}W2ntOp5y{@nOpaHRgL<8iOxqA`urE|;HE5aT(pwQmLa z&P^XKH-0C2fy!P2TV5*1UO-#>~H*I&fAsrZzgwZNLH*iv`m>ZgP8l&z!@^6yjP?!ZYLpZ!R8a z-3EH{!CGv;!=FL29c?zc;8b1YcQRs)r+mB+^A06KT7}oM{CB$GU5{_=crB71yOjDI zs^vpQg|xkF1LM=k)nS}}oE$QLGH6I_s5{GJMF-gl&f%2b7=WE)rfc&BW);c%;afo6 zn{Ty4f^OV2fEQ|TBEp*L?LZo+UuICNn_QU@NZcp<kehZbDU7PZYNiJ=;x!`md1` znFK5}+aIRRgMkw2H-2FQ&`zjxs~ua}VhQ45vmr8KtA)??a@G4#~~i{J=^qM$f+N1;Z)X4e~aL0 zCMppys-+5*^b9+&P#DLXdWk#Qy{+&~{_^ZZ)`ru0&gVYlWGAEmg{lWg8l9g^}%`3_h&*3o&e!;#(!v7RMh?&q~6(J_}lx}ts#mT zB<00JB0nN(D_1-j;%ewn+P}*k@yR(io-PBSm$fEJ$=CiR3Z8oSQM9+S|3QKAD_OsDmIBQz` zvu?*C6~#&ut@Sbr9=fcPWEB5mwEXDJxl>z9W0*Q^%wW=5n=9zXOi@*_8e*^&xafv$ z%aoMw0-Z$InULLjL(djG^f#-CbFK942-FSirV2V=^Zgt=nEdH6BE$6<%yuEqtw`n5 z=a7L|_(A(iQe_$*%)^!$asgi>rYcF7*OFj)jwaRkEBlb=s}KFJrRi1!Rb0NuKJGZy z_eMqJ^V;x(#?Zx3>x$mEwu=uS3c-7);*;(FR*Y9*jF#`&4}2BBytwOH8=NI1G40)K zc{J_P0Ry|?Y&Gv~ex0JAlm78xlBlhzT9tG~Eu+Z8bZS%_ZS!tZ%&YC<$1YdN7%q|+*C}Z3 zAh#1SXrq|5nXj%`r;ak^Gx4ZQx!CYr<--?Ra_xZca`IX~GVxA_6hj$#Ms3lZOsm74 z?@{fnr06W86p=kto2SeP=AW9{2anB4JZAWhq%7c&eoG7&ikF;j}y ziU9Br_A}lVjRhXXzea>3V++s@&Uk^Yrp^Tq+I2lT-!|9m4ap|&j<1jg^3mY$oe9GC zU8io}li!6V36#cVAJwOHeP5p9h3`JcPG0VL7;<&@qu+e?-=5VD*agWvl_kC{!wiB> zMiSc?p!^$}LdMD`;Q9o-L7`<9mI5}P%!wMoXuP1f1@^s2MqD7&DU(~m5GI_Q?BkSY zELcN}@fv9^Q#|ZE_G-MV+KU^8$@pAnk9RSuxiiGf@LVk=sQj6&)zHFI9QaAhmc{?> zOReAWcM<2GZ=2-Xv;-C+%LN0u^`^XitMHAKFTBx`7^>$rtv+gBH{13xtP*~dmskE{IAn>jpHU@-6I(|eZ%(2Exj@{-N#388WYOCOWr_7%Z= zwNbF&g~g%M+yhb&OlY)B-F5Qkxgkuf*N3rztza4Dvtmbn>56?GY*4(XYX4w8L+UsH zm8_b;PEhr$ncc}eV_KF2U-GzukRnDQ2Ul>^UO5GD=f!scl-<=JU$RGH{? zJ_>fo$_{%4Gb-n;3o(M#!F~FHm=Jb`xm}ppjX-*)ZynINT~YjY8ZxT+oAe!)03xoR zD!0kZaAsn~&w9`>3m%;FPUOp174HVWhIqml8&?x=|*TdWcX)zLnJ1i?oO-_uL3?zs3!nQh39ax%URglRc= zt_8hVFdDLbSVhT#SX#4kqDAa=-CkkL(BF&(qAI44dy`#yz9LS1rz zQak|UEb4q8pP-$g+&$m1Lx8QoRv^9k2L09w& z%}L%|7qnLPpj?4k926&}j327TJs!D!F~vsy>sFS<@O)8Xbc6gFAj8E{%3R$AXRGW~ zki~Y`7pkNHrJEA%Fx2RBJ6jI2as}pyVCAkIZDZJ=RA423%OSq4R$j!qM0b9~ z*;j}zY{3E=rLskglR!x8rOm-42g*hew7s*j^w_xw{UY2$T3({0pK8!PZl(`ZS~IO5#{%++Dd~*72|9qpN2^A zg3-?Y9lxDU21mC0=bkEYGrw^uNJ8^BGBGbmFYAsXkgrkm{y|&gYL5#UPG}ZD9==BF zc&steq1kmsuT%UVCMLnc2`%-d`O0gEjuTAFJ1fXC4;Kh zUMCMZAmYAu&x8fFb`g=ifKNF?!$2())F<)uZ_zEEAkumaBIf)I zq{Z$Yrn}$10o9aYSUlf<3*L~(20#Ksc5oH)aG8?Y3h@tJrkeeS-D5Z>E{%TMb3R zK3qxW%A8v{DqWR7B3>&kxaXB5+EB) zln^Z>qf^#42a11LWlHC}nO%Slh?7*)hkL13)|${SKW9w}SXlH+vHT}|C8?@WNPsjr z;b@tl+4d}g8ls(xnTNR<*2}8zQS`ApHXbl*B$W(SW7?p~?SkFSB)JEqtNur}n?Z2| zvW%J7t)8Az-{W-AXH84DcBgml|0~h4iZLS97>TlvIZDdE^jW`u6pTC@O&7xA)XhiN zgAzRI*Ie5j>7?}g!8cb2RqmvUW!i;&u28}qnQEJ@-Iq>7Ch*h0B~PnFua7%b72wu9*$bD zuFBa$u`dOJH3C3=Pj%0Cf~`bONAfKR{FS$*TW(XW48@~!g zB{hsFg+R~2`Z@wp(HDV^4AeywG*x!LIf-pu>JF zLib#$8s=zw^$RNR2%cbozh%+Qp3$vPMJ+7QRTR)ysd{liKAXO%mnIdtGOM$Bw+F7t^zZe3EoyQBZoiB)K0g9cmw+RbClN2%Y78?Pu zV)NPC^6KEoZPXTfaI4?MonOu{kTlPyIRgYBM?uwkL!JC=XXl~a*h76!b@YG@;gmbQ zPbDlvZ`fdT4<2p2AfP)UOc>!FzL-!}eUvt;LHCRqo(Rtd;Vlxx8H~F!qJ`yG6{Q@% z>Lyg-uWe^1XZ7znNy7K>4YM{d{5s5yBPDZDPj#Rj6)5^!23|%!oUtO*F{yQCAzy0= zUOb@8|4`N^T7Gtqrp?SeXWRO?} zy#jJ?3KGkwTk=dI@#>Ir3Z~5X+$6NAtv41=7r>cVPqry3)vOcy=5}WjW}xTxTH6)# zaaQt<($M_`6!c6;MoZSs*!+xBOv0m7u;q5^Miw<;lJ3f;8$YWu8BSMDCxbEQJQSpc@4Kku2Pe#%dIrNAj@PklVja#e z4VMld7Q(aQo{e)FKt|H^gVLa+B#ZcLT5*VjLq!AXb$|OF)A2V`_m9x&zd5Xx{qFNi zVi|jO$47YI*X*gAO6PG|qObaa5~2qE<R(pj-tc`fhSj#l?VZDw!&AQwVH~wYDhyQzttw9R zXE;eyqWd974q@0l$g&lrzu&;()b!z*BYzDH|20exco_b7)^K{o37d_^a+ZUSnXcM2 zuY6mUw^8x)=ou2}37Je!;)BFs(2MxT{1oAc=K^Q)A%UiZWk5zBqa|+7NFB?K)KIX| zr2`XylM!vnx{&Y{K^rVsx{>(uoqAGuKE|I@Yi|8WlPXh2X{XLE?~nB2QR>?HoCGW+ z(j&viO0c3$F!-o{@Sk*QWV%ZTu~xB0hCz*xhInkH8$Z*y_nWSaI;azxZ3DoW`lIFN zi%b&SO!y2!0_A6nyUV*lyI@<*0MI{*Xl2YJOSGsQTO@L@96KVE=}(bUYq#;xdYBC^ z&l1v))b#a+$!*zu9Thcao41QmEjV}_3FMj$vh1W$IbfIQ+aCUcBpFalKRqzT!;3)= z>QBE*srBJ_5F9~=TR;;3Q~Wul-xTkOcZfzU-RZ;64=)}HZoTu5!#g5<~~ zd&+4o$fY})OP84MQN*1baK|R5EWMPBZYYlEPM?+DpDgiI?gwfJ8D#TKML7#6sumUm z&8{dNYuyr+(<@ZpLi>8#g5rv5$VQu4#N&MhNtDNHDu6>ataM z2}BI=;ONoZlPQqp6ydN>mBG3c>lC?VG9ke{J&r8MxJ(Uy+UK3`a*o`a@J(KNx0jG+ z91Uu#1>TZM0XA71U$D%-=wqTPZ4$5zwIS?NurOduQCkEF;;Wnf5hm!q=j6@?3*;bC zBWjlo@-HS+WtqUEWbgzd=N^?y(gp3n>8_@=ufz|Y;BkNdm3lL*1WN?Th1&UAFpSHJ zmbn64A+W*}`r%&bt~d6UKM1K0n&Uh1xHbiuGiA_jfSmY!KYC;P#}~7cUTrjgOOIdB+Ut3q6Y% zuRj?v>!fkQ3Z-WB&HW2qZ|DZW$4|hZ zDIUkG8C$QD6)s5sFMIQY%g`<@hV+_8Mz2d;n~O@BTKTe#)8U%tgcGLU4(~opZwfe3 zm2;O^gc-7eL;!M5m8D<154o9x@s6K13Rby942UA^*SxK<+y*k z4?zUk6U!95rf+lrvW~ie$ha`W-lH7cmSv64#8^Rs`57Jruo||FpbKf5hKB2cqry?= z;{{Y#rdcx0r&d;`EAot1z_R536-@^v69lZ1=A5atrz+75 zP@$w=-IrvBM;`c*q8PBJ@b9}0(;db~mq4kscsYMj6Uoh|NZWrSZdX!^uErGDRf%C+ z+l?lH;(X7z5xfyg;LXR<0Cmx81RAwVFrWsA*7f%e5itcH-0?)K6^t8B0!db{hkazJ zdT)*vlEuYKH#@4eR$jAN7#up4xg+z^G`jkv|A;Ua!Fe zYVpVQW}>vs8{Z2nM~~=Kw0eqaNS&3D1_E)O&2D#|ieyDgJrK4`=w6T&h%WhL<}71| zy|~{jkX-blBD)qL+OF{;DGi$9f1Drw5WStzW1YSt>wdkaL%}b2JXWlOb;xz&(5d5Y z4;C`Mr(-IEF$sk+txFZ6n#(Z^$eAsw_DbU6Z%cfW@(MdkdxcFnIuxalw- zr(fB8KZH*vM3V`(dXrWFM~#j zj+wztP$h?i64bgZNkRzUUy3f=`l+rV?Ld3e3k?Ldd&r{T>3?@V2p3bz+rj z?Q#i4yGmc3#9&*8q=bR+ygvcv6yJ>HJ+5!cw`>tpG^iF+20K~sM>1iw?3$- z2it1sIREjj@$W;QxX>h@XG*;K1&C2f6zr8QNqy59qr4mzK{5lhHRa!yxF*w4Tme4xEQxeL4x6AbIW*{o`BLVO>8%KJnPib&JW zrgA3nOS3WbFdo(@aiO0Addsggjqknl?#1PcvL+iNML+j152d``>QME&UghRD&~dC% z=g`5wzl^^>$$#?Y`F~g5Jbb%$DY8TFcv5-`kAaC|XNU?d?p;B-gmvE=vhPcX869(#4Ne^(Po*ld9eUx|HZei}DZQSxN4-}*o4N!&G$ z9#U{xIT}`&DHoQRb8bs;0+-a!vX)3P40x4AHB}^OKVTQRnXfRhqLgMpzV@k(zSRar@qZjN|yoll8mi1@D2fT})_YEGwY}BTqmYm^fkW+(MSxR5!;?*pNrIMHM~^pVRH3 zZwQf{0OOBOXm|I}eSG-5aUtlO(T$+E(YkUh>!Mi(%G%0dE$OOO9Y%Jd zClrE#KCiRGU1hL^luo_cnD%V6E!_Y4ZG3*@c;ZZ?9s?Ciu7{j;jV|z!ON*X(ewF_# z;P~Y?H~SQ_rw4Xk>^|uGaV+g`!G9-zZ9P44^|IjHmt)CWRv1zm6CKdPkvM%$P&ws{ zew4IlB*75-Z)<`=F7w2yhvUXv-dO2*p??M1;=1XXNAAy{gHnN&!*1ru0>2NTKaY8Iq@GA->`faBt~m!g9j&aF z#9A7)$Aa-vMP4@y8%NqA$@?J!IM@hKiKmeqjF+;zW_K{wh$^2HD)}|Pm}NiKnn)J% zs;-w-6UfpGFHz55WJ&gRHx-tpdYx>>! zti;{mmAw&UF)laEx;jL-ro!bRSjrv!zFFk??7U=9i@dn9TEp}RRdO|MV2$h~C zUxM{K!=nr@lM17*IE4rj%3eiQNjrIxc6wb`?3xN#G!40(;YPVIZD*2mxQfXQBOWF5 zc>ssf#;2%{ZtiPeo%cG5END>fPjfIO%-}4x7i#pgnnGITid?!gBvR6En!-lwx)&-m zSU5!as*7Yr1J&G+N{lT#nG?LI_?S#EN?_QC(OKCCTS6pST7q=moV(`k5h>=AHJZ>| zs_?Ta6jf&>po$@4-ht+C#~f^i;)c+jvdh=41bKBk4B@w_aTby?j!$ALl>^*Iz$rbH2Y> zz4d1KsfWizzAeZ#C0sBXu9JUeRk~sR&Qb1JevtUxhVhc0j#2Mol)|9d!EDg=Q-k(~ zUxVyST@xPce9r-!2bY{3q2mjs0+odRYmEdPkTFy%J0ZCx}?Gz=V zTtmeN@~2bi;>WkC1Y4qLyT2V_P~dtBP8m_fPf~#5`{i(!e&zScEhv z=P-0lTdbAZcoI!(6!#N5Rtdn7#ZQ_<9qkr$UjktC)e6y|+R;WbE>lP>rw&Ccv-#1@ zJB{?oI6?~M{Ok?^eMXZ-Qf|w7ofeS074XUQ?V>xThrICCHuEAm7bL4Y6Z}$XnPN}BWMLi#x0qj0TY`+y>VKgN zII*p~w1qvqsYm_@B=q6_-}%n?0Lgm779_mumBa6x;U8*31dGK$Vl0g|d*YTa6 z-0kZbCS`Ry&c6;QrJ^N6$lahc-*Ijq=PCf;Iykz&jUoa?zy>1JU z8K@IZIhMmCim~S&A%N;R6d`eI`B`GpN*ba+?eiuhog5^)V)dhiA|1>*PzE@Nuy)cE zJkEm^*#cesp=tcgD?DO&0#GPIK-D%=k~Afe;~sl2_@AcKg{t5`DUTTh-xR!2fwQuPJrc$yj2D6u7=Art@*eY(jP3m3#)4zs6v=2x zag2>wKHQvN%Ks#_eh+Tm4?3_bU@*yL2ykCX1PAn2oxu8~yX09=1<~?TYya}Dm;wpd zRULn!gRiZ>A%x#kgg&ShUXef;>`~xbY-L*;{0`uNCIC9hQEKOyY)2@La2d)Accl4v zJ(~(!t`Q$@OtLYe+puk2744e5os`91TWjmC_L#07IKEWeTI+WuQYVJ1vq^<-6BQ$~ zIhta>b8E{xfX0T4r6taTFq(}gMZ$J}#GJ&zdjOTZsoT}GM_6l^64wF+`YO(m6Q?JZ zjYw=4%T5-_vBk7VHYQ45kSicP#;Ewxh_1MeNfJ%N;($=ss7l1r;VrIXs`cYhev#R# z>Qb&MlVh^PwOoptA=sFZsZMHkPXRa?c|r7Pj$-u8w>ThrDo9+i^ewq`aO(QjCE4RE z=`ypE(kj_EqLo6o#==BO55G}AnY=UVW%DSA!25aNwyFlxKa;h6Y*qwr4*_QIepF5E+PNANn&Pti{ROI zs!$6NZ|i_58YBhjpUhHve4R&0LHI4EO9jY~1Yg6D!Bc*)cgLh}yfu_a zShe#tMJ0IprzWJ&W%(u;W)ymR9LsvDNSE?)bmYUPMN_V0Q>@!yjp7oU#1~JDCTSMC zL|7Hg6WFABX=>4PGG!?#Z>wT9jvp~-t#PIYAlc`ywVCX zAkV`t38-7l8_ind*CXazhIPH8`&$=m&nm&ygrab`Ql?U zY{hN%w62n&9U%cbepj1RaUX316`$Ey5?Q7DHb|W$bL#YKO$w0E&(WcSEO|DT`E-3I&@+eX zm2yodj{2+xd>lYiT|p%{%Jw;C_4LU4rH$oj65oQYkMabokTpwfXzJ;<-#dEU&;{u;@6c&^P1B>XuF-iOGId z*E@ckJ=a7MQjm`{oV7L-uaS^>p_!AWTt%yH6Fet;NWTQgJ{sf94pN&w0_o!dmc6f51_a$@o+m$IRs>q z#5F0i5lEycnvUv|Y|NvD4w?d6%K7rIIuT=ZxO%a2Q=BQ2+B`R+P)1d2M7Xf%z1~7f z|R=B3FfQ=sbq{zP*G zl>btsQ23-_pGcm7Y*MKV7NcqfJiyvMxILmgDS#D+5;d1$&&5a3Iv1jGX^D^I$kv}o zk589FtzEQb+v{K7hQ6uUnohWVIpx)qV~b|g=Pjq>@2C@pWf}ZQViJ?BOm?S%<*_^> zCYti!{g$gP$YrA6K4jT$u6|dyHIlB@+XOjP#+dJTyot&}FEdb0s%U|{*Ms)ak%}^i zr1au3*UJqiy^u>v4l=Hfa84ewE0tKns-X7kUy845Zht}MQ(Dfke5Q#X)56_xndA<< zpY&uz3XEa(;$m0T$P@(+pkbA_w?eZIe%dEvT_cr$Q9kN5=B9F4v9va9iX)#Ec0hm$^>tMbfPN5 zTGhh6H_e*!*%Q)iW8W0$$t&!e1gmLw$z3gIipX_d5>?AbzwlpD8C?pwOdTa=CAocC zFRm;x40>{Vtnc5L|D~XT{djBnjLKuu8M2I)1H{Uf;jrd7k^epYQd#J~yN|Kp``$$uBV3Lq0i2bq0uN9a3Ky!o^0aX2YUqhUgD6 z6mYBGlPRiy_)DAkuok98k&~tJKgm4I`cjx2yfJFwJuaV|wc=}(I)2>r1(`0B>+hKB zdo=1Y`R{?00Q=Pza} zF>`A?>_-wHAgJ+8)_hF{b}-|4FXoFtBp5OkLdQ#r}_%vJpoMDcFer)jKHP@Y~c-#3IQJ?*-rfcV(Xmue!;|)=`jEQ?EA^V`QFE0 zU^5GNzvQb;0!g@pYG%yNSnEb{Gh3+z?s{XuON9`zHA)G;N>(Sm$rf!eX?pWu!CzU| z<`8KZv<0+ojRh^)PCdU59I>5BwT<}r*+PMFWZ4SoI=?(b5~vP7y737(3pT5px_2?R zw+W)hJc!XBjcL4l`mFV?bbw0qM}rMlrHr(Lw6upC(=IOHrjBa@UWh(tSiKd{&=D~Z zJ_R2Dsg1nU-?CM~-?f|>KC}juQX4j}8wPZWo|l~O^?rOdNi2EVPOnO!y#Ri&Wb3>J zntWnMj75!FRO>`}>+x-22p>-KH%;A2nF+GxSY;C2<%9wy@6~8o(HE>arO5G{ zGVjaj*KX(g#$=OUpH5GS)bc&0^<1~H|J$7tzmiOhW!h?ND`zBlKPtX_d_E%|`|T1M zq?XUeZ87&Ztbo^&6XhBI!9M(#9v6gy@BT>o{=cFWjHio^^TGp+=f~Hp`7$9X#vt^!~lTw=v1R>$zrCvUd)v{u-fN(DUnz zezxOMdiO%jUE0!>xmQe~&jaUqq2k6VCV(ligK4#;7BMUrcAos#l|*Ye01`2uE#<9> zvA`m_&%}hkO|rmn_-rXGC6F|g%IpVy&q+P^ap6$Jg$P$1DfI17)`24pmr}mHpSvM` z?LtkE;y>dHAG)u*UjBV@B!~k2=f$3T=*xx_Saoz`Xk@zOl@DjF!|9tXxZ}QED!J!& zo!1%?-=&Nrh$U@~if3mbFqNu;ap%g}U* zF~)u=t-BReXo?P4!#J+(v7OoE&pcIiOBIXs=IDTxiNM0wyz8Q-u(nkZ48Hp0qRwKr zYGJ{W`6{wv(IyQQYe6YhD}K%aqYnzrUDYmqAZ`T5+<0*QP0Bx?Hv(gwS^o+%R+GU6 z2Pc|U3&?UQroN^2OVu%bK0X)Q&WRYTBQ!>BpbX-`IUou-tD zD4t9DJgk^HtM$Hy-bl_Us)88W=4jVMMCsgWk7^5GLIi|=%D?vZH=24QlHwBH=6ER8jKz|zvhMQ?CL(&q774!sxP z$)}WH8;1A`sWKZUOQ1BX@dhx42}rv#$a7>#oUck(g4vm8F;bKHHuc~i+c3bn3fuqs zqZ?}rT|ZHiBJO`#a1HtI`^$skq-g#A^rD}uw~y@o93>4UO{V=s-#Gn?1M1&7OMbdL zn#SnLl}Cmu{^OKsp3nr>S6fwDp+C;HYeJoI8kSkVgRb^AxqgUB#{QX|E9~>jQ+^!f zr2KlM*xWFinsi;B+X~H#lb~py$#Jz_!&W#+WFNH)lh9cjY0mbRJ_upqBb~}b_InKn z7Mb=;|Go4%`fTQ5=_zz198#lH(rTN$u3KO{PBr^uy)b{Ynk`k7_A z)9ZZf`Qp>=c`-vli-NeHJRK)DR9#h?=$dA_N#77#{(f_>OJ;E~@{TJ0K768G=1Do# zH5|{B{cHD`uGQ<0USCa?42~NSQ+s3m%@WgQyL+>9tG=lcPK!QmlHmn5B%=6sm3#XC z^=4MVz0e$+(ICBt7_(*p2r0tYPL&kn(_^D_Hi=Fu;Ri7I^J!739XLJpR~ z{l8|GXSOy4<&=s2C!%z@K-d5T%XM{W>paH}z zi?`snIaizz-}Ii#+y!J_SUM0e>GAw>`_s(zCT74*7nsr|5>VTvrZB`N^4uH_sLbQy zx2o9J$&oPt5$-bCB%-ZDBvZp)?>4VQDIi$Dcd)^Si(QFkYm6|U4 zdCro%wjy;MV6QoLL7kecBK7wfJEEM#b<9B2CFP#^@`>L=$9Gvr^_J2ft1OF!)P2DG zZ|Ast&kFO_?gxyDReJpl;Fdc^P5W_Rnw=0pyu}}a>Nu4>-nt91uy?=VV@|g9voTh9 z9q+Zfp3B*1o-L$6kW19zjv7uzPB0hMs2`z~|T#f}W6Z*wC?E~relKJm z&?PelzS@ASc_I)sdf}rlXyA_j814SQhbHVC9iAELpFG193rc$5wZQo_PCgv);}6t2 zxj05&5HBj3P3SF&Mq2){VL^V1SicGVObf2vD{zG{F@Ky!t-ov+;&npT>(^@{yW@9gKTGTA)n+YfTfGCviHp-c!A-f^G@t4{y*+~@%57Bn*T zR{{;gJw?uqzV)^8t(t;GqTqa6_+C)=&7fF@)^|cE%YlQa@S)i@WE52#eizP;tV)_( zh-o|Zy69=yJ9Wy6?h`?jNn-;n;?y6gO1{Q5e~|l*2H{JS%4~Qc3>v*u6j7#CPg(&; zL@oruR6p5I`@Qpe_(Jh=;_1fj_iO&u4*=U8X8u zEHvV}q{LSHuD@gKG3Bfc*K7=>{sPPG+2TyTNWQp!Xld&}k`0o|JX4qp(I@x7Ti>NK zdz&`NeLyUWA@OwxcivSZ;iHl^F^K|+J-?+{^Q4Jt=Ma>h?c223$a$YUWV$h=ba=$h zOYQyu#Gz14%6v$=YD5}Ba9u2Po6@&yK62E2VNijGZo_p4$NaqXe(FhBz5Q;G|8h?iu>!vl9 z7+?U*(RmvrA{3j<5b?%~DR3v{fWN&Tqb_|4%{)7$(H@Bn(}`{Qx%lDd3ay5u63{9> zCg-fN*LU`>!ujAIT2*YV!m_V{>7xIQAVV8+G}}g}r5yH5Rx0}D_g*uOBgi7b&d+9q z|LgOrXpD@|&-mlb4la8aJ%2Xl@YpNrf!znRhQi*2xJ44;}L zS*)DIVqo@>x(jLqcFW_}pS%k7M&FIgBTtG90_Alz6}7J`v=50SlI83^VtFn9%N8w~ z2-At$^ym~sVTx1+h(|TT{J%zU+P4q~_T3A5WC7|G%oh28iFBl4FWo%&4P+qo{(ykn zK66Vf4JbcPR$3q{EdiA`xJvJs2Y!(U!pFsBIpPb;ZI@h?*dD|UvI2cnDLw1Z$Iw0H zsbfkr->fG;8}5QG#SUGPJH7gFwM)rW3TR@ZC1#N8vOb2Fq1+I6qA|vX-*L$8j3Jrx z#bQwm*!g88H3FPPQ+mR^_Yfv)uhzFl>P=YwP#AtcEZnlG81siL@q|3U^OU1-U9dM@ zI@g@6$<9@3QZ}LYpNJ`5B;KE~`>fIE_N&S8g_AnR{$;oNSx$36?q~qszNKR&fA0zY zz1EH#RM%G+QNgG}Bam-p-Z3P~+Ya2ai&9jWwb6mOVawO#A3;;MBV2D$!@2BjL8kLCa|cvQkrcA4VyWta=&fVnrZlN@VfV%JMX*T(64s97nHF@!m(y~o9CEu49+I80h#N%$)Jr$I z9&U`GW}_tb46#p~@Lx7^Az{!Z{AD-K&_8{M+_FEe^;sn_Om(c0a4A{W z7Zj$>p2*LynkmxKoHZ4_p{vpRcu!n4^%-mdfDKzF4h|(zWX_p5VxDoEM(`T6O!bk* zn3WCc=7^{*<`l+@pnSKmW_>?(m5gQTM~2FDqZ9{2W&Y6G_Te%&+B%i+>hO63Hlg_6 zu&b@y@aE<4+Utj!T-vSox^OF8=|LM=p8NR?veG;=>C4mBMgsX9lw1vbJ>f1_*nU_q zUe0<>vdi~$L`uao&DeTdqw@@rq;n=2F-7=2RL;%5n9%zaf}{}{(a2Z!B4bC$9~07LgMgAQexE1A`n}bx%n4FEK&1g4 zsk&`N>6;1L0ro3~ShFFp4B1to=;#wQHtDYBKI7*<5yEqV%qW)%gjhSiSTG@Io`HJz0l5+B zWh9E`xrOh2vcWJcClpq4GS(LlD;S<6@EU;>zpAZ)ca{RuamFJYq4c8>-LTP~hft;iUXZemJv@Zv}&W-%1b0^kmX z+f5K^D&v|c;irZP3g&lk2~2@rJI;+Dxg`|+8med>PW(i092t@!aaRWH>W>CF8Qqhq z2$#kK70AHs;1TJq{)B@=jg>-IsTB+_UFqzg=Vp^Z<%CBCK6E=hu8De_+dR<>Z`tow z4^b}M3sa$hQ~+o3bq549wA}E_f(iioZiI9?fZB^cx#Fa@A^hJ8KPm{1stbhZE{l)h zQA@%KELQIeWT(Wy^C^SSP^B7{TaV;TJ!#Xqx$d>bDDL!|yO+~`ClxWYR}@0)o9<2x zAF%xI;XwUNzE6mOJLF9D#%)b8ipBy5wUVhN8|rcuJn zDG%*~AO5}}^EmC?AWfuid~I4)o({y7ai!yEieH(S!!$T@S!wUr&^=00jeL-uN)Y;j z*=v%vLMF4ejgQM;hN&&fFAzrFhAK2IOXa#=3R?T}`peP7Lzm3&Df6+-dWAM0n*0a$D(cFkdDmXGz-OSJZaI}XvsQ-; zfCZ{TKkgTM4Hy|2Yw+xT!m|s}Q35qW>>-0!*xLn%_QIz=z36##indwmaQm(fJp!hu zia#D{2a0&}i`#(hD%RlD>OFqaqx)nB0Z>89n}I65x!?^{jYf>G`{s&XO;5Cx7VJ7v zPtN$<&cZ(Asaa5=?Na7#5k2P7<@}cA7cc zbwmYnPh_5VS<*h_`E}hY!ES8e!pZ{o{t1uTyL+z&&)(2^VO39Go)f(M z&cQ;bjL!Xd_n=nubD8E*&cM}Q9-F^4D|8N~Af#`Ha49Y3{R8$fr71^OKE8T%^{V6_ zd*#8fMFdwenmP zFToAfT$uh&Pvz)+Ku5XYE7h5b^eW)EoK=Brj=Y0_oTV5yP_3eHyaeZ1AK$a=P^&dn z>>yK@zims1X(c%3W-f(B)g?Uq8M)V(+wC0Ibf(HVy7{zFODo4AY@n^BTyAo`DLJhD zLyNorWEGyKL{hkih+l1C3da}v3Vx-plIa?g$z zHCEjja+~x%k@(!_(d%hH|Ko;Z{{ANjX>I{gTEhq_GBVUYC^u+4-=raQ&2O4?TXM zgNa8SH{gCA7J~0%$1T>_tF`oHYIETk1^YCFW?hxaD9ZEo?3B^7$0v`P-(_H3h_9J` z%C5EfjxKs{cZR2=F492=t*TgxOY-gm^l!a~mnb5&I$ zm3M(yh1WAp49C@vS-19eh+EL8oc!9)qy3tMR98re|ijcat(Bjbc!IRE=THohw z)z}nDWmAD_o1DE&lL_a5RHFs>u@R!`Fd7!#Ky9&*#naGj)heXqdmf@-Mu^B~jauN5V>ny=TQC zvlL7qW60*YRCc*SnCqsIM1GxmSJH`SV%NOnXbm4E180i&Q-OAWO!EeVGIZ+uvlj~A zm5;imMNV)g49dtz7n@MH?S!q){`}^`7EAWd49tOZer9k@y<%v>$t28><+3PN5j(-oo)rcs70XFFT@9zN12))N`KunhaOHI6AHrJtKwRk+*#>g-Mh`XP5! zZYc_8(aG$-V=k$f0j@fju$)V+T9NQd7NE9fbK_^y5_Z@QdPWBj|MbOKH~b#aJItNE zg1L@9H~U(Xw4GBRh-+S%`%44O)HLj0#ymRRAgkbDXpV2!96!BCu8_(xTO5M=EkP7| z?m?_(M{MUZQ9V;S)bY;1fgF43Z1atyB z4QELt(xC#B4?WE$GVJiF^=QX~kA-e`y_+94@;t=@ZR;IcJ7lJE>|#l_a_@LqhI`3$ zbfWN>+>*A4`5~8#LdvJGL*FU5yF9<%rTM0LM7x|>yq}x3j~t{JswhQ4o^HKf&Dj-r z+tc0bN4Tv{r*(;qGRL&qxgGPr+WvZ?%`&QfcHrX0rygw|)fS*Hn(s>Kts(pZXB(@k zE414`JSRHxV_21)9LK|BjbbZHa*5i-aRW;K5C$q`xAbuGmAhwHOk{L>&%LY*_Ps>{IwbC-m1zFX7~W+ z1}Z$Mz{o@=bAEFRa+P-yu7{(D6Xp5_n^hGa0EtIx=a5${Bx$jC$1MiQdB-lPNEXee zsi^^>&ReX;9Y&T(Zn4EbJ>G}YD~=M4$f7O_LsHz05ee+IhYJbl@kWLoj z$;IrffeTzSJpt6QZcRl)rc5%$#&uB4&LdewrRGoNz@XT*9Eh8S0(rv8$7NIQ6+WBN zO2epzg5}^W{W?AY6Yn&PM{K7l+pt`m5h0RGypZ4zAMjQK6oofiFf)LTL%7={O~Tah zpmm8IW=B`-g^h?_D?w5lXRE0F>UrWZ_MM!B3vCpgrMcjbdwy}&VK=;Wh68(_yZ$r= zD%%k4w^i=lJ*T!j(g|?z`cKC9p`~72ihZNGk3?XvfK#JXhS&FWb0h*)<$UsPHQ!Y@ z`Z{&CzkPQkG2nHc?aIP1J8#TX=y}BqV_Wn}XF}1y*kDh)&a$ys%)2R66f}8E?Wp@$ z;3F*!whIQL8PTcou4(AYQzp7hBJVV{nH|udXXY?d8M&7+Z2F|j$jWw_mISV(GeK)# zLJJGa@<)0*e&>Jll9nqS)qX*PnW%!+#EfA^J#O9WicekQf|XA9wT?|CgtA;Y9kHiw zB8^zvc|KGYB4ta2Kr@EeFJ9kIEF8^~N)H%2+G_93^<^GDdyJZ9v?|v#3n5yPPhDMG zg{3%+SuDC0B@L z+!nZi4SUmqIm_2GAC0}l0dozZ8EjZQ8-q=NB~Ue^F}hkSKIlYfrjO>mFjWgCEhUUH z)|{~3s2j^7Mu2o7p;8$Dka81@g~u>w(>|MN?q!nWDTdFz>;R5B*X;v?@7WG1GUwL_JKQPiAw@>&~5jP}l@3R}A zSC%*=&{2P(XE?4Gsisva@Oy0({KnodFe{D}t}||rIoF*Dj)&4X&@i^xzQyjxywQ{z zHTz$KO@MSI-wAAZ45XqVe(d!0m~y{Y#*&IoY7>OSgHZ60FaWWVf$$?3C;n>`%ysea z!YTg<{V^}QOZk7x;0PO8!k3}WBEAbz>;owK;RE>0e2WU{B}QsG7p;0Mv>$^$z8xyv zl~P-YGChV$ikNaaJugyOt8P!LIcUwe1AQhtdo9~l|>U^z)IfZB6I zq2}b}iAv-i`_yfEL9DO%3LklsF8YeBeA5_F#SpzN6s;tQmI|TgcnCHJ*-I7cq$#c5 z^D^*>Hs?z!@?fM;Ofe5Ka|~+vOmfTDZ`D>q6bJt^BI1(&v z>&#giY~Li7#CB9!Ru9|?yuH4^7N{ZxabG|V{IB?i=Cqb)dhuaI&qdg&WT_)IUgdkE zFa}0sl;!_PqxNqjO`efsC5YZuPH!HK5AfIFmcyjpXB0`RtB^#`VeASqa2EZDD+lUI zs^1vR44(JBZG=h4hP9~JG5qyxA@)sPx=R1nEX?cq;c*lIiNM6?lxy;u+OF%) zZ=m+}SO*Pw9Vj#>1p;%Yi}q7RzGIzUEf%EDNl=-HN`_-R&G6*U^67Pr>h!uV7AIU7 zFm+ET5f7O)gFURm1(Pp~v>JUeb(&1#$`FtXybJHxNMAmb%z!@yrerJ_IY@?wGbJ9; zQ6W4qRRR@zA$g*wu<}J=AQ=||09^uz-`pwsU$WV+38TOvR39I;7g1x-i9rfmr6aYG z%_1#DOtAryQ$GXM!$u8kAzxyUb3#-F3DiZBs4A9DVp?Jv_yo`g)_E!ACFYU*dHjv62sAUa2#BHkh zRUv9$ZOfcP_3{)sTNd8c%73~<)w>}|JcvOrOc5)(FOAHjA3ImBLK(Wc-`-2|>4*~o zMOQj3n9=AkmGeTNN|=TYgEEb+fD5cQv&xNu8t=Q|aeUBiQ;}9YrW z!q);K_UCQv>O_v?N6{KM5N=3-pT}shczdvn5?B%*p2Y~U1lFdiOUhSf2O1e#w-QI8 z+RrTD3@>;h!;yihmK}-9HLdf*)h}QJMgFZrT&PQI%24&wdYY2+x@$;1bjIV8`Y)p?OM_%*=8?waf=Z9;BcfkFjjD`GCQ~i$2|gYvVq|aq@ol;bLSYjU?#Dx8%9{cTW@R7 zla7s@3(W0{JvM0*`} zEyo7u5L9DeS6*Iq?T%j7KCGBJ3P#1u}7yp9?iE&_lbl+=ju=C$xlB*h94ytkTT0#5PXAKVM_bT}$ z?JS$8lmQTJnGs3cHz*6?2~@SG$Mp3-QeKAz3D&bLvU?{+(@6KH5n1F2D+lv%jSN$5 zOOV!}kpuFpuI`J3|MlPP|B$h94%(63#>8(4&E;P=(3upY*Pi*un3ucZA5_pjYdj^IicdK$mLh`nKlxd|beByncm z-pMFR$h{>Sa_EF66B7zMz*L17g}~s^!u1du!6`&xvJ{knnrl+SkxeZ zvUR&R5tc}YongdpnQ1`wE0ghRE~LZTmTGK}j_yt!H7Z`eRvS4$*0+QwkjQzXb%u6< z!6x(zt#^DD>)t#_8*j{*?$0~~Q9lO|b^jXMf&^vQcpc$@{#Bg%aSJq%4NAmNZYP}3 zF{x8M73lApz?n~r+KCSqyAyhGvi@sUVv}lmRiN5?Jw>MzsYVHcM&VRj-K&UT2fC;w zUnJ&6@^~4p3EubNqv)*;_03Z%Njy=%c~W-CiSi_@x@XSDv)IdF{V_C<`#c2Q5&9G9 z`nbvM-1~y?H1=QgRCzu0Ek4v>3p7SLu)$8Y{4vW5H(tb`_BKeOs(_O(_~)J9+{axG zzi~sMoCj`i0&DR_9#PTP{uSFH0e*AP3Uq+NtOOX)_q`mbMmxBL7thca-=U)bl9xmV z#do&Wr!Q_;ZUGArS6@MF)D(l;FO-yHF6{3k6*M8_BSTZu)M`=X1_Qk)P(9#yvV zQOzSA_OGspW?50TtxRK86cga~ZVdfKNppe<%+TIV_i)MwACqMgeRk@o50n9@BT=D6 z=S0tOK$$0q7wquWj?h8mDyML^9eNSO-jvBJDCDtljNt$F7Y`2K{MdGL{P z^H(@ck%88I6`ooQzN(PZVjL+^AHU}E^e%5>*)PS0iy{3kuqUiuceU^g_fqIRwiGe`f$nd4M-OFxEyKNe75f z7l1Z^6MP_y22c>9;In^lbO4+W*x`YJbigJb0I>mFwh@;n0l<9ViVXnB0M0N_PkFNE zyAa7j^I9dy+^b^X5VP%I5cv2mX;aOP)`N|LQ$2*><$b;d0o^`St0e#FHgUN!jQ+n=BPQ3#p06 zTy5qCH`)n+xoQ^C|wAhPY61IY}3re(I*GyPV0@r%o=#}o!W25=< zGC>-St~ZW(2XB;|NTiLQuHdHxE)>XGu+~iGRL0SD<7Zwwtv{Ae2sgi`{ua3>>rrc- z|E;}0JZ!ek!HMyS(AFrD&ecj86= zw>!%ek8R9xkc)2FD^74Bwv}nfon7FHC`MD`?bv1;d}^CLh^yaicG&f4f$60|R)P6S zKG;d&8Ak9(H$BQuOmDt#Sd!M-mQX`OpdzQVZdQMtjo}P_og_Bf?E|vyo;}pqvQYby z1C7}#)feRk#yq2S?RUJrk2Pa!Y3-u0TAjJ=lOmeUKbz;Anx!KBf-ipm?ng_LKJOoP z!Cs>(WcI_VSnQRb5pri(b7R?3ZnIqU*oJ#aR`x=_&7C*j_EqEO)GevtY`F6RQoW zJ>DXUg?m=Mj?>u`nUvK+*LQt&2@K$9si(uXnv{n*A*0M8b2|%X5Xeo(MLyw9C~@9P z1a8EeExoU$^M+vMRVTK?BdAm_QH}PSZ<=Pf={5QoV_!P+rg}~HWWLjapuDi@HIWp$ojPnaBJ~;$>>I6pXwfmpAt?`;Y{N3hPnJro`>e%1s{~ok8 z-0IJkNB0R;jKS#j4SBnKU0CsT6t`XMwe3!yj`-l#!`i9AAAQFRtUh<0EgIRKdQs4| zXS(t9b}{t#0;}-lezYt4;_<8?-{dv5-a>IC`H9UdYa+$8k)5$P+j+{j9z?PtDhi4!jj)cU|lX@tH{!LC-I0pVef%Mr*_a&?KSs9!|&yGR7ES zJOJ>64G{kXWPnGyl|)({4+Ta~D=xY!S~2(*8=-)dKY2G`R%L z52I4VO-Xi#FU_*NUxULp`!XkI3F0Z7)sw3VHx9aH8l3@LPtpcLH7SH#_n8jK4h}@T zl?rzmY&>u7h|5|k!7lT=4p(=NhulqMwE^AYwSEj(<6u_ zt|0^~VziOQw-sR z{O?$hoSnZ2AsZJeZarLz{N8Ds^>6E!o5aP<&vJHIJ8rivU)R!hFWphxrW5-}e)^3Bmd`wR?zZ^JkgYXM3nal}H{ z0Lxv@ZD(D85_lF1d^ZFXogJ>89?2jnqzGLH+NWnRFp?2dL}P)W_|se<@n+eTUUTe8 zqKD)71k)pi0g}?Y7(le#&M8k4NOkbj5c}4`BY&2q_osbEB|~DtYAfP<+2h(&3547J zO$4a*9@_({y=KXLloIU~){j8cf**+KNP7)!W6GHrB5U4Nyq~Qa)*CwnD7YeQGJ3b*N?C{ zq#*DT025IELdqD>*eBn3>z|ms<6BM@_XK*%K+TWw{fI+$`Szc3=aUrVx?XH(4%T@S zd#=B0CnGbmxr4RmoD^FOhvxbmEUp)eSyV;q$B28JNm-SN7g2dw#kadwLrXK+4@knO zPkP*G}08NvtUpE4{z%^w!aw8{3CDo|QZn$J9RyB+i#cLgJy5jfuM7mU! zF#Qs56`r1)b;wNliN*UzCqJ|rTTaBkPngZLes$+)a`h3gNg2+>Wl{F>hDbc1{s=EU z9G7J$1S(a5B1q6GQK-94j>m7C8hUFDrgtMj%P$YY;R8Q0a&7rccYvAVp-UHs7kTdrubk7c{b$2#8707oL@+=Au_JeS(tlO+~pVl;GcAZWP$gGcLE zIg$aGU@@l)S;pooL?ufnbD>KbnbUp?n{Ey$TH!RnwJopJ_Kmt&7Ti&KTZLJjFZ|ag z%{&V6y&A0J1s+{QjL|JB5-p~1qb9rB5He5^qvM^PFNn8%4(P}+?)=BILsqo&4YQ*@ zZ(!@^twp1bu>t(w0JPb1F5+cj%QSl~wR9yl&rDeQR4d(Z7wFA_p215w?pa9lE=o;) zgIQ5BdW>^#S(Gj<74*Sto6I4b>;jWnko*KI)0TM~%QE4B)rAT}s)In|L6EIz%MzdQ>PoK+9b5j9uW^P)M!xD5OgH>fJ@%GKZA%85FpyT!kiCIM1haI$ zoiZOKrSZuK1Ko(Us$~)`{Ueir?+rDy4eX1YePg>DGr4@0#^$NEX7 zPdh+*7|3A>mX)DB$k5^^L(+zyu5avgNS&8%R}n&NBF)2&$6t43u&ha$=UR_k+{!%N zD&=C>RJ7ydNVp!odn|y!gxVe|zobEZF}oS;?55M^liMYO z0aY0;>}M|78O}JSes)GSn_*z?d^|dfW|O(CEfwgLwQtEif@7X$WZ7~vT#8GgxUep3 zQVfyjGYi(hf%a9E`q`{=dsQMAlU?_$s{3{Q64!ALUb_OM_&2(0+==k2=yZWMEcL(4 zjs3q9`Fc{X+0XP&I#714 zMm_-Ob>EcB2dc^pk=W zTQJ1TvN3yD=T5;Urk0U*Ee2vp0!ovB|9e^b7f51)dS8k;HpwcYtu-J5@okF|FI776 z|MmSK7JW!zzi=u*NEEL8w0M&|`pPnQkz=42HKL5srk7QW-rcvrF zJ+(E1N`hVx^i&P?@M@~7fBIcTEgr{b)YF0Yw*lshknRAMf1c?*U;q#B^5jjBIH}EM z7m;;KzE`xC?^GIklKV1OuXR{!b9a?rm8X=i=eVoQ?XGa0070Tbow!@Tg8fcP+8Y1m zwU}XLe<&deM7GH=%+K)I(1)=Z&4&OI7~su_Ky)&+`P`D9`C;!VLlB1PgR`aV{FMeC zecbi(AMz^;283vBVB=r^oimq{CQf)fGEhJ3tfnL$KYQ;Ln^oHPb}uE(d@)xy3IdHz z*I=jJunaPx%@UkZQnv&jW?8`gbWJHRL8$w4o6~o_@RXTXNQWn4^VaqgM z?UCzwc+Cf%LV&m|^Td<*SE#@=Ck%wlyFIk;TE(PkUw-~pu`Ea;c>n3`zYb@sGG20X z>r5#Rv)`ywO50|yU84XEv+E4$>5XjV>%9Fd04lZ)8HN{U+KT$_f9l`#y!TboXO^m> zoP;z^W>_o1<#W_;sKFnURkv7|?>@38EiC`!4K|}eG~PHbGW{19z%rTo!=Vaj*O*18 zQ>JD2^hI=hoQfwh9*7MmO&sJn$WX|c-j6FJvSG2=p?lGb`wJ`swSoQlz5A=xGui~y zB^jhC1YKcg9AKw&MPI1uXQ>6RB-r~sxD<9?&&KAoY-Nh9pOdKDVtVCrx(9p4op#2L zuIWLC_V#7CV@^)Ze6Av{+WNC>_!!S=IV;Y1^K5N;fxA^O(BHQ0?oZRF=DE)?gm*@W zhuNL9+B{QEs~|qV%Vx0=lkQ3vaQ6Xm+;7Trohv7Dz()PXsL@-f7*Bxa9|69N7YaCJcQGp}{h`A(2$E)OL7unlZf2 zoOx~Cn@K82`y>16RLAlD$97kKSO_Kd8YIXaQIWU*O2(Y|f{A;R3yepXPG4K&1vDXU zUAHYhNLP8Wth+Bo*6wZ8tx^b$BXak8mnIB=}(&M8c^{&#r8 z$7BbQAsvhOR}WA4zcfGVNKI3o*l^u~gl1Rlr}IThZ*;a{k@ycT^z;>eG5db;vz`3s zCpu9sU>T>I#5D(b|GJko@@CYWU}3c6w-0h9QsMi~ZVtp2>UWVU@~``$sFzOmT-d0p zwD;||f7LEnFL9%?GbCCeNluf_qB5YpO;Bf<>x6phZpN|i(qDgf>&qL5jzi;G%X%w( zdz4%=W`sf6ueXAp5npbGJ$UB4*Y|%E-Dg-*jsFI4RAhsIii()v#F=}w02i)8Q`0iT znLBq@)+4yU95~a|!kJo`nw6E6;5IW{rDcU9)7GQ5$K&tM|IK-Su5+Dpo%22S_jBhc zx91@J02pPK>lB;jCMfK5aIN9pd%`n0g_aE~+WziZ>Zf&g8x8DQ zOOP^}oWd5_P&0ofssW|!Kf5A+yYMZU$R}?L-?<)D+?;%!culaC{!NU|(P@VX_&e4o z<)KOG6GD}p^$i>u3giBWjovQ0%=Qvcft>YeMdpCbqd(swI4SgK!u9s=AOT+8I4MM#|LCkt~vXOFXGR2c=F+r^jEkI_%^= zzGY=lX;KKgx#(IMOR@+OQA!4Uuq0}vq;jcrXgG`OwsZT~{stdoCGOtIMi0#%rt_n7 zn#y7aW$bf3<|E=_LSoj@hj`DD`D*K@mv$w;9jk*Iq#Dk5txpvrmD($>+`jB2P+`PBtpg(o}Kr#KS>$S9mmqVwO>0Z&Hr{_NA|wZ;Mu=S~ z+pozv*kDIIx8XSrL{!=wY9H`?ID|+x4}Xz={LAVSMe``4ho=;`lwcbf_15dQtG}`L z-NXl5W9P13+bS>vYtWgjt0wYphU1hjrtz&lDe!iZQt$|YAo7RQ=EikTf>dL6zjMk? z7zlF4Ck1F+#wYfXG*lCMI&%uF+eyKkU0ZV@9MLD6*Qeq_>0q994yWOi+igjRG-IH1 z2mVBdRA5@-cwG)Ylh2dYjcjB!6vXsUHMqOjoJQ_=9Y^wC0%0~xpdmD}a3=#zmpJ=h z1g#X2j3b>2m${J*eA{#6*GYet{Xe(s)av ztVa8-t&rG#5yPa^N`W*i{jUk=>|vWFSCi=tyC-gxO?_m?gWq@$i`OmoMRt>X4LV+6 z>ynH1A730HC3mo#%6)o*j^X^*`nH{MmNh0@c|5HjaZ_gvU1-s*VE9B@^J{+* z1kRK}&_LNh57TRF37PUnE1Hino-wi}d%}v3e)m7#U|G=@!`e~~iF3x?SxB0GFsE~d z`V6P{Fs92zk>%Y<(|X5^a?(0Khv|WjLq7{8CW2UiZA$52gAL{|E~?eQsi7soV#j=V zM|Pkrs=gLbjWsdcp3KRPZNv>@G;75tRC)t#xkr<3H9oOY~4x?B@*F5YYR5(6srwnd#fX1``T zQ!uV+rwSV52wawUv~N|qd3oN=W}ZX-_Tp<{mT<%;sVDfX4sB{r9V~*n?B%`6slEy~A+3qVKSuRi@p+JeuI zcMMA3MkbKr`I4unPbwjVM`8^L|u(B6j)#5;_Q7=ff(J-;3fkVhyc=zOeR%nc~v=;HG z5=7{2>UvJyaEWKtJ*-ziwmwV`#r$|xoRGO=?^@DhZYod}sw6!Et&>a+x*{OOlaA8jB; z7E&&2q-DLBMPAaB;qh5f6~34YKQbd!7?@KxcwdLwp3~^-5vrGcK*T^D)eMCAzzcLsGJ8$Tle9E^H9{}9A7#P$CSDER#xTD@AlSEjobruVEZ_Apb)BFwB8SwdMpgS4CwvUtV zztr7L6?r%Wn-?D0Um2TZr1^F&!G}jc1!TxrkK~wlbt2Vp#j2dW69|L81^Ywtgh?P6 zl?u4zldD9qCGJ!R{z+AXVRW1aBj~Y^{iQn~&$qQ;_%qoVS&+cuYK!Tht3|TWEeCGp zSQ0=#E8T)*fLndo1`gj&R zSZsBC`7vx?h36F?CWJKj+~dI+jHW0Q9W zJM$YnX6_^YQH1|lj2HHFtPO1qOfC4VAQ@7uDIt-qQ@ z#Kg3&-?0lhJx{|24`e%x6>e|fgy(FrZe&Tafb`*%s`#>(2TAB&; z6yIgxnlnZ#0)#y+qO>Nd->-OoGb)a@id(6}ckc=YM@PGpdG~8C^1nehfP~*j&x>f7 z?6apuY2`mheU9p3n$vL+Vd7KXu%?kBzIM%@Ys4f%KpL?Gw;T1?hlOJj5|HdX2-_e% z;0DF3BfsXxUz+Lzj2-iP70jATIvfZ4xZ z=}(P6X~}=u*uU)&plopsXs{Q!)3juw*TKeJ2uWwX_yp^Xus0D2-8jld#4O|8{q*GN zoy>w_H7OFBhSd@vl!d!&s_fEa{Te;ii!YV4*zdUwHYGwMNnt=QNNuAan*;jP3i^b+ zEI~J8yA@(2-~c3VC4ER|qyc^npq4)j>+z5O8{(q}Caer$1Eh4aoZEg)DE5uYQP{8< z7QQ0`M_{;>zmb1T8VC+kb>Q)SWU&6?axhK$a$D-i`pLL&KIiUh#mEau_% zY?ELjFZrc**`Yg;&~QDHY?mY+Q+;DnPGgqzgAdRRrTUJ&K)t6EEPTVS&%o`*VCOgw z&!VLUak=Cp58jR(eD&gDsE?aKmAAVh5dl^OI?G$IB34E>Tc|^dG)*8Im%lhn8duqS z5MNQFs(0Sn!VNzlMeL=fbh;9KDhLwk@M}Ia>-qPOg(uy)x35t(BBdu)mS}`p+S)6P z`hv+B;f&8w?cm^FSq&lawa;0v*xB{TJN!}#YhS-*yH#dzJTi?=A9ofVvbjckoR!Hz z4mcC1PdW|Ii39W3g(zNzx{b0U(BGtnX6rp*vLnklrr)ZDJsJ?Gv0DJ&R$$2~XYL#C zTWylIdXd=`v+SQ1vd&tz@gi5e5@ugoPFj%G6A5DJ>dhrM1yI}&i$Px-V%J#TJ~9KOA-h$yOv zA0>WWHW(2tS7+!!IpR2l+v*Pb_$2;{#R&X>_G3ha(c;&7T`yhz?Ut3s1=&s3j4L` z6uld6IY+aclQs+O*Nd?qfyh`ZWSTM9J9;zZ70A1YE$oMxk_IHJ_GlZ2HS0@(Hog&) zQ%_zm(6lOx;4#vA*K#nME{c7Esse?r_3bLv?)kUwsLd*nxQF6c`vTU-l6<7YMx@|# z6uiyl$bA38d!yyS@XE9|4qZO-1RCL^Hm_Ip`0U943>RIa1Vjmz+bdM}BQ-PgLi86S z>6aUCHF9fIzCvMNxw$j?;@pTxa91QpR8+Yj=lq|iW<0MV?W)Q`WYJ117?&3)>sqS3 zdifcHpEJU5^Ulsvx|Puc3!rHxSD!U>fpA}5Jk3AiAN7@UAuZ))i|^aJj@*;Cp22+c zkWXG-o;eWcikF&v--5V!WrK*Yn$_qX!#>ce+`cS>;nrnH9uT=I2wd@k+;EscX;wgV zkgl|0cjW}!_-T-IofP@vpz*d1o8WGM2e63|QnwR8q(13uuYZl{=V!Qm#O7*W ziw~4E+8s0=2dax}pDZX_@*Gf_1B0mphO+jK0a=e1=F+tfj+P-F_DEY39IRR@wUIK~ zbm|kF)bkE|Yg_tp zg?O4D6rk=*) z6%gF_Ums9Xmn@Wd}6xj1b~?hwhFp20FX!`7x8bGGlO(_!rQ0*-UYRAWqs1 z`%h&RQG4+!C(q|pS!DX{R*Dbx5dK$C9xBkOC8PRkMDe++0=bxEO|3Nf5td5%)V7n? z|86htOp}gEeeRjq{3_O^;+jB_9EANg@*jY9V#(s(`>z`92DUtVKgdAV%!eb&DGM8HKE)*4#5-}kTO1-MmNonugg%nJqG?cl3VgUzi% zj9nJ^>$ zqsDDtwezJG1w%&I_d!^HFW@(KHW=$>Z!;i7 zar)FvGTn#`E9MSw%BLOBex{*p1rsWg(a@wS#1Q72lLl2966^wG{N4R+c=rxpi~je= zL#uchOQuzuJ?KNE%CP{lUdM_?23s?rCCq`Q#T>%_ZBVVDR2vn3SSY*vtKlsG9ZfG> z60#^2_Z^T$ZkJq3SG3dsLUp9D`M(TQ2MktMB@;ILc>9WrxpjHW0q34GEG}RGFPR#O zpbC(}>daU%!7gotsAiYC;NEC%5~NxX{j4Wpx^v8_P}4GxaJM;h%qnKCWy5mb~p_8FLdAPAAu^*;zmfcSo&;j#0v=AX6GwCi#Zs)p)$ta1OC3 zt}<}b_8F3JD_S15;^)OzsfZPo#NKq8Aj+o<^lbjZp=z@f*RS~}_P}QDJ6*NVp{E+Y z>ch^uy1NcHI9jJldlKl@O~d!&z3~nsB9HQZhDT|jn{V*@JI$YXg{2)XrQC!$5vEMm zL0vKPBc$!UzTYk8bmgIty&e*GV4@2AXZxE0-tP3*nWKMX$9hz{B0*2633}%%EDrbm z?A_WPdpRh*93j+3{B!QVU&W-Cm-pGmj;VAI5=W?DYVwpM9DQi704T{p{>l=FxOU_2=UTBH5iNRnugSapnKA{5^;8vAqe8nDvhbvjV2-v@8E? zJ=UrIpKm^9SSxl$r!t_UDl}Iu*wg9aF?OAkGAVinyN}rd1&u^z)io=-Hl_vTXoZ^$ zs2k4^gq5d0&ZDR@itcDZvJk3UyZ4XerF*@yA%{V8o5{h)rT+>ol|VoWQ*`S{iz(NA zci)y$d^syPc~KnuN&*1@dD<5|znGxnrpE5Z&( zaTUiqGA(T&UCU^WID&?ZUr`t|PM59F8kSAs+M-5-g#8`L4jqT)`c zw}_jR2(OV>IYSR1g#+XC{N|y2JAI>DZSm3MUsf=`rZT_8)`F`tWg!6aM2X+VmWoV357v@a_ewZt|Q<>NzhW zzpV^@I0C|caB~pI>TGddj610ks?cQNNAg;b_P%vDdnaL$b0mKQm&f4Mv32H)AY)3!-{iyR1IR1@Vl&tbFEGbnxe)2bi)94e#-TzoPI>Q)m(HqiK z>3nhgc4bMQ`%vuBm-uk~rOq=|n;{Wg|Dz(d^ZDKa3-#h^he}$tK)e;`Arev}RzXIP z?M3iW29=ESw4vHaCp^YXE@chZ1TdXF$jKD7OA=but!8LD3(XiYCIRXM2JkswO~{7i6Xr*`E19yO(75 z+tA>JJh@av9mFwS2!NA|?9|9CIAUgJ)M8%w`*s?i(P}`U!Wh)NOf`iVu<2ln{5fI! zKiZGI15DaoYWXtJkSx`#Y<-(#K5Pf1;sJiTvTq?;Bi>I&4HuL<;ValSD}6hDdPWnH ztL6c|@N-Pt@g?RlqS@%MA@^8dWK|K*36|o@T>`sZGajVO+0Q z);sMPYAeZ9oD4l zo_YXLMyY2__QTr8dqp|ksW6RJBOQl2M%GgM$y?t4wh`*(q>7p?9WmvHvtWbT%Q|qw% zm8-|j3NY#7OwAEC+GvAyczFetq6J~4{g#(W@;r3EJvp=Ir9q%3HBS(z`f|tL0q|Lj zzH^%JMk8mS@pOv8!6q0Y>YVB*82^n zUb~U@dA03V{GH>PkMRb_CNNdKLA|l`Oy&1{nd=+CH^l=d(w#xC?n2-8iM@U^QsExE zlKmhc#STJ5J??eKR$rFIV|KNFS;zL#W@ecu@Pn3%AkBmL~C@jn&c0N+`i$r41=kJ}uZI&A zS6Mq2U~288`K{0ct8KWiu1yr`4Wgrci_uGV2mu$N{FJcLtailw`KP^*Id6GelC_i32jn>RYvF(nXRF zoZztIsgE_k%W9{_tafcTlb=bP$3EbId*4HCo}r#u_nKDz=e79#y&^F(9);E2L`LOU z1A+=%oe}>~5Wn5fTN`po?#FxHYA%J#Z5EJAxJG6)B8-OC{|I)^z@<>Y6U$g%DeMp* zts5Y@-<2>Mo9XBSyck ziMSfpT|t2)ivxTG7(iVT<6U~aK&?UQ6wpz%G7Gmos&=$!S0xEo$x`v9Dp~IbyYn%t z6vWRyd8?H_8BumXt)~iF1lKX@?E)Hw^wO$P)FP+(kMq8B0@bYkKO33&UPQ~Y8%}{9 z`{GFn+XN3VFL(Cg6?$CbzTeAqZ!KkCFn%caOn|PPaS(3jqEb!B)NNf`_b62hSG*rd zPN)x^3Dqm*Lx!T1KG#Ws9aP2o1a#H`X<6kMsT+yTB0`va%>l!~E*bNY7y4S>w~^kV zBQJ1aNLtt%c37~ODO({1iZhUH;s+b;D|i|1GckH?DbQs9o)sdgyALECmjt5TPK!gF z6Mq~6VE2ckr-UjX%f~w+J`|Sw?~k@J(=j73UAsS!ZmU?MVA4<+8Dy05x3VXmrM&w zRo>JeOCCaKO=9m)?~2qh!PJx1C_U%I-~mJA$WotE0!F3Y=rqWfW3J!h@^4Ex8ZL$X zN!j;HEIA?um z3D=;+5mC(=3KM>n?vS5MgqFG5b5HpflWI2_PBl|?u8Lix zivErr*cXR&4#uiPl;~vd)V&K%?-3}rk<=Qgh)ZLx51nVSwztkY1*u+=QfXXN+{%E~ zP&{h-py5o=r9=pVsy|sr5l2xJ91b+rDO+UGq(8_%+@cAmzQ`R<;6y4MKtfqeN-ims93vMo zZDlF6XhEYOa&f)ICSJ?OF0!?j4D{R0ZKb|zl|`T#fCn~)L5bgVH)0j4|5zpLqs z;WKtYRD*eUALb`T_C3X*1*L3+iSX8tn1y?`ix2MopHC=vGtlN*<>6a$7p89x$p)$T4@aagEu3{%~RQ873{UR|=hwptrUqbTVi13;I% z#$EaEY(sY{9WGK?3j*p0zRj>=9#B<`rD7i~T?%ZYD{f6J>8qh-51OaQPi(!oWa0Y7 z%AvVTbI+#cODmUnl2-N@P*~?anv3tvQgwy>4}a%LWA02A%a&2MU)|_7;toF0jGW{! z{rf5TNh1hn2@Mq}SCn{u5qoMXQgyybE)vc!D$@`squ1rdykq$3nYYov3zN0Xz*z)} zbkmM;9m8;3$+6=CGk)3FE8C;=jGFS%E2BV1%n@CE4}9Mezh2MP(irr1 zJZ2$@VF~Si@JjYSi}^dN{Y*2jXDJf#ac5xM9a;$QH9)W=zCF~BRQNnt(Gn$y;d;Yl z>mzh4mO^w*EgW!)+0%Bf>dD^4f*Kmx0x=p8zNantkug7Z1EB@|SQrW3d791%2 zTvK>;%_8zXJ+%U;7v2-cgw>lzX`a_A{G!mHt<|7#P;)t1?9X@d+bKog){8D480*l) z|192-%7<@Fs)pCOGPZ;Ey%oQ-0mi?oAeCCEMGUu#ybhks&9rhjA^*$vYrkD1%Q=zt zx5hc?PZ~YkK{b8gbca+zH}F}+a6%CBe_5ofPW}fH68Cj4+KEX@(j{_!X8)B$o%tOy zy&@h?Qc1 z27fek{qd0H3LbK&lw#we+VeTPM3Pc{5H47tx)y|WVydqRRO=Tde6Zby-yQ<1NN=A0 z#>%Ciln|@*5@q`vB(lTxH+b3bcg({4i1d8R^2=SO{)oj;CO7qvQat zQQA#$yThy8v-0ji(~-MpPI|2sc=3HKE#!PYQgRtW?k;H+)sTQt_LsFjj7<j!i%=$?VkoekxRMRb!yM-8c5v-e5lT0r2dK^=agcW?mMvGGjv5qV*VjNb}*h#>j99 zMcW_vJu_td?}@hOnj7-TpS;A`jbTPxl{cggnfpPqk7=GR;9tWl=469 z%9nSc^Di&V$JuLRQ5sm|Fih_qm~Y?f0E*>C)A>5M_IufSSxw_tee!rxR91$(&7was zNbx%pZN@V5Y##muIGWvD0Mqn&CiWfzZLR?dd_^RV?8WztBX>CkI!^uDGh%F-7#wk8 z526w^b`<>#ApBHiSZ3gD{8Te;I&W^(6nJ}en};c`(QKojrrOJ_j+e)@XC7aY+cjt0 zw-NRWu#d#Tk{9KAGr(ySV13~G+Sf|&m_9MGBlpd(yju=`>h!SZz8tlenXkMa9Q?7} zIIB}SY0RE09o{MfTGbCR8-N}u^1FIp_I$kZJ#kW~?s9D7hr*Kd^TGQYpHq|-#}VB$ zvuOY*27vh7r^wHI+{=oQZHsw#T0J-j{2P!cF4m5Ame<#xERLF#A2=YaqxPKbK;5-h zJ~9>deqVQyiW6T>NOH=5RaU;z6}eKb`sp%Z9>@rq?FPFo8E)HK3`ie>$^3HtrRG4p zbd2^h%07y!8nLLk{$zS~-# zOKCG#^FYB2Uu+Q}s-Z@?wnbWy&tJUYaj}vWGK^ByK2$+~tqkR>X1SpQ8zXWH1r#@& zQT@%F#Q}Yv7dFYu;lVXFv6WMcJyYlN+@hu;{N-;oS6R8W2Si@9WbkWQxOo8t1D@B9 zZu9**&x@MnX1u(nvXB!c?w1G46}I#w(2NeRj*n_=hw^GHj?3N%>R)|7%l${*AT_C~ zVTD>lOIT{K&xWG)z5gm7JdSO2k!(8Q%|TwZ7CwsuL0_7CHlJSn@%h^kpM>(fJh*in zV(vx+=gf2dd|{4xwN>sK#C4j^k&rL0=0m7jlZaeIt8mh$vQDL?O568JgQiPc>@h zK-072-A$$u-17#UdrPDB`Bv{obz1%Jj*`o~#v;hKW=i08cPp7f?dN~D&RYl|GRW#v z&3RfDBS6gpv#Q;m_E;AJ6|ygnuxO8QM%2;wyr*yrt=F=aLv2P}4?eCxdai5qW`NTl zD0#-}H&^$0)H=CgD)qxB-Npsl*v&?p`z~5bud&boM{N#+7{2jCFJbQ+2};h z;AomA_}VY9p!hww5%F+&=;KOLBk48d_f z)yt9HvtfyU>u4m&L=ki#+h==Gcs43wHzO#{5exA@6S*KBQk)#{#G`_ACzF4ZR@GD7 z<(#@z0$M%i35N$BgYf-F%fGlgynE|}2VEI((Uhrv*NRRLSQ>d**r+?a(4_WYGb`=P z_gd!I;)UlU`Ffi*5p1<^wV+%BT}<|+!2~8T0i2J8kwYgT&c{ug(*m=9_{^mz1Bca9 zFWNdPr*D6|-l+z(8o0b>sTTpx#Hduf8VUxFR2{}WOvql2gh-9b=_Bg1kvp>GkzAdL zOu8H?iztN5{3`;Re@#2sVA0@uQ0J0>*zo>mE2zJ*l#x@q-)4$@Py6PZ)b+Qz?Q`|b z7iYbb{`a5#-Y@wkf^Eq*yaNCl3E|>bIQEc>1WbVnP*6a+SEbLQPwK}d*1C84m@QklwE32bTXe#zz5gkZmm>?qsVkFUW=V$ z9p9YberjTUq&a8LN0NLHfc;2173$5G*7lF6*{%BSi8TUz;!Seqacl$O?FMpRINvWA zp?evu2r+SQU>t9!m5w}cFiS$qz*4Jjo%6bqv|p;$slsK1Qx?b14Z)8Z-!26zzTMfL zq(=BGqn3qR4LvOw`CKNdTI#5S5k`f|+kKfg0_tO@{C4d5xS5M43a~e9a_!ax-ltnK z{U$pm3mS_bnU%k%TMg9tW_CB~K;2vy$vFl+4n{U{Q1?If2YsgVnz#QufuCXyZKKWs zD~Y+X@)8XYfx%HFG8BWU85({I`5z_+zjwM&+x-xHRl9c z=UB+tn3VK1H7}DM79xPLta`eGrVxdewfCc9w}}qev|9td98Fixd@tK}3@WD4-ruk0 zO}T=>Bl_+SJ%LdU%7c}y!OXm47bNfdyEie8L65J!f;~5~T7-L5vH~=|X7@}q4PVS^ zx2?H(z`^gL;$??0bc*t8IY#{xivbVRj-Y-LAc%J@YZ!$z%T&|w%%-EKkVy&lBNZ7k z1c(imaQ`jdZ(YjQRaPT;WF^qouJQ<05@)X*P_|I0So6+CMs2pJZKDKBEM_Rw=n27l z;C81K<4#;8xn`QC`Q!tm)I8m---RrX6(rxRHrTT|Xk%vG^+XDrO?)l)o;XyZU>FW{ z>&Itgb{l``K@M!LTHE#$vbv7%%r;mt9fVF@umTRKmaiUbtKzfD3Tk!eT)+4sP|@#q zN&8JB?_&pu6V#C>qL>#B8nj&8^DJGqq5B*3)B%c83(rmL?uboRv@)c@$s&QANk@h( zy$@T;aXjCpr1`OUMD;*yv2{Iuw)OOh>!D^?@QvT|s;P=+50sR24gG%jA^3-DgtlSb zNaMa+R@K)f|85F=rKG*f`W0uyx7xZpKTtjFz3cPznSWwVbJyib^W}X~z&EBw#R1gp_`%nGs_v7{kjm!ucr9K|W;L?UPX*Gy2%5aD|!pOoQ zNRVZTk~&631(h!^fB5rBvA`%GmKA(2&LODRsQO}XpS}8@IcL+dBk;3xgdG#NV#qD< zM}|hY!j<=*N%x|22Vd9UpD>Hl8vkIs#%Q9Fce0^qV!CXlZ; zTU05~3zvU6e2xfTXPuT-JVK7z?JP-T`|hJC_!V?TW@0(vKsXlPRTfK2j}Xn zSim!xE-|NNIt=W+P^xvA_=&zhulpAfv4Fi6%f!|F=lAZd?`%0|1rq&Fxt+36tW;Eu z!?(B$mIO?)-F}*YY8i*ThJIKLIr(t2AQa%! z90ZZU{7uf$F??oeS}fae8!*`bKufwO(C~2)SW^Tx5+ZJ*k@EEDThI1bd+Dh3=s0#o zKgh9v1kz?U8Txv~41d=SD$x|()_QuIO_*afZ0XiGlA&er^euR|FU!abx=SS6$wR#x z(h@QcQS0NVG`-KbeTfeK6@%FAIuWu?te4#ZyUK$N0%I%TEm3=e=@_SAH1Z8I^9_^EiK*iwcHWWuosndF z1#f&jDG!o89Ts!sYf`*||A#F@R))5-HElSIc}+%HN_gP7R48i$9SJy7vmtZPN~LTP zINSgMwmvBV1;jR=hEIiAd3-Qcd!qnG zQ>_XB(p4R2qBpid0hz%ueRkgV$Fv1_iF0?MZzo#YoaMReI3F{j>+#2enA(5t__G~$ z&%ErOh17sOE_$8~|L%UOm>3IC3?YT41E4x0$dMmhh2U_GjJicoAcVGSr)=@R7tS4A zw9~z%+_xhCKi00x?+UyGrRP}~&q)m(gjKeE~g^=Ku9q#X0%3;gpYZSxsI7z{cNyd%jHc$Ag_?iMyQW3;CIhiYQ+1@W{7pK6tZbIEK`x&IN|4z#K zpidtnrr5m2p3jorXapH%$oMe9eLz!PfT;l=D05pODhQ0xF_+SWAj1{VI%R)1i|Zg@ z;xbOQQyQSnT*N4XhU63ZvhB}HqS1E29kAvRXcUq8WKk`&6a=Gz(>lFu03qrsidhPm zewkuIrl@)K(6>F->k($}TS`2qRboYISgKVfrAQ+3`D7HyC#pvhAy+0L+rfOdnv7FT zQg&UMC%u9gnWhKC3cFHnreo%J{32m`rb8|{7n}MQb2OEarn_jTh>0ek(hZq(G!8CM z`Mr56&{gryj5BK`xvujHgne$yOi^yhjoncCUotMMGl+M`LQ5uuqXga*cm>zV@HSxk zN99Pw@=me0ITqRL=<|XC_Zg(Rd+Gct-4_z6_0H;IW0h{Ci{pPFm_FF(nRGJQ??R8& z1(zlLyL{;oX55N7vXm%2eXx9XII0-N@Zg^u zPH?6w9oY!6{@$y2tq}^mdjmee2OsDI?qtf2@-%vh(48jR2CiSFOH5EhCDjKKkuOAA zq~nfLzxML^HKTV8=Yne($~DYT|3a}-p%?~#iQE9lf$nR&{4}bLPkz_|P3@>X>EL{b z7MRNmr3&iVtl+vUk+B&v4K|QEo<~qINY@oy2R20=R!H4M(^SC59l$-4pc9&qi73uS!XW~KO8AWy01Zk%eZJmw!=wsaClJtJZpC1-~b(^intdmX??as)C zi&Wwm;7)Nf{Z-YNG(+n)!FTx zRQVr<7|E2mU;@&R*uiLNPp(v}6?}`LdCN(E)t>BLzwfn^PM}IfIv3Gu*WNnS_TXFF z(K79uZQ28a+Jh=ONvEzFE639k^-o~oni;UrR{hyNFq(t%8$GhlM>g5%ES)|iAuRfs zr%F`zJxxrkq-E~|NFQZf|C9`^WavE*!Dr%y5G@2Iy`s?nuv8huw-0D3!I%5w-o>Lf zVGdm?vaKelp4J-&yim^=xxFeK+s6|1V5+b|5ptPB9);GNH>)9Esj*Y4Ewl*;_c4|X zTT;8zhAjgsTbyyFy~iA3*9M03@moqZI5?(pd0$QLj{(%t=GAl6~sWo?Az+g3cy@gb9i}N#Nvh z(?!p&lbwM-gA_oWka&jAuZFlD(U9@C9aj44NlqSl0+sI$q;RG!nU@kJ0{x^DH`gGU zL||FJ)xN?1M^F=ZUH`7BTsR|nNZhS-laKRMh~lGFm)Y@eDHC>ep zxV;91!*#PdG%um~(U=14iP+Gdx(--2QFZ1Xj3mJhGoYHgVGjTZ@?Zb-Q~GHs!agyq zn~1#5KfIF(`4FFDy<4ti1GwKrX0J%vlZRLm^-=Os>d&=aY#h1E95}>wa@*aR7m>6e zhBk_572C74>9??S`iZ$Nr>cVakmSM55ba{^zy%kucx!M1+a*kaczRwLHY2%LiCybQ|g z4=kQelD`}ZhC~oS{SDH$__FSvcMnmm4&8Tu;5POnWt7J#v>0IA{XKu{^fRr&O@@feD@_e-W)D2Y3LqcQR``gjo z%(XsdX8tAk`*1k#+}i58cRUIsluDNP*eb#k%CQq+TYTq19@5F!b831CtAzRNVR$%h+qnrLkB4S|! zKFEV#Wuiuz5c>hy5{6u#mxgW$%rhQnDw3v(V0}~mE5zJEEXukPy#UhdnH+#+_sc3@&rtp9Iu0QtG2$m~RcS;w~!c*2wUNPgVrMADK5 zt#|{fa_~558h2vh!k*G&ZD0vVfvlI>T8`)*rCm?-O+VvpSM3QMB?@mhd*9^6>-4Al3S zd1t2^r%pZ*nCU4x)@GaecZXf7=Sfwy^E>b9ET^Z!G2|O(T(@D;|3}e%hb6iHeE?@V zP!XJ{Xn=d~Jz_X8N4Q5;I8!rIGqZVEBJOQyX1HgiW*zldfh$*qBQvv7GqbX?@|1PT z-^24i|J~Ph-(No8_viIKz1^(7I=z`mnT$uIxFen5s&aI5!lg$6?S(hXloulg1-W1h}pWRGHi|50VVvH-rPWp*$!la{!HqqrSD zPzx{n6tl6tJ$tZpV~Z&>3y__`$f|V#=O6!>KAA%ImD;AsJTEd53D_6^4Kv~#e|sgE zI^i7Iuk*s@f0airpOeG&7heW^@e)Na`+6ivT(iaAqnF99!%F#;T}(-1oNFag*(_1L z?dR7g$LIC@ui2dV1(_@}tSm(L!$xxg^vK=iil8r=^N%RCP8WX#_PixW3|;zDue0^c zKT_S)<*uIb-s0@B@y0(eHX_tG>2|R5hEV!5f!nSho@&+RXb9YH6UYdx_`of)do zy!=De;of?0kfE;e)21qO(rM4t_be0|1QO6EcK>p zA{^jYVWGj=oNOVegL~5XdvAQbq%m9)a^G%^s#$1``3cOT%mRcm$l>Em8Jp!$l9Sy6 z-_%WEZ(>+>zXRx95qy19BCam0y~-VDzbB6*+qpy1Mz%w(qGhx%$M}2N2x&669PY+hzE3s9GnNE82i3E6)5no)jgT;XFE6Yj8#IhgZx zQVWx!}(Wi`|7);Jw?+y^=OwQ-mNTOEn$oc&_K z&sMXUB|0@;-iT-(HK|UbE|<}^q3m*xqjNb?!6pn9ld-3LHG*B)%nd*CZ9cX?jmy9sF&XTtUuaZ2dmPM`Q@PO3oF6ps?=l>kmtwU z@{B#}Icr%?n*$x_aPIUARQG;CwJnce}NEmbkNd^6wjIGqUIh zu2;jHQF?2%G-Yn{v9#=#L`;Ebfmrsu!xB|^*KLb-@*a89jN&BruVq64sno}R0ruAM zu{0DM*#$#R96Fl!vGC?lmf1X%+=`bd0L(O@VsWl@u%L~fWg;6Dy`ygG;an|Nq=l1E zF4?q8VaVrHTM=~#k}^r5q;DHE1|eVH`Ov_Kb|}i&dOR{jnHZS|x=pgX4h)gilIz@# zY5868mT43|oGMY#vW+(IOUL;&k^6LXvN#k@R6xmQdYeL8BEq4Mg<@gqq(|#~TB;Sk z!y`3=v6FtVe8t-D3U1y7h#-C9(F=>;bbZ=5@Kk^+`en)<6fauYKjaVA6*CtAD&E=#7%fUNcP%4{M+OJ6~i1@KL%q z+obgh$F4l9V_9=0bB{R$lnV3Xbbiqu5dW%I$@>LSFgknH5SN;V7Zxhq)f5Jw_&ev2 znM`I}tHxjBe?vZ2Wkb_;OI7jwY|sKhdcJ`vRoT@->K>PQHKzfu1Z3mUQOM&?MFs5a z)~`ak^U9&&|0V;VXle0_qa8!4k6s&Ck}E^l^N-KYVmR-E>hhB|Nx-*e94l0AFm6D2 z@(lNyHhDq1NM;15gydi^-h?SjA1jO(2pn@;f=TuAlDGd6)2lF$IjTTv9kCMkji^WJ z0pw4f|7RE*X-*E>!YVxG9M;?Wo=Dyh94wJ*GIYN9Iq*PrSxQOo3!}s@xjCT78bqg- zrI}%gktm6Z350&>#FCQRseh&YTkl5S?hYLc%*4EDwZ7iwBZ^Pu27I`yRa@6HtzX%_ zzS4QAo+n8}5S7GMC5$&|vSpjZef8La#E2{nO#ojMo2DJDeO=ycMRPEt0pf&UfQW&B zXkp-h%xG~r$^(#6mnalaUx{CNqIPI-53#TUAT#WTsb7T?NBsO6gWu-j&1?luHc zz%w@i)EaHe4o7IpTBo#`zg+d~Z5`G4z3t(mRg#}N4>msUY2^@iPAT(jbnAbM*$Hdr z`HS1@^7~Uq?@CSvb%5SjMqJkmwzjOkO0!nzoA}$4eK9y>acK1pRq{aJw;b}|zw&Xr zuE*@Y<&dax$Xjy(f4I-t+Kal*wzadDLwA|`I&o4A3Ai->%s!O_*q!5Mg{4CeYS%P= z?BBg|FcECN&u;UWJHX#K>)(*U#rHQaerV`YH#v^63k1SN*xkNg#aHcj7J_dN2&1iI z4X@rhxtC-0)%4QhDFMgxDH0Eh}C=3YG`TE%Z*VXbH!GX0(P;M~&oMywy`4^-AqwJ&^lwN7aNDwj& z9^?fUwJrVq3OkVCd(7KxS-sWO_N(drW9aUs3IOvL@-2exG>nmeyf%y(mkxOUR@N?+ zLkPU)ov>|fnPHZ5oXk`b(ag@Sc$x;LUfcD_J1c3TlAVKFb$bI(@9 zVJ4wCIX_WFCB1U_+}bvY({u;d{T!L-NH7=h#BmsYYbnc0et+^3m;UqXyrEY7}*vglmR z;R>s#x(xhLdK967aI@yoyT2pza-tTqlQ(lJgwoF=bF!l)rxsbI+^dJSvj*B30linB z6C@vnLi{tYS?d{ZsmT5D#1eRg<-_J!Dwd33ytadU7nXhH+XMFq@HHOuUp{cuqJ9=9 z3w&2=Krx!c$-WNle4QXa?{EV)vCoz-|6gQL)bGAe2#r62f&D1v7DgUwSh_N)skh_= zn=8YvU2{$Z-x)^bq_Fh-yWkCuau!Gh3#4(F3b|Orywycvv)sM4j%3DknkhdUQSlLr zb)QjGNX0}D;IKLcF@omwf5ynyPRezrq5Vz|rf{!%keaIx%DwMu<%0hQ=rqc_ODG0w zMTvj{dE%=4pZ=fUL0vh+am!aIee;+4P(Jdv4Wf#{7JIZ$rbN(k~qQ% zhO-pR7^##+qFCeHqg0j7#oiVgJ7F9u7vGXdwrSnWdcKv_3}93F>;{0?Aw8SYO^Dwj z*eMgF6R$ro!3z(oi}NmRXP=`;4JeHKa+NxX&rZ`5zd+7rcgYCNbFN{a9ZM`tE6PlpYZ>CI zXYUAwCVr8c|5O52@Z7Gr@9OjIJ%|Sf!qXcam@T^=Uj5=3YqhOdsiZ+mAiqPlHz3>u zuR*NGLG^~R8w~o!RGHQ@d2K3qi&*+i;yT$5LWpO~5^LA1WWj=t_B`1W=rM!}4tbvZ z28RF&>|tPp7q#Dus>EZJ`L9kKwKxA0srg9NSaTAo@r6s>hVU+!8V;F+{PfTzV5PjU z*8D=xO{T*N*g%Elj*z!623sO{d$>SiLA{QO!j^gAD=%a?;o4eei8hU{RdVxnTK)gV zi)?>6^_P|PN6S|jd6q>e5OS_b* zL?|s82}B`fuk0Nvos2T^GH2?fKn{LsAr^Nz1c3>C1>IlRNGdx%RB(K=5=@=C?Y$p@ z0LI~)z~1~@!XB3t%Q%#>`7LofM}BYDEpVo#Z&po~kHs`Yw z7JaiP#A1rQ!#9zMyAOduL*AJXXDUP0j&a2qL>gzxtV$u)uOZX6Pl<-yY^Af6X@UB7mc22TcpL-Y7V^{PRaq|y_Jn9olD?+B)J6AX? zJfB|ZC^-e1T}>!c5$)<~Dcm!#|Bhg}JQS68G2T0x+DgD5U7eSHC;Jh*&jtYb3RE~! z;$TZLit{wyq!#b;=?Y;Wk@0%M+xYB`(I5<>@P*u{RT%cA?q6DwO=gx6G;n;@RQ_k# zZwG}((M5(93*W z$ltTU{4F-)cYVaL8tGA>#_E5t+{Fnx%RVM^#|Ei8rk zK?szL2J_`24guKtkz$Dj?5sZ6>Fr9c0bcYH*&o2?YlT4awU5+ihpTEwoZD{@cOSrS zG7#RH!59q!KHPNH%hh|X_5l<1ZcMF~`m#gF} ziXC2LH@I=UIB!Iiha*1lI2v%u6F2kx@(Zp@+C(}%!{(m z*nP?=K-m=Fr|nKB&%)Uu;EyoTK8oz4`l6b2um_p>4c0kx8lv?7NF`m7c5FYkQSM^r z8xHt)EA=&EfQ(>S@iyx}ENidGJ76^CCX4o{h|g^dMXv16OtO=Hu~yx_?_JFO6reLS z&^G?wuNCb`dmdO45DvoSm|7lU7l;RW))%}O>p7-)IMrXSrr zbH=EcsRLj-ra+9fq7qxHLb=+sezIi8R?DESo z<=!sk3;oI${KQ|_G@Zr*%mKZfOGv`yz#nNaxrlnO7F7@ z+PwqbK#3ESyh=2^7ALMFk0-Y3P!aLLyJql(gZXiXUQReUb``zX!SZ78D@Ba zU*E%E6MfixRvNN)glw|Yj!1J3jXr8^MT^4$zlzq@ zG#@(@N!@V(*CBSdkNxA$w31x=9EDXWHB;g-t?ZbV2*&P2nIWI`#Z2M9k50AU#}pIH z?5V{DZwz-42$23e%j(YUux|_g|Gf5}clt8^O|R%>#XI5(I8GlakL*5r*KffGu8o~v z=hyD>N}kH8SX&pq2AK6%;y|yT;AZ3s4=*@p-L)ET*l+Q~{EzXW{^bg*Qk9>tP>~hR zp17UJQ%(cvm)0MI?*(B1&vDw-9`D>%ejoU-orx54r)u{j4p@Wl1>3tb5Qv;>8DnLA zFYEpdHCC%S?G~%~AAZsviM5IB5}uO()OG+xqpSQ^zK=pD>Ot;%fsOl^jzUPtZgL{v z)ln`>osu2Df(*x3#q-*RVnW%6L(g3K6i*gUkFNR9oSgpOr%M%VS!r=42!SIboCVzluYIv-(*z!eQx22 zC6BYO*~S*?u_LL_*ZCmbR=SEF14jlm{>_N#f*>)N&M!GK+HV-|S+xKt_%k$qg>w`j z7EjIH6H^vrW7e2mS%g+_#Mm2s8siKv`#2^$Zbho|Zgy%Wz0nqO(Bb)AN{y?O_zA@C z`yn*~z@5c-RxLuNau*VkDH)EGEd!Ab_A^&e>2Bfe)Y|0pR6`)mFUW6ZgIU(CNCWGK{yEE^^1NUhs^FIl|4t8wI= zX;6u#6VkDG26=E>7Vn9x3|!HN-Lv(_855wJ{e^BIj(yq+zb-l(KEz62=}XuY%Ipo> z99ObP3N7~KiKd3!cjT11E7r-mdgv{lsQbx3x3PaV zMvWmCRBaW|lv`D0<#QuPzNJaUI6*9xh;1xUB_u@UvgYE}1su6D)x-W->#BARr-Iix zR;^bmEbhz2F3H7DH<+!Bch@xwi}sys59PSyNz`nB`EJK&`3i^@CHz5cE@ z7TB@w-ozWN1SzExrINGK;tx+j*1n>x`SbqX=EL05361jI9|^ zdFEfqT$`+^OPL+7DNOo0p<{4wHz)VX@BVt-f=$mB-NMWvPveq>HJE-yY1C$&VU+3w zHU$ICRh~UoEyH=wETic&$*y)U(ks9vvx=9(TnTJzh7$ zuUE&8`DUQgIPU3z$N)8R(2Ph0=XAmL`P(asW0JXonIVUYb|o}!l;!W69Wy0bkB643 zpA=+9Js6QA>Tv`jk?S+e}GBQbM+%aZa?vluBPZQXJSF;eV=GiIY}YXnr>yFr{)fZL zOUv|pQ`}nsMQ_A{Iz^yCE z+l{AcX#Yr&Fm#uJzmJ@|et2&&y7+d6j>DqsW2f)Wj}Of!Oc|7*(c35l{>Z>S~Us89%2#w z>v(~6Hpm?B$OHMy7BKX5566w?byQ+?fp8=!ix)p)q&KlJBup{1Ym9_nXdRY?t?C4( zFhUZ!B&B&8$aowp<71bv*-JGtcAeIgNRaTn6RhMYa)E=oE#B zzX~zd384_Z!*vuxI5csHtF^}6DA{t=d*1^;X>&`3f@Wx%U?nEb zWmw!*d>E76FQFb0XCrUqhe;WKbK9cXvir7cq7NAM#9~Oks;luBYK9Y%k1H#`T_*Zl z^W;_j#^3219ok8f==-oW#d2tJ*NMnQX;m^MuChV+ z9&=LK>*b}&$$C`N3GqKU2mG7Gti9(URskg3ZHxpZb4b2FFJi0nTb^e=-vU&6*vtUv zAl!OwxkC~6FF6;Oz7A5{-h~=7EFDj*#M#~9A5Kpxk$K^)t0taXXp<|SzY{5TcB}() zy0_)(Qca7^3qp3%yg|rDowdU=JJGcETV8s^5afcHtg+3Uia9mf_CX%px;0WNu+LP2 zgaT#vls74xtxBF9XW~Ak76rHBp{;r=S1IYb9@rELBb;hV`%(~0#vxCm+|674LLxEI zcG<@6>X^Fx zdvP;*VEM_M@+$UaW+%g^c(F{=#T@31-lkN9BXu!;5=QSE)CzJHm`SN^Y?({ zz_d#H2gan+YJF`K@~rYzL3Y!ot2WU)bl?IYBSttC9r?#+dv z&oMp4VkNQDm+vk=_Y?lx>E=r02Za*V9GAOhC4**7eeKd|XVWN)Cr0Up+IuWYtn7x}))j$Ns~Dj|e&T zNzjm!oV{U>k%{80o>iQp{&+}xg%TyLR4=ZJOx*f+O--{Y-1Cdd;|nUK8Q*2%iOckb}^o`uBiGg5?KZVklI$J)|kBPr}FqinV zu_Q0O=A#^_KFcP}d@)7%-%Fuq61V~=2;N*3v*j=pwFG%*tbSy1BkRk^iu$_^7K}vap z`kQSrQ${f?W1G3}&tmqSyP8MNYFTH$EY4279eLLa-=34+?shE#D0?yf@<7-YTNB;= z(z{zC_u?yv?xfd`&nAaTjM8GXHuuenM!CZM@NE)FjQwnu5Z7mXT)lUEZ)AifRMbJWTpSU z-bsDEwTt}QL69(^tX|*gm0?UMEkLm(jDV6Z@o!+#ZWhA~hWwq*1A4w*o?(0AStfS0 zuqpxG?|@0$t)C>zhjSMsyN}#0c7fO5yo_>{U!(^thdts}sTEi6-Su0S%=%K1*H5k; z@5&!@!u(s6)%q~YHE@Ik89mVDeaCJDGE`R8CiM$DI%KEn)hg|rI!efK^;iyTyFQQ3 zr^7H)k~mfOpuEo8qO)N+aa+SRra)3|9@s-@4Wj=*viv0IU0(!fvYV>iP}CyPjKZ`y zA$CjVfhm<~o6k2`Cz>F#f4Va7Jfqy9i^;@Zaf%?FnP#kSjamJER4A?fkKp+Lff?L( zD1E&!Wn5{f`-*?w@eC2ot{)AuBMB!kkmIhA@wdiU~IHdmp5;6N~Y7-e8N} zaZ5ms@$Vs91j%rnV6i;xln*woVH)fKC+`kvbu#o*h`Q^`dgY);Sc=b+h&+Ccsa>(c zARgf=kXZ+sD-0*4lFZ^2=?Ww)g=o7)Ha8+^kOg*T!zWN7_Ki&2U6RZ0u){Xfxo_CN ziS(q2XL zZyw~76k>h>*>jb=c5w)61gg(R*7en(UU=2bOuC|=9v9p&`O*WlycP(<(r3iI*T-WM zsTN3Aou^yO%gN>gF*7{x=IS&Kw?|@1duMW5VQ;3pGfiizCo>}1)_0ZvMG2u;5My;-|gz)K~Br!Kefa(%M=oAKg6EA9e3Yb2McrWiRT^1hi z>eA*ad&Ix-kIeN8)UfS`+7qsD_~>}bs2gj9Xykn>GqCC6#to?IFqa35;`zABv=!8< zduJge*s5GQ^8K`9mK)S!x?(lYE_QmHqvJ81H#7)UTY*j15JW$aGYUj^X(6`gh(@2A zDBwQtD~8hInK*f^HIEa#VdcGD`iXUgOO3Dnu!;`%bR7&7o%_HS2ZrM#3h$pdi1~Ev zu<;sBF6x{DE)IdC$30FDttq0tJvIMEc?+QSm2JfvmKY(LDO|-SN6iFnT5b-VBC{PX zkeuh4b{w{C{G!Abe&2=Fn!X_gL4=8Fu|hXl*Q(@s)=+*q(R3bUxJj2s3#8}yQmzDz z-^9bQVB5_xb{xrggKF~Jou1rtMYVWhd^}v^)=OZ-&~m3LxBFYF(&-SdKYRrRVrUNyV>fg#PVlUi zFWWa^P_E4GPNZ%FI!Y6hpU|K26>cJ?44}l*1_HU_CB@8FT|FY*5`;lBhBE!{EN@gl zd8w)gRE?m!J)?KS7^uctRTcwzf}zq$yF(or^wvp*yLp%#qwr%rE9S9BvJZnE*7a{Z zZB%jWb|q|TsE-J!OLUCH6iTld)ku$afRN1~UamU2KFt;Sqq}Y_e?(+w!W+p_SC6<- zUjN1uPJpRBHMA4bc>TlhSRCOtru9kONFza$$JfXwpthTLS6wyv)qYfM2jDX@nHT$g z@8R?(shLi~se6R`HNf-(Epk*MIvxmP5m1dXpH%2_K8OB^gR^GYhqU2m);;(I56*`d zMfb6-BAB+Vq|50!7sJIiyoc|*iT!os`q$l-=N6A?a`D(G*}s++KE@9lZSmo*U>gMw zyDh#}Jop45D$B*=1eP?2CB<<u4<3Is{!3$)ik&#y|<^^E7e$LaGU@}~6)W+(FV zhm5(PA|+92E?GvJpg|@bCV;IW!MfjxdMk8oM4MR($pQ~l$Y)x3BW!z_w&hVS5}Kwd zp3myQ)ltmUEbuor$$_t1tweIV!D$-N`}am$-ZIgaa`{YZ$bRaucEphB-t<+MMTVhJ zp-1Mu@iK_VLDtukF^~gI+m~~Vh;0!noquG+#pHG0K)q6C2M1xTTT%_zft@k(x)Sc{ z9J3;iWyC}l|h zftAKkI@Bxjd>esBg;hV^Oy1!GF#w{f!qCJ;P>Hio$FyJIC#i}^IgOwr_tDkOv96}4 z6D^uP#tA`sX09XCI`fOyCICJ*Scuy40rd8VZUqp(nP3&hdentdi1s#p1{Jo>z39uD z9DmqAce@j_wXoMCtOKFpaBqI&HZ=pu;&?X*InUf8Ie|8+LPi9NT6tvO)j6hZV0zTJg)e(w+$GSWRfJ_EYP z&jSx66jJnUBFNt;MxAcL!s>5bk;&I-nS5@kE3R-&N z`#KjrXGFt&ql5T4Y_vtHgV7{kCv3j^?h${-CbxZ8Ud<;JP`p|F>*3Za)gxb*W*eUk zI-EJ-I}#Wp2z*Lc8Qk{Z`#^Cj{d;o98somx-ICs@9Br|IVo+Vo3OCi5e=z`p(!BI* zt0zj6Jr^~jo-VkX@R*Y656*0*;YyF;_MT_7N}6|o)o_-IA(oc5eP;MumOq}aQ@%vR z2GJ>&%v)ECTRr#X<4xAbVzy{Dc(4Vk-VES*LdL^7-*V}_;=6RZXl31Eoor%8e%)=l z5tp2xEp+~isHZSw>Hy53!uHYWo$5W}Y9l{T#; zLkLhtg!+aGL?0Rl4%3b$>0grL;Wcr-y1{B5q!)Pdzpq#G#((Nk{0fHNHAt`Lps)NK zU>L9T)seOSQcOH|{pQ|O!DP{&n;wy=8f8;zky?EzRqE<)EqbxD>5xEMkOY8@vV-Xa z(h)TTtzy({9Km1;gOjA^V!d!P(mmS;{s+v9N*lsPy7g$~g-!xiQBQVeqfF^) zR;5a)vW7qe~m)A*A!Vp}`N(xc;|iOnwiYzDUqc7vF(o;>I2sc@&y;FzseZXZ12I z9Q&B0zUMH#la#&)&j%rqyQk8lihmEq=kt%g5tRZCN#xtvUF)+evb=xdl*9T*iBO{S z*%Rj8lX;t&FCKi$!W=3)XKOO>HyUs4tW909Ty1PB+qquKRV=%q@_eUWtv99eM!#Xe&C$FR zM6{LRDc!48Mf*ZZlH5N>-#zefEv7m!f!cE7CCeTBz+2Ut`Tln+ct-k42W^jfFtC0d zhIY(DmnJ(@TGUjc9+C@HteZzy;>?Z^nCdSM8vAUmxu^O8%Z-MioPnx<|{%Hf^_q z!{6ZsOs(jw>>6#5Im|le)B1%yOQcecFygIPCNc!);k} z6Sfc1e?2T{jrr=y-)^oGD<{`GOzJfA7>Ug`8Eg7``Zm0Ub}N>tIo7C0de~dvVK%M$ zR>RrCsk;73m~T#QV|Yi`L%@F>Jri-~+KmSEp66JNZx>#%tCGGY@$T=g4V6SIqP$F@ zf%laLgyMS&Soak-?<4G=>Vo4NNsfl$8<#Wgc-!@AAA-GKoaWvrrjtBhJJ>sQH@Vm` zt8O~g4H(`WKVBau`{U=P{No|1=W1#D&))d?_!w}?`^e8X=1DKlN}B#Q`Sq3_evc$8 z?)|MQiB%Fe?m{Mes0K!*BFE5kT4o19B~=@j(^4!Sx0Rp-9o;~nIG;C}=l zOy4@^K+tc=e|cz6y!QwAfi4?J+KXrgc_2g6hHrj zYX8RODM?4FGY+^Z#F;TQMUhCqE`~yBB>Xf>Q?!>JX>WA8EuGqc;%%0<;Gf+0&uSM9 zaCPplFv{!VA?X`tY=>>ER2Xd>yJcqNh{oZRqQf3go6D@mvC5)>$-afm zV&)1Db^=o{5d)fP<4R*QE>ykROYiCLM~od?`BEsf!OAzpnj{>Ui;K3?6=jIC;0avP zlN&ZlkGArSBLecGEQTwuT1y!puks}3N{Ziw;1C0n(y_@nOZNDB(zz)$Jg`Ce6*}vf zc-C}GTB-06(8}O^!sZI>hnSVos8!g?Flx+9Jk#1Z?cbi)Kv(pEzq8FV4W$@DqG}IV+Aatj|7P{@#l#S;1Xq&tQGE9KOyzxz84q0ERE-*W zH||?=YalDtU;US_x&ZPk>O}s!`k9fo&?5#Vs6`>ny2R@dr%jb!F4Ex^S-GBDc(rBH zHI7g>KXLG}+RYWZgwbBfwW9#23UKxs!0q4r2Qx)2JrY^vt*&8|bfQ9Lrl1nF$y5o& zs(j&z)#!1ZL{m6Ce>_XOStu1r4N@N|9+5i26F)`PR6gD3;c~Z zzQn_dFk`m&kcH{L%{biBv5wOD5sIzaQoz7d;Oa6jd-`curEq6=!OnH zD#z_5F)w>qDkvkP#9Fn-Jejtz0S8f7ka}3Uccb~=6_fknIGL^)(J?$>ynfuZ@)nCu z)B)W-4;+fkBUhnUvL*@B1PR%xp>)c27fS|L-qFVz>ZSHv{^?d?T$dB<9qfCmo(L^N zkrWFp_E1$5ot4Pd7VI^B#q3UO$**azNP}56HG-r1>l9QEjg(+pO&QlFz#?5p-1xNQ zt49sr+KU!CTpH;~jRMq4pN-{c=1(0Z{c5kJ6uAj43tju;Do5)5xnl_3lJcXJE zxwtr9r67mE45+!+`R!!RrCu9dqFq6nLgXm~6YgFEwU!vM(Kn7eCQUTHR9kYqt24e> zJbIJ-xcuSGIu7EVqQ5oY3&E3+6*3|Nzv@}LIP?_db|DMGit*}{f}#*Z*Fg0dhEo6d zXi#_czCTv~IJ?&${yw7mY5jV|GAj4w4jS@NJWKJ$zF7^clrK^zy`vxze@2b=gxPzE z5}IljOSUFU$F4>2Ps-s|1Qy;31qm3is!$@g!d_8ZS6`o2*<=JP*p&3;f@BY-41xO7 zSt_op$YJXBGnKjpQo&%wP8vufW-33Hqp1?i8IlSoh$^&k$i2)uMKS9lrxbytC&fgj zVH@J;1C)D3dyq~K)yJ~IYW!VXd3mP6^?fq|4Zr5kdd{$ep3w&no5pAbzL6kof8M$$ z=jm$aQL}~`DKxc*JpH}KRz%2)3!Elk5qQ5>+_=|W73c7AVi)#@|_s5OQu zvzwj8(u*Zi7VTojymTI=BXp+gr71(dTPA~lu$JD|t31m5ImL#j^Bye6cA7WV6vmJE zK23T&xHW2OShe^4S(^M6!8x;Y`qMAByiv_a!&#*{@A@}wj~xCdZ}I8!UQ}9r!nX7u zF32KZe-74?)C-85$BWPNkco8BNzma&^wDa5)T0$+1Ca3%G4t?`u{h0UlZI36RL%C4 zBZ~ldH4lkjDdK6$@cGDy)u-=Ci!pDNTi>7QBDhpuf41?ONGnicTz6UmU%KO?t}&`M z;L~1wV7aE4G-(WbctB6+^Fqh9bM3%+iFIXer1${eS*%c8XRl7x26B-{Sn7&gnWw7^ z0LX*b)tAgwG+Ym3(o@S?Gx*R0A=7LGJ>FP(r)05Go?@B*J%v$DfKkTTD)w8pPYt>xgw|x z)(sfAy`QDOXRPMo9(}m;N!iiObKucnYpV==Rq1zLV4_<-Eh`vDfTIN=N`eo>daj*r z0bKJjEy*N!E%pdVym=IwLWRa}Xhi_V;YRQkehr2Ig++o6Gi9RmzZpHSiEq+_*L_Z6 z7Vds3X~)RHkI6*f5csW@4chm0DqK#Cy5}ni4Ft=juh_RUqX0b#NzuY3K+6JkB;_D9 zm>?a>PFUd;|N4}+v!)udW+xiQJz`+Drn&FE-?XlJ*B(TyV;<|hwIqUAl%5L9R3sJ-xags37@`q@LW3Pc=t36N1knQ7Y*@CXW;ou|&;z zB|HdoQJ{Qh6M+^W*YSwjgJPca;MVw1SirU7A?_to0S@#D&rskavEO~sA$)!@PRD{F zv*>4?@e#JT0<-W$7<*1 zk?uxO%UH2YZZB~JIkY+Q4xfn?XTH1k;d$rCj_xa5ua0l7eWX;M#W-2%W%zF8zyjCy~j z{vHKam4Y>IH~D`wVUzTe*5WpZ-xm0ArIbhtb%aHI8u~z~SvsTDAY*;lsocQkjL_1P zcgf1~9OL0q{<^9ta7Ze%s_2rakhhLJX8q-KXT+(akA1GUdMu;5-ETySXYidFK*ha! zfh|*TbAY54199d?zICOFbAjqg$BEV7lnrlq;#YHQ>g4wjvdL60^tfaVPt3ngBNwP# zTX!*cv`Qmsndkb&%n>(kj2Y&qSCWSXWWEu$OgTv7iQ461qS<$<}#&*=6$CWr`>3?+^*fM?ZEKpDCjd0&v8Al@O&qaa}@i)~! z;1Tbe&5@%lhTD=c2{J%k&&scOJatqLV?;I9>UmTb+B$-WJK9xlL?ny2W={Q z+yw#NbVY|xP}Np3?h5oP6&+~ey3{h+nXTH_qQqMf3#C(9>RdM>)xB+C3s~5qV4sA* z<<%;>SV`^W6SZD}>Lrf95bJNykDn9Z=Ll-e*yFc@KMW`-9ElByg8pOoCd7pfS6WSa zPQ{=J~O2X&#OFW0DXx(^s zZE71of)9@%AVT?Ka1_*=bb4>*N1wErt9YVPZj;U5d&NY|JWYN>#BKw2G71uWyP^xD zao%7#B@dMzJe&cYP{^5Bi;@JLG-{qO-LrZe>z|fMPm1GBOWP4J4$)_k1;1Rt$J5x? z=7+Rz;0ij|tmVEQx+I#>G?;CI!Ve7@%7SaIbpk%)j=wiDZGP-`lz1^#F#&CCR-5#D z33T7(2`+f@e;Lug>L-dJIGMGmC$ndCR30I|b2-%}xc2(m3wF-RRgN)jT z$J}?1oXxbpxM=;@n4{0)OtIV(wazoJ=mDyDx!J)b0p!T4Z5B{OWG+UDt4Gzzx>Myg z{&lwNzwyyJ|6tPrm%988R|W6YJ>-t)%S`i%qp@o-D)$6RJ*%j~Rri6pb17K0T0+rv zDk>Rx>n2@gYgHqWPnQr#{-&$5X;LpEH4Fqh7qG5ph+@z0P>zlUlNElZ#T^?8sfD+|&V3HqqK1#nK;hU54_xfd zaT%Tt{6agzpDD1)w|K9ocbN)Z<3K}KQlH?ZM!u9F>Qzr{{8EZgmyJGxCtizMYlcLQ ziqmC9ygJT({x?xlh);d1;vpXC(?V4oufCi*3}@#QeEkARdgte;2uIW>^uEz%L~EOp zbWr)tJ0L?L4R>zOzrMdzACZ4;`m6ShAuw{dS*`~@xFQb07RKMUY`1mGbMx;Gw>0t5 ze`gzLjo|g9Hvd@T?cKCpZ!&5qs!t6!_@+~@@H%vNg87CvWgQ1G9cqDOB20jd?%pyM zYci87GMTDjf2RIAxW*qZKe?g*=q5nS3S{jfDHowHa|0mlBv9N^^o6*xirR@|Opo6@ z+1$4Twz2AVxD~d^c~a9UD!}HrW!Ke|u0yxF_PN?*+jVXvc0UnU6B(QwR|zIT+&cze zOi+77QnNU(v}9&KCnQ(O6I-#$PG|G|9BDPI17!Mb$K?KkN8+Uz*vPt1G4|ETM&+2P zbA1mK_jq_Y&OgeOdZbQF!DOSb?8=TSPc_b@vy#rHRG_`ZyWOK+HeEB%84(7xM z2V=gWqo6})Qm0Y{7h#=$@sNQR(>hh<0d$XNe6dg-##UkY3I?(D=E&L#qVeX8LQTr; zjZdXGol3sQ<~LlxBb**K`~*bCzlYg;9Gj!bJ22#6SeZ56wZftH&&ehmt-@d42Ebo4 zD`b5w66Ua++42hJHB^`9VD+^o_{~_oubh^@LXBL5K{&&H4c~UqLtdNtOeCIdf zYPjQLE==5bod%bCVLtQ}Gct3A>1~j9R0rW9g*YYs9V4AlAj8MYP@{vDzuUb5a0amFGT4#OK0QF0~bTAVt&o-+Y%92)dxUgHy8_7{H3?JmA z^6(5b?co3V%op7_TFSmITm4_I9FWdw%8Fe!TLXv?!v99#{D1o zx~S1LV2lk$j~=Dl=+c3JlsZBjAx(-RFM4-op)BRK-c8_xZ@n!$EazW-o{FJEA7YmU=Tq$G(f>93wuR zzQ+6{wsD8srwpgPHmdfiHcMHZd6YO3hH@BK+f8rdrX0L;>DROfQZci;_iV{1UzJ?$m==J2JxthF(u6gy=VUWK`FCbF3fzybqvYdV=MvEv-Yd(=N!GUQc zT93G&mhBp8)l9n37nAmc@Pv?-;r?GlQHr(YEhV0Kbh03JYMYo0f*l{H zHTh_Ly-e<8^-JYKYcZB!vMNXgxYZy{&$Ud-}D4nFQTccRpC2iScfH5~J+b z@mT>Cf49`FbMN9(9u*h5dchNW7%xM$f~McYJ7(@N_k?&U`dPmeM?bKCo0(dE?cc4y z(yIy1(#aDBTQW9*?kWemXRmE#eUqP*cd_;}bDW^1h}Dbo^VSP>!*-Rq_F`H{?<;gdD~<6>g+~)AinQb#vOwU zV$dS1Zeh0NpSWTDM+rS`$y2)a9$e#;xUu9H$*M2AFc}N`)boyLOITA1kAgVk3Xhh1 z$53HLpPRMfrRN&Z_2lMk&C>4DwJx>GFDFsdEcBGZ@KF|5tKL8ZiHTj%eaT%gMI8Em zrf%t%##RP)p<6R7Ci#K}(c02eh1L<`hS@8=;I2Ed+Hn_?x~b-g-<#9*UZFSKph5pd z7x_wmaYt0E(}Jm&PyM9|*)(CdeRm`>aZ_9?hMadFnGTp9p}%#=Jco8L_m_(w$Dnk% zwALjzcE^Li4K5vT@%eiSe<{`B+6=uzje={rds1wN$9Mnr4s@NUP_N;49XQ%93&;a;pSH%n~Ak`b;Y9vrHTjqBc*NdWg%}9tyUg1H|L_ z3rpn@6D!)g$u#MhNPz}~D*x{`F}hADOuS~A^ykC5#hFo=2aA4(#zyO?)n!ASi}zHA zn9O&wx%Q62D8-9jJZ$q=-HU-qyJF&+KIKXMJKwl0n!|ER1oWCq=BFdZhzVQ-dqg=? zl)2L6l8f_iq(H|`W~;fV&@2S{!-1YJ%NsN;2J(#6<=|INztc_HlkMlUl^!%xUff)` zhJWpg4zG8iCy%vcTO%UTw5c(|wz;SfM@1f_h%ptTV0lWEEVs7)O(qK040An>62FZvS`)#U4__hjW+dO$RL(Wdb^U*>rFY?IP9yC+Z#L! zL8*sy{$Mpc zflfTTm%kPK1<^AApAtp5XH0QB6@zYXp=FmcKJb9%`%>t-ZW4X)Ze^lN157 ziZS~dGgBBlOr5k`1WXnowBy0ejXu z*kdZvXF9ahR^401y!PDqTbwH7o!Jc;lRi3c{e{fe+?Q?OoxuW1r^WB|{6mqcSL3U+ z#=|j;T&m`kN=)>*Yeo~hT+)PE$z^XH_PG)Lw=BIC;N^9M*?Wg}1&PN5^JtV3h2-XR zWsI}RLMjDBdEEc(hwT#HUZPDu?)C|I`vKkb-V9NNhHk}%-D?4!nY3--GmPfB@ztJ3TDKiarF>ZaIg`??WJGune4_;}dV zt*dBfX_s%>;V!Bb*oahaqM60jqSuaSB(zbnX3_@e5n6zjISdrDRPg5L^fV9*|QM(YvX!?GWi? z7%Ig@n2y^$|7`ic%Nlbgw`$Ky_)7a(e5}C|beeSAyj^}5AFE#R9FSvq+P9E-7pH1; zJ{O-u;vZifN1G#js#`uw81xp6+_<{WCI9@1ScYgWAmCrmo>Du^;%SIBp6jD#hVGmi zQ@+4tHtb9;SmoTDt(y|NAwQz*uELahs4|;vS?2-Xw`nw^i&M{qlP&{JN7FW4H9;J3 zsRVRxJnprwV=NnNz}8L%!12xTL*sfn(4QbkB&IJJS{q5Vj03fzo!qJW(s zDUFz>22EzTURX96veaSq61MKU|( zL9U0(U?&3M>6EZ!3S9O-+PBX}MP<2Ur4(syh{kP~$^ZlQrXlvWYfMP$FIYrBXy2_z zgghI~f?mYX7=D>^O7Gh~-wC`Z1$wY7mDda_+5krZ^y7rEM20>SyYHQDPWKed=LnW9 z;s-JEZH3Ua07SOLgQ1}BTn78E@_=TQ_oroZfPRLe9!XEEST-;Jp91M+qwGQ6E~tFQ z_NJ(I?2)G(Tj*oAi(ZfwPT0vHBc%q0P|{e?Uy+<{F929BwZ@Ve!NR7N6>qX-Zc?OP z3nND*>iO;(bdG=aWexh0Zyj6N2Avfl58XyRCpM%IeKBLbS&b0s2{!x~Mf!A_jgTc5 zL4sVB$noi5f0FEqhym3-KF=;waksb$$n21mEqX{<7?uyPndChupHy3Aw)ke2TU}W~ z@uK1OdfLGOCnw~QbrOXCXU(!b8a>GxV^qxTKK!X{j1Nw)Jb-dPTB{@jGnb5DVI3#- zXln+Xa>JP6Ze=4)5FEG$3c&5@e<*qik%P|)~i#JiW&xFsP!Glowu0m zke}+1HB8PJ6WBaq%YKm*+w~oIq{dL-6g}%GddX~7)Gh3Q*!)#4-a=Sdykr#r70WsI z0w$D5CpReDrAMW9imx}!CMOOo8nn82x6;;q>wDpLLWqY1>?MJ;9+Gv_mww(Tg^Y|* zm_`Nm!p8y9SlISt6h$RLwnVbS)u_L$-P@A<*b$~nKI+0odot==BMVt9f~QDyj&_|p z+LiRvrNX5EWS!zd~tJl9NK-*UG8?k;?+ zYor{or0mq)?B>S+e@!=w8Qf7!!9~*%PK_W(9AYnf3oF9~_gqRzn&`PQxwF}Ya_e&- zPz~uLs($)R_c2ZzuaHk{bX+ZmUJBJ8tks_8grSE*QFC=Srm3M7`cWOQTCstUgy6C+ zPFb86A59Khtv|h%i~mNno=AS;m7`Qmh0k9sDGRqT5Xj*S!qv4VN2w)((CaC23d;=) z&wYsuz4XQcAr=;i+i%W2tGen!I!9>lfZCGG@Z23dHpqyM5TaqUUThFMY#7j_TJEW# zn4!U809s@nNk~<1`4F%=X1&Que(*7|`J_iPAbL~-KH+HfX$vIHF&^gCX=K-F4AQ0ADbU8ZU_X?~3ab=OZA-V4 zxhX>J>4m=ll%5xX%S2Frk?d7f+ZBfF5<_bDS8K$Z*mERQ`TCXe^^Q1(VY}zm!Snh_ zbCo|+Pk`OXLulm!k^N!%Ngwn{E;zwH8dgBJKk|@1sEZj)uDG3CS=CHfF2z)q)zLeW zGFSvG%4+in8Q;^2EJZ_^{qUsvA+dChvuP z+?Bz9+?37;pu8AjpYJ?j;b^$b*cp$&@QDnU#AVFLNM{4s!4g92B7|F2XfwzclNqom zNDsCb6^*{su2!R&YuGdg5l-v)0UFo$1RK%Pi3~GJWv~`mOT|q}dd6D~*i>L`@uS&i z&vSTif;p~qXpJKGk|c$^bRe_Z0xEwwvcMuH{Bmd7<#Y3qroSR-qte_VfCfqKtw=iE z5os|RMNQ5sUL6|zbMRko*+}=DtG+U~=+b+lg+-28ZU4#07)oE+5GtxjkFZ8+>C)TY zXE$pL?2m@#unrENL;S~(UD#0ZiACR(#QJ@{5-@tg&k%8U_TofT{^4e#>XVcLR>jH@ zsZxo?SrY$Y3+F6dOLWKn^%2AkwB}J;|ICMUyH2I~7D8y@sU-`($NLF`bj-`&S}j*V zu?(lJ6_9!n-*e2(ySJ-evWG{4-;j64L|nLFl_%3|3=P4bi-V11=^8!3ZoJ>wcG|%^ zgcb@(5v2}X8qN9Xo3Pu?l`Cn&ElUcpEin{m(!>2!q9h&7nRt;)j&!SCpg@*n zB3AZ0Y5JQa;|p4etgB=UBfZ1QZ3r#b(iDwsqsgBcAgW49Vc92c*)2BQ``PdgwAz1? zgK7Us%jHQwl#zehh{jeS_K>8dZKRKqAiLJ(ezG9eSnzw6+$*}wp0KtpA?E(BQQyWs zM4i_@FFTz&n!!HWt+u0xnQ#7w#TpeMn$!_Qs~CnL1hFTt{SSl8R@AK2BM(uv)~fc; zeV%*wbMFv0GywDfK(5jnhUg9UhWA6(sPG?(hpjW+xh&0x$qD|C5|YZCdK4WcqYjJF zaLepjh*Q8Nc_RxuLj6Ph(vop{Yq)b+BUgev-UzDNKqUw5Okraa*~r+%+P>F27caf7 z>r65ljNqUN^^?y1ww1uvhQst7Jah@F8hXcn|I6j&c<1qqfN_9_;0DgMhY1^gXmFmj zld}vvgf%tVo9w#iQofvgafj<2TLGcn7cIXD()#NX_~APs zyfRB;j~Ez|B}q-O)txY$uFFF@v@@` zaXDjs1MbN{o?-*xjLh|WK#U~AGFI`bwbb1cGQ}QpCMq)PXwnnFXJM(-JCb4p{&+$> znrJVbB^oYr1P9aACQYQJKKaRRp~b?imTyvd6zCvJ>Xr!Q!jOEB$mNJZ){TgWcE70) z_eRmfLt-UES&a#Kxc%GwBTk zh>OSgtjEJ~E5hj8HmnZ|NF_mWoe?BRV@ruC_EoNleH<7X;8sv&5XyU0VOXK-}nfe*5=-|GxP0 z<;G!6-!I{B$8x-qQzmjt^lZM&kE@)qw2V2Y=Q2)Ee|pi0?)n1Peh%8A?koL>Ok3IK zd~=@Vd`w?Dl*C|KG zwVY6ey`^gBIr3WNfG0908~Bf*P~>S_YEh}lN` z?SQg&qg`tfzFcQSvmPc`)Rb?t5W+0(-d@j?rD95>?|8~ST}7(~vqq)MK!&r$E*}DOFf|w=ilF6@Ih@#wlXFKT#O6qBHb^;Dd1&ner?bo^83Av&Ref`jLBHV zbK~Z#yO?@H7aa0?E8ii2>EjT=))N+Y(WeTPefP~b3cG~S=c$4jBPsj2j%DG+WqL+4 zUj2m9RIFYZeN3=Jfw>h%el`q3+rJ5ELn{Z57#0)mlUwwyTGI{vYcmge;R-KQZZsbK z+Zx>})pjNryYnDT(9DtLGH3A@2bsDh8ta}13|5rpc4r-Zv6rA%Eyfj@q%8B~&Q69L zRJKTokCD|niRhAT?yqq5g^!0uPg@Z0{Sr-@VS6PpN)VZ&QSZ4d2>j%4VE&gsdu!+Rzh^Y{rOf?2mT_4!6*0iT= zKTtgVxfe{P3Pa_Zcv<1qYE0z6;oQy%lOA?fECcK^FGlEMSmHI45I!pJFsPAl(5I zsWV&KyjzM2JXU?Tt|cVk#*aawyEA|PLrVlk*1wxdSvDNCd|WjgXo~DhAOL^wz4m6I zjB*2y_sUY;9s&q!5@T>+LxGDC{}wMKCZ_a{-wvYd+NcQR>NsJ7 z0n&=#%7ssJRaV74K5UTzmhKXK8FZ-!Vig@R%|*w@SyhobsL5gokxjMfMao#DCEtzK zaIs1J1JZJ=fmrRhq%vvq#`NaKWRh53hN=DWbV)7j?aYt+JwO@j1DfH^AD^mZ2E$aJ zW=c8C#Uf#zBJDe5cqFT(4)A9Q1CapX=w7*42txrPVmli!`=^i$VKhHAw9!`w~(pgE2{K&OPgf){YQ^VrxsV!@*Prb2bf7Xgz zyKn^d?99KiKtypMQ7?J5m12pfIdn%tU;FIUfXu?QPH-t?EY#4p>5Q9*SO&8X-5B^;94+L}#FvP>`v%B7VFet>fYKfb$Bw@?CRI^pqb^j#A*qKou&` zK&y3$i!_M=R6vjU1q5{AvUdBmoKd9emxVZ*EW1hlJh0`v9_CJV>d<{>q*BuW%43t+(84!E2wLk_Yb(#C|a?Y1t+7yonRbw}w@Da$PX0%FAN6k|6JN4?S1E%Rk#~;q&f{5E?!{MYrrvN!G$Wqq62qqC@4xMSb-k z{Ni6Hx%mmMSqIWv?1uiYeYB6roWN?7=IFOZ$|sROqCWj<4xEmI!VPE7DQNzy0Hs~| z7@+OjTz4+Jua%}y8YvyBDR3*Y#d(KjqfP=WEf@@-6M7UxV)f~wO7XcNuBWvFa2&xZ z4fT|-$!c^ZE}6g8yz+#H*{XQNXL1zN>D0ov3@d6ywJg*1;%&n(@;TSh1=739{#$c} z*KwMV$I)N}v9yBR*u*xSRn-EwCvQ6R6aMt9mM9LIdD^uuU1ejQa8OqOc@Yt`uUz6otxt_ed&Ks1w08nO71+H?`F&Nr zDj)L+0HXkKhyH6GXGZHB^DX4sH~aUC1(UT2xSZshY8<=zDwPMBZ&XL7?j#S?*O=8k zoo1+{es{U2k};=v@A;{)u=x*|wZhvwAEEDBnlycRNdg1cr|vc6`T#hxM}WGN0mOX^ z<}tNj7&v9jK9ju#$P(l*H?s#D2fP$>ysr-UJ{-VH1~Qsq@oU*F4qe{AtsgcT#*?6i znINwzo(h?}Zv*ba0%(m(beqY=sn}t1U|v2*?eDDU}>SGdUT& zF5edNuDn#DAj>JM*t7sY95P?#S9v+hCUa~$^4uhiS7~FJj@pFJdcZKy_?)7YcV5|4 zW!ly13Q9%MRO4v4`7Vu~V!DcL2+tmXDu1fBBgFocB)6*E#1@}-+8Alvo#*z4s;j^km45 z1A?3tH07#bht+S(_rJaFgjfzv?RdAcqXkl^zjAu0(fZ%=u^mFkb3`16 z+RBI#6TLoMX|ImMLKz`muu2sqTJVfFQH6B86`gA|wWYT&47x)NvV#GTm%vX+IM5D) z3J+|LXuHz|GWFkOR+?A7Af(TpRp@knou})1v@9)7P(RXkfCI50_f<{1xdZuaZ)<~)xgs|M{g_^P7T|Hi|+0gco6H|aU zaXrzTM#M~+t%d8>XzDKLZ^a*X8wbMMZJFG9G04oD=Y{5!f7Nvh8tGt-=y;+`)%fD8 z1dm}8S|qqU6Y0)v&9SN!rI}*kEr8!s*EL0C(rZhC4wt=CJ$Q^(sl7UPu-7%1{^sWP;6YoYn)qC z^1zLfGCL9WpuQrZ-rp{9TJ4qbY@nv71~LOuQsbLQz)pwv3~oXnGmE^`IKY_t2uc5C z%KjFm`DWd~Ta^9R)ujKav5eh>9o!~4JF=rBIZ4Evq|KZlMox$TYJVP{g@CUKU@2?! zv71|gaYR@=HfY^3C{V^KsO{x;J;&bf);&x0Ez|q|q43ci@K=ChxS4Dgws-}>)~b>C zz9?E2o<_FAkA~D=6B#giA0HA0M+vxklE-+aCvd_CLreQ$zU7e zo9kUQJDMtu=^z3dNLU-Hii5=OSGQ=)Zy<3_hUa601bv%VZ8!7BPw;wK5zI&gPNVWK z=-|&qp*4#i#O1ecSh&p!jv)8VI`50)w0vK8A327HCtS?1zvhAK722pJsN=aC_~$(! zg7|DHz1=v3t9&Pq;h(?U1`R_?u8I2}F90nNxz~iSYLLu_phB9P_hq%ygO0Tebq?#e_ODvmF{Co*~&0n>nd9xm!#`PU2J!cB`bWd;5H06@Lz# ze_7ZS`Un{N2-JS#4uvu4FWHFzW(locQF22m@N}`|D|+tOJMhz9@AM7W;N6tw0SCb;D!KH_PFL^kGggGI0n_8rj$Y2C@fnbyLfOpaW{(s4#EQ38sV zz;*7!A{3VcvMG9I#_EAldwZdK5ypX|CXcKfqf>}5sP(9lQu zBdK)NH8%aJf}9ZNs*9u>A~{Iq@T1RdM5gJ*;%XN7w6UJjue0O7S_np^^bz#PKJau8 z*F@Z6k?HUuiSO16KKQf3saR+BSNBujtun(LwQ=co^V{vc*967b zM)FknuNiKT8d#pq_hC6ZeHqg41)2`?BSNxYOBu9n;FNkfkA1)@3=Xp75UDde2?2d* z2(w}FcO~;z{*--eYs|Y)Htd%8(w4`!NTq`>jL)eY`iaPhdvSE>B5g*+!=Vj#*lAIw?__eXeSX-e zR6n_cyKDBgeF$NP0wgT6|1$PwT|G2i08<#|GZ}?zg9bG(zcvl*sr;rD1uF@i@QE?y1h#o*$5 z>s3P%-wZ$b7qN4?sxSqbGqZmQFNDe;=LBZv9^J@ES;K4yij}RS`pQNH$2}iy@xwip z3%?sHUwLCIT zhm2=;IB!43^Dj03?`hfJ>HDU@)nkER=Ve{Ge4Z7f!kEn|qx+4nU~Nm+tUs@=#3;U% zy^<6TCfXt~o+eur;!2fx9S1U)BYxhuodXGhdeb>OL$a+UZj?qa3^Z&FfGLsyz3^k0 zCr*J=Tpt9q_A~AjSRA#CEy=sj)stVzO^A-a44ncm0z``)~60Ebos{;y00_Z@N6Fg2l5g_hsq+N@^e@#4s+ zz_2HU-R`kVLqLGOzIs*VK@VtJt6<@Gei7ia38HH03@Q>g_Zc|D+eztxgi9e0{#VuTeTI=9DNbc~=-;bT#jm&i9n0|x8)V3)dihivFPb0y zGM8PRw+4|CBk46_K{UA8&(4*y^%PWAxpnBn}= z?k0B*>Q?sog$7lx*nz2P^O=I=ulJ8y<@>h#Gv;qR`-R^IS_A4RBi`?~Ey?hqsx(og zzOaCx-L>%4^{p7CdiVpzGMbmIXYa#Wu!+8XU&D^|VYpF|>7^{ku67LzJc!h?O9Sk+ zh|bwnXH-{BhxwkHIqj0M<9XaASh1!fM;6+z#k6^=oN;&l7H5TDpx9n=*7jk}mLJB7 zhx*%L#XR!XDrm9Szu!>do!j9xp)K!pVC%hD{_mdtFQU%^pl`=8U*&Mmxl6YQ;8kV45={eVCIOt);iHJO7XnS_wqE_L`V4 z26YVyg6Xr3CzcKMF$2@Ej^@K_!y$+A@>Mz-b9X(BZO$%MH*6~0n$r(smi~$}Je3gP zAJ1W<5!=Qo$^wB(k!^O@h(j!UleBs&5=gjo{PtdGTAz#c6Y1Xt)+4yO57v)J#%2A} z4)4YN_bq>aupY)47g=5u2nG0QH1Dp}vqru9=# z*R$LBR$9f)|A8>QBJgkHorsBTepSt`=uLHrQ zeRB-3zw5|{Uwtl*GED-e@k1@${<71q<%R*0iaiQh-;_T4yF$PAlDyMhkczw2mvl2Q2EAlzuuc#6lUj8M|`0~QGFU#F#Uro0-nW6)U3B$Py>GeUgFHt7e zOP(4PgG-~h(uFtf%B;YpFy+JD0FM}sNk3nT4a^YKGz7TUZik0Po;S70e45m_qSY^0 zN*I>7!&`r*pF3$Lv43`CnKSLKoJuixp_@xT3X4nZ?F+$viBy?R%D>3r!C4#G3%cZC z$=K1RrXNGa_ipZmMwJB_s-L?0D5p)I(Q0wqNa~9Oich5wb+dg$jcytvYz9SeSM)y@ z002UQ!4xvT%D~jVaR7gf{xASQ5e2aRBo_)`0B9ZA8vTts9Hs9|rwGTp=dlQbd%CB$zh@BkYyql`zf|Ms2!a$&=yr#Wq&&W) zp>OK8DR-mlW>*6Ck@z8%YX zb(&7>I^~5!))(JZd@F0IH{Kv-JPe&k(6B&Q5tmR&6yF$c6#T~YL7Ch>zXKT$8%DGB zt#j8gkpEih-;2>ZOKKk_V3l&MnUS*5A#SR!45W{MtsLJwUL|@6bDW{VUr7N9@nUGK z2?cai9d(tnO>_iNHx+iZ5qwc~htEd&{a9&XqUL zQ+EicP~0lmAS#nvJRv3T{1Eh3!nFxL_2r9oP52&D~i8uKYF8L7a0RsR_fK zeOBRSWc$Rb7gQdFF{EuFwzPe7u_HxTncTHi4ywxrDt9>l&42 zF}cQ+rrR-{i3jz5DqFeJ^Ac8&6@y(AnseT|AU=|lY^;Hm%k=5_Puw@T%-8$C%cquaTO)Fx% z!PvE(Q_=aZyn15ekU~BQ+%+#ke?_mF^K}PXBJRaA3OZbG)AGZ+Zz)EW^VApnk*^x> zwQG~cQNfDWY_<`C(_Su-ryDW1&Uo3IZE7~KDDz`B588bY5cg24$yvzq)!4KPl_K7& zUum!%?fwZb@hcoFQ(&48X_s>F%FtBp66!D=oHofYa2i~a=8+-#q*1#YL04sFi|gVd zSOQOB7P5^~!YX!gxdkxWkt7^Sba+%_GqvOx2Z2Cu(cwPZzxPfc0)~DFq2!1y;O;|Y zp3FQJ<|$zJlQdjR^hbe6Gyr&!41i^#*`uyUJjb!AGH4$XHR|OB#g}t;T@zh%zkK4EzK3~x7+ACJjd^X^2d-si3 zHhL`TJf4WDQuQxLxr@GYY){aMuIss{k5$qx8GL^E#3l@{RMWMfMmpg^pNTC>`RUkh zy=*2+VpB0`cm(K>u2hWOg~~-CY&ShR3iY)T8%^bUPw8o_`+B-KsrWVvL-&^OW5$Si z=z{{RI<1OtpEFyOS+k%pOeiLN99+Ogn1Bnf zMcj>KBQibB8o3$>j!ONK1U+RsIT~f+F8+**IVdbqJA*_#QA-oge&*cj1nM|Vn>^E! z*UJBN&AnB+I5D$2S`4dD0--v9aTWgUYP0e^FPbt{6_lG!xdvtBMZd6VQ8iV4?3|zS zU2C{AUpY=z-Z1>w68c%?jB;zKLU&%VHAf~9jT*?;U{Wj5rHK4jV~sA>t~`fLAW|@D zLx-e}`WMXiKlR>3IICrGJ|#^8!2b*@XH7 zc?WfV_vu&w_x$+_I`f@o5i%-|;mED(DD`e5T_Bt77l7#XJh>LQgDUg?@h%8Qv31O8 zn3rQ8w)d_Mj63FYf*8K2MTM;8^q#c z$GtV{WlswHKW(MyA|kfyewMD zzBE-d+h7s|0ElEsV33K(m<0e1)I&nJcbclm*B%q@j%v;BN@N!IMV@K-gE8+~GuJ+& zw#H`t6{*bw@x*aE;`r?JO!r!c7LryHAXMqto$`(;H+nnzCtbA*aKx~b=~~%3keBJ@ z?ckt_S&cn0Sgr6m*MAf(UF6lmc%U5GnONpt>2!aEJ);Ws*8HHyCjzDJ=BrErF-Xpq ztt?GN4Ntqwp`rGeS;ts6x|j{G5kz1bI^HZ1-`~m~T~$~&obw3t@wD{uV7RFM;gGL^ z2=hSYMYb-BT9Q0UHUVFNfK8oR$pc*FRjM(1JTGFr;5*NGaol-zRBvLOuJO9(Q$WDF z(Vj0x#?vOdDd0JZi|lQcj|2c) zQ+Q@h8hgJfn0ao?5ddBE7+RVGZs0k3f=Mys{tIK!D`SyWw<51V?HH#`8!L>_Fe1Nk zVL?V{)^3~$Aa0K7FHli)qc}vjK@9BT-xpUu7OjZxbw|Rwek#SQZ7FB+-L+r3Gnn#{ zKZ{XxWQ)@NO4kzK+8nrRagT*uyySMgtG5~M9lry*OB zb8IU7+qXbTJsiD1@}wzM{tE0k5vISmjlF@8$ZA9(8aB;287AiU0xxROFm$@a_La(;;zH?c=@eA^xZLPbs6PqMx$m zklXcDAr(_qDy--_1c)g8Ffhu+1*fG`GqDyqj_cFDx%TkaE}y|a>v&x$FXA@U%e6BZ z&!cm35GtC*!EMmW++3RPkJ$`^oomMR=s;!sn5791dlc-vwm_zE2;ca4dBTM#l89Ik z@FO1H@~?gJMh^0Q&OV>MSi%MJo&K(kK7A=EkJofH#fTpuyI!ucE098pbjTr_pnkMx#bF+?5ZpE#^<3G3^4Gu>=7WM-VWzyfplTlqKZXfZS^b5)> z=2tsR`uX3}IPQ8s^26!3-8r6pK#bFExd^TvhO2NlSubS2hUaelLiXWIcJe0)YCLGhIy7=1lH_ ztcG2#dV}reXMaBmhX9Q+<-^&S!J6SV(e@F%0Z zQdYm&`|!6mtt0#vFBu!6i}9jJ-e6RIp3?P98x1~PzP;%1oZz?}mvN;kT^qNjlpip5 zgY9Z>3q2j4CZpAz{bCA8T3{~-v7}6{!SFU~0hjOsdP|@0+Eiu&f;yH`6+kBDb8%)h4OEnd3V>0#1VR{T#cz&0-w8=LtDE~ zHB~b+$_#qp1cUe=E_lDJxE0|rMDJ44&J1bzXC$ex9g$WeD*4~Atbn7oK`k9uNmL#~ z)Gr%;VHaw9m5K9I*%VWz>~K&+bYQ+qYH8}fFWMFZUj+@g6;zycoqaj0=P^`Wi^Xqw zoR8g`o-{njT4EUpMF z3V(Tx+eEmirg7}%7tHA{)1FtH4=eWFrI~~bIuG+~5nww8n2-rnG9f`q#vQ*=j0(r8 zbYJg3Qeo!?{gbl&uYSMq-X@};mlSblY9k-1Hy3>Q73!i|Hsa zo@XS0c`7AX?>@?C01vx4c9{7_9|KzGWg9Poq%Y{RHs@ar9KmP8;#;>S1HJykrMYK4 ze)Ner8~?|ky+-S}v%FnSj+6wD5v+*`Ip;I#u}@HudU3SAkh3jiYh2Gs2Xy|M3Ai{q z+R>>w`3z|I{PQ9Dy9C#dXVANqi{x)-y-lov4f)Y>mGacc;_iss(8xudZ`4vpPz4`Y zVN$~_I8q`!qtPKMRa=G$hF6hP#lj@)6h~`;tvSWfRLYT|(+AHqF=Mo3k5BSDz83P} zt01nK%9Q4&f!OQJ-Ifztkm;S(hzhCdT!2_kK#|L&;*_2mUmL><={kQ#bwb9J__Pz) zLnrQ@`IuziSeW=68u_ZIrRGWQ*{QwJq891H{!XCCehc$QTk&zP@+Nmoa#XCT8q#A0 z#iObX91XR}3rodS5u=8Zu|2da{qg0yncyaeRh!LGRq5Uw-L4&nSuOvr|K{S~7K!jA z@P&$BG%7c`9q8`tBFxA|Pa#I=2vnfJcx`#l?AjJEqJ{-7_= z;^(o!1DW#C?nRo$IG4x0x-_AlSC#cqg@fLe57C#?e$KVIM4Ft^?Vc>#kMMbtRQzG2 zEN1YmZlr!-$LFV);s<(V5|nREOY7{2Ulc{>YxHJI*~dC%BT$O^TNyG(BNH30GRGRI zJcP8`1^>n!k9vIa=U-eMN;>gODNl(Q--vmdGCxtiWyvz>X>#ob)_Pg!0tjwxR4EJZ>PqpI(Dk+rf%eIqapVf2pre>QyH5MGR zyA+j|OE8l)&5toC6F(Elz#Vg>WMr)#cgTzwMeum`4p-{N735uOR-L}hdGch*a~;h^ zVG7iVGB2}{R-r>k=q;sP8gY;^+Wu9%l1)JO3fvRB#8Wom^R zM}{L)^AwKM%*<6;r#jA38^`9cPRHNl^Dppy0pI(&uh07fvjFrPl<3}8DseIISxDX$ z8!x73PU$Xl!m-A%pP@zOV>s|M?Pq2AMe7~f=Q6YROSNznT_PiU-?8RT58Y z0bw^^`Kw~}mh(o6sY!Q7&q#nW@1e{^7fW%_pDN4aFcw2uucKhq>wDshHg@{B;MACzV?&%`Rg7A}fWpJ|*jHZy5x}qh#9J;%Cw6gamLawmP3n3Si7qgzD*;GYFX$h%~m0{;!IIBh+ zoZPDLYhcyEE)<zJ-@m9)StjCCc7>x!jN9E$oRsMz2ifJ1o`%!97~_wvq?=Arsx1^B3zf(P z@%9JD?Ve}-?U>;OikTXJ&hb$i;~A&k1{;>LriyWB`ksJyfRv6Dt2?&bDv+js1HhFA zTt4Fds{GpV(=izCIkX}Z3@V{(-?k~y=;Dk%IgFCG#nM+;su;tb-Ms1Iz(E8f3-3y0 zK(zr3fG-C`6geTRCwRDpnI?TA&nlIN*;x|%K$ie;HfCTb%Z(ZIYIj*VtIKhJ8f2E9fOqIO=u zv;16(y#V0uzy4s4tmR^o#B+*^d^_i~R?Jc2>Xk0qF!s`xcX8~~CbpsZGhwUO<-qU_ zf5*1D9`DlRuePx`>htop;e8TFaLm8itBee%J0wJQv%$HQ*nzUb$9~>g^8JF(fmD ztD$=3V|o?(P(8I(l>*K%ZK}4bqJ|KsQQl;9uFUzyQNxhGL6dj;Vjh?KtX}^9Wh|TB zLa6O2tezLx8$WZx@LeI-rA|+bX92GiL+Ujq1zE%zW|o>-EzFfateWGA?i0GiDKlU+ zbx=`aI!`(yohvme1F?IRsq$tA8svVN$yywi=6`*pu_7Zk?xb;|1dyZZzkO$dIU-#s z7@_Rl_M7gk;8On_J?6;pn%UtI0Rwg(s!97~m&9N|EdL!kOW_msk zV~OMuSMQXzdD=VI`nb%qKC*}dzh|s=otb;bS2pNRsuM27KO40r&NVX{f~jb_|E%Kp zeb|E;XaXBA2g8Bank|@TI@2hcug>c|AHhw0QL$A-wh%2l2F?ua8zk|pv%+wh>DQu% z!IowSo>Yj)R+p)tch8LHucu&JwLpcVw&-x9g#;;IYL4%W2}XGJ24RM;Zc;_YwE?l8 zG~d-=e-?OaRvb(-+kqSFg$V$5poTSLGi@ zI+m@UeS0)7*%;IHnOAo!GjW!=TCXY|7B-8ZcA@}6Rq2_BCLg(niOeYq&hGN!Sc7gXK?l&P}x+2FQq%_Y`O;s{O4z6Eg=9EwtQ5C8Z zU>p=2bwl*w5j{VDd~2PWoMBjf(CN5E8_?jgSW4ma_2bm7m?vkt)MP9AxKd?%7_iz9 z(t~fUku!_Q`sgB;A!Bcn#g(|3q^i@$(Nh10gIe#XO3U1Kux}iaiGYl#euuO;o>I8u zv2ox8*RV)u?-nRlF6wE=HimgDrI5}CVb7B7;y-N5hD+TxcqadP!+l=(QVC)Dy4FVR zLuU2%ZPaxAVWl0?Zh}_^^%)`hNwKc+~?k)i` z;V4&T?hYt$S%6BM(dkcG0)1EvMe6WXVg^-}ai^P2=#Y!iyg0bJ2#B^9+E8qnaP?LO zju&0>{)5QUOz`|%%0y_x6dM-yQ?sW#`zPtG55lpXVow7N$tmauVP}D`|Lp}ZiVDZE z>i%yof$C@fn`o4t#G5{UOi=9ER(0fXr1pi4B=z}d`8wq~v7@u};kD3TKZV$~8HSAC zZ0_N0`g$^gl67_+riQ1LwDX>_Rn%*s-N!cvtwlSC_^_rz_n~`r(kkb^Ukg68cro_u zvt4#F4Q}bTfrJr(~`3N)eit`y_)teKEZ&ua{9E|E~<$aI^n^ zQ%1MqADhOc=-T26H>=kw>WEg3!`Zm>lB_3et(S*JT8jKEL+czWC-ER{s3nspofj$> zUn`3e0cw}!d&yb^s+KdCAQaF49>VcBvi5Gu&L1O~ZgE!HdzFq2+bd5k$tMV^4N44b zg#%V$m8}G%Qfdtnhh$$ZNqt}mz3O47woaNX+5W)eDgrfEGRn6gLMlHFE|thK|H zcE&PU0aZ|AA4iM6dK74{a>nK&EF$%X&DqNwleL2qQm$vQ44jEA<{-D@LoCjZ???q= zK@BK2-~%xZ3JS&7jW4BxPkDu#v-j9jv6X^aYOE>_z&$^=MA)K32E}&gZLxmfw)E9C zNF;iJgjpkD1!@wop}Pwaxf5(|7u^juEHs!2S*$SHnF4|prs``uh0aLiGOmOd|#)Fnd(1vp|J z!DbrrROoxz-7n;U%)cF8ArHg`>m~O(Y1`3sY`knjCua9xdw@a~LnJaGHpu)NM_<$| z%(217y1+-pZgkHhD`KB}YhMB6Z?E$Iwbu$h_)zgAi{K}zm>5&Rs;kbh4ti@BSvRDp z5elDSkfVpgc`F+JPj=2~O@>5OR4z*oa^UFz$vlSq^ewf62G=4e|0d5(ziRw9qf6t& z+>i6RY9F$HS8_F+2owH1H9wD|RdY`U!pN~Jig{$okR?PIKU?c&BM}S6rAK3#YjFFeJ9Ak!GFhML(DY{w*ew?JWVGRJQqS24 z^qIWeWJg@aw8L37?kfm%fu28k&}?nIpn_%oL5SS`ENLrnZZPBH_X zEX4mTak{Q*;G&O!zemkv4u>~`l@E9@2*P`B>!_|I3vY~ z0Y^JNPLd69ESg}ubGMpBR1UGYO?eSG$v9(oes<(L1akaQI4c_Qg8fw z^t2!ahgKTF?sLh`9=4U0Ur=d#l$bq2|AH^t#cpHcy+cflZJ}Sz+TgNCMB)#tZqBY} z6VzqM*5=rjr%nE`$|2Q1fC+WLAEIRicfFt6PFe|XAH4EW4_;BI421;#efh(BXh)<* z-o;}-dOR+)xNX|WSBA=8W++zx3I11p4d#2M%z5sU{!e)Ebb?06DALV2Tv z_0Ix@Qg%H#!Wt>x15_SeRz@!)zIpgcB*0=>umZZy9Rq1E1|g73cmqrRBQNFA$z?B5o@uY7O@q7o39~t_8VU&`Dh#*nL4hLwi`(+8Sy@; z+OIT?cE!D206;@ZPG1g}1~RPs*icl1)f508uX;48l6F~u`RxY38fiW;F0$41Y84<~ z(hFH}9kwj#B2w;fhuJ8`JmMMCKrh!G-2a>9>DfR;Hd^XgFHa)2|FY|hz)RdKX)oyLBIRl)VI4n9>V^Vs zIs0~&;R*t`IqyRWZN2e2i~$);YG<~#x?k+<`zcS^(9d3ek=`;VOhcL#ua2HDd*D;2 zV{B)4^@t5_fsUX7q1Ifkm74i==z{@G7q%+KcE;s#(av72-0mRo82x$b^Pi#O?5Jnx z-=j!Es`%O^vW$|w0u`gi<6ep4$}bFE*#Ih+{q0e|eCjGg-n~JCARV#<^(ns+arvLq z-N@Iw0m_-?WrfjY6;V-q4X5dU zT1f0oXedgcj-=_7CC z%qQWw@@kUDj=vC&U?Wid?Y4!BTaaxVtVz<|N3#oW@bR}CH~Av*=A{@~mFZh`9LZN@ z1TQJa)uVR8r3h`Jt*4UxGtPjalB#grTW6>)16>6`&k1C-a!ZeTV*BFIbk9%m0+sRz zvlYR;fjnvCULr~zE7M>X7l(5+>KRSR{V`y3Z_P~Qr#Z9x*4M<@ zR}oic1sHd1fwerrKh2pv4~ivZIPjgJedk%gs=ORWBQBG}efD+0JmAWWjw_Eo z`--p9_i$43(MP+Eip<^x3yH=mG2~NeO2y@3$*Vt0>gnwXWMA_@AgnYqdX$Iv{DwQK40-9SV$k z=@aR7b5C)WeM8aF7(R34B}KrUBZ1^ghE!Y0$}6=hpF))ahfotyh!O#G=LxcoxKT zEhM^`)(fq#S%AG=|DN5m-TtWp{G7o45d1!%_B9;>-(E#XtzqyUj=V;bR^_t;BjMop zt|o?s;(C0YCuCQEO$DGo(fQ7gUO4X`n&Mc#F~t1RKBEM2-inicEGSxg7hrV$CSSip z^uumUa3`XJuuf+mYnJ=l{O*kaMr~r{^Uun~M{N~>(2NFx@BY6H@>~J6sHUF;QYhpX z;9a!DE%XF)5CHZ&4O#r*o;vzg@i$q*~GVFqw8ZgEGLV3I}uG@M_|!QA0DYz zsVQ7gb2_1^{jyHycE~~~Je^ga+4W-}Qh_iDI9DsDap6nhknG!62Q00hm|35>rLMe0 zRzbX;@gBlOvk*Dr3nu`m-CH@LM^Z)Pl=D8`|ep+q2Ob9r91u0^6RA#n(LrhurV z+K@Vrm!X}gVl@iM1|igDt0CE66iHJcNyd)Qr#08?Q7Cin z(kd^Rt@W+QyP2GgAu*EY8m<}&aSi*l-x?`2M3s%~N5I@!*(Itn{`g!c)}&*KEcQ%NL7TjGQ;- zd7h5sa3S7TRWqwBTOn9F@+Cp}HgP;FKI{3ILsW!tIcVXia4sk=eP=m{y<7QK=GcLR z%FsNSlj*gdI#G)`GJi_fS*yl2gpPT+ob!3(rPNEVR=N(bd2HmU(Xv+NN|B!`XIkYq z7(Gv4#h7mh>)kI0g-AWV(mgsijyO8So4#`YH^t3(9%5K*;x@L~V02$lKcYQZY4eci z!z4eD%D~mXt?K!0P!)Kr~DS-iVC2Yva z*lZDM_qQ~(^cHA0Uh2|zGulce^UUw}px$?!u+nlhx&9;BMSnz81}8F^VFh}BSblXi zVP00yHt_VTceB`@u<5 z1N;QVBbDfpI}?#BuhWCiQ?!F7`vpfohU5mNJlvZu9k>+jCv7lPo9xiMoJI_`e;7$r zjMnsoDs?Ay?M;Y+4gLxun#d@`9whn_0&S&z7nm|bpObThjkmLuG5zET((%fqY(f;# zI~Nf`q}WTvaPYa&Qg!1}Zsp3p0SYw&f&IwTIy#5nVlUc2iM??>aQuq z76Bj1X|wNCEAS8?Pxr*d(l4$X2ae1}?At5dN{#9a&Dm9C8v%cjG*jOxXXu7hdOMax z$Vw)r5xX2T?mi|5Da7K0AT{&t+OZ#*!DWR8ZAJR_%2zfx;U$R^p*b>v=oq|mz<6kF zs!opP^^dm|4~HqLpHKfc=Vx_Eo>3Ng&?wX@e2057H4D}#T2{pbqtcr8BbaDsg*}e6 z{Cb6kCd1kLSizX_AaR(G!h-E8f~0NinRxdmkNo?bY|Bi>;lw5(?3pY-t*P+zn5 zd%Z(=aE4jDzQz5Coh~6V5#(yp1C4a~e76}gLF+xzhTnhJCS4bInEME2#RD7aHpdxx zDh4_e^ZEXUme%=BBuZVE}4*^UM|n$fRhV48@dO8-PvNpKPPhQ!X%@ zd7D&sRViefa9#TGLHh&%L}M2)0;_!(dgiGPB+<6{I)he+bmJ5|TpH4OQE~qCh<75N$)2EevJM=&=B#` zXBJm_WqX*W283GT>T`4UF8ikJTC9)SxV#fy`J;o;EHTV3Y8_Zi7oBa%M!Y&Rls`ykTKbq;1);8~PN93h_J^Y;Fcpz@Gqm{%oFvjAAcBCU#F z6_-`rYfwK$TU5d2i2j%X+IR-EDct8|X_&R-oo4te*G@gZ_iG&$Dl;#8Pmrk^&(ERs zTjM0a**cuh)a)0-Qg^5m5Au+%nFojCwf==D(@aD`4RfTs^_k-byDUum)}y3C1DtFP zOe{W(hMx?@X7~~6jEN608v!8aSHtc-57pz@`j6}=%gAz*I{eug##4o%J2k29r&!Mn zl&nBq7a5SNHw&|jbqINGkQKF3U`kH#JAGrDgfqPDEG1YiPm39bwvHb5s$%6J(^B=C z&78~D4R+^rBWh**&eX{4{xI7AcLzIx;ga);ji8IT^(XlwZnCCuci~W1tyl*YzeKAa z?J!%ApAC~gW6@1bL;hI}M!8n09!<%z+*b>Z?;6Qv=K-PjXRZhIp2{&cvOo2-zD?of z5$IML7z*>Z(|!A~*xxnm&1WWTXFIDoEQ^An*E3m?F8(7by{)(7v~pvgU&b`r*+H_V zOXkGN(hFp?e`p{$?)axh*JliG9>XLtN(7w04_iQC~q z&f;q0j(%5jgMy_k({m|2dj1J?rrc=+Pi;)!$=nUB{4Kg6YjYVR*BZm@r~KXYN@xGB z^~rzU-)dmXjXT*W~#>LwFr zn)pIGwF6$2zWLkxhIAQn783jkEk2njqKZ)&R-TN*R1)7yK@i zH^WnkXJ8zn_wEo_K#9C>q97&ECE>RR2eMx96IDP{Ew=jFS03um^{G+v(?(G3HWf{1 zDBPSp{kL-0@3#{Xr^rbLGb{%aK|2fj!4?@f^R+9VI4?bvII2$AOX#3MZOI!=`Rxx& zpDCwIh`4zYTPrzsGk^Wg1)TqWT!}5lS$R^L7>Cl+kfd#JeWrQxjKTpRV)w+1oR6`* z4uvV>2@PPPU#P;aMuFRM_Ri3*Dy1fy6$ym^1pZr0?lZ+qk)rhyhfjr9lMyp&76(=i zE9H{!NKq_TB4XL{uS!+l==-jIhV40Z-DHomB43Sd3BgEA{0!{4NXaD$%BCXt01#gQ zQ!BMfVM>r#fJreJ0sz2S$Q2PlVlSw$1brk10|5ZwHuyH_(1t#FI+T4r+Fuu>gnUP~ zn`e{MV{un}w3DDjQ}|(alKh>3g=Lr}MX}UgVu*v;#9^u9DS1}{?|Ru7^=MJE7)_c5 z^Zda3e2Fa_<`@-pj%t0ZE^z<2T5O%xFkgbgLI>k^A?@3k2Xu4`4$?xEd?Y}d`Cy*o z((qAwzDG0{1sZyzYDu};LI0d|SxIC4la?5Evc>Dr+#09nTFPOi? zsPv`%r)N-aw=orA*i}9%BLX_gL_eXY|EcMmX;8Q*z$^>YgA8nbQP2xz>2GjpJ0cWw z`(X9E(C=Q@8^>9yQO?lN2>1}FgC+5MPA`UZrUZa0nbDJ2MGcLY4)@?-jI&DJ;mY1T z2|NKBsh|@HQ#Wv1<5Ud~0o}5@AqUxp$)R{zysW{joBK%w zJXGSg7+EVuHh9DP#mJf^WHl94xyv^wFO@noCIx*8%jI7fGa(-JrgTUl9(cMypG>wzB zzCueP7VDFH$M9^NVyqZB1x8wPKQt;P52ZS=G+H!OGln% zfx>WDlS-%)LN%TViv%E)Xh*JcYEINb!x10^9StObZjpcz+bB2*_=p3*v5GpZPQ%l1rH%!s~^}<_jqop-D#ouw)V>4Uiuv3aI&xuh?4@Hwk|!z-)>H z%k-l+3F^B;tfL2V8OGljw|CSGh+4}WZ1;9>rwA;{v;L4>%+6(8r(h@|iC`w^78Cu7 zj~x`I=Hnz(_iJq|Vci=v19_MkN3D6yQ%~heUhG_CJuhDI4NP>X(=ZLO&ag{7QrFsO z+tURO5!kQU?Vl8*G6cuUL>ISs==E*PstB8dgPP*fuRAmd2|z!bp+br&Q9TpLbm!Ifhy$P=PFCwy91c`N$*;tEo_LG9EOUZ^QTr#{=x)d?Kwu?&) zy>k9E@lNjfu!r!=+54}Ek-efbQT@n_0JF8`gyb2kmUp0eqC8BR@DyUi^+F~+hRc$IJ9B+If+plxfBD&$^{YKxTOBeGXA z8#(;Ych696GJz+~(yuoqy{m;?-3H4wXVfs!x0&e32~=!40Kx{mkq12z10^_!d@+zf z2OvbKBc-ryLpH>r8k$S~Vpgr4L_v-#$+-CcY$5v!pl$%m;$0CbRuJI)86P8pee9<8 zZfmSFvCBB@CI_p!7KGETJut5QNnQO&1S_6UoBxlx>$h5q8Mw%}Hcv4~pNQ3ZdF`2_Sa zYsqXyyViC46Kd!MCOY6(i5CERsaoO{AM=8R`N+q9V@tMdpW>G3{BJ||2E}1vTMMe8 zC8venGstHFX`cj`HUXR<4>=czU0Bk%ZXG6IXx60Br z1EVa-6}d|lL(7rRh>^DVXphfP0W8p266UrTUdxpD-xp*h6(v7%AIx>t_S;7t1Ic$u z`LYp9zl_d@!jwsI4OT&Vc#;0rW4+&Hi_2yvlbd|~v1VQE@SQ#`e8yGG?%kBBY004zO`O*pS z@*xn;#sW##5p2K|6K)o^3okA`tXUSXNTFyBE0ODNkCmV0PRNSU%2A(X4&W(}EOLP0 z&`>u?j>6EMwK_|e=%HfPMfnm1>aU8h@)=rrvITKn8hYd`m~){}VOJDH6n4Kd0Zxho>0Y7T@`|_+#XBKd{T3C}&F+h9;LmW-P6oAi{Ev+a;2r>0x(NMFg#N_Ap0JqTBw<%^ z5Iq2N6o+YgWC#B-k7z2sVc?AXskK0;g&vSx<4bt)B|oqsHhlDtCF~ETeY2DNz zoo`BVRr!i@0PeBZQKdw{G1>k9H2G4PT^@lPX;Y6YA3A|Ce6s0lA(ER6^ue6c+3L$3O z3P)!egxO?>j ztkB!an%esmjD&H3ZJg_EIKbP7*DkXF6u~|?2@BYUhirFGvE{st$$hUX^>}&TGbzhM z78CYU8(hw0snTSpl5FLa^037+nPkp~&XxCGOsl#g2r@Ie^ZM)>()^@5VOrQmfh>?{6L zU<$73k26)Vktdf*QHCF`lN4`P+{cc*kH4vp;yT~hqTVoXX;y1#EjF^mUkb5r#q!7| z_lBN_;0cVlDNNM2pOpX9lxS3UyHKU#KcdZ+^oK(z+~sdAR}SGj0`fX@k*v?6=f2 zBK(hf6=c?-Og`+S=s~cqih9@auMhF_vn#tDs~e>jFUJ)3RMpiH3;XOF*BU$LEfrtX zssyW7UjBX~jrG?R+>s)*dFZ9yyG2WDvji9grokfpxvL7v)CT}qfNTN!Fbe>n0<=Xa z7yvXW0Kr6PG%Htv4G4+!lF?TS$daN0-k)8~%b?FnIzEb~*oMYs*`0jzF1+DgpxW$(PctVRX2UCH)vx4F ziO_ohwfI&b9Cp{SOye~UmCaB6-z9iE3+v}KbEJ&Vq+s6i;ZeU3lcJ5Wzm9s<<-6R3 zcWp1Y^HIGalJCBh8cw}$Za4!-dOzRfwtIupi5sO9lNi@6U*OgzOG&I=%Th{Z605?#p(6-cxsJ+R2=`m=s5TjamQJZ@}mJ837}PAs zKHbB|3_zsR^mM*XjNtpfz051te7letE);@FW6hc`>rfeA)77X9eUkoi*^{}xS&W&| z=TVtN5pw@E_JHkvSN(C_W>>>U3mT7Op9Om>nEn-=M^ zQx)s{!*zw!j{LD1n_HnhvvGr;$C$R{&9JLS2A9aC5oJydm;A~~)k?KU5A77pGxMV^ z$E`8OAII$W%s)MJ{ONAFyXE`AOYWg3*KZ#=b^&U#$cj?$^$-7h%J@j|i-;eKVc*Zr zFC1z7{8kb_m_Ozg+l0@Vj=q=XD;L}F)9$KyA|fo0{Ke5;`9uR_zj931y6^NcN7uLc znhhlTDi#k)V{y-YPf3rg$w_%&qs5uYg2du{-lfcjn=ZQFZ4AgSZux}Cn;P_wN(-B zcKPoPO*3&}B@6}mwL`nVzur1@pcj5uqi%(@N;e@ygb$e#);!==Zzt-IBlCP4oP!b` zcWdz8a7+Z)2st35%btEM8YO?xwI?8VqGmD6dG+O}n=nG9mX zJImt^mMh?jhK6h-{yXPh0nBuAwlTGbsvOP>9So+8=!K5t1f$0D>Kb*As~6@z8ZN)_ z@*8ku4=%4A%7NEC^xQk$0%KO#kD_oOgz|eNq=5dN>&{ z(KRo5fkrh!#rq%6QtgcWK0+JA2cGCUVl>#9d2rChePQuw&D#g)DBD%{7n~fEiqaxW zVWC9XUf_PAwj_>a&D~x$`MiJvTt`Fapprq?i`+iAx&MECUEl4TQIx2+dhUl zGsK$zu*6e0axiOK&NA>3p?nsGrRMhXjf*I_(y1nOpidyv_cQdUVNw!@}=TA`=h$s!&h&vgAf+4>@h#{=al9E z(z6lU`u1(FlVhJBqS#S14S|ZsGMp_Y(={@TCs8FT={f&{=v}a;15v`~dH+!L($O&& zmqpHuuT1ROWgD;}0&>rEv=$TNxbH#_yIg$-qRl3eji8XnJDs_Pjoz7ZUq;}@!Zd84 zBP*&pQ>v9|=VXS8D^6cCtu=nDRc%+2ywIoVq4HCEBSt>o0Cl=3?xU4}H;(ff0ON3NAzj^>jmp40IA4%*GI-4!MQ0eet_xOz_A_u60xZ$nR~3T6Ujmq?NmVy6T!e1 zQo)}2-zwJI8QMxk#cnxuj~#VuwbL_;y;Z`?{~5ZKHP&6~F?mY0E2kElEwl+-w)x|S z_*Q4KkmVb1ol9F-Xv(S&{`u{XBeOf-O){yB<5eO#bI}kw`>wTjh&NNz>0s`=b87pS z;UUluhi=qpen#aQ{VUGxap*eIS8PujTvuz`^@?#non9eW9DD{T=C_=lV?)jQtQB=e z?N6k=33AY%yklmRsC+J3HwwDtVU4aciMmvH41w}k=6^7){9F*}b56dxtHQLxv(@ca ze%^tF+LjH+zrvrkmi)B4OMSC*%tE8^W5N|FLSqsp;{yx+ZQ@yuwB@b%{sOl-lD%rK zINPsMRk=LXf!xML2mhY*n84-e<<#EV-%}?SGk#W-j1RX)bI1PfODgeP#to#~hCe4J zm%e5rBwr27lFUXApsXX3Wk#!Y`je^NU)-K4Hy-fb&GUUOzA_k$ZDzI%yR?#|DA)R1 z8Nc4yK**f1f3Nfx;Vlz(Cu$*}u>As9~o2YxL z9p6)LpZVCmEHztW<2YQKdV80HR%T$-^T1~dm4NsxqZy>-nc^)#!L7V18vtYk(7V~v z9_9kx61L8>d;h=}sB7U`#mZOuj2!Ec32pn|d!z0Oj1ggkuc6?t8yBh3cxjI0ZnWf& z=!?rPxVq>PxrIAt>XkCuT3#pJ_P<-K%fV@B)f%(F^ghsbt&)1BbtfBj0GD+GRHY42 zG2SS$r*o&7nT2uK8{+(0N5bPT`Cq6iA_X%I#z?!c;EIqW@b8B>f%Q2zkwc1_4QACk zxz|)j<&rD(fp-Pn^=rGl`r9t$NA&NSdk$FPD^i!h%6xNatVqXfpSW838{0kr8Z zERBDK!W%UxyKGos8`ODgDyZ@HnY+Ix%L5`F1T*TQ^Rn@IQ1%N^-)h}jvQ!v9OEVp) zqiLDWf%|>Vu|~lnI622B8jdn@%l!S&R^}RBXJ82mx8ZB$s8JF`aOP!=Snn!vPz_W{n^e|yQ z(GZVxd-|Eb)#T)Mf0FWIQEP`mh5)S@Fe@MH-Bc!h3U84?DTREcz8q5Sjp!?_TJOF<)Q z`r20M{ZD9pWMVO%$aqsbm86JbEL~*G0G0V=%Q}2u7P^zwU1)Pr-l^;>;11%8U}YIE z#1$woN@~-wW{BD+gZWjc(O9`r>jg8r0OAcB+ZC_Tt~k5E&RhSzR6C@=jf&WXC70$T zm0DalU0+k(z$VGM4@@8Ty!_o#^@NgKK+l8YotJ*94Hyr>-YQg_@XS?~0}krcUD%Yl zXDB`K2o}1NeZoJ-PmU+c2l*l3=>m8fjrjenbSU5c@DS7cmn>@o=9~`Fn8|z;S5Gwb zj6m2)f0g$61$FM@CK+X^3b|(-9nJ`IPx>65UzNLCsdwar;sR;^B9ldkg{J00G`K%F^Cn+m zVdvf|t$@GxGZ;xFhO+3soN3PxA3?h z9M^rFI$uL^ce`|&w*_DL_{%BafBI}IGp)4;+deZS2}P*MhN)U=&C=&7Yo>PMe4m0R zNF@IpQdX*=b$z&~7$m<4=|p~?a78#1RtJlaKl8t|^vANN-+kz}=hoVes?w65M0R>? z7Qm!8c~;R57q~g}uJX_%DVwWm%3aLbxF;V5^>%mAaz8&T8nr&Ddf;w|f*%UY!84^z zhsuGGRBvuhf;c;=-`q|Pq{GTi)Ra0E3S;_+V(74#R}HO?)iCQYzX^LCW{$7lDf5WTneIv^ z@}jt@pa7CvsaZkFzD6lo%)+c{X|`?{o$a#f0HS3lB)0O9}i=Be} zg~C4$OinwG&^Pv3{Q+^uWfkRR>5$88IEwRzdDnsU8J;(eQfVPocynV2@?h9DGwTrcWflRQ-(GV#rC+leG_Dj9J$LpV!MoAKURKZca!04}vdAMeDvenpA!< zyMh{E*Bf{FPWg+g4+6{vUQpdt?^na(XX>rwfN2vl{`q%nujPOr!&v@m2m62#zhUXo z@Kkp0Xkm_@_KV|F4Q;!Z>~H@|jTJ&I)47V&Z2t^(*~~RR)B~rcHRq;?&g@QWX3zy{ zmYO5zph!Qz1s=~A?)y4B{vYDBrC)IXSV?HaXIF1d$Q`uFSH(2(NqIl}BlrIi>2?=c z<7|9=A|JGg>Znzq*`S3pRS$Lw7PfoJ9`&{FbbYjV=(rff0~y_4ZZ~5JefzSQ+gW@* zR`+4$^Y@602;{qT^vhtN`L=-}AehfaRS*l*SzuRwrY^m3-yWUG0-EukJpmdG(XlP1 zCCN`girbTQo!%A+`xAP-=`&4(n!aSsUR~0N8P(fT3Kg1m*zin#y0+H!r050c8*|mN zAs|FOowWPLmH(p>y~zMll~_AurKtI53CjB}G%8Qft`EmQm-!^QA)PtR0h`dlpoddW zP{&Gpte$h9e>q*OX9PT;T|4NCy|JkKB#a>aOt&$|szgG8@HY0ZQT$tKjSCYvN#L!{ z+{v3Td$({?uVwLC99f`h@o(@GHNP-MSeW$}Sjb?GW=>9Q)>uY=m}7p$VD*E8QFrxV za#iZ#Ypr(0krt*g=8gYC9r0W%dhYMk@b@c}7$GclDJN`W?|YX?>H|7YtjaoDO6S93 zCvw0~XF}Lg?H{4BOKPE-Fh3SZCpsdQ^l49OocMIfR}8+rp6j!{ChG`GtI73*?M(xB z=l+nl66VPZ`?Bdf61BF)*Zk*LU!clv)pmlbK{=5}?emK(4LrIP*EbXe2xTF#`YEmK z{vY|x;)0s4>Xn_YLIsJZRVG(=gcl2Z;#_QwN-3T{S#w}e!;78W2QZ+78Z|}lSAUq_ z%6PcC-PH#~^9?ekqc6e(YPu^kFWl4O{&>x7M7A)CM+2i3n=g@s z>Mw1OUAMZ8n(LabX7M%(mfGJvwapPF8lGl1-Emh$_f7`lj1X88O>OD|5VuQ*noVe1 z#^3%zCpFrK8ltzRP=76K38Z9nzEY=e`(Tiwu!8(9}dNM^sVaA`pbiv@Hnl8->K%aGIvdvZTp7srv`_kdqc6JcXo6D*`FN+BOm31r6=dVj8Wwbwn+|&O# z`L|o2lb2()W+dwR#Or%WNQ=cRfmreTgv6Q2g7#Xlq+APqTl4Rn;N9Ba`ELy`Hp@BJ zwHwu6z2=zt+II#$o|JV?sp}HsL=nFv9F@PkO8Q3gd$#>)x3@<+8l9bvQ{Le&%R|T_ zpqyT@fLc{L)^a$!yOUMa%rE(Cl*B%uG@Xom`T?y!BWZ;@?jkjfy@IV(I^A(vVaT6* z=c3}errAw$zDb1H`d+KF;~9M%$bg8OHzyeG-*-;+`7r#A#o%L=(Y$88;7>hIIMr)EH-K69ApCpuP!2V0{BPUc`k>tU!2d~ajbGC5c(D)U z;XDdEf#V()%eZU91BI}FpLb8Nvi&B&k51!i0z0f; ze@S~^F5z`=%C;bNW4_tOWuqs6J7ObEO9a`yv@KZJ?`7_`KVVs`JTfPFrnA1Hz-? zmj{&DOWXa!*{}OkH_=`uO}JKOZ%Nm;wdmQKLDksTyA>}NiuOYIui9f)14X&&Tx@^9 z=k$jJ*_H$$KE)}QZRp@B=sC=J)4%ahz+`%_VHzcjmb=v_~s@b+wjt@=ILoL zG?uboV!u?_&h}{wxjXW*G}5gX=0% z`Tt<)o?1%jb)@jtkQ4`n4(>mzik#8fY?**yuRLUO`YAt_L|r7TIEuRy87Gl!)>K)x z><6uic9y*ske%O>MKGEp=ONSbYI9_EqxRMf6?uEai8&W#E)* zc56WA)-}{u9nlcVLbvn550rMt!?y+P&Ew{An_ZWj=VOhBe9EWQC^g+v#{JRW3C3lV zayOHwn)3x}!H}d(>4!xrB^IsrQjd)$78PBo{aJJQnzyr(C$;MN29uhtd=TnD2b%|% zuHo=z?F5dN`VVe!!t}>F_;vc@U@6PV2ZDIZFV06Ic@9LI9k3VPm>)rK{y&QD`z^`+ z{{uKnmV$`55DjpV6Zgyj_r^Wa!jY+|;mpSA0B%!5b7c!>WM*nsYF6M%%}mYAI%a5U zw#~+A9Y0^be*o9T5BGiD@9Xt`z8=q!oULLLZ*kYKPje1GSi4aJ>#k(5xHh_tW%n$* zCb}pD2l_11m8yeIC0u4~IvLu$URn&$S>Is+0Bm^7bB_${L9-T!2_aTxn5O<)sK!-D2VsNU(zAbpQ_y zesRmqEKTZq^?bG=!`4r_0G*p$7)1K5TMlS!&~i^Iir7nt`Q$;`%MpOIk55*(6eJRD z<~gZ`3wZSbgEIvLy;jv#C8PeZ|9xDcHBoOYyV~|UE9a~>S;H^8Y;T<=Xv8bRafus- z`o9$L3)6WPlXMs#1D5R_!Z)4S?^R(hTbHE0d#E%q`M}EA`q^QE)zy}K&#*znV+mbD zk|^E17hJ?;p%zMCs{nMO|LpbxSSjFpg<6re(jU$l>;AMZ zzupF8kIy4|4m70ww70%jf|Js7t9qx>{P^avXOA{8#a7I+#OCn}M&7ZL8PB#U*|gW5K2np$*?GsAK<$o?uSizDbvg?hC-YQb(HQPzEsOT?QNH6)R; z(g3eRhk45Vs!*SAd+Ngm#~=MG2GSjaW@du4V01YK{N5Yk-}Bv|EGWvy>;~9gez_KS zL)@sM#AHDl=wA|q9US)Tcf2W7NorQ=Gq-b%G#W^smQOjP3#4b3vthV~4JWm$GrZ2# z0P{#7n#`Cb{MgK8EDBM_W0nD0x1ctg8RF{fNFXcbq*a&7;dQtTC^Da)%V{WbQH&5RRTS1(Oynf!6 z$%9EJOvHK&EbC-~%PsI^A#Kjys)z5Y-4>m2XwQ&x0}zw_#)lL{Elfgp_Q&_8X5aWH z>eGqC1Fwue9dR3Jhn~_1lsnPc5_!DLbAzU_RSo4`%0~xZZM*mGzgX&~`{jYA^MMfE zPVUdC%Md5iI|pFe$A5Q7l33t<&wHWr14MKp9E=}?hc@&4%iaIusb1xS6!!`WLZ#H>6L4AXKuXlRwsMS1A zZpjb*d6&=Jy0+K(aX^gd7pi0@*$jg;qLhlBZ&--Oq6X!KG}0Uw6Gva>KR&_s*jrkbHCdE~s$Ur6$GMl^ zJW`a)+!xNDBK{GnCIIVu;6w`jwiI0;@QBm&+Zx3ha1h#JPz^4=s)ni`1`_`SZeOp! ztjQHAv#T*hbd6HF#w7hWU&b#;=}&vXS@)@03-?GqQ&wRbEaqx-LvxzT3rf$S`O9T> zz=NiqfH7^(#^mZ~TKLZ3!cSTZ^WXzKYti{TtP5Az zPDbY*pq$0BQr&Mc*QQG0ZHp%LxEf`yZs`5&P_L%x!wV))_I{L!OR!|`GeVInUeP0O3<@3Xz9%$Ce{BL$qSdb!cZN$sSUCDy0T`rb~KT#VF(q0@P zOE{QJp4P_jIryMkDkwJ6$7P(Z^vAc}Lt?n;74S%ny{J0sx|qPpD_xw}9uVT^g1)1`Evs1wNF zbk=B9PI8}08LUcDZ9R;Ouc1Umzeh(&fwN6|W{bG4w~;v5XlOh!kMcJ{PZ+H?T2`L_ zdkbj=4Qu{?M^>yNbh;u5rH&$B7-?}U1>qVdP^#^b*|umQbaGIgA~Gf4mj;cJ1o{jl25z1?lE94! zdE+;guZ$~H6^P2y8Cp4mq*741La8ZT=EGHPzpcl~X1a1I@N#7|i8d$|I(L);<%JwL zho*t!sp1L`v;}1g5Z+=S+8ugiZX~qXM zQOQ?=vYiLN*S$n)&cyfQi2Z#5FzH_2^KSep- z&OaPDb5K)AXKp^avt*!dQ}m~I^um50JEeGn@k)xvYpA6Z7xud#cxe!G{;#D2CyXba zo~bpi(ddb6+Oa?OxX%Miw!#(Mt5&&XfhiAU`RAle#ev ztt+g-nx6wFm%UQFGhyndyuXZP*T6wT3wLdC!!sSL(f|EbSdOt`6dFrBom?3DMx086W8mhw8l9d&KXH&`UFw) zOyD%atXK$5-{6gVs$1Wchcu*$l7YU{BiUp`<@zxC=#ZJzV$T!uEV`&Qu zMl9ZA1YK9oi6Z1;f`FiSt||@$XAhPu=W2CAh{xrma|Zn^KfiRonw5d}@Swi;VB7a< zc>AYcN5$H6^w9l9TI~B@))Z+nIxi2EiR$@=3+VDj{3cv;;To7f&metjPQG~&m*nII)dS9G+4@A%vDlwIS5vHja_ zAGm`JuKf2`#_syv-!(Np_=qElCg7p}VP1qzbCuiWZ?AY9kH3^P5;KpO_-p0B)Z0rr zpY>)_DJI`na?fkDM-HicrStHwlt(-tJAPP-66X4Uu!{Kb1bwm~{gsx%(o^4D>j4Iw zGTR(haix8*1TDrmbG5aHY}vE6&bidMTxwm^i;tD}!c6oA(uav?>Q7Z))o1=}s6;tY z8_ZZgA1;bpd&G_LS*Sl{Iiu$SdE#b3$<1;s6 zf@Sz6rOH|H;u}lu7X*ukM)X8h?!k8ovU7^n`$y$XnM6vGE-tdYoa-k;Q2uoAQC4Kg zNObtWHx7AT3NLc6Wn=E3K)+WJveNR>=kEoQ_k@GB8ka&fx4{Y8UJp1^gJ?+l^Ry+y zNrnZyUiHB33GMWmJxmHDBavsNSv8z}=*9FiH#rxL3ju(ZUxe&zQRHNz+b}LB#=D|Cc)QCF`>Nlf3sLU~l)cx+jkxEmVc&`Pe-<&3he~ zBm(Yi=7ODRQo0%9okR)CFC!84+}{xM2Cp|hXZk0a=5WBQK|Ri4XdFNfbN|;SFuxt7 z7ddQwAm4p9 zY$w5Hxgb**;&z?|YN3Z3L{$J5KTgIM&nd+&KN@TS#`Qsu*GwZ}eBz*3?fx}2yB76G zAl{;D(st^1(a7GH<y1A}xa^%AUub;(^-UfJfgs+0i--{uNfFUbp&GcSQD?t^=d;rU=vNcL!P2KZ zd>j`e>O-pj^c30&R<3KYc~xGvZTg4X=DDN4s;_L($YG2sKieH+^jGrpF~yPixjF(z z|2-Rl-avJ9kIuve)!%c;-tMf9865#MjpB1_Rw+sGQl6Sn9B3t8OS-F(R~QiwUt?}6 z$(?r8U?_$er{+1nxkayDf{!3Z!skz^e6b)HDm#W|4lgWJFqthU7ky(*@vDJKov(Fp z(%;&h%tO=`d3dR1{W&-98Eve1_Gl56ug*+V<#4)oo{NUtpWL*&rt(6;y6axsxASO^ z-`5RjaDQ{52i_d^uwB}xS$5Wv7nx{KSWfOgsB$fMuD3#Kq;zA|-@DfMoWB_)w>v(lWl!EyLxuu1TClqUj}>gKDL1K~>L_ zx~q^GdR{^saY5Pk8#BYnu5j@QquXDSL$4)`#A}=lthY9ansa%4%tz-IyC%Y=cnjod zkoJ1&q5XXoweF^SoW5?EiPL(_2B;-6*dR!l`dgB=f-4DJ^Sk8Vr0b&pP50@w!26DO zJhD^%xwOOM$Z~Um;onA;kR#TQF6@e*U>u1dU6RIEE67=#*aIUaP8E^8TyUZ9Ag#jc zSux}i@|+#YFCF>9W{P={iQ${qSK0XhNwX>NN zyrS(}WedRa4y=YcIdZRnD{DN4QN>x6(#n{4N?JLTS}IUiCiJd>V0HsUoarcM6{aM` zPL=(>|FbQ{Gzp*cPP-b@_g8BbAJI|JbWR9>D2)oim0GfDPI9)88HOw!QlW*il134c zRA6yapUXt4R=GiylquDpf9FfLd6;8Pw|fsCF1LFD&$G7$+cw{^T+038V7}x9pUHi1 zP7I_l z0B5KyN)2>Y(=2L7YGuV8U52JIUJ09LwA{B;dwx3E3H zh|ZVwv2e^Mj;b{Lz~D2=ECw|o`SiTa^q72ADSuaNF}TrG&RnNxXAUMl({4;9A8fff zTY2PaXJ)|VOBV;;#0VRk>H0hQhO^^sRc&t(`ezThzFTiS-|^G1HKnny{c;-b%6e_j ziN4n+(=!J6Ox`k7b-zHeQbcM`kVb&#x?U3wH8;4b#(kZTQQgCn!&bT4T+TP2VQ}4c zNWruG?tfGdEq=bgd;CMb_PK?Ti&{MOj@~e<*|APpwKLZ?}Oq##;OL{ zTTfo17gG7hB~3dV-i${*V+1v>4;B2--XMZXoNJ+6q{7z*jC4i07p|M9RbHUjI58~j z=F6yC&@mTOmz7uoi1|Xa0{FS_VTp7sJ37p7g2=slM}UP${cQ z`G$lopx&V)dXZ0ctU`=Sc6vv_ergYm z=Yq?0Bd*||T=-ro5F6_214~qXi6#YWy!v}KX9A1Td!*`RLKuDB^Ja{BtZv|eJx641 zXU1##1m;Qc9e8#rm3$e)mz8CXucd%w;1p|%Z4>_Nl zDU*wn97hQXl=1umREp(s3dL2+jLns^Aj%SFdEk|4Sy<#J>~7T|eeDfoOjjDFc1%WJ zF04qsm;(zG4C^Cvnsw2GA^S?eM9Alz+wGmcnH#KI(*j+~Z*P%P$NWxg%nAOtdYJ}K zWNyCiG>A;oJKV4ft`6!s|KY2L-`-zOLI0bEx#MC^-@ZB|PZNNY0OAuF826SA9XK_G zk1G2WjJ(pR`*4&ViijwxpAS1@2XYGEjEu0oVung$f(IU^=Miwjbc=y#`|!FU9q(zl ziA+&-DC;R5ooA4F|K9$zdaz3!2-pq%GHszG5O%{!s^!k@Z zOSl)EsLo~}efh)APWp2I=m91d$;7-fD-#MGZC)WKf^^wRSMy)m;Wud4;}v(D7t6{m z%R6b4kdzyR_UQ^pqp|xagHHL|TY3F7$=h89lflF`3DA6CYS$`Pns$c;eqCrlaRc@X z;#J^`^Vz%f98xyf=ZV|f&Wv_LcPFiNOi51x$XNuq64qD3v&A(5DMFbd8}40t`ymW! zgGOKl$vG~{W5auE_NptS2K@=^1pcOzkiycL%mT&T69F=pWQ$zFuAm%^PHC)kVnPZM zugmp;&LgufoV9wL)p<;X{25|c_pu~D-)!}k@Z^>Mk|UUz0+!ZMe$j4fBIK`)p&i(S z(4F)_+_4{A4Aiv9HZB_T)N?4-t&TlEn29+1S8l!bkuH09V;FdH#H1)@f7?u{euaWF zWA4F{d7Sj-Eul{t7m)w=%Uj*pr;l${*53a*mRP7%zS=RxzF^R5N?P)Q&=Cu93fNiI$F;ASZ=sm0y%LMAJvh$Dc?nc=>(izV=6Qh)oH)5cO@YrjW z*&6=yod0P06o1(}{!fGKu8?92PARH{^=kiGvyAm6ZgpIU%Q$oCc?6qq?AiULt;p-g ztIzzY)Vr;jaBn(YWng(tkmaP-KJA0s;>)E;3+9v)p=JVlS&Z{@vvSs9ias9iJ3XV& z^GrJ8_LnP~pHb*GEfdV_P-px-*IaBhELR{&h^JW0Y1B=VB7cKW(wda2leF_M$Hq^(*j?#OyN;?`?1)Q5*gnRc!9Ev ze?*F3!Q0?U?k5h4P9TBUe>y^0S2kBTg-J6#+k}t1H_@n3 zNNn~yn3T^19UAVR+VpOZ!yWn@H2T>YG#V4XmE69S^y$mp_bNf;FRjZebz*Fe1RpS{ zSI)q9k_aX7JDOI$R=FO`K833uy*(!7{?opzrq?|;jTfvZVA73{l?Gue`)BE}U&J4c z4LZ$S)h!lvi=$d6pE?k&+}2>aZaV8bq-c?=sNbdvxn$uGtr(Jvv(vIQeDx=w$!zbC z<*eUym=<=EhLkSDH;Vo%7tBce>#6MwnDCesXkMDT%J@ME^7FXA%{CsF$mr z%)On=#iIjF%uCASD;1mRYIlKu&-fL+_ASlAB>EPy0ew>;z{&+>6OGQhf@y6o?)|(6m$)lI&wtX1gmw;{ zm#)nNReOOtF`rBqV#qry^^?Vl(+}hV6wp&FjMORBP6)(~*5E)n=_A(pr$y`!v7qG2 zLFZ@=5(dJALV#G_JB1s=(7LYBt%p4!+X#|eKK+>p{|pd|%v2(Z&BV>`KFScXJP6}m zS`o=wM3Vz?qjl-0H2(RlR~~~Gn0(pztds-LI6g=@LH|lw(Ktm;Y@@3O`JEpo>qE_U zpWdmfn!ZE5lC)!sw8zi*1nyk>9N8oQKP!gll72J;)dovdQ)$!)I$mm4&WY8lfSM1+ zl^vrM?MQ4WNjIbWMY43t6|LYX{^j|~&4Jz&ldG3o*%bE5LTXBU6;Ue8ka1!}xukhr ztP#_o(65EXH-m*ujuHt*Ia={!Y<7si5h9cCtC#aORu&wc|Gjw$kMLu;>`FvCLZ^1? z8e>0FZ)CsSwM8K*mi-~X#6+vQN#MO<)QZl5xau_Gw(h6v2MJGY?(3wdHU(WhMzU<#R%X7_p8#@q59-*KH z655psokFEtvRzz@dTZn#r>Fv^`(SxCpW}jmP*J)-$kyvOCydrQTq>+4_sy~F?Z3cI z&|vEv^>4Y3^NNmd*Dm)A`798dS}k-$JvvE-Ny(#Qjf`uPV7Zq9q$p6PL>M{C@tFpA z=eX0KzuFz=5_(tW6Mlb`XI0b%&pXpJM3n&fULrS1LqF%BBs3I~4)Z2P<_Q8fM?{U^ zV|ql}vIidapjYK4zwgp_M=@FU_eBAN&4c>Mfe@}TMt&qUQ>$)}Fta@3DWCAo9JlWi ziSDo4M#4)C;%tBhTW;ge3|a71rG4fbL3 zeg~$4bmQeU+(p=_tP9aFeHCh3rq-#`So<6G- znTw-FE9ws&+PEGz<#+q-m{o#PL{d|L1`H>u#nHG{+g~wXB~pcBx{-*WUV@G;Ywjf} zLmDzSAIVILg6u`MZ2%aXC8J!o%L?P`Ad(3aAUfj$2ZGTX#O_%@fi8MJX}csF@9g*X zYJUG1?dXhw%m^y=z^+*l)GnXx4sNn@L7UllC5sADT8Lq+l6qQX`Kpv&PRydgY+O*? zfxUIvO1;-H!?eAxvx2E4Y~%(eGFG8~NR9>b@4&)VSSu@QL~QDf#m6MW5U*J$2eh*> zEDtO%20hhjb4^j0+PsdRwjOduWvnp`aZ!&s_W7I$>-}>!)V(6Ace#zM)f>#;#0a$q zkzpHiJiVLP#$A}&vliAnMlS2NyW+$io_}kj%u4OU&@#q!xo%cq8`0$@YbbgC?e_=t zY`9-QQT|(Fl7vH=_wr_3_MOy1Nmzk~b+MT=xgRvSVNt;L20*UqJF*8LM^28OX4PL( zyV)%W>Nv7yrK|98KPjS*L>LT#EMwcpwG10;@wH@PsP=^Ttl?qlQ%JsH5pNcHj@LkF zCy(|lzaHdfoQa0aEx$eyW1`PUzFX?~G|}rujOQB=&duMz@kr9d*CcGeMxtCf3#2yM z=vC&eUO#@Ii=^W2*SybF!plIKaF({kz)B#jPw@<1llV;SSlRdjsS*kNL8 z)!+q)E?L|1M_p2`6TTo+vmHoQu42Xdh=`i{CT0fCk)*f$)WUihQzw8@4^Ybm@NyBN zvO)6~4OUM?qq+1Nn%^W#W@-N}7R}*q?DlT9BqLd*$ap`HH}ZZ^C*J7vp357Uu&~`U z#5y1v_yXYc%Eg!V;G_yrQUAg24I;|-Qin!^>N*QU)c0!J@Q<1I!jJnuFOTgBm(3HX zqBqnRZlH&0uu>8H2LM~=3_+42?8hUlT5A61eD9-@|77aelGQ*<)i+Rd>+Ew3iq%1(_p@GLQ5f z%|guq&P4me^JsUI*D6}i^xtja83P-5-h}&IX&F6a*T(a3}*=GSP{pQV_t@r_rJ<*-1 zNQr8A^p&J%s^2tk)aDo4b}=GiBh*s$KwDb3xjy)cRzq{SB=Ecdeq01=r2UKmDiOHB z7fO(|(oCKN5-EXfvsl1?05Cg|41u&}mMbsVzDM++(R}7(${aV#jkW0LGg&vYN9gy2{(>RD*4%v= zSx-Ru`(EYG&JLZV_#ElJy=xSblXL{JzzVkN^co<>AsDiC>hdycYQNQ=u8g3KDQiIh z>?jSk_s`1>_U?6MuYu+gcBsY^-sBL*5E)t#e^}FL>?i z$=%*PFnt5^UhQ6fYxc(Bx8-CuFr@d%<`1<;UXs{uJOh{ke-wE7k-)XX>ONk-*Ew?^cDo7Y! z*Ir5dPQ(t>xqf}wXE696J6Cy1sXsey^!x3>Ca>Mi2Y#JO@e3VQ2zF3NnBw+7kD%NL$n3F_xG z4qOgD|6;F(qGN+1pz-u+ZMT8}*8Q>j6_oM1tNm5+#b=~1EVErzSOPQ8o;2h(Y_Cor z%z1>g`iz~mX6~^;e^fI(;3a3wpUefACoq-Olj`A~T6#W8r&2b&E&4~bT+B6}cJ^V@ z%bd;G&y$g>V>R_q6m-0JS^&0A%1tiV(!PKCimooqvOcgqQQ0TCeBbRI|Pb-j7Bd5Uw;rMuNf&me^epdDx-N)U9fFjb)&6?qo%QveTI^ z*IHBE{jPXk4#Ugxd2_CB{YK=F?%AZfhy&-+Th+>~0N4ap?$M8zU!OmYr1%JqdVcY; zC~yr-Xn5*;onqcK+zpohyNJk8kw!}8%xgQwDR#4lLY5`B-0tl~Ftmx4+iV4IyC7^$ zN@_p0zVo28tF?r9xgTBf(Bz&W9<{6bN0(Dq6yeo4)R7izVqt_=^O7t$Uv7VH&xyDnDH`v%i16w>!R zfyqwjJp;Dr!ntqb6!htUYXf!3-5$Efw7F@hf*+@uvi_w_rD%=h&cW!>tNt$P6_D;T z8paFoTo^TZrARBZ(F_VFmYR;r|Ba{!)M}VCeN1Rr4XVLp&@LC?=`9NGrS&bYZg?My zu2}VCirAIy;*#!8jJ6{?gH<|3gEHtG$%KqT90sVj2QgR%HWW+)78Nod)0fmNBqhY~ zx=pY-**yZjBc{2cATi4Brt>Pc!C?m9c6olgK-Qr7y?DJQxaQ-!({fluKya_ZvjhXz zb=Or~imO{`|607idSkfn*`yMWxU)HmB$)1@)Hr2Wa!TCz7x@gL+HLH~XRh}H?>c1{ z6{o;?fy$0faW@F(#Nsfb`D~gd^TGnd;0o+l;=9YjMo*MwhfO*GAKz0U<4ZBTHd21F zMNVz5xmCaAjV~Z-j%<8qEW2?hI)!|{h46FKrs?4GDkc%)Nb!aL%s`$H#TB&fr^Aza zAv-dOr@tDSc>Q-w;q<)7M)0)||D57xJKfxB6&362a6Hgvu%et;09IdMUF6I@y}Rc| z5|bBD-7wd%GfP_5R4p7<$)@rQW428pm@pAYd$VwV+}nV+VTxxn^GBop+es5f#9%cb zB<;ItWf|?Scps}qcWb;YzAlRC`ZZmsK=cZqOb@YWHkI>q2O`HQQ0pEAQdn>3?)$Y-phr+Z1Yjnko7@)$RcksU(WTKn z7Ik_emoDG1c4-S3&-70kB;T2<`eL(~V5ML+IxxyHXdKgR<3rux42;*65n?tJA)N%u zWpRe}(;Ev6S3eo48p`@I{7(j3Fi?cN0OmZ^a`TTis?OX;>3t{sA44)$BhAU`^)vui zS}KFu1UMb^FO^GXZDUaGxkmUBYu`8v!yCOcFmt>%`2Q1qs=1hCK%;?(V(QJ6oUAj$ zy{m4uc=ef6GF%8AYZrmNL}J+fe|vxXqq7Fyc*mi;G#(P-y_P@wxK*95e~~tPSouhK z0$QP%W8CNveR_z1YydgN0qxtnq7Dqh8fWK5tth!9MAjBq<9#5;|J@iK1Ux(T-VM_s zC{Xy^bN62V+hDn0kA54zDc*Q?4fTcqfvs_xYcCjQ0edu0$R#wk&p?k;S9}1rvf}9l z-Q=>%@I<~vp{oT0u8YaFxzCrmaaWk=^C}nN0|1#eXXn$yy=x7Jo2`h6-WyjMP*1Cd ztiG}PwrO%Pb_G8!hm0$PWZ>G{>fHr27p=h48z5y5z+UWevAq9RC=`d`VJI{ZDQCpC zQHW->D3m?s7qn~kT(#@}wEOXsYk&VwcHviBn&0d~M{C_ozQTK2;1aXYTKRoh1{-ka z?Ha%w{?Es}Q}dCuN7K zxAyf7teIpbJ~(PQ87z-siz+NQQepY3lLA}MqG$Ef$A<=eCk zBAE?zYkvqvZMYhl@qs1r*MaH_w5IDAu3_GYqNa zzP_ZO0RR?FIg_!sl^-R{0$P~+$g&Z9r*9%@311`$eb=SLrE*YlhteQK1FM*d_CFi* z??;E>Z|~aY-CpcotEJ_gcfGZ)QfS>1jZkjBq^0^aq*dt70S_3)@6@yZ=gh|wOX5Z6 zenQjP?s&2%)rnA+%Y$#+I=? zKTe?@*U zpFll#o5D*^TxAcal6*U=?(IwiRr7_pNIZrb z0+i=4K(rDtSDNMQ1lH>+BJ!;#m{u8_gbNs;f^-8^BBM_PTi58U2v-Jb*$8ob{|LeiKA01Hp1xzQ75Bj3KA4o2 z%}F^|k&N?CjvTbPY^j~ zF0%V$BCR0Ki7UDjBbg4`&eJZG|Ft?^WfTr3=(ltepDUpd%CcWYa>p`{-kUbKCz8t> zmfs}+RLMd3F zWIF2Lft>nu^n?Eg?&ND5_aLI22&>gL zX&{!p+(f+r$bY6)E@LR)0!-?95EnU>2TYIk9H2_lspfBx-(slfj5vdE(=!5?HSxGF zMrNCjbYny4e7PYEVqv>Rkp~F0ih*r>w}=pt@}=`3ehHy&H;x)E_gm{^Rvm< zF-AMJA;KlVV(YbUK#Wd#wM>lkhE`R&(;67hl9gi0DxJVNmi0&{kktjeH;vNF0%G?k z8qNT;g19Ooz)KdwftsZI1X3q06qDq7NALKjfxJhDTodbXqe9IfH#Eo}ZxERL@N<>eZJpd#}R4l3S z9pQks7x_ArTk62PqPy$i{ODcR<~5|#3>h432!Wk-)0$AgCI|$ihJZv7$mg*N_p5Yk z7o_wHYy8?bd{*_}CUwWxI=9o{0*<~_5c)Y)&u;GI-5-(Qc>NQz`vTkyUcV`?IS`hY zj#^@#s!gYSQ9|Kq(MSN0#6o-;$xb=!o@R|&Y%_lBJPUb>4ffwWtsV<^qC$>P(eAcLX)(82 zG5FMV2p5A0iM2c!B5SyejTI<{vkDHfWfC@_2~vZO9a7T^S%?N(u{94_GTziGnpMbH zwZ?x`ybP)CRXfXO*J|%>sF&qdqWg#z7i(pg0hNm3a#q=%wF0?o)PRs; zkQE0}hk>1B!);|8Q#;|=f`e`T1e-C{AEr3p7V^&P>iV6+;3N_F zXoPDaj`BDRb&sX@oL883$!Lu^@R->mY(&ipP<>3)zsLazrF?%$dBDFd?0*}(d4+4? zs5^s{I;{IXHo`uS!Uf$fV1Sl3?@VGquUYcjU#Df@8~}`V?jlZ$)}loC7a+Ldzspv z-lc4cdVK5x4smfBaf}1=mq2zO@%E=eTochZXvii2oY^R2z*iM2>KgKOQkTQ>V)tmR zD#9UTPl4c(xY+TD>NS$aZth>3T#a;M{5dN8@Srq-{&SP2Zl`0L7W2U2A4}ljQN65Mx#f)<0K|jF*^?O@7{g zvkMWLj);(B)F| zVG3%QeeKTHEOT4s@rp@9DCX4=5pE zLCBZItR=L#Q!irtKJ!gpZelnO5lPF)vmC??XU{vgQV_9J!RF=5&yT(4#kCVZ4l*c1X%bJ@~Ff%IY(T0wR25*(Tw9j|_ze2{5&nhuLnM%b_qD$PJ0BJHZv;P-B* zDbl+DzxMnuqo*RUcC@~G@FoP;)1cv|)%O0t51f^;0nA5WA>0H{Uy`}^1`4_BgWm~H z7eNcTU@Cx|#+tCTWcc$yrqZs)FSt$xr#MFWjN!v~3+0amh&qmjm(z{CB*G|t*Pi|4 zsd?JT$my2HXFHX((gai9gST45HtyXW3(=N7Ec;CYE=fSw>p^m=Sc-Pg82;{55fu3g zT6uQI&igj8Sh>-BU9ZAOPuPY$YQ{i+rfa29X_uSIU#$hardmuzUQGFnU>mKKGFy`> ze~pDWjgdvS-ra8@TY$a9ymWNe)gxz1MC)fS|Hfo*N#6eY@!0J93@TNr2znJfHcdBVFLhs zI2-^_1pxmi3j|Q_IY>27001BYyygJD5z*f`fCrnXS`4z5+D7@gAi$Uy_93rqqJB-w z|B?uWnackuJlk1w+Dz><@{1Utc;%P?=1&D9mmxnzXkRwCl8D~=ir@giUAx-*!V6+6 zp=B(_#1P1$1EPn4eY~vtXH}U6h96aj9eDs%{+wHg;ni)c>Pl`}i0elf)YPPDIKJA~ zOAEG=yTkSxc^}*om<2akg2ymny9K`Af_k%v0fAJR)162|`p!TmJQNJC<0x6eUaD}- zzNV>Bh4`vie2doEb7@N1G}GuJuPWBi2j+%k|f3>X2+<9fHTK!*CZz={y zjOw61c3l|B^I1Ev)&&Iz%gDQ_u%5u<%2RM%KD0E;&lQ2Psp*~ou#)Kx@1-!>X{e2W zZ2iBKm=(lk*tqk^b9vq+4Ni`7Sh2!Qx*_EIpH2J9osv z)Ok<-vHudYPkhlWEiLArAm#*I_k75n}xHO~|hQ45mDI~&v9ZjE1d|Ih2M z{=N34B}z|#5S!(iLlQBLkW2I`l z4ox;j1z!3X(UY(-y%tfNuDuIQm?JS&PHSGTe46gzRl6y9#1g|p41PS`@Nf30Hs1ey z?u|HIoSyHbQg+7r&|j|ai(1iZzRAw=A+owuJmT8*foIooD}K8)fkbyc=87gq^9zwE zaRGo#G$EBb_97=RZ$AN0w8@ZE>j1thvxN77+*`p7wEQgyvP#dd!}-+Gq{p*chdzJ( z5PSaJ>^tVU$O`he#K*0EJ-a`wr>OVnZ7*K5NdtY*pVQpwUrz7{7-CkELPq~`QYx8K zS52(1yFA-kQfb2>B>I_eR2JJEU+s);f-DAKwl4r8frA)jw+Hp$@A<~dF@o9EQnV*QQICY0TCcdatRVTyCU zE#BJ+P8t?AmG9L25(7~>I^bWVI|$aUw2%^)a@K54v>5VK(1ZlIMDm6$7i}9gT-573Nplt;+c_xqR(v1@%~;}N@>tGaJ~sBp1cH}ENNeS`XNcX%SH z7=u_Qm;k0Dcpd{l2?q?f5mbX2d}pvY$Cd_xQa9YrjnsT*fZza{a~@zgoRfQqvE1G} z6S2dnDxzO%(9Sh39Z+&7j>}X(<-Ge~CCPmWvGC@p8(a7>#+{K)3uj1(jgvMEY}yHW zi?aU`&B@+evJrrw0s0z?x7K@|W!!%xFl23>%l=)avti>bL0%VTh_RmyXMA+FoY`Xi zVmMm1Qd&VoEQH;WRZ^Yc?{f4b{E$4f%h7haNd8LBwt~Df zd(ari%Tqd9r{(auSl@<$`9F&8#h=Ok{{#3=wi#w-PBUzdbDl%a)i!fJG{=x?jv<6p zlH5CCa|$6zay}&?mE`W;=8&X9DoNdQyyfnoQmMQ4{q6TB?D4rC*W=pz^LjsDsbR{e zNIQEmG{k3HJfLC=kW}8Pk(tUL0wM3C6B7>V2ehQ0I9ey!Z!4XFEYYW+Q-rogr(s zPzp~}tl;ToGF-19y4-n=1W$FfDq?lVVDHX5Jdm}{toFTJpXuE@k#E-yS&93Lr(rci zD7(4jdu4Wd{&ChxB!7#evzrcZ1N9CTknx$)hrobFW{#buSpgVKC-q+NiJA|J>SE?@ zKpJu=7}_M?aDP`C6ypz-t2d47d`bL%O__lGxR!KRU><$<}~Rhg99O^ z(2j2vOOdl1T*pC_Xdwp)Y@ak?iW0Ype0n0(SER$%e`e$~b$_2JqX||GIQS6)8Y2ALgN)9iWZ@RTToRtHqRJ8+u4N$SZ5h%wKl6{aLM7=)nI0(V@~Zmt z%l^FqZ;kF-D?|*hqn%LTJk+T7u5VuZ+Ps@wd!R}!K#kPoa~xiajp3(Z)M4fU)t!s<;cwJ?mC>akrSouaYf$^;}6}| zRnBK`ogPYooELjpw6PRU`@3GzOUFKW2b^YtQnVprfeWS=a0W*PeIo;)kA-v}65z#! z1JEAFUgq&(T>!pH;HcY7_klDhz99>stL1V-c`4Xx--~qH*ZpVnUl}bxZ+AV?vdg^W zY`jzo-Fw}NAN_&Z7DnOQ@^mp*0C6cVHbet`*0S1zZn$NX5UBk{qLfVvwnV9WM12U@ z3Prg?aB1>zz!q4R#K6U_u^dP!03@?ny>1?olA10j`n+!6@I&6{BpNxK4c6aeC>-6M zDVayzSFv1G*op)qs;>Imly121{Ut=M>o1-5;8CsbmCGHFZ&EH`aBu^3;zv|JVzuZK zURosWxxsY*okTr1R@FfJYNg;%R;e-n28tVCjZy}xE~B31o2C`+t}Em`GykgVSz{+x zx}khQOmeeVpFyIzBUtT|^=BR+LK|y1*7mandorWsolsSleU2*z0Gv>A%b~ z?pKFJNIi@`S+8pXaEQY)p<)oC_$yK7j|?H?5@xSN$t{|;Eenyk;6ss3r5-5S8*nOl zjks}W@C+>tg>xfzHRY_>+h$1 z(11laX2VgAoRord|GQoNGuQBGXYxpX4qA@*%B}&WTD9EL28Tw7wdR(=yGZ|!pxI_| zWYLiQx);{^@43(1GwD^eOX#(XzM(WqwxwU+cltsIouzpPqF2sYI#6Cv4t7N80WyXy zXPqgO7Be!?pUyGs067kV3WRFC@Pf33n#?RwCPVb{G}SnkTE(QA6pPLYZ7Y76Xf76jZ3Zf=m z_5#ZOOmkl1gl25OML#OMu2D0hZY;Ukn(M%?(dgjzIF_aGaz~y{?hX(r(0PzRb#3N@ywrKv z*(@$m9n@;dmgr{zy+|-mB6L@pjB^+l7=7&*Vz;seH^{hw3Xp=8mTn^z{|o213GTVg zt73#eS>1BiFpe!BVzsI2COaut1a@L@tOr4kOs;Vadv6Dr29}zom9ApsMn5N&+CXzs z)ay{w64>}!0ks5b*wYtM*#`EEzs$s3tn*!PI&1G8wh9SFRKnoW9=;AzA+jo6%PQ?U z9_%9a{n})@@gRz6Xv1)JZC%B;jLPL^&Ku|E-TX=e;vU7|UgfAtQX}5hsS=T4pv*D= zN2{S7fq(!Gr9%cYcmrI7H{xUd7shezt#DH>AFiuVp}Sy3Ldp)ySrbACEB_WIxd0A_ z0Ob*OzHsn1pY17#d1-fSq~&}%xebm8Cd+uzfrtTtd@+X{w%3td`Qho5Y?3c9O3^L> ztSr489RjJilxaLDb5j4yDXYwGP@sar0ESe*Ebg=G6*y#Z9LP^>)FCPW5TYDx002`4 zA;CL&g?+YW#nUF;B6zFFcz{|Z7L|VzS>;@$=eU{vtN)Mh#lr%-lDBNBeS1QE{FJqv za~+lkAR@!^bMd1Sy)bPDK(+%@s{uyNwA1bw-|GN7BnHR;0Q$W*QS)7=0hhhzb2eIxNuqM>2Y84HtA)ly8G8x^O2y zd<)JP;6{7n$`WoLdwBbb;MP@jV8=2mj4Ychu*>3&pACaH3wiqi#4{bRV`T-I%mUiw z+BiJ)_!e|H5PB>PdtwKAj0ge1^P)Rs_BD8%i43{@6GA7IM$C75zxPP*C^)-OklTO^ z#+NRqneJ0Z{+%Y1yk(k2%JZE78xh&MTP%GhD0C-(H!05%kRL$Lv+p&B@WJX1T>lyl z_zv5a2~is4ew|T1rh)%jiAf3Ajs9V2zoWdv*RgZNY`b9GD9xlojA#ZPk zn)&p8|I^CKeae)HGRH8Pcr+(RiQ_dftS!7Cksb?*)9Mfg#)bW*61CU7%o$B7HxaOA ze3bVjTH?g#!2C&glDQS1yKJL!#kTCF&E7EUy+LoSK1`#*;cUa@b9E_yx-ktRto7XA zY9CngeUWVOKOPtFT{a&HqA5e#OE_-u^ENeLJqj!5qK{e{*!;nim6g&))1y5BFI|aN zL9&&O;u};HM?H}}69m$9;x6lxSEq~|e-9|-J>JMc9DaFOt%Yp<|8@hIyzw~Ix~k^j zFe>v=(j8EK=XKLN>=8Hce;k#$p>i01?|&{&=9(=fF8csc=Voz`_#_A+t;XQY9V5s^ z;#7Ou`PcTl1goPLUtOy)rWu(lNJdV6rte*wZcZRK@`9Qi9v>zb+|#(pD0Ypaz_WE_ z+Zqdwcf84UV3tNO{bGq{P95%Q3HuV zceV4O$)@9%eoS*nf||E%R4?~9BmYeQZAR8W7abPA(^bOkYVL!jZ+KLdL9=#bJi}zp zGN5tz#yNPwQ`A*B6E@T@^ExcV;Df@nI`Y|DaT-AOm_tccn2b3GmaJat-jO%Y)Dox3 zq;6eJ6)*cvbBGd8c3etmG#$E<B{(PRY8~kqsDDA9LM%*=}nLZmUj$Vd= z78P}6O|j~?rBqM0GsYZ9H^(^k{)`2G+Q`?bVLN$VQ4w;Ly}-`A$t@*C2-;A)1ME`+ zvgG9<16fuLYJ-=-9`okplNbjc*Z8!q*$m--p3+z)xM{6*&%FvyFAVS%0k;J(j9|Gk zsw(sRk`gVQ8h8J2RQ}l8Yat6+I%An3?Dh02bCXrxAK|iLVr9Y(2gabC5+G_OeUXh| zjEx^b96^|F9OURyEtEh{uRt0fNqQ^BIXb&KILKv=0^y1w6PPax0ow9Fm9PJdYxmI| zk*r4|d`xB{1^&A{YexJcamkT63e6|3S^gZVFbxO)d7vDAT2*Tz4?(d73aiexSsE@q zF$o8sP^v_*AgB%NJ*blY8mi}rx=G|nw0csLMN;#)MsrVI#Dq-S0Oe@lKRrN}*PCPOECk{{YJVjT{J z-c`S!YM;n^T3;b{sB%pv&aC?K_m$*D*V-AY%HHKCgl|9PFh*)FLXjuJ#HkgVDndg9QsZ~&HdN#QiDU!{+@6gqRJ!( zwzC~`ksQr2Xy1rubZdg$R&Be)E z+&g~3j0jQM;lw87(L5pATS{+!v{D+ZK5n76Q5MVbRy8Z>WBaZu&u2-z}?a z)QL6|p?fb^{BN^Nh2-P$lKbJUqK=$0WXAGm8GgeF@=@@>#IboyvJ!tL4r8)h5gCuR z?&rAcDj9_J7?!C%t8)1@W0ld!@q=?I66J+$Rikql+0TF44q~jfw#<5kwhn5}TM!13 zcX{Zb>idBL!;vgRp?6ctJC{tGi3BHsP{EnvVx^~vzVJYc@&Q#j>iQ(i%1^hYjJAT5 zhu{y}QECj|79UiCQ_Qt`>utZjEouxlGkYHy<``}H?eSLeldwBi{w;#}>% z*RW$^o)_u+9>Mn}0@jDrsBCS>(#eWhV4a1z|M_|I<>ix-cCvkLAncho5WU3-C&LcX z##kshANKpi3A@75=%v*0b7@h?Z##(zFsrJh5Lan1B42BQw}_OBB^0`v&y)*p9d0Oy z+hC!#K!;GU_=#ISaG9)W*y0ix_eQH%c*mXW82TvnjM>S?t1_!|vvXm2A)X4)NXH_d z6+{op%p`E>X)>=+QYD$a;l++6!Qv)B7SAER0`Ad2hbmQe+n_-HBo)w$6_vz4X~42I z_7)twZdmaW-?U|-q!%`OKIo(N-iu9&N;SVZD#4p-Vdd3D7K_f)7;`#@Do|4YXrZJI zW;TAZ5A9dv>&S1uG`o!kP7Daa;ew+yh~i{;tL7^%?#>1+vsVzqSAw zn~EPdI&VZ!dM5`N=jiz?sZB|-PllRP!ZD9_^@Ws#N~|Wc!;mf>U_%vk>r~Cu%_DBH ze;2Cy$>{7#oEko@4D-i2rfy89--vI~ut`}Q=nRbWQn5Mee=BNaz0lV_O+4Ur-tb`^ zB}p0{UnTlovWpLHq6B^Jn5=N~vM_fHDyp_k+KG&aTMo2M{5;E2C`dJP>5Xb#qpl^H zzq;8#zFBg&8gp>p$@fDh`Q~-=eUjYYht3;@MTkUCAqcxV)TaSq=^t*xC;gge4{rR* z`O##tV3vv2+VAX%>bHGRRdc$z-T!jZvC8Y6Um~3j+CJFmIbCfZ-fw?wD*9`D+hcp1 zrS0GwAFf$E{}|S1NVzh1BwJ_W?9e-Pt7wl4UL9wGs-ZtFtcx)F0m8h zo2P9D7#wNmO2|`fo3CcdRQMmf6@`fU!N8(DTMZd}#!_U4tVv^9pgd6;%xo!hMLVt< zdLT}#5$ki7A5@&o3oN#sZ~0Opv7QX`Dt4Ugo^3U*<4JRyH`A6Uh_`vvJdBrq08L?~ zCdgNnBj5CZSTh`ur|Kg{Jyl7mvw~?IB{sSdbRz>pRMMEM<7l6GCAbe>26W%sAy{dG zz?xrj4>H|#qSD9De90JjbeM3xdhLqOE^L!{wxikA(HvDCm{f)sVn|`dg5vSdYfJijGfq zIiC8N351EZatK<&*W-u_se0kDOS8=>W}+Usb@%=r4GXc{u}3M);R$S&MHKkz1&CXQ zDs?Iy?O8w14&&(}&QTT%Hgd;!BWQw{Ww1dg z@!=Wc{@cmJw#<~I&QU!Y;dy$g6t%~M^y8O^v*q=xg@k-6HtZ)<YWq?6M~oKeCx!`o3XO)ouJ#kO){94B~)ogp~3(jLYUr!nJ`Dyb4mFUDj9@s z3Kpsl1@oL5eBqhuT)edY(SA@Lm6l))u>9>?uG+KQTqd$ZgTx_O$w%I!R_U`+!!Dsm?O`9k)PmYCiT)`n(N`lc-P+M4ZkBJzMF892kwf*v8)c^}A=QEG} z8Iq?`6BWy;ah2{obEuIZ{l$zt^ML?efR&##Z-RYp7KZw9%_2#m(T$?pRH{U#Dnumk z=aY3|{&7AA$g>YdtE~rj3H%H#`wFqpD3I26EXpYy>|derELPI`${8~QRJdDNTo~OqZE)Q~KJd^vNQ>VfNc{}q^z4}2l(ATS1I(U*aEWJ+3EvSQ^baNpjHL^2UB~m(4ccILkWCHs$tqf|;?p#cE zRhEa!#JK9}!)dyoo%lKTHT!y}L8rt8VRO(^OS0FZwUrgn+rU3Q@ZBL71NatYWM^Hv z5?aFG?;l=PJcP&hbv-Sx%xpx@&_5jUbiMh=={*JI^k!GI6?@@V6LxKjo_hQ);ez|I zq=k-pr|KmcB_3I0X^X38(WrQ~p-KHU(cMZ~piQ5bL@T$b=apB(TkaZ`TVQjGYRF26 z;0CHom}r(;0BrLV@H`sSWsIE~4ecHYWWVq{te^pMlmrm!7@4C9&+at^G$D?tj|ME` zc_DajqmZmZQCF`I^DHgcCylkobeB~*H>5nthVMgesz~lTYH&0Gbm^1<+`NOLkhcR= z9R9Cx5AQRqtYf79$8?u%a`dHLL0)%EhS1s9Vh@zEDYza|&nc@`g$FJ|-=B^XCyXoh zRmywac95Cf)h!^JZtVYSOU3t4g~o+E+Y>6PLOc636I9{qk%hvhjSHEzLBAG_1i=vj zeuRvMQj@#J|YN3oB{zZVlZa_{|tYpr@A>RE5rr*CaMMjqsqPItuPwj3(N*iN! z!!`qz#)8{Eu3eT%shuCA`6W545bQFRO7;{lp~F2jH33Q!DWQm96Mk@4{UoMZV16-? zc|0UQ0Nbt*_zbcy+owLd0_k_AW@>5Ne$o1owP~7ew{}iyyvM z8O7f#GPSn7GdpJ2t`^MmYuAr>am#4$jG$Oj_$qI6YWY;z!)~Yz5&PWWH}*55B)Q)3 z@a1-+fB%^|{W0SEfkm?;=&hN@$a~DlMcrKw9+#9k?>uVbvAilsm}(Ov0#G`oGt#=ZzUgFh8I(O{lrs3xA5*XYk)Lqu`BTQv~Y-C zVUY)c2VU{)co>ijlI(H>eR_aEaM|>OqgWhhKwsX{F{s7Nd(5R!Yw)23r2Mq+{t(+r4%zYK?X}mLGo))nz?_LUS6lfbV*9?(f z&nx3>Rf4(|tc-=V52VEEQV1;)n-&>-St0DxqtW8gr8<@3d6Dfe*ux)rF1{i-$#t^) z@haEVY94gBynbp1ce)oBR#y56SN%AAnhbZB172p()trJwpcBHG(*-{U$N!Bym~(U4 zk1%RUyzDZ~NnPFl%nY8Be$d8gcq^hyBjVInHR|cSm1-rMx zup1?p8PwJmRy*_X`0H{F8WlG|A!I6C3P+5)G+N|&FBDG>J7C5jAj|M@Pg>spI%x~s zBJ|?l3;N1Iyy}W&soGMoa|A;G2;W`QwpelN;9>8yqMHc|zT1~9 z9zDE4T;7}FqnS)?5eOwq%vO1WzgtFcO!<2EDm=!W*FPO>xy0S`aujdSpz&_jRTAQT zrRWDhhVnr1 zXmd-Dz8R2oku^TZ)taE(mSq`JsFas1DbGnAKIifwQVi67(MfP!RNa;4`Za6BaGi~v zj=}*%b~m3kL+0A%SpmNSI)+CBvex|N*{BV`=-5U721xKrg7hk5tdFAatvj&>+EqFI z$jjeh0t{JeUdf4e>c!au!0(ox8M*M!54&-kHrUquGM5M3A&_R0t0N;JKbmMmg=3xk z_LX?7@%Bw0bk*rNcOWq)oy2)N`aC|Qq(B?`#F%FB3whMRf4@tNq;$QK-B7y8^RVsc zutVcZrlCoe+&g^H8x2 z>7e_5{4e&>CnDLxo&SsKqQ0s_1nYN$)?3}ee~D0T8?-ke>y^>#W=iYHRQFP9V>FeM z3o>NR7^Z=Y0DR9tp~4hd!;SU+cbMLw=mWog0uYbu1!_qdQf33Xg`(vP)R7vu4&I$< z_~uN^E7CKa_d$_Ka&gifx^Gv;5X%n-DB|dmh=I9!1AAxu=dwKGfz3M*-*57O<}*xtiF{SJon#m zlJ~^Bwmq_j$FrG}74 zeqK(Mt|k(hFy=uuywH(`a{6WDBmf`gf_Ke_l*O)cCR5yz54a|Lqpi8TBx3#$Mv4m- zpFEg2j7)71sm=N8(Dyq_s;49wZ;M0+?#>XMaIoE&bETWaZ2f;~KXfeLLqdR0^ph|2 z@|cpt%tO1;aewITrHL3Q)%q3ee8V}=-$bz75l-m3c2{G}XD1f$Ea zPIPg8%shvR&ob%g0=7r8aK%Cl4=9&4TKzlDCs_XC+EZJK&uj)`l!<`2d*8-U@!^xt z_8|VAkHWPG4an5qZjoo+QJe#)d)OO#>7u?8=~WAYa7}P`f}%XZvRitnLVk+gfLzT9 z43W*lB~$fX&P%G+e0;W*J@t#*r|^DIkljsTLh4gAjXJQ{u+O_fH+=G*JHjG_HMAY* zH7^brVBp^uyPt&&k&h0Lqo&01Ltz*3JEM3El^zCx+xD!%WyPiy7*1r`rasJqbkbHorN!v(R7UwY6)tQ&)MB>09im5v? zED+CpK5x6?jpZ>UCq9;u&UurR@b@9V3qPAb4f~tjXD{Va7E|26(x;+qOyC7F zHMCn~KRWWBvbxk?Vou$7zeKHvG(@-F1z}HyT~saN8i=mx;8^l>L4-p%5vY_F+cHC^gYMsLs+KdY1Cz#iZAa{vo6ICQW>g= z4IEf_wcTyamAvr}3)Rm676o5$6xcxIAoY;dkMj@n%x!H+gHiAA73gHjaLA9Q9-sTL z05VVGloG)k{?rTH1K#&tT|7triSqZk>v;Hg)xqIBqcqEP^6J+@kv5%z#<4Cn=$j1I z8!fU7A+>K}=gl#jy7;f=G?3vS_4j9r`H5gTYQ(ov{;|L@ z1Vx=I)SiBTixqynBN%XuPmMVIa?14fYFs-?;@j@y?H=bnFzTb?`^rJd+~CvsR9CS~ zo7L@PL`#;BigKUs4C~gT;vTocbeO1r8=}u-PEP=Xg$wXn1=8AjY(GttG1-K&M{c4! zeG0v3=^eX${h6YaAGmWp4!gZe#t(l#j-Zj!q#p5v{yV0116yyJqBSn`8W!K#iR~B| z#c~y}VYrcyDkP6Gnh4a?&zl4r%zQwo(^O0>i;MsqyWVRS|BWRLSG_=Hu?~QYn%-NC zgj$$+(a`)W{kF!7Y_iXty?>`Z3%h;%`AVg)yIJvN7W2N@=atl#p<5L{FVPDiH4C0CJ22Sd*_=R0HTb4z&QrRr);$BzkGQ5|K7HIVtJ z+rP3l<)|C(NwKf{gI)jbA$j_pX;J$X=0}n=C;w)fH(WO*j_9PpY_^~5*>Lr4ceU!? zzBF>}ACH~&UvMd9lm3~P|18fa($aY2hZ2sZSXXY{!P1+1rnM;49b~DUh;7uS>ZOUy z=Y>STi0Wt7!Gy|yUYdtW-Zxn!t-O|ShjLvbr7N;eN1@IQ zNXN6&iw16E(60XRyo;Xf7u}zn`~2qGigM!>R)bSi3{su06O8YNkWywvUN_9&zqp?G ztnKT!-~XK=E!`{l0=1BS^1pt@o~hL;(YT>BnUP)*l;?-D%iM_EJwWWgSb5<^jbjn? zca=#*e{3R`dcV#wv9{%O&RFNGrre-~BVX1P?4EnNmd*Fd!Y)H*yo|I?uLjVCnUg|y z?PGI}FuvEYOuWAX-M{BxjQ2n1NFcaTyl35 z=e)aCo0NLjP;Ku$^(dIapPvkC4gV-N^wcQ_XVkvx9-h6W!@`u_j?CcaYo8D$Tl?s= z{s{0idlq@a*ZhyjFZQq4va^G4)>}S%WxDe$YLDg$^L@0BU3J%dS$+40KmOWON`Pem zWevSQ(q+a?+1M}ryl?m%W=tui{8La=7Un~(*01UJkKZhGXM`-~${MLL(u6eS;7SeG zypWo=u9tJ?ei+uLC6y1%Mwfhryo}v#rF7o+^6OD$|CY5DC4XvXYfW?#f7RKeMMma) zz@C?!LOn_8R%3qp@)C{aVLQXcI8!xfW&3pb5R~&{;<%cYY~V5vDcCiOet^7My+usMjc3o7xb|DGl=WS+&gSz<48}^~`^AY4vg=#lxkoD%njwxWP5QZ$wARGgOBUl3YZYd*g3`vk?iZyB1{p6t z6fQm_b4pB-=`r%2!RoGi{-+%zl0u$k8wi8Su;pSKoaxFLn{{!X*9JY&1}l{KmTIWbe5 zF-KLCPHCyJndZ|B3eBdIb#s=D* z95L9axY=nAK1#GrFZ45uMm46n>96+kJDH;LjE+3(VNzZM{-Fu0llr{}0KSnpb<{`G z2X7Zp-92hvmom*(U2Tw|*fvJ3rTd*;=&dv{(?uebM)6ZdAn&;-RDlCcUuu#3;U1lM zsl!KeJRa8JYk*cg=z4I2Vc#ob+i+3ipyuNSXor^{!cxK$DYTICNOeGBhy!1kVPDNZ zJEm8(!4Ck~Tu_;gl3Du1^uU{_eCjwZi%hJEBju~w`?|Zd&(vgX^g=qq#*Z$)m5W?Q z%l6GT#%#!PapIr?onH-j;*38ISzhs!cf>3+jOMYlfL7dDJ0Ud&V<*?4_5!jq32%g- zlvM}Gij*IR`{VwotXoR-atta|POS}wYpsG*K0XC$s)7RZ8Q0Ie6dwFAy_NqX1u4~0 z!9PhN=&yr7dB15d>~aD0S4UKQfuP6CA;jp`@+&r0r*mF;P{K{5y3E-k01wCuGFS078K(9{YCFoqm zLY)0LfbD&6S^GjU%&v>m?YBt1P&&AB>J#zM=l+SY(rN2{`l^}oXqeWC}bS5 zS*q}k8hQ%(nUS;AV+jxgv=!&LqY(GkyqA7WC*RXd6j8+E^19J*4Oozd%Z{s+`=+FP|@LJc#1+{KhPg0!(rE-8F9XF~%bQU-$n-t56PS$BJLl5?ZY7kE_*o zGRa^qEyA17Yk{0<9<*XP8WIG`QkUof$9+_ zwra3bdRo@7-o((Nf90bwOJ8RAqYS&lvCShR0#()98ZxJ19E;U6>99ScKdoK{u~jph z@)nlcb4&TI8j@u?exVZl@6y(|b%l@HaQT%D(xA7i#65B$$$e){`$JxJ!ro2C~PpNv`47E=&nexNUq#|FlaUJD^pvA5@7R_JuD{th0oUgmDAS8nEuYf^#|f+B@Vx=YH)C}pzn z09x{1F)0dzqfEDn6w+X)NbZ?XjyVPqWf7A(rx3H)APb3V*5z23rRqO!Qm|MtIqC6? z^bf2O*x0jzSd1r~Y`fBx)VwFEhsd$M<(^5^wQzDpE(xZbr%Qle} zzcnb)?#t9R$Swk4L-&(GG105uJuV{T8->~EuI4f*%v$eQnjqocGNU6p7ANX598VLa z>E?iDCs!;aiH*c{;zo+BzN?uoP=V(Tw`fE?{s+x%kPUW~1x>olzV93an=edTQjPv@ z6Ir`7U4zExhYE-bhCSNry{Labm2&>lF5>a-^Rr{tGYB|e{O=rhx#tXmWZWB>`bmF4 zfH-TYcEqZ)CKW|zph`=Rk9Q6%7rdPl$WN~*Obg^UkAQA~QIRa{1oGcrq*gRRDVwa6 z6{Y<+%4@ZeEhcEynreQBgx=$at(JZ`^sc=3i7K~oye#HpM29GWMQ0P#s_*BgB|x&s zuI|3yLa%>9RD_gzI2bR%hvvfqbSTjP+G;O>Mp6e>sL!&v0c1x(xFYPYlwGi_t_kZSHtZZ>c1}^D6 zi(*6rRe?^CX9O5c#<%nctTF5I2JT0!3m5by@zI`o)`)9e5g_wO!SdtiMV?4LH2~=% zRK@)(^6zuVK#!Hd91>Uoqj*&-5KzZud zFm6LzZZ4B2+Zu6`eYgo0PP*RNBt&Q9;ekt99~!miSnOWlzf(P~vjkiZOI!XUF@SP+ z4~yf_F-c#YEFP2A(PNxgZol9~v^eahRoMVVvrXvJ0@P}1L~kC2MMrJn%)Yu{4E{6y z;%g>=7mr%++rhMcA)@oSJp9EKwMv#ckMciur+Gef0 zjl~<+f(W&`&R8mPP7DiO>JFvIug5eSu0J8IJAW`R$ecXt;%ZeDggou15Ol+3(^UaW zQBaMN@oeteG)k(+FLa1oy8tMpJ1PfC+~Fwv&nxy?Q*s4@`kEo*A~teqFu=AcNI~+h zq~wL^6S_+hqDa|g9i)#kI(9Ecrk~oFt!v@ZmmG=*Ul5sYF#ceqqxQTva%po(PD=bW zxobJP@wp(?{k}pAwY^XjW!9W2Cd-A9o2u=B(kDgrBxJp#!y#+s)G9IK zphd=aE45L^vo}TxnF=ugu=ySiaWixyGa41oR$e@kojfVOe8hTjdGFRg3Kh}yOn2Hv zA4=&qb1n)uh=*ec8XqV)JLAHs$9BEK(=$=3?hKVTAJJPKRiVb=ZM#fv+jDFu43tr7 zu`2#4X7{*={k)^3$eGTD`cf+2=RhB|J>*VK`9=t^sNsIiGvE1*2@Q0M3B zZv^e3s6)(<$Qj!MNrbz%6sK}_&SP2X<3gR!ER;M2w*ydtQXXzpap3P&BZ2CvK;V%< zrApQh918T&t+X#X$~ND8c=ON@`0BFm$@%w$97B^K*VjKcP5m*bHvlAbNgTSAgP@{7 ztf;I}A`z_~th$NrY_@mkir!kWFq-SS~1iA~N|lxyuqvD5WOOy=AnYOx_cn$c~no z41b8fZ$_rR76bI!(CW1@8TOTH-c}BVFR}p7{Pk})yl-)g_s5dIqwS7lUKeG%`=6;u z9T@VEC!a!cnTJTrQ6I*w#&yl+8wP5ukh$YZ$rLPh;?5$g?Yb0A1#0vXv@3v!Q~}~% zsd`2+(;zvoC`xAx?={u0N+$U3TfzwBxJ^!6PhQtwS0#5l-jzlYa_)c6t1i%})>l2r zgfP-|j-ea%k~y6lrxv)(BLr;ahX42P0=p-Fe3A28KlP`ziY_0hSdUi>V`&Hyuuk1x zHNwIUAba;$hqM)Jd=#vvrTgy*Jj+O3>>o~b7%x8O=FjJ*$ZL)_gdS&&x!+ZFe>Jf0 z)xa@JH{>G~2g>ibaT> z$JpG0+67J9)SK*D5!n^7h0eOEY+RLJ8S%p{gdN399k$B@M!g-LxEX!4I$kE$PW{21 zx)l#uZ79qMmOS+cJPA%#wJ|V^Hj`{AFGGpHj?0b@9^a#zS>tF8!d_xFHoX7-i9X|@ zZy&O$pO1}rqW8onm!LK7cw=Fu>AFX5CH`$jLESt<9>G$K%|srmJGoq_tKDW(qePw; z9AS>DxJ)KH0|?1wYE(W`4dijs z2#?$^jngm9r!)?P7aKUA$uW3n2feO{xGonD)avKj|Lo~Q{p@~w>KY}(9~0=?BDcMc z)z$mFZw-Fk%^>K(#&(Mkw@q&S+#u~0RqsBForn$c`RFkarPEB=_fuRTS@rVW#pDwM zLTAg%TDhcGTCd9gYqQpN4pC<>-S{C^qZ5WLB+L6YRC9!CVa3?lcWRph_atImb(Hof z@aHx`8^45ZGdN=t7ZF__9_2Na*7^L#)zc%CJLzZ64*}Jyu|rqPFx?k0)Fq`Byy7nw z>N6QPc=tSn)d_nlQOV>3+JmOyy-L#u{*;@hVK$wvJUFyHpXp)p)bswX?ZKEm%Zyq! zaf3PUk8kMq5QI}8k?+s$xoTETjh$DB(*Kf5%uHOpwmiPx>A%umkuNisCgU%LEW=oA)G8==CEQ#)UTIGwy0zM1wL!VdKyLiyky=1Yanqhrs!Qkq@$)5T!{%dGyjPlZ zs3ptGkzfP;ahu#GJzYIBCiGGQ{B#6Th123_@?E6+{Yl7-PY+Pj6BC$8H7adteR`&HJjuozJxM>+Q7{a$CLt>Q+sUyE5lm$k1jc7Z#JF2=6vn?-*?Y_`dN~1esEB5`p4$w&=a)-2@T*czvFtI6%j2z z59mHG!S=skzgSS|{{HXHqlbsGiVRCqS081|937WSj(@ao;{4y+EyH)d{4-ttD(6Gj zg^;AB+{j7E30chXw~vl4{F3nJ+Vhg3s$t((?%Z+p z)PDF2#4xwL!h`x--q!!&g-;PsU|J#FeO$Sa z;3W{!r)J4rGk$r)^}ejDL|Kba8T>UJIBU7(pc$fRpF zyHoEI%4t73DDhe3`&f(avN)Bn9!7y)$&`?fir#9>!?OYGaZT1_xm$H0qL3q(V%9g_ z6S_=)YnYil+pbkUkd*J{AQt?CcEC|h-@=Kvy4OD4$Ny2cpENM7x)?Fg zG91bv{=@dRs65ytQqP%+`hq)>AQfNzY#Wbbo&WPSnm$p3AIFE!mzAOALc)z3;^$?{ zDM#STeDh}soDz>0Wv)o47g;wBIKEFi|JeS)YPzfTioUCS@wlJSWmC(Kk``AddYm4{ zzK4FpI_*^A4U9ck?Qv!3OGs1BpaQa~BjDdR8 zn`ur>H1p-!i(|*XLND&0$-g z6r+mlUrayFet${EwZuH#)#y~_;_<)!%xD|%LDCiluu;DP+#}@I`mozudj;YtkY$P zWT39nGa}7kc0{3g=^SBEtRn4rl75c!R3GyU|4HZK?IN9OZ8nVer4Nc!mg$DhFZqegK@hHo!LgO6DqNM8=7cup+XC3S z@k~yAQd|M2yU~Xk?Mbau#=q)4;>~mGqIGeHlayG~vm!HN1CKu^6;`vWuO-Fh zA8|}PU&NznhTUNwsy-UUN)T!Eli*hBPods-+30yxhixnn`Fr!3sB3`RF4Ki5htrho zDJB#*e%SB+7ELp+ZUX_c@+3Z3^Q0lnL;AY{qrxYsl6N5Z~L z|K#9i@Tik%dG5E%iqi2uq<5kb=iaGjPfsu764qrdITh6(p4c5dwz)L3x#I5UT37cM z_2K7urW@!C26ykW=fNYc-QJ}@u0e>@()(ftBi*|8`#s~M|2IWP!z!3jeDmoJQ52n2U$Fel>eUzEDrCLtG7se&rLj* z1+EAJ8q;`=z6w-0db5V}VAJuWC6p&jhIuh<(PQIp?X7-Q>05DU@Ul=!-y$}e(T3x( z3OBJB+WD1SqidqJb24CPS7GF(jqjEb(~0V{GHss)dApC-_zq=ZlQN(UNh^<38EzmN z&X+Sqkpuh<9|}4& zw&#f{KbqiS15B=8jU0*>L;UArk}i@f@rN#Rk99PSNBVL#Ch>6hs2jmasxvrIuh`AN zA#%~`h%1 zCj^gyYD3wm97n!~7Dz*)dd%pIh-1@wwdg<4W`n1a)!+f@zGGQK`i_Z(d-BbU=2!u{ zV}n)q=e)qBRQP*MmZ2N6O!WWOgFEELF;;Tmv}r_sMA>bfgMlN5FMu1EbGR=ffW3Cn zQojS2O%6NMEVLVrLw7v<=+NsC-?jf?=t{1S$Kw-2_DBBPf8`W7`rd}$Jl*{d(+59gTq zJdWnicZ4JiSTeOY=0C$7*acW4cweKaiRdVWSazCivT)9K?tM`ZHt2NsbNofTz}a^v zqFMbamQDEmmmthHNgZ^iZRH`ibL(?zQhY|Y!GWU>$%7_^D(YI!f0*Y-cd*rR>9FpD z38T(h!68xMZIAD!z&Agfnx1`AQF)Vx^}e7tgi9R~6`|p}@{JRTrQ+w+idl1y1_pE% zHEbNq04Ky=tn2XAxh5pr7#fD0JDnk@E=*NVt2H*+i`D58YYALBDIq7;_8wl|%X$;G z*@9GC85YV3E)qdY{hyejn&X@+Go_sX%1w&e^PAC*|JPU$xlbW$Mv3F3krp_EaRDF>_o zpm3sp(#(D()fp$ps@$+Ru;5EVz$su@<^rNmg4!+tA5~x!bp01QR5JwVS36Xv6___d zbUYS9WuU*&)%4f4zgsjUWvhHyic4fO4R>y6Vd3QUC0%O2$LBJEV1?qJ3i4ngE6sL3 zVX6En#KeBp!Mtax>Sp@?ZNTyK_$X3cVPzIKWoCEl2~9973=fm{vEwbuJsl6JTz5)@ zekDfdC_KXzAbSS1T!=O(SA9%TnZv5ySlA(QKza)GuJjj{HSLmoJqZ8Fl)qj(szqGXJ^>!SGREvR83!f6ew5=a77kP0Uy=%no)`bh~tV$R`Y$Fw;Z zJ=5cIZ4~I2(bp%(Ta^7QSky$KW<4HtP67!MstI(Vog_FHivs9)*NBlEQMTa+#4(a@ zST51t9m5e3mfo+d`w+&VhH=3bak2t9jV0l>ots+<7R>8f=7cXOV^a9C}AUk2jAu-aGrCus>;4r{+ z4duE&{h%0h$RB=cCT(oZW;p9KblvvgW`~G1|Ls|cf0mz(&yo`z7IxA}xMD_X?l;t@3c&D0%ndxmhOiGtDx535N0G zm0Q}t#ASFLu zqVhLg?fZh}B-JNZFXxt`{@HfSgCNiu>=vnzY~f^hAlkjNKX!^~AObcG$Pe!!z%;Mu z%+U*ELUO+(u3^2YE=JI}J^QzX$DRni_afCP&7ydAVHIJ^v~cIOm_p(+J-=$*Nl9!7 zbq7ys@OCq_Jp!yGLgN|GaWM)WS+pl&=UsKr({gD24_zO4(a9zeyNUXz^dc*uw5P9R zda9(Kp)x>+X8_EoPqLTOb|viG68lYePMq~h4o{ha|2c&2X0|Noy&@-gs#AuDmq$lj$Ah+#U?n1G z-`|$$ctjSfrdI>ezJv|Tw%Tg$K%Wi)uG`%5Adtin0~&#-ZVMLv%rOx}Iq(-o%Ci9w z?O2 zZ4lI8$o7;Au#lxl=%AwwxfOO{yteTAQr{O671RXuzYb+ z*P7Kcnos%yf-6cmA&Y3bNeGXEegSR!&CBr1&*=Q=KuG4EVsG=-Gmx@SsU1#dr|T)( z`@DWRdA2xZk$N(t2o5W-{rd3Z3*r3|%uALlT@Eh5E2T&ETVK^z7$&d9=q^(Jj}sTa zvsCTgqHf_$FN#nq0~eosEqNkTSs@|K7T}8%m7V8BJFN68)Lmam&=r(pDGnf|U)fC` zNST6OWnl)es!GoGP6S=4=oR}-*RZ^Xp1 zDq8MPF)^8*+%nH;5&EqNZcl;)EP!oEkS+>x;}qUR0`TnoroSQvSCESOBL3JkWE}j} z`r)5jJQJ=0kbze8 z8)DQw=0T(>-Tv*i0SWTUQ6FOs6jlQTlo`5AwT~~N&dnif@z9_Ju+IV{h=CX;Ay7{` zI4tBvk=^oQco8dZrToN$g+1)|C!E#s8eW>ty;nSv_OuGaZhrK=4C#F^(Fxb4R5OmF zq7bGMaHj;G&V-XlY1u6G+!;cjse_MKMO2c*d@m{RsdY;d>}hjqcx7-HsmgydA)r>%*>L=cersS||^ePsw}h*$_fs z?aA@Ty2IbY>U%-PLPEW--S#&ioeG%RliCpv*kuvDc z0{tf>JcWDqWD+bL0MA;0r7t{uHDCYN4B`BNYeTfjzp{PGqcKE>Te~GHZ$-_EfLkkg z*VhcyWeU1L4t5l)d|~FFFu)8@G+UTQS$`YdVi-Ng>xmZO=1kx|LAtKwDuw#KTFj? z?~K1hG>o{b=<>9pf!dYYyeAagTRAF1GU$Va$ef_7(P+`t;;q#!v-BdpJKWi16^7B% zB-^Jg@3x%1e+l*SB72wFk+-Nm7AjA1m)&Nzc1m;phS|>KjIJsd*_1XlUq?uSCG~=@ zO3)K5$}6l2DE?fp664^ZTozFp(-^fVbRTO79HhBb`??J)Ld`ATIqU4o|mT zl3C_~2%sA$*_;P+OSvKP%7+sv86*Vzp=OeE#6Dp-nO@bmvnqQPnbCW46s{bS>*}6K zi|K(_DhL((oQ8)rk1CwS^L-+%j8DBs*KayrxY2s{l1`xYL;s>)8CACm?rb=6W2B+p z$Yt{Bu(L_D`lcuKjSAyUnV1%$>vwVN3X>1}@7V8o)JZ%v5&*3tseNOp{an~kJ@~K6 ziznranVKfe-cNzr>_uJ3QRG&P4tr61OYpf%L+Awgl&5fDlBIg%t*%z~J=fre_IEzC zy?Ao5Zzj*gwE!4rc)X>7tu^~CT;0*6d8GW>#>Zy3NgwuU(4zM}`O#^i%7pyS69DMl zYq;$s80PymvhjEy}K34ym_cPc>Ezzks4@@E{ z|A4IuCCDQpP#%jOhlk`+E`z5wACV(I*dqMZz~7QAoq9qo8I~lWUoh6sWTcZE63RI} zzw_=5QLjzYF`H62+O%TnDt9_m^_@)r-M4Jr?Pn@&T_-mt{^vh&k2I-6oVrz|)lJA+ zKsnq#xIT~G4R!_fp(^F5ay)dK0W-B1|J)Rl#6Z zvjjITyG+xUyX8>8RO+z6D8a(v+j2>)b2^3#t+xnmCZ)+Ob`RG?y!9_Nat{Z)cf}3y zO~M+T^=;#5hd(ywnQ!iz3$2i~2lc)~X|>Zw3M2mb@!@fM1g*r_@1$0x_2t&Umz<9( zdM?6uXa0-ypN#JbD2NN|Xz4U@O*h#Wyz6;eksDunrO6qYi6iX{`c&Rizw^uPyQhDg zq(u`$Y{Nxf&wpgkrX@Ai2Oj)qY5Zwa|M|9&;@S-rqdPIXH-1wk)Q0|(hB`BEBhK}D zoCK`&wFYdv;DOYb`A=ctnc$K0!Fc%}?N85qGOmSom#>)?*!bJ-4s>)&E42~!6_2fV6Ed_W%33bD-*YR~8t=ZoP3UG@e(zNo-kk-x6yyUBtU{VV zeB)v*szU4{TGUOJSwqEEb4+E=l2tDg<9cmDj8dtVP-#xRa#M`^MsgDpP%mym+SW0W zp{}Y@bD{pHm?Ws$4rU^htQu=t?&>9g`GszjT0B!DpSZQD1!eXr zrlrv8DT!O^^Aur@^;{4v30)^y+)~$#x`_wL* z^Z^z4VySatFg6rWFr4RdO_L^|FUu3rA(DwCAr%Eo)OMJ`HkSt3Exs*}xL$9j;?5L+ zG0v3$h>%=IH^mT^`b{yEI+hfs*Gn!t=gz}&)wEIiJ|!-V32(zx+WHg^T^5vD>drLw zn5`hKu%L5K)(K*3{t~D|nNqiPGvB%KSoR>J>AcfiTGxfA4+xbEGga4(lA#h8NG7yC zYi_e;Vzzu>@8W*kj{Eh8CxZxu#N9bkZ1U%U`;A9pPJOCp7G@G0MtF|;n|B@+C z{_3jW0by6JO61`j34&_1h6V>4%LddAyL=grG|?-tu+?^xTbuc|6uQVUU)EEqx_ zMHm6lfWQ?c-Mk=bocrj`xEPbtW@OycoVeGdtiK~qd&_RUH;7|v?xuw)lgKg*nm=y*q*rr@ts_!9YB&3=SC1X(2z*k!}5w6=g zj&a8==9wdttU#3K{RqN%?cq@udl2MZSUh$9#9|4E@ zuY{;Jh$^r#P)~>q1NZXPHjNkwgCqroh{f~)En+H53%|+&QU@y%NG72Y%TFOl&+rUp9Pq zgP1Y_JK3@4eIE=#)Gi^d*n-InJy zs#v6s&pYFjT&jGzN1}!V;Tqe=%JuUu6ol+b?*F^sW_ma-<`>_wrYw?M$2*c%FpAKU z2?d;CX3g?{)66FAm7pDJTcJD1GC`!7B?xt0CrEE!vYxkRRDmg zj+nL61_1cE002o&vW_q)p!WcYyDSa|k((J}kgr18)!l-a>j>tl63Y zLbhV>TXXgn1UZLbvV>qz*aw)!&2)iY-1Z8uUUU2CMb@Dny0$J*7MP-^Z}HI4_qQdeAWy7+v8GRZik8M;36hX;A7~ej^5|t zlsI)DKseQ;npxbUU5KDzZn}e&TfZu=8}56Rn;^IJCKlSR4Xq^(ie2%6 z(aSgSp0(z0Zx=4W;3+lnEgGJTqP9I%?ioudZTJ0M_u$@dA0v!hAhyIu*yqnoOfI>R zF2NysBsD07Vf-5)@lwTtLX=?q*^L}O1fB&@;-sKgs(*W(1OTA`k@4a4{)$#MI_)<{ zyRiSSg#S#6a7&BELkkeEfUh-eS~{B7atgQmetjMdAn9_+lSIG^@B0hTGi6NPczk!G zzw?_Lef^L;GZu5AJT2&~ulNbbu>B7+LYxEEIEIs1=DSDjM1iVfyap^&ZU+A>4t<$G zHJF&;+i2_VY8`8?USL?v-KG{FYo}{}@g2bgzs%QIs;Irvf4T30G<0u2N5gPM;FhHv zOP{4&8Yb~m6MB%BJT`U|(wPPB@%NW5T?C_4KfFl>2L2Ty5UTu~Q5M$4BQn@AQpLZA-12Zv%>ZY zxYhq2g~vw?)vEafc(gWQc)uPQj0Zw>%iZ>QJj0__1O2Sl7QPAZ z?DZW&WaJ1kByJ5StlLnHw-?z)N~%s?zq7f3r_qO5k`&4cxUM)=%etY1fj7;gXpLD7 z7YiF&^sY>7I9N?~bnML9d<)xCQ4TvPhJb4F0SLC2$}R8;@QeZkSb)OtfRhid9Iwx3 za#=1s)^!y85T&SR43PPo(`T~#27^te}h=6Q|7$v+7JZ%LD1OO3l!7gba5*?(&6fK;( zPD-y(jWnH&)S}LwTVJ`hj!-9ynq4!2hII7+23sqLYe6rw`HD3}m|4YvpYP&X*HsZ> zfrj1X9_iQJky;XuN($cIutwvv$#nu7e|tYSCBEFbR#Rfoq%1LOv#ziKmTwrxL9Dx* zU1$IBOuvqru4ZSJ69il{b>55Z+$6CW9>EPE*=lP-4Epc5D|pBCA>L!0VBjSm8p(v! z9ZUtfXL2=Jc!HQ~aiU7C2W(3LMgdxJe~`41cUKL(7b~v+9n5v$TD)%|OftN02vp)D ztsbT8W;c2NRiwN6*n#oJd(V~XeIIn)0xguyBynHt6seX??cQ%h-Q1hU>!>5YgDgWfbVaMw8dG*G9l7nsq-k%5 z+3$8jWmrSp&HF&BG_EY|Mnenj3L0Hd%|_VHqe1kconi>HKEJCj-`y)e^KITqPdJPP z=*)P8&3SY^7=AEKkU`-e7~>~NxcD?6j?Bp&%fFKWJ)#H8dtm&tqhT9j;DftEAf3M! z1C3(l^=z&$E$TnER$1Yb7mt<4>xHB)4DK}K(iojy8u)-fuKNNwb)B7BYfkFtSSUP4 z{;)SZVei;vcR8fue7s1L#dc-`^JgQmvKy=X2i};m6G{a%G|n z^yY?}N4i`;71t_Z<0EeYr$~>kXN1rFvN{3=z}BzKrS<9l5f(+Zvm)7e(RZxcvcc(Ab4dPdBh9H=McRWfL0es zj|?0bGI<~OhxH}X;k9NYEkJFScN041-GogZ#6uk8Np+h3=}%nNdH;IwQsN_ez-C|S zD*COj`5`tm`c+JH_3R)g?bYWXhUF%Z^4r=my#0w9@Hx$k-%NE8Ji9oKGh>WKXXC(8 zMp!WU0$3yBanQ6jA&q0B(9q5C_vq5nzFO(efAvOdNE{<)IXpTdw4rNJ7+?zd z`qxjQZ;lDl5Id>@($$LGSzc#qjWmWlu{d@2&Lf(mf_`dV}7h0%`7j_ga z2zJP1Wpw1;8lHMPq~)vgm592mzQs%G&n7di)KgSmqhZINAn?n$M0tUEWuITAbYi1l z&{@mhBjLx>^X(VFZ4=O1Jk8ps;kMu*@7x6lRTu_nTm&aTE@L3&SPJ0&ku%8y=1&=$LU zx4+5MxBd559Q7qubqZv%&K~`TeIZtD79^b2(4;w-zDg>$%VcY=N943}tpN}M3+N&> z@yL0h6~`s7EN^j#s7WHsLF#VA1bBIcN~AsZ7ep;)9O2-vOSIcKWn;JfHr}8w@<*M9 zwWF!^ZLIhRTebiG=a04|^@>+n*RVa2;$1uoWRul-Qzr)vLYzhbpj+uYZ=#52G|jcs zs+=j6Q!YQp%6REDJp^n$RszsStaS)NV1j}heo<|@(u$}91vW3coxFMXrp1%Gy zL6l(AqJE{>M;`Sv!O_jQsFgj*Ks-sfN;npqyo)uXb#$Vw!S;WbCIF>X)^k`_Avj9N z7j#3sm`>L1Y#lNu@dvv$?*XocjsD3tZQqZ6wUAN*vIDf8T!*D2@|v1`FB4GlD-TKo zgZn=i7_EaAYZl4;w?_sR$2SlK7`~BzANqHq$>;_m1(2i_B~9^sqcgaZ2SO%NkmN&2 zEy^*geh7aZy1$4x^!$z8&4az0yQBzcnx8=v$jW&~vg;$mrqF}ZR_7`PKW2q$55V_ag|k~s-tkpr?05m3J=s_t=$X^cba@0uOFXhAN7iaO}aA5xZ+dNv_x&H-n2_|AkftPI!*FiW? z^9S#Gs(Mm7GkLgbBJaKndEVYZQ)P{X%EasC*t>}|)H5~oM0+VpWK4oLI4@M~!Rx;p z^#~eWEAMit#e3vvcn^#sO%v8OWMxLNVD&pf^KuPSw*bomHJFu|cY&5u48#=4Q(Z7l zj%OdRtI$G48n=Mm*B?LnH%E}w#8g<0h0z~cG2R3K?po&yKj=yz!`1iq&SDD1FM8}=9iTJqc z9BrOSU3~Pr*Nd8$UDvA_gbtU z@aR{M9`d)ob$)cbb((fqcY^DIlYije3qtS^4TldSt|E<3GGO)ypvtTjTEWj;`Fzuk zqc3u@Sm0#p!9xUj(b$WSTXTo$BkC)l+JE`yxmL!AceFsoa2hYEpLd1O6%sR0rJ3Dx zCvG$jJNBkC;Fqpq*LO!|rH^=1EA%{V0Svq?UVZ}FbS@(Ea(UbUMRbYnb!0(F6eo( z$BpET%5QJV)W*9>LGz9dn!jV%>!#%ZzA69l&mbMZrZKuHY3kmH zWvU!RAhRv7ywedJvIuM|;A&nve`cUs<3wwzd3GHi^8VI`YVLMVPRpXLRw(gIm!_U* ze+A~!%!rG3_@|j&2BmFw)%XWgOYjc{UFr(I7Ww6M)ccIIU)39Z%6bA$+&c6K{a0ZJ z;|~~O69}pkqj?^($f z6#Tq8=Ug+rce=s-V4Rq->G;~}hi{g0k#a=cHlvp~nYOax5Um?FcfH;{U`=Z?dLt$% z{+ie6kN=Eo?!2CKzZ9ynWA*a3Ypj=@L6vpR&CUhQ#milP>So_-l@=E`y^rX)8Ltdw zu9Myj7wT5~MF)hn(;F$;-hVymc@`!*5YeKu{;jw6)c*3z$eg{sY0qX(n@Edm-S$3v zgTwGY?aY{cRYj#^O?e>I6P;*xM^aD;@yq%T?AQAS-w(h3e0MFW!}xKGu##G+ef5=f zNcWDeH&G#Tw{n(tZr;8l0Gg|vH@JBhw1aLE)o%ABYiazvSq(rosvSJ64mZeTaF9j^ z+ou9et`-A9=GBv#i8%es-F$;=`JxOJV$wS1S8|9?t%jLntPjm z$&PMcgKQB=fU}~MLs41sL?6xE7%PZMummsAu_k6h(d4uUh9L`(<#+W^XH%YXg0n0S zJAiV?#~qn$A1&s_%aFyAD`le^dm&kPG?V<7zk&Q(x4%J&>m2_Evvm<^OZpM5mA?5u z(Cn@3=$3~;QaP>Y_G5rkjTvW*+SL~_Gv3rYb2IUM@8h_QQz8TT-1w*wXQp|Q+%>r1 ztD;7zl%sz|U`^dUU+ zjSeF@ymLOrkj~O39;aWu^Yd||2vmUWD6KePSt)7U>RPWgX((mRfh4h}9b z8FlISO1LrU3spj-D}&1=2-J6p+7T2}-8b_WX_}^CuP2LF8{|ob9Dn>yM5-a?AOGp~ z%}_jPJvpOWPx!boqxrzPg2I`=b9uB*!;OO);~a?2 zm|12Q~-dvX(p$D2sSUe!A z{6puiNQ8x=tE}Djr7PHb+Q$Mw8*!Zqs+&(1r5R5=*wkN!Mj7n1-RHN7S)pH zOBRk(I46H$T{Y`qnYY=bg?#kp>9r#V5AA}YmS8%ktF~t>&!G2taUV9Gw{R|Pi4D#w zBmYM@lo`!_G`XZ>Xejb)uSE@<~kAtGLVcLSgehGSQNLmS61nRa1=How3o7y3`7zB_H0kq1Gg8> zGzAi+ltXD8<9qo9WTFW*jmOphB<6i$mr;~!WtN67KJe52ZN{L-=ubRb4@faz;nO17 zxcS7^=k^Vp;CKD4f2PUZT~{Tdz}NimBd~zXv5JPuFLyx}q{yEtmB*PQ_LEIwLF_7C zvtriA@Yn?KjGO z?UQ&gGzPC`^#g8_M&U*cn(3P;Xscuq07_x${#Jm12&Vg)g=~Ge@+^hCA?5-g0Cho* z3n&N9f&v)F-Y-!8zaI06(EVeq3UQ3^t6LP5& zdwzo|0 z67?pRx~%J^g^gTkf8NCZb~*nVaS>1DZ)#7rKe2nCT`@&>UF(wocj-K<<0aP=!bW!v zc}{}WeoNa5<_@oqdgVxglQZsRB(gSTdOTHUsJ1`|dDdbc z(qbe1fz`<=5UBUXYeVAkodDdT{LrFABQ2;ps;E;!#plwrhgEQ?<%#YO_IF9LBB}&A zluDi;wY%V64stz3ibt^Z6l`oeRa=76`pVY3!ZzzQL=I34i7z`5(yVmgbo6{n%~Opu zpi1ad4T;*cQ3O78BPg|&dKJJ~JiQGkX61W5ut*H47LOy-ADkU~Iq#zJd?Dd^zih|g zc|$EBm#{nU)0hFEm}tBE?H#s(7>KK+>WkR=4OELjpr&}*C%0y6hVgq0?>)6x(^ra# z^0mL@Lyq!M9Oc4eR*of{Nma%KT`SrHZ!c3JlC`mw=vCwzT01iKA&mzJMS0vZ`q!$ zn=ftmNKFw*tU}c+Su$e$J0b-W!vZ}Pjac+^Y$X>X#E%uH`3H&5F;{#X=wLU6ad_HL z=&D3-LjXQ4<`|QxTE1*kJ@7JrFrdV%p$ zqzQxd#{Hc^0 z(vTB)yF_<-b8}NCMGyu2PZYSn$x2E! z0OR_-h6a>z6Q%8#f`v%g#SjM@A1iFudHVMS^YyUE!Q69iPDgtDRKay78)io?jF@e{BP}H+=nqTvL)qRIQrn}G zTQ7eY)`L^MiigQk6(T2W3hdE36cp;?gBo&Lp}DTtdd$B{L+HideEe5!N(sAlTw)yL z|6Eub@_KU5*$;XlCm;3~r+hoHG&`9R_IZ2w+){Yb_HCoQr^7N;Uv4~6)wQL`DkV+K zeHt(K3T{yjeULdq-v>yo2~$#nb{gAYR$@|#Qn3L_E=kYlZsRUcio_JWn3D6MO(%u= zL(8bPld3CapGC2=rY1_#q?{fKZmg(q<0-0dT#8QtTK=Zst|dJ_T8)Y4AZFNR{U{>@ zTOgKd*wFZC)v7{tExV9bw}tiTMpwJ-QSl*0|r^QCD75zk5<;NxEfn z$i1C=;Z=HL7Q{`?aV~yhh5cBo9vei{cW}7k@=3qdYC4qXkCU?_gNZ(S5Lf>sc3xe!-^w0|ik-N25_Ij@$p7XrF}!!zOH;)b;%c^j zwNmz-*% zBUP(3$t{*DyCAwJfn8BgQ4JELL~;ViF`Zk+b+UA#)KtdU7zF6Ct<-Rwif=D7>&ud|VG-{G9i{)*-hoGZB#2Ka4To5_=u=Gd#Z3UJL@STba8OF9wJv zf1$wcpAP?f^5efhL)*iLTylm~LuG;2(Ld_mF|sdheE#k}z&YWit95ZC`s4Pt%Sy2K zQq1m)AWMZu=JN;r5MkdCp;+R+5PpgT-lEjD+qr}#>KEL7bu<-AqRblIW4(pn9Ht00cyEmz%bVl@Wc&Z0KiME0s);; z97<~lZ}V_h*EPfVW4Kw$1)Bnuk!@snv61h&Jqa|s+5C~YubR6#dNq>xuT-7MF+*|! zPBf*FCZ9NO@aStHuC>`{Xm#?kiMcc9P=HyRGh3;crk$Xfho=390IARmi{lYi&Kzd_ zo4lH4<>#9VBX8Nh!L^PX)0!I*oa&mYKWV#RwvN!}J4V8uB4Y~vd^Nwvg_e~`GaI~# zNpaQfSiy%%^~fLtB2^o4NL$QS?`C1f*5>r9I%jU)!R{z|BZMRfHxQp56p#-j1Fm}``_#041G`f=z2&piY=x?o@`@(W zpuoQu*bb|74sDs?dc^zuC-wcdTl16p9HHMlppv(?IkGY1%edBd2iso(xo;IE4+koJ z*6qwM+wJ+~?;iZ@;Y|E<>S?RrxV)#u0H@z2|7l!`8Vp(g>fC5HdY1g&svTgtY6T2= zsAg;vxk*8@JsPHW`&GIRsS<1(57y_gJcmIZl>3-cE@1%(*UeYs9aW}w{05`;-sK?V z-~)^wj=fPSqorHEbGHm?nG!*axf%CCnA0%GN&d)DFl6n!ab7bd;AF1Psos2Xh)<6) z#G_=AGHI z9^F=;??OF$PWsQVlvRO@d0sZ{U3l>1F`#?!p+$O$j*=OaR$AGHujerxsN;>P;tLcQ)Fr*#U)WF{cfdKZJ2J%F zJ$*WDsvM?zfqY;VFs=XoVVKjQ`KVt(_aEY%;(CgIIb7b>8J#!UF7$wv_e6VSBh($+ zrY=#FvMF(?eUH?%P33EpKdNlcZH;z!QPxI{iJY>Xt&~~zFhj!G%mZ2{N6}M+1B`g( zlvZi2W^{N1>Be|yt80E0LU}z$MAbHRTE~3$jr$pJFaH*TiH}ew$5sG6H%!Acb))Ac z4mVmS$|cwra)+UH`DH?O;J-SbmXj7sT0i&}_6|ek^Gxv{>5un@DT@4_wSHYHj1#9X zw_VzKuW`%28SlAl4Xp~Z`=2)`x86T{?eBTqH4TuPT;lSGY*q|@F-yd_$Rs`*KIPm&n6bnpC$)<%-pF7}<3?X$dE|DG@F`^xNz`-wFiH@pi(x;Yvbu%X;?i=N8 zp1Qy3^io8i(TiQz>;8V~q&C8seC%I;Yyug8OXvK`^EJ%We^(K9^Y`!rdnx)`$hNWX z=96J8;N`KOrSaS9_TOF^s!tO(WCj0NO|jQYWT{`S-tBMVM|VIfE0BED!=fVW-H>?| z0eJ|&NGG+Usao;OAg3DFFWFzlw!MI4g?T;pQOBhTk^c4Z*AkZv_sYxmK^$$6iM*Y0 zCrk^?$_1vI&PM8UiZ+fecfU70xf0QKPW>NxzW(0k0e<7wjheQYQ-k9>E*yD;Of=q2 zLr69D-d;)4xPjMCRyCQS50z18a(c_C{kRmNKe=p1hdwdStK0=RL@X~Nq9E4`KtTvq z!zxk>MljC;!aP>nn^B%m#Yz}BNxp*$^^06m1 zTs3c0Ea7GEMowz^5lelQa_><^Qej_CP4hY0FmAv01(nUMHXV=ZI2ysCD?__IO?|-F z>(5zT&Sa0fC9C{rBs&f273NYq0fO|!8bX?e%A8#vFP6JQYR8MMy{;v_v8tmSEPnEa zOhtIi36eLEbG?D&jIM?XYhO3L6aBR&Teia(I=YF}V(HT&rxvUdi8fdTqNs#HHQV zUBR}^UR04T*@w??G)KlbICE{3j_-rQ8boF!9fSb+Ha4%Tp^L%LU+I`@sxV$B43Zb@ZD@p7)d$-)lY zld`S@wmy*2p8K18#l>555;d&!z?L)g`r0umEQ`s*FDGjRcz}-IR6vOFi^#}aQ;mc< zo-cYwP2H>%qq-k`H0rqE(E#3(g@3VaRnE8K9a8spAKsST3~}wBOtt+3iK}Gscf49k z+5Cr0dzRAg9GIngL{DiV{)G1_Bq~*+D&0BLJ3C`!8kMYJ{7(y*i$Hlh>EKN;X?{pN zNMlxu{RH3C$H@_)I>*Ox=*YZc<`P!#RIB0M*-#fW%{o@}4D+7iwBf*7&~Q;1cPQ(q zv4S+C(kWp^hKoH`4NaAqOtwB`5iyF+uQW7E-5Onu7t~0htsNXVNWF4#a2j2-vhTf} z>w~{$Anx6Nm2SIr#FG)_b6l@ibZSAFmeTV}8S!j9Ds&UQI|@HJ+XB*lgAW0RQPtd- zG>x(QCcE?0e4GMh{ITOaeCJ!^j}&;p`m=k6(c=adSgvbY8D`+~`Rk{(r#}nljCCA3 zaC3nH9`ekRw3__WRudX3dtxl;IR#o~k}V z!e~1DuA{ciqi>M`K`5wm!%T)Wr2~H%>38U4lKO@3#_1b|J ze~R+mv=dE7tgGrg%!<+i5eGH0JivV{czCZ0-E`q) zfLZ%eQ3L}L$p$SB#P=9)6grzo6mwH>&Yw4Y^ZPXwZGnEV54zqRI%e|P{LG_yinA7k-W=dR+9`w}IFp059i28bP$E7bxCU#HX?F@(O7QKZ};6KEn zQox}vKRw0TybNyLyMp-O*0LC0 zN;^{eEadbu9|*Z)rCt*Ia;E>ZkX7%uIr{2r6#n+7`OF95=3Zq6C^k*v8`aaU7qM}6c5iU?QNJz1;ualmx>tmD zu?r&QkLwmjM+aK?79Q6*e=a}{j6}Ql%voMO|KewbX{1*8LU;QFCbH3DvxU!@3q&34 z1}hHyM4YFNoKB%XLp_<{87DE|u|piKE;`75k^yh-S;Oa21d(3+*{Py;XAa7cS1Jif zW~rewMxa4)3FvDuP>cHp$d5|e|Kh~k@OGItwGphR|6758Llqjtk!GY7w>=UC)|S>~ zdK=S&`+SYiD zfL(U_uX&sfE>SCS71zhuu~2>mMuyi4*#VQ@bG-j27K1MAm}^C(6T^?Px~1p+YPw%X z>AgXdwYrJ$-)7#@2iJfVp#x*8#!1f{yw!cE+Miqh*Unf9sCcG2=`97*d>G2P9m{f? z*T}^|DNDGZp${~ovQ?+z?+sXZt7}|_YoGhTdcvToWvo=$VNk@V{g*BSTdT&^C6DJnml6t3Pz{bBtY4`Ss7iVvhPR zkz%X6=4$i(JOFlPS+UOmRYz?xFvdKD-h~R#JaPPS4?lLjvS!yI^8i6liah-PU*v1;d1>Ois^%7=I4q78Z&_r!D zY;vO5-envaB^o|HipDYFrec%}Qr@rgU>V3Rw}hE>aX8o5Ii>tb!<5khl)tRLBp-#R zE@k8dgyV~e5 z<|jc*C*;bnE=kb+zgSDkG$mW#CFLkK*`@)Mmk&g@K>k)RHb?7PhVm$39`j|X~a zXdj&*pCv--oHQXmLbF6!?OAZuVI)`)`I)M8IS8{pZCX`k<|PN7*Edh_K)Y~69!v5c zs7BI`W%=GmzeVfv#|bGj#g$$jKZ@=pWXvO1B{(lRCYOxEWW(GyC4Vn5>J7A zi+xV*!Y5cNDp?sNJP#AX#Y6DvuEnGDqYopkOn67^^kl9nWa_@=xKQ(^@XBl5+sB75 zceHBo+->T|j;B42siz%^8zz_r8)BjiKhqgIn)f6>e$~#gzbg#*it&EfeQgf2x`45$ zR6^fcDv03mSiIF}FT_2w>dCR*UL}d%$7RYr6}nxbH9T<#KrPO_0-33ny@c4+DlJ~n z#XVW^lZAA@nz+R`D~eOWxf1+|_GH86dt!jj97AIPgD+{;6fp47&FEJ8PR#t#kq#&+ z0fuB~Z!Kxf12oM!Nmn?kQ|^&8qP96vqc!RJ%_Xfm&W8q$&I~~NW`z#W|1a87%pP~J zluf!j^wWXZ%%du>uGpK8YoszsI1fXELg%a}zJ@^bWjUya?Zx#4#ZJOT)e$g#qH`N!xq~f?A|^G_$cdSvc?Pkep+e@hO#B)Nx-UX5u>bw>mi(`W z#cCgt0Xg0-eSAP2k|5(qu%9hjHDq=52s+4G;&s zg#G|LW8RD%lbEEvJv~n_VSTY%e&{$Is%bVZ_L9S`iW2M)@=pn9bD+M2PV)07&so+Q z3=A`OjwTq5I~b6A3(hAbj|z2pOJUoEwPRxGOD#d3g# z73I4kX=#tTU{N&aoi5MmgD-CTTJ5JpyUgGz1Vs-$jfWub_N!N3emmCf!5L5YH?H#q zs#@efP}N$}?;l07xb<-Y6i`KB+YiPCq85%^YN0Bao8e?I;y1GbWSlG2&bxt?psngH z=-814*caI!816OTMs;Zn{sq$Qf4GWj@hE=&vgjp)P|klEV?_nTziREf7`-?k-%k%^W(GGy!~odSi>s3vowEW3+jRhq0E94>PR5DuurY4E5gXbM9q zVJA9ke$=5|V|(*sw`fl3NSoB20y{v>YN;tY6@68=%V!iFfR zRw(>hp2A>5a2&quT0uVbMVT!p;qiie0VC<|hAXXg$7_dPfzoOQ06(Q$`YuI-xhlW0 z<;J({NPl77AY0+)eHG^f*dBuVqOtaMXSL%?GJ5q+3jW6M8mR{sP$xyIRbSPs56{P8A7$44gAQsv7e9STcH?NpRTGq!iVRKC|f zuMZ(KO4H$P|1WT1E#YPpe%lY`?L!(M3f| zg&MdyiN#spgL1?1RNbA5M6%1eRmpxhZnZ^&Wmh(0Y75NHjMw!6eT#aaA#27v_kk3G zS58}UdDs&3%mF>M@GNwD|#^L}5)LcU(Y-KHiR0<=ZuH$SZ&`??zLm#Gp>#LhIW#Y&{GNkeD_ zLFLczwx@vlU3A$w>7LQ%;HhwpAHQ#0TJ?+L_@52Y`f3Nxb8mbHNPj{+xe;}stw=pW z@~Dh3IWEGOxbM2J!ng*d_i zpU=nDBnE5RWiCXQ4UCtK+iE?F?u&*#Xfz7D=#^Qe*EU7+j<|jlZv8f(&j0kh-`w&f z*6`=&M(h2EFtenQ64{$sIi{QL&4Fhdo9^lt&lKs@)CCq+u;v;x{zcfW?`(zNKjm7Q zd&-QKyf~2l4H|s9MEOe6$U&RVgyE7r$>0pluLX?MIk#1r{CQz%;lqoL@gZ7S*I0IB)0?QQWtC_sW z88vX|>jD3NQ@x)`&cJ4ydg@S*mrCkP&ln=YT*@)fii`mGHU*}=_aWr^DNhp92YSYE z8EK6^O0h%kh3bJ3*L_qDyZ(<(?Y&OSp0@UGJQfjeZ}N0LzOwjQ z19h?Q@xw!b5c!hqN7#v&oc3eVVnY$wO?8Q{P*9Gqp3i;pfkAwt9MKQ<{gr%Qe|KAddY%N^yEXdAiPuLN!`MeepLBQ~rxg6(Gr!SuzqFtxdQ*_7@1tcgejx3h1$P(* zpqo5H!r$2tsIhV?4wp%!^7R<526y$m?c^}_!Uh-dfdzNFX^J`<4b;+Wvovw^z+@$X z?$*%@J!zdNw#f686|*q(Q!*s7TNzE->`2_~4AsCssY~#d&mSaUuHw}0eog4g@}5Oj zTJ5~*aSg1wKu0-6$i!1jy@9#ALOGSA@w!8hFuR0U3-cI{l5klZQw(P+LM3f&qU<6) z%EW;;{&fwiW=h00QN;>Rg-`-28BI#F;ufXQW`SuX#HoJer4wT%~$%a1>8P6~J7Vu#E$E$v&A5i<~#=*E;-jX!? zV%N(Jf&HIy65D|v3N^ejQe$RyxKZmB0o1roxoh;KBgpAPb?GYa^C6c9XWkFhT`vpp zL>^lUShqS=zi9g=cYLWc{LHuchXmieemcrlc-W;sxj@lJP7-$5+9BcPdj2{CD##j^ z*Dm5HzNQOywJ>If$@+N7D%7@bxHejkq_~znf=t28{E7)SDHO0Fco?xF-I#D4 z+l>0J8KfQk)7#wq{@J}EQa<#L_kUR&M9$18j@Zq!zaCb;molnw!ePRSX$kaOWBZqW zXt)d}3L@u+!3L8fgttO|J>&w)k;llC_wMoMTVYbWJ@u4p4R^bJz*JIHR4!~ZdLp~R zawkcmI8l#N<8auC`Jzsjx4wHd=Here%H!f_2mxzf>xgVS+c(5@RzgRQ z?SKSqNCVb5FpW11vcgJ$>JxSPP#w!oxXN&}4u7rZLk1VU__ypEtiJ8M+jTYCvWA-4 zi3Y5>+jDetjX}rJXsepGb+e4;b9UEe^n68cn-XIt9UdHgxF?xs?wk6~dC6#p(aj;a z@6fxT@5TFn^o*TzBaiGoNh#2sry`DE_{njW&bQd!YA?tocBV}gaYe)6tsYd=Fw?#w z0Dg=mHEizCIzSx_fhTr z+PT47^~&kA7Oy!f#8@hXncO76twsL04?DNVAl(gvo7|;1F^%i;az$rlW}Eddd)KtQ zwcf(YUFQAq&kI{f+8vYLHq6kI+c&7%b$1cgQVBo%biBOtBp=h;-H`<$VYnEuVVEt< zt$}WLn&xTRM5xSe426FPc$0VLwBftOVTfp~*~0STdDfB~jVt~<6%6Sr?z|UL%T+eC z)q;m+evOHUbv)X%@P_z?LKDnSYFwif*0->gWQ4F%7`njWG(lIUhSpv$wRG0zqP@y^ z+I9ygHqDuVC$pCLJ1JekHL2yj%De~JyW}xt&8jhyF)czE-*0TGER8mzd3gwGJ){u4 zD>j1c{uZWv(@KGKp=dBlBH&$vm(D&I6iE}jw~F^6LT@O}=iboV=fIjx5F0%OI|y_-B)i$+yM_?~4pv0?wr8T5H^X^e`{X9kGMzf9A{k0+$PS&K-;L zKXXjNGo-mIpII9-WFHRmKEZGHel9GEWZ{F{mFEp~niAP(a*c?#VVT_;KmSS10pSBe z?QYT13pUlj3u+}jVb!{rB}(ca4q~cV{pMq5{ekdttb=9k2@m}=pTAYc1snM`OKNQ7Z)x>`vW8ML%zj}z0 zZ>(#%F$-)&pCqPd2ziA9?4eNA{y`8~XhAe=a6|XY8~Vr-a56mWgaoD@x`tQvy->;zS2aD;6vb|!ExC6|ls+mLeGuf6w|M(^k z!3~DwQ)a+fQXy28xMzlG&>ZGfO0YP%9C)wFl!e*Hz?FrQj^c)`jGo0!L>LEB)`~)BO?>x?S1AJHr7YI%;Thd3U$}_^)xo(7B?zTCaGT%MDl)*kFU}vzQBwMvK zT3F{d_JXC|4kGf_P`!GREW;h)pzAoHi&~OXe9zi8h)F*++@P1_bgx; zO5wvW9@BM3Vx+u8pLu3f zA%Pi2!48Um@go12Az+0Ztbrgz84GW0<;;Zbju__vxokD& zjFtKQug!@PK$t98DXy})4*mq(!wWNKSnc79tJ?lY=7=C!6sf%7P9(HM#Fx0+&6osG z>SW;7> zu%Wvkq!CP0ul5m|vuAsIU znNt$fze9?sEaLBO#b0dHMJoIXO;P7Jx&#CEl;8mSV99HW>r~iLfl_EU`VUL-2eojP zp7|o&I`0}w1mpinyGF( zcgSi;3@wm@3+W<01!nlnC{S{)-NU@Mm%Q$=??Bk@mt^x2CZm!WciqPPL}7x7Z{CBi zzI?G~=&CrQ(qIUD5K?XA#$&+xp}`WLeN7&*fH*shspcua=7@mTTY)yl`Z*qP#=i{7 zC2bMy;`e>WOw=jt^M+@uATpIgsyA-tv9*_(0OA46H^BHtU0J)*6Qg6`o)tf`07VO!Ndw9J;ul89rIH{fW3BEV6Z;W?|pxcrG z@J}X)9Dg=~GOb>cffJvt3uNLzgFNKCalT)nCYayWckROb8w3v?L=_lLT+! z9apdH4Mh!#Sk>e5I$TY<U%G#gocezY1YFv0c*<3v3Bztk@4A=Kb#t7 zVuwdWygQiF8?1%}7REYF`D=M+0gl$u-C=JUT)YvXrjhMe zyE_}l=$0yf6(DL~pU})Dp+aS1j9%8+Nm^GE zADy_*)o29y_Y6t%f7dER5M@iXRWi)&LtJPL)NW^g%+Oxftl}|}PpnY?HCaBcb@!nv zgJV_}ktqi2VMfvYWro+G+MapFMp!X!Uv8CA71P*3VmXplS(bIU8E2eMJse2~)jPnN zXWUctp>AKv+$y@C>fS7hoA)EHF_9u?^ql5$u*=21B^d} z+=9XD3D6E^lJ)gGO3%Ju0^)lC`}>!lsd9yPyAtYWwt=kg0*Dl0jtm_58zM!;|;9~5oVGdzH~?)0dbn>a_eb#C`TFP zJS?Y|T`zIn8OPp74AGuWP7A%wlGv+o;OB=%`2{YoXh_a(WI=vdS~lh}8<`S55+6Tu zl-NCKO?}TET@sD%H*@ZK-dJY~T#}Jz_^9l=%2l2+JYPNZXlkgw%jKEc;@tYw{B2f- z_R#Ig}g)$Sp|Da@QU*JtQD=SkLguCID-&cBb()>uF zcVpJHInVMl(@^WGp;mZcXR9Hh)6Z1N_(AQ1kwQzqTq7AI(_FK+9Ft}s0(p?31wtr4 zY}diB(~MQ4Sc&KI*7BQm-_2fA4TBc`N8phOOU)IJ;HSGeBSBdrP&`d4()|JQ4-*}v zK-23X*P|f^-F+*?kOO^DPPA-7gm8mwbvxSfpp}v2@9En$er<)IqI%0hmZdp5*Pmzf zRv$~`cN&J@-gdVW2Qb!oeoWiaN}Qsws(0$_ko9VqjnBV^N4Qph{R7Pq+C~0QVVakn-QYgWPbi65jTV@>x>J}sa)>u8BJRn59loE>m5TVCeO`|`FcL>>6 zhj`Q7irpuZL(W8dV*tfO?kxGGe4BS{hnsT)MjVx78c4Jn24fQDH-k#6%1XnmtTLdNTwteXo-#$C0y=Pf*RD3T03D?*hH-ziM!-sMDFI7Hl|alls2^^A>-sGkKi}^)^?3|PM=>J&N0$|9CGnG zl)7vcLvk4&8rgbi9h8D4*_!VcVJ}ZX(N_yfD-sE?Txpq&*oH5@4SF(G+yq0|D=BF)AKrE%?(b-UC)oDHd8ly8KwL@pw;MHdU&0oAI3!(=MFL`)3l9ye)c`i zAG%>(5!6ksb?*efIfP?VYT0S$C}do*^Gd^=UusGxAQ6 zOFna9Av2yu;DOwWQXQ7Ptx>yN4C8Ea_|2=ik^RskP|tD?QUm~uF}_JDb)SnKLEG01ERWmB=Ds+U4-KPbWR)_IFc`hLF}^L6 zo>?CH!X+!fc=tkrpITC!SD@j6qjTk&`TKIq?YJ+C%k7toJkQ%Lvf_8y_da-va(KCr z{37i_Yqpn@Yf$_v8*t_ramqB?`wscR)_as)p3@4_P%TFnY1~@0Y`5(wnQ6~cippD3 z$M4DepFqWJWPo^4xGL3&&tQAtvTwiFs|xi=F6u4nSZX(`zFGRVuwsqdVNw+`WvbuM z)sx?TD)X|D!7i$Q%L|8NU;V4Y$r@>iB?0gGRfHrw5aGwZ*WS7pjBQohJ-~fyl9INR zcyXi%_*SjB9i6OK)jsQ^lm9lWlvLd{>~lHwkH@di*BvxDm+x#u`BPeG;&$UWR?l6P zovNv6)f4!tX(bx@#`Krz+v1K(>objaPg?lkn@gjaM54yro9Dh}8Qn1uMe?2wMv-0x zZNUC7%=o7tG8S~C;mDul&yH*Dp644swgI9$`JU8(65{CzM5YyV#>hd<|_GjPFbGD6&M{vl$8bmmY3R zb?3~#foY@z{Uf7?Nvh2)TH5}8Ig%$eSz{MS3xHM<+DDb^kn-lS4 zL@LHhcYX-26gL8np?RvF8se%Bc6jT?0{dRFb&1$c-_#Bdf*qg`lk(ojq<0i2cp~ug z%7oiE)mO;1UNm}6ED4OAD~#?2D2wPA{s2bqge=MxXN)q-0%yy{6H(hf@GjGh!G%$8&vFdf>0A=~foRH^Mo+s9XP}?6Y)PA`IrPD$lSDVPZ zNi-M{{G=>;r$N2xZ-)~PO{=WGF)4&=0`iU@bflpf&@p-0v{3~JW3sSwes zt07ZNQ?mfLtDzzKmV=ECb8~kzZ6T9bMV=>?sWvT;&l$JYc*+8ajSJEf?@+2=#lbR9O&FO)Tx6FJMOV89fJ0s7RjL3qk8^k$2t(PTZ-tonQheRB&@A67tbbl@MIyGm2FK#j1EJ1T5wKeOMR^%RB^hc$L>bfyQ=Nii3y>#W; z?>1te+2P|kES`d=d%Gjj1D4zYM90p*vKx?#Hjf_FdEdwT3F$!M_EYiTGq8w*eH3wA8 zcL0NTEpt_k9_R<0QT`dH8{1~`uJ-o#YfFfvq5^fToX+oO8Hh0Y2$CBU^qPE?F_E*7 zkyFh}bW&=xrdH@xxg)c?xwtXBkNwe2_)a0$vQ^G-+0?VNtZ!aXZwqL*Sm0W2ZkztS z&(KZ#SoP`Y-g7!`cWFhWnMOc7J|fTk^H(Y|<-3Q{gX-)v>F%hKQzOcalVFn~_c8^Q z0_zJjfmNK6UCS+Rotb9efEccJy*R$zsysPZr951kRz^^J?B~DZ7Vc)PFP+Y&jB2J= zL+z|c)z{?yT+X|j8u;?L_9yVuGaW+HTyal$HC@3QT5 z63V253o4IfsydCN)PJbb06P?cw)<@lWO3vXoiDvb@SO&+;H#KcJtIcTl2V&CcYG_Z|ux*mP)eF^*E3EN$X~hxRIcQ9p_{NbGUMuJ1TJl ze!?Yg#GrK;Tge6?1*^Jl5dQ!Z*kGP3odgxIPTF%-w8T6z_({@nubXc)R1+?x{kuLv zmDcUZxKeGS^>eKA=Tq-0+Q;FK=Y^F;qxG4R+j=JzSCMS*{jo`d`E8|n{Pq^N>I z?A70}!0fH!WSEmu0S>>+GaNH23E^>-PA??J6ZSWbGC?W^1oz1EoKiXqlAQ&qi81zd z9`b#BzWHjM!w@gLqa@kkRHG5pb;iwb5)`!tt;`ebjfMnnaE&H;#R2~_5&>OjK>kS( z{~6HEct}(O*D3nWUO8xrl%knHDW0Rrud(EF9N_nxHR{6(SCWev7dq*3r@Nz#lVS4v z;`?te^`EoS+KK1y=@>CKJw4J>*2DH-khN-);6f4fe$VL?5&yu8qG*|4worI1>13P; z>`MQiE}47Cx#9IjSFcsWdMUS5z3>QL{>Y5`p(I&RC^UHkT_P4Vjx_R8`?BXs4T8m#G_oCnBFJ5mJZnn#??aVIIKMPZsgxr{_VF zV1mdz+WWRc&(~|;k`xB10wlDdFTTRo%vkj z0|SC<-4&WPBf4O%{6&w|?*ap4al?{FJ5GVo4T*@Mwa2X*ehfV4>KuZA^UnS{ns z1su;pY!T!@1LT8IA-c@dV5`k0zMUsC8XRW0j`-Edf+GX+M>+}*(+aCSjFyv2k6$1c z55as^%YG1K54Ookhp{h{5UlIi=b^DfR>i3zd7TaJzkgt{QhMp=EKnbgGZTX}H@Y2Y z+|SEZ=IUU_A*|vkPpMlKyHW+bcex?Fz!ByKmqnd^Hrr+@g==87Sgz6j=PtL1KA);u z;noo|3&>TK)FPbrU==IV)vTxBZ2`w~peAq$L~JPV*=l?Nysi_o&93TlBiQFK(%InN zmCDCvKLeD$#Bz{Xpc^Lj|1Dhd#MF3FTgCAOq_*oii&fL9Kt~zd>jzR$2Pw?})(Rd6 za&BI{zDRJ2C~Wlk1o4gg#Uf|DQk zmDGom#sVCVt2q`sCVBSIBO6Y|1aenej zMf@Ggf;6@uL&TH&x2;fJI^*JkHs z#K$&(cl`0H770QlCjM){nyEk4ig{wt4NxWPX%vH@xMsTzIVVSOl^4W~0)T|8I68o} zioh?J1z`6lUCuRr9afUw2}QUi)-n1Lnz5>;hx(Tt!%*hg^J& z2;gWgXVnfkrjPDsT6@we-VF#`zsZIk=XcFq{i(nY9Ku|Dsf>{n1Y%4tN_Lr-#nnwM zfL<<~axnc4yQo|q+BjsJWc|E*4MIS+H$0^I{||R&_u-$`@g+i+W=UCN3dYIf6ZGXv#vTxPf2J21|QTiDFUE z)mMoFot-UyBmLf56CFqmUeo1Eu~qCu4J7pDlU=cFK^lwC7%Cw9HCPtlM{FzB?+5$) z#pxu(v2$NG4on{rcb4KErh*;(cw=#L&)-%;Q(kk-G4hF$N2~^DMmGfW6Rygsl^KV| z9j*G!mA;JD=9WQ?P(MN^P?N=Tq(Us$&N<=1sxv_CeBUmYTBV{o&y8EC0u`q%jM>7) zwK<$Y3|OO4Y2S0W?X4T+{1knSDmymE0mcJ%76hq-wDCOC=z3fC2!#<&1NiY~O2Nu? ztVeUTMmFb=Y&X~gV}{@vH5W9RwkBpOZ# z(OU)EH@N-_Def=^GXeYk`jWoC>X%sFlLwdA13;>ibxKsysa*hPGB(HPidC(iZ%nUY zW0lt1)-&^!I?`5sh2_wasB9yqO@nGnoZ68XrWqc)Ck28c$nBBvqa>UJAsFb#mR)!< z``!4?cgN+dkAFU@&jT-f1`Fc>N%4RVxub0y_`>Heb&!`cZmWL$gGsc^hLC@5@S72a ztgm2x?H!qMsv?^7h~^)l26>-`6|p`1`&XyOE*x2m=H|j>QI!PNNX;=YnGFos5-=T_ z(%OUQ-5m_!Ro@{H<^(u^!e=yeTv+hflG===ZtOb}y=ZX}dLZw@d&xqA4?O1~Qcth= zpXrjlGLf(We0wR8I8oL^*)bN16wpr>M(?8elnm#W-rNU`=zwO(oc000u>PIrmG@t& za~CHI64nZjILPb#+0V{+S=w#ltJ~i+TQPh%8+{Pvr*}p$Ivg|e$RE$ynRlU3K|YTK zF)OOGi{`C%z0!g=ds9Bz5%_A)#h&zP?c@R_!YBQrOLJ4NoFoN4R7Q0K?x}UFZ!!4A z@8Wi)dY{e6{q8A0x-8=c#h%0~UcQnqDPp7I0&)t^u>%8DDzGZP)G$9$p_8VNRr3=y zNH!LeNfyeMpvB#Q&8q80F#xH5Sh6#Vqnhce-duo%)^EdJxpTZ#9IhH2s%n^DX@}`S zJy#Cw=4~$$I!c0a9IGlX0Nos`ygN)g+Z~<#ZU^ssMH~iLQ+xsaX5&Y$|7F$6NVuh$ z|4F;zla4`~@wHDGM!kAXb+UzrGn8fH^xJ{Y>rG*xDDvl(3`o^Z9*)j~W8{1~*g!g1 zqX^)$kl}-o`kcXBbdSin3FEMitgg4V}*A- z_CynB0G4D~b9aaO%eTPu3FQaNE6yZ<_p(am6h>Gq*m;2$o4YGYpzs%B_vG6ezejr{ zf)Wkr4U5f;0Yu4+w7+X>cz1%VW#9^Sy$qyN{rx#IPwbs%Jzr66a4ELz(%-vHfa zyr8STB|iG|)%0|Dp4NmM#KLs{wcW+TJ+R~%jvF0%m|eJ+4k35E8%$^yI06mVz7ix{ z&z5n@3}3%bGf^ueDY_UYk3KJ=WkJHoRY`>a_E+Y%DWz{!DP8=yuS69<6pb z`P2e68B`HjpFS|x37nXo9tk$Qz#H9qALvvu*H;`9Wu5HAT1>F5KK1p~)61SW;AX#V zo9vT%dyTT6A)8(>j{o<16afNwcZRvbNV(aWO|IHnqL%D0*hJv!@f;IAw`;$Z4ygPqqrGUs51r$trwEjo%gTuj0I*S*Jo zsz^O$oBFTtj(-cxW4mJDib&y zUMY9_-WncEv8lLcP>3ff1zAVWiT627VXDttP5F7E>~F6wKhWFj)sk2ff~a0?Dz#ed zXgO(>(nh&804y;)%LYyvo}+hl8eSe?wzZZz6}Q>cuCpGx{U#SopggrI$FSa8wA4$! zT9#u+#&2dna$PP8-{Jbbqu`|B_rA3N?KCYYK$RJmG_+lN%7w+3nMy0R?pv5L zuPztHNV9@%633V8oIZGcOE=?2WEQF0ReBeyd+)vmM#Gneu-Z+gOo4n3W}SN*+Qtb} zis8+{jLSuGbk*1nqKqX-Viag(BdY``wY?YbdjB|;$;Ty$;-U$KeE@le!&+apXQYHW zfiQdH{CFoGl`w(xV8!`q8z94aR5gb!pW@7i)&=U|6OrDUm0nDF?Le=$04)=nYrrQN zd6IFWiQbrv_V&<-1)RBadZA`&_ZTiR@*>C6+ks{i86I{Lg575^oQdDo@kca*?_qZb zXvdHNqq{S^hoMNbTRD?n>0EpOHX>9>qlnukTCjs2^0!%=m~ zt;3sk`?_9zKJ^)L*KSv8^WWppeLHK0$9Gp=vAnAsLD>P(57!b=We)1bF!*WcHqfEz z%$mu<0m}V0hu8hx!HC zxnmF=sNF$S{!g8D+$B_DmqW3?@RWq_5<$vnBNu3ZvHu&ZHNzAad!%;F@_3c{+M8s> zsUi%&>U){)ZnFRNAqc-Wle~*c!{b_o)jpB#rpd*Ddw}7vC7W`qSx|6FmeT2lfewd# z{$9X@ySiWH_?lFJB7dQPEZ9UP28k7(O@`H8Y$YS4V$1TBu^Has+0xKFrc%T8`F8U+ z@DR?3O21^Osewl}^Nt69`&j58qVQD5KEV3BA{AmOyoie{+F!cmd#3x$_wR1eS|u0S zO;w`P928>b=2VS#@kk0$vLbqRsC?U|8TOV{oE~u2Vgy^Bo!_GP{}ez}W-+LfXl4^y z9$Dm}T(8aC7re=q2VVuwXaf^Il;owb~Cf^$A<&HYMHhdZuH z!XvwI5GVb7tfYi-Q~yby{V&Zp3a6W`7ME3jtvqV~kmT!h92kKa`_weXYfCK~!hKlt z-7caQ;Kz*NeZ5bxFb48rSeVX6QWW`0XnEGqfYv9NuY*S;I)r5Jr8~7*Oz#V@1*ip5 zrsgs)%!-qbkz<#(AWqa6c@;1Qcd#K)byEGxdEoLMR$hu;tay6zj~>Q^QuKz!;@Zvt zlo|#aP&i$%EfNTU@~s{CB9Xh~c~i1~0fyWTF-v%3PA>KIUK36q?*E)y(_9d|`~-VS zMUWwd=W8(LC`G0swqpH;fzx{f+pZuD-INP{gt>kmTG$41+r^2BPN(pNW>z7?9EHmQ zq1_AvYdm}{p~^?S+J}jaaQ}9b8mxZdeUL#S8{oT2C^T5>W09iA@#8Z*6WX^&vHwTW zefTA{|9=2y3n&V>XP~%8s5mk$K-?qTBP(#FriEr^R>pFVaHOVII5JZ+H7oPh4RB}myb>B zy1io+)5-$O2aObU-)pr=5jzG;J0P}WG2y)0*Yp1zXP-FayYTzqUD`iynF6ZHemYNc zE)03Crq4Q%36B_Q+^O}{ad_v&)Dm0CqJ}YlB#x3+e1#jfxtx5y(ON!u92C5%2{rou z0S=`$vSQ$j1h<{DGjCFZvQ&ok8XES3V_I)JZGD%fZXfwJzcCYRkrpsbq2VtjFiXo?Rq?+3p@LBR!n`>!9DuP-^=MOE6vkepiCxnSTZ zGdjAc=IHqxY8l-d?C!MqEZi`vFIv2Lh^S{9_h8Rbs>E;!UBz!ON|p_e-=On$M^eC1 z&m0hhCn0)-iHNMI#o>#mLYmso4~YX<7^(OOJwJ*Cv}npl0s^L2pJxR#C2D;jPAEl24RVM@_5 zEtNUGQ%RpZw}&zCUh3z(7-8`9z?PPZ`@<+;!`0xcZdxJqC%!X}YYdhBoZXq3!K%bT z{0Q)397Sy%cqKsDgN1tII5^p>vUljQelnGiFs46FnfW<*``IjV1~t~F=!bhnqXh>* zKyzLE$6bT>X83X<{u9rfcR06Z{7{vc+$VD+^EnzO14`>`csPgRmdv9+!u_eRo=7GO}=)O*NR7C~dL<4H83^gcq=%$3Y&q(>r2%A@zKI z*`3KLJKa>(Fu6qfb1&B_=|~2`)jEA&4*NBUOUJ|+oMgt-laOUh*H;sJN=H43M4rRyR3Sf@!3xK2+~~DIVsg%VUa@hRsw>pq@Z_S zloG$3)s47FTgBMbz;lHo+cO8?M6m5NZ}(5|PEz#hj&gkQ{}X8rqueS&Q}-swelXlO zE6|DW`VYREze%;atETuRcF!t?*e5}GA?#+jxMt^8It)bv zW`Ej;63n0#fDcon4dx#yNDGZY7nP^g_3@LkKJXkTFlA17?Wx&O1TDo zT-kcjq51x=fokF$KK^In$~c-D4=;nhu(&r;Br3PIS)Bay9ENz&m%iVc%prV3WS-Bz z8C+erELq^}bIkIz6Y{DnV=Cr6=O%d0&ETp&gGmIzoW{vF6f3OjDmwx@g%>-V1K~s- zXT1d4N}XqcrBq@{TG}3{7x-H)nNz6u5>y)f`kCqr=;b1(O^KN^*ZQaHu2J&& zsuhL1Y<$PSb(4YZ;}SzszQ6Z?C6m|NI@wyj+M2aO^4#*Y3P!dHs7rT}+TCX?H=~j{ zNa9@GjrpWVIQPa5iM}J1Fe27+lo)+$!_Dng9_6S}K|qd3iOjWG=hUBuxe4$wkiCbH zb(-lBe*^qq_lcDK{Wme8rSX- zk>g5(QG5L#M?rtDJVt61kC^#A?)7g5=6qq)8Q=~@`AP`u(@N7~v*5kDDiS3X*=t-yUohx)R+JM&<+fPe$i_u3j4q*rMe;^PQzYv`wVi$>WMT>&$J9&-R{s_G;$C zq1c>@Up{En%ZOQuib){5Wxr`)>nfsEnFut?2H+Ex<)+m;M?ksXQ<~k|YTFX!yH?Ok z94DGQku5Qj)uyFO7RwW4Yvl4NZ%ZLtOJ#ulExIKfdFk*YBLQ_g>qX^^NrV03)KGre zABp98pT9DF%>OLI@Gk|!o6i(Ij*X(EQ^SGB0W=9m(tV)P(e zi9kVpJEzxs(Zn6d*8`%KkxH8qe(Zp-Htd4U{>pfGg_M3VNn)D$&-N|C7a;gn?6^Qp z-BtkDX$Cf&W1Bp$RwmDF$$LsJ*Ya%Wmy&_`rVxQmrmLeTIMwpSg}{Hd9Etyt*FU&Z z6H~a{6iFbu_{c`-W9Mf<4{IAfF&>LJ^7INWiE7=!eezmPtd~1=YG_gg_OUfro9@S4 zpAV1%bOxsaL{rZ!I6C|awQRoHg60F25{G;3cVO?iDt?Za#ona&AQRd%ljNse+95N8 zvOZCNxWwTL2NA^;&$88KC_23=FIx?%@M48%Yad6{$P+#}Un`sj(mf>!3lSTkbG#8y zrRbr%8iV&*jjz6*auq@7zK~jsLZ|AWmefthaj)Z$n9Z>m4eL2mYO~vJ3DQM55vX|gYri5 z=&laEdbVA$XSGP{=X57?s;kGrP6|g5Lj?}WSM5Dx7Bx-34wL0+Vgod9$29Ny+<>D7 zsAI|7Om^tz8)62v{ikBn&uI8Bn!t&q^K)M%yeIC1g!EAqZ*%0k zYFTD9KhF~V`>0wbOnV$?7oxfC=bA?HJF_*)o~vwlf{jwE=>I3JZze00zFDt^Pa$4>dQZ{2yJEg#f<93+ zlNfgX)j#iZnlcT`+$sb_Dag`y;JlDmTa2Cz;MvTHF!_1&SXXC409NekT+Abg3Jtut z$SCTL4n$S6uAg#^-x1G?7bT{&H(S1Xkw=gwkpC6pUKWHmmv(OjlA4R0JzXu)U>Q>G zG8?0^#JMg*SC^>UhcSz}R6qn4qLFUM37C=J_9=g1pIuz9t0OAgBfj zwTOI*W9nX=C^4d5M!;yj_LaJNx!tp5FuP_a72FRrkA>^p{yS>jU)Vh@`|x=BMT`{r z;~?RlTBEJjV?JY3IJbmmckL0entrP2KPBMS9}#FBkXIlA`AT|9#kZ=>|L6`y%#8(A!P|SzOmYX@25=HMg&yKM>aR z^?7&4ogK4q*>kWT`IU9g0J`HLap}ZYcZyBTKvk!KQ?IKNJgaS+SV4oU*UBc5T%j--kpAwa-owHtV=!9zr{M86zZTj$Oc6*Y<1Sy}9I!lVxb_j$RjaIeqxrDAFd8JKVaVyhLDNN5-x$2rR z8yvA9@fErjt<`W`AD|R84ufv4>^O?jaY!Ex_Oi`?T4UW#F2*#)msXjv6s?CaCg=-> zd6LPi(Zgfq^<|R&tCP9@xSfYCbrE6W?EBYlmb`kth%0xo9 z7KOED|EZt}wb$0~YiReK;ZdxPRn>U6vVDn`^(o(|M>fVK=gn^{Y8*~d$@@S~d@8wC zaLZ`f(9Y+^zFwMfy=ur>U;2&2Ew6KAN3{zsf5jHOs!dEei; zWQRP5ib%MzO42lZVgdel4YfiofBD7PysgZx*z+NwS6xdeJu~eVJNVN|yhQNl?GbNKL+1P2I63`wQOJ zCS*6*X_Q)NRfQMjo>PhNC_DdND{uUKu6|N$!%$V?WW$HiA}8;vS01OC?-0hNjtjY| ze+N9ckbZrlW$Fmw1ZPCO7{e@2sHMQPm{}`TZ?pgSWxR?#xdN>b01O|}!;%|@1AP-q zLT0@$cayZ@qcxehxcr9$=nS|FQ$6;k`$I!S!WVv}ry4#^d2cf%PcOZ(z^LY|*@CA= zJ^oXi%5M2T=g0j(FKP5J@|Sjmc6b7n=mEhDwjw>676q>QJA4W{Zj|yAcW4!~m4*+~ zWpwGZz}0*zU4QGbVtZXrmV@nVXBTS1w+ZoA9YaQ_nzs=jE9lfliYb((J`n2f^RfXv z9!8<$`8=v}b$jZ(_^Ee%y5}%rzVY8zy_W*_Ub|@l^BRB}&d{T8nT3Ww>0D#61}t@Q z<@wiI!DUvRZi*Ppo!9N*gT=11A|_NRstYXVl)w! z@D7|KpP%c+qgfi5d|GoO_T7-|{4J(9o}@oMn(t;WDbt)`=jxqfp?0T%%9EsgoUWIb z*A_Jp3;)6akqa=zKSk%TIDk1q2v8ltD!JLWWUYa9CxuYU>FI(>L9|MZMvm~k3NnaH zg`S9n8YeN){67T*?gp)4mV~v=T2Q$n%CSraDmcs*>#P4#znt{0t6*qV{r)Jwgt$85s=k)9{q#+&%%fE? z*Zp5v0j-^poUCkw#v0Rmtu>~8s^V=WrbqhQnAL4HhIa+sC7P0LeE1f(<~?3;U|fQM zF%hQJFE;5TVBH%cf{rP-IQzG4pfh;(=_&U@@+k?XlWvk-84uPTnNQ?;D(E~9au_vn za~~D+bpe<>#~BJbe4eL)3xKPtEMV&euDH-HzTC?tiybQ${q1}89=qi!YPiVjJQvv6 zM**$w1U%M?TF34P2BN&&zzPdExt2tVVkri+?e`UEdlH_QC@9r+4pM(YIbxok&kyXW zfbH6pVy|JixL!u?JyRgen^71U%J}R3X(P8(1Y}Q}sEUkxkW?fn@?V+^{NG3dBKH_D zu5n8Fku(lfhVy^D>&M+ONHpq#t^5h^uM`LjW?qlxZmsk(cq9N7aah!*mM#SUoOp`8 zo|$8qxei9h2BbA;@U^~|!#W;`x8Ilj^*dHx?ALT_(McP}4}U;z7MmywVICd2T&HOCqEjoLAloz{-gI`#N zAgT9Ol!dIpdMhRl&1b4^LfyP!vr<^mmp#Zs21JvaUh;c41UcVSY;~fQPBuT*{U+;0 z36AsdOEnuXt_pWBS1tB`2g1h8w2*pUwiPW|pPg*YwV#3d`{~J_zwa$qutU=$5@MjU z;i%kOSuC9D4n&dm#RM(V@+T`s!#mJ+4$K2EtMQ>X_%$)j;2)u{2Rnhvjwu zuPDlwcj+;twTZgGs8m@sJTp6JxhCaSJxZ|p8|?GLu~Pd2%WmgK#GaJw1(HVsM=5w^ zU`tjMwbsFeTGd>T?^Un08yC|Wy6fK+-=2!CgOlpS{)eSq5Xa49s2g{TfqAMI!Z@ab717T*)~4& zrl!>Nt39o{KVo~C$912^wN5dAE(KhAwfg_T8 z&>E4FJOh|5eWACS55M5VY2_T2sP>NynFZ73ta7=^LUh3u(=$JJmo+|7`1k$$9dA!; zJM^OR|&4^_ZF3fh7>w3hk#XU9JNoF-9_I778u2S!KL|NHqW3gbSn zUvfS#Uwe*_R}_0vE#s&BhXS3q%PGH6lCPWC$4ymQzh!lnJ*@-Uz(O{#=zz(D4aEiotE!HW7~ z1z$^Jt320aeJAdsF}l@sxe;dPL9!~!+E}>K6K`Khb_c~MjvC1avk_+is)bL^B;}8Z`i0&>9P!| zN(Y$NvUiS46qXpSKZl#9sVFK%(P^gSj((o#J`}F-G-~w0ev6RtGd!Nho~4PJSD>p<&# z$%2VSX|h(={Kcx|O|wSz+J>=?;D`*^DgT z4uQRR&ieDm_BqtfH(w8!yE z44flRegAolf}6ys4lnPSouE56sKFZ4vc(WzvhJ5u-Cr0Vl-outMf;K1=e~;XK(MdU zh5%b0{WD*WJgVIY3`&3M`*Qox{-=)|drDg<+9N_G$$Z%A_H7&Uu)z7hzdcrcG@|Oo9Aj+1SD=K(cFRo|Z|^h1Qn_j2nNcIx3OZ5Y3ZlaZTP1Sn=I$bR zP=akyzK7mc9!1fAZ*E%&nHqGi9w zUbiYm&jGh=OVgT0=jR4j7KGS8@rpeFY`WBLQiQPwH^tI+N94KI%J5GL6ei840f34O z&1j!ngA|=AyxJB6YazxuN|oZxZ$&qtza4DW5+_4AiDVV5OAM`Sf_Aki!@1S{=TuXY ztB2Va)GQe_D?9);$Z2LHvah-;zxY%xg+~fgA3*QLGhoAUQOT1U|FrAPnW|?UD;AnO z`8J{aJ=T8^tvX$%+%|H5smJR^x^Q7owH4@_=bLe`IwNn?=i#(qbULW^^4Y_QdqR%< z+5c3q6s-SoIV|d3`M0ggFz0uBt^))7hJuX{6+J5lM%Ogwd=39q3Fv;ORdsf=a30Yp zRK<$1l}vprZs5uJtT8b%R|st+E6JEl>h%EkXW1`0{CUSiLKMonC~9(EnyVt@21j>4 zAnX$pM@vyAH(<0H)T9knaFD7>?g7C~pt%yg=9r;A(?zn-=iDqaOI(rDv}1*)n4V$v=HXRSlmLq!(BgewZI-L#nNfdnU7GiL*&NbrQe z1)$r=^2# zc)Zk;zjSx*aFot%Ic$oK%@6Htal)|4ljkq*lR5YMJQZAls>_6Q<;zDo@9E*{msS)q z?)PT=7m;x!Eb~B4omJe)J7EM)Scoz9`}*1-J-IB>x9;2YiG%M?G=%BdI5kb+x9HX@ z2h#DIYC}Umr-N;>!taG`_B3cT3!?%9w4Q|R<%>uAm^%A~sq;4#h(3 zz1w?zU7eG;(8{mQQv)_WJPRk{PG>S}M4xykNqLr`^h40@EWDh%HDFoO)O8qiMgL_| zw%-i3@9-^&n?K(+3G%L2lo~0o3Ip!$G;k#ULT*SgiQAKem#5=Yk{U1y zM~Y9i&^x@gMZ7o|b^Fpim8`t_kp>4{h!%dVvhlN5{7m|f&^=Ez`WuRBg~vwq6Pb;> zq58zEraR6}C*7(y-PF@5&8jPb-jRBfczxPAS(PMmt68Me&x|&TGZ^hq@5gHj&%zqn zNLa(~b&|%#hgalWT66jZGfMuaE$YUZaz^;8rWE7`;J-$Oy2>Z5C`w^|u5zc7-a>;$ zvbeS{a;kF3(iGGox5_{o8fJqm3MGn8A9$0zD_!8@%Z(}hAVt%EDIlIwKj6ALAEgy) z<%~;WgZhT__cvJH2nFoDc9iJK0s{{_*st@e{qEjbeC{`>SpI&QPPSL(D^CNUbOu_O zX|x`VSYq6M&sMgg$gQR*{AAw_XC#IKFlRnp6(M&X7dVe_GgRQOoO##AcqXI=Z@?5A z<&XPs^Ew<4?vzkTYmFvodFgYq+||8X?&*W5=%eU#w$i3t%4Ab!F?~@=M7;-SkJbUz z001(8iXqFYiZubaTLb`r$WR61su4mm5O*l%qmoXq5{?4rU_-QxJu;a(bsqU z2Q|E1@eO95={3QZ#_}(Tdq@6z6I^;%C#%C(d1Kh>cj?8bGn$e9_q~GBeYHF5alJah z{hOcdYMV~S8UGa*Ai&oJ8)|uL>3J3p)9lp$rYN;CekY3qNR6XeV)X?IHUS4q5Fvww z+PfAWup7qZE^In^HL7p*XYT75+7BylZqQhl!p0w>8yULqf%?w#exwUie>JE{MpWG! z+PyCPyY1a{z@LMMDRJbg2;i=rl7WLMbf>lTym@`_e`D8`fbD|}8ETG&0ayNOnf`5N z+rO|2tcjcLunxNHU1w%4g&TWni)b~h?ykSV7&OeUHV#0w{rfI+G3w5 zbkq%)zp~}g>DGCY9JG)k938>%8z<^(>eH8{2DxW_ElYPz?ode_!{%BfHz!69yFIEv z$?B{7iS%Z}_|T5<8_RT69mh6{p4Itryc3I6BcA8_yItX^L3?gA7JBSV(LX)5e7i)~ z3056UTAtXx<#RU0@zFyYlcG|}(aB~`P(XpEq0*;5&E)|9rYM}kV4)$9Vi6CcS?q-| zZb1M#`|q`#CUf)OE?As6KYegg34IJV_He^yOx4hv0r^VEi>tRQd(k&P)AH~4&#xm2 zKR?MGtNVK7(b@m{TAb@_)*tu2bbaSlLiqXhTpMhePnb= zups1JI%R`2YRbfVo79`w4lD7dxeex+T$5=2#mZ%biLkhJq0p>q8EN-!FkI1z^gLm) zWMw3LjO6@%+2HIXw@=Vy;nJwdgxD&E z`%zTM##ZHtF$*!g>3wZ-XMwy`FC++MKbc>k=n~h$L!F)OUHNc{eM_^LG)Vs9>Cl(J z8?7C&nDyEIx;VyD!CD6cF&t`5jH~5jtgc)U=KfA>YOX5uCHil`-&O98M7$$~XBkM$ zCrv6mR01~Dhn&7CsH7yTu+^xYEdNHY0jK&{rwX!PV4PPH3q^1BWs~?=;K=v3&Ug z-Ll?*iT(LIrqRsw^d)tj6cK(h1HF$>E7G$R!*JrxR8QQY1m*^=gw+T`Qb{)X=&jvj zgLw#Q>z5vbw1jT7Po8DB1bZrRo}-m$5-OmDTfTdahcvtc>7=*vSzhKp%_pgPI*EAWX>m7p6*GX#)sMvGVF>qa#J*v=q|NYk% zeeUf3|J^q)DDYV&Kd6=rimcB))Si%OU2kHGko{aWn$}cJAOA`^ze%eedk%g*xHXjJ zs!>#4Ma&9NIO@rTRYubg;SRF)|6XSljk)mulQcwZHp@jONhTzCT6LtX9x; zB=*4>O$afAJsSb~g0SzQ$Zs^h(179d$>}Awhx7RfOXZmRArQG0iT~6{4p!8Wf>XWf zIS7jur2n!c6cD^rzc*S{3tEbyUqd+&$e>~irFXY<=HY+NixnCf#j6(#biBlM*~U-j%G^F0loiN2m9|*TylmFxfYNA!$jI`chxf)dz>nWexbHVR zK_sc&G!vIjBwnBV39Ztp5$UF^%3eYh(2j6iIzMI)c9a==W~MpH0<3fv07C|83@5S{ zwXYEeX9vsAn;iOWaPG5tyRK?5vaI&1h1Z%E?hoAgrqK-Y- zfN>}*(9y!?*~k2PRIFKVkQ7kN+T^A`wuG65HYmgqN}oPr4>~wamqe==MBLOEEG>Dd zV@Fr*Dv&uUg-*k}kNemQF-$Z)K?3CuH%x)Shz4lLxZPirzjatM@j% zF7-G>oOGTn&k-?=gCB_T84H!J7Swxn3?3GMkuwxFjHR+YwbJZW?TVSIW*V;ez`Xuv z6DeEg*lf;wIb%nM84Wp;XH;F(cg%7B^SHvqzjW=bAC0iD;mnuW(P(hqrNlJE$mN6D zto005V34&h;^Euf)b8%GT!R@B54RRtQGI#-(J9xQ#Zmgv_?s4=WAGXhL`XW~P9-ifPQG*< zkrW&{X`eC_ub&j6pZqR#_EyUG3RadK>cmYmT-f0c&kc4jhfe_v!)@f>FmRvMwonDC z6!w?Lx};%@f^)Lt$bWXuZ4kI=4m#TlanWN6U1acd9No{w53DqclHw-X^zXxTAs z1+5RMX(oHeiF;l0cRTAVOoNs$QLu4!?(P6k!-4Uk|0syx+L&BsE z$+7DC7)LGo6`*o6YU8jqMycshY1kBE}ORwa3_1_>I;YkU3bKjrCvqIc*oz7nRj)A2ZWH<+{0G^ zMvlwm4RQ`Y4p$L^D>G#w(oxRQ1sUOox9*GNg6ML_v8;Olg&DF^f+NJ0=t9SVMun*_ z?ee-b>r!mU?}m3LVNs(*&?*@vD<#|#Tmi{e(f=FOg z^|Q8S#EATYGv|i9pb`7-84*X>laI$PF2IgZth58LSp*0xM(&h>+$jLUnH(4iP;2T{ z4~mnVd+up?IIgSgtMWSAjcvZ7LJ*&gA9pJ9kSk)_`Uwx@_9Ef|U`CmB?XY{m z54E=mKH3}L-9%VR6U?3sHfAH(!njMRAi1(jODb0G2AJG#TcDGcv=<@Yps{cVa!Bm{ zB%B!i#g@whOP{H~JGX_7z5yXUvPZ>Ov!*oE2C$K-aKKH74TW1mw@nl=Tbm)NXpd`C zy2hplyvJ2EfpSjvs)=ZXhpF66x&4tq4J*;vjDS;+468Jc%XR}Y$^x{gQ7RUk_ACgx zq^9S)BWNxl=#GJMxsme2m~G<~kX>l-E_N^j!5Om!{|*2L5EU0#sNQwlYggpQ7=85~ z$F> zt>YB$ZV@4t0!XevPQME|Lq|=U*8O(MTYk@zDTw0$RNoHJGEU*%L--a09x$G^NUj&M z3Kp8 z$d~;^BKH$ZUP$2fqbx+VP~lXqPBcT=zDuT$qhfbl%_r2K%X6JE;DL-r>DQpqO5a%5X4|g4C{#QdZTR1B z!22OHQTsClGUF+(540=NG!JiM{FKFB zpKn@ zdjAW|>KsuiqC?;1fU^i!3^~UVn4Y<@H}$^kpMIfAR)&W%VZ|R+W`K5$4a1+Iep|=( zKTGe0Ho$01TcSZ0^U-B}VE=aUo{j%mv)LtKboAk^@tZ4?2a;Lv7Z_bO3(gh5?8(tw zDg0NlE&X5gQLu9K`VO2BG}P{(a>F~B==|ynEI}|nr7d@a4m(0*-&*u$Tj;f`!Qdv) zP=L$0@>P}mZ8yK)b2}F7c@E-j=i<<%c$cBH%0@jH)*o9zYH-~Ic?Y&l)~7zKkDZ%( z-co&P$8Lu{5$@pNZ9BsN36G3Dg7(dU+K3;_?sgA9A4R=Zwj!&O3b!ZiH993eWr*_L zl%F1#e@d)<%TO%0fpoaR=|pIb=*V7;!QTHFO@~m%uS%lEA{N!2Nd&0XTYEpOBO!J2 zIU?wqKxw;(_O}}9<6myCT#;qVi1BAbTb21Q=A9_fQN#7E7uL@aSJSNWI7EJwM#Dbr z{vhm{J{;3<;xD#txIfBLdvl8DfKueiRG?EDD(W`VxtpB&v>TtL6+kQt(1SJkMz~`%F^tcK#m9P8GC0jA_~_Ml`(9%_;8QA% zV$DRsm9y?Blh&Z;-QJrAx11BkVKz+ImwDJhK+ukJGI7jV>vhr|daBp@gf;GvD*SIR z#^gCC@OB~mD@!rl64ii1_Gszf63ISAZFkTo+5w?QOshvv+~0LN)aK`XN{WH2-}Io? zvz&ry-x`AN1;f1?)86?Xyvhv=UtT+AWAsg`xGae1y=m&?SOf1Ed?qxyCqTVsC_WTH zll;JZX>PTY(o2?4)2$o-)iKAS>b!V1+&2AV>V+7sIDwY`DgvJdD9M;5L)MJ9O8s}X zq6mOrHNcm?=2Vuva5}hlxbgnp-un)6zW(l~RmE1GdFW7PjQkh-bfN9jR1ixD<uhS^rjc`F~iw`Nm9#b`Vdv==85c#E*Wdjs1M! z&P&sA%agh@!B&I;d+?$vi>F32@<=6QzKI1B2jtq>i_>MWGK}>#3-mw0r87F)!0-uC zP|)QJw)VbrT}G3;$Ulrxe}`_dH z{no$QANrg=kODtsD3aaBMC85?8af0<;I&m2il)CzE;o#6Bu@y#(Zz}(x-Xo*g&)^I zDPA#}W360cKwBR|PXM6KT}aA?eA>p*CdIm^W_elnUI3;|jz;s(;6UCr^2b=<(QEGyUcR6N!h{W5EpT-so)1G2>Rt|UX=ub#Ah?Xa zE*O_H03ae5;P`bIARulG%L!5Fw2ost|MpNs5u^ft&-tmkK}^_DF@EUTyRd{ur2SGb`jw`v@#`WkM) z660)~i^1&hP3c?)h2aSr_@{rR%>d1$RK z)fWB0(@96^<_?8CLPUQ^rKm5h$Gi68(UzARmBVqhc53r|UW>V?I>*D)t(>ndzUS=@ z&fTT(;(!1AhuK0e(|?Dk_@7td-5&u2FYLBmrK1ln9r$n4;LqE~yYB9IU25~wSD zl|9bCnkjO-zwnj)(1?ups5 z94LeP>z&rIyI+hnm``Z7es(W>zdmNPee2eW9k+--N8`e&FxS35CNfS^W>n2+lL;Bg zGc;0wn|fStfQRlo%mah=>D($-+MQfY%zp9v>L`;t9Nmn1PkrS6HOd233?-IT2~&qa zsq|zZtj6Q%;E*y#drox-<^@y>N2jC#WsvDlWG5Mv*m}!T9|1AgsmO6fJ7mWglv93& z$K_#gn8pITaiXhjPgScoo?BH@*j8*4X!2&*s(;I*Iqv*xLb;hRM`7P>P~t$2*&5wn z5f3}xRA4xZ9>h?VGBX7}d^A@{x6_BWK;hG46z#MqOhpwwnFDod(ayv<@eNuFNbu-I ze5;mPGlBz7b#*-Ua-TToIxXfoPt`er_d!*mM?S(46gpzc*B`J%JNoNCF3GIZ?(4Pt zyT`TEdiW<(Fp8Y@YA1gt$*D(m52;Nx{GIjj z`kn{FX#)O@lLmKwTpaN_9$fBa+(4pht+!Xc*Rr_o<)O9x-e%ya_1N8ziS^^O_nHa( z^X^tiS@4M^MpMvYGbvDE?^-MU7f;wEu%;mW=>e*}3vhBf%v1+x%cY(V?|o49bZOay_>==*5*-u2{0IAT)%5Ed=fD*rC8$o?poLNb#LIf^I_3Akz z#;A9`p(TRXKQqtVpO%BOnuljSzj3j49OCjz2(x6^D5f~Nk|+A^tlynK`fkV5tXbu4 zzgkOo?P8g%5q@9N?`6QjPI(UWhMjLtNK|jap%3Y0@-Dd&)e+l?jr!Yx2TnAqS)^gU z$0oUax_0Eu*8E55mY{Mfv3R@Jed4yt>G@s1URQceFak>7EE)ZK&Wo4apIB;CQ?lJ_ zQMF*1qn{;(9`#)G^>&&1@1oOlJEnlEK8TBWHf>P%z4g?_Pp;xfFx6j35B`xVx!O5j z;3=Tey3;}m6CBMM^u^FV4Ja@CcLBrtyYk=pgZL`ZE!TQRWP1K%&%+fpnI-WEo=@?i zWgOftYC+D74cXSiK->4aIp%d2xMdkUIo?pFU+b#2!!-?ly#u)YMz`x;5Hok6k7>xe z!9yhra@}zXMM?BWIG2{(i$X?paU~Di0_N2?a-&TE-nuwz7~X<|s&eLF{aNLzgEV>P zEiayCCt0^HA10^Yz>|}*@`Jg`a%KA@6S66$ZHF`=IReDo^-9oI!zT_kk5Gl@US^&i zvWm~|a`UbbkW7v$QU$!&Q6WEX`W-rTxp2F=je~>q_~C%4mu`LNc?Rl+>Ii?G_ z|Hd!bevJd;1cqwcCnO^tEji}p$+3I;s1K|AXyFT8z3foUlH~Pz%1gkw?Ha{pPbOD6 z)?u5mVATcdX1n_4C?Bs8qqiV%H_=j=`S437b<%kVnO)%17fsOvO_)%7+o6`v#|des zzI$l~?gJ(#9SW10Ppsa%koNS-k=ekCi!`NU#H+3Ue(n9uA{jHM_vl`!+Im|(9eLT{ zW@O+qK#*f41)-Pht#eB zQoZjAcgOkefaLXZn0tXx;A)+)nWE? z{onzsHrb9gS^hA!mvzCXm7$O$J*dhUpKYYHpg7~B2}nuJ-J9Yhwc8J~FWFt|A;%%S z22m%Apmpy}ni1JLd`16%!K>6?Sb+&mH#F5c`%weJg8T*HFTDNmv|}^Qu}`aSqqpA9 zeY=7+Pr}xbBLnma?!jZo#$)Bf!JG)kn&a6nxa#2+TM_o^!zika zw%);(Rm||ZFFHC`v7Hgq7%`wq`&jEH496Y44i6@McwBgCX}%-TCq)s1NJ%3c?TPa} z`rJ#GOqeitoXoYK4Ny!;c>48|%yMvY!&6L*g-Q^;&2#@hj_$>u>HdHGxE;(+X3jIC&G~H3bEvi%<`{BLHIkeg(M4Bv z*}?gcW6qlMAqlCFYMVnyg;bKdMiNpH1<55O61i{xPt`c#wN#7ZUxCWC;4IB`Y&-+bSZD7iNEz zW@|0wA^$m8&U}vm5Yw>Q`VcQ3SC7P2Vi0t_xoO!Dzo7C%ySG%ZMj@2aq*n*S$Tp0L z2I|{#wQ%))Lm7}k$=)-=+L-RXo`ND`&qPU;XtBakg1b7!&ZS5y4uH)nLB>o@#$n}@ z=-%=ijfu)TIq9Is!n}eW4t*Q?f=-OE<$98GP7bO>Mso&=un#8L&d^*c=Ko+O2R{}s zD1$msX%xkN*QfQJ4Zw}CEWP2)3z$2lAU-Llt+GOIQ)M%opHKG;o0d+S6{A zz6J$bGup1@NDtA6lXEnfbG1Xew+j{qUZghFh9R=^di6r`-4$|zLOHk&$j7Ss55-bx z>9XWWNu({tVy4hdOXsRjzTrfzwKqqf{(nrGVbNVeJK)vf8?T;T`XA$lGA1i=J9Ft5 z*aMUC+DCx2z1LU?f=7eT8Wyj+%WlSYoIPCc$&^%rWnG#+;QZO+(N!wkMUfv<%D_z)FjlcdH~Op zDk9JKn8%E-^^o-KxM53Nrx~sB8tm=uT!S9f>#wF8XK#D1H_m-kL+f!X&)E895AKry zC{C~`JuHcuLWRTGiah(dmC8c>+z%p*5(8iyWb%%Q!!Q7UbA!7oxrB5O<) z*8viqGDEfn=dAHsCLHZ@n7LW8MDtE)??lx!ts<)(Yf%I;E20_?^cnVmPO)427O;Or zqdI$Xmd`mOIR$?;*w}AFtMw{M&H$7L4q0L`?PyrxwsFng!nDCe@#7n^RaB`ok!A8k zNwRn5fhCTo1otDRCFWeAHR-xl5r>*8c`SYQkD#ohL@ptUrPU7jnJtq(Dw7+dW{8nW z^v?8-D$m#|Iur%|yCb8(2LvxJdP^uqC!&W&Cki!W866o3ym}|Zq)M-BJVx%bg{!OM zWZAszNl@q4<^Jx_$wQpd%H~NhEwf&jDeal{^TVVm!MfE$;iN?geNgVu!wdy}CeF6> z3fHYo>ozR|-%&eN-zXWH2T7^q%2rC6P_j%mGQ;)2v6@_2Blm(uMP2uaKtRSVt**oq zr2`6(gH3bgKqd9z4Dv4b{44H1tJlNzv3Awiy@o2)j$Ua?I9NM~t<#R7djk$^S6UY3 z8w{}9d89#Goo`5_abNZIY_9f1z;)ux^6#Nj`aPmX&dB=hXN%9xG$AQd5 zoYZ$5cf7h6227rBWO!sh39pDwEGU)Fn`AYw8^Dd3Dx7#AAHdBED!4V%_`3vtK^D_& z(Zg!!ZJ4Z5jrb3JA{U`^{rQ4c`+PEas!C14a zOmC!SQ%1UXE)Vgbr{EthNozAO{I^DX5#n4^%}M=={`3I@v|QZogfgqxh$WlG%>#JWrbiQ$r2nx^d7>B5%4H@{}Wkm=yqmWs?@;ItQ2vqa{Us{n638CC)c-5 zxrqBGF)MV}0zV{q&OrT&2=q5$PBOThQaY#qY{EdTI4}hrsF&F>cF&}|xcI!E#ecnE;5Y4L%C3V_Dodh7C7w48&kkHtf%+@_3-$@R?I~fS=lRU40Wwk| z2&sBFk@4*HF9Iq3@{>7Sc@R*B2yN5HI)yKG@T&yTAnkUx5t`$_4%Aa%SGZ>1rQbD= z8F=WS@%J3=Q7`<;UNrQ{zeAZp!v@~X6OEVqXCBI`=JZElHsO-neIu`Em9}(vp({tH z65E{(>L7aE4dz-c43=V`nZe=*HJJXpr}PAC)$-Lp-0UREWo;6`uZu07=(Y%&DQ(xR z4g32>Hn{7yk2^uj*q z*8BgL2Z>vUoFo$z!wFk;Qa-Q2pPrRxaEqi8C669{W}))8MXw@c$HMVL!}uci@Q&nh zJtE!#+;zwQmY&Eeh-DXo@eQ5%CL|Je|5n>&<*1mkQxCm#gN7T3fcHqLI9S{EP*P5w z6LI=4g6D84!e&TFUw~~+0lk9YUfI;mYHBaDDs-NTX@_R?k^;jKcT<~mCYF25mW`tJ zgyUulKSOGxePA}N@V`S%u4OJOX*yjxyb@XlPhKW*c)8D0oo_)M3;8NTL{KaXdS|Bl zMiWS!`MHA6wPZeb3sp*dZI*)><}&j{K;%`)E*>UyqN) z(*<=1vKd)EPXG8H8a46}`d1tkO}yvlCf3?I@qu$IgB| zCKFclnjroHOI18db@CLy+?$Z={USB^(31&L`<9;Ht@+DITY09!<(q`M1?RjOv^bq* z9R*Zq=eQYz%_e{r*`QO>;KbzDp3%4n#pD|l4TJE&#@Bw}d!JJTJ zzxubwHVV96&6zt=!D`42%&*9tK~$#$J_=d2(NHBapw*dW8^?8?(Il6k8`0w9S=jIb zHch``^dH2Gm-esyI&4gBCkfnZucZ2vSTs}H5uBYZ%D%-c*!%i>r=8B(@CB(2yx4`C zle6=l?NX6DpP zB+8|ve@v^C`m^$5T2DOQD(f`;-~j}}qx!l*CC7s(d6JiPhWImW0^~&G{uv~tS|{VT z<}**QxPMnF2z9$O3Ct)eD*Dax56bvtt^Zm3fc%#)UoNU+e;0X5WQ0TcMasX5|2XK- z;`+Z=k;L!%<^^l@8k1@OgOu!#4RfFW%aq5#$Z!n^Lx$ljXH6 z`V#AT(LwJIU;yjo+o)4z@CW6{+rN=d8oHhkI(IW8wTE_qw(O9SF>X0oGvioN>2w$S5)8Ju@ycQokiO2At+cgjXEP!|6y6gvQ^q%s{J)|(C44hYJMhSH089z{Hipx%Y$^@O zDd&x5$a-0cz-kFXa%kRv{y>Eq(>?9%ZYE2S#IoA`D9_ir9n`2zX4OY=jflDH6?;aa zGvtssu{*5=tSlgMNaq}7-;Um&K`5v0oQTm_f!ho8^33H5j+cAq1yyQkAkCCc3`TQ8 z+e`>EtmYZEHN&nA1hr3CO@cGsTRU6WDCF@3nG|qx_ znHYLQuS8XR=4r&1hicSWMWWQ$(5wa=rNY}4k@ZQ}E@g9Ru;$c!z5SnG&rS{$PlpaA zFSjnwf%Uv<4HMS9W6E_e`CR;(dbd>Y?#*3-%X4-8l2zlSfuvG}#6L$}%M#b_J=;m1 zDs2;E=2q~Qw^@Du8A%yQv|NQ&pDQ|tA#uOV^`@$A&U{+>wX^0~ANtOq?!$|X`F776 zkDWfG^e~?FKYAW{+w|C9Uw=RAlxHtL^^$F@LmFClvc* z^}(MfE@eu;>SqYYOJ#n%Iy=zrJF<7-!T3KH65lTLKIlIEzl&SjPpZsRS)Dd%!g*yp zOA?!kXG>|^z?*|t%;8+w!WDRiLVZ$$F<+t-pQWY|rEdMiiBo8Ep@>&#zR5W8%4n8x zJ6D;4DSd)Ct#5wGX3j$g&bUJVk)vA^{YfZPQZd-L$YNbqU^~fVpL79frd+J4ud$t6Gwe` z;FpOLuKqONSq(+$p;3x{eBUG$6~97NWW|-Vszox!oaK`&g_HQIX_gRAU0^_(rTxW% zDD$4#2StdnKu2{iX)8?4dXbDmOY(NME0et9sE}U44-^loY|Yq zr%0hlQA3mP;)tR3fn>0+WR3gzRiJBvQ7;=2OCF-B#v{S)YAmDh2aYEe1>UOPZiRi4 z3+xe$D@s=tXJTWJK=3~4Sbz&IP~d~6m|p|Pxj_7vA@^AeKBo7h1A4*t)jq32QOW(i zKKHU{zNL+MbKm4}u`>cWHU9;G0aEOUN*T{Je=T*`fjV9CbWCR!P;V@*{7WPu4hTWaH1LK9+i4_m{fe#+BpLytu)R zdqbQ3#f_^0Dql4ID}_haLx%s_o1f-s%AoObDLt+k83i5>wm&o)B4- zEr*Ka=n#z%9Wg;ltK{pYM^_n%>GD|gINC-$cbJYIRYXDzt@{NibqQD1xgM}xOjsdR z9Hr`;Ax1x24i4)H7@6luDoJ$aqx9dxzokHBz*`=le>XhAP_NgujDp?^!`nLXvMK&F z&q$`gc|kEd-zW^9;}N7dN0hSKqKJ>`9I@48sjg4~QYF!muJByP040aA>>Y90d}m-o z1EN%e+0~*RHha1cehNFOfxTH0c8!3}tIl>S<4Iy@=F(2~*txxRg7*g6&+coG3%XXs z?U;3Bi;V%}m||FfSYixFWyxpqX8l$<56 z7d2*A{+4dD4RtCEJS9~ta?<<*=%1BW{nq|n+yk2zBM>`p3+WhK`+>on+^?cy%X1Ts zyo-u(#oc%`OS2+by(?ZpM2?Fgk17ws(0C{)t$#1)|m*33j7%VW;u-UR> zJif@HsW;=)M)kpO@$0Dwc&5!|0YcV~ihY5}wM*TzxDwj~ggfH72a!G?UFNddd&ama zm7x95!@vy#o+~xCnZK6;QRwdJHr88(saOkSS=(dAp(1w0?p8rzC!3P4|FCq>0~Uz} z_WLnm#>1N$>z)xUzB=x)t+YtX99OORlaCyi+#K$1vOs6?87&(siBPz zJZTj9Af};=9>qs};pKfMvs9|-Yq74q3U47IQ=SNx!p6RAn10q#L7>(CZ%3tB55noH z6oj4F72x-7_L3QKM-b63I4X^MkDOdrPy9PHp?Y%bMyNsBu%7Ho6e7AWd@<}uIyMIl zax#tkPhRh@?}n5oW|#h;31Ci4w$`|hOyYc#`WjJ6TfHGmS!`yTg|98#H9D7pO#~8qfi^4gcqWy`Rj{_?lpU3jvrq9v6FTk^< z6WSc3-M7x_n!pmXP>95f!ahp}=lf)@?_8Y&aBo zaZycLZxz3Mq{pLTzv}9EJL$SEC`KA&;W#rck)xjUkqgQk4cnV_*MfA6f3lVKy&dULgvcR&;NQ&6Y^%udx@?(CPHh^?V) z54whVh5x8@ET2*%x6^B6a2Fo#T&X%g1C5>xBmBsnq%8J?8=Z?7>yP-#+v?9j+&lfMt21w^ zL&3OO{(Xj9!%0dXlZ~lNhh?*{p=>={;jIOE9C1O#n01?1`A-4(+;@+<6s}88E>#T0 z_D`w~BCjGxt?Q5zl>o%~>3aMXELSL#&%!C>T1Drn1|OvoD>=Qv2MmzJ$kE3=xnJt? z=<6kFal)~lEBE|H`NOn3mNbGOsP=oQF~(Eaz`5VSIWC*wJ3L}S@|N2}QWl2E3g1=% z-H&9A7?Ry(R9gXvUFO~B=ermp?SKc*c_LP8_gnf|t^#$>vrTDi-G7Cs_H=QLepRnA z$nMqjw~}BTe}dwi^c)blAwuoZWQ3~nz_7u(BZ<2Zph|s9QhGCVioa3u@Q8ffIA7vIC&0~qqTJ6&1Jh$AjC)l6h}6ph2bu@PQn~H zadcH|dC_p#WYrq0sCDukHMoFmM7r#Oa+>ycJWCrfoL`b%bu@4gxj*9UXE+)gy;u$P z)+l`U;FqVSFSHi`dQSuD@3N~c#b;@=I>X#=RRW!X!+Sh%u{4yTHqQ#|q{xeu`C28y z=GQB(G}Zi^p<#rpnDx#FH>ZzQZRb5{=h8mSmJX7!dWty7Dz%Mtj3paA&BCX1jl$rn zX%S7B<92dxKhx5`0H#(ca8g65%M4;Wy8cUA{RoonCpH-U9$IuzdM7+oe<3a97R(wS zn%0pIP~oq>Bc0#ma-4E$Ko92$uuDX|Lce)ux!PibTRt&Vb7ZpR zEI%t80*Ywl1gKoff=XRPKlp+FV8w7up~a>QWZw*z`Qh;_K9LXPlA%U?h#$yO1dQtp z8&6X*2@O1D*%mp_8dH9iRkUUSr3a@7l;>-L31K~6j+SW7>6?7?Jl)GcXusfSr@%3E zaE*k*?$jHc^3G22$wr=3Mhpo5kX*$29lM=6JJ=;bL6@4G1h+5K>rZvfY>I?-NLD94nicSi&n{#z+^Em+k!`{(*q*3j) zQ}iwYtRMClgCF!rZjzxbHRYrd)&NVF2TH8wzWkH&cJgTW~2M4LdZLyyTm8LHe-(m9f= zjhy4|As_RC)q(HnivEJlBhF)8@Yri}??S6phZKtd-#3r#b%u192G!N{+UfM!EyeiY z%$KCP&mQ`TpE1p*12~uLjL0(Epo80d;407ruQU9^G)=;B$hrn%wG}b2hVx>kW1b9q zf$S=w;$P9X>|ygqx*UVykw6`^Lli7<)?V6vyq#^?DQSz zl<`ful?%vLBleY^Cb=&0%&-v8Ylj)hTYmJoi{q|#W3GCrU*#456J2GW%lad^fh-SU2)miu9^Wlvb7Wm$>Fa0;h zw(3>#>r{;7t6fX75}qn?MjMhx&wqMIouIklX^Op5^mUJk0i+@;zqJfg)BBR{$4xDd z-lsZR#XpYi>Hc7ceLBBtw?#ETvaakL2k#`1?mK~3LKKF{owMEB^?s7AexwmB55-D+ z$+0=lv7GjXA7h(W;^Ee8-TcnhaAwEacF3cADfBMajORgzhe}y?G;{o(}^Cs z-hDRf-<=k@-z{4n$xf;t7>$l+Q+tTsAS)(lpHh)qo8>+%dU$NyYL7^U61Q(3mt|k%Y=^W-=F^+cG>m= znMaI>|9z$O$THv3flTtWKhFEcWn-dhf5G@6&`sBUhb*_q`*KubI!$EvaL5}okSAW_0u}nJA@0r&DK1gta}*B95-Bf zxsDpWMUzYTD!^r*i#3H=i9ZEQ9&Ts?yfAZ?C{z9#m+-l z_5*SsKrQSqdaxFO3AC{f=8uB7kI;MleEUoDU23MGxXuHS6~et$PTQ}MRn*xS+C!O>a-v|IS#C+SF< zaQwvOSd-SQqCg)i97*Rnwfg`dNp1cc&;6+&1Cmfp0(c+%*Y<~pJeQgq`^OLJ{#I3L z*KB6#1{teCzdUA)@&{OZh{{`b7c1#qENnhyUwb+~&rXrxfmh4_<}Z6c?)cyL)P~E7 zjXQG;D7lyq`45{*$mjFvMD={h>Ac5C98{Vt?#yRhDzXi(_Dk%I$f+_&zy$>RYKt$= zqbmlAg2tvxtl~*_(*v#WIs>^wGiFa)3#!V`0s*;aoLpTjLcF0q7W6C(J$x-Htk}5- zuN>Jev4iP_*NT6mDXZ+e6|Jem7yh}j{r>i6 z{lCPwqX9^7KH$Gs5yg{e4{tPB2G-mj+8pdDgz1+N>*jlkokyZcTQegv6r|6ntbJE2 z7VTNKRuL8Yb-X63v3Q(@viX2X&LF(xz$@K?1m}# zrw>i-u>a<2+pq6zh1;)4G`H6Dd7oILtj@j2za!V=c>S_hU+(Hiz?`96XIl(tlRk_! z?6uN3)1^Vu+pK81YCZH&E5|nGYX4J{T5q+K>cd*OT5$-}j_=*84ugN=UAi;y3G~)| z=g4i}YEDm^=~e}NoA{J5d4%{qGhmaTqwKe<`A*&^Mt}8gD0Sm*Df#Cs|J~&*8=~N? zeB3rA&Y){s_}&z$JAzH@dUH(wN^N0e;B8Njow zLn*rZzf2yw0ewiN!AY+g`3GedV(Y z9aONLVXWFTLT!&!EHa}R#`lb9pn9R00TzmwE`VRgXL;|nFDsv!VZnBZS+?y0Swb)S+M(@=s?#=sU(#IW?l%?aoW&hDY%+L=Ka;batLY-(CC-Szrb4NRMCOFs zZ;X_67tP4)-zlAG5xaNEsqy%i+(7)f3dgM^X+yo7;2%`w%b{~9Hhtu9weNG=Y+oc* zqDC6KLc77+In|z=on_0WE4Op^nn*n8T_)pJA9vLt0LG16kTebS%|x-$KzP8C{DG|q zlu-{wev8~MUkQM)CSGW~xz$@xsWnlf1v~Ec{Bf&RQfUtO1vWyMDNmn`ttA5%b}(L* z&0jUI%dI;yN_b@X8%BTDzrZT_7~Xic*{HBF#gcwH`?d-hArlr6M1Pwb6Qzl(`}8el z-Aqky3EyIs%_i&=Nf{!SK^FU|3CzQTzS0Ev__oXbdkqbtGb?gGHn@lWa?6jf)K!^R zk?JNa-#L^o%q!XzU@GCc-xogJ`e%7Fu-ACFC--}5gw@rUnHa*&*34@O1s?``Uj)k3 zL7lG&Vt1n=5>SuIX?ypBWEqlqsC{BPI*aODw&f<{zRdt2cn`Fcm=Y=9SQ21Hy4_jv z3I(06f|QT|Rk9>7Z{C6o`<5z^lP9d~{}jmEW{7v+5S;1X~bDq_ijRI4!2Vbis%r+BRCi1zQ+CoXEBb)jHHP|*$B?PaNqcE`LL zJk)mkBRnxE&~I@P{h;#NL||36s}9h#w4AG1tQV}fCLWOMqrkG5?vFp!W;oo^GF|=+ zH9l%SIa*|67PfIeNCRM|B{WiJo0-SJtT+85B;#XF%0hB9Csv}4WoU@_(&q8ajk;>x z0;k6fQl$6uz74i(%WB95Nn+Lz(Z%N82rTNFt#~gxso@1=Ofk+S(7HjsI!2M+mh9%D za!yx)7A8*@$j1Q`{dkCQRtI%io*1S;Y*_3v=Jv(IB_m!96i~nd`6R-UPQ=pQMEtoK zW2DO{ZT^wt;x#M2%k@>y?kR8DR=m=-bpMY>YwJDqy#gf>AbAfD#)~8#13)y9#8w!< zC=7jr>m~`>;wk!;cUw%&lTLpmBy_#@k57&i+jk#sn$^>YHU|%MWl$*V|B~UdHpcX{Z z5o0+)jVg7}0Nlv%Xm{Y$gcAGYJxJ>TTv-0|cfoJE1G&uo2M7|j%UoAvj0kmhsxT0-r3J_2PRj4&{$YotyvR!q$|E8qrbCSPNHQrn>%@1Uq8IP+TvE34=Jtin3mnDzkTXH$}zC>FOW5rJb-1#0^^0LLO zBlKBQ;DOShq=s}qup(8Ubbz8rrznv2#IjyGI1(Ti5GJ~`--pG;=w3}`kSx&-&24;yn$ORDXsK(kKx)K8 z&$2d$0g77^(lflzwX+rmGb$}m9pR^|q@x2sVOD+NG;_Locx>+tcOMm-&n zX)5H(C}V2C_tNNeFivAaiBhX0H0#zT?yiL=T{552Xuz`r<|YlN8Ip~M2>6)eqcEdX zXVe-GnT+u02wf1tH=|Kv zK-tX|fr%d7xpWyWCGkM`L@WGqyG|2sWp=UMaENWn_BcKhUdj|TK5x;S@z7#u6pw${ zVzWFNY2EnjG^IB6HHzIPOWi|3O^%?_OqK}|V0;NFeUA3N2JmC2aN;7I2TPutOn-*B z>%6?DV7aW|MWIW*I|O+re=%P1qR7XaQdVuC;1H(Z)?jl%UXgR=L{{mmiyK=PS1{=`)*Bei1A1(V=T>H--u^u1n&5{-GTVa|3$y+QCo+qUsk}z79 zI0gWuiWC*nL8Nqm5gpaR#sXdZAweXoZ53Q70;Bbobs#}&f=Ws$hEv*=nAj&G_?>oyP#o(a|tj)*E$~a1ZLcVnOiYBk$ zVztF3ukGOc)xd#S+1!=`Q*QT|+PH^ia$rgzodP|0Nw|)e?q{IK$>3v@J>Z@xsv4-X zI!O`x4~0oE4VDI@F*tNilHC=c4$>~(@G)eEblx;z%2?)vY@E2tVbu} ztVZ9Ye^PFZ!DLrv|18G@vE<4~vT&B1M;OL~pgRfx67s`@P z@>WT9kuq6UmiuvtzX`KBTW|yViI_{u(;zx3EfrZ@JGW}*qx3r9mSf0sr_M8%uDP5a z|DiktR-X%Fy=i;LM7`fatKfbW6MhxvX<^#K=t_pzNI^TInj46yWbt&@q9Wgt+1Ax4ftpt#-?#YFL@9Mk=%xxf9etKtu5|i%y2XY-o_2Jrk&IDASmStz~b3D45 zAA3&+!6l>C$x$WBS}`qfYeFG)oY?hK-$6oZu}k~IW6ggXnbU~V42@{ZM&Qwv_}!C| zknvHiKJ>VRo|`KwLIagLF3Y7_kbtJ6`W0@Ajbr`C{dUpoVq%iPK1vmc80d>U)VQd6 z&N*OLBXnIDVF_m%or4u^m_DK&n^++ag&$poXx%em*-tg~Xv{1|X$^0kwP>{H3J0jz z6k6Oqz_skBe9ax4P^5}==RM+0)rO9l-Gkj21={i-Z%_y4M>y4;N&%F^{ycP2gVGR* z`jRw|mvvkrf>Nkk7|UWo+Y9TYkY5*U0aYjOymw;IPL6pODQ6cQ{!rAiOkY{O^R@Zi z=sy%=+mc5|N`iY!YF1U0VwEZZ$?IYJW+>|48j27vi$2%f%tzdN7Iu%~uD+{!Blm|Q zjUlE$mFE4O_&2cw0Ob%9)fR8p4V9{ww=P^(akvtU;jxsU!N^ew>iJcrhXUGJH`RWY z>bO6(#7lK3Y@>*Sj$ql8@L}w9<<3H}kB(Y0Lq(O<{jZ~b{tx-pGzFdWo$plus2A4f*@^I-pHjq=c+LZte5s-Y`{Beq$Oh(6Sa_oxkDj9>swpO;ZQ#-Bdf(SS-3 z$>)H&|7qRprdawG`qjbj~ta|HD z7JGU4iT$Z8?rrv{tI10zkr?-mxX>&$+7fnd${0Jd{C=RhVq{r439zQ$f`IZ6>jb6W z3@JJvEY?uh7CbN0oF0$JU!1v>pGIb06^_VPaz<~nwJ>MBr0|le=hJGQa;2}bumh}7 zU9ioiyp z4jVZpbbCOz;;QGq zeA-F}gCym&Cc59;VdLHjFPjjD1YCrwPA#g&GWsdj{vy)0Z*{UvE%0i82-3(z{OG7M0uZ?jz{*Omhn-N_|o&y74Shny-v^59r148 z6-<`?jWtw;6cArdIOm1i@haSt7t5X&S{EwX6vP$y%cH{Haph&yeRf5Ta#sD`G&y5& zhMx3&z6q(DWZCPidU|-m83(jop&ved_hy-4H>o63DXHjWrrO>W8ufvWhm+id?(L$I zRh?RB5>)3*%AD)rN~i`zOJkb|r6zQRLDb|0y+sHUn&q6MztJoN=icR=anvxVpH>vv z)k}4C7fEEP`FF8KsF4a05L9gUAngk3Wfk^Xlg$zqUrb_&Q z3!6X)%d&Oz0n+ReUu_`K}kj3&6Yf{vP`@lH?GpSjTwT<$c| zN~rLg^b$9nnl0xkgByaRhjBexIyhqiYi_VOu5VH=mPJ+c-^uXxFg}sK4bqxgovS}z zn7ZN?eAp@F#JumJ*Uv*!K4w@QyBH>4$jG{IwDMf8C+B5UwVI!BwtXio zqCX=r==}ml{v} zbE2geqUL2goUN8O(4eJn^OrD3(=(wD>O&ioj~<{AXbhN@7T52~hf=i(r=q+r2`@E7 zMb+9WS_U5h8;66hdI$ya86QF=3I3{^--;dMXpyZ{KP-g6HS&u=?au|NGDW}~BZ?oq z0p#o<9I+~<9hJRGmD)@O9`bLHR^yba!l@=+8PPBaBv|SdIg4Hi&}44=)#>?4Dj^$X zIh7n(GC#{U0w5V9@{x~Y3Nz5FL4VnWDK6oQ^dR9!+d@Dx#=f1H-Y`^#H-!+#%&=!o zA4^LXUGo~?=3qVqUD(T|)dIta$;Msq3n8P|6wY?;&w!nvH@>6>Fm4oLrXV|-7%OSlwX~c%FfwIuk!-neO}KxjA-C`{#HuY@`@(!z z`I;v1D1ixGosfQQ>7dt#ILs#Jn1Lc!JDjofO6McW8@pw7H-aC?yZo z8Tn4L8$Iol7YBN{B6bcQT|M>(P`;a_bTBFU`60Q)!$s>t9l0L{frwyv*rC)Su0)Jv z&mwnE&E$g~E^1}~TsQv-zX$snl_=-Vp1LY~^9*;r+|V_oPYZCwP13x((cpsc5a>4|Zfc_2Tr~ z&W3_+Cn@T`Te^|-HtAez)7F2NJy+mEGTVNdNjC&z>3w05i_I#J`ob)#Ca3R6EVHqG z<3|>I8jybC@}6GyGCut>A}TRFeSgiAp7Sii@POP!37a7p<`9R!X+pv+&bMO zgwwxM5DUvPR2-nwHCwoUb6I*j=XyzK3Ou;o_fbnxWArUvW?>uwT0l}dg%zks?7eb( z;Y8Cgv1u|aND?U0qX^6B8qlcAGfdwKrsFstqUPn@_@qKfckf-SBun^t9^V0iCx$+!%j- zT&QBkis&J|SLDB}MKRwY_kAMmkZ7b4(4Y&;Ru+<$xulg?e$dFB- z@k5}NILl<)1Mo0jd2x5>S*O(?CF8T^slE_0+H>HJsh%D)gZ9+9h=x(M5wa8ZaU z;%ZGZlz;NXfu@EG&TAu+duLM`I`m`NGhoaEV6+I9)B`)a?8-2YyN!k=&Bi9*l)a4( zFPwoEFkuEWQYn0mi+;yrw4wXwl&u-cSq!WEMds?gvl ze1?V)7zkES@-|hED0OB#!JLI&S|gUX zVg+qgY5qz>=ciEbzh~3~#m5|o5H?d%R>!?Z>)3-5>Y)n91bis>r|h4-L>|rJ&>u!h zi7xy8A4T{5mvsBK0bBtQP!PclDmZcD#*qmw+&FSqICB<`N}GbXN8mQK+?koVGA%2d zxiVMfs@qvMZ?$n-ncv6fAGm(|jMsS`=lf{0!PcqnD5ji7LN~3{)oO_b>vkv8QSF|3 z_Y)lN4eA^@rS~vVFFF|pttRfJd2xC6+K;k9Q>INFuUgrEA>sI6H}4)ud8y*J%d4A{Or0VDui6RMLj~!Y8si zDEo&hm9-^Vn<^VB%NJyOmgKMe(cG&NXo=>@rYLpzhZYmK~IRm97rSCzPFG{;!_Plse<@L@1M2*@a}{{0Wvlj8cmP)AcD|!c$zFDXPAIirYUWG{=Llmzui$gpbk%+^Viv(FG0|BJv6f20Vdphk3PEk|BW- zXHFGf77U7L6N5Tdc_1*Fb7D*l63fU?L*M)R>f~mvRSchM zx%9jn)+E^P5(mqevLaj`?OnARQRSu*x!IwCv1*5hZr zSyOGx16mN%01{_=pV%ROTk*hrw)W4C8T6Oi2L@CHDVsxvcM>I9I6+PW2Dcc3-7C<; z3lN=67&QgfNf)vlgSqn>7UO|;S43{FfIdnB4O)dfigLX8qimq(Ppw8>YO-+TuXq0D z#A?0mLS~B1NB#a5MCvf`VnP2#7^qhPrWFML+O|>wq5^GJh&HH2CnT&DFz82EQ(<%) zmUaE|b2)|bnLA_MX}R%}jgDKolaDgIxIbe1D@1Rq(uJiBqWu#K!B z`o^8r{ZgGlOnU>+F8=@k6<8Tt#a(w77}FE*&}yh-&rJ@2(rPc&4XZs~J4kUR?zy&9RLi4MS z)4tLUlB25(5RjJ3XWpK(gMU9PrLjkJ_*_M4Cm;!=@6B})qUnt&#SRrBLp$(0rNUAz zk|QwwuK4ONt+PSuKiyH9IX=SEe%D{?`1g zpT;EtVat~Au5D6}W%Z?%u~nKdd6qk^wLH5mGgQ6Yf<`>$tEVeVM2rIBszg>Fi)qheclrolQgE7 z$FYg}0nO%#sSidy-VO59jsEZ%Qr!wG!~r<;w-HF)mnsLEzH?etdLgNwbi?= zWsc;i%oj(q?E7DW(|M{k+Q?r;zNhIxqt@22D$tK0$HTp~L!i@=aOQknmeY6S-p~#4 zZ+4M03aN?r!gzY#9D!8N^jJ&v?p@^dm271+Ym4;lY`Cw zVClY~1;4#qI1CnJ5hbdTi1p)wK~>u4O@>N7ft}z1?8Q& zkX-R($Y-%iB&?DzI8-zja{bf7nPAd8NQ))>Eqft-)p?b3;a%eAs=rdX=F2GeMJ5;f z{+i3}%7Z(%!X*viZfS_69n43|uZJ%LHa~I~G*f#EA=?!>%hC$S5H2HTh$_3(kwoWh za(L>e*^O>et*Nrglrhv$+M-c^d_Xo*6adYzps&?s!h0M;|ajVYh7QQXfi3PQ; zwZtS?ee_u2F~6=cZmsEV^~7nwg6}#YPgbAQ)LpgSKXl#5aia9LyMSPxB~xOvC62I% z!iIg;%$jS+`k@}xB&>6zGEdfYFuUAW!H#oXGnd_1R$wc%qx|Gi)JgY$4)VW{0!kO; zu@>i77kviuEEgFE13AKXsv*pcKi_~)bSB5} zQuI27lw}4I1VA-sgh@D%2vz?{3UEyRfq%{T$Cdb6IRA_0nkNe=VNf0wHps}eO7rgr z;^6@W&PUf1*PD!ChTq`^EvET!qb&2?>E`8RVBTGI*^PYLLE36M#thUB z6Vq_OJ?9sB>$XaDGHG@guo6jr~%sLANK6yBF9laDja zR7~T9VB?>Vhs0@!rEyR9IOT~}SL)7SvD*<{-@N9dnS;^e!go48_gSK!a~(5817XzC z_5pLUObR2@=waRZu&KAt+P}(Pq!tU|2l3TeR0)%f=SE=(>(7-{YMn{@8JKp`!T|t! zhNP_ECV$$|Q<8(|x`@cxfZKQGZTn9rR zU_XVxF1K2Bt6xXf-S2bdA;8Lj%vz|4!QJ)rnfIK5T*C42ATBvNG(HOqc5EQ zjcTOn6A>oh+g7QRrt4=$u$vL4y*;SY-6_B~Z^A?qNbL8P-&KG{swUbzmuubV48!Sa zmutWmc;fq;n8&v*qt$1wTdLyzh(|bl0eW&5-zyTxS%&^hX?aUSuz2!{pd)2SnbHU4 z`8@Z!JL4ysn`o^x;}FxaBd*opmKOqz^7&m8B$j#~r5Pl8+<#-ObL~J}_?l*&%qGFk zFwQ+UBU;GcH>5vl`f-TXCdFm zgmX+$*nXlw^>aGdo=SmN&>@tnO^8bdOX+L1a71EF=}eN~D

NU9e(u@he7O? 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

uvBR5V{f}J-cJ3y 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�p?wYX4M{|;z0QetGSy4BB6qLhTB>RQV~-Ph#j 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 v&=+@&p7_)!bWV9_&7i6OSe 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 HKmY(cCWiQ$ literal 0 HcmV?d00001 diff --git a/coretk/coretk/wallpaper/sample4-bg.jpg b/coretk/coretk/wallpaper/sample4-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b3d29dc60c3c76c56a31a1dde3c280ce1c23fea GIT binary patch literal 201030 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 Date: Tue, 29 Oct 2019 09:04:16 -0700 Subject: [PATCH 034/305] adjust open xml --- coretk/coretk/app.py | 7 +- coretk/coretk/appcache.py | 33 +++++++++ coretk/coretk/coregrpc.py | 13 +--- coretk/coretk/coremenubar.py | 10 ++- coretk/coretk/coretoolbar.py | 45 ++++++------ coretk/coretk/coretoolbarhelp.py | 31 ++++++++ coretk/coretk/graph.py | 37 ++++++---- coretk/coretk/graph_helper.py | 15 +++- coretk/coretk/grpcmanagement.py | 19 ++--- coretk/coretk/menuaction.py | 14 ++-- coretk/coretk/prev_saved_xml.txt | 2 - coretk/coretk/setwallpaper.py | 117 ++++++++++++++++++++++++++----- coretk/coretk/sizeandscale.py | 22 ++++++ 13 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 coretk/coretk/appcache.py create mode 100644 coretk/coretk/coretoolbarhelp.py delete mode 100644 coretk/coretk/prev_saved_xml.txt diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1f85fb9f..644f1eb2 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +import coretk.appcache as appcache import coretk.images as images from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar @@ -13,6 +14,8 @@ from coretk.menuaction import MenuAction class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + appcache.cache_variable(self) + print(self.is_open_xml) self.load_images() self.setup_app() self.menubar = None @@ -51,7 +54,7 @@ class Application(tk.Frame): def create_widgets(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - self.core_editbar = CoreToolbar(self.master, edit_frame, self.menubar) + self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) self.core_editbar.create_toolbar() def draw_canvas(self): @@ -63,7 +66,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - self.core_editbar.update_canvas(self.canvas) + self.core_editbar.canvas = self.canvas scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/appcache.py b/coretk/coretk/appcache.py new file mode 100644 index 00000000..b2f87f71 --- /dev/null +++ b/coretk/coretk/appcache.py @@ -0,0 +1,33 @@ +""" +stores some information helpful for setting starting values for some tables +like size and scale, set wallpaper, etc +""" +import tkinter as tk + + +def cache_variable(application): + # for menubar + application.is_open_xml = False + + application.size_and_scale = None + application.set_wallpaper = None + + # set wallpaper variables + + # canvas id of the wallpaper + application.wallpaper_id = None + + # current image for wallpaper + application.current_wallpaper = None + + # wallpaper option + application.radiovar = tk.IntVar() + application.radiovar.set(1) + + # show grid option + application.show_grid_var = tk.IntVar() + application.show_grid_var.set(1) + + # adjust canvas to image dimension variable + application.adjust_to_dim_var = tk.IntVar() + application.adjust_to_dim_var.set(0) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 047e7c92..9b51116a 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -190,7 +190,7 @@ class CoreGrpc: logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): - sid = None + # sid = None if delete_session is None: sid = self.session_id else: @@ -228,7 +228,6 @@ class CoreGrpc: ip4=interface.ipv4, ip4mask=interface.ip4prefix, ) - # if1 = core_pb2.Interface(id=id1, name=edge.interface_1.name, ip4=edge.interface_1.ipv4, ip4mask=edge.interface_1.ip4prefix) logging.debug("create interface 1 %s", if1) # interface1 = self.interface_helper.create_interface(id1, 0) @@ -241,15 +240,12 @@ class CoreGrpc: ip4=interface.ipv4, ip4mask=interface.ip4prefix, ) - # if2 = core_pb2.Interface(id=id2, name=edge.interface_2.name, ip4=edge.interface_2.ipv4, ip4mask=edge.interface_2.ip4prefix) logging.debug("create interface 2: %s", if2) - # interface2 = self.interface_helper.create_interface(id2, 0) - # response = self.core.add_link(self.session_id, id1, id2, interface1, interface2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) - self.core.get_node_links(self.session_id, id1) + # self.core.get_node_links(self.session_id, id1) # def get_session(self): # response = self.core.get_session(self.session_id) @@ -283,10 +279,7 @@ class CoreGrpc: """ response = self.core.open_xml(file_path) self.session_id = response.session_id - # print("Sessionz") - # self.core.events(self.session_id, self.log_event) - # return response.session_id - # logging.info("coregrpc.py open_xml()", type(response)) + logging.debug("coreprgc.py open_xml(): %s", response.result) def close(self): """ diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index a9780072..1129e8b7 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -66,13 +66,11 @@ class CoreMenubar(object): underline=0, ) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( - label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - ) + # file_menu.add_command( + # label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 + # ) # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command( - label="Save As XML...", command=self.menu_action.file_save_as_xml - ) + file_menu.add_command(label="Save", command=self.menu_action.file_save_as_xml) file_menu.add_separator() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 4e263d28..d46a9791 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -3,12 +3,11 @@ import tkinter as tk from enum import Enum from core.api.grpc import core_pb2 +from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from coretk.graph_helper import WlanConnection - class SessionStateEnum(Enum): NONE = "none" @@ -25,13 +24,14 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, master, edit_frame, menubar): + def __init__(self, application, edit_frame, menubar): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - self.master = master + self.application = application + self.master = application.master self.edit_frame = edit_frame self.menubar = menubar self.radio_value = tk.IntVar() @@ -50,15 +50,6 @@ class CoreToolbar(object): self.canvas = None - def update_canvas(self, canvas): - """ - Update canvas variable in CoreToolbar class - - :param tkinter.Canvas canvas: core canvas - :return: nothing - """ - self.canvas = canvas - def destroy_previous_frame(self): """ Destroy any extra frame from previous before drawing a new one @@ -169,6 +160,7 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click START STOP SESSION button") + helper = CoreToolbarHelp(self.application) # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT @@ -176,24 +168,29 @@ class CoreToolbar(object): # set configuration state state = self.canvas.core_grpc.get_session_state() - if state == core_pb2.SessionState.SHUTDOWN: + if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + self.application.is_open_xml = False self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - for node in self.canvas.grpc_manager.nodes.values(): - self.canvas.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - ) - - for edge in self.canvas.grpc_manager.edges.values(): - self.canvas.core_grpc.add_link( - edge.id1, edge.id2, edge.type1, edge.type2, edge - ) + helper.add_nodes() + helper.add_edges() + # for node in self.canvas.grpc_manager.nodes.values(): + # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) + # self.canvas.core_grpc.add_node( + # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + # ) + # print(len(self.canvas.grpc_manager.edges)) + # for edge in self.canvas.grpc_manager.edges.values(): + # print(edge.id1, edge.id2, edge.type1, edge.type2) + # self.canvas.core_grpc.add_link( + # edge.id1, edge.id2, edge.type1, edge.type2, edge + # ) self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) - # self.canvas.core_grpc.get_session() + # self.application.is_open_xml = False self.create_runtime_toolbar() def click_link_tool(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py new file mode 100644 index 00000000..a02c7ae9 --- /dev/null +++ b/coretk/coretk/coretoolbarhelp.py @@ -0,0 +1,31 @@ +""" +CoreToolbar help to draw on canvas, and make grpc client call +""" + + +class CoreToolbarHelp: + def __init__(self, application): + self.application = application + self.core_grpc = application.core_grpc + + def add_nodes(self): + """ + add the nodes stored in grpc manager + :return: nothing + """ + grpc_manager = self.application.canvas.grpc_manager + for node in grpc_manager.nodes.values(): + self.application.core_grpc.add_node( + node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + ) + + def add_edges(self): + """ + add the edges stored in grpc manager + :return: + """ + grpc_manager = self.application.canvas.grpc_manager + for edge in grpc_manager.edges.values(): + self.application.core_grpc.add_link( + edge.id1, edge.id2, edge.type1, edge.type2, edge + ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9f4b87db..4107401b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -43,7 +43,7 @@ class CanvasGraph(tk.Canvas): self.core_grpc = grpc self.grpc_manager = GrpcManager(grpc) - self.helper = GraphHelper(self) + self.helper = GraphHelper(self, grpc) # self.core_id_to_canvas_id = {} # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() @@ -61,7 +61,8 @@ class CanvasGraph(tk.Canvas): :return: """ # delete any existing drawn items - self.delete_components() + # self.delete_components() + self.helper.delete_canvas_components() # set the private variables to default value self.mode = GraphMode.SELECT @@ -73,12 +74,14 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None + print("graph.py create a new grpc manager") self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc - + print("grpah.py draw existing component") self.draw_existing_component() + print(self.grpc_manager.edges) def setup_bindings(self): """ @@ -130,6 +133,8 @@ class CanvasGraph(tk.Canvas): for node in session.nodes: # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: + + # draw nodes on the canvas image, name = Images.convert_type_and_model_to_image( node.type, node.model ) @@ -138,7 +143,10 @@ class CanvasGraph(tk.Canvas): ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id + + # store the node in grpc manager self.grpc_manager.add_preexisting_node(n, session_id, node, name) + self.grpc_manager.update_reusable_id() # draw existing links @@ -165,6 +173,9 @@ class CanvasGraph(tk.Canvas): self, is_wired=False, ) + edge_token = tuple(sorted((n1.id, n2.id))) + e.token = edge_token + e.dst = n2.id n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e @@ -195,8 +206,8 @@ class CanvasGraph(tk.Canvas): ) # TODO will include throughput and ipv6 in the future - if1 = Interface(grpc_if1.name, grpc_if1.ip4) - if2 = Interface(grpc_if2.name, grpc_if2.ip4) + if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) + if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) self.grpc_manager.edges[e.token].interface_1 = if1 self.grpc_manager.edges[e.token].interface_2 = if2 self.grpc_manager.nodes[ @@ -207,14 +218,16 @@ class CanvasGraph(tk.Canvas): ].interfaces.append(if2) # lift the nodes so they on top of the links - for i in core_id_to_canvas_id.values(): + # for i in core_id_to_canvas_id.values(): + # self.lift(i) + for i in self.find_withtag("node"): self.lift(i) - def delete_components(self): - tags = ["node", "edge", "linkinfo", "nodename"] - for i in tags: - for id in self.find_withtag(i): - self.delete(id) + # def delete_components(self): + # tags = ["node", "edge", "linkinfo", "nodename"] + # for i in tags: + # for id in self.find_withtag(i): + # self.delete(id) def canvas_xy(self, event): """ @@ -237,7 +250,6 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) - print(overlapping) nodes = set(self.find_withtag("node")) selected = None for _id in overlapping: @@ -263,7 +275,6 @@ class CanvasGraph(tk.Canvas): self.focus_set() self.selected = self.get_selected(event) logging.debug(f"click release selected: {self.selected}") - print(self.mode) if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 2fc6e011..64f38acf 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -7,13 +7,26 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.images import ImageEnum, Images +CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] + class GraphHelper: - def __init__(self, canvas): + def __init__(self, canvas, grpc): """ create an instance of GraphHelper object """ self.canvas = canvas + self.core_grpc = grpc + + def delete_canvas_components(self): + """ + delete the components of the graph leaving only the blank canvas + + :return: nothing + """ + for tag in CANVAS_COMPONENT_TAGS: + for i in self.canvas.find_withtag(tag): + self.canvas.delete(i) def draw_wireless_case(self, src_id, dst_id, edge): src_node_name = self.canvas.nodes[src_id].node_type diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index ebd34f6c..9638c7c5 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -160,13 +160,17 @@ class GrpcManager: """ Add preexisting nodes to grpc manager + :param str name: node_type :param core_pb2.Node core_node: core node grpc message :param coretk.graph.CanvasNode canvas_node: canvas node :param int session_id: session id :return: nothing """ + + # update the next available id core_id = core_node.id - if core_id >= self.id: + print(core_id) + if self.id is None or core_id >= self.id: self.id = core_id + 1 self.preexisting.append(core_id) n = Node( @@ -198,12 +202,13 @@ class GrpcManager: :return: nothing """ - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) + if len(self.preexisting) > 0: + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) def delete_node(self, canvas_id): """ @@ -263,8 +268,6 @@ class GrpcManager: edge.interface_1 = src_interface edge.interface_2 = dst_interface - print(src_interface) - print(dst_interface) return src_interface, dst_interface def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 4a1332d2..999801c2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -37,8 +37,8 @@ def file_reload(): logging.debug("Click file Reload") -def file_save(): - logging.debug("Click file save") +# def file_save(): +# logging.debug("Click file save") def file_save_shortcut(event): @@ -390,6 +390,7 @@ class MenuAction: def file_open_xml(self): logging.info("menuaction.py file_open_xml()") + self.application.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", @@ -413,16 +414,19 @@ class MenuAction: self.application.core_grpc = core_grpc self.application.core_editbar.destroy_children_widgets() - self.application.core_editbar.create_runtime_toolbar() + self.application.core_editbar.create_toolbar() + # self.application.is_open_xml = False + + # self.application.core_editbar.create_runtime_toolbar() # self.application.canvas.draw_existing_component() # t1 = time.clock() # print(t1 - t0) def canvas_size_and_scale(self): - SizeAndScale(self.application) + self.application.size_and_scale = SizeAndScale(self.application) def canvas_set_wallpaper(self): - CanvasWallpaper(self.application) + self.application.set_wallpaper = CanvasWallpaper(self.application) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") diff --git a/coretk/coretk/prev_saved_xml.txt b/coretk/coretk/prev_saved_xml.txt deleted file mode 100644 index bfb9d15a..00000000 --- a/coretk/coretk/prev_saved_xml.txt +++ /dev/null @@ -1,2 +0,0 @@ -/home/ncs/Desktop/runningtest.xml -/home/ncs/Desktop/notrunning.xml diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index a4a8c908..beec0162 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -14,6 +14,7 @@ WALLPAPER_DIR = os.path.join(PATH, "wallpaper") class ScaleOption(enum.Enum): + NONE = 0 UPPER_LEFT = 1 CENTERED = 2 SCALED = 3 @@ -22,15 +23,23 @@ class ScaleOption(enum.Enum): class CanvasWallpaper: def __init__(self, application): + """ + create an instance of CanvasWallpaper object + + :param coretk.app.Application application: root application + """ self.application = application self.canvas = self.application.canvas self.top = tk.Toplevel() self.top.title("Set Canvas Wallpaper") self.radiovar = tk.IntVar() + print(self.application.radiovar.get()) + self.radiovar.set(self.application.radiovar.get()) self.show_grid_var = tk.IntVar() + self.show_grid_var.set(self.application.show_grid_var.get()) self.adjust_to_dim_var = tk.IntVar() - self.wallpaper = None + self.adjust_to_dim_var.set(self.application.adjust_to_dim_var.get()) self.create_image_label() self.create_text_label() @@ -75,6 +84,11 @@ class CanvasWallpaper: img_label.image = tk_img def clear_link(self): + """ + delete like shown in image link entry if there is any + + :return: nothing + """ # delete entry img_open_frame = self.top.grid_slaves(2, 0)[0] filename_entry = img_open_frame.grid_slaves(0, 0)[0] @@ -115,10 +129,28 @@ class CanvasWallpaper: b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) b4.grid(row=0, column=3) - self.radiovar.set(1) + # self.radiovar.set(1) f.grid() + def adjust_canvas_size(self): + + # deselect all radio buttons and grey them out + if self.adjust_to_dim_var.get() == 1: + self.radiovar.set(0) + option_frame = self.top.grid_slaves(3, 0)[0] + for i in option_frame.grid_slaves(): + i.config(state=tk.DISABLED) + + # turn back the radio button to active state so that user can choose again + elif self.adjust_to_dim_var.get() == 0: + option_frame = self.top.grid_slaves(3, 0)[0] + for i in option_frame.grid_slaves(): + i.config(state=tk.NORMAL) + self.radiovar.set(1) + else: + logging.error("setwallpaper.py adjust_canvas_size invalid value") + def additional_options(self): b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) b.grid(sticky=tk.W, padx=5) @@ -126,15 +158,21 @@ class CanvasWallpaper: self.top, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, + command=self.adjust_canvas_size, ) b.grid(sticky=tk.W, padx=5) self.show_grid_var.set(1) self.adjust_to_dim_var.set(0) - def delete_previous_wallpaper(self): - prev_wallpaper = self.canvas.find_withtag("wallpaper") - if prev_wallpaper: - for i in prev_wallpaper: + def delete_canvas_components(self, tag_list): + """ + delete canvas items whose tag is in the tag list + + :param list[string] tag_list: list of tags + :return: nothing + """ + for tag in tag_list: + for i in self.canvas.find_withtag(tag): self.canvas.delete(i) def get_canvas_width_and_height(self): @@ -159,6 +197,7 @@ class CanvasWallpaper: return def upper_left(self, img): + print("upperleft") tk_img = ImageTk.PhotoImage(img) # crop image if it is bigger than canvas @@ -178,7 +217,8 @@ class CanvasWallpaper: # place left corner of image to the left corner of the canvas self.application.croppedwallpaper = cropped_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" @@ -213,7 +253,8 @@ class CanvasWallpaper: # place the center of the image at the center of the canvas self.application.croppedwallpaper = cropped_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) @@ -231,7 +272,8 @@ class CanvasWallpaper: image_tk = ImageTk.PhotoImage(resized_image) self.application.croppedwallpaper = image_tk - self.delete_previous_wallpaper() + self.delete_canvas_components(["wallpaper"]) + # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" @@ -241,11 +283,34 @@ class CanvasWallpaper: def tiled(self, img): return + def draw_new_canvas(self, canvas_width, canvas_height): + """ + delete the old canvas and draw a new one + + :param int canvas_width: canvas width in pixel + :param int canvas_height: canvas height in pixel + :return: + """ + self.delete_canvas_components(["rectangle", "gridline"]) + self.canvas.draw_grid(canvas_width, canvas_height) + + def canvas_to_image_dimension(self, img): + image_tk = ImageTk.PhotoImage(img) + img_w = image_tk.width() + img_h = image_tk.height() + self.delete_canvas_components(["wallpaper"]) + self.draw_new_canvas(img_w, img_h) + wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) + self.application.croppedwallpaper = image_tk + self.application.wallpaper_id = wid + def show_grid(self): """ :return: nothing """ + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + if self.show_grid_var.get() == 0: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.HIDDEN) @@ -256,28 +321,46 @@ class CanvasWallpaper: else: logging.error("setwallpaper.py show_grid invalid value") + def save_wallpaper_options(self): + self.application.radiovar.set(self.radiovar.get()) + self.application.show_grid_var.set(self.show_grid_var.get()) + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + def click_apply(self): img_link_frame = self.top.grid_slaves(2, 0)[0] filename = img_link_frame.grid_slaves(0, 0)[0].get() if not filename: + self.delete_canvas_components(["wallpaper"]) self.top.destroy() + self.application.current_wallpaper = None + self.save_wallpaper_options() return try: img = Image.open(filename) + self.application.current_wallpaper = img except FileNotFoundError: print("invalid filename, draw original white plot") if self.application.wallpaper_id: self.canvas.delete(self.application.wallpaper_id) self.top.destroy() return - if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: - self.upper_left(img) - elif self.radiovar.get() == ScaleOption.CENTERED.value: - self.center(img) - elif self.radiovar.get() == ScaleOption.SCALED.value: - self.scaled(img) - elif self.radiovar.get() == ScaleOption.TILED.value: - print("not implemented yet") + + self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + if self.adjust_to_dim_var.get() == 0: + + self.application.radiovar.set(self.radiovar.get()) + + if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + self.upper_left(img) + elif self.radiovar.get() == ScaleOption.CENTERED.value: + self.center(img) + elif self.radiovar.get() == ScaleOption.SCALED.value: + self.scaled(img) + elif self.radiovar.get() == ScaleOption.TILED.value: + print("not implemented yet") + + elif self.adjust_to_dim_var.get() == 1: + self.canvas_to_image_dimension(img) self.show_grid() self.top.destroy() diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index 44d3adbc..3552c2e5 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -4,6 +4,8 @@ size and scale import tkinter as tk from functools import partial +from coretk.setwallpaper import ScaleOption + DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] @@ -246,6 +248,26 @@ class SizeAndScale: meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 self.application.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid(pixel_width, pixel_height) + print(self.application.current_wallpaper) + print(self.application.radiovar) + # if there is a current wallpaper showing, redraw it based on current wallpaper options + wallpaper_tool = self.application.set_wallpaper + current_wallpaper = self.application.current_wallpaper + if current_wallpaper: + if self.application.adjust_to_dim_var.get() == 0: + if self.application.radiovar.get() == ScaleOption.UPPER_LEFT.value: + wallpaper_tool.upper_left(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.CENTERED.value: + wallpaper_tool.center(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.SCALED.value: + wallpaper_tool.scaled(current_wallpaper) + elif self.application.radiovar.get() == ScaleOption.TILED.value: + print("not implemented") + elif self.application.adjust_to_dim_var.get() == 1: + wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + + wallpaper_tool.show_grid() + self.top.destroy() def apply_cancel(self): From 46127b44f9c58da991b9618d6dc5b62144cfd21f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 30 Oct 2019 13:33:22 -0700 Subject: [PATCH 035/305] more work on configurations --- coretk/coretk/canvasaction.py | 38 ++++++ coretk/coretk/graph.py | 88 +++++++++---- coretk/coretk/grpcmanagement.py | 1 - coretk/coretk/imagemodification.py | 91 +++++++++++++ coretk/coretk/nodeconfigtable.py | 69 +++++----- coretk/coretk/serviceconfiguration.py | 10 ++ coretk/coretk/wlanconfiguration.py | 181 ++++++++++++++++++++++++++ 7 files changed, 419 insertions(+), 59 deletions(-) create mode 100644 coretk/coretk/canvasaction.py create mode 100644 coretk/coretk/imagemodification.py create mode 100644 coretk/coretk/serviceconfiguration.py create mode 100644 coretk/coretk/wlanconfiguration.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py new file mode 100644 index 00000000..addd2cd8 --- /dev/null +++ b/coretk/coretk/canvasaction.py @@ -0,0 +1,38 @@ +""" +canvas graph action +""" + +# import tkinter as tk + +from core.api.grpc import core_pb2 +from coretk.nodeconfigtable import NodeConfig +from coretk.wlanconfiguration import WlanConfiguration + +NODE_TO_TYPE = { + "router": core_pb2.NodeType.DEFAULT, + "wlan": core_pb2.NodeType.WIRELESS_LAN, +} + + +class CanvasAction: + def __init__(self, master, canvas): + self.master = master + + self.canvas = canvas + self.node_to_show_config = None + + def display_configuration(self, canvas_node): + pb_type = NODE_TO_TYPE[canvas_node.node_type] + self.node_to_show_config = canvas_node + if pb_type == core_pb2.NodeType.DEFAULT: + self.display_node_configuration() + elif pb_type == core_pb2.NodeType.WIRELESS_LAN: + self.display_wlan_configuration() + + def display_node_configuration(self): + NodeConfig(self.canvas, self.node_to_show_config) + self.node_to_show_config = None + + def display_wlan_configuration(self): + WlanConfiguration(self.canvas, self.node_to_show_config) + self.node_to_show_config = None diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4107401b..dd3bbc1c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -3,12 +3,14 @@ import logging import tkinter as tk from core.api.grpc import core_pb2 +from coretk.canvasaction import CanvasAction from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo -from coretk.nodeconfigtable import NodeConfig + +# from coretk.nodeconfigtable import NodeConfig class GraphMode(enum.Enum): @@ -19,6 +21,12 @@ class GraphMode(enum.Enum): OTHER = 4 +CORE_NODES = ["router"] +CORE_WIRED_NETWORK_NODES = [] +CORE_WIRELESS_NODE = ["wlan"] +CORE_EMANE = ["emane"] + + class CanvasGraph(tk.Canvas): def __init__(self, master=None, grpc=None, cnf=None, **kwargs): if cnf is None: @@ -36,6 +44,7 @@ class CanvasGraph(tk.Canvas): self.grid = None self.meters_per_pixel = 1.5 + self.canvas_action = CanvasAction(master, self) self.setup_menus() self.setup_bindings() self.draw_grid() @@ -44,15 +53,30 @@ class CanvasGraph(tk.Canvas): self.grpc_manager = GrpcManager(grpc) self.helper = GraphHelper(self, grpc) + self.is_node_context_opened = False # self.core_id_to_canvas_id = {} # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() + def test(self): + print("testing the button") + print(self.node_context.winfo_rootx()) + 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") + self.node_context.add_command( + label="Configure", command=self.canvas_action.display_node_configuration + ) + self.node_context.add_command(label="Select adjacent") + self.node_context.add_command(label="Create link to") + self.node_context.add_command(label="Assign to") + self.node_context.add_command(label="Move to") + self.node_context.add_command(label="Cut") + self.node_context.add_command(label="Copy") + self.node_context.add_command(label="Paste") + self.node_context.add_command(label="Delete") + self.node_context.add_command(label="Hide") + self.node_context.add_command(label="Services") def canvas_reset_and_redraw(self, new_grpc): """ @@ -74,14 +98,11 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None - print("graph.py create a new grpc manager") self.grpc_manager = GrpcManager(new_grpc) # new grpc self.core_grpc = new_grpc - print("grpah.py draw existing component") self.draw_existing_component() - print(self.grpc_manager.edges) def setup_bindings(self): """ @@ -218,8 +239,6 @@ class CanvasGraph(tk.Canvas): ].interfaces.append(if2) # lift the nodes so they on top of the links - # for i in core_id_to_canvas_id.values(): - # self.lift(i) for i in self.find_withtag("node"): self.lift(i) @@ -272,16 +291,20 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - self.focus_set() - self.selected = self.get_selected(event) - logging.debug(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, self.draw_node_image, self.draw_node_name) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + if self.is_node_context_opened: + self.node_context.unpost() + self.is_node_context_opened = False + else: + self.focus_set() + self.selected = self.get_selected(event) + logging.debug(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, self.draw_node_image, self.draw_node_name) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -367,11 +390,17 @@ class CanvasGraph(tk.Canvas): 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: - logging.debug(f"node context: {selected}") - self.node_context.post(event.x_root, event.y_root) + if not self.is_node_context_opened: + selected = self.get_selected(event) + nodes = self.find_withtag("node") + if selected in nodes: + logging.debug(f"node context: {selected}") + self.node_context.post(event.x_root, event.y_root) + self.canvas_action.node_to_show_config = self.nodes[selected] + self.is_node_context_opened = True + else: + self.node_context.unpost() + self.is_node_context_opened = False def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] @@ -482,8 +511,15 @@ class CanvasNode: if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) else: - print("config table show up") - NodeConfig(self, self.image, self.node_type, self.name) + self.canvas.canvas_action.display_configuration(self) + # if self.node_type in CORE_NODES: + # self.canvas.canvas_action.node_to_show_config = self + # self.canvas.canvas_action.display_node_configuration() + # elif self.node_type in CORE_WIRED_NETWORK_NODES: + # return + # elif self.node_type in CORE_WIRELESS_NODE: + # return + # elif self def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 9638c7c5..4cfc6a16 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -169,7 +169,6 @@ class GrpcManager: # update the next available id core_id = core_node.id - print(core_id) if self.id is None or core_id >= self.id: self.id = core_id + 1 self.preexisting.append(core_id) diff --git a/coretk/coretk/imagemodification.py b/coretk/coretk/imagemodification.py new file mode 100644 index 00000000..646f31b7 --- /dev/null +++ b/coretk/coretk/imagemodification.py @@ -0,0 +1,91 @@ +""" +node image modification +""" + + +import os +import tkinter as tk +from tkinter import filedialog + +from PIL import Image, ImageTk + +PATH = os.path.abspath(os.path.dirname(__file__)) +ICONS_DIR = os.path.join(PATH, "icons") + + +class ImageModification: + def __init__(self, canvas, canvas_node, node_config): + """ + create an instance of ImageModification + :param coretk.graph.CanvasGraph canvas: canvas object + :param coretk.graph.CanvasNode canvas_node: node object + :param coretk.nodeconfigtable.NodeConfig node_config: node configuration object + """ + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name + self.canvas_node = canvas_node + self.node_configuration = node_config + self.p_top = node_config.top + + self.top = tk.Toplevel() + self.top.title(self.name + " image") + self.image_modification() + + def open_icon_dir(self, toplevel, entry_text): + filename = filedialog.askopenfilename( + initialdir=ICONS_DIR, + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if len(filename) > 0: + img = Image.open(filename) + tk_img = ImageTk.PhotoImage(img) + lb = toplevel.grid_slaves(1, 0)[0] + lb.configure(image=tk_img) + lb.image = tk_img + entry_text.set(filename) + + def click_apply(self, toplevel, entry_text): + imgfile = entry_text.get() + if imgfile: + img = Image.open(imgfile) + tk_img = ImageTk.PhotoImage(img) + f = self.p_top.grid_slaves(row=0, column=0)[0] + lb = f.grid_slaves(row=0, column=3)[0] + lb.configure(image=tk_img) + lb.image = tk_img + self.image = tk_img + self.node_configuration.image = tk_img + toplevel.destroy() + + def image_modification(self): + f = tk.Frame(self.top) + entry_text = tk.StringVar() + image_file_label = tk.Label(f, text="Image file: ") + image_file_label.grid(row=0, column=0) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") + image_file_entry.grid(row=0, column=1) + image_file_button = tk.Button( + f, text="...", command=lambda: self.open_icon_dir(self.top, entry_text) + ) + image_file_button.grid(row=0, column=2) + f.grid() + + img = tk.Label(self.top, image=self.image) + img.grid() + + f = tk.Frame(self.top) + apply_button = tk.Button( + f, text="Apply", command=lambda: self.click_apply(self.top, entry_text) + ) + apply_button.grid(row=0, column=0) + apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") + apply_to_multiple_button.grid(row=0, column=1) + cancel_button = tk.Button(f, text="Cancel", command=self.top.destroy) + cancel_button.grid(row=0, column=2) + f.grid() diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py index 76550289..6d03f52f 100644 --- a/coretk/coretk/nodeconfigtable.py +++ b/coretk/coretk/nodeconfigtable.py @@ -8,6 +8,8 @@ from tkinter import filedialog from PIL import Image, ImageTk +from coretk.imagemodification import ImageModification + PATH = os.path.abspath(os.path.dirname(__file__)) ICONS_DIR = os.path.join(PATH, "icons") @@ -16,14 +18,15 @@ DEFAULTNODES = ["router", "host", "PC"] class NodeConfig: - def __init__(self, canvas_node, image, node_type, name): - self.image = image - self.node_type = node_type - self.name = name + def __init__(self, canvas, canvas_node): + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name self.canvas_node = canvas_node self.top = tk.Toplevel() - self.top.title(node_type + " configuration") + self.top.title(canvas_node.node_type + " configuration") self.namevar = tk.StringVar(self.top, value="default name") self.name_and_image_definition() self.type_and_service_definition() @@ -58,65 +61,67 @@ class NodeConfig: toplevel.destroy() def img_modification(self): - print("image modification") t = tk.Toplevel() t.title(self.name + " image") f = tk.Frame(t) entry_text = tk.StringVar() image_file_label = tk.Label(f, text="Image file: ") - image_file_label.pack(side=tk.LEFT, padx=2, pady=2) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=60) - image_file_entry.pack(side=tk.LEFT, padx=2, pady=2) + image_file_label.grid(row=0, column=0) + image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") + image_file_entry.grid(row=0, column=1) image_file_button = tk.Button( f, text="...", command=lambda: self.open_icon_dir(t, entry_text) ) - image_file_button.pack(side=tk.LEFT, padx=2, pady=2) - f.grid(sticky=tk.W + tk.E) + image_file_button.grid(row=0, column=2) + f.grid() img = tk.Label(t, image=self.image) - img.grid(sticky=tk.W + tk.E) + img.grid() f = tk.Frame(t) apply_button = tk.Button( f, text="Apply", command=lambda: self.click_apply(t, entry_text) ) - apply_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_button.grid(row=0, column=0) apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.pack(side=tk.LEFT, padx=2, pady=2) + apply_to_multiple_button.grid(row=0, column=1) cancel_button = tk.Button(f, text="Cancel", command=t.destroy) - cancel_button.pack(side=tk.LEFT, padx=2, pady=2) - f.grid(sticky=tk.E + tk.W) + cancel_button.grid(row=0, column=2) + f.grid() def name_and_image_definition(self): - name_label = tk.Label(self.top, text="Node name: ") - name_label.grid() - name_entry = tk.Entry(self.top, textvariable=self.namevar) - name_entry.grid(row=0, column=1) + f = tk.Frame(self.top, bg="#d9d9d9") + name_label = tk.Label(f, text="Node name: ", bg="#d9d9d9") + name_label.grid(padx=2, pady=2) + name_entry = tk.Entry(f, textvariable=self.namevar) + name_entry.grid(row=0, column=1, padx=2, pady=2) - core_button = tk.Button(self.top, text="None") - core_button.grid(row=0, column=2) + core_button = tk.Button(f, text="None") + core_button.grid(row=0, column=2, padx=2, pady=2) img_button = tk.Button( - self.top, + f, image=self.image, width=40, height=40, - command=self.img_modification, + command=lambda: ImageModification(self.canvas, self.canvas_node, self), + bg="#d9d9d9", ) - img_button.grid(row=0, column=3) + img_button.grid(row=0, column=3, padx=4, pady=4) + f.grid(padx=4, pady=4) def type_and_service_definition(self): f = tk.Frame(self.top) type_label = tk.Label(f, text="Type: ") - type_label.pack(side=tk.LEFT) + type_label.grid(row=0, column=0) type_button = tk.Button(f, text="None") - type_button.pack(side=tk.LEFT) + type_button.grid(row=0, column=1) service_button = tk.Button(f, text="Services...") - service_button.pack(side=tk.LEFT) + service_button.grid(row=0, column=2) - f.grid(row=1, column=1, columnspan=2, sticky=tk.W) + f.grid(padx=2, pady=2) def config_apply(self): """ @@ -140,10 +145,10 @@ class NodeConfig: def select_definition(self): f = tk.Frame(self.top) apply_button = tk.Button(f, text="Apply", command=self.config_apply) - apply_button.pack(side=tk.LEFT) + apply_button.grid(row=0, column=0) cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) - cancel_button.pack(side=tk.LEFT) - f.grid(row=3, column=1, sticky=tk.W) + cancel_button.grid(row=0, column=1) + f.grid() def network_node_config(self): self.name_and_image_definition() diff --git a/coretk/coretk/serviceconfiguration.py b/coretk/coretk/serviceconfiguration.py new file mode 100644 index 00000000..a539f1fb --- /dev/null +++ b/coretk/coretk/serviceconfiguration.py @@ -0,0 +1,10 @@ +""" +service configuration +""" + +# import tkinter as tk + + +class ServiceConfiguration: + def __init__(self): + return diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py new file mode 100644 index 00000000..53e98ea5 --- /dev/null +++ b/coretk/coretk/wlanconfiguration.py @@ -0,0 +1,181 @@ +""" +wlan configuration +""" + +import ast +import tkinter as tk +from functools import partial + + +class WlanConfiguration: + def __init__(self, canvas, canvas_node): + + self.canvas = canvas + self.image = canvas_node.image + self.node_type = canvas_node.node_type + self.name = canvas_node.name + self.canvas_node = canvas_node + + self.top = tk.Toplevel() + self.top.title("wlan configuration") + self.node_name = tk.StringVar() + + self.range_var = tk.DoubleVar() + self.range_var.set(275.0) + self.bandwidth_var = tk.IntVar() + self.bandwidth_var.set(54000000) + + self.delay_var = tk.StringVar() + + self.image_modification() + self.wlan_configuration() + self.subnet() + self.wlan_options() + self.config_option() + + def image_modification(self): + f = tk.Frame(self.top, bg="#d9d9d9") + lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") + lbl.grid(row=0, column=0, padx=3, pady=3) + e = tk.Entry(f, textvariable=self.node_name, bg="white") + e.grid(row=0, column=1, padx=3, pady=3) + b = tk.Button(f, text="None") + b.grid(row=0, column=2, padx=3, pady=3) + b = tk.Button(f, text="not implemented") + b.grid(row=0, column=3, padx=3, pady=3) + f.grid(padx=2, pady=2, ipadx=2, ipady=2) + + def create_string_var(self, val): + v = tk.StringVar() + v.set(val) + return v + + def scrollbar_command(self, entry_widget, delta, event): + try: + value = int(entry_widget.get()) + except ValueError: + value = ast.literal_eval(entry_widget.get()) + entry_widget.delete(0, tk.END) + if event == "-1": + entry_widget.insert(tk.END, str(round(value + delta, 1))) + elif event == "1": + entry_widget.insert(tk.END, str(round(value - delta, 1))) + + def wlan_configuration(self): + lbl = tk.Label(self.top, text="Wireless") + lbl.grid(sticky=tk.W, padx=3, pady=3) + + f = tk.Frame( + self.top, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + bg="#d9d9d9", + ) + + lbl = tk.Label( + f, + text="The basic range model calculates on/off connectivity based on pixel distance between nodes.", + bg="#d9d9d9", + ) + lbl.grid(padx=4, pady=4) + + f1 = tk.Frame(f, bg="#d9d9d9") + + lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") + lbl.grid(row=0, column=0) + + e = tk.Entry(f1, textvariable=self.range_var, width=5, bg="white") + e.grid(row=0, column=1) + + lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") + lbl.grid(row=0, column=2) + + f11 = tk.Frame(f1, bg="#d9d9d9") + sb = tk.Scrollbar(f11, orient=tk.VERTICAL) + e = tk.Entry(f11, textvariable=self.bandwidth_var, width=10, bg="white") + sb.config(command=partial(self.scrollbar_command, e, 1000000)) + e.grid() + sb.grid(row=0, column=1) + f11.grid(row=0, column=3) + + # e = tk.Entry(f1, textvariable=self.bandwidth_var, width=10) + # e.grid(row=0, column=4) + f1.grid(sticky=tk.W, padx=4, pady=4) + + f2 = tk.Frame(f, bg="#d9d9d9") + lbl = tk.Label(f2, text="Delay (us): ", bg="#d9d9d9") + lbl.grid(row=0, column=0) + + f21 = tk.Frame(f2, bg="#d9d9d9") + sb = tk.Scrollbar(f21, orient=tk.VERTICAL) + e = tk.Entry(f21, textvariable=self.create_string_var(20000), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 5000)) + e.grid() + sb.grid(row=0, column=1) + f21.grid(row=0, column=1) + + lbl = tk.Label(f2, text="Loss (%): ", bg="#d9d9d9") + lbl.grid(row=0, column=2) + + f22 = tk.Frame(f2, bg="#d9d9d9") + sb = tk.Scrollbar(f22, orient=tk.VERTICAL) + e = tk.Entry(f22, textvariable=self.create_string_var(0), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 0.1)) + e.grid() + sb.grid(row=0, column=1) + f22.grid(row=0, column=3) + + # e = tk.Entry(f2, textvariable=self.create_string_var(0)) + # e.grid(row=0, column=3) + f2.grid(sticky=tk.W, padx=4, pady=4) + + f3 = tk.Frame(f, bg="#d9d9d9") + lbl = tk.Label(f3, text="Jitter (us): ", bg="#d9d9d9") + lbl.grid() + f31 = tk.Frame(f3, bg="#d9d9d9") + sb = tk.Scrollbar(f31, orient=tk.VERTICAL) + e = tk.Entry(f31, textvariable=self.create_string_var(0), bg="white") + sb.config(command=partial(self.scrollbar_command, e, 5000)) + e.grid() + sb.grid(row=0, column=1) + f31.grid(row=0, column=1) + + f3.grid(sticky=tk.W, padx=4, pady=4) + f.grid(padx=3, pady=3) + + def subnet(self): + f = tk.Frame(self.top) + f1 = tk.Frame(f) + lbl = tk.Label(f1, text="IPv4 subnet") + lbl.grid() + e = tk.Entry(f1, width=30, bg="white") + e.grid(row=0, column=1) + f1.grid() + + f2 = tk.Frame(f) + lbl = tk.Label(f2, text="IPv6 subnet") + lbl.grid() + e = tk.Entry(f2, width=30, bg="white") + e.grid(row=0, column=1) + f2.grid() + f.grid(sticky=tk.W, padx=3, pady=3) + + def wlan_options(self): + f = tk.Frame(self.top) + b = tk.Button(f, text="ns-2 mobility script...") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(f, text="Link to all routers") + b.pack(side=tk.LEFT, padx=1) + b = tk.Button(f, text="Choose WLAN members") + b.pack(side=tk.LEFT, padx=1) + f.grid(sticky=tk.W) + + def config_option(self): + f = tk.Frame(self.top, bg="#d9d9d9") + b = tk.Button(f, text="Apply", bg="#d9d9d9") + b.grid(padx=2, pady=2) + b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) + b.grid(row=0, column=1, padx=2, pady=2) + f.grid(padx=4, pady=4) From 891e9aef9af1befefc2cdc1f8aaeb72b3b9614a7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 14:06:50 -0700 Subject: [PATCH 036/305] initial add with a common dialog class and leveraging it for a session options dialog --- coretk/coretk/configutils.py | 52 +++++++++++++++++++++++++ coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/__init__.py | 0 coretk/coretk/dialogs/dialog.py | 23 +++++++++++ coretk/coretk/dialogs/sessionoptions.py | 35 +++++++++++++++++ coretk/coretk/menuaction.py | 10 +++-- 6 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 coretk/coretk/configutils.py create mode 100644 coretk/coretk/dialogs/__init__.py create mode 100644 coretk/coretk/dialogs/dialog.py create mode 100644 coretk/coretk/dialogs/sessionoptions.py diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py new file mode 100644 index 00000000..97230561 --- /dev/null +++ b/coretk/coretk/configutils.py @@ -0,0 +1,52 @@ +import enum +import logging +import tkinter as tk +from tkinter import ttk + + +class ConfigType(enum.Enum): + STRING = 10 + BOOL = 11 + + +def create_config(master, config, pad_x=2, pad_y=2): + master.columnconfigure(0, weight=1) + master.columnconfigure(1, weight=3) + values = {} + for index, key in enumerate(sorted(config)): + option = config[key] + label = tk.Label(master, text=option.label) + label.grid(row=index, pady=pad_y, padx=pad_x, sticky="ew") + value = tk.StringVar() + config_type = ConfigType(option.type) + if config_type == ConfigType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox(master, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pad_y) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif config_type == ConfigType.STRING: + entry = tk.Entry(master, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pad_y) + else: + logging.error("unhandled config option type: %s", config_type) + values[key] = value + return values + + +def parse_config(options, values): + config = {} + for key in options: + option = options[key] + value = values[key] + config_type = ConfigType(option.type) + config_value = value.get() + if config_type == ConfigType.BOOL: + if config_value == "On": + config_value = "1" + else: + config_value = "0" + config[key] = config_value + return config diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 1129e8b7..1a558684 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -620,7 +620,7 @@ class CoreMenubar(object): underline=0, ) session_menu.add_command( - label="Options...", command=action.session_options, underline=0 + label="Options...", command=self.menu_action.session_options, underline=0 ) self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) diff --git a/coretk/coretk/dialogs/__init__.py b/coretk/coretk/dialogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py new file mode 100644 index 00000000..9ce421b9 --- /dev/null +++ b/coretk/coretk/dialogs/dialog.py @@ -0,0 +1,23 @@ +import tkinter as tk + +from coretk.images import ImageEnum, Images + + +class Dialog(tk.Toplevel): + def __init__(self, master, title, modal=False): + super().__init__(master, padx=5, pady=5) + image = Images.get(ImageEnum.CORE.value) + self.tk.call("wm", "iconphoto", self._w, image) + self.title(title) + self.protocol("WM_DELETE_WINDOW", self.destroy) + self.withdraw() + self.modal = modal + + def show(self): + self.transient(self.master) + self.focus_force() + if self.modal: + self.grab_set() + self.update() + self.deiconify() + self.wait_window() diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py new file mode 100644 index 00000000..3612dab5 --- /dev/null +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -0,0 +1,35 @@ +import logging +import tkinter as tk + +from coretk import configutils +from coretk.dialogs.dialog import Dialog + +PAD_X = 2 +PAD_Y = 2 + + +class SessionOptionsDialog(Dialog): + def __init__(self, master): + super().__init__(master, "Session Options", modal=True) + self.options = None + self.values = None + self.save_button = tk.Button(self, text="Save", command=self.save) + self.cancel_button = tk.Button(self, text="Cancel", command=self.destroy) + self.draw() + + def draw(self): + session_id = self.master.core_grpc.session_id + response = self.master.core_grpc.core.get_session_options(session_id) + logging.info("session options: %s", response) + self.options = response.config + self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) + row = len(response.config) + self.save_button.grid(row=row, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.cancel_button.grid(row=row, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + + def save(self): + config = configutils.parse_config(self.options, self.values) + session_id = self.master.core_grpc.session_id + response = self.master.core_grpc.core.set_session_options(session_id, config) + logging.info("saved session config: %s", response) + self.destroy() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 999801c2..22329516 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,6 +7,7 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale @@ -314,10 +315,6 @@ def session_emulation_servers(): logging.debug("Click session emulation servers") -def session_options(): - logging.debug("Click session options") - - def help_about(): logging.debug("Click help About") @@ -433,3 +430,8 @@ class MenuAction: def help_core_documentation(self): webbrowser.open_new("http://coreemu.github.io/core/") + + def session_options(self): + logging.debug("Click session options") + dialog = SessionOptionsDialog(self.application) + dialog.show() From c65c846638041214cc8cb77652b59f909ee8f83e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 15:18:49 -0700 Subject: [PATCH 037/305] updated configutils to generate a scrollable view for configurations --- coretk/coretk/configutils.py | 59 +++++++++++++++++++++---- coretk/coretk/dialogs/sessionoptions.py | 5 +-- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index 97230561..fd5263f3 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -9,34 +9,77 @@ class ConfigType(enum.Enum): BOOL = 11 -def create_config(master, config, pad_x=2, pad_y=2): +def create_config(master, config, padx=2, pady=2): + """ + Creates a scrollable canvas with an embedded window for displaying configuration + options. Will use grid layout to consume row 0 and columns 0-2. + + :param master: master to add scrollable canvas to + :param dict config: config option mapping keys to config options + :param int padx: x padding for widgets + :param int pady: y padding for widgets + :return: widget value mapping + """ + master.rowconfigure(0, weight=1) master.columnconfigure(0, weight=1) - master.columnconfigure(1, weight=3) + master.columnconfigure(1, weight=1) + + canvas = tk.Canvas(master) + canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) + canvas.columnconfigure(0, weight=1) + canvas.rowconfigure(0, weight=1) + + scroll_y = tk.Scrollbar(master, orient="vertical", command=canvas.yview) + scroll_y.grid(row=0, column=2, sticky="ns") + + frame = tk.Frame(canvas, padx=padx, pady=pady) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + values = {} for index, key in enumerate(sorted(config)): option = config[key] - label = tk.Label(master, text=option.label) - label.grid(row=index, pady=pad_y, padx=pad_x, sticky="ew") + label = tk.Label(frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="ew") value = tk.StringVar() config_type = ConfigType(option.type) if config_type == ConfigType.BOOL: select = tuple(option.select) - combobox = ttk.Combobox(master, textvariable=value, values=select) - combobox.grid(row=index, column=1, sticky="ew", pady=pad_y) + combobox = ttk.Combobox(frame, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) if option.value == "1": value.set("On") else: value.set("Off") elif config_type == ConfigType.STRING: - entry = tk.Entry(master, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pad_y) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", config_type) values[key] = value + + frame_id = canvas.create_window(0, 0, anchor="nw", window=frame) + canvas.update_idletasks() + canvas.configure(scrollregion=canvas.bbox("all"), yscrollcommand=scroll_y.set) + + frame.bind( + "", lambda event: canvas.configure(scrollregion=canvas.bbox("all")) + ) + canvas.bind( + "", lambda event: canvas.itemconfig(frame_id, width=event.width) + ) + return values def parse_config(options, values): + """ + Given a set of configurations, parse out values and transform them when needed. + + :param dict options: option key mapping to configuration options + :param dict values: option key mapping to widget values + :return: + """ config = {} for key in options: option = options[key] diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 3612dab5..65faae81 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -23,9 +23,8 @@ class SessionOptionsDialog(Dialog): logging.info("session options: %s", response) self.options = response.config self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) - row = len(response.config) - self.save_button.grid(row=row, pady=PAD_Y, padx=PAD_X, sticky="ew") - self.cancel_button.grid(row=row, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.save_button.grid(row=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): config = configutils.parse_config(self.options, self.values) From f0c32304db4d3662b09dac4db70052c30e63d935 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 15:54:38 -0700 Subject: [PATCH 038/305] removed canvas border for configuration options --- coretk/coretk/configutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index fd5263f3..7c87f9dd 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -24,7 +24,7 @@ def create_config(master, config, padx=2, pady=2): master.columnconfigure(0, weight=1) master.columnconfigure(1, weight=1) - canvas = tk.Canvas(master) + canvas = tk.Canvas(master, highlightthickness=0) canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) canvas.columnconfigure(0, weight=1) canvas.rowconfigure(0, weight=1) From 01d3a3158a5e1d192d622eba70770960d6a7002f Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 31 Oct 2019 23:17:26 -0700 Subject: [PATCH 039/305] updated sessions dialog to use common dialog base, cleaned up code and made widgets expandable --- coretk/coretk/app.py | 2 +- coretk/coretk/coregrpc.py | 24 +--- coretk/coretk/coremenubar.py | 2 +- coretk/coretk/coretoolbar.py | 115 ++++++++------- coretk/coretk/dialogs/dialog.py | 11 +- coretk/coretk/dialogs/sessionoptions.py | 4 +- coretk/coretk/dialogs/sessions.py | 159 ++++++++++++++++++++ coretk/coretk/graph_helper.py | 2 +- coretk/coretk/images.py | 26 ++-- coretk/coretk/menuaction.py | 12 +- coretk/coretk/querysessiondrawing.py | 183 ------------------------ 11 files changed, 250 insertions(+), 290 deletions(-) create mode 100644 coretk/coretk/dialogs/sessions.py delete mode 100644 coretk/coretk/querysessiondrawing.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 644f1eb2..f253fd01 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -40,7 +40,7 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") - image = Images.get(ImageEnum.CORE.value) + image = Images.get(ImageEnum.CORE) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 9b51116a..13fe28b3 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -6,8 +6,8 @@ import os from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.dialogs.sessions import SessionsDialog from coretk.linkinfo import Throughput -from coretk.querysessiondrawing import SessionTable from coretk.wirelessconnection import WirelessConnection @@ -18,12 +18,9 @@ class CoreGrpc: """ self.core = client.CoreGrpcClient() self.session_id = sid - self.node_ids = [] - + self.app = app self.master = app.master - - # self.set_up() self.interface_helper = None self.throughput_draw = Throughput(app.canvas, self) self.wireless_draw = WirelessConnection(app.canvas, self) @@ -62,16 +59,6 @@ class CoreGrpc: self.core.events(self.session_id, self.log_event) # self.core.throughputs(self.log_throughput) - def query_existing_sessions(self, sessions): - """ - Query for existing sessions and prompt to join one - - :param repeated core_pb2.SessionSummary sessions: summaries of all the existing sessions - - :return: nothing - """ - SessionTable(self, self.master) - def delete_session(self, custom_sid=None): if custom_sid is None: sid = self.session_id @@ -100,18 +87,15 @@ class CoreGrpc: :return: existing sessions """ self.core.connect() - response = self.core.get_sessions() - # logging.info("coregrpc.py: all sessions: %s", response) # if there are no sessions, create a new session, else join a session sessions = response.sessions - if len(sessions) == 0: self.create_new_session() else: - - self.query_existing_sessions(sessions) + dialog = SessionsDialog(self.app, self.app) + dialog.show() def get_session_state(self): response = self.core.get_session(self.session_id) diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 1a558684..aa00a927 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -594,7 +594,7 @@ class CoreMenubar(object): session_menu.add_command( label="Change sessions...", - command=action.session_change_sessions, + command=self.menu_action.session_change_sessions, underline=0, ) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d46a9791..83831a8a 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -200,55 +200,55 @@ class CoreToolbar(object): def pick_router(self, main_button): logging.debug("Pick router option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.ROUTER.value)) + main_button.configure(image=Images.get(ImageEnum.ROUTER)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER.value) + self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER) self.canvas.draw_node_name = "router" def pick_host(self, main_button): logging.debug("Pick host option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HOST.value)) + main_button.configure(image=Images.get(ImageEnum.HOST)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HOST.value) + self.canvas.draw_node_image = Images.get(ImageEnum.HOST) self.canvas.draw_node_name = "host" def pick_pc(self, main_button): logging.debug("Pick PC option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PC.value)) + main_button.configure(image=Images.get(ImageEnum.PC)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PC.value) + self.canvas.draw_node_image = Images.get(ImageEnum.PC) self.canvas.draw_node_name = "PC" def pick_mdr(self, main_button): logging.debug("Pick MDR option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MDR.value)) + main_button.configure(image=Images.get(ImageEnum.MDR)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MDR.value) + self.canvas.draw_node_image = Images.get(ImageEnum.MDR) self.canvas.draw_node_name = "mdr" def pick_prouter(self, main_button): logging.debug("Pick prouter option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PROUTER.value)) + main_button.configure(image=Images.get(ImageEnum.PROUTER)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER.value) + self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER) self.canvas.draw_node_name = "prouter" def pick_ovs(self, main_button): logging.debug("Pick OVS option") self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVS.value)) + main_button.configure(image=Images.get(ImageEnum.OVS)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.OVS.value) + self.canvas.draw_node_image = Images.get(ImageEnum.OVS) self.canvas.draw_node_name = "OVS" # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EDITNODE.value)) + main_button.configure(image=Images.get(ImageEnum.EDITNODE)) logging.debug("Pick editnode option") def draw_network_layer_options(self, network_layer_button): @@ -262,13 +262,13 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.ROUTER.value), - Images.get(ImageEnum.HOST.value), - Images.get(ImageEnum.PC.value), - Images.get(ImageEnum.MDR.value), - Images.get(ImageEnum.PROUTER.value), - Images.get(ImageEnum.OVS.value), - Images.get(ImageEnum.EDITNODE.value), + Images.get(ImageEnum.ROUTER), + Images.get(ImageEnum.HOST), + Images.get(ImageEnum.PC), + Images.get(ImageEnum.MDR), + Images.get(ImageEnum.PROUTER), + Images.get(ImageEnum.OVS), + Images.get(ImageEnum.EDITNODE), ] func_list = [ self.pick_router, @@ -312,7 +312,7 @@ class CoreToolbar(object): :return: nothing """ - router_image = Images.get(ImageEnum.ROUTER.value) + router_image = Images.get(ImageEnum.ROUTER) network_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -329,41 +329,41 @@ class CoreToolbar(object): def pick_hub(self, main_button): logging.debug("Pick link-layer node HUB") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HUB.value)) + main_button.configure(image=Images.get(ImageEnum.HUB)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HUB.value) + self.canvas.draw_node_image = Images.get(ImageEnum.HUB) self.canvas.draw_node_name = "hub" def pick_switch(self, main_button): logging.debug("Pick link-layer node SWITCH") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.SWITCH.value)) + main_button.configure(image=Images.get(ImageEnum.SWITCH)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH.value) + self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH) self.canvas.draw_node_name = "switch" def pick_wlan(self, main_button): logging.debug("Pick link-layer node WLAN") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.WLAN.value)) + main_button.configure(image=Images.get(ImageEnum.WLAN)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.WLAN.value) + self.canvas.draw_node_image = Images.get(ImageEnum.WLAN) self.canvas.draw_node_name = "wlan" def pick_rj45(self, main_button): logging.debug("Pick link-layer node RJ45") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RJ45.value)) + main_button.configure(image=Images.get(ImageEnum.RJ45)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.RJ45.value) + self.canvas.draw_node_image = Images.get(ImageEnum.RJ45) self.canvas.draw_node_name = "rj45" def pick_tunnel(self, main_button): logging.debug("Pick link-layer node TUNNEL") self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TUNNEL.value)) + main_button.configure(image=Images.get(ImageEnum.TUNNEL)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) + self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL) self.canvas.draw_node_name = "tunnel" def draw_link_layer_options(self, link_layer_button): @@ -377,11 +377,11 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.HUB.value), - Images.get(ImageEnum.SWITCH.value), - Images.get(ImageEnum.WLAN.value), - Images.get(ImageEnum.RJ45.value), - Images.get(ImageEnum.TUNNEL.value), + Images.get(ImageEnum.HUB), + Images.get(ImageEnum.SWITCH), + Images.get(ImageEnum.WLAN), + Images.get(ImageEnum.RJ45), + Images.get(ImageEnum.TUNNEL), ] func_list = [ self.pick_hub, @@ -421,7 +421,7 @@ class CoreToolbar(object): :return: nothing """ - hub_image = Images.get(ImageEnum.HUB.value) + hub_image = Images.get(ImageEnum.HUB) link_layer_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -437,22 +437,22 @@ class CoreToolbar(object): def pick_marker(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MARKER.value)) + main_button.configure(image=Images.get(ImageEnum.MARKER)) logging.debug("Pick MARKER") def pick_oval(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVAL.value)) + main_button.configure(image=Images.get(ImageEnum.OVAL)) logging.debug("Pick OVAL") def pick_rectangle(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RECTANGLE.value)) + main_button.configure(image=Images.get(ImageEnum.RECTANGLE)) logging.debug("Pick RECTANGLE") def pick_text(self, main_button): self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TEXT.value)) + main_button.configure(image=Images.get(ImageEnum.TEXT)) logging.debug("Pick TEXT") def draw_marker_options(self, main_button): @@ -466,10 +466,10 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.MARKER.value), - Images.get(ImageEnum.OVAL.value), - Images.get(ImageEnum.RECTANGLE.value), - Images.get(ImageEnum.TEXT.value), + Images.get(ImageEnum.MARKER), + Images.get(ImageEnum.OVAL), + Images.get(ImageEnum.RECTANGLE), + Images.get(ImageEnum.TEXT), ] func_list = [ self.pick_marker, @@ -498,7 +498,7 @@ class CoreToolbar(object): :return: nothing """ - marker_image = Images.get(ImageEnum.MARKER.value) + marker_image = Images.get(ImageEnum.MARKER) marker_main_button = tk.Radiobutton( self.edit_frame, indicatoron=False, @@ -520,13 +520,13 @@ class CoreToolbar(object): """ self.create_regular_button( self.edit_frame, - Images.get(ImageEnum.START.value), + Images.get(ImageEnum.START), self.click_start_session_tool, "start the session", ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.SELECT.value), + Images.get(ImageEnum.SELECT), self.click_selection_tool, self.radio_value, 1, @@ -534,7 +534,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.LINK.value), + Images.get(ImageEnum.LINK), self.click_link_tool, self.radio_value, 2, @@ -548,7 +548,7 @@ class CoreToolbar(object): def create_observe_button(self): menu_button = tk.Menubutton( self.edit_frame, - image=Images.get(ImageEnum.OBSERVE.value), + image=Images.get(ImageEnum.OBSERVE), width=self.width, height=self.height, direction=tk.RIGHT, @@ -605,13 +605,13 @@ class CoreToolbar(object): def create_runtime_toolbar(self): self.create_regular_button( self.edit_frame, - Images.get(ImageEnum.STOP.value), + Images.get(ImageEnum.STOP), self.click_stop_button, "stop the session", ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.SELECT.value), + Images.get(ImageEnum.SELECT), self.click_selection_tool, self.exec_radio_value, 1, @@ -620,7 +620,7 @@ class CoreToolbar(object): self.create_observe_button() self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.PLOT.value), + Images.get(ImageEnum.PLOT), self.click_plot_button, self.exec_radio_value, 2, @@ -628,7 +628,7 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.MARKER.value), + Images.get(ImageEnum.MARKER), self.click_marker_button, self.exec_radio_value, 3, @@ -636,16 +636,13 @@ class CoreToolbar(object): ) self.create_radio_button( self.edit_frame, - Images.get(ImageEnum.TWONODE.value), + Images.get(ImageEnum.TWONODE), self.click_two_node_button, self.exec_radio_value, 4, "run command from one node to another", ) self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.RUN.value), - self.click_run_button, - "run", + self.edit_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" ) self.exec_radio_value.set(1) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 9ce421b9..b218d8c7 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -4,14 +4,15 @@ from coretk.images import ImageEnum, Images class Dialog(tk.Toplevel): - def __init__(self, master, title, modal=False): + def __init__(self, master, app, title, modal=False): super().__init__(master, padx=5, pady=5) - image = Images.get(ImageEnum.CORE.value) - self.tk.call("wm", "iconphoto", self._w, image) + self.withdraw() + self.app = app + self.modal = modal self.title(title) self.protocol("WM_DELETE_WINDOW", self.destroy) - self.withdraw() - self.modal = modal + image = Images.get(ImageEnum.CORE) + self.tk.call("wm", "iconphoto", self._w, image) def show(self): self.transient(self.master) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 65faae81..a34c6658 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -9,8 +9,8 @@ PAD_Y = 2 class SessionOptionsDialog(Dialog): - def __init__(self, master): - super().__init__(master, "Session Options", modal=True) + def __init__(self, master, app): + super().__init__(master, app, "Session Options", modal=True) self.options = None self.values = None self.save_button = tk.Button(self, text="Save", command=self.save) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py new file mode 100644 index 00000000..10165e2e --- /dev/null +++ b/coretk/coretk/dialogs/sessions.py @@ -0,0 +1,159 @@ +import logging +import tkinter as tk +from tkinter.ttk import Scrollbar, Treeview + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images + + +class SessionsDialog(Dialog): + def __init__(self, master, app): + """ + create session table instance + + :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param root.master master: + """ + super().__init__(master, app, "Sessions", modal=True) + self.selected = False + self.selected_id = None + self.tree = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_description() + self.draw_tree() + self.draw_buttons() + + def draw_description(self): + """ + write a short description + :return: nothing + """ + label = tk.Label( + self, + text="Below is a list of active CORE sessions. Double-click to \n" + "connect to an existing session. Usually, only sessions in \n" + "the RUNTIME state persist in the daemon, except for the \n" + "one you might be concurrently editting.", + ) + label.grid(row=0, sticky="ew", pady=5) + + def draw_tree(self): + self.tree = Treeview(self, columns=("id", "state", "nodes"), show="headings") + self.tree.grid(row=1, sticky="nsew") + self.tree.column("id", stretch=tk.YES) + self.tree.heading("id", text="ID") + self.tree.column("state", stretch=tk.YES) + self.tree.heading("state", text="State") + self.tree.column("nodes", stretch=tk.YES) + self.tree.heading("nodes", text="Node Count") + + response = self.app.core_grpc.core.get_sessions() + logging.info("sessions: %s", response) + for index, session in enumerate(response.sessions): + state_name = core_pb2.SessionState.Enum.Name(session.state) + self.tree.insert( + "", + tk.END, + text=str(session.id), + values=(session.id, state_name, session.nodes), + ) + self.tree.bind("", self.on_selected) + self.tree.bind("<>", self.click_select) + + yscrollbar = Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=1, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=2, sticky="ew", pady=5) + self.tree.configure(xscrollcommand=xscrollbar.set) + + def draw_buttons(self): + frame = tk.Frame(self) + for i in range(4): + frame.columnconfigure(i, weight=1) + frame.grid(row=3, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.DOCUMENTNEW), + text="New", + compound=tk.LEFT, + command=self.click_new, + ) + b.grid(row=0, padx=2, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.FILEOPEN), + text="Connect", + compound=tk.LEFT, + command=self.click_connect, + ) + b.grid(row=0, column=1, padx=2, sticky="ew") + b = tk.Button( + frame, + image=Images.get(ImageEnum.EDITDELETE), + text="Shutdown", + compound=tk.LEFT, + command=self.click_shutdown, + ) + b.grid(row=0, column=2, padx=2, sticky="ew") + b = tk.Button(frame, text="Cancel", command=self.click_new) + b.grid(row=0, column=3, padx=2, sticky="ew") + + def click_new(self): + self.app.core_grpc.create_new_session() + self.destroy() + + def click_select(self, event): + item = self.tree.selection() + session_id = int(self.tree.item(item, "text")) + self.selected = True + self.selected_id = session_id + + def click_connect(self): + """ + if no session is selected yet, create a new one else join that session + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.join_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("sessions invalid state") + + def click_shutdown(self): + """ + if no session is currently selected create a new session else shut the selected + session down. + + :return: nothing + """ + if self.selected and self.selected_id is not None: + self.shutdown_session(self.selected_id) + elif not self.selected and self.selected_id is None: + self.click_new() + else: + logging.error("querysessiondrawing.py invalid state") + + def join_session(self, session_id): + response = self.app.core_grpc.core.get_session(session_id) + self.app.core_grpc.session_id = session_id + self.app.core_grpc.core.events(session_id, self.app.core_grpc.log_event) + logging.info("entering session_id %s.... Result: %s", session_id, response) + self.destroy() + + def on_selected(self, event): + item = self.tree.selection() + sid = int(self.tree.item(item, "text")) + self.join_session(sid) + + def shutdown_session(self, sid): + self.app.core_grpc.terminate_session(sid) + self.click_new() + self.destroy() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 64f38acf..ba58a435 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -94,7 +94,7 @@ class WlanAntennaManager: x - 16 + self.offset, y - 16, anchor=tk.CENTER, - image=Images.get(ImageEnum.ANTENNA.value), + image=Images.get(ImageEnum.ANTENNA), tags="antenna", ) ) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f865c191..6a817b6f 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -21,8 +21,8 @@ class Images: cls.images[name] = tk_image @classmethod - def get(cls, name): - return cls.images[name] + def get(cls, image): + return cls.images[image.value] @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): @@ -35,28 +35,28 @@ class Images: :return: the matching image and its name """ if node_type == core_pb2.NodeType.SWITCH: - return Images.get(ImageEnum.SWITCH.value), "switch" + return Images.get(ImageEnum.SWITCH), "switch" if node_type == core_pb2.NodeType.HUB: - return Images.get(ImageEnum.HUB.value), "hub" + return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.RJ45: - return Images.get(ImageEnum.RJ45.value), "rj45" + return Images.get(ImageEnum.RJ45), "rj45" if node_type == core_pb2.NodeType.TUNNEL: - return Images.get(ImageEnum.TUNNEL.value), "tunnel" + return Images.get(ImageEnum.TUNNEL), "tunnel" if node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get(ImageEnum.ROUTER.value), "router" + return Images.get(ImageEnum.ROUTER), "router" if node_model == "host": - return Images.get((ImageEnum.HOST.value)), "host" + return Images.get(ImageEnum.HOST), "host" if node_model == "PC": - return Images.get(ImageEnum.PC.value), "PC" + return Images.get(ImageEnum.PC), "PC" if node_model == "mdr": - return Images.get(ImageEnum.MDR.value), "mdr" + return Images.get(ImageEnum.MDR), "mdr" if node_model == "prouter": - return Images.get(ImageEnum.PROUTER.value), "prouter" + return Images.get(ImageEnum.PROUTER), "prouter" if node_model == "OVS": - return Images.get(ImageEnum.OVS.value), "ovs" + return Images.get(ImageEnum.OVS), "ovs" else: logging.debug("INVALID INPUT OR NOT CONSIDERED YET") diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 22329516..97ba9029 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,7 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.dialogs.sessionoptions import SessionOptionsDialog +from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale @@ -291,10 +292,6 @@ def widgets_configure_throughput(): logging.debug("Click widgets configure throughput") -def session_change_sessions(): - logging.debug("Click session change sessions") - - def session_node_types(): logging.debug("Click session node types") @@ -433,5 +430,10 @@ class MenuAction: def session_options(self): logging.debug("Click session options") - dialog = SessionOptionsDialog(self.application) + dialog = SessionOptionsDialog(self.application, self.application) + dialog.show() + + def session_change_sessions(self): + logging.debug("Click session change sessions") + dialog = SessionsDialog(self.application, self.application) dialog.show() diff --git a/coretk/coretk/querysessiondrawing.py b/coretk/coretk/querysessiondrawing.py deleted file mode 100644 index 84786ba5..00000000 --- a/coretk/coretk/querysessiondrawing.py +++ /dev/null @@ -1,183 +0,0 @@ -import logging -import tkinter as tk -from tkinter.ttk import Scrollbar, Treeview - -from coretk.images import ImageEnum, Images - - -class SessionTable: - def __init__(self, grpc, master): - """ - create session table instance - :param coretk.coregrpc.CoreGrpc grpc: coregrpc - :param root.master master: - """ - self.grpc = grpc - self.selected = False - self.selected_sid = None - self.master = master - self.top = tk.Toplevel(self.master) - self.description_definition() - self.top.title("CORE sessions") - - self.tree = Treeview(self.top) - # self.tree.pack(side=tk.TOP) - self.tree.grid(row=1, column=0, columnspan=2) - self.draw_scrollbar() - self.draw() - - def description_definition(self): - """ - write a short description - :return: nothing - """ - lable = tk.Label( - self.top, - text="Below is a list of active CORE sessions. Double-click to " - "\nconnect to an existing session. Usually, only sessions in " - "\nthe RUNTIME state persist in the daemon, except for the " - "\none you might be concurrently editting.", - ) - lable.grid(sticky=tk.W) - - def column_definition(self): - # self.tree["columns"] = ("name", "nodecount", "filename", "date") - self.tree["columns"] = "nodecount" - self.tree.column("#0", width=300, minwidth=30) - # self.tree.column("name", width=72, miwidth=30) - self.tree.column("nodecount", width=300, minwidth=30) - # self.tree.column("filename", width=92, minwidth=30) - # self.tree.column("date", width=170, minwidth=30) - - def draw_scrollbar(self): - yscrollbar = Scrollbar(self.top, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=1, column=3, sticky=tk.N + tk.S + tk.W) - self.tree.configure(yscrollcommand=yscrollbar.set) - - xscrollbar = Scrollbar(self.top, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=2, columnspan=2, sticky=tk.E + tk.W + tk.S) - self.tree.configure(xscrollcommand=xscrollbar.set) - - def heading_definition(self): - self.tree.heading("#0", text="ID", anchor=tk.W) - # self.tree.heading("name", text="Name", anchor=tk.CENTER) - self.tree.heading("nodecount", text="Node Count", anchor=tk.W) - # self.tree.heading("filename", text="Filename", anchor=tk.CENTER) - # self.tree.heading("date", text="Date", anchor=tk.CENTER) - - def enter_session(self, sid): - self.top.destroy() - response = self.grpc.core.get_session(sid) - self.grpc.session_id = sid - self.grpc.core.events(sid, self.grpc.log_event) - logging.info("Entering session_id %s.... Result: %s", sid, response) - - def new_session(self): - self.top.destroy() - self.grpc.create_new_session() - - def on_selected(self, event): - item = self.tree.selection() - sid = int(self.tree.item(item, "text")) - self.enter_session(sid) - - def click_select(self, event): - # logging.debug("Click on %s ", event) - item = self.tree.selection() - sid = int(self.tree.item(item, "text")) - self.selected = True - self.selected_sid = sid - - def session_definition(self): - response = self.grpc.core.get_sessions() - # logging.info("querysessiondrawing.py Get all sessions %s", response) - index = 1 - for session in response.sessions: - self.tree.insert( - "", index, None, text=str(session.id), values=(str(session.nodes)) - ) - index = index + 1 - self.tree.bind("", self.on_selected) - self.tree.bind("<>", self.click_select) - - def click_connect(self): - """ - if no session is selected yet, create a new one else join that session - - :return: nothing - """ - if self.selected and self.selected_sid is not None: - self.enter_session(self.selected_sid) - elif not self.selected and self.selected_sid is None: - self.new_session() - else: - logging.error("querysessiondrawing.py invalid state") - - def shutdown_session(self, sid): - self.grpc.terminate_session(sid) - self.new_session() - self.top.destroy() - - def click_shutdown(self): - """ - if no session is currently selected create a new session else shut the selected session down - - :return: nothing - """ - if self.selected and self.selected_sid is not None: - self.shutdown_session(self.selected_sid) - elif not self.selected and self.selected_sid is None: - self.new_session() - else: - logging.error("querysessiondrawing.py invalid state") - # if self.selected and self.selected_sid is not None: - - def draw_buttons(self): - f = tk.Frame(self.top) - f.grid(row=3, sticky=tk.W) - - b = tk.Button( - f, - image=Images.get(ImageEnum.DOCUMENTNEW.value), - text="New", - compound=tk.LEFT, - command=self.new_session, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button( - f, - image=Images.get(ImageEnum.FILEOPEN.value), - text="Connect", - compound=tk.LEFT, - command=self.click_connect, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button( - f, - image=Images.get(ImageEnum.EDITDELETE.value), - text="Shutdown", - compound=tk.LEFT, - command=self.click_shutdown, - ) - b.pack(side=tk.LEFT, padx=3, pady=4) - b = tk.Button(f, text="Cancel", command=self.new_session) - b.pack(side=tk.LEFT, padx=3, pady=4) - - def center(self): - window_width = self.master.winfo_width() - window_height = self.master.winfo_height() - self.top.update() - top_level_width = self.top.winfo_width() - top_level_height = self.top.winfo_height() - x = window_width / 2 - top_level_width / 2 - y = window_height / 2 - top_level_height / 2 - - self.top.geometry("+%d+%d" % (x, y)) - - def draw(self): - self.column_definition() - self.heading_definition() - self.session_definition() - self.draw_buttons() - self.center() - self.top.wait_window() From 60fff5491802b7bd55ef6fd50925b91bdd56c8d8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 1 Nov 2019 08:35:14 -0700 Subject: [PATCH 040/305] work on wlan config --- coretk/coretk/canvasaction.py | 11 +- coretk/coretk/coregrpc.py | 100 +++++++++---- coretk/coretk/coretoolbar.py | 62 ++++---- coretk/coretk/coretoolbarhelp.py | 113 +++++++++++++-- coretk/coretk/graph.py | 4 - coretk/coretk/grpcmanagement.py | 7 + coretk/coretk/icons/emane.gif | Bin 0 -> 1111 bytes coretk/coretk/images.py | 3 + coretk/coretk/interface.py | 6 +- coretk/coretk/menuaction.py | 12 +- coretk/coretk/nodeconfigtable.py | 11 +- coretk/coretk/nodeservice.py | 195 ++++++++++++++++++++++++++ coretk/coretk/serviceconfiguration.py | 10 -- coretk/coretk/wlanconfiguration.py | 150 +++++++++++++++++--- coretk/coretk/wlannodeconfig.py | 29 ++++ 15 files changed, 606 insertions(+), 107 deletions(-) create mode 100644 coretk/coretk/icons/emane.gif create mode 100644 coretk/coretk/nodeservice.py delete mode 100644 coretk/coretk/serviceconfiguration.py create mode 100644 coretk/coretk/wlannodeconfig.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index addd2cd8..0c56d4ba 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -8,6 +8,7 @@ from core.api.grpc import core_pb2 from coretk.nodeconfigtable import NodeConfig from coretk.wlanconfiguration import WlanConfiguration +# TODO, finish classifying node types NODE_TO_TYPE = { "router": core_pb2.NodeType.DEFAULT, "wlan": core_pb2.NodeType.WIRELESS_LAN, @@ -27,12 +28,16 @@ class CanvasAction: if pb_type == core_pb2.NodeType.DEFAULT: self.display_node_configuration() elif pb_type == core_pb2.NodeType.WIRELESS_LAN: - self.display_wlan_configuration() + self.display_wlan_configuration(canvas_node) def display_node_configuration(self): NodeConfig(self.canvas, self.node_to_show_config) self.node_to_show_config = None - def display_wlan_configuration(self): - WlanConfiguration(self.canvas, self.node_to_show_config) + def display_wlan_configuration(self, canvas_node): + # print(self.canvas.grpc_manager.wlanconfig_management.configurations) + wlan_config = self.canvas.grpc_manager.wlanconfig_management.configurations[ + canvas_node.core_id + ] + WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) self.node_to_show_config = None diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 9b51116a..3a1b1719 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -29,8 +29,10 @@ class CoreGrpc: self.wireless_draw = WirelessConnection(app.canvas, self) def log_event(self, event): - logging.info("event: %s", event) + # logging.info("event: %s", event) if event.link_event is not None: + logging.info("event: %s", event) + self.wireless_draw.hangle_link_event(event.link_event) def log_throughput(self, event): @@ -206,6 +208,54 @@ class CoreGrpc: ) logging.info("delete links %s", response) + def create_interface(self, node_type, gui_interface): + """ + create a protobuf interface given the interface object stored by the programmer + + :param core_bp2.NodeType type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface object + :rtype: core_bp2.Interface + :return: protobuf interface object + """ + if node_type != core_pb2.NodeType.DEFAULT: + return None + else: + interface = core_pb2.Interface( + id=gui_interface.id, + name=gui_interface.name, + mac=gui_interface.mac, + ip4=gui_interface.ipv4, + ip4mask=gui_interface.ip4prefix, + ) + logging.debug("create interface 1 %s", interface) + + return interface + + # TODO add location, hooks, emane_config, etc... + def start_session( + self, + nodes, + links, + location=None, + hooks=None, + emane_config=None, + emane_model_configs=None, + wlan_configs=None, + mobility_configs=None, + ): + response = self.core.start_session( + session_id=self.session_id, + nodes=nodes, + links=links, + wlan_configs=wlan_configs, + ) + logging.debug("Start session %s, result: %s", self.session_id, response.result) + + def stop_session(self): + response = self.core.stop_session(session_id=self.session_id) + logging.debug("coregrpc.py Stop session, result: %s", response.result) + + # TODO no need, might get rid of this def add_link(self, id1, id2, type1, type2, edge): """ Grpc client request add link @@ -217,30 +267,30 @@ class CoreGrpc: :param core_pb2.NodeType type2: node 2 core node type :return: nothing """ - if1 = None - if2 = None - if type1 == core_pb2.NodeType.DEFAULT: - interface = edge.interface_1 - if1 = core_pb2.Interface( - id=interface.id, - name=interface.name, - mac=interface.mac, - ip4=interface.ipv4, - ip4mask=interface.ip4prefix, - ) - logging.debug("create interface 1 %s", if1) - # interface1 = self.interface_helper.create_interface(id1, 0) - - if type2 == core_pb2.NodeType.DEFAULT: - interface = edge.interface_2 - if2 = core_pb2.Interface( - id=interface.id, - name=interface.name, - mac=interface.mac, - ip4=interface.ipv4, - ip4mask=interface.ip4prefix, - ) - logging.debug("create interface 2: %s", if2) + if1 = self.create_interface(type1, edge.interface_1) + if2 = self.create_interface(type2, edge.interface_2) + # if type1 == core_pb2.NodeType.DEFAULT: + # interface = edge.interface_1 + # if1 = core_pb2.Interface( + # id=interface.id, + # name=interface.name, + # mac=interface.mac, + # ip4=interface.ipv4, + # ip4mask=interface.ip4prefix, + # ) + # logging.debug("create interface 1 %s", if1) + # # interface1 = self.interface_helper.create_interface(id1, 0) + # + # if type2 == core_pb2.NodeType.DEFAULT: + # interface = edge.interface_2 + # if2 = core_pb2.Interface( + # id=interface.id, + # name=interface.name, + # mac=interface.mac, + # ip4=interface.ipv4, + # ip4mask=interface.ip4prefix, + # ) + # logging.debug("create interface 2: %s", if2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index d46a9791..58c25c77 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,22 +1,23 @@ import logging import tkinter as tk -from enum import Enum -from core.api.grpc import core_pb2 +# from core.api.grpc import core_pb2 from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip +# from enum import Enum -class SessionStateEnum(Enum): - NONE = "none" - DEFINITION = "definition" - CONFIGURATION = "configuration" - RUNTIME = "runtime" - DATACOLLECT = "datacollect" - SHUTDOWN = "shutdown" - INSTANTIATION = "instantiation" + +# class SessionStateEnum(Enum): +# NONE = "none" +# DEFINITION = "definition" +# CONFIGURATION = "configuration" +# RUNTIME = "runtime" +# DATACOLLECT = "datacollect" +# SHUTDOWN = "shutdown" +# INSTANTIATION = "instantiation" class CoreToolbar(object): @@ -161,21 +162,22 @@ class CoreToolbar(object): """ logging.debug("Click START STOP SESSION button") helper = CoreToolbarHelp(self.application) - # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT # set configuration state - state = self.canvas.core_grpc.get_session_state() + # state = self.canvas.core_grpc.get_session_state() + # if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: + # self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) + # self.application.is_open_xml = False + # + # self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) + # helper.add_nodes() + # helper.add_edges() + # self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) + helper.gui_start_session() + self.create_runtime_toolbar() - if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: - self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) - self.application.is_open_xml = False - - self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - - helper.add_nodes() - helper.add_edges() # for node in self.canvas.grpc_manager.nodes.values(): # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) # self.canvas.core_grpc.add_node( @@ -188,10 +190,8 @@ class CoreToolbar(object): # self.canvas.core_grpc.add_link( # edge.id1, edge.id2, edge.type1, edge.type2, edge # ) - self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) # self.canvas.core_grpc.get_session() # self.application.is_open_xml = False - self.create_runtime_toolbar() def click_link_tool(self): logging.debug("Click LINK button") @@ -366,6 +366,13 @@ class CoreToolbar(object): self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL.value) self.canvas.draw_node_name = "tunnel" + def pick_emane(self, main_button): + self.link_layer_option_menu.destroy() + main_button.configure(image=Images.get(ImageEnum.EMANE.value)) + self.canvas.mode = GraphMode.PICKNODE + self.canvas.draw_node_image = Images.get(ImageEnum.EMANE.value) + self.canvas.draw_node_name = "emane" + def draw_link_layer_options(self, link_layer_button): """ Draw the options for link-layer button @@ -380,6 +387,7 @@ class CoreToolbar(object): Images.get(ImageEnum.HUB.value), Images.get(ImageEnum.SWITCH.value), Images.get(ImageEnum.WLAN.value), + Images.get(ImageEnum.EMANE.value), Images.get(ImageEnum.RJ45.value), Images.get(ImageEnum.TUNNEL.value), ] @@ -387,6 +395,7 @@ class CoreToolbar(object): self.pick_hub, self.pick_switch, self.pick_wlan, + self.pick_emane, self.pick_rj45, self.pick_tunnel, ] @@ -394,6 +403,7 @@ class CoreToolbar(object): "ethernet hub", "ethernet switch", "wireless LAN", + "emane", "rj45 physical interface tool", "tunnel tool", ] @@ -582,12 +592,12 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click on STOP button ") - # self.destroy_children_widgets(self.edit_frame) self.destroy_children_widgets() - self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) - self.canvas.core_grpc.delete_links() - self.canvas.core_grpc.delete_nodes() + # self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) + # self.canvas.core_grpc.delete_links() + # self.canvas.core_grpc.delete_nodes() + self.canvas.core_grpc.stop_session() self.create_toolbar() def click_run_button(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index a02c7ae9..b4b6fe28 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -1,6 +1,7 @@ """ CoreToolbar help to draw on canvas, and make grpc client call """ +from core.api.grpc.client import core_pb2 class CoreToolbarHelp: @@ -8,24 +9,114 @@ class CoreToolbarHelp: self.application = application self.core_grpc = application.core_grpc - def add_nodes(self): + def get_node_list(self): """ - add the nodes stored in grpc manager + form a list node protobuf nodes to pass in start_session in grpc + :return: nothing """ grpc_manager = self.application.canvas.grpc_manager - for node in grpc_manager.nodes.values(): - self.application.core_grpc.add_node( - node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - ) - def add_edges(self): + # list(core_pb2.Node) + nodes = [] + + for node in grpc_manager.nodes.values(): + pos = core_pb2.Position(x=int(node.x), y=int(node.y)) + n = core_pb2.Node( + id=node.node_id, type=node.type, position=pos, model=node.model + ) + nodes.append(n) + return nodes + + def get_link_list(self): """ - add the edges stored in grpc manager - :return: + form a list of links to pass into grpc start session + + :rtype: list(core_pb2.Link) + :return: list of protobuf links """ grpc_manager = self.application.canvas.grpc_manager + + # list(core_bp2.Link) + links = [] for edge in grpc_manager.edges.values(): - self.application.core_grpc.add_link( - edge.id1, edge.id2, edge.type1, edge.type2, edge + interface_one = self.application.core_grpc.create_interface( + edge.type1, edge.interface_1 ) + interface_two = self.application.core_grpc.create_interface( + edge.type2, edge.interface_2 + ) + # TODO for now only consider the basic cases + if ( + edge.type1 == core_pb2.NodeType.WIRELESS_LAN + or edge.type2 == core_pb2.NodeType.WIRELESS_LAN + ): + link_type = core_pb2.LinkType.WIRELESS + else: + link_type = core_pb2.LinkType.WIRED + link = core_pb2.Link( + node_one_id=edge.id1, + node_two_id=edge.id2, + type=link_type, + interface_one=interface_one, + interface_two=interface_two, + ) + links.append(link) + # self.id1 = edge.id1 + # self.id2 = edge.id2 + # self.type = link_type + # self.if1 = interface_one + # self.if2 = interface_two + + return links + + def get_wlan_configuration_list(self): + configs = [] + grpc_manager = self.application.canvas.grpc_manager + manager_configs = grpc_manager.wlanconfig_management.configurations + for key in manager_configs: + cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) + configs.append(cnf) + return configs + + def gui_start_session(self): + # list(core_pb2.Node) + nodes = self.get_node_list() + + # list(core_bp2.Link) + links = self.get_link_list() + + # print(links[0]) + wlan_configs = self.get_wlan_configuration_list() + # print(wlan_configs) + self.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + + # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session + # print(res) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + + # print(res) + + # def add_nodes(self): + # """ + # add the nodes stored in grpc manager + # :return: nothing + # """ + # grpc_manager = self.application.canvas.grpc_manager + # for node in grpc_manager.nodes.values(): + # self.application.core_grpc.add_node( + # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id + # ) + # + # def add_edges(self): + # """ + # add the edges stored in grpc manager + # :return: + # """ + # grpc_manager = self.application.canvas.grpc_manager + # for edge in grpc_manager.edges.values(): + # self.application.core_grpc.add_link( + # edge.id1, edge.id2, edge.type1, edge.type2, edge + # ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index dd3bbc1c..ef0f064f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -58,10 +58,6 @@ class CanvasGraph(tk.Canvas): # self.core_map = CoreToCanvasMapping() # self.draw_existing_component() - def test(self): - print("testing the button") - print(self.node_context.winfo_rootx()) - def setup_menus(self): self.node_context = tk.Menu(self.master) self.node_context.add_command( diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 4cfc6a16..e7373dc6 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -7,6 +7,7 @@ import logging from core.api.grpc import core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager +from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -72,6 +73,8 @@ class GrpcManager: # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() + self.wlanconfig_management = WlanNodeConfig() + def update_preexisting_ids(self): """ get preexisting node ids @@ -145,6 +148,10 @@ class GrpcManager: logging.error("grpcmanagemeny.py INVALID node name") nid = self.get_id() create_node = Node(session_id, nid, node_type, node_model, x, y, name) + + # set default configuration for wireless node + self.wlanconfig_management.set_default_config(node_type, nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/icons/emane.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a3d3850d4c01d35e3322c97b8cd0ab00251ecd8 GIT binary patch literal 1111 zcmW+!Uue)(7(D}p89_BM)XmtxBE44IKCNjFQrT)0L!wKI3bD;^%+_DXryeeCA)}Qv zqWBE4bQrr}ZY?5waS`Q$uZ-BG@MV#(psWa4Qc771P(TF@bg;Tq+#*nc3B1B2 zK!Qk62`0fMP$DFXM3ra~T_Pnxl1NfXCdnmHG9-&+m28q-GNnL@NKq*!#idXPp$JuI zLKjv#sxO zw1^hfVp?1al~9UOm8NuMWkKb*7}Q_}uN<)e3t~Ynm<6{$i?Apb)uLH+i?jqwVo5ET zCAUP&uq>9w?ZR~VpO9U-B`I_g)I(sn8T|uJivo^P!Hz8JGnl_iy~ z3aqLSz#t6DU<}ScMlcGaG8&^Zk_k-0q)f)-Ok@VLFe|e$J2P3pA}q>cEY7k_Ru!#E zyRy8RdxdK$1^z}{u4nEZX z&53(&(@U*+B98YSUj5?1%Z=$|%fORwc6Tg3piPIL-c~!jIQ{Ctxs&TQ*DYRn`If;W zpa1>ssk!l6f3I8h!yTs{o9sUN^RM&kj?3&_KOHFbkM?(+9%=8n{z%#K;M|Fg7gt~V z_J{pnZhvj`w@=o8FnDHP`_7B4y*Cbh-@5Cav4)wCXW!~s7+)Ie%@;>", self.group_select) + + def group_service_select(self, event): + print("select group service") + listbox = event.widget + cur_selection = listbox.curselection() + if cur_selection: + s = listbox.get(listbox.curselection()) + self.service_to_config = s + else: + self.service_to_config = None + + def group_services(self): + f = tk.Frame(self.config_frame) + lbl = tk.Label(f, text="Group services") + lbl.grid() + + sb = tk.Scrollbar(f, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.S + tk.N) + + listbox = tk.Listbox( + f, + selectmode=tk.SINGLE, + yscrollcommand=sb.set, + relief=tk.FLAT, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0) + sb.config(command=listbox.yview) + f.grid(padx=3, pady=3, row=0, column=1) + + listbox.bind("<>", self.group_service_select) + + def current_services(self): + f = tk.Frame(self.config_frame) + lbl = tk.Label(f, text="Current services") + lbl.grid() + + sb = tk.Scrollbar(f, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.S + tk.N) + + listbox = tk.Listbox( + f, + selectmode=tk.MULTIPLE, + yscrollcommand=sb.set, + relief=tk.FLAT, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0) + sb.config(command=listbox.yview) + f.grid(padx=3, pady=3, row=0, column=2) + + def config_service(self): + if self.service_to_config is None: + messagebox.showinfo("CORE info", "Choose a service to configure.") + else: + print(self.service_to_config) + + def node_service_options(self): + f = tk.Frame(self.top) + b = tk.Button(f, text="Connfigure", command=self.config_service) + b.grid(row=0, column=0) + b = tk.Button(f, text="Apply") + b.grid(row=0, column=1) + b = tk.Button(f, text="Cancel", command=self.top.destroy) + b.grid(row=0, column=2) + f.grid(sticky=tk.E) diff --git a/coretk/coretk/serviceconfiguration.py b/coretk/coretk/serviceconfiguration.py deleted file mode 100644 index a539f1fb..00000000 --- a/coretk/coretk/serviceconfiguration.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -service configuration -""" - -# import tkinter as tk - - -class ServiceConfiguration: - def __init__(self): - return diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 53e98ea5..32bb56e2 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -2,13 +2,20 @@ wlan configuration """ -import ast import tkinter as tk from functools import partial +from coretk.imagemodification import ImageModification + class WlanConfiguration: - def __init__(self, canvas, canvas_node): + def __init__(self, canvas, canvas_node, config): + """ + create an instance of WlanConfiguration + + :param coretk.grpah.CanvasGraph canvas: canvas object + :param coretk.graph.CanvasNode canvas_node: canvas node object + """ self.canvas = canvas self.image = canvas_node.image @@ -20,10 +27,14 @@ class WlanConfiguration: self.top.title("wlan configuration") self.node_name = tk.StringVar() - self.range_var = tk.DoubleVar() - self.range_var.set(275.0) - self.bandwidth_var = tk.IntVar() - self.bandwidth_var.set(54000000) + # self.range_var = tk.DoubleVar() + # self.range_var.set(275.0) + self.config = config + self.range_var = tk.StringVar() + self.range_var.set(config["basic_range"]) + # self.bandwidth_var = tk.IntVar() + self.bandwidth_var = tk.StringVar() + self.bandwidth_var.set(config["bandwidth"]) self.delay_var = tk.StringVar() @@ -34,6 +45,11 @@ class WlanConfiguration: self.config_option() def image_modification(self): + """ + draw image modification part + + :return: nothing + """ f = tk.Frame(self.top, bg="#d9d9d9") lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") lbl.grid(row=0, column=0, padx=3, pady=3) @@ -41,20 +57,40 @@ class WlanConfiguration: e.grid(row=0, column=1, padx=3, pady=3) b = tk.Button(f, text="None") b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button(f, text="not implemented") + b = tk.Button( + f, + image=self.image, + command=lambda: ImageModification( + canvas=self.canvas, canvas_node=self.canvas_node, node_config=self + ), + ) b.grid(row=0, column=3, padx=3, pady=3) f.grid(padx=2, pady=2, ipadx=2, ipady=2) def create_string_var(self, val): + """ + create string variable for convenience + + :param str val: text value + :return: nothing + """ v = tk.StringVar() v.set(val) return v def scrollbar_command(self, entry_widget, delta, event): + """ + change text in entry based on scrollbar action (click up or down) + + :param tkinter.Entry entry_widget: entry needed for changing text + :param int or float delta: the amount to change + :param event: scrollbar event + :return: nothing + """ try: value = int(entry_widget.get()) except ValueError: - value = ast.literal_eval(entry_widget.get()) + value = float(entry_widget.get()) entry_widget.delete(0, tk.END) if event == "-1": entry_widget.insert(tk.END, str(round(value + delta, 1))) @@ -62,6 +98,11 @@ class WlanConfiguration: entry_widget.insert(tk.END, str(round(value - delta, 1))) def wlan_configuration(self): + """ + create wireless configuration table + + :return: nothing + """ lbl = tk.Label(self.top, text="Wireless") lbl.grid(sticky=tk.W, padx=3, pady=3) @@ -86,7 +127,12 @@ class WlanConfiguration: lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") lbl.grid(row=0, column=0) - e = tk.Entry(f1, textvariable=self.range_var, width=5, bg="white") + e = tk.Entry( + f1, + textvariable=self.create_string_var(self.config["basic_range"]), + width=5, + bg="white", + ) e.grid(row=0, column=1) lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") @@ -94,7 +140,12 @@ class WlanConfiguration: f11 = tk.Frame(f1, bg="#d9d9d9") sb = tk.Scrollbar(f11, orient=tk.VERTICAL) - e = tk.Entry(f11, textvariable=self.bandwidth_var, width=10, bg="white") + e = tk.Entry( + f11, + textvariable=self.create_string_var(self.config["bandwidth"]), + width=10, + bg="white", + ) sb.config(command=partial(self.scrollbar_command, e, 1000000)) e.grid() sb.grid(row=0, column=1) @@ -110,7 +161,9 @@ class WlanConfiguration: f21 = tk.Frame(f2, bg="#d9d9d9") sb = tk.Scrollbar(f21, orient=tk.VERTICAL) - e = tk.Entry(f21, textvariable=self.create_string_var(20000), bg="white") + e = tk.Entry( + f21, textvariable=self.create_string_var(self.config["delay"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 5000)) e.grid() sb.grid(row=0, column=1) @@ -121,7 +174,9 @@ class WlanConfiguration: f22 = tk.Frame(f2, bg="#d9d9d9") sb = tk.Scrollbar(f22, orient=tk.VERTICAL) - e = tk.Entry(f22, textvariable=self.create_string_var(0), bg="white") + e = tk.Entry( + f22, textvariable=self.create_string_var(self.config["error"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 0.1)) e.grid() sb.grid(row=0, column=1) @@ -136,7 +191,9 @@ class WlanConfiguration: lbl.grid() f31 = tk.Frame(f3, bg="#d9d9d9") sb = tk.Scrollbar(f31, orient=tk.VERTICAL) - e = tk.Entry(f31, textvariable=self.create_string_var(0), bg="white") + e = tk.Entry( + f31, textvariable=self.create_string_var(self.config["jitter"]), bg="white" + ) sb.config(command=partial(self.scrollbar_command, e, 5000)) e.grid() sb.grid(row=0, column=1) @@ -146,23 +203,33 @@ class WlanConfiguration: f.grid(padx=3, pady=3) def subnet(self): + """ + create the entries for ipv4 subnet and ipv6 subnet + + :return: nothing + """ f = tk.Frame(self.top) f1 = tk.Frame(f) lbl = tk.Label(f1, text="IPv4 subnet") lbl.grid() - e = tk.Entry(f1, width=30, bg="white") + e = tk.Entry(f1, width=30, bg="white", textvariable=self.create_string_var("")) e.grid(row=0, column=1) f1.grid() f2 = tk.Frame(f) lbl = tk.Label(f2, text="IPv6 subnet") lbl.grid() - e = tk.Entry(f2, width=30, bg="white") + e = tk.Entry(f2, width=30, bg="white", textvariable=self.create_string_var("")) e.grid(row=0, column=1) f2.grid() f.grid(sticky=tk.W, padx=3, pady=3) def wlan_options(self): + """ + create wireless node options + + :return: + """ f = tk.Frame(self.top) b = tk.Button(f, text="ns-2 mobility script...") b.pack(side=tk.LEFT, padx=1) @@ -172,9 +239,60 @@ class WlanConfiguration: b.pack(side=tk.LEFT, padx=1) f.grid(sticky=tk.W) + def wlan_config_apply(self): + """ + retrieve user's wlan configuration and store the new configuration values + + :return: nothing + """ + config_frame = self.top.grid_slaves(row=2, column=0)[0] + range_and_bandwidth_frame = config_frame.grid_slaves(row=1, column=0)[0] + range_val = range_and_bandwidth_frame.grid_slaves(row=0, column=1)[0].get() + bandwidth = ( + range_and_bandwidth_frame.grid_slaves(row=0, column=3)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + delay_and_loss_frame = config_frame.grid_slaves(row=2, column=0)[0] + delay = ( + delay_and_loss_frame.grid_slaves(row=0, column=1)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + loss = ( + delay_and_loss_frame.grid_slaves(row=0, column=3)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + jitter_frame = config_frame.grid_slaves(row=3, column=0)[0] + jitter_val = ( + jitter_frame.grid_slaves(row=0, column=1)[0] + .grid_slaves(row=0, column=0)[0] + .get() + ) + + # set wireless node configuration here + wlanconfig_manager = self.canvas.grpc_manager.wlanconfig_management + wlanconfig_manager.set_custom_config( + node_id=self.canvas_node.core_id, + range=range_val, + bandwidth=bandwidth, + jitter=jitter_val, + delay=delay, + error=loss, + ) + self.top.destroy() + def config_option(self): + """ + create node configuration options + + :return: nothing + """ f = tk.Frame(self.top, bg="#d9d9d9") - b = tk.Button(f, text="Apply", bg="#d9d9d9") + b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.wlan_config_apply) b.grid(padx=2, pady=2) b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) b.grid(row=0, column=1, padx=2, pady=2) diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py new file mode 100644 index 00000000..647cd7cf --- /dev/null +++ b/coretk/coretk/wlannodeconfig.py @@ -0,0 +1,29 @@ +""" +wireless node configuration for all the wireless node +""" +from collections import OrderedDict + +from core.api.grpc import core_pb2 + + +class WlanNodeConfig: + def __init__(self): + # maps node id to wlan configuration + self.configurations = {} + + def set_default_config(self, node_type, node_id): + if node_type == core_pb2.NodeType.WIRELESS_LAN: + config = OrderedDict() + config["basic_range"] = "275" + config["bandwidth"] = "54000000" + config["jitter"] = "0" + config["delay"] = "20000" + config["error"] = "0" + self.configurations[node_id] = config + + def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): + self.configurations[node_id]["basic_range"] = range + self.configurations[node_id]["bandwidth"] = bandwidth + self.configurations[node_id]["jitter"] = jitter + self.configurations[node_id]["delay"] = delay + self.configurations[node_id]["error"] = error From 96d020a53df7ccea6547799c283e06b1d32db925 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 1 Nov 2019 09:01:56 -0700 Subject: [PATCH 041/305] coretk --- coretk/coretk/coretoolbar.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 628ddc35..78688725 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -384,12 +384,12 @@ class CoreToolbar(object): self.destroy_previous_frame() option_frame = tk.Frame(self.master, padx=1, pady=1) img_list = [ - Images.get(ImageEnum.HUB.value), - Images.get(ImageEnum.SWITCH.value), - Images.get(ImageEnum.WLAN.value), - Images.get(ImageEnum.EMANE.value), - Images.get(ImageEnum.RJ45.value), - Images.get(ImageEnum.TUNNEL.value), + Images.get(ImageEnum.HUB), + Images.get(ImageEnum.SWITCH), + Images.get(ImageEnum.WLAN), + Images.get(ImageEnum.EMANE), + Images.get(ImageEnum.RJ45), + Images.get(ImageEnum.TUNNEL), ] func_list = [ self.pick_hub, From 0e5d7a6f801cf30c6969351a0e3fbec8b0779a56 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 10:45:47 -0700 Subject: [PATCH 042/305] cleanup for organizing how some parameters are passed around, merging logic into central places, avoiding things like needing to requery a session multiple times for joining --- coretk/coretk/app.py | 56 ++++++-------------------- coretk/coretk/appcache.py | 33 --------------- coretk/coretk/coregrpc.py | 50 ++++++++++++++--------- coretk/coretk/coretoolbarhelp.py | 17 ++++---- coretk/coretk/dialogs/sessions.py | 5 +-- coretk/coretk/graph.py | 62 ++++++++++++----------------- coretk/coretk/grpcmanagement.py | 15 +++---- coretk/coretk/images.py | 14 +++---- coretk/coretk/linkinfo.py | 18 ++++----- coretk/coretk/wirelessconnection.py | 3 +- 10 files changed, 100 insertions(+), 173 deletions(-) delete mode 100644 coretk/coretk/appcache.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f253fd01..bcc43e43 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,8 +1,6 @@ import logging import tkinter as tk -import coretk.appcache as appcache -import coretk.images as images from coretk.coregrpc import CoreGrpc from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar @@ -14,28 +12,25 @@ from coretk.menuaction import MenuAction class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - appcache.cache_variable(self) - print(self.is_open_xml) - self.load_images() - self.setup_app() + Images.load_all() self.menubar = None self.core_menu = None self.canvas = None self.core_editbar = None - self.core_grpc = None - + self.is_open_xml = False + self.size_and_scale = None + self.set_wallpaper = None + self.wallpaper_id = None + self.current_wallpaper = None + self.radiovar = tk.IntVar(value=1) + self.show_grid_var = tk.IntVar(value=1) + self.adjust_to_dim_var = tk.IntVar(value=0) + self.core_grpc = CoreGrpc(self) + self.setup_app() self.create_menu() self.create_widgets() self.draw_canvas() - self.start_grpc() - # self.try_make_table() - - def load_images(self): - """ - Load core images - :return: - """ - images.load_core_images(Images) + self.core_grpc.set_up() def setup_app(self): self.master.title("CORE") @@ -59,10 +54,7 @@ class Application(tk.Frame): def draw_canvas(self): self.canvas = CanvasGraph( - master=self, - grpc=self.core_grpc, - background="#cccccc", - scrollregion=(0, 0, 1200, 1000), + self, self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) @@ -86,31 +78,9 @@ class Application(tk.Frame): b = tk.Button(status_bar, text="Button 3") b.pack(side=tk.LEFT, padx=1) - def start_grpc(self): - """ - Conect client to grpc, query sessions and prompt use to choose an existing session if there exist any - - :return: nothing - """ - self.master.update() - self.core_grpc = CoreGrpc(self) - self.core_grpc.set_up() - self.canvas.core_grpc = self.core_grpc - self.canvas.grpc_manager.core_grpc = self.core_grpc - self.canvas.grpc_manager.update_preexisting_ids() - self.canvas.draw_existing_component() - def on_closing(self): menu_action = MenuAction(self, self.master) menu_action.on_quit() - # self.quit() - - def try_make_table(self): - f = tk.Frame(self.master) - for i in range(3): - e = tk.Entry(f) - e.grid(row=0, column=1, stick="nsew") - f.pack(side=tk.TOP) if __name__ == "__main__": diff --git a/coretk/coretk/appcache.py b/coretk/coretk/appcache.py deleted file mode 100644 index b2f87f71..00000000 --- a/coretk/coretk/appcache.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -stores some information helpful for setting starting values for some tables -like size and scale, set wallpaper, etc -""" -import tkinter as tk - - -def cache_variable(application): - # for menubar - application.is_open_xml = False - - application.size_and_scale = None - application.set_wallpaper = None - - # set wallpaper variables - - # canvas id of the wallpaper - application.wallpaper_id = None - - # current image for wallpaper - application.current_wallpaper = None - - # wallpaper option - application.radiovar = tk.IntVar() - application.radiovar.set(1) - - # show grid option - application.show_grid_var = tk.IntVar() - application.show_grid_var.set(1) - - # adjust canvas to image dimension variable - application.adjust_to_dim_var = tk.IntVar() - application.adjust_to_dim_var.set(0) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 13fe28b3..e38a0940 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -7,8 +7,7 @@ from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog -from coretk.linkinfo import Throughput -from coretk.wirelessconnection import WirelessConnection +from coretk.grpcmanagement import GrpcManager class CoreGrpc: @@ -22,15 +21,14 @@ class CoreGrpc: self.app = app self.master = app.master self.interface_helper = None - self.throughput_draw = Throughput(app.canvas, self) - self.wireless_draw = WirelessConnection(app.canvas, self) + self.manager = GrpcManager(self) - def log_event(self, event): + def handle_events(self, event): logging.info("event: %s", event) if event.link_event is not None: - self.wireless_draw.hangle_link_event(event.link_event) + self.app.canvas.wireless_draw.hangle_link_event(event.link_event) - def log_throughput(self, event): + def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs for i in interface_throughputs: print("") @@ -44,6 +42,30 @@ class CoreGrpc: throughputs_belong_to_session ) + def join_session(self, session_id): + # query session and set as current session + self.session_id = session_id + response = self.core.get_session(self.session_id) + logging.info("joining session(%s): %s", self.session_id, response) + self.core.events(self.session_id, self.app.core_grpc.handle_events) + + # determine next node id and reusable nodes + session = response.session + self.manager.reusable.clear() + self.manager.preexisting.clear() + max_id = 1 + for node in session.nodes: + if node.id > max_id: + max_id = node.id + self.manager.preexisting.add(node.id) + self.manager.id = max_id + for i in range(1, self.manager.id): + if i not in self.manager.preexisting: + self.manager.reusable.append(i) + + # draw session + self.app.canvas.draw_existing_component(session) + def create_new_session(self): """ Create a new session @@ -56,7 +78,7 @@ class CoreGrpc: # handle events session may broadcast self.session_id = response.session_id self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.log_event) + self.core.events(self.session_id, self.handle_events) # self.core.throughputs(self.log_throughput) def delete_session(self, custom_sid=None): @@ -114,6 +136,7 @@ class CoreGrpc: else: sid = custom_session_id + response = None if state == "configuration": response = self.core.set_session_state( sid, core_pb2.SessionState.CONFIGURATION @@ -229,15 +252,6 @@ class CoreGrpc: response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) - # self.core.get_node_links(self.session_id, id1) - - # def get_session(self): - # response = self.core.get_session(self.session_id) - # nodes = response.session.nodes - # for node in nodes: - # r = self.core.get_node_links(self.session_id, node.id) - # logging.info(r) - def launch_terminal(self, node_id): response = self.core.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) @@ -252,7 +266,7 @@ class CoreGrpc: """ response = self.core.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.log_event) + self.core.events(self.session_id, self.handle_events) def open_xml(self, file_path): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index a02c7ae9..883f293a 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -4,18 +4,17 @@ CoreToolbar help to draw on canvas, and make grpc client call class CoreToolbarHelp: - def __init__(self, application): - self.application = application - self.core_grpc = application.core_grpc + def __init__(self, app): + self.app = app def add_nodes(self): """ add the nodes stored in grpc manager :return: nothing """ - grpc_manager = self.application.canvas.grpc_manager - for node in grpc_manager.nodes.values(): - self.application.core_grpc.add_node( + manager = self.app.core_grpc.manager + for node in manager.nodes.values(): + self.app.core_grpc.add_node( node.type, node.model, int(node.x), int(node.y), node.name, node.node_id ) @@ -24,8 +23,8 @@ class CoreToolbarHelp: add the edges stored in grpc manager :return: """ - grpc_manager = self.application.canvas.grpc_manager - for edge in grpc_manager.edges.values(): - self.application.core_grpc.add_link( + manager = self.app.core_grpc.manager + for edge in manager.edges.values(): + self.app.core_grpc.add_link( edge.id1, edge.id2, edge.type1, edge.type2, edge ) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 10165e2e..b2bd90f0 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -142,10 +142,7 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - response = self.app.core_grpc.core.get_session(session_id) - self.app.core_grpc.session_id = session_id - self.app.core_grpc.core.events(session_id, self.app.core_grpc.log_event) - logging.info("entering session_id %s.... Result: %s", session_id, response) + self.app.core_grpc.join_session(session_id) self.destroy() def on_selected(self, event): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 4107401b..e474934f 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -4,11 +4,11 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.graph_helper import GraphHelper, WlanAntennaManager -from coretk.grpcmanagement import GrpcManager from coretk.images import Images from coretk.interface import Interface -from coretk.linkinfo import LinkInfo +from coretk.linkinfo import LinkInfo, Throughput from coretk.nodeconfigtable import NodeConfig +from coretk.wirelessconnection import WirelessConnection class GraphMode(enum.Enum): @@ -20,7 +20,7 @@ class GraphMode(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, master=None, grpc=None, cnf=None, **kwargs): + def __init__(self, master, core_grpc, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -33,20 +33,15 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - self.grid = None self.meters_per_pixel = 1.5 self.setup_menus() self.setup_bindings() self.draw_grid() - - self.core_grpc = grpc - self.grpc_manager = GrpcManager(grpc) - - self.helper = GraphHelper(self, grpc) - # self.core_id_to_canvas_id = {} - # self.core_map = CoreToCanvasMapping() - # self.draw_existing_component() + self.core_grpc = core_grpc + self.helper = GraphHelper(self, core_grpc) + self.throughput_draw = Throughput(self, core_grpc) + self.wireless_draw = WirelessConnection(self, core_grpc) def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -56,7 +51,9 @@ class CanvasGraph(tk.Canvas): def canvas_reset_and_redraw(self, new_grpc): """ - Reset the private variables CanvasGraph object, redraw nodes given the new grpc client + Reset the private variables CanvasGraph object, redraw nodes given the new grpc + client. + :param new_grpc: :return: """ @@ -73,15 +70,11 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.drawing_edge = None - - print("graph.py create a new grpc manager") - self.grpc_manager = GrpcManager(new_grpc) - # new grpc self.core_grpc = new_grpc print("grpah.py draw existing component") self.draw_existing_component() - print(self.grpc_manager.edges) + print(self.core_grpc.manager.edges) def setup_bindings(self): """ @@ -119,16 +112,13 @@ class CanvasGraph(tk.Canvas): for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") - def draw_existing_component(self): + def draw_existing_component(self, session): """ Draw existing node and update the information in grpc manager to match :return: nothing """ core_id_to_canvas_id = {} - - session_id = self.core_grpc.session_id - session = self.core_grpc.core.get_session(session_id).session # redraw existing nodes for node in session.nodes: # peer to peer node is not drawn on the GUI @@ -145,9 +135,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.grpc_manager.add_preexisting_node(n, session_id, node, name) - - self.grpc_manager.update_reusable_id() + self.core_grpc.manager.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +167,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.grpc_manager.add_edge(session_id, e.token, n1.id, n2.id) + self.core_grpc.manager.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,12 +196,12 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.grpc_manager.edges[e.token].interface_1 = if1 - self.grpc_manager.edges[e.token].interface_2 = if2 - self.grpc_manager.nodes[ + self.core_grpc.manager.edges[e.token].interface_1 = if1 + self.core_grpc.manager.edges[e.token].interface_2 = if2 + self.core_grpc.manager.nodes[ core_id_to_canvas_id[link.node_one_id] ].interfaces.append(if1) - self.grpc_manager.nodes[ + self.core_grpc.manager.nodes[ core_id_to_canvas_id[link.node_two_id] ].interfaces.append(if2) @@ -316,13 +304,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.grpc_manager.add_edge( + self.core_grpc.manager.add_edge( self.core_grpc.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.grpc_manager.edges[edge.token].interface_1 - if2 = self.grpc_manager.edges[edge.token].interface_2 + if1 = self.core_grpc.manager.edges[edge.token].interface_1 + if2 = self.core_grpc.manager.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -382,10 +370,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.grpc_manager.peek_id(), + core_id=self.core_grpc.manager.peek_id(), ) self.nodes[node.id] = node - self.grpc_manager.add_node( + self.core_grpc.manager.add_node( self.core_grpc.session_id, node.id, x, y, node_name ) return node @@ -477,7 +465,7 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.grpc_manager.nodes[self.id].node_id + node_id = self.canvas.core_grpc.manager.nodes[self.id].node_id state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) @@ -496,7 +484,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.grpc_manager.update_node_location( + self.canvas.core_grpc.manager.update_node_location( self.id, self.x_coord, self.y_coord ) self.moving = None diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index 9638c7c5..5f10433b 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -57,17 +57,14 @@ class GrpcManager: def __init__(self, grpc): self.nodes = {} self.edges = {} - self.id = None + self.id = 1 # A list of id for re-use, keep in increasing order self.reusable = [] - - self.preexisting = [] - self.core_grpc = None - + self.preexisting = set() + self.core_grpc = grpc # self.update_preexisting_ids() # self.core_id_to_canvas_id = {} self.interfaces_manager = InterfaceManager() - # map tuple(core_node_id, interface_id) to and edge # self.node_id_and_interface_to_edge_token = {} self.core_mapping = CoreToCanvasMapping() @@ -172,7 +169,7 @@ class GrpcManager: print(core_id) if self.id is None or core_id >= self.id: self.id = core_id + 1 - self.preexisting.append(core_id) + self.preexisting.add(core_id) n = Node( session_id, core_id, @@ -219,8 +216,8 @@ class GrpcManager: """ try: self.nodes.pop(canvas_id) - self.reuseable.append(canvas_id) - self.reuseable.sort() + self.reusable.append(canvas_id) + self.reusable.sort() except KeyError: logging.error("grpcmanagement.py INVALID NODE CANVAS ID") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 6a817b6f..241a6997 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -13,6 +13,13 @@ ICONS_DIR = os.path.join(PATH, "icons") class Images: images = {} + @classmethod + def load_all(cls): + for file_name in os.listdir(ICONS_DIR): + file_path = os.path.join(ICONS_DIR, file_name) + name = file_name.split(".")[0] + cls.load(name, file_path) + @classmethod def load(cls, name, file_path): # file_path = os.path.join(PATH, file_path) @@ -91,10 +98,3 @@ class ImageEnum(Enum): FILEOPEN = "fileopen" EDITDELETE = "edit-delete" ANTENNA = "antenna" - - -def load_core_images(images): - for file_name in os.listdir(ICONS_DIR): - file_path = os.path.join(ICONS_DIR, file_name) - name = file_name.split(".")[0] - images.load(name, file_path) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index c21a56b0..17bc99c8 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -11,7 +11,7 @@ class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): """ create an instance of LinkInfo object - :param tkinter.Canvas canvas: canvas object + :param coretk.graph.Graph canvas: canvas object :param coretk.graph.CanvasEdge edge: canvas edge onject :param ip4_src: :param ip6_src: @@ -104,29 +104,25 @@ class LinkInfo: class Throughput: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core_grpc): """ create an instance of Throughput object - :param tkinter.Canvas canvas: canvas object - :param coretk.coregrpc,CoreGrpc grpc: grpc object + :param coretk.app.Application app: application """ self.canvas = canvas - self.core_grpc = grpc - self.grpc_manager = canvas.grpc_manager - + self.core_grpc = core_grpc # edge canvas id mapped to throughput value self.tracker = {} - # map an edge canvas id to a throughput canvas id self.map = {} - self.edge_id_to_token = {} def load_throughput_info(self, interface_throughputs): """ load all interface throughouts from an event - :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface throughputs + :param repeated core_bp2.InterfaceThroughputinterface_throughputs: interface + throughputs :return: nothing """ for t in interface_throughputs: @@ -134,7 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.grpc_manager.core_mapping.get_token_from_node_and_interface( + token = self.core_grpc.manager.core_mapping.get_token_from_node_and_interface( nid, iid ) print(token) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 63042e19..3e4a98f8 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -8,8 +8,7 @@ class WirelessConnection: def __init__(self, canvas, grpc): self.canvas = canvas self.core_grpc = grpc - self.core_mapping = canvas.grpc_manager.core_mapping - + self.core_mapping = grpc.manager.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 1f230146a67df157298088f64f12f89c94dc793d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:15:45 -0700 Subject: [PATCH 043/305] merged grpcmanager with coregrpc --- coretk/coretk/coregrpc.py | 426 ++++++++++++++++++++++------ coretk/coretk/coretocanvas.py | 11 - coretk/coretk/coretoolbarhelp.py | 53 +--- coretk/coretk/graph.py | 28 +- coretk/coretk/grpcmanagement.py | 328 --------------------- coretk/coretk/linkinfo.py | 2 +- coretk/coretk/wirelessconnection.py | 6 +- 7 files changed, 368 insertions(+), 486 deletions(-) delete mode 100644 coretk/coretk/grpcmanagement.py diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 029a2d16..94f5ca7b 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -6,8 +6,54 @@ import os from collections import OrderedDict from core.api.grpc import client, core_pb2 +from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog -from coretk.grpcmanagement import GrpcManager +from coretk.interface import Interface, InterfaceManager +from coretk.wlannodeconfig import WlanNodeConfig + +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] + + +class Node: + def __init__(self, session_id, node_id, node_type, model, x, y, name): + """ + Create an instance of a node + + :param int session_id: session id + :param int node_id: node id + :param core_pb2.NodeType node_type: node type + :param int x: x coordinate + :param int y: coordinate + :param str name: node name + """ + self.session_id = session_id + self.node_id = node_id + self.type = node_type + self.x = x + self.y = y + self.model = model + self.name = name + self.interfaces = [] + + +class Edge: + def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): + """ + Create an instance of an edge + :param int session_id: session id + :param int node_id_1: node 1 id + :param int node_type_1: node 1 type + :param core_pb2.NodeType node_id_2: node 2 id + :param core_pb2.NodeType node_type_2: node 2 type + """ + self.session_id = session_id + self.id1 = node_id_1 + self.id2 = node_id_2 + self.type1 = node_type_1 + self.type2 = node_type_2 + self.interface_1 = None + self.interface_2 = None class CoreGrpc: @@ -21,7 +67,16 @@ class CoreGrpc: self.app = app self.master = app.master self.interface_helper = None - self.manager = GrpcManager(self) + + # data for managing the current session + self.nodes = {} + self.edges = {} + self.id = 1 + self.reusable = [] + self.preexisting = set() + self.interfaces_manager = InterfaceManager() + self.core_mapping = CoreToCanvasMapping() + self.wlanconfig_management = WlanNodeConfig() def handle_events(self, event): logging.info("event: %s", event) @@ -37,7 +92,6 @@ class CoreGrpc: for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: throughputs_belong_to_session.append(if_tp) - # bridge_throughputs = event.bridge_throughputs self.throughput_draw.process_grpc_throughput_event( throughputs_belong_to_session ) @@ -47,21 +101,24 @@ class CoreGrpc: self.session_id = session_id response = self.core.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) - self.core.events(self.session_id, self.app.core_grpc.handle_events) + self.core.events(self.session_id, self.handle_events) + + # set title to session + self.master.title(f"CORE Session({self.session_id})") # determine next node id and reusable nodes session = response.session - self.manager.reusable.clear() - self.manager.preexisting.clear() + self.reusable.clear() + self.preexisting.clear() max_id = 1 for node in session.nodes: if node.id > max_id: max_id = node.id - self.manager.preexisting.add(node.id) - self.manager.id = max_id - for i in range(1, self.manager.id): - if i not in self.manager.preexisting: - self.manager.reusable.append(i) + self.preexisting.add(node.id) + self.id = max_id + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) # draw session self.app.canvas.canvas_reset_and_redraw(session) @@ -74,12 +131,7 @@ class CoreGrpc: """ response = self.core.create_session() logging.info("created session: %s", response) - - # handle events session may broadcast - self.session_id = response.session_id - self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.handle_events) - # self.core.throughputs(self.log_throughput) + self.join_session(response.session_id) def delete_session(self, custom_sid=None): if custom_sid is None: @@ -164,28 +216,10 @@ class CoreGrpc: logging.info("set session state: %s", response) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.core.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) - # self.core.events(self.session_id, self.log_event) def delete_nodes(self, delete_session=None): if delete_session is None: @@ -213,29 +247,6 @@ class CoreGrpc: ) logging.info("delete links %s", response) - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface 1 %s", interface) - - return interface - # TODO add location, hooks, emane_config, etc... def start_session( self, @@ -274,29 +285,6 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - # if type1 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_1 - # if1 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 1 %s", if1) - # # interface1 = self.interface_helper.create_interface(id1, 0) - # - # if type2 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_2 - # if2 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 2: %s", if2) - response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) @@ -335,3 +323,279 @@ class CoreGrpc: """ logging.debug("Close grpc") self.core.close() + + def peek_id(self): + """ + Peek the next id to be used + + :return: nothing + """ + if len(self.reusable) == 0: + return self.id + else: + return self.reusable[0] + + def get_id(self): + """ + Get the next node id as well as update id status and reusable ids + + :rtype: int + :return: the next id to be used + """ + if len(self.reusable) == 0: + new_id = self.id + self.id = self.id + 1 + return new_id + else: + return self.reusable.pop(0) + + def add_node(self, node_type, model, x, y, name, node_id): + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + self.node_ids.append(node_id) + response = self.core.add_node(self.session_id, node) + logging.info("created node: %s", response) + if node_type == core_pb2.NodeType.WIRELESS_LAN: + d = OrderedDict() + d["basic_range"] = "275" + d["bandwidth"] = "54000000" + d["jitter"] = "0" + d["delay"] = "20000" + d["error"] = "0" + r = self.core.set_wlan_config(self.session_id, node_id, d) + logging.debug("set wlan config %s", r) + return response.node_id + + def add_graph_node(self, session_id, canvas_id, x, y, name): + """ + Add node, with information filled in, to grpc manager + + :param int session_id: session id + :param int canvas_id: node's canvas id + :param int x: x coord + :param int y: y coord + :param str name: node type + :return: nothing + """ + node_type = None + node_model = None + if name in link_layer_nodes: + if name == "switch": + node_type = core_pb2.NodeType.SWITCH + elif name == "hub": + node_type = core_pb2.NodeType.HUB + elif name == "wlan": + node_type = core_pb2.NodeType.WIRELESS_LAN + elif name == "rj45": + node_type = core_pb2.NodeType.RJ45 + elif name == "tunnel": + node_type = core_pb2.NodeType.TUNNEL + elif name in network_layer_nodes: + node_type = core_pb2.NodeType.DEFAULT + node_model = name + else: + logging.error("grpcmanagemeny.py INVALID node name") + nid = self.get_id() + create_node = Node(session_id, nid, node_type, node_model, x, y, name) + + # set default configuration for wireless node + self.wlanconfig_management.set_default_config(node_type, nid) + + self.nodes[canvas_id] = create_node + self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) + # self.core_id_to_canvas_id[nid] = canvas_id + logging.debug( + "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + session_id, + x, + y, + name, + ) + + def add_preexisting_node(self, canvas_node, session_id, core_node, name): + """ + Add preexisting nodes to grpc manager + + :param str name: node_type + :param core_pb2.Node core_node: core node grpc message + :param coretk.graph.CanvasNode canvas_node: canvas node + :param int session_id: session id + :return: nothing + """ + + # update the next available id + core_id = core_node.id + if self.id is None or core_id >= self.id: + self.id = core_id + 1 + self.preexisting.add(core_id) + n = Node( + session_id, + core_id, + core_node.type, + core_node.model, + canvas_node.x_coord, + canvas_node.y_coord, + name, + ) + self.nodes[canvas_node.id] = n + + def update_node_location(self, canvas_id, new_x, new_y): + """ + update node + + :param int canvas_id: canvas id of that node + :param int new_x: new x coord + :param int new_y: new y coord + :return: nothing + """ + self.nodes[canvas_id].x = new_x + self.nodes[canvas_id].y = new_y + + def update_reusable_id(self): + """ + Update available id for reuse + + :return: nothing + """ + if len(self.preexisting) > 0: + for i in range(1, self.id): + if i not in self.preexisting: + self.reusable.append(i) + + self.preexisting.clear() + logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) + + def delete_node(self, canvas_id): + """ + Delete a node from the session + + :param int canvas_id: node's id in the canvas + :return: thing + """ + try: + self.nodes.pop(canvas_id) + self.reusable.append(canvas_id) + self.reusable.sort() + except KeyError: + logging.error("grpcmanagement.py INVALID NODE CANVAS ID") + + def create_interface(self, node_type, gui_interface): + """ + create a protobuf interface given the interface object stored by the programmer + + :param core_bp2.NodeType type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface object + :rtype: core_bp2.Interface + :return: protobuf interface object + """ + if node_type != core_pb2.NodeType.DEFAULT: + return None + else: + interface = core_pb2.Interface( + id=gui_interface.id, + name=gui_interface.name, + mac=gui_interface.mac, + ip4=gui_interface.ipv4, + ip4mask=gui_interface.ip4prefix, + ) + logging.debug("create interface: %s", interface) + return interface + + def create_edge_interface(self, edge, src_canvas_id, dst_canvas_id): + """ + Create the interface for the two end of an edge, add a copy to node's interfaces + + :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param int src_canvas_id: canvas id for the source node + :param int dst_canvas_id: canvas id for the destination node + :return: nothing + """ + src_interface = None + dst_interface = None + print("create interface") + self.interfaces_manager.new_subnet() + + src_node = self.nodes[src_canvas_id] + if src_node.model in network_layer_nodes: + ifid = len(src_node.interfaces) + name = "eth" + str(ifid) + src_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[src_canvas_id].interfaces.append(src_interface) + logging.debug( + "Create source interface 1... IP: %s, name: %s", + src_interface.ipv4, + src_interface.name, + ) + + dst_node = self.nodes[dst_canvas_id] + if dst_node.model in network_layer_nodes: + ifid = len(dst_node.interfaces) + name = "eth" + str(ifid) + dst_interface = Interface( + name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + ) + self.nodes[dst_canvas_id].interfaces.append(dst_interface) + logging.debug( + "Create destination interface... IP: %s, name: %s", + dst_interface.ipv4, + dst_interface.name, + ) + + edge.interface_1 = src_interface + edge.interface_2 = dst_interface + return src_interface, dst_interface + + def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + """ + Add an edge to grpc manager + + :param int session_id: core session id + :param tuple(int, int) token: edge's identification in the canvas + :param int canvas_id_1: canvas id of source node + :param int canvas_id_2: canvas_id of destination node + + :return: nothing + """ + if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: + edge = Edge( + session_id, + self.nodes[canvas_id_1].node_id, + self.nodes[canvas_id_1].type, + self.nodes[canvas_id_2].node_id, + self.nodes[canvas_id_2].type, + ) + self.edges[token] = edge + src_interface, dst_interface = self.create_edge_interface( + edge, canvas_id_1, canvas_id_2 + ) + node_one_id = self.nodes[canvas_id_1].node_id + node_two_id = self.nodes[canvas_id_2].node_id + + # provide a way to get an edge from a core node and an interface id + if src_interface is not None: + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_one_id, src_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_one_id, + src_interface.id, + token, + ) + + if dst_interface is not None: + self.core_mapping.map_node_and_interface_to_canvas_edge( + node_two_id, dst_interface.id, token + ) + logging.debug( + "map node id %s, interface_id %s to edge token %s", + node_two_id, + dst_interface.id, + token, + ) + + logging.debug("Adding edge to grpc manager...") + else: + logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py index e808735f..6e767db3 100644 --- a/coretk/coretk/coretocanvas.py +++ b/coretk/coretk/coretocanvas.py @@ -8,7 +8,6 @@ class CoreToCanvasMapping: def __init__(self): self.core_id_to_canvas_id = {} self.core_node_and_interface_to_canvas_edge = {} - # self.edge_id_to_canvas_token = {} def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token @@ -33,13 +32,3 @@ class CoreToCanvasMapping: else: logging.debug("invalid key") return None - - # def add_mapping(self, core_id, canvas_id): - # if core_id not in self.core_id_to_canvas_id: - # self.core_id_to_canvas_id[core_id] = canvas_id - # else: - # logging.error("key already mapped") - # - # def delete_mapping(self, core_id): - # result = self.core_id_to_canvas_id.pop(core_id, None) - # return result diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 3a0b8623..b26269db 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -15,8 +15,8 @@ class CoreToolbarHelp: :return: nothing """ nodes = [] - manager = self.app.core_grpc.manager - for node in manager.nodes.values(): + core = self.app.core_grpc + for node in core.nodes.values(): pos = core_pb2.Position(x=int(node.x), y=int(node.y)) n = core_pb2.Node( id=node.node_id, type=node.type, position=pos, model=node.model @@ -32,8 +32,8 @@ class CoreToolbarHelp: :return: list of protobuf links """ links = [] - manager = self.app.core_grpc.manager - for edge in manager.edges.values(): + core = self.app.core_grpc + for edge in core.edges.values(): interface_one = self.app.core_grpc.create_interface( edge.type1, edge.interface_1 ) @@ -56,61 +56,20 @@ class CoreToolbarHelp: interface_two=interface_two, ) links.append(link) - # self.id1 = edge.id1 - # self.id2 = edge.id2 - # self.type = link_type - # self.if1 = interface_one - # self.if2 = interface_two return links def get_wlan_configuration_list(self): configs = [] - manager = self.app.core_grpc.manager - manager_configs = manager.wlanconfig_management.configurations + core = self.app.core_grpc + manager_configs = core.wlanconfig_management.configurations for key in manager_configs: cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) configs.append(cnf) return configs def gui_start_session(self): - # list(core_pb2.Node) nodes = self.get_node_list() - - # list(core_bp2.Link) links = self.get_link_list() - - # print(links[0]) wlan_configs = self.get_wlan_configuration_list() - # print(wlan_configs) self.app.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) - # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - - # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session - # print(res) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - - # print(res) - - # def add_nodes(self): - # """ - # add the nodes stored in grpc manager - # :return: nothing - # """ - # grpc_manager = self.application.canvas.grpc_manager - # for node in grpc_manager.nodes.values(): - # self.application.core_grpc.add_node( - # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - # ) - # - # def add_edges(self): - # """ - # add the edges stored in grpc manager - # :return: - # """ - # grpc_manager = self.application.canvas.grpc_manager - # for edge in grpc_manager.edges.values(): - # self.application.core_grpc.add_link( - # edge.id1, edge.id2, edge.type1, edge.type2, edge - # ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5b76cdcb..7859a8e6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -147,7 +147,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.core_grpc.manager.add_preexisting_node(n, session.id, node, name) + self.core_grpc.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +179,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.core_grpc.manager.add_edge(session.id, e.token, n1.id, n2.id) + self.core_grpc.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,12 +208,12 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core_grpc.manager.edges[e.token].interface_1 = if1 - self.core_grpc.manager.edges[e.token].interface_2 = if2 - self.core_grpc.manager.nodes[ + self.core_grpc.edges[e.token].interface_1 = if1 + self.core_grpc.edges[e.token].interface_2 = if2 + self.core_grpc.nodes[ core_id_to_canvas_id[link.node_one_id] ].interfaces.append(if1) - self.core_grpc.manager.nodes[ + self.core_grpc.nodes[ core_id_to_canvas_id[link.node_two_id] ].interfaces.append(if2) @@ -318,13 +318,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.core_grpc.manager.add_edge( + self.core_grpc.add_edge( self.core_grpc.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.core_grpc.manager.edges[edge.token].interface_1 - if2 = self.core_grpc.manager.edges[edge.token].interface_2 + if1 = self.core_grpc.edges[edge.token].interface_1 + if2 = self.core_grpc.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -390,10 +390,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.core_grpc.manager.peek_id(), + core_id=self.core_grpc.peek_id(), ) self.nodes[node.id] = node - self.core_grpc.manager.add_node( + self.core_grpc.add_graph_node( self.core_grpc.session_id, node.id, x, y, node_name ) return node @@ -485,7 +485,7 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.core_grpc.manager.nodes[self.id].node_id + node_id = self.canvas.core_grpc.nodes[self.id].node_id state = self.canvas.core_grpc.get_session_state() if state == core_pb2.SessionState.RUNTIME: self.canvas.core_grpc.launch_terminal(node_id) @@ -511,9 +511,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.manager.update_node_location( - self.id, self.x_coord, self.y_coord - ) + self.canvas.core_grpc.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py deleted file mode 100644 index 85d8c8f5..00000000 --- a/coretk/coretk/grpcmanagement.py +++ /dev/null @@ -1,328 +0,0 @@ -""" -Manage useful informations about the nodes, edges and configuration -that can be useful for grpc, acts like a session class -""" -import logging - -from core.api.grpc import core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping -from coretk.interface import Interface, InterfaceManager -from coretk.wlannodeconfig import WlanNodeConfig - -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] - - -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - -class GrpcManager: - def __init__(self, grpc): - self.nodes = {} - self.edges = {} - self.id = 1 - # A list of id for re-use, keep in increasing order - self.reusable = [] - self.preexisting = set() - self.core_grpc = grpc - # self.update_preexisting_ids() - # self.core_id_to_canvas_id = {} - self.interfaces_manager = InterfaceManager() - # map tuple(core_node_id, interface_id) to and edge - # self.node_id_and_interface_to_edge_token = {} - self.core_mapping = CoreToCanvasMapping() - self.wlanconfig_management = WlanNodeConfig() - - def update_preexisting_ids(self): - """ - get preexisting node ids - :return: - """ - max_id = 0 - client = self.core_grpc.core - sessions = client.get_sessions().sessions - for session_summary in sessions: - session = client.get_session(session_summary.id).session - for node in session.nodes: - if node.id > max_id: - max_id = node.id - self.preexisting.append(node.id) - self.id = max_id + 1 - self.update_reusable_id() - - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - - def get_id(self): - """ - Get the next node id as well as update id status and reusable ids - - :rtype: int - :return: the next id to be used - """ - if len(self.reusable) == 0: - new_id = self.id - self.id = self.id + 1 - return new_id - else: - return self.reusable.pop(0) - - def add_node(self, session_id, canvas_id, x, y, name): - """ - Add node, with information filled in, to grpc manager - - :param int session_id: session id - :param int canvas_id: node's canvas id - :param int x: x coord - :param int y: y coord - :param str name: node type - :return: nothing - """ - node_type = None - node_model = None - if name in link_layer_nodes: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name in network_layer_nodes: - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("grpcmanagemeny.py INVALID node name") - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) - - # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id - logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", - session_id, - x, - y, - name, - ) - - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.add(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reusable.append(canvas_id) - self.reusable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - - def create_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[src_canvas_id].interfaces.append(src_interface) - logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, - ) - - dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): - """ - Add an edge to grpc manager - - :param int session_id: core session id - :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node - - :return: nothing - """ - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id - - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) - - if dst_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - - logging.debug("Adding edge to grpc manager...") - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 17bc99c8..a306bfdc 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -130,7 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core_grpc.manager.core_mapping.get_token_from_node_and_interface( + token = self.core_grpc.core_mapping.get_token_from_node_and_interface( nid, iid ) print(token) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 3e4a98f8..0c45e896 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -5,10 +5,10 @@ from core.api.grpc import core_pb2 class WirelessConnection: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core_grpc): self.canvas = canvas - self.core_grpc = grpc - self.core_mapping = grpc.manager.core_mapping + self.core_grpc = core_grpc + self.core_mapping = core_grpc.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 6fa3beb1c1442871dbaa08a7d33edf9753946c38 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:42:49 -0700 Subject: [PATCH 044/305] refactoring to change core_grpc to core, and update CoreGrpc to be CoreClient and have core now be client, also refactored usages of application to be just app to keep it short --- coretk/coretk/app.py | 8 +-- coretk/coretk/{coregrpc.py => coreclient.py} | 72 ++++++++++---------- coretk/coretk/coremenubar.py | 44 ++++++------ coretk/coretk/coretoolbar.py | 14 ++-- coretk/coretk/coretoolbarhelp.py | 19 ++---- coretk/coretk/dialogs/sessionoptions.py | 8 +-- coretk/coretk/dialogs/sessions.py | 10 +-- coretk/coretk/graph.py | 56 ++++++++------- coretk/coretk/graph_helper.py | 4 +- coretk/coretk/linkinfo.py | 10 ++- coretk/coretk/menuaction.py | 32 ++++----- coretk/coretk/setwallpaper.py | 54 +++++++-------- coretk/coretk/sizeandscale.py | 34 ++++----- coretk/coretk/wirelessconnection.py | 5 +- 14 files changed, 176 insertions(+), 194 deletions(-) rename coretk/coretk/{coregrpc.py => coreclient.py} (90%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bcc43e43..72749524 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import logging import tkinter as tk -from coretk.coregrpc import CoreGrpc +from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph @@ -25,12 +25,12 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.core_grpc = CoreGrpc(self) + self.core = CoreClient(self) self.setup_app() self.create_menu() self.create_widgets() self.draw_canvas() - self.core_grpc.set_up() + self.core.set_up() def setup_app(self): self.master.title("CORE") @@ -54,7 +54,7 @@ class Application(tk.Frame): def draw_canvas(self): self.canvas = CanvasGraph( - self, self.core_grpc, background="#cccccc", scrollregion=(0, 0, 1200, 1000) + self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coreclient.py similarity index 90% rename from coretk/coretk/coregrpc.py rename to coretk/coretk/coreclient.py index 94f5ca7b..e5ba838e 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coreclient.py @@ -56,13 +56,13 @@ class Edge: self.interface_2 = None -class CoreGrpc: - def __init__(self, app, sid=None): +class CoreClient: + def __init__(self, app): """ Create a CoreGrpc instance """ - self.core = client.CoreGrpcClient() - self.session_id = sid + self.client = client.CoreGrpcClient() + self.session_id = None self.node_ids = [] self.app = app self.master = app.master @@ -99,9 +99,9 @@ class CoreGrpc: def join_session(self, session_id): # query session and set as current session self.session_id = session_id - response = self.core.get_session(self.session_id) + response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) - self.core.events(self.session_id, self.handle_events) + self.client.events(self.session_id, self.handle_events) # set title to session self.master.title(f"CORE Session({self.session_id})") @@ -129,7 +129,7 @@ class CoreGrpc: :return: nothing """ - response = self.core.create_session() + response = self.client.create_session() logging.info("created session: %s", response) self.join_session(response.session_id) @@ -138,15 +138,15 @@ class CoreGrpc: sid = self.session_id else: sid = custom_sid - response = self.core.delete_session(sid) + response = self.client.delete_session(sid) logging.info("Deleted session result: %s", response) - def terminate_session(self, custom_sid=None): + def shutdown_session(self, custom_sid=None): if custom_sid is None: sid = self.session_id else: sid = custom_sid - s = self.core.get_session(sid).session + s = self.client.get_session(sid).session # delete links and nodes from running session if s.state == core_pb2.SessionState.RUNTIME: self.set_session_state("datacollect", sid) @@ -160,8 +160,8 @@ class CoreGrpc: :return: existing sessions """ - self.core.connect() - response = self.core.get_sessions() + self.client.connect() + response = self.client.get_sessions() # if there are no sessions, create a new session, else join a session sessions = response.sessions @@ -172,7 +172,7 @@ class CoreGrpc: dialog.show() def get_session_state(self): - response = self.core.get_session(self.session_id) + response = self.client.get_session(self.session_id) # logging.info("get session: %s", response) return response.session.state @@ -190,27 +190,29 @@ class CoreGrpc: response = None if state == "configuration": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.CONFIGURATION ) elif state == "instantiation": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.INSTANTIATION ) elif state == "datacollect": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.DATACOLLECT ) elif state == "shutdown": - response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) + response = self.client.set_session_state( + sid, core_pb2.SessionState.SHUTDOWN + ) elif state == "runtime": - response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) + response = self.client.set_session_state(sid, core_pb2.SessionState.RUNTIME) elif state == "definition": - response = self.core.set_session_state( + response = self.client.set_session_state( sid, core_pb2.SessionState.DEFINITION ) elif state == "none": - response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) + response = self.client.set_session_state(sid, core_pb2.SessionState.NONE) else: logging.error("coregrpc.py: set_session_state: INVALID STATE") @@ -218,7 +220,7 @@ class CoreGrpc: def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(self.session_id, node_id, position) + response = self.client.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) def delete_nodes(self, delete_session=None): @@ -226,8 +228,8 @@ class CoreGrpc: sid = self.session_id else: sid = delete_session - for node in self.core.get_session(sid).session.nodes: - response = self.core.delete_node(self.session_id, node.id) + for node in self.client.get_session(sid).session.nodes: + response = self.client.delete_node(self.session_id, node.id) logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): @@ -237,8 +239,8 @@ class CoreGrpc: else: sid = delete_session - for link in self.core.get_session(sid).session.links: - response = self.core.delete_link( + for link in self.client.get_session(sid).session.links: + response = self.client.delete_link( self.session_id, link.node_one_id, link.node_two_id, @@ -259,7 +261,7 @@ class CoreGrpc: wlan_configs=None, mobility_configs=None, ): - response = self.core.start_session( + response = self.client.start_session( session_id=self.session_id, nodes=nodes, links=links, @@ -268,7 +270,7 @@ class CoreGrpc: logging.debug("Start session %s, result: %s", self.session_id, response.result) def stop_session(self): - response = self.core.stop_session(session_id=self.session_id) + response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) # TODO no need, might get rid of this @@ -285,11 +287,11 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - response = self.core.add_link(self.session_id, id1, id2, if1, if2) + response = self.client.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) def launch_terminal(self, node_id): - response = self.core.get_node_terminal(self.session_id, node_id) + response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) os.system("xterm -e %s &" % response.terminal) @@ -300,9 +302,9 @@ class CoreGrpc: :param str file_path: file path that user pick :return: nothing """ - response = self.core.save_xml(self.session_id, file_path) + response = self.client.save_xml(self.session_id, file_path) logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.handle_events) + self.client.events(self.session_id, self.handle_events) def open_xml(self, file_path): """ @@ -311,7 +313,7 @@ class CoreGrpc: :param str file_path: file to open :return: session id """ - response = self.core.open_xml(file_path) + response = self.client.open_xml(file_path) logging.debug("open xml: %s", response) self.join_session(response.session_id) @@ -322,7 +324,7 @@ class CoreGrpc: :return: nothing """ logging.debug("Close grpc") - self.core.close() + self.client.close() def peek_id(self): """ @@ -353,7 +355,7 @@ class CoreGrpc: position = core_pb2.Position(x=x, y=y) node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) + response = self.client.add_node(self.session_id, node) logging.info("created node: %s", response) if node_type == core_pb2.NodeType.WIRELESS_LAN: d = OrderedDict() @@ -362,7 +364,7 @@ class CoreGrpc: d["jitter"] = "0" d["delay"] = "20000" d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) + r = self.client.set_wlan_config(self.session_id, node_id, d) logging.debug("set wlan config %s", r) return response.node_id diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index aa00a927..7b3f2633 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -9,19 +9,19 @@ class CoreMenubar(object): Core menubar """ - def __init__(self, application, master, menubar): + def __init__(self, app, master, menubar): """ Create a CoreMenubar instance :param master: :param tkinter.Menu menubar: menubar object - :param coretk.app.Application application: application object + :param coretk.app.Application app: application object """ self.menubar = menubar self.master = master - self.application = application - self.menuaction = action.MenuAction(application, master) - self.menu_action = MenuAction(self.application, self.master) + self.app = app + self.menuaction = action.MenuAction(app, master) + self.menu_action = MenuAction(self.app, self.master) # def on_quit(self): # """ @@ -649,23 +649,23 @@ class CoreMenubar(object): :return: nothing """ - self.application.bind_all("", action.file_new_shortcut) - self.application.bind_all("", action.file_open_shortcut) - self.application.bind_all("", action.file_save_shortcut) - self.application.bind_all("", action.edit_undo_shortcut) - self.application.bind_all("", action.edit_redo_shortcut) - self.application.bind_all("", action.edit_cut_shortcut) - self.application.bind_all("", action.edit_copy_shortcut) - self.application.bind_all("", action.edit_paste_shortcut) - self.application.bind_all("", action.edit_select_all_shortcut) - self.application.bind_all("", action.edit_select_adjacent_shortcut) - self.application.bind_all("", action.edit_find_shortcut) - self.application.bind_all("", action.canvas_previous_shortcut) - self.application.bind_all("", action.canvas_next_shortcut) - self.application.bind_all("", action.canvas_first_shortcut) - self.application.bind_all("", action.canvas_last_shortcut) - self.application.bind_all("", action.view_zoom_in_shortcut) - self.application.bind_all("", action.view_zoom_out_shortcut) + self.app.bind_all("", action.file_new_shortcut) + self.app.bind_all("", action.file_open_shortcut) + self.app.bind_all("", action.file_save_shortcut) + self.app.bind_all("", action.edit_undo_shortcut) + self.app.bind_all("", action.edit_redo_shortcut) + self.app.bind_all("", action.edit_cut_shortcut) + self.app.bind_all("", action.edit_copy_shortcut) + self.app.bind_all("", action.edit_paste_shortcut) + self.app.bind_all("", action.edit_select_all_shortcut) + self.app.bind_all("", action.edit_select_adjacent_shortcut) + self.app.bind_all("", action.edit_find_shortcut) + self.app.bind_all("", action.canvas_previous_shortcut) + self.app.bind_all("", action.canvas_next_shortcut) + self.app.bind_all("", action.canvas_first_shortcut) + self.app.bind_all("", action.canvas_last_shortcut) + self.app.bind_all("", action.view_zoom_in_shortcut) + self.app.bind_all("", action.view_zoom_out_shortcut) def create_core_menubar(self): """ diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 78688725..5d7c05ae 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -25,14 +25,14 @@ class CoreToolbar(object): Core toolbar class """ - def __init__(self, application, edit_frame, menubar): + def __init__(self, app, edit_frame, menubar): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - self.application = application - self.master = application.master + self.app = app + self.master = app.master self.edit_frame = edit_frame self.menubar = menubar self.radio_value = tk.IntVar() @@ -161,7 +161,7 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click START STOP SESSION button") - helper = CoreToolbarHelp(self.application) + helper = CoreToolbarHelp(self.app) self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT @@ -593,11 +593,7 @@ class CoreToolbar(object): """ logging.debug("Click on STOP button ") self.destroy_children_widgets() - - # self.canvas.core_grpc.set_session_state(SessionStateEnum.DATACOLLECT.value) - # self.canvas.core_grpc.delete_links() - # self.canvas.core_grpc.delete_nodes() - self.canvas.core_grpc.stop_session() + self.app.core.stop_session() self.create_toolbar() def click_run_button(self): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index b26269db..ca30141d 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -15,8 +15,7 @@ class CoreToolbarHelp: :return: nothing """ nodes = [] - core = self.app.core_grpc - for node in core.nodes.values(): + for node in self.app.core.nodes.values(): pos = core_pb2.Position(x=int(node.x), y=int(node.y)) n = core_pb2.Node( id=node.node_id, type=node.type, position=pos, model=node.model @@ -32,14 +31,9 @@ class CoreToolbarHelp: :return: list of protobuf links """ links = [] - core = self.app.core_grpc - for edge in core.edges.values(): - interface_one = self.app.core_grpc.create_interface( - edge.type1, edge.interface_1 - ) - interface_two = self.app.core_grpc.create_interface( - edge.type2, edge.interface_2 - ) + for edge in self.app.core.edges.values(): + interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) + interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) # TODO for now only consider the basic cases if ( edge.type1 == core_pb2.NodeType.WIRELESS_LAN @@ -61,8 +55,7 @@ class CoreToolbarHelp: def get_wlan_configuration_list(self): configs = [] - core = self.app.core_grpc - manager_configs = core.wlanconfig_management.configurations + manager_configs = self.app.core.wlanconfig_management.configurations for key in manager_configs: cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) configs.append(cnf) @@ -72,4 +65,4 @@ class CoreToolbarHelp: nodes = self.get_node_list() links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() - self.app.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + self.app.core.start_session(nodes, links, wlan_configs=wlan_configs) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index a34c6658..6279d844 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -18,8 +18,8 @@ class SessionOptionsDialog(Dialog): self.draw() def draw(self): - session_id = self.master.core_grpc.session_id - response = self.master.core_grpc.core.get_session_options(session_id) + session_id = self.app.core.session_id + response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) self.options = response.config self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) @@ -28,7 +28,7 @@ class SessionOptionsDialog(Dialog): def save(self): config = configutils.parse_config(self.options, self.values) - session_id = self.master.core_grpc.session_id - response = self.master.core_grpc.core.set_session_options(session_id, config) + session_id = self.app.core.session_id + response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b2bd90f0..b7c4d128 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -12,7 +12,7 @@ class SessionsDialog(Dialog): """ create session table instance - :param coretk.coregrpc.CoreGrpc grpc: coregrpc + :param coretk.coreclient.CoreClient grpc: coregrpc :param root.master master: """ super().__init__(master, app, "Sessions", modal=True) @@ -51,7 +51,7 @@ class SessionsDialog(Dialog): self.tree.column("nodes", stretch=tk.YES) self.tree.heading("nodes", text="Node Count") - response = self.app.core_grpc.core.get_sessions() + response = self.app.core.client.get_sessions() logging.info("sessions: %s", response) for index, session in enumerate(response.sessions): state_name = core_pb2.SessionState.Enum.Name(session.state) @@ -105,7 +105,7 @@ class SessionsDialog(Dialog): b.grid(row=0, column=3, padx=2, sticky="ew") def click_new(self): - self.app.core_grpc.create_new_session() + self.app.core.create_new_session() self.destroy() def click_select(self, event): @@ -142,7 +142,7 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - self.app.core_grpc.join_session(session_id) + self.app.core.join_session(session_id) self.destroy() def on_selected(self, event): @@ -151,6 +151,6 @@ class SessionsDialog(Dialog): self.join_session(sid) def shutdown_session(self, sid): - self.app.core_grpc.terminate_session(sid) + self.app.core.shutdown_session(sid) self.click_new() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7859a8e6..e53a5274 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -26,7 +26,7 @@ CORE_EMANE = ["emane"] class CanvasGraph(tk.Canvas): - def __init__(self, master, core_grpc, cnf=None, **kwargs): + def __init__(self, master, core, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -45,10 +45,10 @@ class CanvasGraph(tk.Canvas): self.setup_menus() self.setup_bindings() self.draw_grid() - self.core_grpc = core_grpc - self.helper = GraphHelper(self, core_grpc) - self.throughput_draw = Throughput(self, core_grpc) - self.wireless_draw = WirelessConnection(self, core_grpc) + self.core = core + self.helper = GraphHelper(self, core) + self.throughput_draw = Throughput(self, core) + self.wireless_draw = WirelessConnection(self, core) self.is_node_context_opened = False def setup_menus(self): @@ -147,7 +147,7 @@ class CanvasGraph(tk.Canvas): core_id_to_canvas_id[node.id] = n.id # store the node in grpc manager - self.core_grpc.add_preexisting_node(n, session.id, node, name) + self.core.add_preexisting_node(n, session.id, node, name) # draw existing links for link in session.links: @@ -179,7 +179,7 @@ class CanvasGraph(tk.Canvas): n1.edges.add(e) n2.edges.add(e) self.edges[e.token] = e - self.core_grpc.add_edge(session.id, e.token, n1.id, n2.id) + self.core.add_edge(session.id, e.token, n1.id, n2.id) self.helper.redraw_antenna(link, n1, n2) @@ -208,14 +208,14 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core_grpc.edges[e.token].interface_1 = if1 - self.core_grpc.edges[e.token].interface_2 = if2 - self.core_grpc.nodes[ - core_id_to_canvas_id[link.node_one_id] - ].interfaces.append(if1) - self.core_grpc.nodes[ - core_id_to_canvas_id[link.node_two_id] - ].interfaces.append(if2) + self.core.edges[e.token].interface_1 = if1 + self.core.edges[e.token].interface_2 = if2 + self.core.nodes[core_id_to_canvas_id[link.node_one_id]].interfaces.append( + if1 + ) + self.core.nodes[core_id_to_canvas_id[link.node_two_id]].interfaces.append( + if2 + ) # lift the nodes so they on top of the links for i in self.find_withtag("node"): @@ -318,13 +318,13 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - self.core_grpc.add_edge( - self.core_grpc.session_id, edge.token, node_src.id, node_dst.id + self.core.add_edge( + self.core.session_id, edge.token, node_src.id, node_dst.id ) # draw link info on the edge - if1 = self.core_grpc.edges[edge.token].interface_1 - if2 = self.core_grpc.edges[edge.token].interface_2 + if1 = self.core.edges[edge.token].interface_1 + if2 = self.core.edges[edge.token].interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -390,12 +390,10 @@ class CanvasGraph(tk.Canvas): image=image, node_type=node_name, canvas=self, - core_id=self.core_grpc.peek_id(), + core_id=self.core.peek_id(), ) self.nodes[node.id] = node - self.core_grpc.add_graph_node( - self.core_grpc.session_id, node.id, x, y, node_name - ) + self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node @@ -485,10 +483,10 @@ class CanvasNode: self.moving = None def double_click(self, event): - node_id = self.canvas.core_grpc.nodes[self.id].node_id - state = self.canvas.core_grpc.get_session_state() + node_id = self.canvas.core.nodes[self.id].node_id + state = self.canvas.core.get_session_state() if state == core_pb2.SessionState.RUNTIME: - self.canvas.core_grpc.launch_terminal(node_id) + self.canvas.core.launch_terminal(node_id) else: self.canvas.canvas_action.display_configuration(self) # if self.node_type in CORE_NODES: @@ -511,7 +509,7 @@ class CanvasNode: def click_release(self, event): logging.debug(f"click release {self.name}: {event}") self.update_coords() - self.canvas.core_grpc.update_node_location(self.id, self.x_coord, self.y_coord) + self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): @@ -529,8 +527,8 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) - if self.canvas.core_grpc.get_session_state() == core_pb2.SessionState.RUNTIME: - self.canvas.core_grpc.edit_node(self.core_id, int(new_x), int(new_y)) + if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: + self.canvas.core.edit_node(self.core_id, int(new_x), int(new_y)) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index ba58a435..bdedd65e 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -11,12 +11,12 @@ CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] class GraphHelper: - def __init__(self, canvas, grpc): + def __init__(self, canvas, core): """ create an instance of GraphHelper object """ self.canvas = canvas - self.core_grpc = grpc + self.core = core def delete_canvas_components(self): """ diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index a306bfdc..916e8dfb 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -22,7 +22,7 @@ class LinkInfo: self.edge = edge # self.edge_id = edge.id self.radius = 37 - self.core_grpc = self.canvas.core_grpc + self.core = self.canvas.core self.ip4_address_1 = ip4_src self.ip6_address_1 = ip6_src @@ -104,13 +104,13 @@ class LinkInfo: class Throughput: - def __init__(self, canvas, core_grpc): + def __init__(self, canvas, core): """ create an instance of Throughput object :param coretk.app.Application app: application """ self.canvas = canvas - self.core_grpc = core_grpc + self.core = core # edge canvas id mapped to throughput value self.tracker = {} # map an edge canvas id to a throughput canvas id @@ -130,9 +130,7 @@ class Throughput: iid = t.interface_id tp = t.throughput # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core_grpc.core_mapping.get_token_from_node_and_interface( - nid, iid - ) + token = self.core.core_mapping.get_token_from_node_and_interface(nid, iid) print(token) edge_id = self.canvas.edges[token].id diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 4ed5e529..7598f210 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -321,10 +321,9 @@ class MenuAction: Actions performed when choosing menu items """ - def __init__(self, application, master): + def __init__(self, app, master): self.master = master - self.application = application - self.core_grpc = application.core_grpc + self.app = app def prompt_save_running_session(self): """ @@ -335,21 +334,20 @@ class MenuAction: logging.info( "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) - grpc = self.application.core_grpc - state = grpc.get_session_state() + state = self.app.core.get_session_state() if ( state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION ): - grpc.delete_session() + self.app.core.delete_session() else: msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") if msgbox or msgbox is False: if msgbox: - grpc.stop_session() - grpc.delete_session() + self.app.core.stop_session() + self.app.core.delete_session() def on_quit(self): """ @@ -358,23 +356,21 @@ class MenuAction: :return: nothing """ self.prompt_save_running_session() - # self.application.core_grpc.close() - self.application.quit() + self.app.quit() def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") - grpc = self.application.core_grpc file_path = filedialog.asksaveasfilename( initialdir=SAVEDIR, title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - grpc.save_xml(file_path) + self.app.core.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") - self.application.is_open_xml = True + self.app.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=SAVEDIR, title="Open", @@ -382,17 +378,17 @@ class MenuAction: ) # clean up before opening a new session self.prompt_save_running_session() - self.application.core_grpc.open_xml(file_path) + self.app.core.open_xml(file_path) # Todo might not need # self.application.core_editbar.destroy_children_widgets() # self.application.core_editbar.create_toolbar() def canvas_size_and_scale(self): - self.application.size_and_scale = SizeAndScale(self.application) + self.app.size_and_scale = SizeAndScale(self.app) def canvas_set_wallpaper(self): - self.application.set_wallpaper = CanvasWallpaper(self.application) + self.app.set_wallpaper = CanvasWallpaper(self.app) def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") @@ -402,10 +398,10 @@ class MenuAction: def session_options(self): logging.debug("Click session options") - dialog = SessionOptionsDialog(self.application, self.application) + dialog = SessionOptionsDialog(self.app, self.app) dialog.show() def session_change_sessions(self): logging.debug("Click session change sessions") - dialog = SessionsDialog(self.application, self.application) + dialog = SessionsDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index beec0162..edace83c 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -22,24 +22,24 @@ class ScaleOption(enum.Enum): class CanvasWallpaper: - def __init__(self, application): + def __init__(self, app): """ create an instance of CanvasWallpaper object - :param coretk.app.Application application: root application + :param coretk.app.Application app: root application """ - self.application = application - self.canvas = self.application.canvas + self.app = app + self.canvas = self.app.canvas self.top = tk.Toplevel() self.top.title("Set Canvas Wallpaper") self.radiovar = tk.IntVar() - print(self.application.radiovar.get()) - self.radiovar.set(self.application.radiovar.get()) + print(self.app.radiovar.get()) + self.radiovar.set(self.app.radiovar.get()) self.show_grid_var = tk.IntVar() - self.show_grid_var.set(self.application.show_grid_var.get()) + self.show_grid_var.set(self.app.show_grid_var.get()) self.adjust_to_dim_var = tk.IntVar() - self.adjust_to_dim_var.set(self.application.adjust_to_dim_var.get()) + self.adjust_to_dim_var.set(self.app.adjust_to_dim_var.get()) self.create_image_label() self.create_text_label() @@ -181,7 +181,7 @@ class CanvasWallpaper: :return: nothing """ - canvas = self.application.canvas + canvas = self.app.canvas grid = canvas.find_withtag("rectangle")[0] x0, y0, x1, y1 = canvas.coords(grid) canvas_w = abs(x0 - x1) @@ -215,7 +215,7 @@ class CanvasWallpaper: cropped_tk = ImageTk.PhotoImage(cropped) # place left corner of image to the left corner of the canvas - self.application.croppedwallpaper = cropped_tk + self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() @@ -223,7 +223,7 @@ class CanvasWallpaper: wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def center(self, img): """ @@ -252,13 +252,13 @@ class CanvasWallpaper: cropped_tk = ImageTk.PhotoImage(cropped) # place the center of the image at the center of the canvas - self.application.croppedwallpaper = cropped_tk + self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def scaled(self, img): """ @@ -270,7 +270,7 @@ class CanvasWallpaper: canvas_w, canvas_h = self.get_canvas_width_and_height() resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image_tk = ImageTk.PhotoImage(resized_image) - self.application.croppedwallpaper = image_tk + self.app.croppedwallpaper = image_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() @@ -278,7 +278,7 @@ class CanvasWallpaper: wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" ) - self.application.wallpaper_id = wid + self.app.wallpaper_id = wid def tiled(self, img): return @@ -301,15 +301,15 @@ class CanvasWallpaper: self.delete_canvas_components(["wallpaper"]) self.draw_new_canvas(img_w, img_h) wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.application.croppedwallpaper = image_tk - self.application.wallpaper_id = wid + self.app.croppedwallpaper = image_tk + self.app.wallpaper_id = wid def show_grid(self): """ :return: nothing """ - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.show_grid_var.get() == 0: for i in self.canvas.find_withtag("gridline"): @@ -322,9 +322,9 @@ class CanvasWallpaper: logging.error("setwallpaper.py show_grid invalid value") def save_wallpaper_options(self): - self.application.radiovar.set(self.radiovar.get()) - self.application.show_grid_var.set(self.show_grid_var.get()) - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.radiovar.set(self.radiovar.get()) + self.app.show_grid_var.set(self.show_grid_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) def click_apply(self): img_link_frame = self.top.grid_slaves(2, 0)[0] @@ -332,23 +332,23 @@ class CanvasWallpaper: if not filename: self.delete_canvas_components(["wallpaper"]) self.top.destroy() - self.application.current_wallpaper = None + self.app.current_wallpaper = None self.save_wallpaper_options() return try: img = Image.open(filename) - self.application.current_wallpaper = img + self.app.current_wallpaper = img except FileNotFoundError: print("invalid filename, draw original white plot") - if self.application.wallpaper_id: - self.canvas.delete(self.application.wallpaper_id) + if self.app.wallpaper_id: + self.canvas.delete(self.app.wallpaper_id) self.top.destroy() return - self.application.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.adjust_to_dim_var.get() == 0: - self.application.radiovar.set(self.radiovar.get()) + self.app.radiovar.set(self.radiovar.get()) if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: self.upper_left(img) diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/sizeandscale.py index 3552c2e5..5c746c39 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/sizeandscale.py @@ -10,16 +10,16 @@ DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] class SizeAndScale: - def __init__(self, application): + def __init__(self, app): """ create an instance for size and scale object - :param application: main application + :param app: main application """ - self.application = application + self.app = app self.top = tk.Toplevel() self.top.title("Canvas Size and Scale") - self.meter_per_pixel = self.application.canvas.meters_per_pixel + self.meter_per_pixel = self.app.canvas.meters_per_pixel self.size_chart() self.scale_chart() @@ -108,7 +108,7 @@ class SizeAndScale: label = tk.Label(self.top, text="Size") label.grid(sticky=tk.W, padx=5) - canvas = self.application.canvas + canvas = self.app.canvas plot = canvas.find_withtag("rectangle") x0, y0, x1, y1 = canvas.bbox(plot[0]) w = abs(x0 - x1) - 2 @@ -222,7 +222,7 @@ class SizeAndScale: :param int pixel_height: height in pixel :return: nothing """ - canvas = self.application.canvas + canvas = self.app.canvas canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) # delete old plot and redraw @@ -246,24 +246,24 @@ class SizeAndScale: scale_frame = self.top.grid_slaves(3, 0)[0] meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 - self.application.canvas.meters_per_pixel = meter_per_pixel + self.app.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid(pixel_width, pixel_height) - print(self.application.current_wallpaper) - print(self.application.radiovar) + print(self.app.current_wallpaper) + print(self.app.radiovar) # if there is a current wallpaper showing, redraw it based on current wallpaper options - wallpaper_tool = self.application.set_wallpaper - current_wallpaper = self.application.current_wallpaper + wallpaper_tool = self.app.set_wallpaper + current_wallpaper = self.app.current_wallpaper if current_wallpaper: - if self.application.adjust_to_dim_var.get() == 0: - if self.application.radiovar.get() == ScaleOption.UPPER_LEFT.value: + if self.app.adjust_to_dim_var.get() == 0: + if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: wallpaper_tool.upper_left(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.CENTERED.value: + elif self.app.radiovar.get() == ScaleOption.CENTERED.value: wallpaper_tool.center(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.SCALED.value: + elif self.app.radiovar.get() == ScaleOption.SCALED.value: wallpaper_tool.scaled(current_wallpaper) - elif self.application.radiovar.get() == ScaleOption.TILED.value: + elif self.app.radiovar.get() == ScaleOption.TILED.value: print("not implemented") - elif self.application.adjust_to_dim_var.get() == 1: + elif self.app.adjust_to_dim_var.get() == 1: wallpaper_tool.canvas_to_image_dimension(current_wallpaper) wallpaper_tool.show_grid() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 0c45e896..d1e17bc5 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -5,10 +5,9 @@ from core.api.grpc import core_pb2 class WirelessConnection: - def __init__(self, canvas, core_grpc): + def __init__(self, canvas, core): self.canvas = canvas - self.core_grpc = core_grpc - self.core_mapping = core_grpc.core_mapping + self.core_mapping = core.core_mapping # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} From 6c49a73e3883e34f885c577ef4c5ff3d5966ea27 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:45:42 -0700 Subject: [PATCH 045/305] removed old image, fixed get image calls in images.py --- coretk/coretk/images.py | 4 ++-- coretk/coretk/switch.png | Bin 5286 -> 0 bytes 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 coretk/coretk/switch.png diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 3f230fd8..7a7c0331 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -46,9 +46,9 @@ class Images: if node_type == core_pb2.NodeType.HUB: return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE.value), "emane" + return Images.get(ImageEnum.EMANE), "emane" if node_type == core_pb2.NodeType.RJ45: return Images.get(ImageEnum.RJ45), "rj45" diff --git a/coretk/coretk/switch.png b/coretk/coretk/switch.png deleted file mode 100644 index f8c852947639b68084a32c96f156ec1be99829e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5286 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 M8hYwA5Ua@l0JEh!?f?J) From 0f78acaa0ce4c55bd99a753b93ec37fc3ccb6bc8 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:57:41 -0700 Subject: [PATCH 046/305] update method names for drawing main app, moved delete window call into app --- coretk/coretk/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 72749524..73cbcfbd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -27,26 +27,27 @@ class Application(tk.Frame): self.adjust_to_dim_var = tk.IntVar(value=0) self.core = CoreClient(self) self.setup_app() - self.create_menu() - self.create_widgets() + self.draw_menu() + self.draw_toolbar() self.draw_canvas() self.core.set_up() def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") + self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) - def create_menu(self): + def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) self.core_menu = CoreMenubar(self, self.master, self.menubar) self.core_menu.create_core_menubar() self.master.config(menu=self.menubar) - def create_widgets(self): + def draw_toolbar(self): edit_frame = tk.Frame(self) edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) @@ -86,5 +87,4 @@ class Application(tk.Frame): if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) app = Application() - app.master.protocol("WM_DELETE_WINDOW", app.on_closing) app.mainloop() From f10acbc8d97d096c8ea99e31cc9eaf118180aabc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 16:31:40 -0700 Subject: [PATCH 047/305] added gui directory check and creation, created module for referencing gui directories --- coretk/coretk/app.py | 2 + coretk/coretk/appdirs.py | 39 ++++++++++++++++++ .../{wallpaper => backgrounds}/sample1-bg.gif | Bin .../{wallpaper => backgrounds}/sample4-bg.jpg | Bin coretk/coretk/images.py | 12 ++---- coretk/coretk/setwallpaper.py | 7 +--- 6 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 coretk/coretk/appdirs.py rename coretk/coretk/{wallpaper => backgrounds}/sample1-bg.gif (100%) rename coretk/coretk/{wallpaper => backgrounds}/sample4-bg.jpg (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 73cbcfbd..671504a8 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,7 @@ import logging import tkinter as tk +from coretk import appdirs from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar from coretk.coretoolbar import CoreToolbar @@ -86,5 +87,6 @@ class Application(tk.Frame): if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) + appdirs.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py new file mode 100644 index 00000000..7dae4bf3 --- /dev/null +++ b/coretk/coretk/appdirs.py @@ -0,0 +1,39 @@ +import logging +import shutil +from pathlib import Path + +# gui home paths +HOME_PATH = Path.home().joinpath(".coretk") +BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") +CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") +CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") +ICONS_PATH = HOME_PATH.joinpath("icons") +MOBILITY_PATH = HOME_PATH.joinpath("mobility") +XML_PATH = HOME_PATH.joinpath("xml") +CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") + +# local paths +LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() + + +def check_directory(): + if HOME_PATH.exists(): + logging.info("~/.coretk exists") + return + logging.info("creating ~/.coretk") + HOME_PATH.mkdir() + BACKGROUNDS_PATH.mkdir() + CUSTOM_EMANE_PATH.mkdir() + CUSTOM_SERVICE_PATH.mkdir() + ICONS_PATH.mkdir() + MOBILITY_PATH.mkdir() + XML_PATH.mkdir() + for image in LOCAL_ICONS_PATH.glob("*"): + new_image = ICONS_PATH.joinpath(image.name) + shutil.copy(image, new_image) + for background in LOCAL_BACKGROUND_PATH.glob("*"): + new_background = BACKGROUNDS_PATH.joinpath(background.name) + shutil.copy(background, new_background) + with CONFIG_PATH.open("w") as f: + f.write("# gui config") diff --git a/coretk/coretk/wallpaper/sample1-bg.gif b/coretk/coretk/backgrounds/sample1-bg.gif similarity index 100% rename from coretk/coretk/wallpaper/sample1-bg.gif rename to coretk/coretk/backgrounds/sample1-bg.gif diff --git a/coretk/coretk/wallpaper/sample4-bg.jpg b/coretk/coretk/backgrounds/sample4-bg.jpg similarity index 100% rename from coretk/coretk/wallpaper/sample4-bg.jpg rename to coretk/coretk/backgrounds/sample4-bg.jpg diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 7a7c0331..f9b5f870 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,13 +1,10 @@ import logging -import os from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") +from coretk.appdirs import LOCAL_ICONS_PATH class Images: @@ -15,14 +12,11 @@ class Images: @classmethod def load_all(cls): - for file_name in os.listdir(ICONS_DIR): - file_path = os.path.join(ICONS_DIR, file_name) - name = file_name.split(".")[0] - cls.load(name, file_path) + for image in LOCAL_ICONS_PATH.glob("*"): + cls.load(image.stem, str(image)) @classmethod def load(cls, name, file_path): - # file_path = os.path.join(PATH, file_path) image = Image.open(file_path) tk_image = ImageTk.PhotoImage(image) cls.images[name] = tk_image diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/setwallpaper.py index edace83c..81932208 100644 --- a/coretk/coretk/setwallpaper.py +++ b/coretk/coretk/setwallpaper.py @@ -3,14 +3,12 @@ set wallpaper """ import enum import logging -import os import tkinter as tk from tkinter import filedialog from PIL import Image, ImageTk -PATH = os.path.abspath(os.path.dirname(__file__)) -WALLPAPER_DIR = os.path.join(PATH, "wallpaper") +from coretk.appdirs import BACKGROUNDS_PATH class ScaleOption(enum.Enum): @@ -60,7 +58,7 @@ class CanvasWallpaper: def open_image_link(self): filename = filedialog.askopenfilename( - initialdir=WALLPAPER_DIR, + initialdir=str(BACKGROUNDS_PATH), title="Open", filetypes=( ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), @@ -207,7 +205,6 @@ class CanvasWallpaper: cropy = img_h = tk_img.height() if img_w > canvas_w: - cropx -= img_w - canvas_w if img_h > canvas_h: cropy -= img_h - canvas_h From b4f4ecd93d56157dbb29026a45e0bdf10f81a23c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 16:47:30 -0700 Subject: [PATCH 048/305] avoid issues when open/save xml provides no value --- coretk/coretk/menuaction.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 7598f210..bfbb5c5d 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,13 +7,12 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 +from coretk.appdirs import XML_PATH from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper from coretk.sizeandscale import SizeAndScale -SAVEDIR = "/home/ncs/Desktop/" - def sub_menu_items(): logging.debug("Click on sub menu items") @@ -361,24 +360,26 @@ class MenuAction: def file_save_as_xml(self): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( - initialdir=SAVEDIR, + initialdir=str(XML_PATH), title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", ) - self.app.core.save_xml(file_path) + if file_path: + self.app.core.save_xml(file_path) def file_open_xml(self): logging.info("menuaction.py file_open_xml()") self.app.is_open_xml = True file_path = filedialog.askopenfilename( - initialdir=SAVEDIR, + initialdir=str(XML_PATH), title="Open", filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), ) - # clean up before opening a new session - self.prompt_save_running_session() - self.app.core.open_xml(file_path) + if file_path: + logging.info("opening xml: %s", file_path) + self.prompt_save_running_session() + self.app.core.open_xml(file_path) # Todo might not need # self.application.core_editbar.destroy_children_widgets() From 09e18889b0605b4c0ded372ffa766bdb0c3cfb8c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 1 Nov 2019 18:14:36 -0700 Subject: [PATCH 049/305] start to hooks dialog and hook dialog create/edit --- coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/hooks.py | 106 +++++++++++++++++++++++++++++++++ coretk/coretk/menuaction.py | 10 ++-- 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 coretk/coretk/dialogs/hooks.py diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 7b3f2633..0751ee06 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -607,7 +607,7 @@ class CoreMenubar(object): label="Comments...", command=action.session_comments, underline=0 ) session_menu.add_command( - label="Hooks...", command=action.session_hooks, underline=0 + label="Hooks...", command=self.menu_action.session_hooks, underline=0 ) session_menu.add_command( label="Reset node positions", diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py new file mode 100644 index 00000000..bdf96d33 --- /dev/null +++ b/coretk/coretk/dialogs/hooks.py @@ -0,0 +1,106 @@ +import tkinter as tk +from tkinter import ttk + +from core.api.grpc import core_pb2 +from coretk.dialogs.dialog import Dialog + + +class HookDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hook", modal=True) + self.name = tk.StringVar() + self.data = None + self.hook = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + + # name and states + frame = tk.Frame(self) + frame.grid(row=0, sticky="ew", pady=2) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=7) + frame.columnconfigure(2, weight=1) + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="ew") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + combobox = ttk.Combobox(frame, values=("DEFINITION", "CONFIGURATION")) + combobox.grid(row=0, column=2, sticky="ew") + + # data + self.data = tk.Text(self) + self.data.grid(row=1, sticky="nsew", pady=2) + + # button row + frame = tk.Frame(self) + frame.grid(row=2, sticky="ew", pady=2) + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save", command=lambda: self.save()) + button.grid(row=0, column=0, sticky="ew") + button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=1, sticky="ew") + + def set(self, hook): + self.hook = hook + self.name.set(hook.file) + self.data.delete(1.0, tk.END) + self.data.insert(tk.END, hook.data) + + def save(self): + data = self.data.get("1.0", tk.END).strip() + self.hook = core_pb2.Hook(file=self.name.get(), data=data) + self.destroy() + + +class HooksDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Hooks", modal=True) + self.listbox = None + self.edit = None + self.delete = None + self.selected = None + self.hooks = {} + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.listbox = tk.Listbox(self) + self.listbox.grid(row=0, sticky="ew") + self.listbox.bind("<>", self.select) + frame = tk.Frame(self) + frame.grid(row=1, sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + self.edit = tk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit + ) + self.edit.grid(row=0, column=1, sticky="ew") + self.delete = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete.grid(row=0, column=2, sticky="ew") + button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button.grid(row=0, column=3, sticky="ew") + + def click_create(self): + dialog = HookDialog(self, self.app) + dialog.show() + hook = dialog.hook + if hook: + self.hooks[hook.file] = hook + self.listbox.insert(tk.END, hook.file) + + def click_edit(self): + hook = self.hooks[self.selected] + dialog = HookDialog(self, self.app) + dialog.set(hook) + dialog.show() + + def select(self, event): + self.edit.config(state=tk.NORMAL) + self.delete.config(state=tk.NORMAL) + index = self.listbox.curselection()[0] + self.selected = self.listbox.get(index) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index bfbb5c5d..64caf8ae 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,7 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH +from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog from coretk.setwallpaper import CanvasWallpaper @@ -299,10 +300,6 @@ def session_comments(): logging.debug("Click session comments") -def session_hooks(): - logging.debug("Click session hooks") - - def session_reset_node_positions(): logging.debug("Click session reset node positions") @@ -406,3 +403,8 @@ class MenuAction: logging.debug("Click session change sessions") dialog = SessionsDialog(self.app, self.app) dialog.show() + + def session_hooks(self): + logging.debug("Click session hooks") + dialog = HooksDialog(self.app, self.app) + dialog.show() From c947d8c6c240f6329cc9d9e1a8f3642ad05d68e1 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 10:29:16 -0700 Subject: [PATCH 050/305] update to hooks dialog to leverage grpc, allows for getting hooks and creation --- coretk/coretk/dialogs/hooks.py | 67 +++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index bdf96d33..a4195246 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from tkinter import ttk @@ -11,10 +12,12 @@ class HookDialog(Dialog): self.name = tk.StringVar() self.data = None self.hook = None + self.state = tk.StringVar() self.draw() def draw(self): self.columnconfigure(0, weight=1) + self.rowconfigure(1, weight=1) # name and states frame = tk.Frame(self) @@ -26,12 +29,33 @@ class HookDialog(Dialog): label.grid(row=0, column=0, sticky="ew") entry = tk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - combobox = ttk.Combobox(frame, values=("DEFINITION", "CONFIGURATION")) + values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") + initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) + self.state.set(initial_state) + self.name.set(f"{initial_state.lower()}_hook.sh") + combobox = ttk.Combobox(frame, textvariable=self.state, values=values) combobox.grid(row=0, column=2, sticky="ew") + combobox.bind("<>", self.state_change) # data - self.data = tk.Text(self) - self.data.grid(row=1, sticky="nsew", pady=2) + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(row=1, sticky="nsew", pady=2) + self.data = tk.Text(frame) + self.data.insert( + 1.0, + ( + "#!/bin/sh\n" + "# session hook script; write commands here to execute on the host at the\n" + "# specified state\n" + ), + ) + self.data.grid(row=0, column=0, sticky="nsew") + scrollbar = tk.Scrollbar(frame) + scrollbar.grid(row=0, column=1, sticky="ns") + self.data.config(yscrollcommand=scrollbar.set) + scrollbar.config(command=self.data.yview) # button row frame = tk.Frame(self) @@ -43,15 +67,26 @@ class HookDialog(Dialog): button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") + def state_change(self, event): + state_name = self.state.get() + self.name.set(f"{state_name.lower()}_hook.sh") + def set(self, hook): self.hook = hook self.name.set(hook.file) self.data.delete(1.0, tk.END) self.data.insert(tk.END, hook.data) + state_name = core_pb2.SessionState.Enum.Name(hook.state) + self.state.set(state_name) def save(self): data = self.data.get("1.0", tk.END).strip() - self.hook = core_pb2.Hook(file=self.name.get(), data=data) + state_value = core_pb2.SessionState.Enum.Value(self.state.get()) + self.hook = core_pb2.Hook(state=state_value, file=self.name.get(), data=data) + response = self.app.core.client.add_hook( + self.app.core.session_id, self.hook.state, self.hook.file, self.hook.data + ) + logging.info("add hook: %s", response) self.destroy() @@ -59,29 +94,35 @@ class HooksDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Hooks", modal=True) self.listbox = None - self.edit = None - self.delete = None + self.edit_button = None + self.delete_button = None self.selected = None self.hooks = {} self.draw() def draw(self): + response = self.app.core.client.get_hooks(self.app.core.session_id) + logging.info("get hooks: %s", response) self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self) - self.listbox.grid(row=0, sticky="ew") + self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) + for hook in response.hooks: + self.hooks[hook.file] = hook + self.listbox.insert(tk.END, hook.file) frame = tk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) button = tk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit = tk.Button( + self.edit_button = tk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit.grid(row=0, column=1, sticky="ew") - self.delete = tk.Button(frame, text="Delete", state=tk.DISABLED) - self.delete.grid(row=0, column=2, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew") + self.delete_button = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete_button.grid(row=0, column=2, sticky="ew") button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") @@ -100,7 +141,7 @@ class HooksDialog(Dialog): dialog.show() def select(self, event): - self.edit.config(state=tk.NORMAL) - self.delete.config(state=tk.NORMAL) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) index = self.listbox.curselection()[0] self.selected = self.listbox.get(index) From 8c1b70822e1fb0ff987b3c2db102593eb1d604e4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 14:34:00 -0700 Subject: [PATCH 051/305] fixed how hooks get created and sent to grpc StartSession, also query hooks when joining a session --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++++----------- coretk/coretk/dialogs/hooks.py | 23 ++++++++-------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e5ba838e..ad912422 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -71,6 +71,7 @@ class CoreClient: # data for managing the current session self.nodes = {} self.edges = {} + self.hooks = {} self.id = 1 self.reusable = [] self.preexisting = set() @@ -97,19 +98,30 @@ class CoreClient: ) def join_session(self, session_id): - # query session and set as current session + # update session and title self.session_id = session_id - response = self.client.get_session(self.session_id) - logging.info("joining session(%s): %s", self.session_id, response) - self.client.events(self.session_id, self.handle_events) - - # set title to session self.master.title(f"CORE Session({self.session_id})") - # determine next node id and reusable nodes - session = response.session + # clear session data self.reusable.clear() self.preexisting.clear() + self.nodes.clear() + self.edges.clear() + self.hooks.clear() + + # get session data + response = self.client.get_session(self.session_id) + logging.info("joining session(%s): %s", self.session_id, response) + session = response.session + self.client.events(self.session_id, self.handle_events) + + # get hooks + response = self.client.get_hooks(self.session_id) + logging.info("joined session hooks: %s", response) + for hook in response.hooks: + self.hooks[hook.file] = hook + + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: if node.id > max_id: @@ -262,9 +274,10 @@ class CoreClient: mobility_configs=None, ): response = self.client.start_session( - session_id=self.session_id, - nodes=nodes, - links=links, + self.session_id, + nodes, + links, + hooks=list(self.hooks.values()), wlan_configs=wlan_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index a4195246..7e1338ad 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import ttk @@ -11,7 +10,7 @@ class HookDialog(Dialog): super().__init__(master, app, "Hook", modal=True) self.name = tk.StringVar() self.data = None - self.hook = None + self.hook = core_pb2.Hook() self.state = tk.StringVar() self.draw() @@ -82,11 +81,9 @@ class HookDialog(Dialog): def save(self): data = self.data.get("1.0", tk.END).strip() state_value = core_pb2.SessionState.Enum.Value(self.state.get()) - self.hook = core_pb2.Hook(state=state_value, file=self.name.get(), data=data) - response = self.app.core.client.add_hook( - self.app.core.session_id, self.hook.state, self.hook.file, self.hook.data - ) - logging.info("add hook: %s", response) + self.hook.file = self.name.get() + self.hook.data = data + self.hook.state = state_value self.destroy() @@ -97,20 +94,16 @@ class HooksDialog(Dialog): self.edit_button = None self.delete_button = None self.selected = None - self.hooks = {} self.draw() def draw(self): - response = self.app.core.client.get_hooks(self.app.core.session_id) - logging.info("get hooks: %s", response) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self) self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) - for hook in response.hooks: - self.hooks[hook.file] = hook - self.listbox.insert(tk.END, hook.file) + for hook_file in self.app.core.hooks: + self.listbox.insert(tk.END, hook_file) frame = tk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): @@ -131,11 +124,11 @@ class HooksDialog(Dialog): dialog.show() hook = dialog.hook if hook: - self.hooks[hook.file] = hook + self.app.core.hooks[hook.file] = hook self.listbox.insert(tk.END, hook.file) def click_edit(self): - hook = self.hooks[self.selected] + hook = self.app.core.hooks[self.selected] dialog = HookDialog(self, self.app) dialog.set(hook) dialog.show() From b991dc0242387b4b9f17b519777983f3e3a3641f Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 14:46:30 -0700 Subject: [PATCH 052/305] updates to handle delete hook and button states --- coretk/coretk/dialogs/hooks.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 7e1338ad..95ca8af7 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -114,7 +114,9 @@ class HooksDialog(Dialog): frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button(frame, text="Delete", state=tk.DISABLED) + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) self.delete_button.grid(row=0, column=2, sticky="ew") button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") @@ -133,8 +135,19 @@ class HooksDialog(Dialog): dialog.set(hook) dialog.show() + def click_delete(self): + del self.app.core.hooks[self.selected] + self.listbox.delete(tk.ANCHOR) + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + def select(self, event): - self.edit_button.config(state=tk.NORMAL) - self.delete_button.config(state=tk.NORMAL) - index = self.listbox.curselection()[0] - self.selected = self.listbox.get(index) + if self.listbox.curselection(): + index = self.listbox.curselection()[0] + self.selected = self.listbox.get(index) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) From eabca7dfcf10fbc0e4f2db22bf0d023674779171 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Sat, 2 Nov 2019 21:59:29 -0700 Subject: [PATCH 053/305] work on wlan configuration --- coretk/coretk/custom_node.py | 19 +++ coretk/coretk/dialogs/mobilityconfig.py | 210 ++++++++++++++++++++++++ coretk/coretk/images.py | 4 +- coretk/coretk/wlanconfiguration.py | 13 +- 4 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 coretk/coretk/custom_node.py create mode 100644 coretk/coretk/dialogs/mobilityconfig.py diff --git a/coretk/coretk/custom_node.py b/coretk/coretk/custom_node.py new file mode 100644 index 00000000..5cc8ea75 --- /dev/null +++ b/coretk/coretk/custom_node.py @@ -0,0 +1,19 @@ +""" +edit node types +""" + +import tkinter as tk + + +class EditNodeTypes: + def __init__(self): + self.top = tk.Toplevel + self.top.title("CORE Node Types") + + def node_types(self): + """ + list box of node types + :return: + """ + lbl = tk.Label(self.top, text="Node types") + lbl.grid() diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py new file mode 100644 index 00000000..25339406 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -0,0 +1,210 @@ +""" +mobility configuration +""" + +import os +import tkinter as tk +from pathlib import Path +from tkinter import filedialog + +from coretk.dialogs.dialog import Dialog + + +class MobilityConfiguration(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of mobility configuration + + :param app: core application + :param root.master master: + """ + super().__init__(master, app, "ns2script configuration", modal=True) + self.canvas_node = canvas_node + self.mobility_script_parameters() + self.ns2script_options() + self.loop = "On" + + def create_string_var(self, val): + """ + create string variable for entry widget + + :return: nothing + """ + var = tk.StringVar() + var.set(val) + return var + + def open_file(self, entry): + configs_dir = os.path.join(Path.home(), ".core/configs") + if os.path.isdir(configs_dir): + filename = filedialog.askopenfilename(initialdir=configs_dir, title="Open") + if filename: + entry.delete(0, tk.END) + entry.insert(0, filename) + + def set_loop_value(self, value): + """ + set loop value when user changes the option + :param value: + :return: + """ + self.loop = value + + def create_label_entry_filebrowser( + self, parent_frame, text_label, filebrowser=False + ): + f = tk.Frame(parent_frame, bg="#d9d9d9") + lbl = tk.Label(f, text=text_label, bg="#d9d9d9") + lbl.grid(padx=3, pady=3) + # f.grid() + e = tk.Entry(f, textvariable=self.create_string_var(""), bg="#ffffff") + e.grid(row=0, column=1, padx=3, pady=3) + if filebrowser: + b = tk.Button(f, text="...", command=lambda: self.open_file(e)) + b.grid(row=0, column=2, padx=3, pady=3) + f.grid(sticky=tk.E) + + def mobility_script_parameters(self): + lbl = tk.Label(self, text="node ns2script") + lbl.grid(sticky=tk.W + tk.E) + + sb = tk.Scrollbar(self, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky=tk.N + tk.S + tk.E) + + f = tk.Frame(self, bg="#d9d9d9") + lbl = tk.Label( + f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED + ) + lbl.grid(row=0, column=0, sticky=tk.W) + + f1 = tk.Canvas( + f, + yscrollcommand=sb.set, + bg="#d9d9d9", + relief=tk.RAISED, + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + self.create_label_entry_filebrowser( + f1, "mobility script file", filebrowser=True + ) + self.create_label_entry_filebrowser(f1, "Refresh time (ms)") + + # f12 = tk.Frame(f1) + # + # lbl = tk.Label(f12, text="Refresh time (ms)") + # lbl.grid() + # + # e = tk.Entry(f12, textvariable=self.create_string_var("50")) + # e.grid(row=0, column=1) + # f12.grid() + + f13 = tk.Frame(f1) + + lbl = tk.Label(f13, text="loop") + lbl.grid() + + om = tk.OptionMenu( + f13, + self.create_string_var("On"), + "On", + "Off", + command=self.set_loop_value, + indicatoron=True, + ) + om.grid(row=0, column=1) + + f13.grid(sticky=tk.E) + + self.create_label_entry_filebrowser(f1, "auto-start seconds (0.0 for runtime)") + # f14 = tk.Frame(f1) + # + # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") + # lbl.grid() + # + # e = tk.Entry(f14, textvariable=self.create_string_var("")) + # e.grid(row=0, column=1) + # + # f14.grid() + self.create_label_entry_filebrowser( + f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)" + ) + # f15 = tk.Frame(f1) + # + # lbl = tk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") + # lbl.grid() + # + # e = tk.Entry(f15, textvariable=self.create_string_var("")) + # e.grid(row=0, column=1) + # + # f15.grid() + + self.create_label_entry_filebrowser( + f1, "script file to run upon start", filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "script file to run upon pause", filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "script file to run upon stop", filebrowser=True + ) + f1.grid() + sb.config(command=f1.yview) + f.grid(row=1, column=0) + + def ns2script_apply(self): + """ + + :return: + """ + config_frame = self.grid_slaves(row=1, column=0)[0] + canvas = config_frame.grid_slaves(row=1, column=0)[0] + file = ( + canvas.grid_slaves(row=0, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + refresh_time = ( + canvas.grid_slaves(row=1, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + auto_start_seconds = ( + canvas.grid_slaves(row=3, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + node_mapping = ( + canvas.grid_slaves(row=4, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + file_upon_start = ( + canvas.grid_slaves(row=5, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + file_upon_pause = ( + canvas.grid_slaves(row=6, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + file_upon_stop = ( + canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() + ) + + print("mobility script file: ", file) + print("refresh time: ", refresh_time) + print("auto start seconds: ", auto_start_seconds) + print("node mapping: ", node_mapping) + print("script file to run upon start: ", file_upon_start) + print("file upon pause: ", file_upon_pause) + print("file upon stop: ", file_upon_stop) + + self.destroy() + + def ns2script_options(self): + """ + create the options for ns2script configuration + + :return: nothing + """ + f = tk.Frame(self) + b = tk.Button(f, text="Apply", command=self.ns2script_apply) + b.grid() + b = tk.Button(f, text="Cancel", command=self.destroy) + b.grid(row=0, column=1) + f.grid() diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 1042042d..b22afa49 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -39,9 +39,9 @@ class Images: if node_type == core_pb2.NodeType.HUB: return Images.get(ImageEnum.HUB), "hub" if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN.value), "wlan" + return Images.get(ImageEnum.WLAN), "wlan" if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE.value), "emane" + return Images.get(ImageEnum.EMANE), "emane" if node_type == core_pb2.NodeType.RJ45: return Images.get(ImageEnum.RJ45), "rj45" diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 32bb56e2..6e028084 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -5,6 +5,7 @@ wlan configuration import tkinter as tk from functools import partial +from coretk.dialogs.mobilityconfig import MobilityConfiguration from coretk.imagemodification import ImageModification @@ -224,6 +225,12 @@ class WlanConfiguration: f2.grid() f.grid(sticky=tk.W, padx=3, pady=3) + def click_ns2_mobility_script(self): + dialog = MobilityConfiguration( + self.top, self.canvas.core_grpc.app, self.canvas_node + ) + dialog.show() + def wlan_options(self): """ create wireless node options @@ -231,7 +238,11 @@ class WlanConfiguration: :return: """ f = tk.Frame(self.top) - b = tk.Button(f, text="ns-2 mobility script...") + b = tk.Button( + f, + text="ns-2 mobility script...", + command=lambda: self.click_ns2_mobility_script(), + ) b.pack(side=tk.LEFT, padx=1) b = tk.Button(f, text="Link to all routers") b.pack(side=tk.LEFT, padx=1) From d4f77a01e30c559f765a29fc98648a85701ef8f0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 2 Nov 2019 23:47:43 -0700 Subject: [PATCH 054/305] moved all modules creating dialogs under dialogs package, updated node config and node icon dialogs to use common dialog class, fixed common dialog class show order to fix delayed cases with grab_set --- coretk/coretk/canvasaction.py | 8 +- coretk/coretk/dialogs/dialog.py | 5 +- coretk/coretk/dialogs/nodeconfig.py | 94 ++++++++++ coretk/coretk/dialogs/nodeicon.py | 69 ++++++++ coretk/coretk/{ => dialogs}/nodeservice.py | 0 coretk/coretk/{ => dialogs}/setwallpaper.py | 0 coretk/coretk/{ => dialogs}/sizeandscale.py | 2 +- .../wlanconfig.py} | 13 +- coretk/coretk/imagemodification.py | 91 ---------- coretk/coretk/images.py | 8 +- coretk/coretk/menuaction.py | 4 +- coretk/coretk/nodeconfigtable.py | 164 ------------------ 12 files changed, 184 insertions(+), 274 deletions(-) create mode 100644 coretk/coretk/dialogs/nodeconfig.py create mode 100644 coretk/coretk/dialogs/nodeicon.py rename coretk/coretk/{ => dialogs}/nodeservice.py (100%) rename coretk/coretk/{ => dialogs}/setwallpaper.py (100%) rename coretk/coretk/{ => dialogs}/sizeandscale.py (99%) rename coretk/coretk/{wlanconfiguration.py => dialogs/wlanconfig.py} (97%) delete mode 100644 coretk/coretk/imagemodification.py delete mode 100644 coretk/coretk/nodeconfigtable.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 0c56d4ba..b5f09333 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,8 +5,8 @@ canvas graph action # import tkinter as tk from core.api.grpc import core_pb2 -from coretk.nodeconfigtable import NodeConfig -from coretk.wlanconfiguration import WlanConfiguration +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfiguration # TODO, finish classifying node types NODE_TO_TYPE = { @@ -18,7 +18,6 @@ NODE_TO_TYPE = { class CanvasAction: def __init__(self, master, canvas): self.master = master - self.canvas = canvas self.node_to_show_config = None @@ -31,7 +30,8 @@ class CanvasAction: self.display_wlan_configuration(canvas_node) def display_node_configuration(self): - NodeConfig(self.canvas, self.node_to_show_config) + dialog = NodeConfigDialog(self.master, self.master, self.node_to_show_config) + dialog.show() self.node_to_show_config = None def display_wlan_configuration(self, canvas_node): diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index b218d8c7..f9cfcabe 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -17,8 +17,9 @@ class Dialog(tk.Toplevel): def show(self): self.transient(self.master) self.focus_force() - if self.modal: - self.grab_set() self.update() self.deiconify() + if self.modal: + self.wait_visibility() + self.grab_set() self.wait_window() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py new file mode 100644 index 00000000..d670853c --- /dev/null +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -0,0 +1,94 @@ +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog +from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeservice import NodeServices + +NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] +DEFAULTNODES = ["router", "host", "PC"] + + +class NodeConfigDialog(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of node configuration + + :param master: dialog master + :param coretk.app.Application: main app + :param coretk.graph.CanvasNode canvas_node: canvas node object + """ + super().__init__(master, app, f"{canvas_node.name} Configuration", modal=True) + self.canvas_node = canvas_node + self.image = canvas_node.image + self.image_button = None + self.name = tk.StringVar(value=canvas_node.name) + self.type = tk.StringVar(value=canvas_node.node_type) + self.server = tk.StringVar() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_first_row() + self.draw_second_row() + self.draw_third_row() + + def draw_first_row(self): + frame = tk.Frame(self) + frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=0, padx=2, sticky="ew") + + combobox = ttk.Combobox(frame, textvariable=self.type, values=DEFAULTNODES) + combobox.grid(row=0, column=1, padx=2, sticky="ew") + + combobox = ttk.Combobox(frame, textvariable=self.server, values=["localhost"]) + combobox.current(0) + combobox.grid(row=0, column=2, sticky="ew") + + def draw_second_row(self): + frame = tk.Frame(self) + frame.grid(row=1, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Services", command=lambda: NodeServices()) + button.grid(row=0, column=0, padx=2, sticky="ew") + + self.image_button = tk.Button( + frame, + text="Icon", + image=self.image, + compound=tk.LEFT, + command=self.click_icon, + ) + self.image_button.grid(row=0, column=1, sticky="ew") + + def draw_third_row(self): + frame = tk.Frame(self) + frame.grid(row=2, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Apply", command=self.config_apply) + button.grid(row=0, column=0, padx=2, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_icon(self): + dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def config_apply(self): + self.canvas_node.name = self.name.get() + self.canvas_node.image = self.image + self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + self.destroy() diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/nodeicon.py new file mode 100644 index 00000000..d82c0756 --- /dev/null +++ b/coretk/coretk/dialogs/nodeicon.py @@ -0,0 +1,69 @@ +import tkinter as tk +from tkinter import filedialog + +from coretk.appdirs import ICONS_PATH +from coretk.dialogs.dialog import Dialog +from coretk.images import Images + + +class NodeIconDialog(Dialog): + def __init__(self, master, app, canvas_node): + """ + create an instance of ImageModification + :param master: dialog master + :param coretk.app.Application: main app + :param coretk.graph.CanvasNode canvas_node: node object + """ + super().__init__(master, app, f"{canvas_node.name} Icon", modal=True) + self.file_path = tk.StringVar() + self.image_label = None + self.image = canvas_node.image + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + + # row one + frame = tk.Frame(self) + frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=3) + label = tk.Label(frame, text="Image") + label.grid(row=0, column=0, sticky="ew") + entry = tk.Entry(frame, textvariable=self.file_path) + entry.grid(row=0, column=1, sticky="ew") + button = tk.Button(frame, text="...", command=self.click_file) + button.grid(row=0, column=2) + + # row two + self.image_label = tk.Label(self, image=self.image) + self.image_label.grid(row=1, column=0, pady=2, sticky="ew") + + # row three + frame = tk.Frame(self) + frame.grid(row=2, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = tk.Button(frame, text="Apply", command=self.destroy) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=1, sticky="ew") + + def click_file(self): + file_path = filedialog.askopenfilename( + initialdir=str(ICONS_PATH), + title="Open", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) + if file_path: + self.image = Images.create(file_path) + self.image_label.config(image=self.image) + self.file_path.set(file_path) + + def click_cancel(self): + self.image = None + self.destroy() diff --git a/coretk/coretk/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py similarity index 100% rename from coretk/coretk/nodeservice.py rename to coretk/coretk/dialogs/nodeservice.py diff --git a/coretk/coretk/setwallpaper.py b/coretk/coretk/dialogs/setwallpaper.py similarity index 100% rename from coretk/coretk/setwallpaper.py rename to coretk/coretk/dialogs/setwallpaper.py diff --git a/coretk/coretk/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py similarity index 99% rename from coretk/coretk/sizeandscale.py rename to coretk/coretk/dialogs/sizeandscale.py index 5c746c39..0267a300 100644 --- a/coretk/coretk/sizeandscale.py +++ b/coretk/coretk/dialogs/sizeandscale.py @@ -4,7 +4,7 @@ size and scale import tkinter as tk from functools import partial -from coretk.setwallpaper import ScaleOption +from coretk.dialogs.setwallpaper import ScaleOption DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/dialogs/wlanconfig.py similarity index 97% rename from coretk/coretk/wlanconfiguration.py rename to coretk/coretk/dialogs/wlanconfig.py index 32bb56e2..8065a5fa 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from functools import partial -from coretk.imagemodification import ImageModification +from coretk.dialogs.nodeicon import NodeIconDialog class WlanConfiguration: @@ -57,16 +57,13 @@ class WlanConfiguration: e.grid(row=0, column=1, padx=3, pady=3) b = tk.Button(f, text="None") b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button( - f, - image=self.image, - command=lambda: ImageModification( - canvas=self.canvas, canvas_node=self.canvas_node, node_config=self - ), - ) + b = tk.Button(f, image=self.image, command=lambda: self.click_image) b.grid(row=0, column=3, padx=3, pady=3) f.grid(padx=2, pady=2, ipadx=2, ipady=2) + def click_image(self): + NodeIconDialog(self.app, canvas_node=self.canvas_node, node_config=self) + def create_string_var(self, val): """ create string variable for convenience diff --git a/coretk/coretk/imagemodification.py b/coretk/coretk/imagemodification.py deleted file mode 100644 index 646f31b7..00000000 --- a/coretk/coretk/imagemodification.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -node image modification -""" - - -import os -import tkinter as tk -from tkinter import filedialog - -from PIL import Image, ImageTk - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") - - -class ImageModification: - def __init__(self, canvas, canvas_node, node_config): - """ - create an instance of ImageModification - :param coretk.graph.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: node object - :param coretk.nodeconfigtable.NodeConfig node_config: node configuration object - """ - self.canvas = canvas - self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name - self.canvas_node = canvas_node - self.node_configuration = node_config - self.p_top = node_config.top - - self.top = tk.Toplevel() - self.top.title(self.name + " image") - self.image_modification() - - def open_icon_dir(self, toplevel, entry_text): - filename = filedialog.askopenfilename( - initialdir=ICONS_DIR, - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if len(filename) > 0: - img = Image.open(filename) - tk_img = ImageTk.PhotoImage(img) - lb = toplevel.grid_slaves(1, 0)[0] - lb.configure(image=tk_img) - lb.image = tk_img - entry_text.set(filename) - - def click_apply(self, toplevel, entry_text): - imgfile = entry_text.get() - if imgfile: - img = Image.open(imgfile) - tk_img = ImageTk.PhotoImage(img) - f = self.p_top.grid_slaves(row=0, column=0)[0] - lb = f.grid_slaves(row=0, column=3)[0] - lb.configure(image=tk_img) - lb.image = tk_img - self.image = tk_img - self.node_configuration.image = tk_img - toplevel.destroy() - - def image_modification(self): - f = tk.Frame(self.top) - entry_text = tk.StringVar() - image_file_label = tk.Label(f, text="Image file: ") - image_file_label.grid(row=0, column=0) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") - image_file_entry.grid(row=0, column=1) - image_file_button = tk.Button( - f, text="...", command=lambda: self.open_icon_dir(self.top, entry_text) - ) - image_file_button.grid(row=0, column=2) - f.grid() - - img = tk.Label(self.top, image=self.image) - img.grid() - - f = tk.Frame(self.top) - apply_button = tk.Button( - f, text="Apply", command=lambda: self.click_apply(self.top, entry_text) - ) - apply_button.grid(row=0, column=0) - apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.grid(row=0, column=1) - cancel_button = tk.Button(f, text="Cancel", command=self.top.destroy) - cancel_button.grid(row=0, column=2) - f.grid() diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f9b5f870..768b33ba 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -10,6 +10,11 @@ from coretk.appdirs import LOCAL_ICONS_PATH class Images: images = {} + @classmethod + def create(cls, file_path): + image = Image.open(file_path) + return ImageTk.PhotoImage(image) + @classmethod def load_all(cls): for image in LOCAL_ICONS_PATH.glob("*"): @@ -17,8 +22,7 @@ class Images: @classmethod def load(cls, name, file_path): - image = Image.open(file_path) - tk_image = ImageTk.PhotoImage(image) + tk_image = cls.create(file_path) cls.images[name] = tk_image @classmethod diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 64caf8ae..1d9adca1 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,8 +11,8 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.setwallpaper import CanvasWallpaper -from coretk.sizeandscale import SizeAndScale +from coretk.dialogs.setwallpaper import CanvasWallpaper +from coretk.dialogs.sizeandscale import SizeAndScale def sub_menu_items(): diff --git a/coretk/coretk/nodeconfigtable.py b/coretk/coretk/nodeconfigtable.py deleted file mode 100644 index a1a7c1d2..00000000 --- a/coretk/coretk/nodeconfigtable.py +++ /dev/null @@ -1,164 +0,0 @@ -""" -Create toplevel for node configuration -""" -import logging -import os -import tkinter as tk -from tkinter import filedialog - -from PIL import Image, ImageTk - -from coretk.imagemodification import ImageModification -from coretk.nodeservice import NodeServices - -PATH = os.path.abspath(os.path.dirname(__file__)) -ICONS_DIR = os.path.join(PATH, "icons") - -NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] -DEFAULTNODES = ["router", "host", "PC"] - - -class NodeConfig: - def __init__(self, canvas, canvas_node): - """ - create an instance of node configuration - - :param coretk.graph.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: canvas node object - """ - self.canvas = canvas - self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name - self.canvas_node = canvas_node - - self.top = tk.Toplevel() - self.top.title(canvas_node.node_type + " configuration") - self.namevar = tk.StringVar(self.top, value="default name") - self.name_and_image_definition() - self.type_and_service_definition() - self.select_definition() - - def open_icon_dir(self, toplevel, entry_text): - filename = filedialog.askopenfilename( - initialdir=ICONS_DIR, - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if len(filename) > 0: - img = Image.open(filename) - tk_img = ImageTk.PhotoImage(img) - lb = toplevel.grid_slaves(1, 0)[0] - lb.configure(image=tk_img) - lb.image = tk_img - entry_text.set(filename) - - def click_apply(self, toplevel, entry_text): - imgfile = entry_text.get() - if imgfile: - img = Image.open(imgfile) - tk_img = ImageTk.PhotoImage(img) - lb = self.top.grid_slaves(row=0, column=3)[0] - lb.configure(image=tk_img) - lb.image = tk_img - self.image = tk_img - toplevel.destroy() - - def img_modification(self): - t = tk.Toplevel() - t.title(self.name + " image") - - f = tk.Frame(t) - entry_text = tk.StringVar() - image_file_label = tk.Label(f, text="Image file: ") - image_file_label.grid(row=0, column=0) - image_file_entry = tk.Entry(f, textvariable=entry_text, width=32, bg="white") - image_file_entry.grid(row=0, column=1) - image_file_button = tk.Button( - f, text="...", command=lambda: self.open_icon_dir(t, entry_text) - ) - image_file_button.grid(row=0, column=2) - f.grid() - - img = tk.Label(t, image=self.image) - img.grid() - - f = tk.Frame(t) - apply_button = tk.Button( - f, text="Apply", command=lambda: self.click_apply(t, entry_text) - ) - apply_button.grid(row=0, column=0) - apply_to_multiple_button = tk.Button(f, text="Apply to multiple...") - apply_to_multiple_button.grid(row=0, column=1) - cancel_button = tk.Button(f, text="Cancel", command=t.destroy) - cancel_button.grid(row=0, column=2) - f.grid() - - def name_and_image_definition(self): - f = tk.Frame(self.top, bg="#d9d9d9") - name_label = tk.Label(f, text="Node name: ", bg="#d9d9d9") - name_label.grid(padx=2, pady=2) - name_entry = tk.Entry(f, textvariable=self.namevar) - name_entry.grid(row=0, column=1, padx=2, pady=2) - - core_button = tk.Button(f, text="None") - core_button.grid(row=0, column=2, padx=2, pady=2) - img_button = tk.Button( - f, - image=self.image, - width=40, - height=40, - command=lambda: ImageModification(self.canvas, self.canvas_node, self), - bg="#d9d9d9", - ) - img_button.grid(row=0, column=3, padx=4, pady=4) - f.grid(padx=4, pady=4) - - def type_and_service_definition(self): - f = tk.Frame(self.top) - type_label = tk.Label(f, text="Type: ") - type_label.grid(row=0, column=0) - - type_button = tk.Button(f, text="None") - type_button.grid(row=0, column=1) - - service_button = tk.Button( - f, text="Services...", command=lambda: NodeServices() - ) - service_button.grid(row=0, column=2) - - f.grid(padx=2, pady=2) - - def config_apply(self): - """ - modify image of the canvas node - :return: nothing - """ - logging.debug("nodeconfigtable.py configuration apply") - self.canvas_node.image = self.image - self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) - self.top.destroy() - - def config_cancel(self): - """ - save chosen image but not modify canvas node - :return: nothing - """ - logging.debug("nodeconfigtable.py configuration cancel") - self.canvas_node.image = self.image - self.top.destroy() - - def select_definition(self): - f = tk.Frame(self.top) - apply_button = tk.Button(f, text="Apply", command=self.config_apply) - apply_button.grid(row=0, column=0) - cancel_button = tk.Button(f, text="Cancel", command=self.config_cancel) - cancel_button.grid(row=0, column=1) - f.grid() - - def network_node_config(self): - self.name_and_image_definition() - self.select_definition() From 91ab1b0ee6f1810e3f2d387c266576c074df3783 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Sun, 3 Nov 2019 22:58:45 -0800 Subject: [PATCH 055/305] mobility config, start emane config --- coretk/coretk/app.py | 5 +- coretk/coretk/canvasaction.py | 10 +++ coretk/coretk/coregrpc.py | 56 +++++----------- coretk/coretk/coretoolbar.py | 4 +- coretk/coretk/coretoolbarhelp.py | 35 +++++++--- coretk/coretk/dialogs/emaneconfig.py | 47 +++++++++++++ coretk/coretk/dialogs/mobilityconfig.py | 71 ++++++++++++++------ coretk/coretk/graph.py | 6 +- coretk/coretk/grpcmanagement.py | 5 ++ coretk/coretk/menuaction.py | 8 +++ coretk/coretk/mobilitynodeconfig.py | 89 +++++++++++++++++++++++++ coretk/coretk/wlanconfiguration.py | 6 +- coretk/coretk/wlannodeconfig.py | 35 +++++++++- 13 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 coretk/coretk/dialogs/emaneconfig.py create mode 100644 coretk/coretk/mobilitynodeconfig.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f253fd01..64eabd5d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,7 +15,6 @@ class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) appcache.cache_variable(self) - print(self.is_open_xml) self.load_images() self.setup_app() self.menubar = None @@ -100,6 +99,10 @@ class Application(tk.Frame): self.canvas.grpc_manager.update_preexisting_ids() self.canvas.draw_existing_component() + self.canvas.grpc_manager.wlanconfig_management.load_wlan_configurations( + self.core_grpc + ) + def on_closing(self): menu_action = MenuAction(self, self.master) menu_action.on_quit() diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 0c56d4ba..353c40c9 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,6 +5,7 @@ canvas graph action # import tkinter as tk from core.api.grpc import core_pb2 +from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.nodeconfigtable import NodeConfig from coretk.wlanconfiguration import WlanConfiguration @@ -12,6 +13,7 @@ from coretk.wlanconfiguration import WlanConfiguration NODE_TO_TYPE = { "router": core_pb2.NodeType.DEFAULT, "wlan": core_pb2.NodeType.WIRELESS_LAN, + "emane": core_pb2.NodeType.EMANE, } @@ -29,6 +31,8 @@ class CanvasAction: self.display_node_configuration() elif pb_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) + elif pb_type == core_pb2.NodeType.EMANE: + self.display_emane_configuration() def display_node_configuration(self): NodeConfig(self.canvas, self.node_to_show_config) @@ -41,3 +45,9 @@ class CanvasAction: ] WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) self.node_to_show_config = None + + def display_emane_configuration(self): + app = self.canvas.core_grpc.app + dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) + print(dialog) + dialog.show() diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py index 5cb6b0b1..5db0cc6d 100644 --- a/coretk/coretk/coregrpc.py +++ b/coretk/coretk/coregrpc.py @@ -3,7 +3,6 @@ Incorporate grpc into python tkinter GUI """ import logging import os -from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog @@ -143,22 +142,22 @@ class CoreGrpc: logging.info("set session state: %s", response) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.core.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.core.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id + # def add_node(self, node_type, model, x, y, name, node_id): + # position = core_pb2.Position(x=x, y=y) + # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + # self.node_ids.append(node_id) + # response = self.core.add_node(self.session_id, node) + # logging.info("created node: %s", response) + # if node_type == core_pb2.NodeType.WIRELESS_LAN: + # d = OrderedDict() + # d["basic_range"] = "275" + # d["bandwidth"] = "54000000" + # d["jitter"] = "0" + # d["delay"] = "20000" + # d["error"] = "0" + # r = self.core.set_wlan_config(self.session_id, node_id, d) + # logging.debug("set wlan config %s", r) + # return response.node_id def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) @@ -232,6 +231,7 @@ class CoreGrpc: nodes=nodes, links=links, wlan_configs=wlan_configs, + mobility_configs=mobility_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -253,28 +253,6 @@ class CoreGrpc: """ if1 = self.create_interface(type1, edge.interface_1) if2 = self.create_interface(type2, edge.interface_2) - # if type1 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_1 - # if1 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 1 %s", if1) - # # interface1 = self.interface_helper.create_interface(id1, 0) - # - # if type2 == core_pb2.NodeType.DEFAULT: - # interface = edge.interface_2 - # if2 = core_pb2.Interface( - # id=interface.id, - # name=interface.name, - # mac=interface.mac, - # ip4=interface.ipv4, - # ip4mask=interface.ip4prefix, - # ) - # logging.debug("create interface 2: %s", if2) response = self.core.add_link(self.session_id, id1, id2, if1, if2) logging.info("created link: %s", response) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 78688725..a9809c2c 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -368,9 +368,9 @@ class CoreToolbar(object): def pick_emane(self, main_button): self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EMANE.value)) + main_button.configure(image=Images.get(ImageEnum.EMANE)) self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.EMANE.value) + self.canvas.draw_node_image = Images.get(ImageEnum.EMANE) self.canvas.draw_node_name = "emane" def draw_link_layer_options(self, link_layer_button): diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index b4b6fe28..bbc25f2b 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -71,6 +71,11 @@ class CoreToolbarHelp: return links def get_wlan_configuration_list(self): + """ + form a list of wlan configuration to pass to start_session + + :return: nothing + """ configs = [] grpc_manager = self.application.canvas.grpc_manager manager_configs = grpc_manager.wlanconfig_management.configurations @@ -79,23 +84,33 @@ class CoreToolbarHelp: configs.append(cnf) return configs + def get_mobility_configuration_list(self): + """ + form a list of mobility configuration to pass to start_session + + :return: nothing + """ + configs = [] + grpc_manager = self.application.canvas.grpc_manager + manager_configs = grpc_manager.mobilityconfig_management.configurations + for key in manager_configs: + cnf = core_pb2.MobilityConfig(node_id=key, config=manager_configs[key]) + configs.append(cnf) + return configs + def gui_start_session(self): - # list(core_pb2.Node) nodes = self.get_node_list() - - # list(core_bp2.Link) links = self.get_link_list() - - # print(links[0]) wlan_configs = self.get_wlan_configuration_list() - # print(wlan_configs) - self.core_grpc.start_session(nodes, links, wlan_configs=wlan_configs) + mobility_configs = self.get_mobility_configuration_list() + + self.core_grpc.start_session( + nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs + ) # self.core_grpc.core.add_link(self.core_grpc.session_id, self.id1, self.id2, self.if1, self.if2) # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) - # res = self.core_grpc.core.get_session(self.core_grpc.session_id).session - # print(res) - # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 1) + # res = self.core_grpc.core.get_wlan_config(self.core_grpc.session_id, 2) # print(res) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py new file mode 100644 index 00000000..5ab2f545 --- /dev/null +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -0,0 +1,47 @@ +""" +emane configuration +""" + +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class EmaneConfiguration(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__(master, app, "emane configuration", modal=False) + self.canvas_node = canvas_node + + def create_text_variable(self, val): + """ + create a string variable for convenience + + :param str val: entry text + :return: nothing + """ + var = tk.StringVar() + var.set(val) + return var + + def choose_core(self): + print("not implemented") + + def node_name_and_image(self): + f = tk.Frame(self) + + lbl = tk.Label(f, text="Node name:") + lbl.grid(row=0, column=0) + e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") + e.grid(row=0, column=1) + + om = tk.OptionMenu( + f, + self.create_text_variable("None"), + "(none)", + "core1", + "core2", + command=self.choose_core, + ) + om.grid(row=0, column=2) + + # b = tk.Button(f,) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 25339406..258d2f9c 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -20,6 +20,10 @@ class MobilityConfiguration(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node + self.node_config = app.canvas.grpc_manager.mobilityconfig_management.configurations[ + canvas_node.core_id + ] + self.mobility_script_parameters() self.ns2script_options() self.loop = "On" @@ -51,13 +55,13 @@ class MobilityConfiguration(Dialog): self.loop = value def create_label_entry_filebrowser( - self, parent_frame, text_label, filebrowser=False + self, parent_frame, text_label, entry_text, filebrowser=False ): f = tk.Frame(parent_frame, bg="#d9d9d9") lbl = tk.Label(f, text=text_label, bg="#d9d9d9") lbl.grid(padx=3, pady=3) # f.grid() - e = tk.Entry(f, textvariable=self.create_string_var(""), bg="#ffffff") + e = tk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: b = tk.Button(f, text="...", command=lambda: self.open_file(e)) @@ -88,9 +92,11 @@ class MobilityConfiguration(Dialog): bd=0, ) self.create_label_entry_filebrowser( - f1, "mobility script file", filebrowser=True + f1, "mobility script file", self.node_config["file"], filebrowser=True + ) + self.create_label_entry_filebrowser( + f1, "Refresh time (ms)", self.node_config["refresh_ms"] ) - self.create_label_entry_filebrowser(f1, "Refresh time (ms)") # f12 = tk.Frame(f1) # @@ -107,18 +113,15 @@ class MobilityConfiguration(Dialog): lbl.grid() om = tk.OptionMenu( - f13, - self.create_string_var("On"), - "On", - "Off", - command=self.set_loop_value, - indicatoron=True, + f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value ) om.grid(row=0, column=1) f13.grid(sticky=tk.E) - self.create_label_entry_filebrowser(f1, "auto-start seconds (0.0 for runtime)") + self.create_label_entry_filebrowser( + f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] + ) # f14 = tk.Frame(f1) # # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") @@ -129,7 +132,7 @@ class MobilityConfiguration(Dialog): # # f14.grid() self.create_label_entry_filebrowser( - f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)" + f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] ) # f15 = tk.Frame(f1) # @@ -142,13 +145,22 @@ class MobilityConfiguration(Dialog): # f15.grid() self.create_label_entry_filebrowser( - f1, "script file to run upon start", filebrowser=True + f1, + "script file to run upon start", + self.node_config["script_start"], + filebrowser=True, ) self.create_label_entry_filebrowser( - f1, "script file to run upon pause", filebrowser=True + f1, + "script file to run upon pause", + self.node_config["script_pause"], + filebrowser=True, ) self.create_label_entry_filebrowser( - f1, "script file to run upon stop", filebrowser=True + f1, + "script file to run upon stop", + self.node_config["script_stop"], + filebrowser=True, ) f1.grid() sb.config(command=f1.yview) @@ -186,13 +198,28 @@ class MobilityConfiguration(Dialog): canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() ) - print("mobility script file: ", file) - print("refresh time: ", refresh_time) - print("auto start seconds: ", auto_start_seconds) - print("node mapping: ", node_mapping) - print("script file to run upon start: ", file_upon_start) - print("file upon pause: ", file_upon_pause) - print("file upon stop: ", file_upon_stop) + # print("mobility script file: ", file) + # print("refresh time: ", refresh_time) + # print("auto start seconds: ", auto_start_seconds) + # print("node mapping: ", node_mapping) + # print("script file to run upon start: ", file_upon_start) + # print("file upon pause: ", file_upon_pause) + # print("file upon stop: ", file_upon_stop) + if self.loop == "On": + loop = "1" + else: + loop = "0" + self.app.canvas.grpc_manager.mobilityconfig_management.set_custom_configuration( + node_id=self.canvas_node.core_id, + file=file, + refresh_ms=refresh_time, + loop=loop, + autostart=auto_start_seconds, + node_mapping=node_mapping, + script_start=file_upon_start, + script_pause=file_upon_pause, + script_stop=file_upon_stop, + ) self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef0f064f..d9d1460c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -90,8 +90,8 @@ class CanvasGraph(tk.Canvas): self.draw_node_name = None self.selected = None self.node_context = None - self.nodes = {} - self.edges = {} + self.nodes.clear() + self.edges.clear() self.drawing_edge = None self.grpc_manager = GrpcManager(new_grpc) @@ -100,6 +100,8 @@ class CanvasGraph(tk.Canvas): self.core_grpc = new_grpc self.draw_existing_component() + # self.grpc_manager.wlanconfig_management.load_wlan_configurations(self.core_grpc) + def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py index e7373dc6..ffc1f0f9 100644 --- a/coretk/coretk/grpcmanagement.py +++ b/coretk/coretk/grpcmanagement.py @@ -7,6 +7,7 @@ import logging from core.api.grpc import core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.interface import Interface, InterfaceManager +from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -74,6 +75,7 @@ class GrpcManager: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() + self.mobilityconfig_management = MobilityNodeConfig() def update_preexisting_ids(self): """ @@ -152,6 +154,9 @@ class GrpcManager: # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) + # set default mobility configuration for wireless node + self.mobilityconfig_management.set_default_configuration(node_type, nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 6a1ef0e7..bf30b713 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -400,6 +400,14 @@ class MenuAction: # print(grpc.get_session_state()) self.application.canvas.canvas_reset_and_redraw(core_grpc) + self.application.canvas.grpc_manager.wlanconfig_management.load_wlan_configurations( + core_grpc + ) + + self.application.canvas.grpc_manager.mobilityconfig_management.load_mobility_configurations( + core_grpc + ) + # Todo might not need self.application.core_grpc = core_grpc diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py new file mode 100644 index 00000000..7bcb58a1 --- /dev/null +++ b/coretk/coretk/mobilitynodeconfig.py @@ -0,0 +1,89 @@ +""" +mobility configurations for all the nodes +""" + +import logging +from collections import OrderedDict + +from core.api.grpc import core_pb2 + + +class MobilityNodeConfig: + def __init__(self): + self.configurations = {} + + def set_default_configuration(self, node_type, node_id): + """ + set default mobility configuration for a node + + :param core_pb2.NodeType node_type: protobuf node type + :param int node_id: node id + :return: nothing + """ + if node_type == core_pb2.NodeType.WIRELESS_LAN: + config = OrderedDict() + config["autostart"] = "" + config["file"] = "" + config["loop"] = "1" + config["map"] = "" + config["refresh_ms"] = "50" + config["script_pause"] = "" + config["script_start"] = "" + config["script_stop"] = "" + self.configurations[node_id] = config + + def set_custom_configuration( + self, + node_id, + file, + refresh_ms, + loop, + autostart, + node_mapping, + script_start, + script_pause, + script_stop, + ): + """ + set custom mobility configuration for a node + + :param int node_id: node id + :param str file: path to mobility script file + :param str refresh_ms: refresh time + :param str loop: loop option + :param str autostart: auto-start seconds value + :param str node_mapping: node mapping + :param str script_start: path to script to run upon start + :param str script_pause: path to script to run upon pause + :param str script_stop: path to script to run upon stop + :return: nothing + """ + if node_id in self.configurations: + self.configurations[node_id]["autostart"] = autostart + self.configurations[node_id]["file"] = file + self.configurations[node_id]["loop"] = loop + self.configurations[node_id]["map"] = node_mapping + self.configurations[node_id]["refresh_ms"] = refresh_ms + self.configurations[node_id]["script_pause"] = script_pause + self.configurations[node_id]["script_start"] = script_start + self.configurations[node_id]["script_stop"] = script_stop + else: + logging.error("mobilitynodeconfig.py invalid node_id") + + def load_mobility_configurations(self, core_grpc): + """ + load mobility configuration from the daemon into memory + + :param coretk.coregrpc.CoreGrpc core_grpc: CoreGrpc object + :return: nothing + """ + self.configurations.clear() + sid = core_grpc.session_id + client = core_grpc.core + configs = client.get_mobility_configs(sid).configs + for nid in configs: + node_config = configs[nid].config + cnf = OrderedDict() + for key in node_config: + cnf[key] = node_config[key].value + self.configurations[nid] = cnf diff --git a/coretk/coretk/wlanconfiguration.py b/coretk/coretk/wlanconfiguration.py index 6e028084..cd9b3a35 100644 --- a/coretk/coretk/wlanconfiguration.py +++ b/coretk/coretk/wlanconfiguration.py @@ -32,7 +32,8 @@ class WlanConfiguration: # self.range_var.set(275.0) self.config = config self.range_var = tk.StringVar() - self.range_var.set(config["basic_range"]) + # self.range_var.set(config["basic_range"]) + self.range_var.set(config["range"]) # self.bandwidth_var = tk.IntVar() self.bandwidth_var = tk.StringVar() self.bandwidth_var.set(config["bandwidth"]) @@ -130,7 +131,8 @@ class WlanConfiguration: e = tk.Entry( f1, - textvariable=self.create_string_var(self.config["basic_range"]), + # textvariable=self.create_string_var(self.config["basic_range"]), + textvariable=self.create_string_var(self.config["range"]), width=5, bg="white", ) diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py index 647cd7cf..d8bb2996 100644 --- a/coretk/coretk/wlannodeconfig.py +++ b/coretk/coretk/wlannodeconfig.py @@ -14,7 +14,7 @@ class WlanNodeConfig: def set_default_config(self, node_type, node_id): if node_type == core_pb2.NodeType.WIRELESS_LAN: config = OrderedDict() - config["basic_range"] = "275" + config["range"] = "275" config["bandwidth"] = "54000000" config["jitter"] = "0" config["delay"] = "20000" @@ -22,8 +22,39 @@ class WlanNodeConfig: self.configurations[node_id] = config def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): - self.configurations[node_id]["basic_range"] = range + self.configurations[node_id]["range"] = range self.configurations[node_id]["bandwidth"] = bandwidth self.configurations[node_id]["jitter"] = jitter self.configurations[node_id]["delay"] = delay self.configurations[node_id]["error"] = error + + def delete_node_config(self, node_id): + """ + not implemented + :param node_id: + :return: + """ + return + + def load_wlan_configurations(self, core_grpc): + """ + load wlan configuration from the daemon + + :param coretk.coregrpc.CoreGrpc core_grpc: CoreGrpc object + :return: nothing + """ + self.configurations.clear() + sid = core_grpc.session_id + client = core_grpc.core + for node in client.get_session(sid).session.nodes: + if node.type == core_pb2.NodeType.WIRELESS_LAN: + wlan_config = client.get_wlan_config(sid, node.id).config + config = OrderedDict() + for key in wlan_config: + config[key] = wlan_config[key].value + + # config[key] = wlan_config[key]["value"] + # print(config) + # for k, v in wlan_config.config: + + self.configurations[node.id] = config From eb862dd9daa2c0b51b0ae4639bbc9d5e5517e2fe Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 10:48:22 -0800 Subject: [PATCH 056/305] emane config --- coretk/coretk/dialogs/emaneconfig.py | 116 +++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 5ab2f545..75cbca0e 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -3,14 +3,26 @@ emane configuration """ import tkinter as tk +import webbrowser from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images class EmaneConfiguration(Dialog): def __init__(self, master, app, canvas_node): super().__init__(master, app, "emane configuration", modal=False) self.canvas_node = canvas_node + self.radiovar = tk.IntVar() + self.radiovar.set(1) + self.columnconfigure(0, weight=1) + + # draw + self.node_name_and_image() + self.emane_configuration() + + def browse_emane_wiki(self): + webbrowser.open_new("https://github.com/adjacentlink/emane/wiki") def create_text_variable(self, val): """ @@ -27,12 +39,12 @@ class EmaneConfiguration(Dialog): print("not implemented") def node_name_and_image(self): - f = tk.Frame(self) + f = tk.Frame(self, bg="#d9d9d9") - lbl = tk.Label(f, text="Node name:") - lbl.grid(row=0, column=0) + lbl = tk.Label(f, text="Node name:", bg="#d9d9d9") + lbl.grid(row=0, column=0, padx=2, pady=2) e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") - e.grid(row=0, column=1) + e.grid(row=0, column=1, padx=2, pady=2) om = tk.OptionMenu( f, @@ -42,6 +54,98 @@ class EmaneConfiguration(Dialog): "core2", command=self.choose_core, ) - om.grid(row=0, column=2) + om.grid(row=0, column=2, padx=2, pady=2) - # b = tk.Button(f,) + b = tk.Button(f, image=self.canvas_node.image) + b.grid(row=0, column=3, padx=2, pady=2) + + f.grid(row=0, column=0) + + def draw_option_buttons(self, parent): + f = tk.Frame(parent, bg="#d9d9d9") + b = tk.Button( + f, + text="model options", + image=Images.get(ImageEnum.EDITNODE), + compound=tk.RIGHT, + bg="#d9d9d9", + state=tk.DISABLED, + ) + b.grid(row=0, column=0, padx=2, pady=2) + b = tk.Button( + f, + text="EMANE options", + image=Images.get(ImageEnum.EDITNODE), + compound=tk.RIGHT, + bg="#d9d9d9", + ) + b.grid(row=0, column=1, padx=2, pady=2) + f.grid(row=4, column=0) + + def click_radio_button(self): + print(self.radiovar.get()) + + def draw_emane_models(self, parent): + models = ["none", "rfpipe", "ieee80211abg", "commeffect", "bypass", "tdma"] + f = tk.Frame( + parent, + bg="#d9d9d9", + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + ) + value = 1 + for m in models: + b = tk.Radiobutton( + f, + text=m, + variable=self.radiovar, + indicatoron=True, + value=value, + bg="#d9d9d9", + highlightthickness=0, + command=self.click_radio_button, + ) + b.grid(sticky=tk.W) + value = value + 1 + f.grid(row=3, column=0, sticky=tk.W + tk.E) + + def emane_configuration(self): + lbl = tk.Label(self, text="Emane") + lbl.grid(row=1, column=0) + f = tk.Frame( + self, + bg="#d9d9d9", + highlightbackground="#b3b3b3", + highlightcolor="#b3b3b3", + highlightthickness=0.5, + bd=0, + relief=tk.RAISED, + ) + f.columnconfigure(0, weight=1) + + b = tk.Button( + f, + image=Images.get(ImageEnum.EDITNODE), + text="EMANE Wiki", + compound=tk.RIGHT, + relief=tk.RAISED, + bg="#d9d9d9", + command=self.browse_emane_wiki, + ) + b.grid(row=0, column=0, sticky=tk.W) + + lbl = tk.Label( + f, + text="The EMANE emulation system provides more complex wireless radio emulation " + "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + bg="#d9d9d9", + ) + lbl.grid(row=1, column=0, sticky=tk.W) + + lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") + lbl.grid(row=2, column=0, sticky=tk.W) + self.draw_option_buttons(f) + self.draw_emane_models(f) + f.grid(row=2, column=0) From 22a55b69d3231cbb72bf372b3007c2fbc4a98ed0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 11:34:28 -0800 Subject: [PATCH 057/305] update node service dialog to use common dialog class --- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/nodeservice.py | 216 ++++++++++++++------------- 2 files changed, 122 insertions(+), 102 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index d670853c..f1de2e50 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import NodeIconDialog -from coretk.dialogs.nodeservice import NodeServices +from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] DEFAULTNODES = ["router", "host", "PC"] @@ -56,7 +56,7 @@ class NodeConfigDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Services", command=lambda: NodeServices()) + button = tk.Button(frame, text="Services", command=self.click_services) button.grid(row=0, column=0, padx=2, sticky="ew") self.image_button = tk.Button( @@ -80,6 +80,10 @@ class NodeConfigDialog(Dialog): button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_services(self): + dialog = NodeServicesDialog(self, self.app, self.canvas_node) + dialog.show() + def click_icon(self): dialog = NodeIconDialog(self, self.app, self.canvas_node) dialog.show() diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 3bcedfb7..82d36cca 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -4,6 +4,8 @@ core node services import tkinter as tk from tkinter import messagebox +from coretk.dialogs.dialog import Dialog + CORE_DEFAULT_GROUPS = ["EMANE", "FRR", "ProtoSvc", "Quagga", "Security", "Utility"] DEFAULT_GROUP_RADIO_VALUE = { "EMANE": 1, @@ -57,35 +59,28 @@ DEFAULT_GROUP_SERVICES = { } -class NodeServices: - def __init__(self): +class NodeServicesDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__(master, app, "Node Services", modal=True) + self.canvas_node = canvas_node self.core_groups = [] self.service_to_config = None + self.config_frame = None + self.draw() - self.top = tk.Toplevel() - self.top.title("Node services") - self.config_frame = tk.Frame(self.top) - self.config_frame.grid() + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.config_frame = tk.Frame(self) + self.config_frame.columnconfigure(0, weight=1) + self.config_frame.columnconfigure(1, weight=1) + self.config_frame.columnconfigure(2, weight=1) + self.config_frame.rowconfigure(0, weight=1) + self.config_frame.grid(row=0, column=0, sticky="nsew") self.draw_group() - self.group_services() - self.current_services() - self.node_service_options() - - def display_group_services(self, group_name): - group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] - listbox = group_services_frame.grid_slaves(row=1, column=0)[0] - listbox.delete(0, tk.END) - for s in DEFAULT_GROUP_SERVICES[group_name]: - listbox.insert(tk.END, s) - for i in range(listbox.size()): - listbox.itemconfig(i, selectbackground="white") - - def group_select(self, event): - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.display_group_services(s) + self.draw_services() + self.draw_current_services() + self.draw_buttons() def draw_group(self): """ @@ -93,36 +88,113 @@ class NodeServices: :return: nothing """ - f = tk.Frame(self.config_frame) + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - lbl = tk.Label(f, text="Group") - lbl.grid() + label = tk.Label(frame, text="Group") + label.grid(row=0, column=0, sticky="ew") - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( - f, + frame, selectmode=tk.SINGLE, - yscrollcommand=sb.set, + yscrollcommand=scrollbar.set, relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", highlightthickness=0.5, bd=0, ) + listbox.grid(row=1, column=0, sticky="nsew") + listbox.bind("<>", self.handle_group_change) - for grp in CORE_DEFAULT_GROUPS: - listbox.insert(tk.END, grp) - for i in range(0, listbox.size()): - listbox.itemconfig(i, selectbackground="white") - listbox.grid(row=1, column=0) + for group in CORE_DEFAULT_GROUPS: + listbox.insert(tk.END, group) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3) - listbox.bind("<>", self.group_select) + scrollbar.config(command=listbox.yview) - def group_service_select(self, event): + def draw_services(self): + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") + + label = tk.Label(frame, text="Group services") + label.grid(row=0, column=0, sticky="ew") + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") + + listbox = tk.Listbox( + frame, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0, sticky="nsew") + listbox.bind("<>", self.handle_service_change) + + scrollbar.config(command=listbox.yview) + + def draw_current_services(self): + frame = tk.Frame(self.config_frame) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") + + label = tk.Label(frame, text="Current services") + label.grid(row=0, column=0, sticky="ew") + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=1, column=1, sticky="ns") + + listbox = tk.Listbox( + frame, + selectmode=tk.MULTIPLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + listbox.grid(row=1, column=0, sticky="nsew") + + scrollbar.config(command=listbox.yview) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(row=1, column=0, sticky="ew") + + button = tk.Button(frame, text="Configure", command=self.click_configure) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Apply") + button.grid(row=0, column=1, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=2, sticky="ew") + + def handle_group_change(self, event): + listbox = event.widget + cur_selection = listbox.curselection() + if cur_selection: + s = listbox.get(listbox.curselection()) + self.display_group_services(s) + + def display_group_services(self, group_name): + group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] + listbox = group_services_frame.grid_slaves(row=1, column=0)[0] + listbox.delete(0, tk.END) + for s in DEFAULT_GROUP_SERVICES[group_name]: + listbox.insert(tk.END, s) + + def handle_service_change(self, event): print("select group service") listbox = event.widget cur_selection = listbox.curselection() @@ -132,64 +204,8 @@ class NodeServices: else: self.service_to_config = None - def group_services(self): - f = tk.Frame(self.config_frame) - lbl = tk.Label(f, text="Group services") - lbl.grid() - - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) - - listbox = tk.Listbox( - f, - selectmode=tk.SINGLE, - yscrollcommand=sb.set, - relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3, row=0, column=1) - - listbox.bind("<>", self.group_service_select) - - def current_services(self): - f = tk.Frame(self.config_frame) - lbl = tk.Label(f, text="Current services") - lbl.grid() - - sb = tk.Scrollbar(f, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.S + tk.N) - - listbox = tk.Listbox( - f, - selectmode=tk.MULTIPLE, - yscrollcommand=sb.set, - relief=tk.FLAT, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0) - sb.config(command=listbox.yview) - f.grid(padx=3, pady=3, row=0, column=2) - - def config_service(self): + def click_configure(self): if self.service_to_config is None: messagebox.showinfo("CORE info", "Choose a service to configure.") else: print(self.service_to_config) - - def node_service_options(self): - f = tk.Frame(self.top) - b = tk.Button(f, text="Connfigure", command=self.config_service) - b.grid(row=0, column=0) - b = tk.Button(f, text="Apply") - b.grid(row=0, column=1) - b = tk.Button(f, text="Cancel", command=self.top.destroy) - b.grid(row=0, column=2) - f.grid(sticky=tk.E) From 2334f0f688d5dfb0c118497bae6f2d97ecad66e3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:29:01 -0800 Subject: [PATCH 058/305] updated canvas background dialog to use common dialog class --- .../{setwallpaper.py => canvasbackground.py} | 256 +++++++++--------- coretk/coretk/dialogs/sizeandscale.py | 2 +- coretk/coretk/menuaction.py | 5 +- 3 files changed, 131 insertions(+), 132 deletions(-) rename coretk/coretk/dialogs/{setwallpaper.py => canvasbackground.py} (60%) diff --git a/coretk/coretk/dialogs/setwallpaper.py b/coretk/coretk/dialogs/canvasbackground.py similarity index 60% rename from coretk/coretk/dialogs/setwallpaper.py rename to coretk/coretk/dialogs/canvasbackground.py index 81932208..fe9b5f6a 100644 --- a/coretk/coretk/dialogs/setwallpaper.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -9,6 +9,7 @@ from tkinter import filedialog from PIL import Image, ImageTk from coretk.appdirs import BACKGROUNDS_PATH +from coretk.dialogs.dialog import Dialog class ScaleOption(enum.Enum): @@ -19,44 +20,116 @@ class ScaleOption(enum.Enum): TILED = 4 -class CanvasWallpaper: - def __init__(self, app): +class CanvasBackgroundDialog(Dialog): + def __init__(self, master, app): """ create an instance of CanvasWallpaper object :param coretk.app.Application app: root application """ - self.app = app + super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas + self.radiovar = tk.IntVar(value=self.app.radiovar.get()) + self.show_grid_var = tk.IntVar(value=self.app.show_grid_var.get()) + self.adjust_to_dim_var = tk.IntVar(value=self.app.adjust_to_dim_var.get()) + self.image_label = None + self.file_name = tk.StringVar() + self.options = [] + self.draw() - self.top = tk.Toplevel() - self.top.title("Set Canvas Wallpaper") - self.radiovar = tk.IntVar() - print(self.app.radiovar.get()) - self.radiovar.set(self.app.radiovar.get()) - self.show_grid_var = tk.IntVar() - self.show_grid_var.set(self.app.show_grid_var.get()) - self.adjust_to_dim_var = tk.IntVar() - self.adjust_to_dim_var.set(self.app.adjust_to_dim_var.get()) + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_image() + self.draw_image_label() + self.draw_image_selection() + self.draw_options() + self.draw_additional_options() + self.draw_buttons() - self.create_image_label() - self.create_text_label() - self.open_image() - self.display_options() - self.additional_options() - self.apply_cancel() - - def create_image_label(self): - image_label = tk.Label( - self.top, text="(image preview)", height=8, width=32, bg="white" + def draw_image(self): + self.image_label = tk.Label( + self, text="(image preview)", height=8, width=32, bg="white" ) - image_label.grid(pady=5) + self.image_label.grid(row=0, column=0, pady=5, sticky="nsew") - def create_text_label(self): - text_label = tk.Label(self.top, text="Image filename: ") - text_label.grid() + def draw_image_label(self): + label = tk.Label(self, text="Image filename: ") + label.grid(row=1, column=0, sticky="ew") - def open_image_link(self): + def draw_image_selection(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=2) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.grid(row=2, column=0, sticky="ew") + + entry = tk.Entry(frame, textvariable=self.file_name) + entry.focus() + entry.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="...", command=self.click_open_image) + button.grid(row=0, column=1, sticky="ew") + + button = tk.Button(frame, text="Clear", command=self.click_clear) + button.grid(row=0, column=2, sticky="ew") + + def draw_options(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + frame.grid(row=3, column=0, sticky="ew") + + button = tk.Radiobutton( + frame, text="upper-left", value=1, variable=self.radiovar + ) + button.grid(row=0, column=0, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="centered", value=2, variable=self.radiovar) + button.grid(row=0, column=1, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button.grid(row=0, column=2, sticky="ew") + self.options.append(button) + + button = tk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button.grid(row=0, column=3, sticky="ew") + self.options.append(button) + + def draw_additional_options(self): + checkbutton = tk.Checkbutton( + self, text="Show grid", variable=self.show_grid_var + ) + checkbutton.grid(row=4, column=0, sticky="ew", padx=5) + + checkbutton = tk.Checkbutton( + self, + text="Adjust canvas size to image dimensions", + variable=self.adjust_to_dim_var, + command=self.click_adjust_canvas, + ) + checkbutton.grid(row=5, column=0, sticky="ew", padx=5) + + self.show_grid_var.set(1) + self.adjust_to_dim_var.set(0) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.grid(row=6, column=0, pady=5, sticky="ew") + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_open_image(self): filename = filedialog.askopenfilename( initialdir=str(BACKGROUNDS_PATH), title="Open", @@ -65,102 +138,39 @@ class CanvasWallpaper: ("All Files", "*"), ), ) - - # fill the file name into the file name entry - img_open_frame = self.top.grid_slaves(2, 0)[0] - filename_entry = img_open_frame.grid_slaves(0, 0)[0] - filename_entry.delete(0, tk.END) - filename_entry.insert(tk.END, filename) - - # display that onto the label - img_label = self.top.grid_slaves(0, 0)[0] if filename: + self.file_name.set(filename) + width, height = 250, 135 img = Image.open(filename) - img = img.resize((250, 135), Image.ANTIALIAS) + img = img.resize((width, height), Image.ANTIALIAS) tk_img = ImageTk.PhotoImage(img) - img_label.config(image=tk_img, width=250, height=135) - img_label.image = tk_img + self.image_label.config(image=tk_img, width=width, height=height) + self.image_label.image = tk_img - def clear_link(self): + def click_clear(self): """ delete like shown in image link entry if there is any :return: nothing """ # delete entry - img_open_frame = self.top.grid_slaves(2, 0)[0] - filename_entry = img_open_frame.grid_slaves(0, 0)[0] - filename_entry.delete(0, tk.END) - + self.file_name.set("") # delete display image - img_label = self.top.grid_slaves(0, 0)[0] - img_label.config(image="", width=32, height=8) - - def open_image(self): - f = tk.Frame(self.top) - - var = tk.StringVar(f, value="") - e = tk.Entry(f, textvariable=var) - e.focus() - e.grid() - - b = tk.Button(f, text="...", command=self.open_image_link) - b.grid(row=0, column=1) - - b = tk.Button(f, text="Clear", command=self.clear_link) - b.grid(row=0, column=2) - - f.grid() - - def display_options(self): - f = tk.Frame(self.top) - - b1 = tk.Radiobutton(f, text="upper-left", value=1, variable=self.radiovar) - b1.grid(row=0, column=0) - - b2 = tk.Radiobutton(f, text="centered", value=2, variable=self.radiovar) - b2.grid(row=0, column=1) - - b3 = tk.Radiobutton(f, text="scaled", value=3, variable=self.radiovar) - b3.grid(row=0, column=2) - - b4 = tk.Radiobutton(f, text="titled", value=4, variable=self.radiovar) - b4.grid(row=0, column=3) - - # self.radiovar.set(1) - - f.grid() - - def adjust_canvas_size(self): + self.image_label.config(image="", width=32, height=8) + def click_adjust_canvas(self): # deselect all radio buttons and grey them out if self.adjust_to_dim_var.get() == 1: self.radiovar.set(0) - option_frame = self.top.grid_slaves(3, 0)[0] - for i in option_frame.grid_slaves(): - i.config(state=tk.DISABLED) - + for option in self.options: + option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again elif self.adjust_to_dim_var.get() == 0: - option_frame = self.top.grid_slaves(3, 0)[0] - for i in option_frame.grid_slaves(): - i.config(state=tk.NORMAL) - self.radiovar.set(1) + self.radiovar.set(1) + for option in self.options: + option.config(state=tk.NORMAL) else: - logging.error("setwallpaper.py adjust_canvas_size invalid value") - - def additional_options(self): - b = tk.Checkbutton(self.top, text="Show grid", variable=self.show_grid_var) - b.grid(sticky=tk.W, padx=5) - b = tk.Checkbutton( - self.top, - text="Adjust canvas size to image dimensions", - variable=self.adjust_to_dim_var, - command=self.adjust_canvas_size, - ) - b.grid(sticky=tk.W, padx=5) - self.show_grid_var.set(1) - self.adjust_to_dim_var.set(0) + logging.error("canvasbackground.py adjust_canvas_size invalid value") def delete_canvas_components(self, tag_list): """ @@ -316,7 +326,7 @@ class CanvasWallpaper: self.canvas.itemconfig(i, state=tk.NORMAL) self.canvas.lift(i) else: - logging.error("setwallpaper.py show_grid invalid value") + logging.error("canvasbackground.py show_grid invalid value") def save_wallpaper_options(self): self.app.radiovar.set(self.radiovar.get()) @@ -324,51 +334,39 @@ class CanvasWallpaper: self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) def click_apply(self): - img_link_frame = self.top.grid_slaves(2, 0)[0] - filename = img_link_frame.grid_slaves(0, 0)[0].get() + filename = self.file_name.get() if not filename: self.delete_canvas_components(["wallpaper"]) - self.top.destroy() + self.destroy() self.app.current_wallpaper = None self.save_wallpaper_options() return + try: img = Image.open(filename) self.app.current_wallpaper = img except FileNotFoundError: - print("invalid filename, draw original white plot") + logging.error("invalid background: %s", filename) if self.app.wallpaper_id: self.canvas.delete(self.app.wallpaper_id) - self.top.destroy() + self.destroy() return self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) if self.adjust_to_dim_var.get() == 0: - self.app.radiovar.set(self.radiovar.get()) - - if self.radiovar.get() == ScaleOption.UPPER_LEFT.value: + option = ScaleOption(self.radiovar.get()) + if option == ScaleOption.UPPER_LEFT: self.upper_left(img) - elif self.radiovar.get() == ScaleOption.CENTERED.value: + elif option == ScaleOption.CENTERED: self.center(img) - elif self.radiovar.get() == ScaleOption.SCALED.value: + elif option == ScaleOption.SCALED: self.scaled(img) - elif self.radiovar.get() == ScaleOption.TILED.value: + elif option == ScaleOption.TILED: print("not implemented yet") elif self.adjust_to_dim_var.get() == 1: self.canvas_to_image_dimension(img) self.show_grid() - self.top.destroy() - - def apply_cancel(self): - f = tk.Frame(self.top) - - b = tk.Button(f, text="Apply", command=self.click_apply) - b.grid(row=0, column=0) - - b = tk.Button(f, text="Cancel", command=self.top.destroy) - b.grid(row=0, column=1) - - f.grid(pady=5) + self.destroy() diff --git a/coretk/coretk/dialogs/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py index 0267a300..dbc5a29c 100644 --- a/coretk/coretk/dialogs/sizeandscale.py +++ b/coretk/coretk/dialogs/sizeandscale.py @@ -4,7 +4,7 @@ size and scale import tkinter as tk from functools import partial -from coretk.dialogs.setwallpaper import ScaleOption +from coretk.dialogs.canvasbackground import ScaleOption DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 1d9adca1..bbb72895 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,10 +8,10 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH +from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.dialogs.setwallpaper import CanvasWallpaper from coretk.dialogs.sizeandscale import SizeAndScale @@ -386,7 +386,8 @@ class MenuAction: self.app.size_and_scale = SizeAndScale(self.app) def canvas_set_wallpaper(self): - self.app.set_wallpaper = CanvasWallpaper(self.app) + dialog = CanvasBackgroundDialog(self.app, self.app) + dialog.show() def help_core_github(self): webbrowser.open_new("https://github.com/coreemu/core") From 8c4ed15629ce455d85145dddcebee0fc1245e36c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 14:09:59 -0800 Subject: [PATCH 059/305] converted canvas size and scale dialog to use common dialog class --- coretk/coretk/dialogs/canvassizeandscale.py | 211 +++++++++++++++ coretk/coretk/dialogs/sizeandscale.py | 277 -------------------- coretk/coretk/menuaction.py | 14 +- 3 files changed, 214 insertions(+), 288 deletions(-) create mode 100644 coretk/coretk/dialogs/canvassizeandscale.py delete mode 100644 coretk/coretk/dialogs/sizeandscale.py diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py new file mode 100644 index 00000000..20b464d0 --- /dev/null +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -0,0 +1,211 @@ +""" +size and scale +""" +import tkinter as tk +from tkinter import font + +from coretk.dialogs.canvasbackground import ScaleOption +from coretk.dialogs.dialog import Dialog + +DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] + + +class SizeAndScaleDialog(Dialog): + def __init__(self, master, app): + """ + create an instance for size and scale object + + :param app: main application + """ + super().__init__(master, app, "Canvas Size and Scale", modal=True) + self.meter_per_pixel = self.app.canvas.meters_per_pixel + self.section_font = font.Font(weight="bold") + + # get current canvas dimensions + canvas = self.app.canvas + plot = canvas.find_withtag("rectangle") + x0, y0, x1, y1 = canvas.bbox(plot[0]) + width = abs(x0 - x1) - 2 + height = abs(y0 - y1) - 2 + self.pixel_width = tk.IntVar(value=width) + self.pixel_height = tk.IntVar(value=height) + self.meters_width = tk.IntVar(value=width * self.meter_per_pixel) + self.meters_height = tk.IntVar(value=height * self.meter_per_pixel) + self.scale = tk.IntVar(value=self.meter_per_pixel * 100) + self.x = tk.IntVar(value=0) + self.y = tk.IntVar(value=0) + self.lat = tk.DoubleVar(value=47.5791667) + self.lon = tk.DoubleVar(value=-122.132322) + self.alt = tk.DoubleVar(value=2.0) + self.save_default = tk.BooleanVar(value=False) + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_size() + self.draw_scale() + self.draw_reference_point() + self.draw_save_as_default() + self.draw_buttons() + + def draw_size(self): + label = tk.Label(self, text="Size", font=self.section_font) + label.grid(sticky="w") + + # draw size row 1 + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = tk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.pixel_width) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.pixel_height) + entry.grid(row=0, column=3, sticky="ew") + label = tk.Label(frame, text="Pixels") + label.grid(row=0, column=4, sticky="w") + + # draw size row 2 + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + label = tk.Label(frame, text="Width") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.meters_width) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="x Height") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.meters_height) + entry.grid(row=0, column=3, sticky="ew") + label = tk.Label(frame, text="Meters") + label.grid(row=0, column=4, sticky="w") + + def draw_scale(self): + label = tk.Label(self, text="Scale", font=self.section_font) + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = tk.Label(frame, text="100 Pixels =") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.scale) + entry.grid(row=0, column=1, sticky="ew") + label = tk.Label(frame, text="Meters") + label.grid(row=0, column=2, sticky="w") + + def draw_reference_point(self): + label = tk.Label(self, text="Reference point", font=self.section_font) + label.grid(sticky="w") + label = tk.Label( + self, text="Default is (0, 0), the upper left corner of the canvas" + ) + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + + label = tk.Label(frame, text="X") + label.grid(row=0, column=0, sticky="w") + x_var = tk.StringVar(value=0) + entry = tk.Entry(frame, textvariable=x_var) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Y") + label.grid(row=0, column=2, sticky="w") + y_var = tk.StringVar(value=0) + entry = tk.Entry(frame, textvariable=y_var) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(self, text="Translates To") + label.grid(sticky="w") + + frame = tk.Frame(self) + frame.grid(sticky="ew", pady=3) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = tk.Label(frame, text="Lat") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.lat) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Lon") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.lon) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(frame, text="Alt") + label.grid(row=0, column=4, sticky="w") + entry = tk.Entry(frame, textvariable=self.alt) + entry.grid(row=0, column=5, sticky="ew") + + def draw_save_as_default(self): + button = tk.Checkbutton( + self, text="Save as default?", variable=self.save_default + ) + button.grid(sticky="w", pady=3) + + def draw_buttons(self): + frame = tk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(sticky="ew") + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, pady=5, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, pady=5, sticky="ew") + + def redraw_grid(self): + """ + redraw grid with new dimension + + :return: nothing + """ + width, height = self.pixel_width.get(), self.pixel_height.get() + + canvas = self.app.canvas + canvas.config(scrollregion=(0, 0, width + 200, height + 200)) + + # delete old plot and redraw + for i in canvas.find_withtag("gridline"): + canvas.delete(i) + for i in canvas.find_withtag("rectangle"): + canvas.delete(i) + + canvas.draw_grid(width=width, height=height) + # lift anything that is drawn on the plot before + for tag in DRAW_OBJECT_TAGS: + for i in canvas.find_withtag(tag): + canvas.lift(i) + + def click_apply(self): + meter_per_pixel = float(self.scale.get()) / 100 + self.app.canvas.meters_per_pixel = meter_per_pixel + self.redraw_grid() + # if there is a current wallpaper showing, redraw it based on current wallpaper options + wallpaper_tool = self.app.set_wallpaper + current_wallpaper = self.app.current_wallpaper + if current_wallpaper: + if self.app.adjust_to_dim_var.get() == 0: + if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: + wallpaper_tool.upper_left(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.CENTERED.value: + wallpaper_tool.center(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.SCALED.value: + wallpaper_tool.scaled(current_wallpaper) + elif self.app.radiovar.get() == ScaleOption.TILED.value: + print("not implemented") + elif self.app.adjust_to_dim_var.get() == 1: + wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + wallpaper_tool.show_grid() + self.destroy() diff --git a/coretk/coretk/dialogs/sizeandscale.py b/coretk/coretk/dialogs/sizeandscale.py deleted file mode 100644 index dbc5a29c..00000000 --- a/coretk/coretk/dialogs/sizeandscale.py +++ /dev/null @@ -1,277 +0,0 @@ -""" -size and scale -""" -import tkinter as tk -from functools import partial - -from coretk.dialogs.canvasbackground import ScaleOption - -DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] - - -class SizeAndScale: - def __init__(self, app): - """ - create an instance for size and scale object - - :param app: main application - """ - self.app = app - self.top = tk.Toplevel() - self.top.title("Canvas Size and Scale") - self.meter_per_pixel = self.app.canvas.meters_per_pixel - - self.size_chart() - self.scale_chart() - self.reference_point_chart() - self.save_as_default() - self.apply_cancel() - - def pixel_scrollbar_command(self, size_frame, entry_row, entry_column, event): - """ - change the value shown based on scrollbar action - - :param tkinter.Frame frame: pixel dimension frame - :param int entry_row: row number of entry of the frame - :param int entry_column: column number of entry of the frame - :param event: scrollbar event - :return: nothing - """ - pixel_frame = size_frame.grid_slaves(0, 0)[0] - pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] - val = int(pixel_entry.get()) - - if event == "-1": - new_val = val + 2 - elif event == "1": - new_val = val - 2 - - pixel_entry.delete(0, tk.END) - pixel_entry.insert(tk.END, str(new_val)) - - # change meter dimension - meter_frame = size_frame.grid_slaves(1, 0)[0] - meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] - meter_entry.delete(0, tk.END) - meter_entry.insert(tk.END, str(new_val * self.meter_per_pixel)) - - def meter_scrollbar_command(self, size_frame, entry_row, entry_column, event): - """ - change the value shown based on scrollbar action - - :param tkinter.Frame size_frame: size frame - :param int entry_row: row number of entry in the frame it is contained in - :param int entry_column: column number of entry in the frame in is contained in - :param event: scroolbar event - :return: nothing - """ - meter_frame = size_frame.grid_slaves(1, 0)[0] - meter_entry = meter_frame.grid_slaves(entry_row, entry_column)[0] - val = float(meter_entry.get()) - - if event == "-1": - val += 100.0 - elif event == "1": - val -= 100.0 - meter_entry.delete(0, tk.END) - meter_entry.insert(tk.END, str(val)) - - # change pixel dimension - pixel_frame = size_frame.grid_slaves(0, 0)[0] - pixel_entry = pixel_frame.grid_slaves(entry_row, entry_column)[0] - pixel_entry.delete(0, tk.END) - pixel_entry.insert(tk.END, str(int(val / self.meter_per_pixel))) - - def create_text_label(self, frame, text, row, column, sticky=None): - """ - create text label - :param tkinter.Frame frame: parent frame - :param str text: label text - :param int row: row number - :param int column: column number - :param sticky: sticky value - - :return: nothing - """ - text_label = tk.Label(frame, text=text) - text_label.grid(row=row, column=column, sticky=sticky, padx=3, pady=3) - - def create_entry(self, frame, default_value, row, column, width): - text_var = tk.StringVar(frame, value=str(default_value)) - entry = tk.Entry( - frame, textvariable=text_var, width=width, bg="white", state=tk.NORMAL - ) - entry.focus() - entry.grid(row=row, column=column, padx=3, pady=3) - - def size_chart(self): - label = tk.Label(self.top, text="Size") - label.grid(sticky=tk.W, padx=5) - - canvas = self.app.canvas - plot = canvas.find_withtag("rectangle") - x0, y0, x1, y1 = canvas.bbox(plot[0]) - w = abs(x0 - x1) - 2 - h = abs(y0 - y1) - 2 - - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - - f1 = tk.Frame(f) - pw_scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) - pw_scrollbar.grid(row=0, column=1) - self.create_entry(f1, w, 0, 0, 6) - pw_scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 0)) - - self.create_text_label(f1, " W x ", 0, 2) - - scrollbar = tk.Scrollbar(f1, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=4) - self.create_entry(f1, h, 0, 3, 6) - scrollbar.config(command=partial(self.pixel_scrollbar_command, f, 0, 3)) - self.create_text_label(f1, " H pixels ", 0, 7) - f1.grid(sticky=tk.W, pady=3) - - f2 = tk.Frame(f) - scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1) - self.create_entry(f2, w * self.meter_per_pixel, 0, 0, 8) - scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 0)) - self.create_text_label(f2, " x ", 0, 2) - - scrollbar = tk.Scrollbar(f2, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=4) - self.create_entry(f2, h * self.meter_per_pixel, 0, 3, 8) - scrollbar.config(command=partial(self.meter_scrollbar_command, f, 0, 3)) - self.create_text_label(f2, " meters ", 0, 5) - - f2.grid(sticky=tk.W, pady=3) - - f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - - def scale_chart(self): - label = tk.Label(self.top, text="Scale") - label.grid(padx=5, sticky=tk.W) - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - # self.create_text_label(f, "Scale", 0, 0, tk.W) - # f1 = tk.Frame(f) - self.create_text_label(f, "100 pixels = ", 0, 0) - self.create_entry(f, self.meter_per_pixel * 100, 0, 1, 10) - self.create_text_label(f, "meters", 0, 2) - # f1.grid(sticky=tk.W, pady=3) - f.grid(sticky=tk.W + tk.E, padx=5, pady=5, columnspan=2) - - def reference_point_chart(self): - label = tk.Label(self.top, text="Reference point") - label.grid(padx=5, sticky=tk.W) - - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - self.create_text_label( - f, - "The default reference point is (0, 0), the upper left corner of the canvas.", - 1, - 0, - tk.W, - ) - f1 = tk.Frame(f) - self.create_entry(f1, 0, 0, 0, 4) - self.create_text_label(f1, " X, ", 0, 1) - self.create_entry(f1, 0, 0, 2, 4) - self.create_text_label(f1, "Y = ", 0, 3) - self.create_entry(f1, 47.5791667, 0, 4, 13) - self.create_text_label(f1, " lat, ", 0, 5) - self.create_entry(f1, -122.132322, 0, 6, 13) - self.create_text_label(f1, "long", 0, 7) - f1.grid(row=2, column=0, sticky=tk.W, pady=3) - - f2 = tk.Frame(f) - self.create_text_label(f2, "Altitude: ", 0, 0) - self.create_entry(f2, 2.0, 0, 1, 11) - self.create_text_label(f2, " meters ", 0, 2) - f2.grid(row=3, column=0, sticky=tk.W, pady=3) - - f.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) - - def save_as_default(self): - var = tk.IntVar() - button = tk.Checkbutton(self.top, text="Save as default", variable=var) - button.grid(sticky=tk.W, padx=5, pady=5, columnspan=2) - - def redraw_grid(self, pixel_width, pixel_height): - """ - redraw grid with new dimension - - :param int pixel_width: width in pixel - :param int pixel_height: height in pixel - :return: nothing - """ - canvas = self.app.canvas - canvas.config(scrollregion=(0, 0, pixel_width + 200, pixel_height + 200)) - - # delete old plot and redraw - for i in canvas.find_withtag("gridline"): - canvas.delete(i) - for i in canvas.find_withtag("rectangle"): - canvas.delete(i) - - canvas.draw_grid(width=pixel_width, height=pixel_height) - # lift anything that is drawn on the plot before - for tag in DRAW_OBJECT_TAGS: - for i in canvas.find_withtag(tag): - canvas.lift(i) - - def click_apply(self): - size_frame = self.top.grid_slaves(1, 0)[0] - pixel_size_frame = size_frame.grid_slaves(0, 0)[0] - - pixel_width = int(pixel_size_frame.grid_slaves(0, 0)[0].get()) - pixel_height = int(pixel_size_frame.grid_slaves(0, 3)[0].get()) - - scale_frame = self.top.grid_slaves(3, 0)[0] - meter_per_pixel = float(scale_frame.grid_slaves(0, 1)[0].get()) / 100 - self.app.canvas.meters_per_pixel = meter_per_pixel - self.redraw_grid(pixel_width, pixel_height) - print(self.app.current_wallpaper) - print(self.app.radiovar) - # if there is a current wallpaper showing, redraw it based on current wallpaper options - wallpaper_tool = self.app.set_wallpaper - current_wallpaper = self.app.current_wallpaper - if current_wallpaper: - if self.app.adjust_to_dim_var.get() == 0: - if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: - wallpaper_tool.upper_left(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.CENTERED.value: - wallpaper_tool.center(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.SCALED.value: - wallpaper_tool.scaled(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.TILED.value: - print("not implemented") - elif self.app.adjust_to_dim_var.get() == 1: - wallpaper_tool.canvas_to_image_dimension(current_wallpaper) - - wallpaper_tool.show_grid() - - self.top.destroy() - - def apply_cancel(self): - apply_button = tk.Button(self.top, text="Apply", command=self.click_apply) - apply_button.grid(row=7, column=0, pady=5) - cancel_button = tk.Button(self.top, text="Cancel", command=self.top.destroy) - cancel_button.grid(row=7, column=1, pady=5) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index bbb72895..7971553d 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -9,10 +9,10 @@ from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog +from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -from coretk.dialogs.sizeandscale import SizeAndScale def sub_menu_items(): @@ -155,15 +155,6 @@ def canvas_delete(): logging.debug("Click canvas delete") -def canvas_size_scale(): - logging.debug("Click canvas size/scale") - SizeAndScale() - - -def canvas_wallpaper(): - logging.debug("CLick canvas wallpaper") - - def canvas_previous(): logging.debug("Click canvas previous") @@ -383,7 +374,8 @@ class MenuAction: # self.application.core_editbar.create_toolbar() def canvas_size_and_scale(self): - self.app.size_and_scale = SizeAndScale(self.app) + dialog = SizeAndScaleDialog(self.app, self.app) + dialog.show() def canvas_set_wallpaper(self): dialog = CanvasBackgroundDialog(self.app, self.app) From bd7055a87c81f60f30e9e20e561675ad21fb04c0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:43:05 -0800 Subject: [PATCH 060/305] cleared out old files, leftovers from merge --- coretk/coretk/coreclient.py | 2 +- coretk/coretk/coregrpc.py | 303 ---------------- coretk/coretk/custom_node.py | 19 - coretk/coretk/grpcmanagement.py | 337 ------------------ coretk/coretk/oldimage/OVS.gif | Bin 744 -> 0 bytes coretk/coretk/oldimage/core-icon.png | Bin 2931 -> 0 bytes .../coretk/oldimage/document-properties.gif | Bin 635 -> 0 bytes coretk/coretk/oldimage/host.gif | Bin 1189 -> 0 bytes coretk/coretk/oldimage/hub.gif | Bin 719 -> 0 bytes coretk/coretk/oldimage/lanswitch.gif | Bin 744 -> 0 bytes coretk/coretk/oldimage/link.gif | Bin 86 -> 0 bytes coretk/coretk/oldimage/marker.gif | Bin 375 -> 0 bytes coretk/coretk/oldimage/mdr.gif | Bin 1276 -> 0 bytes coretk/coretk/oldimage/observe.gif | Bin 1149 -> 0 bytes coretk/coretk/oldimage/oval.gif | Bin 174 -> 0 bytes coretk/coretk/oldimage/pc.gif | Bin 1300 -> 0 bytes coretk/coretk/oldimage/plot.gif | Bin 265 -> 0 bytes coretk/coretk/oldimage/rectangle.gif | Bin 160 -> 0 bytes coretk/coretk/oldimage/rj45.gif | Bin 755 -> 0 bytes coretk/coretk/oldimage/router.gif | Bin 1152 -> 0 bytes coretk/coretk/oldimage/router_green.gif | Bin 753 -> 0 bytes coretk/coretk/oldimage/run.gif | Bin 324 -> 0 bytes coretk/coretk/oldimage/select.gif | Bin 925 -> 0 bytes coretk/coretk/oldimage/start.gif | Bin 1131 -> 0 bytes coretk/coretk/oldimage/stop.gif | Bin 1204 -> 0 bytes coretk/coretk/oldimage/text.gif | Bin 127 -> 0 bytes coretk/coretk/oldimage/tunnel.gif | Bin 799 -> 0 bytes coretk/coretk/oldimage/twonode.gif | Bin 220 -> 0 bytes coretk/coretk/oldimage/wlan.gif | Bin 146 -> 0 bytes 29 files changed, 1 insertion(+), 660 deletions(-) delete mode 100644 coretk/coretk/coregrpc.py delete mode 100644 coretk/coretk/custom_node.py delete mode 100644 coretk/coretk/grpcmanagement.py delete mode 100755 coretk/coretk/oldimage/OVS.gif delete mode 100644 coretk/coretk/oldimage/core-icon.png delete mode 100644 coretk/coretk/oldimage/document-properties.gif delete mode 100644 coretk/coretk/oldimage/host.gif delete mode 100644 coretk/coretk/oldimage/hub.gif delete mode 100644 coretk/coretk/oldimage/lanswitch.gif delete mode 100644 coretk/coretk/oldimage/link.gif delete mode 100644 coretk/coretk/oldimage/marker.gif delete mode 100644 coretk/coretk/oldimage/mdr.gif delete mode 100644 coretk/coretk/oldimage/observe.gif delete mode 100644 coretk/coretk/oldimage/oval.gif delete mode 100644 coretk/coretk/oldimage/pc.gif delete mode 100644 coretk/coretk/oldimage/plot.gif delete mode 100644 coretk/coretk/oldimage/rectangle.gif delete mode 100644 coretk/coretk/oldimage/rj45.gif delete mode 100644 coretk/coretk/oldimage/router.gif delete mode 100644 coretk/coretk/oldimage/router_green.gif delete mode 100644 coretk/coretk/oldimage/run.gif delete mode 100644 coretk/coretk/oldimage/select.gif delete mode 100644 coretk/coretk/oldimage/start.gif delete mode 100644 coretk/coretk/oldimage/stop.gif delete mode 100644 coretk/coretk/oldimage/text.gif delete mode 100644 coretk/coretk/oldimage/tunnel.gif delete mode 100644 coretk/coretk/oldimage/twonode.gif delete mode 100644 coretk/coretk/oldimage/wlan.gif diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 08753781..0063a0c8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -539,7 +539,7 @@ class CoreClient: """ Create the interface for the two end of an edge, add a copy to node's interfaces - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to + :param coretk.coreclient.Edge edge: edge to add interfaces to :param int src_canvas_id: canvas id for the source node :param int dst_canvas_id: canvas id for the destination node :return: nothing diff --git a/coretk/coretk/coregrpc.py b/coretk/coretk/coregrpc.py deleted file mode 100644 index 5db0cc6d..00000000 --- a/coretk/coretk/coregrpc.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -Incorporate grpc into python tkinter GUI -""" -import logging -import os - -from core.api.grpc import client, core_pb2 -from coretk.dialogs.sessions import SessionsDialog -from coretk.linkinfo import Throughput -from coretk.wirelessconnection import WirelessConnection - - -class CoreGrpc: - def __init__(self, app, sid=None): - """ - Create a CoreGrpc instance - """ - self.core = client.CoreGrpcClient() - self.session_id = sid - self.node_ids = [] - self.app = app - self.master = app.master - self.interface_helper = None - self.throughput_draw = Throughput(app.canvas, self) - self.wireless_draw = WirelessConnection(app.canvas, self) - - def log_event(self, event): - # logging.info("event: %s", event) - if event.link_event is not None: - logging.info("event: %s", event) - - self.wireless_draw.hangle_link_event(event.link_event) - - def log_throughput(self, event): - interface_throughputs = event.interface_throughputs - for i in interface_throughputs: - print("") - return - throughputs_belong_to_session = [] - for if_tp in interface_throughputs: - if if_tp.node_id in self.node_ids: - throughputs_belong_to_session.append(if_tp) - # bridge_throughputs = event.bridge_throughputs - self.throughput_draw.process_grpc_throughput_event( - throughputs_belong_to_session - ) - - def create_new_session(self): - """ - Create a new session - - :return: nothing - """ - response = self.core.create_session() - logging.info("created session: %s", response) - - # handle events session may broadcast - self.session_id = response.session_id - self.master.title("CORE Session ID " + str(self.session_id)) - self.core.events(self.session_id, self.log_event) - # self.core.throughputs(self.log_throughput) - - def delete_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - response = self.core.delete_session(sid) - logging.info("Deleted session result: %s", response) - - def terminate_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - s = self.core.get_session(sid).session - # delete links and nodes from running session - if s.state == core_pb2.SessionState.RUNTIME: - self.set_session_state("datacollect", sid) - self.delete_links(sid) - self.delete_nodes(sid) - self.delete_session(sid) - - def set_up(self): - """ - Query sessions, if there exist any, prompt whether to join one - - :return: existing sessions - """ - self.core.connect() - response = self.core.get_sessions() - - # if there are no sessions, create a new session, else join a session - sessions = response.sessions - if len(sessions) == 0: - self.create_new_session() - else: - dialog = SessionsDialog(self.app, self.app) - dialog.show() - - def get_session_state(self): - response = self.core.get_session(self.session_id) - # logging.info("get session: %s", response) - return response.session.state - - def set_session_state(self, state, custom_session_id=None): - """ - Set session state - - :param str state: session state to set - :return: nothing - """ - if custom_session_id is None: - sid = self.session_id - else: - sid = custom_session_id - - if state == "configuration": - response = self.core.set_session_state( - sid, core_pb2.SessionState.CONFIGURATION - ) - elif state == "instantiation": - response = self.core.set_session_state( - sid, core_pb2.SessionState.INSTANTIATION - ) - elif state == "datacollect": - response = self.core.set_session_state( - sid, core_pb2.SessionState.DATACOLLECT - ) - elif state == "shutdown": - response = self.core.set_session_state(sid, core_pb2.SessionState.SHUTDOWN) - elif state == "runtime": - response = self.core.set_session_state(sid, core_pb2.SessionState.RUNTIME) - elif state == "definition": - response = self.core.set_session_state( - sid, core_pb2.SessionState.DEFINITION - ) - elif state == "none": - response = self.core.set_session_state(sid, core_pb2.SessionState.NONE) - else: - logging.error("coregrpc.py: set_session_state: INVALID STATE") - - logging.info("set session state: %s", response) - - # def add_node(self, node_type, model, x, y, name, node_id): - # position = core_pb2.Position(x=x, y=y) - # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - # self.node_ids.append(node_id) - # response = self.core.add_node(self.session_id, node) - # logging.info("created node: %s", response) - # if node_type == core_pb2.NodeType.WIRELESS_LAN: - # d = OrderedDict() - # d["basic_range"] = "275" - # d["bandwidth"] = "54000000" - # d["jitter"] = "0" - # d["delay"] = "20000" - # d["error"] = "0" - # r = self.core.set_wlan_config(self.session_id, node_id, d) - # logging.debug("set wlan config %s", r) - # return response.node_id - - def edit_node(self, node_id, x, y): - position = core_pb2.Position(x=x, y=y) - response = self.core.edit_node(self.session_id, node_id, position) - logging.info("updated node id %s: %s", node_id, response) - # self.core.events(self.session_id, self.log_event) - - def delete_nodes(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - for node in self.core.get_session(sid).session.nodes: - response = self.core.delete_node(self.session_id, node.id) - logging.info("delete nodes %s", response) - - def delete_links(self, delete_session=None): - # sid = None - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - - for link in self.core.get_session(sid).session.links: - response = self.core.delete_link( - self.session_id, - link.node_one_id, - link.node_two_id, - link.interface_one.id, - link.interface_two.id, - ) - logging.info("delete links %s", response) - - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface 1 %s", interface) - - return interface - - # TODO add location, hooks, emane_config, etc... - def start_session( - self, - nodes, - links, - location=None, - hooks=None, - emane_config=None, - emane_model_configs=None, - wlan_configs=None, - mobility_configs=None, - ): - response = self.core.start_session( - session_id=self.session_id, - nodes=nodes, - links=links, - wlan_configs=wlan_configs, - mobility_configs=mobility_configs, - ) - logging.debug("Start session %s, result: %s", self.session_id, response.result) - - def stop_session(self): - response = self.core.stop_session(session_id=self.session_id) - logging.debug("coregrpc.py Stop session, result: %s", response.result) - - # TODO no need, might get rid of this - def add_link(self, id1, id2, type1, type2, edge): - """ - Grpc client request add link - - :param int session_id: session id - :param int id1: node 1 core id - :param core_pb2.NodeType type1: node 1 core node type - :param int id2: node 2 core id - :param core_pb2.NodeType type2: node 2 core node type - :return: nothing - """ - if1 = self.create_interface(type1, edge.interface_1) - if2 = self.create_interface(type2, edge.interface_2) - - response = self.core.add_link(self.session_id, id1, id2, if1, if2) - logging.info("created link: %s", response) - - # self.core.get_node_links(self.session_id, id1) - - # def get_session(self): - # response = self.core.get_session(self.session_id) - # nodes = response.session.nodes - # for node in nodes: - # r = self.core.get_node_links(self.session_id, node.id) - # logging.info(r) - - def launch_terminal(self, node_id): - response = self.core.get_node_terminal(self.session_id, node_id) - logging.info("get terminal %s", response.terminal) - os.system("xterm -e %s &" % response.terminal) - - def save_xml(self, file_path): - """ - Save core session as to an xml file - - :param str file_path: file path that user pick - :return: nothing - """ - response = self.core.save_xml(self.session_id, file_path) - logging.info("coregrpc.py save xml %s", response) - self.core.events(self.session_id, self.log_event) - - def open_xml(self, file_path): - """ - Open core xml - - :param str file_path: file to open - :return: session id - """ - response = self.core.open_xml(file_path) - self.session_id = response.session_id - logging.debug("coreprgc.py open_xml(): %s", response.result) - - def close(self): - """ - Clean ups when done using grpc - - :return: nothing - """ - logging.debug("Close grpc") - self.core.close() diff --git a/coretk/coretk/custom_node.py b/coretk/coretk/custom_node.py deleted file mode 100644 index 5cc8ea75..00000000 --- a/coretk/coretk/custom_node.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -edit node types -""" - -import tkinter as tk - - -class EditNodeTypes: - def __init__(self): - self.top = tk.Toplevel - self.top.title("CORE Node Types") - - def node_types(self): - """ - list box of node types - :return: - """ - lbl = tk.Label(self.top, text="Node types") - lbl.grid() diff --git a/coretk/coretk/grpcmanagement.py b/coretk/coretk/grpcmanagement.py deleted file mode 100644 index ffc1f0f9..00000000 --- a/coretk/coretk/grpcmanagement.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -Manage useful informations about the nodes, edges and configuration -that can be useful for grpc, acts like a session class -""" -import logging - -from core.api.grpc import core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping -from coretk.interface import Interface, InterfaceManager -from coretk.mobilitynodeconfig import MobilityNodeConfig -from coretk.wlannodeconfig import WlanNodeConfig - -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] - - -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - -class GrpcManager: - def __init__(self, grpc): - self.nodes = {} - self.edges = {} - self.id = None - # A list of id for re-use, keep in increasing order - self.reusable = [] - - self.preexisting = [] - self.core_grpc = None - - # self.update_preexisting_ids() - # self.core_id_to_canvas_id = {} - self.interfaces_manager = InterfaceManager() - - # map tuple(core_node_id, interface_id) to and edge - # self.node_id_and_interface_to_edge_token = {} - self.core_mapping = CoreToCanvasMapping() - - self.wlanconfig_management = WlanNodeConfig() - self.mobilityconfig_management = MobilityNodeConfig() - - def update_preexisting_ids(self): - """ - get preexisting node ids - :return: - """ - max_id = 0 - client = self.core_grpc.core - sessions = client.get_sessions().sessions - for session_summary in sessions: - session = client.get_session(session_summary.id).session - for node in session.nodes: - if node.id > max_id: - max_id = node.id - self.preexisting.append(node.id) - self.id = max_id + 1 - self.update_reusable_id() - - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - - def get_id(self): - """ - Get the next node id as well as update id status and reusable ids - - :rtype: int - :return: the next id to be used - """ - if len(self.reusable) == 0: - new_id = self.id - self.id = self.id + 1 - return new_id - else: - return self.reusable.pop(0) - - def add_node(self, session_id, canvas_id, x, y, name): - """ - Add node, with information filled in, to grpc manager - - :param int session_id: session id - :param int canvas_id: node's canvas id - :param int x: x coord - :param int y: y coord - :param str name: node type - :return: nothing - """ - node_type = None - node_model = None - if name in link_layer_nodes: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name in network_layer_nodes: - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("grpcmanagemeny.py INVALID node name") - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) - - # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - - # set default mobility configuration for wireless node - self.mobilityconfig_management.set_default_configuration(node_type, nid) - - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id - logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", - session_id, - x, - y, - name, - ) - - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.append(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reuseable.append(canvas_id) - self.reuseable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - - def create_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.grpcmanagement.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[src_canvas_id].interfaces.append(src_interface) - logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, - ) - - dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): - """ - Add an edge to grpc manager - - :param int session_id: core session id - :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node - - :return: nothing - """ - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id - - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_one_id, src_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) - - if dst_interface is not None: - # self.node_id_and_interface_to_edge_token[tuple([node_two_id, dst_interface.id])] = token - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - - logging.debug("Adding edge to grpc manager...") - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/oldimage/OVS.gif b/coretk/coretk/oldimage/OVS.gif deleted file mode 100755 index 38fcbb2ea684f2cb3724f55bb668d66dd952ee4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 744 zcmVJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS diff --git a/coretk/coretk/oldimage/core-icon.png b/coretk/coretk/oldimage/core-icon.png deleted file mode 100644 index 0b0ff5aa3a51f71807691032f516d7a15d7da896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2931 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 diff --git a/coretk/coretk/oldimage/document-properties.gif b/coretk/coretk/oldimage/document-properties.gif deleted file mode 100644 index 732d8436455ba607ff30774ef83faa434789f775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 635 zcmZ?wbhEHb6krfwI9A3`UtizZ-QL^V)6>`4+Ydxt6Z*URCUj4n)H`Wv|CDJHr_Go& zZN}v3v!=|LJ$2^nX|v}}pFMZR-1#%-Eu6hz(VT@#<}O+?Z}GAP%a$!&x_sgCm5Z0J zTCxI&Rxe$-dg-b)D^{&twiXE1tzNrs?b`LL*REf)X2YuWo7QaFym8yMZ98^v+r4}D z-hI3FAKZQ9*uj%$j-Nbx?EKkN7cQN-eEIUFs~4`{xO(Hp^_#bD+`N6`*6o|O?p(Wh z_u4HGx_egDy&`;YEE0HVit9zMSJ`0=BM zPwqW>djIj$2algUeDdt!vuDqqJb(1!`IDzF9=~}0?D@;5FJHcR`SRtfmoHwweE#Ou zi?^>|zJ2}b^_!P(-oASC_Vv5>Z$Eu}|LN1G&!0Yi`}Xzw_wPTxfBW&{`%fVH2}D1B z{`&dr*Uvvd@b}MOAo%z99|-;X_y6C25W)u(f3h%gG1N2YFaQB4P8is4G}PBC$jQn` zOH0W%^)$&PmQ9?P6RzIdBv~967UJ*YYhIu;UA!+K3@N%$caBBCo?vDZnK?>>} z4H6mGRzwyX@-nz_KC4-2cu1l_BwJ^i<gzK44%5h8Cn(&TO1gfn3+!O Kwon&fum%8seSYfz diff --git a/coretk/coretk/oldimage/host.gif b/coretk/coretk/oldimage/host.gif deleted file mode 100644 index 5bd60ae3d34d9bcd8be206ba2a8a701b19aea319..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1189 zcmb``30Kkw008hm262dT`d1VcH7ko8t5<8Y=8+H2yze8AmB-d>TJ62nYg4kad?v4& zM~5QL6hp;kPN#X`g=i`SsGx=l2;OM*vP`yphyA|8&&!8?{OcqFKn51n00031BLEHn zNC*Id0!Ap-3M58&meYM}^Nii1^A`;HUW+aDPOb$JlayBa^A~QAaVrt~&^ysVU zu~#pg$<9c~y!cbrrA)@p=L&u~%ef84YDQ6|53A zyOhN)Z>=og)RebX6*gBFKC7*0t$xCN#;)aZI%*#_*0XpGPkXscKDVf^?p}MtlfL?r z{)STi^M``Q$GuIDyP7#eFUp6TS;FS0!nVrczj^H)ZQ`~nQ9F0AF*Tvx5@_UB?C>0{ufgN&9cE}$zZ!w_;Nz{*UVt6WVly6 z(m6TOEfNXjqx|X7mt*2l#aPdzNccwFDU}Q)4o~IY1S8%i#nxVJG-+|;J&gZw=hEE2=`yIo!;s0j01%;ph13(4-_1_5q zu>%;8WN#j;`#OSvdLYVS@EK^2sT%CutgEd(_JPmM)a$tvWO-8!ZZMjb+;`VMC~55O zZ8Nu)t;HMEVqV7i7$TIA?NOFFUVpL&d_}6G;!tO9Hwt5&FH99hSJIbFigv>uofpWs zil%hc_f_o8f&6TSug$RC!irVvj|0^3kD=;Q-S+#9SWcSL6V#M4uup%lA`x$XVv5UD z|6pDlk~f#;>(=$uM(SdIU&W(eXoPydho|^6LAH- z5a8TqvnWKEOTGL2@5uXX*GJ%NS|CpVgB{s(IKY-cv>&v9ThKhI(04%|NGmAs4*3eV zF&DNkP4DBLKw}yiL%OoOERP)rdMHmXC1Xvn?aG|Pp=5wzTGz6H@;FVQb$gC}yVs diff --git a/coretk/coretk/oldimage/hub.gif b/coretk/coretk/oldimage/hub.gif deleted file mode 100644 index 17f7c4d3ef726f744da685423057366093df33f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 719 zcmZ?wbhEHbRA3NeIF`X6<6R-+T`3<>s}$0x8qum2(W)NPsSyiAUD`>#I?27dDHHY5 zChDb6G|ZS{kU7OLYpPMsG?V;UrunnXi|3jb&$BFDXi>JnvTT7>*?jBr1(p@_Eh`pS zRm`^nk&CQB{tt%H;S1z=vTxeUh(6(loea&Lqx}}bFOPuPKIM)NwQisM> zt_{l^n^$`_t#EE#=hC{)xow?q+bXxNO@WAtOJ`nH{!xc%(pooDClzr6I=^<~Fy zEI)o@`=tlFu0Gm*?eU&#kM~}Ge1=Lu@h1x-7ehUR4g(N?;)H?yUqgLUb4zPmdq<0u zTu*Obf4`D_=L9(`C!;~HitzeCPr?FNJxqa zbCy%KUAIBbJ2m~}sncho)np9qjxL(X>fF`|N{-XJqoblAaq)7Kbe~IXuu89G zY>4^!<#n%FX}%(x=oD7Zx(x^D7Cd=>$48HrD@OVHGfFIMB$#qu{aN1LM(d zN%JxW&7zjh$r?;@OmoaOK04YXZC%G>x#`KtDFRD*voZx2yYdE7cm|vGV*3$4sHUKQA;Xt`D2tb#>OIgNj>peGaF!W-$oH tY>1eD&xfTUiIIg<%3(%;VvDJ9wZvc%wRbpgMV>J9(izdZ9gfp+0+}KYOA*e5OBqqdk75L4Bk` zeWOBtq(FhHMS!M3gR4h^r$~aQNrI?Dg|0+}u0w{dOogdTg{n`7s!xckMvAgji>+0Q zu1SuzRgA7lkGD#Xw@Q$=SB#%r9qZk@Ytp1W|LyjZ2mSf$H#qQ6_I&0DF?cci~~ zrNDcq!F#8|eW=5Ks>Fe;#fPuQh_T6uvdNFN%aggzn!VDVz|)|@)uF=GrpDN($JnUI z*{jOhtjpW3%-prm-?-G_xzypg)Z)9<;=R}7%*@Qp%*@Qp%*@QpA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=dv*jE#jucwYQHYIwy=OOkz4Hj~p>W&4i3Z zU|?EVaBDeOZhEkLdR2@d7~9=~8y-G!bar@qcWq-WUF1-iLJ&x!2p8np`UC026(iCh zJzDS}A-9DYGIZ!5v5|<6A4!l-8|AaEc70|*i*d`R)429F;{oItU%<%@ul z7Q3Zf+45z~nKf_b+}ZP|Np6FNvUCYkCQX|-b@KEHbQ>Y5MXMskx^$~ki(gevHEK0! zR;ET=O%?)6_2^ZlTAP-%V2Cc;v3TMBba2S8*t>7d8bD;&uG_Lr69G_J06~HV5GGUz aLDL40AWED>vBKp`qSL5Tt6ohg5CA)88DCBS diff --git a/coretk/coretk/oldimage/link.gif b/coretk/coretk/oldimage/link.gif deleted file mode 100644 index 55532ecf0d14eecb81f57e71ae8f90cd9c8f2fea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmZ?wbhEHblwgox_`m=Kia%Kx85kHDbU=KN3M?hZsq{JzSM|!8CHm iM7a#L1g zw35rPkmRJB!>gvuyrA2_kkrJV)Xlf=-J;{txy;PW+SI|;*4E(H*5%&Z_wUx>;o<-P z|NsC0A^8LW002J#ECc`q02lxm000J*z@KnPED`}ofN^OAs3wpIl1UYa7>b8-NL-kM<*DutFxg;BP1>~th8?)DLBExWd|U~ Vk^lwFilNX&NxIh8*xA-W06UFctKt9v diff --git a/coretk/coretk/oldimage/mdr.gif b/coretk/coretk/oldimage/mdr.gif deleted file mode 100644 index d6762f6500828ead06fb73c7a9061165ccdef85b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1276 zcmb``jXTo`0KoCzh-N62mQF9`y{bhS8&Di5W%q3E|!j5ZXj{V1dP z7^69zTAohjVrksC>#Y$b%3K=zJhSp5qbmMJTLzOyzDB>y-XS*imde`bawS84 zmw2%6!-V0cL*B2(Jh`PG1(ilOjJz+z~L3DWyE~ zO;xt!+NFMoy%loArqJ-l_Jm(**X)msz`k|c5+7%ztE@5^S_rHsgwwkfx+OT=Z$;za z1?W%IUsZdN*kB!mhEtzXfV>X7ht>Edn=uv}N8`!@xG`X6rD(8m2lfq+q_=}^$LNJe zrch2W6c0TaQtZgz-JcRb=$y)|Wj z-BiLi_^0&q5I9?(v7Hx8apXh0#<03WyT?yj39`SYQ1$*P0GqVK6ELG(0k9Ks?dS+d z3pf%;4@qAVb9%@ro@T;!2e%9u$e;%Wd0%mEb+qw`b4TNcjXCa=mAQUIuzk0S85EZ* zBUtH@1DtHVzSAt$x4=OWCFxq)a-ePq&RPZ!3`gHsVKo}*{{&+f1UUU>gn=xwL|_AW r4z4Sh`#3ytbr^3Q6*$HAfFoFup6oRk3ar@WthBAq6&LC31nl__5JiQ; diff --git a/coretk/coretk/oldimage/observe.gif b/coretk/coretk/oldimage/observe.gif deleted file mode 100644 index 6b66e7305f35484095a2182ed828c6507c72a746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1149 zcmd_pYfn=L0LJlylbbZAPQ->d0;0q*%+PGvh{hCQEG*WwP@oJp1|%#lOP0+=yD9^E zJMC#%P#A&~sl5%f$fdVaENf{gXltbuDYVeS>4mnZW77@Ay;;_Njy?H(f#=2lWZqG%4!O^wyx&OriRAmhAY&|RN9pvX;*32s2z+p0&S8&Q(U~J z`1HDh)T$s}R}x#5q*i6+PpZl`6}e4S)vl^;SJiZ=Yv^hUU0qAp)G{=mF*Nl|O&t@i zXTqPe;4j#41E_80z%3B`6)#3hlP=O(9c(TGgqR=%a(E!f5Ddi(3PnRA>5yz#qJh;~oqAfY1LaE{BhQ%km%&jd zZ)}wU1Re`@`9oJa7`GpO_2 znf6ZT%y*~P#QJrKeqCd6rkk^g3>#8|2exj&Pd3Cen=<33%!K@ELWXCN zky+%P*>~UUlh66(^ZwC!|AUxE{;{~g*yF%>j3qE(2~5TX71p5gNl<0m(mag`!3!a6 zj6F1E4?SEA|F#s-FGUP<7R!Rey5O)mJx^D>3o)zS8Hdlb>yO>g zpS$Rb*U=ZRf&bZZ{@I%V0OLSG{CB(wAmv}6G4)7z54#w5GAY0Qgx$RVR97jNeyk-K z!%n+IJ#Z+6-;>|?jPuv|H^lhNu21mKffLy5bD4QP?c|#|n0Jp8wco#gbTJ1jr?B#o zpwIZ0DDQaz=@LnV&pIoiXMa6!%xH2@H(fh#3yU%-1l+Ma#DMEE?9C(%k6MeqHBjI^ zNfj8@$s_i!;2S6J=`b1%yJ zjzYQTh5_ro()5X;@-#bfQ6lc4$C74ZnCAn1Vq}@Ai zXX99F^M5aGkwK}khXSw%@jRar$%MOj@_Rb53* zLseZvO+&*#LsMN-TSG%fPg_?@Ti;MmUsumSSJ%+k(AdDh*umJu$k^1_)ZE0}(#*=* z(#pot+Sba}-p06XUk)~1%W z=GOL>_RhA>?vAdW&hFl>u1PZ|Pn|S<=Cnz(7R{J7eaf6Av**s7Id8$7C2Ll!TE1r8 z>UA5|uGz9@%eKuMb{yEe{n+-M+jj2Wv3bvtz5Dml;r#-+uDu;gbhD7>etiD&*~3?# zUc7q#==JB9uU|ZP^Xu`OFR$LbeE9ayleb@AzkBuk{f}4gKfQhb`t^s;?>@YJ`SI7A zk6+$@eE0U#*AJiGfBgL6)0dC$zkdJx_0xxMKfZkb{OSA8&p&)21O&UZ4U+dI^{!gd>D|h#E6iqaFl|zFQsMJyw=OWRyvJa6$Rl!U zR@l1ZMcNNJ3l}mw>9FJ}Z2q#Shs%CVOlO(ttDwnBECCWCiy96tj+>F9s9ci5+v~R4 zt(VKRlY^y$HMgRo`OM^{iN{KBDit4fo9;Yc%kk90LmM147@K$wKlQwB;JwN4(@H0| z)u(1i(xn6iz5D;z&8dl-$AS zrM*MM*<1JUic6;qwoFj+F)Y&%Z06!ucyvOES=E6-%8p~vY3;2Fi@SBUUUBj^*tDgo zN2{iU;jnU_h0_HN#u!6a(QXcBZ~bi#no@LkeR1MeOki2y)~_Pu;>N(uQq(MZ@J~>; z?!kZ+-HH+o4h+K6bT)OUvr9TM2owiQY?jkgTFfED&(h#5pQ+c>=_GGEv0Ye)C&`VO tJw%~hJ~@t1va|g8z`l6e8jtN{jVv^D?$ diff --git a/coretk/coretk/oldimage/plot.gif b/coretk/coretk/oldimage/plot.gif deleted file mode 100644 index 3924adbf821d1cfeb34a0b9efc7e9d79041fd1df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265 zcmV+k0rvh!Nk%w1VITk?0OJ7w0094gp6-C3?t-B1gQ4!&*x1wB`seBX=<5FI>i+8M z{_E`i?Ck#S?f&lW|9_tDA^saEV<{jqFflS9bZBKDLT_bhZf77vZ*6d4Zg~J9`2+z9 z01p5x00000AOIi$00fut$w{ldIO|=y|6nLmg=3y*s-^|GzOdO&Wo)CfRNr_0?7Wx{ z7<>(jwj&azT$&g-m(u|NL}|P#3J?fbWzrac;9XF=-EZ@i;HCisbU;vTDio=PTh2!- z4^;pSCRKfgT}KOh00w)9eM1P2UXPDMla-Z2nVfzkot>MYnWCeXpQfiYsj51xheIN= PvmP6^x45~wyAc38TK{^> diff --git a/coretk/coretk/oldimage/rectangle.gif b/coretk/coretk/oldimage/rectangle.gif deleted file mode 100644 index ed271f5737d0e8c2e35e9f03735de27509996893..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmZ?wbhEHbRA5kGSjfcC(9rPV&GC|Nli|1^))!lG!&+qxHk36QOake}^#rBeM zi_xlcpRkb%J(0K*PL82|tP diff --git a/coretk/coretk/oldimage/rj45.gif b/coretk/coretk/oldimage/rj45.gif deleted file mode 100644 index 9ab7ac56dba8946e37182cf05ca160683f53e2e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 755 zcmZ?wbhEHbRA5kGI2O;~>+9?1=NA+d6cQ2=8X6iN?hzjD84=+X8R;Dr6%`ZX5gi>J z6B82`=NcCmmynQT6n4RsEljD$^YhO@cS6FCQP*6}( zQc_=US6^S>(9qD>*x1z6)Z1&^+iTL%(b3u2Icbu?gb8&MCe%-yIC1ji#;H>)Cr_R{ zZCcfoDO09TFP}cWa{Bb?bLJGyom)6-)~q>m=FFWtcj3b9MT@dltWaIDBy06*okfcl zEnd8M#fp@5>y(!)S+Ze+`id1RR<2yRYSpUMt5>gGyLR2WbsIKp*tBWW=FOY8Y}vAP z>(=etx9`}oW6z#F`}glZaNxkXbJB+n9lCT$_}H;y$B!RBdGh3`Q>RX!K7HoQnKy4Z z-n`*^`AY@?w*HGWo+|t_C-qG3B79HZ>KVjme2?5b& zEz#jV-gD;8n?E<)TfQaAf5D3RfsslI&C!uzn>KIRx+U6NVO@Rn-hKNI9E=XywfoTV zL#jvjoH)DJ{ph-LXCo9Z`uX0xb^Fd;ql*gOuIA>3CMJ58n)ce(Iwr;j78_ zwFCvl;?#p%KCCcW)XlD^vg!rURSp6*7r%Vup0Kc?jagL6VZj0h<|bY?J(HRl`SC3b zyuqttEQOK`I|U_lW>_{XJ;lP^w&RLwAk%&hA&wh48VA%T&VBgp=M(jZ2bpHG$bGPu zdfRWu>8&jPY;I>~*ysT1_lCK_Z;G0L58oIA}pZ@NkT43mOcrUkRiiszUY&owWZYf(DavUI*h z*+R?m`IhAit;!czR?M@im~U0Fz^Y=QHIS@aU=2bGZ7LU9RV}gxp~W^;3vH_wT30W! zt6peVy~w_Lk!{UV+uCJzwM!jq7dzH2v8!9|ShvKkewjnVN{2=uTIJfX%&}>eN7Hi8 zrWKAYYn)ovcr-8fY+m8gvevt0rAzzzz>YO;-J3mnw*+^u4e4I%(YH0ccfI$-ol*T8 zqbF>PnXt)s%5LAOK(r@r;%5Kpd;MqZOPsPLY1-DHSqD<4Z4I1#D0RlR;JHWAXKoLk zcO+!q(X81!vuE$jnX@x&(ea3-r;8WvjaqRodd1n&Mf*w@?=N4nzjE2Zs^tf(mmjKK zakzff;fB>mTGk!w*m$yU>zT57yZQXRtrza^xcp$xwa0s}KRz=GXchvBKUo;L82&TpFaQB4 zPcU%&WBAW07$KqoEo1|3* z!_CHs4~gB{Wjwl{j8FI;7LI4q)tb`Ld9+cY`_9GB?E#YVRbre*mzCJu8YN_JWf(8# z>@kmBq{2Dr(V_#q;`4JRHfIFNX)2%6h}xoYVahuGKn|4$2Or68>$+sav-qg5yleZT z1F{EQ6&e^t<(@SNFFV=0!e!fzj1|m1QimB?R2B#*X}O8mubAUl{8PT+3>%+Lg#jZo zyP&xDo)w8HoMQUPZqw~_6}C39@hP}WC~!Q?E^1uH;kf7sr-*iB!Ruppejj-M;IMAW zj{{9^{fcMi#yL;-o3$(d!6DU*=kH`|K0m)$Jl(wamesE}w|Cd`^Utq&S^qV5`n{@O zJJsWA7O!;-C}nzd{9fJrUiJBotO^$vRQ=p~{C-7Jz!!ez8;L&_GzNq`RCKykx`Bz$ LhNm#efx#L8|Dlf2 diff --git a/coretk/coretk/oldimage/router_green.gif b/coretk/coretk/oldimage/router_green.gif deleted file mode 100644 index 76e3ecd57c59ec99767f5641e701f908f09f0258..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 753 zcmV$ZwARvGsB7!0$gd`+|BqoR^Cx|C0iYY0J zDJqL9D~&5Hku5KgFE5iXF_tkhmNGJzGBlYqG@CRwn>IF_HaDF&IG;EPKS85GJf%E6r9DBULPDfMJ*GWE zq(eTZK0c^EMy5tVsX<4lNJytiLaIVQt3XMpN=&IvL#;zYu0u|$P(`stMzKamu}4#_ zRY$T%RIOG=vqwj?M@hCxSg=}Juw6^JOJB2LU$bIPyiQ`YWKF$IW3*;ZzE5SgXi&dU zP{2@9!BT3tZc@ThYq)P~xo>Q_Z*979RK-+pyK-^7bymn$R>@X$y?1rKd3C>eSj$*< zzk6EFT7krdfyIV|#)^%~l8(xhlg*iw&YG3ao0iX=n9!e@(Vw8yr=!)Xq}8jY*R816 zucz3rtJ<=#+_<#fy|v!Gw%@Eq<#>~vjA^8LW3IKlqEC2ui z03ZMs000O6fPaF6goTEOh=my(jE#=El$DGfh?9&+W>zhgGe=rwY)+0Jg^nay zZG3!fWM&|jQh9!Ub|jCOf|fFFeYa;FPI7*AUNAtGy_SqBI$bsnKeZk z1rZZ4K!FI3>S252fd&*X6#oDSAb`LD2Ng7J300IRJ9!Q{YVZ)>j z9!jK$fuo0!Bu}QqoGFqd0h>4*Jb*C40)`MMUew63L&%V$NtcEkS@R|XsZ_0E)ymZ? zSfx)g$2x_2c52nDS+{omiWDwWvQN#Ty}MSg+rDB4%@u4{ZQizg!^$i)AaK;ic-iiK ji-ij!x`Q2SKKvv~q(ODlW;XnF$?4M`P`G$4C=dWUiqcBu diff --git a/coretk/coretk/oldimage/run.gif b/coretk/coretk/oldimage/run.gif deleted file mode 100644 index 71dcc67eddc7b829b3221ba4823a04e93c317ba9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 324 zcmZ?wbhEHbRA5kGXkh>WMn*;!78X`kRv8%?d3kvQ0|P@tLt|rOQ&ZEBkdW~3@W#f* zzP`R`)22WfAZwX)2B~gy?XWL&6~Gx-+uY>JSuFtAW4$w*aj%1_PAOIL8t&n-yIt7K68$->CRAkUx!(gbo61M8v(>U}Ah^DZGE(+-Pd9n~wOxE_RahlU()Kf}#eGDh z*YY|kSclnhF*_f*ZL2LVvRih;gcu3Vl7L(W;oNF5dkLndh`{&>RXH)u)s9RO_M-ic zeWf|^96S<2Tyxsf7B~v9GqLh5uUZ+O;*@5-)iu^LcxTxBeY@PPj~q2OIdSsT=`&}~ Ko!3%ium%9#D0q7S diff --git a/coretk/coretk/oldimage/select.gif b/coretk/coretk/oldimage/select.gif deleted file mode 100644 index bb7e128c878317f6287564514a302e5e3c40c10e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 925 zcmX|AT}TvB6#nkcsH?a+J25!wWJw!YSVoy{g=r6ESt%99vPu|jWwavpvRe--w$f%s zYflbuf*4Z;~=R4<~Ip225ShbBKwQc6#Xwq9Tr2mq=851@csnR&8f+ycq>V-cDai3y~@!gr8wL0nBC zU{r)Hb5*5f7u61mZ3t|yIUb`VBe1-QqVleY#wih00Goly!xkwyVZ{1lB8I%Sj2 z2m-M!HhYQ+$jnGNeiv{GnB~nh`yh{T`At9|X4{_DxaFbKW*UGN8*$vq~F1bXlQwoK`C%h5?z2 z^9{6>N|K@L0aob!9DW(7DB*YfpQyG!K!#mAI~x7?(4Lbn6<4Dx zBsmXiOOfS?u)5v0FmUO{muJG>%YKj3_qk`!#gV43zkL%=4mf&UnMa1E`pa7HX6 zwqJ$`n~{uVTRP)1JIzL7vZz_If|{*~lS3(L>1YMNREp5@O@UgWlR;}sSO0_k0ekZL z_4(yVp4S^+tEp{9fME3$6#a%3d6h+8OxB~4_u{fkYWYPR?;%tdh>aI$;&(>#2d(q5 zq|ZS55Pb#FeTca%FjoY_pJqd!1@)Oxp9Ml1`7i-t4cyYgh!#e*aJztjI|SS%;9h|a zB09LQQ-@ea61aXdeO(}jc{0Q^LuMvumWN6CNC71urC^MNC<)^vJSd=Gf`Um(ai38{ z8O1oOm|(THknXlgcgI9TOhi;5A6OtpL5zkt4O28s(=ellSv}0@;UNQ$7?@{Zp@4-& z77{Ex<{-(z6Aqg*V)HyUZ^9BLSmGdMfWHe6JVmgA;2$Hb8db|?STjO~hb+(iC2}E= zy;n>}t!$*2i`p!Ao&2Q35-AghO2uI5=DfEu#zz=H@$_Mb?{Rq z{9+lOEYmC(YgRUCGIq4&M5(gkhc7$gRgRhMj=5@EvbrQ*Q!-U!TdHnN9cfQ>p3I%` zKE32ym!#DJX>CAC`=yLudghn1gHmqL|199o2K>39h3w)&E|JY=*YoT7d>;PKe_lXL z%Qpa=2Iup}^G|?k9cpo5kGH%3s7hbT?CS`gs@|e@IvxFC@zo~jk+^@))dtz&Y*T;l z?A{-~bETvTcUU_8qRZQ!rUz#@r|W!B>HK04c84jYlF?$DP{j?^i+0ZQ2+ZIX4SO-=6I%AGsiKi6a3?s^GQOGO8fMnKq);bg%%10X@M4zBI6uI7|O(9f`)8t&J&IEhZ*K% z8p0H3vMt-(>;N^g83y9QrosRx$V4G+SB}zh6w0L>BIQtq|HD3c{rvguotmB!tKG*1 zE-VAE0ptL&09yeP015#11C#)q0w@Kz0PqdK&j3}M2!tvku^OOe6M=9I;5xuH0$ktZ z0aXZc!vk)j#99QYB_Y3{$So3DOF@67l5SyG9hK5R#Tq<48*uD4oq7+)?=tY;nACer z`X3y|eIItSz^mDxWAgMc(LBsFqM1&#;6w|9Xu{EdnM5lKv9ggqFQlJ?40xjhKIq^k zU)07$ZGPwwk7V~J+4&?BlVb6uTR9Y~H`eCk^}vVI%Hwnk{46}Cna4KqIA)QbgHIY3 zl1D`3zs1}xsegxz*BL5&94hP%6%9yzEn==&%crFW;+X`=Xnfd;p0S?9 zTz`wXu$@1jB>69icnv2A%1q*_d#8!8+C0 zTZrvD>>8Vox>4Mdm_Es^ZH+W4(mo!~Z1|$qT$^|IwxO%!+@-oJh{nC8;r*=WWA`-;QAzjtz^Ii7u|z=Wv2fXj#VN>!l;b z?mLBTO~!ZdvdioVmi{Zhp5ZE9)4|Gr^t?m#a)!v5$*HJ3Tk$2o=T;c6)t=K>yv+RC(pN0M-Dd-mC&;N5vlB2PJtvQ!1!r1BjF z+s~Ac1T8?_TUcM_wWIJ%I-4ebdPz@^w*X1jEB<6*W6va?-5+;v-TUT4p_f*)Gl%!N9?d`v_mp{QI;sYb*91-N d=esbwJ0>tWSn~6fcWtY;{pRgIt;xz@4FI19FuMQ% diff --git a/coretk/coretk/oldimage/tunnel.gif b/coretk/coretk/oldimage/tunnel.gif deleted file mode 100644 index d574147f535122637516f4887d931779c5903ead..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 799 zcmZ?wbhEHbRA5kGI2Oae!0_MK*VoU_FDNJ|BqSs>G&DTiBRt$QBEl;&(mN_DDkjDw zIyyQgCMGVRu= zo;-Qlw5lmnrc9q+K7D%S^y$;*%qf~Xw{X_1S###hnLBsx!iCw37GTOS zvSsVmt=qS6-?3xIo;`c^@85smz=3n;qz@fBbm@}tv17-MA3uKbEDBDn2z=biC+ZOKAtSKatw%=FXNJe4Me>Sj+A1D9KCrS*RkU3* z;lrne-kjRjO)MM=OCIfUlrZJGkdVO4#LcG^5-~xMv7K2WY}*zN!ma^ zbPjCKZ)C4{X}6U-tSN3!tNM$%udfIkJDRAe;MWte!OBrWPFoND ze%zhu(T2k5n;-J6D%2?Cv=W`H(&hR5YrE{RRjOvEka_>yUw;J#YXB&pnhF2_ diff --git a/coretk/coretk/oldimage/twonode.gif b/coretk/coretk/oldimage/twonode.gif deleted file mode 100644 index 28e75fac3aadc9286cdb65b1269f3f1355f055d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmZ?wbhEHbRA5kGIK;vL1pmSK$$5tVNI>zQv_`U~f{}rNg+fV2s)AE~YGz)#f^&Xu zL1JDdgW^vXMlJ>x1|5(AAfp(Vn>=>i`Dbv-A$vka-~r3C5f^&JB{~NwA;X2aQL-=@frKKWwv3( zs%b?M|9mdLmiXy?;63*j?HbL7MqLIbcP^&3j?Qk~UiW^@i7u0sr`k2mm^rJirgNbX HCxbNrD`Hob diff --git a/coretk/coretk/oldimage/wlan.gif b/coretk/coretk/oldimage/wlan.gif deleted file mode 100644 index d72fe9c3f8db0c7013424313bc826eca7022c19a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmZ?wbhEHbRA5kGXkcUjg8%>jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A From 81e9bda65dc91aac48cc309870b09ae40f5d5b57 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 15:51:37 -0800 Subject: [PATCH 061/305] fixed broken get_mobility_configs --- coretk/coretk/coreclient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0063a0c8..d36932df 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -127,13 +127,15 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) + logging.info("wlan config(%s): %s", node.id, response) node_config = response.config config = {x: node_config[x].value for x in node_config} self.wlanconfig_management.configurations[node.id] = config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - for node_id in response.config: + logging.info("mobility configs: %s", response) + for node_id in response.configs: node_config = response.config[node_id].config config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config From c5d5226384b6ae7e0f93a2255d8954279fa31f8a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 16:20:35 -0800 Subject: [PATCH 062/305] updated configutils parser to save values within configoptions, allowing them to be reused to redraw a config --- coretk/coretk/configutils.py | 11 +++++------ coretk/coretk/dialogs/sessionoptions.py | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index 7c87f9dd..bb3e793e 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -78,9 +78,8 @@ def parse_config(options, values): :param dict options: option key mapping to configuration options :param dict values: option key mapping to widget values - :return: + :return: nothing """ - config = {} for key in options: option = options[key] value = values[key] @@ -88,8 +87,8 @@ def parse_config(options, values): config_value = value.get() if config_type == ConfigType.BOOL: if config_value == "On": - config_value = "1" + option.value = "1" else: - config_value = "0" - config[key] = config_value - return config + option.value = "0" + else: + option.value = config_value diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 6279d844..e887c2a1 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -27,8 +27,9 @@ class SessionOptionsDialog(Dialog): self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): - config = configutils.parse_config(self.options, self.values) + configutils.parse_config(self.options, self.values) session_id = self.app.core.session_id + config = {x: self.options[x].value for x in self.options} response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() From d7280a3f6dd303b4b43f39a988c490aec5f3322e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 4 Nov 2019 16:24:35 -0800 Subject: [PATCH 063/305] work on emane config --- coretk/coretk/canvasaction.py | 1 - coretk/coretk/coreclient.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index f45086ab..60df71b0 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -45,5 +45,4 @@ class CanvasAction: def display_emane_configuration(self): app = self.canvas.core.app dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) - print(dialog) dialog.show() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d36932df..a6d4f19c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -136,7 +136,7 @@ class CoreClient: response = self.client.get_mobility_configs(self.session_id) logging.info("mobility configs: %s", response) for node_id in response.configs: - node_config = response.config[node_id].config + node_config = response.configs[node_id].config config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config From 22601a4580971dd15e5844aa395342bc7c90fbcd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 4 Nov 2019 17:18:59 -0800 Subject: [PATCH 064/305] updated wlan config dialog to use common dialog class --- coretk/coretk/canvasaction.py | 9 +- coretk/coretk/dialogs/wlanconfig.py | 367 ++++++++++------------------ 2 files changed, 139 insertions(+), 237 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index b5f09333..6e1492ed 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -6,7 +6,7 @@ canvas graph action from core.api.grpc import core_pb2 from coretk.dialogs.nodeconfig import NodeConfigDialog -from coretk.dialogs.wlanconfig import WlanConfiguration +from coretk.dialogs.wlanconfig import WlanConfigDialog # TODO, finish classifying node types NODE_TO_TYPE = { @@ -36,8 +36,11 @@ class CanvasAction: def display_wlan_configuration(self, canvas_node): # print(self.canvas.grpc_manager.wlanconfig_management.configurations) - wlan_config = self.canvas.grpc_manager.wlanconfig_management.configurations[ + wlan_config = self.master.core.wlanconfig_management.configurations[ canvas_node.core_id ] - WlanConfiguration(self.canvas, self.node_to_show_config, wlan_config) + dialog = WlanConfigDialog( + self.master, self.master, self.node_to_show_config, wlan_config + ) + dialog.show() self.node_to_show_config = None diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 8065a5fa..e6e1cfd4 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -3,294 +3,193 @@ wlan configuration """ import tkinter as tk -from functools import partial +from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import NodeIconDialog -class WlanConfiguration: - def __init__(self, canvas, canvas_node, config): +class WlanConfigDialog(Dialog): + def __init__(self, master, app, canvas_node, config): """ create an instance of WlanConfiguration :param coretk.grpah.CanvasGraph canvas: canvas object :param coretk.graph.CanvasNode canvas_node: canvas node object """ - - self.canvas = canvas + super().__init__( + master, app, f"{canvas_node.name} Wlan Configuration", modal=True + ) self.image = canvas_node.image - self.node_type = canvas_node.node_type - self.name = canvas_node.name self.canvas_node = canvas_node - - self.top = tk.Toplevel() - self.top.title("wlan configuration") - self.node_name = tk.StringVar() - - # self.range_var = tk.DoubleVar() - # self.range_var.set(275.0) self.config = config - self.range_var = tk.StringVar() - self.range_var.set(config["basic_range"]) - # self.bandwidth_var = tk.IntVar() - self.bandwidth_var = tk.StringVar() - self.bandwidth_var.set(config["bandwidth"]) + self.name = tk.StringVar(value=canvas_node.name) + self.range_var = tk.StringVar(value=config["basic_range"]) + self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) + self.delay_var = tk.StringVar(value=config["delay"]) + self.loss_var = tk.StringVar(value=config["error"]) + self.jitter_var = tk.StringVar(value=config["jitter"]) + self.ip4_subnet = tk.StringVar() + self.ip6_subnet = tk.StringVar() + self.image_button = None + self.draw() - self.delay_var = tk.StringVar() + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_name_config() + self.draw_wlan_config() + self.draw_subnet() + self.draw_wlan_buttons() + self.draw_apply_buttons() - self.image_modification() - self.wlan_configuration() - self.subnet() - self.wlan_options() - self.config_option() - - def image_modification(self): + def draw_name_config(self): """ draw image modification part :return: nothing """ - f = tk.Frame(self.top, bg="#d9d9d9") - lbl = tk.Label(f, text="Node name: ", bg="#d9d9d9") - lbl.grid(row=0, column=0, padx=3, pady=3) - e = tk.Entry(f, textvariable=self.node_name, bg="white") - e.grid(row=0, column=1, padx=3, pady=3) - b = tk.Button(f, text="None") - b.grid(row=0, column=2, padx=3, pady=3) - b = tk.Button(f, image=self.image, command=lambda: self.click_image) - b.grid(row=0, column=3, padx=3, pady=3) - f.grid(padx=2, pady=2, ipadx=2, ipady=2) + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + frame.columnconfigure(0, weight=1) - def click_image(self): - NodeIconDialog(self.app, canvas_node=self.canvas_node, node_config=self) + entry = tk.Entry(frame, textvariable=self.name, bg="white") + entry.grid(row=0, column=0, padx=2, sticky="ew") - def create_string_var(self, val): - """ - create string variable for convenience + self.image_button = tk.Button(frame, image=self.image, command=self.click_icon) + self.image_button.grid(row=0, column=1, padx=3) - :param str val: text value - :return: nothing - """ - v = tk.StringVar() - v.set(val) - return v - - def scrollbar_command(self, entry_widget, delta, event): - """ - change text in entry based on scrollbar action (click up or down) - - :param tkinter.Entry entry_widget: entry needed for changing text - :param int or float delta: the amount to change - :param event: scrollbar event - :return: nothing - """ - try: - value = int(entry_widget.get()) - except ValueError: - value = float(entry_widget.get()) - entry_widget.delete(0, tk.END) - if event == "-1": - entry_widget.insert(tk.END, str(round(value + delta, 1))) - elif event == "1": - entry_widget.insert(tk.END, str(round(value - delta, 1))) - - def wlan_configuration(self): + def draw_wlan_config(self): """ create wireless configuration table :return: nothing """ - lbl = tk.Label(self.top, text="Wireless") - lbl.grid(sticky=tk.W, padx=3, pady=3) + label = tk.Label(self, text="Wireless") + label.grid(sticky="w", pady=2) - f = tk.Frame( - self.top, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - bg="#d9d9d9", + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + label = tk.Label( + frame, + text=( + "The basic range model calculates on/off " + "connectivity based on pixel distance between nodes." + ), ) + label.grid(row=0, columnspan=2, pady=2, sticky="ew") - lbl = tk.Label( - f, - text="The basic range model calculates on/off connectivity based on pixel distance between nodes.", - bg="#d9d9d9", - ) - lbl.grid(padx=4, pady=4) + label = tk.Label(frame, text="Range") + label.grid(row=1, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.range_var) + entry.grid(row=1, column=1, sticky="ew") - f1 = tk.Frame(f, bg="#d9d9d9") + label = tk.Label(frame, text="Bandwidth (bps)") + label.grid(row=2, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.bandwidth_var) + entry.grid(row=2, column=1, sticky="ew") - lbl = tk.Label(f1, text="Range: ", bg="#d9d9d9") - lbl.grid(row=0, column=0) + label = tk.Label(frame, text="Delay (us)") + label.grid(row=3, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.delay_var) + entry.grid(row=3, column=1, sticky="ew") - e = tk.Entry( - f1, - textvariable=self.create_string_var(self.config["basic_range"]), - width=5, - bg="white", - ) - e.grid(row=0, column=1) + label = tk.Label(frame, text="Loss (%)") + label.grid(row=4, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.loss_var) + entry.grid(row=4, column=1, sticky="ew") - lbl = tk.Label(f1, text="Bandwidth (bps): ", bg="#d9d9d9") - lbl.grid(row=0, column=2) + label = tk.Label(frame, text="Jitter (us)") + label.grid(row=5, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.jitter_var) + entry.grid(row=5, column=1, sticky="ew") - f11 = tk.Frame(f1, bg="#d9d9d9") - sb = tk.Scrollbar(f11, orient=tk.VERTICAL) - e = tk.Entry( - f11, - textvariable=self.create_string_var(self.config["bandwidth"]), - width=10, - bg="white", - ) - sb.config(command=partial(self.scrollbar_command, e, 1000000)) - e.grid() - sb.grid(row=0, column=1) - f11.grid(row=0, column=3) - - # e = tk.Entry(f1, textvariable=self.bandwidth_var, width=10) - # e.grid(row=0, column=4) - f1.grid(sticky=tk.W, padx=4, pady=4) - - f2 = tk.Frame(f, bg="#d9d9d9") - lbl = tk.Label(f2, text="Delay (us): ", bg="#d9d9d9") - lbl.grid(row=0, column=0) - - f21 = tk.Frame(f2, bg="#d9d9d9") - sb = tk.Scrollbar(f21, orient=tk.VERTICAL) - e = tk.Entry( - f21, textvariable=self.create_string_var(self.config["delay"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 5000)) - e.grid() - sb.grid(row=0, column=1) - f21.grid(row=0, column=1) - - lbl = tk.Label(f2, text="Loss (%): ", bg="#d9d9d9") - lbl.grid(row=0, column=2) - - f22 = tk.Frame(f2, bg="#d9d9d9") - sb = tk.Scrollbar(f22, orient=tk.VERTICAL) - e = tk.Entry( - f22, textvariable=self.create_string_var(self.config["error"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 0.1)) - e.grid() - sb.grid(row=0, column=1) - f22.grid(row=0, column=3) - - # e = tk.Entry(f2, textvariable=self.create_string_var(0)) - # e.grid(row=0, column=3) - f2.grid(sticky=tk.W, padx=4, pady=4) - - f3 = tk.Frame(f, bg="#d9d9d9") - lbl = tk.Label(f3, text="Jitter (us): ", bg="#d9d9d9") - lbl.grid() - f31 = tk.Frame(f3, bg="#d9d9d9") - sb = tk.Scrollbar(f31, orient=tk.VERTICAL) - e = tk.Entry( - f31, textvariable=self.create_string_var(self.config["jitter"]), bg="white" - ) - sb.config(command=partial(self.scrollbar_command, e, 5000)) - e.grid() - sb.grid(row=0, column=1) - f31.grid(row=0, column=1) - - f3.grid(sticky=tk.W, padx=4, pady=4) - f.grid(padx=3, pady=3) - - def subnet(self): + def draw_subnet(self): """ create the entries for ipv4 subnet and ipv6 subnet :return: nothing """ - f = tk.Frame(self.top) - f1 = tk.Frame(f) - lbl = tk.Label(f1, text="IPv4 subnet") - lbl.grid() - e = tk.Entry(f1, width=30, bg="white", textvariable=self.create_string_var("")) - e.grid(row=0, column=1) - f1.grid() + frame = tk.Frame(self) + frame.grid(pady=3, sticky="ew") + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) - f2 = tk.Frame(f) - lbl = tk.Label(f2, text="IPv6 subnet") - lbl.grid() - e = tk.Entry(f2, width=30, bg="white", textvariable=self.create_string_var("")) - e.grid(row=0, column=1) - f2.grid() - f.grid(sticky=tk.W, padx=3, pady=3) + label = tk.Label(frame, text="IPv4 Subnet") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.ip4_subnet) + entry.grid(row=0, column=1, sticky="ew") - def wlan_options(self): + label = tk.Label(frame, text="IPv6 Subnet") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.ip6_subnet) + entry.grid(row=0, column=3, sticky="ew") + + def draw_wlan_buttons(self): """ create wireless node options :return: """ - f = tk.Frame(self.top) - b = tk.Button(f, text="ns-2 mobility script...") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Link to all routers") - b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Choose WLAN members") - b.pack(side=tk.LEFT, padx=1) - f.grid(sticky=tk.W) + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) - def wlan_config_apply(self): - """ - retrieve user's wlan configuration and store the new configuration values + button = tk.Button(frame, text="ns-2 mobility script...") + button.grid(row=0, column=0, padx=2, sticky="ew") - :return: nothing - """ - config_frame = self.top.grid_slaves(row=2, column=0)[0] - range_and_bandwidth_frame = config_frame.grid_slaves(row=1, column=0)[0] - range_val = range_and_bandwidth_frame.grid_slaves(row=0, column=1)[0].get() - bandwidth = ( - range_and_bandwidth_frame.grid_slaves(row=0, column=3)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) + button = tk.Button(frame, text="Link to all routers") + button.grid(row=0, column=1, padx=2, sticky="ew") - delay_and_loss_frame = config_frame.grid_slaves(row=2, column=0)[0] - delay = ( - delay_and_loss_frame.grid_slaves(row=0, column=1)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) - loss = ( - delay_and_loss_frame.grid_slaves(row=0, column=3)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) + button = tk.Button(frame, text="Choose WLAN members") + button.grid(row=0, column=2, padx=2, sticky="ew") - jitter_frame = config_frame.grid_slaves(row=3, column=0)[0] - jitter_val = ( - jitter_frame.grid_slaves(row=0, column=1)[0] - .grid_slaves(row=0, column=0)[0] - .get() - ) - - # set wireless node configuration here - wlanconfig_manager = self.canvas.grpc_manager.wlanconfig_management - wlanconfig_manager.set_custom_config( - node_id=self.canvas_node.core_id, - range=range_val, - bandwidth=bandwidth, - jitter=jitter_val, - delay=delay, - error=loss, - ) - self.top.destroy() - - def config_option(self): + def draw_apply_buttons(self): """ create node configuration options :return: nothing """ - f = tk.Frame(self.top, bg="#d9d9d9") - b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.wlan_config_apply) - b.grid(padx=2, pady=2) - b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.top.destroy) - b.grid(row=0, column=1, padx=2, pady=2) - f.grid(padx=4, pady=4) + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=2, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, padx=2, sticky="ew") + + def click_icon(self): + dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def click_apply(self): + """ + retrieve user's wlan configuration and store the new configuration values + + :return: nothing + """ + basic_range = self.range_var.get() + bandwidth = self.bandwidth_var.get() + delay = self.delay_var.get() + loss = self.loss_var.get() + jitter = self.jitter_var.get() + + # set wireless node configuration here + wlanconfig_manager = self.app.core.wlanconfig_management + wlanconfig_manager.set_custom_config( + node_id=self.canvas_node.core_id, + range=basic_range, + bandwidth=bandwidth, + jitter=jitter, + delay=delay, + error=loss, + ) + self.destroy() From 68a5468ffba89fefc138bd637a440497a274a0bd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 12:37:47 -0800 Subject: [PATCH 065/305] added servers dialog for distributed core, updated usages of ttk.Combobox to readonly --- coretk/coretk/coreclient.py | 10 ++ coretk/coretk/coremenubar.py | 2 +- coretk/coretk/dialogs/hooks.py | 4 +- coretk/coretk/dialogs/nodeconfig.py | 10 +- coretk/coretk/dialogs/servers.py | 169 ++++++++++++++++++++++++++++ coretk/coretk/menuaction.py | 10 +- 6 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/dialogs/servers.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ad912422..38e0ec2c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -56,6 +56,13 @@ class Edge: self.interface_2 = None +class CoreServer: + def __init__(self, name, address, port): + self.name = name + self.address = address + self.port = port + + class CoreClient: def __init__(self, app): """ @@ -68,6 +75,9 @@ class CoreClient: self.master = app.master self.interface_helper = None + # distributed server data + self.servers = {} + # data for managing the current session self.nodes = {} self.edges = {} diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 0751ee06..5add9790 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -616,7 +616,7 @@ class CoreMenubar(object): ) session_menu.add_command( label="Emulation servers...", - command=action.session_emulation_servers, + command=self.menu_action.session_servers, underline=0, ) session_menu.add_command( diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 95ca8af7..99647635 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -32,7 +32,9 @@ class HookDialog(Dialog): initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) self.name.set(f"{initial_state.lower()}_hook.sh") - combobox = ttk.Combobox(frame, textvariable=self.state, values=values) + combobox = ttk.Combobox( + frame, textvariable=self.state, values=values, state="readonly" + ) combobox.grid(row=0, column=2, sticky="ew") combobox.bind("<>", self.state_change) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index f1de2e50..b8548c51 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -43,10 +43,16 @@ class NodeConfigDialog(Dialog): entry = tk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") - combobox = ttk.Combobox(frame, textvariable=self.type, values=DEFAULTNODES) + combobox = ttk.Combobox( + frame, textvariable=self.type, values=DEFAULTNODES, state="readonly" + ) combobox.grid(row=0, column=1, padx=2, sticky="ew") - combobox = ttk.Combobox(frame, textvariable=self.server, values=["localhost"]) + servers = [""] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) combobox.current(0) combobox.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py new file mode 100644 index 00000000..dbc31267 --- /dev/null +++ b/coretk/coretk/dialogs/servers.py @@ -0,0 +1,169 @@ +import tkinter as tk + +from coretk.coreclient import CoreServer +from coretk.dialogs.dialog import Dialog + +DEFAULT_NAME = "example" +DEFAULT_ADDRESS = "127.0.0.1" +DEFAULT_PORT = 50051 + + +class ServersDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "CORE Servers", modal=True) + self.name = tk.StringVar(value=DEFAULT_NAME) + self.address = tk.StringVar(value=DEFAULT_ADDRESS) + self.port = tk.IntVar(value=DEFAULT_PORT) + self.servers = None + self.selected_index = None + self.selected = None + self.save_button = None + self.delete_button = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_servers() + self.draw_server_configuration() + self.draw_servers_buttons() + self.draw_apply_buttons() + + def draw_servers(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + self.servers = tk.Listbox( + frame, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + relief=tk.FLAT, + highlightthickness=0.5, + bd=0, + ) + self.servers.grid(row=0, column=0, sticky="nsew") + self.servers.bind("<>", self.handle_server_change) + + for server in self.app.core.servers: + self.servers.insert(tk.END, server) + + scrollbar.config(command=self.servers.yview) + + def draw_server_configuration(self): + label = tk.Label(self, text="Server Configuration") + label.grid(pady=2, sticky="ew") + + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + frame.columnconfigure(1, weight=1) + frame.columnconfigure(3, weight=1) + frame.columnconfigure(5, weight=1) + + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Address") + label.grid(row=0, column=2, sticky="w") + entry = tk.Entry(frame, textvariable=self.address) + entry.grid(row=0, column=3, sticky="ew") + + label = tk.Label(frame, text="Port") + label.grid(row=0, column=4, sticky="w") + entry = tk.Entry(frame, textvariable=self.port) + entry.grid(row=0, column=5, sticky="ew") + + def draw_servers_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button( + frame, text="Save Configuration", command=self.click_save_configuration + ) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_configuration(self): + pass + + def click_create(self): + name = self.name.get() + if name not in self.app.core.servers: + address = self.address.get() + port = self.port.get() + server = CoreServer(name, address, port) + self.app.core.servers[name] = server + self.servers.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected and name not in self.app.core.servers: + previous_name = self.selected + self.selected = name + server = self.app.core.servers.pop(previous_name) + server.name = name + server.address = self.address.get() + server.port = self.port.get() + self.app.core.servers[name] = server + self.servers.delete(self.selected_index) + self.servers.insert(self.selected_index, name) + self.servers.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.servers.delete(self.selected_index) + del self.app.core.servers[self.selected] + self.selected = None + self.selected_index = None + self.name.set(DEFAULT_NAME) + self.address.set(DEFAULT_ADDRESS) + self.port.set(DEFAULT_PORT) + self.servers.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_server_change(self, event): + selection = self.servers.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.servers.get(self.selected_index) + server = self.app.core.servers[self.selected] + self.name.set(server.name) + self.address.set(server.address) + self.port.set(server.port) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 7971553d..a60f7458 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,6 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog +from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -295,10 +296,6 @@ def session_reset_node_positions(): logging.debug("Click session reset node positions") -def session_emulation_servers(): - logging.debug("Click session emulation servers") - - def help_about(): logging.debug("Click help About") @@ -401,3 +398,8 @@ class MenuAction: logging.debug("Click session hooks") dialog = HooksDialog(self.app, self.app) dialog.show() + + def session_servers(self): + logging.debug("Click session emulation servers") + dialog = ServersDialog(self.app, self.app) + dialog.show() From 2d1f5edf79f15dbfe9f82b130577064712ee48f8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 13:10:42 -0800 Subject: [PATCH 066/305] started initial data for gui config file and added example server to it when initially running gui --- coretk/Pipfile | 1 + coretk/Pipfile.lock | 378 ++++++++++++++++++++++++++---------- coretk/coretk/app.py | 1 + coretk/coretk/appdirs.py | 19 +- coretk/coretk/coreclient.py | 5 + 5 files changed, 300 insertions(+), 104 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index da8c8df8..dfeb664c 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -15,3 +15,4 @@ pre-commit = "*" [packages] coretk = {editable = true,path = "."} core = {editable = true,path = "./../daemon"} +pyyaml = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index d3547813..07906a87 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2cb6b23bfb8b9bebb5ece1016de468cc57fe46cf4df08a61b1861aee4bed1028" + "sha256": "eb59f8233d6608de2d67743d8d8afe51c929f837cf6cf4d991ffc79ab5134910" }, "pipfile-spec": 6, "requires": {}, @@ -14,12 +14,65 @@ ] }, "default": { - "configparser": { + "bcrypt": { "hashes": [ - "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c", - "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df" + "sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89", + "sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42", + "sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294", + "sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161", + "sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752", + "sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31", + "sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5", + "sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c", + "sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0", + "sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de", + "sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e", + "sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052", + "sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09", + "sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105", + "sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133", + "sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1", + "sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7", + "sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc" ], - "version": "==4.0.2" + "version": "==3.1.7" + }, + "cffi": { + "hashes": [ + "sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", + "sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", + "sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", + "sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", + "sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", + "sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", + "sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", + "sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", + "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", + "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", + "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", + "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", + "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", + "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", + "sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", + "sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", + "sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", + "sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", + "sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", + "sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", + "sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", + "sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", + "sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", + "sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", + "sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", + "sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", + "sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", + "sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", + "sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", + "sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", + "sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", + "sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d" + ], + "version": "==1.13.2" }, "core": { "editable": true, @@ -29,54 +82,106 @@ "editable": true, "path": "." }, - "future": { + "cryptography": { "hashes": [ - "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" + "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", + "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", + "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", + "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", + "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", + "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", + "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", + "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", + "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", + "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", + "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", + "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", + "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", + "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", + "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", + "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", + "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", + "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", + "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", + "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", + "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8" ], - "version": "==0.17.1" + "version": "==2.8" + }, + "fabric": { + "hashes": [ + "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389", + "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6" + ], + "version": "==2.5.0" }, "grpcio": { "hashes": [ - "sha256:0337debec20fe385bcd49048d6917270efbc17a5119857466559b4db91f8995b", - "sha256:164f82a99e08797ea786283b66b45ebe76772d321577d1674ba6fe0200155892", - "sha256:172dfba8d9621048c2cbc1d1cf7a02244e9a9a8cff5bb79bb30bcb0c13c7fd31", - "sha256:18f4b536d8a9cfa15b3214e0bb628071def94160699e91798f0a954c3b2db88d", - "sha256:2283b56bda49b068b0f08d006fffc7dd46eae72322f1a5dec87fc9c218f1dc2d", - "sha256:26b33f488a955bf49262d2ce3423d3a8174108506d8f819e8150aca21bdd3b99", - "sha256:31cc9b6f70bdd0d9ff53df2d563ea1fb278601d5c625932d8a82d03b08ff3de0", - "sha256:37dd8684fbc2bc00766ae6784bcbd7f874bc96527636a341411db811d04ff650", - "sha256:424c01189ef51a808669f020368b01204e0f1fa0bf2adab7c8d0d13166f92e9e", - "sha256:431c099f20a1f1d97def98f87bb74fa752e8819c2bab23d79089353aed1acc9b", - "sha256:4c2f1d0b27bcef301e5d5c1da05ffd7d174f807f61889c006b8e708b16bc978e", - "sha256:59b8d738867b59c5daaff5df242b5f3f9c58b47862f603c6ee530964b897b69b", - "sha256:8d4f1ee2a67cf8f792d4fc9b8c7bb2148174e838d935f175653aec234752828b", - "sha256:97ab9e35b47bda0441332204960f95c1169c55ec8e989381bedd32bdb9f78b05", - "sha256:9cf93e185507bfdaa7ed45a90049bd3f1ed3f6357ad3772b31e993ff723cf67d", - "sha256:a5a81472c0ca6181492b9291c316ff60c6c94dd3f21c1e8c481f21923d899af0", - "sha256:aaa1feb0fdd094af6db0a16cbd446ed94285a50e320aede5971152d9ea022df8", - "sha256:b36bf4408f8400ee9ab13ff129e71f2e4c72ce2d8886b744aeab77ce50a55cf6", - "sha256:bb345f7e98b38a2c1ef33ff1145687234f78dfeedf308b41b3e41f4b42eba099", - "sha256:c13ae15695e0eb4ba2db920d6a197171d2398c675afa4f27460b6381d20a6884", - "sha256:c4a233a00cc5b64543e97733902151bc6738396931b3c166aad03a3aaadbd479", - "sha256:c521c5f8a95baabba69c68dd0f5e34f37c8adf1a9691f9884dba3eab4ebadc29", - "sha256:c772edd094fe3e54d6d54fdecb90c51cb8d07b55e9c1cda2d33e9615e33d07e8", - "sha256:cebfba6542855403b29e4bc95bbcd5ab444f21137b440f2fb7c7925ca0e55bfd", - "sha256:d7490e013c4bad3e8db804fc6483b47125dc8df0ebcfd6e419bd25df35025301", - "sha256:dfb6063619f297cbd22c67530d7465d98348b35d0424bbc1756b36c5ef9f99d4", - "sha256:e80a15b48a66f35c7c33db2a7df4034a533b362269d0e60e0036e23f14bac7b5", - "sha256:ea444fa1c1ec4f8d2ce965bb01e06148ef9ceb398fb2f627511d50f137eac35b", - "sha256:ec986cbf8837a49f9612cc1cfc2a8ccb54875cfce5355a121279de35124ea1db", - "sha256:fb641df6de8c4a55c784c24d334d53096954d9b30679d3ce5eb6a4d25c1020a3", - "sha256:fb88bd791c8efbcb36de12f0aa519ceec0b7806d3decff16e412e097d4725d44", - "sha256:ffa1be3d566a9cbd21a5f2d95fd9262ec6c337c499291bfeb51547b8de18942e" + "sha256:01cb705eafba1108e2a947ba0457da4f6a1e8142c729fc61702b5fdd11009eb1", + "sha256:0b5a79e29f167d3cd06faad6b15babbc2661066daaacf79373c3a8e67ca1fca1", + "sha256:1097a61a0e97b3580642e6e1460a3a1f1ba1815e2a70d6057173bcc495417076", + "sha256:13970e665a4ec4cec7d067d7d3504a0398c657d91d26c581144ad9044e429c9a", + "sha256:1557817cea6e0b87fad2a3e20da385170efb03a313db164e8078955add2dfa1b", + "sha256:1b0fb036a2f9dd93d9a35c57c26420eeb4b571fcb14b51cddf5b1e73ea5d882b", + "sha256:24d9e58d08e8cd545d8a3247a18654aff0e5e60414701696a8098fbb0d792b75", + "sha256:2c38b586163d2b91567fe5e6d9e7798f792012365adc838a64b66b22dce3f4d4", + "sha256:2df3ab4348507de60e1cbf75196403df1b9b4c4d4dc5bd11ac4eb63c46f691c7", + "sha256:32f70f7c90454ea568b868af2e96616743718d9233d23f62407e98caed81dfbf", + "sha256:3af2a49d576820045c9c880ff29a5a96d020fe31b35d248519bfc6ccb8be4eac", + "sha256:4ff7d63800a63db031ebac6a6f581ae84877c959401c24c28f2cc51fd36c47ad", + "sha256:502aaa8be56f0ae69cda66bc27e1fb5531ceaa27ca515ec3c34f6178b1297180", + "sha256:55358ce3ec283222e435f7dbc6603521438458f3c65f7c1cb33b8dabf56d70d8", + "sha256:5583b01c67f85fa64a2c3fb085e5517c88b9c1500a2cce12d473cd99d0ed2e49", + "sha256:58d9a5557d3eb7b734a3cea8b16c891099a522b3953a45a30bd4c034f75fc913", + "sha256:5911f042c4ab177757eec5bcb4e2e9a2e823d888835d24577321bf55f02938fa", + "sha256:5e16ea922f4e5017c04fd94e2639b1006e03097e9dd0cbb7a1c852af3ea8bf2e", + "sha256:656e19d3f1b9050ee01b457f92838a9679d7cf84c995f708780f44484048705e", + "sha256:6a1435449a82008c451c7e1a82a834387b9108f9a8d27910f86e7c482f5568e9", + "sha256:6ff02ca6cbed0ddb76e93ba0f8beb6a8c77d83a84eb7cafe2ae3399a8b9d69ea", + "sha256:76de68f60102f333bf4817f38e81ecbee68b850f5a5da9f355235e948ac40981", + "sha256:7c6d7ddd50fc6548ea1dfe09c62509c4f95b8b40082287747be05aa8feb15ee2", + "sha256:836b9d29507de729129e363276fe7c7d6a34c7961e0f155787025552b15d22c0", + "sha256:869242b2baf8a888a4fe0548f86abc47cb4b48bdfd76ae62d6456e939c202e65", + "sha256:8954b24bd08641d906ee50b2d638efc76df893fbd0913149b80484fd0eac40c9", + "sha256:8cdea65d1abb2e698420db8daf20c8d272fbd9d96a51b26a713c1c76f237d181", + "sha256:90161840b4fe9636f91ed0d3ea1e7e615e488cbea4e77594c889e5f3d7a776db", + "sha256:90fb6316b4d7d36700c40db4335902b78dcae13b5466673c21fd3b08a3c1b0c6", + "sha256:91b34f58db2611c9a93ecf751028f97fba1f06e65f49b38f272f6aa5d2977331", + "sha256:9474944a96a33eb8734fa8dc5805403d57973a3526204a5e1c1780d02e0572b6", + "sha256:9a36275db2a4774ac16c6822e7af816ee048071d5030b4c035fd53942b361935", + "sha256:9cbe26e2976b994c5f7c2d35a63354674d6ca0ce62f5b513f078bf63c1745229", + "sha256:9eaeabb3c0eecd6ddd0c16767fd12d130e2cebb8c2618f959a278b1ff336ddc3", + "sha256:a2bc7e10ebcf4be503ae427f9887e75c0cc24e88ce467a8e6eaca6bd2862406e", + "sha256:a5b42e6292ba51b8e67e09fc256963ba4ca9c04026de004d2fe59cc17e3c3776", + "sha256:bd6ec1233c86c0b9bb5d03ec30dbe3ffbfa53335790320d99a7ae9018c5450f2", + "sha256:bef57530816af54d66b1f4c70a8f851f320cb6f84d4b5a0b422b0e9811ea4e59", + "sha256:c146a63eaadc6589b732780061f3c94cd0574388d372baccbb3c1597a9ebdb7a", + "sha256:c2efd3b130dc639d615b6f58980e1bfd1b177ad821f30827afa5001aa30ddd48", + "sha256:c888b18f7392e6cc79a33a803e7ebd7890ac3318f571fca6b356526f35b53b12", + "sha256:ca30721fda297ae22f16bc37aa7ed244970ddfdcb98247570cdd26daaad4665e", + "sha256:cf5f5340dd682ab034baa52f423a0f91326489c262ac9617fa06309ec05880e9", + "sha256:d0726aa0d9b57c56985db5952e90fb1033a317074f2877db5307cdd6eede1564", + "sha256:df442945b2dd6f8ae0e20b403e0fd4548cd5c2aad69200047cc3251257b78f65", + "sha256:e08e758c31919d167c0867539bd3b2441629ef00aa595e3ea2b635273659f40a", + "sha256:e4864339deeeaefaad34dd3a432ee618a039fca28efb292949c855e00878203c", + "sha256:f4cd049cb94d9f517b1cab5668a3b345968beba093bc79a637e671000b3540ec" ], - "version": "==1.24.0" + "version": "==1.24.3" + }, + "invoke": { + "hashes": [ + "sha256:c52274d2e8a6d64ef0d61093e1983268ea1fc0cd13facb9448c4ef0c9a7ac7da", + "sha256:f4ec8a134c0122ea042c8912529f87652445d9f4de590b353d23f95bfa1f0efd", + "sha256:fc803a5c9052f15e63310aa81a43498d7c55542beb18564db88a9d75a176fa44" + ], + "version": "==1.3.0" }, "lxml": { "hashes": [ "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:1409b14bf83a7d729f92e2a7fbfe7ec929d4883ca071b06e95c539ceedb6497c", "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", @@ -87,11 +192,14 @@ "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:9277562f175d2334744ad297568677056861070399cec56ff06abbe2564d1232", "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:ae88588d687bd476be588010cbbe551e9c2872b816f2da8f01f6f1fda74e1ef0", "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c7fccd08b14aa437fe096c71c645c0f9be0655a9b1a4b7cffc77bcb23b3d61d2", "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", @@ -99,64 +207,126 @@ ], "version": "==4.4.1" }, + "paramiko": { + "hashes": [ + "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", + "sha256:f4b2edfa0d226b70bd4ca31ea7e389325990283da23465d572ed1f70a7583041" + ], + "version": "==2.6.0" + }, "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" + "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", + "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", + "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", + "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", + "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", + "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", + "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", + "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", + "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", + "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", + "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", + "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", + "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", + "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", + "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", + "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", + "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", + "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", + "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", + "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", + "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", + "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", + "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", + "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", + "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", + "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", + "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", + "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", + "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", + "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" ], - "version": "==6.1.0" + "version": "==6.2.1" }, "protobuf": { "hashes": [ - "sha256:26c0d756c7ad6823fccbc3b5f84c619b9cc7ac281496fe0a9d78e32023c45034", - "sha256:3200046e4d4f6c42ed66257dbe15e2e5dc76072c280e9b3d69dc8f3a4fa3fbbc", - "sha256:368f1bae6dd22d04fd2254d30cd301863408a96ff604422e3ddd8ab601f095a4", - "sha256:3902fa1920b4ef9f710797496b309efc5ccd0faeba44dc82ed6a711a244764a0", - "sha256:3a7a8925ba6481b9241cdb5d69cd0b0700f23efed6bb691dc9543faa4aa25d6f", - "sha256:4bc33d49f43c6e9916fb56b7377cb4478cbf25824b4d2bedfb8a4e3df31c12ca", - "sha256:568b434a36e31ed30d60d600b2227666ce150b8b5275948f50411481a4575d6d", - "sha256:5c393cd665d03ce6b29561edd6b0cc4bcb3fb8e2a7843e8f223d693f07f61b40", - "sha256:80072e9ba36c73cf89c01f669c7b123733fc2de1780b428082a850f53cc7865f", - "sha256:843f498e98ad1469ad54ecb4a7ccf48605a1c5d2bd26ae799c7a2cddab4a37ec", - "sha256:aa45443035651cbfae74c8deb53358ba660d8e7a5fbab3fc4beb33fb3e3ca4be", - "sha256:aaab817d9d038dd5f56a6fb2b2e8ae68caf1fd28cc6a963c755fa73268495c13", - "sha256:e6f68b9979dc8f75299293d682f67fecb72d78f98652da2eeb85c85edef1ca94", - "sha256:e7366cabddff3441d583fdc0176ab42eba4ee7090ef857d50c4dd59ad124003a", - "sha256:f0144ad97cd28bfdda0567b9278d25061ada5ad2b545b538cd3577697b32bda3", - "sha256:f655338491481f482042f19016647e50365ab41b75b486e0df56e0dcc425abf4" + "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", + "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", + "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", + "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", + "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", + "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", + "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", + "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", + "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", + "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", + "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", + "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", + "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", + "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", + "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", + "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" ], - "version": "==3.9.2" + "version": "==3.10.0" + }, + "pycparser": { + "hashes": [ + "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" + ], + "version": "==2.19" + }, + "pynacl": { + "hashes": [ + "sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255", + "sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c", + "sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e", + "sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae", + "sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621", + "sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56", + "sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39", + "sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310", + "sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1", + "sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5", + "sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a", + "sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786", + "sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b", + "sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b", + "sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f", + "sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20", + "sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415", + "sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715", + "sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92", + "sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1", + "sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0" + ], + "version": "==1.3.0" + }, + "pyyaml": { + "hashes": [ + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "index": "pypi", + "version": "==5.1.2" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" } }, "develop": { @@ -176,10 +346,10 @@ }, "attrs": { "hashes": [ - "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", - "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "version": "==19.1.0" + "version": "==19.3.0" }, "black": { "hashes": [ @@ -212,11 +382,11 @@ }, "flake8": { "hashes": [ - "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", - "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" ], "index": "pypi", - "version": "==3.7.8" + "version": "==3.7.9" }, "identify": { "hashes": [ @@ -230,6 +400,7 @@ "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" ], + "markers": "python_version < '3.8'", "version": "==0.23" }, "importlib-resources": { @@ -270,11 +441,11 @@ }, "pre-commit": { "hashes": [ - "sha256:1d3c0587bda7c4e537a46c27f2c84aa006acc18facf9970bf947df596ce91f3f", - "sha256:fa78ff96e8e9ac94c748388597693f18b041a181c94a4f039ad20f45287ba44a" + "sha256:9f152687127ec90642a2cc3e4d9e1e6240c4eb153615cb02aa1ad41d331cbb6e", + "sha256:c2e4810d2d3102d354947907514a78c5d30424d299dc0fe48f5aa049826e9b50" ], "index": "pypi", - "version": "==1.18.3" + "version": "==1.20.0" }, "pycodestyle": { "hashes": [ @@ -306,14 +477,15 @@ "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" ], + "index": "pypi", "version": "==5.1.2" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" ], - "version": "==1.12.0" + "version": "==1.13.0" }, "toml": { "hashes": [ @@ -324,10 +496,10 @@ }, "virtualenv": { "hashes": [ - "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", - "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" + "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", + "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" ], - "version": "==16.7.5" + "version": "==16.7.7" }, "zipp": { "hashes": [ diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 671504a8..9c9c0d2f 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -26,6 +26,7 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) + self.config = appdirs.read_config() self.core = CoreClient(self) self.setup_app() self.draw_menu() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 7dae4bf3..80710920 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -2,6 +2,8 @@ import logging import shutil from pathlib import Path +import yaml + # gui home paths HOME_PATH = Path.home().joinpath(".coretk") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") @@ -17,6 +19,11 @@ LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +class IndentDumper(yaml.Dumper): + def increase_indent(self, flow=False, indentless=False): + return super().increase_indent(flow, False) + + def check_directory(): if HOME_PATH.exists(): logging.info("~/.coretk exists") @@ -36,4 +43,14 @@ def check_directory(): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) with CONFIG_PATH.open("w") as f: - f.write("# gui config") + yaml.dump( + {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]}, + f, + Dumper=IndentDumper, + default_flow_style=False, + ) + + +def read_config(): + with CONFIG_PATH.open("r") as f: + return yaml.load(f, Loader=yaml.SafeLoader) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 38e0ec2c..ccb4c7ee 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -77,6 +77,11 @@ class CoreClient: # distributed server data self.servers = {} + for server_config in self.app.config["servers"]: + server = CoreServer( + server_config["name"], server_config["address"], server_config["port"] + ) + self.servers[server.name] = server # data for managing the current session self.nodes = {} From 20637da14020dd8b3903972f9b22ada9d84e3d46 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 5 Nov 2019 14:25:25 -0800 Subject: [PATCH 067/305] change to appropriate toolbar when join session, emane config, emane model config --- coretk/coretk/configutils.py | 7 +- coretk/coretk/coreclient.py | 19 ++ coretk/coretk/coretoolbar.py | 12 -- coretk/coretk/coretoolbarhelp.py | 13 +- coretk/coretk/dialogs/emaneconfig.py | 268 ++++++++++++++++++--------- coretk/coretk/graph.py | 6 - daemon/data/logging.conf | 2 +- 7 files changed, 217 insertions(+), 110 deletions(-) diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py index bb3e793e..267feb73 100644 --- a/coretk/coretk/configutils.py +++ b/coretk/coretk/configutils.py @@ -7,6 +7,7 @@ from tkinter import ttk class ConfigType(enum.Enum): STRING = 10 BOOL = 11 + EMANECONFIG = 7 def create_config(master, config, padx=2, pady=2): @@ -52,8 +53,13 @@ def create_config(master, config, padx=2, pady=2): else: value.set("Off") elif config_type == ConfigType.STRING: + value.set(option.value) entry = tk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif config_type == ConfigType.EMANECONFIG: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value, bg="white") + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", config_type) values[key] = value @@ -68,7 +74,6 @@ def create_config(master, config, padx=2, pady=2): canvas.bind( "", lambda event: canvas.itemconfig(frame_id, width=event.width) ) - return values diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index a6d4f19c..67385809 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -80,6 +80,7 @@ class CoreClient: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() + self.emane_config = None def handle_events(self, event): logging.info("event: %s", event) @@ -110,11 +111,15 @@ class CoreClient: self.nodes.clear() self.edges.clear() self.hooks.clear() + self.wlanconfig_management.configurations.clear() + self.mobilityconfig_management.configurations.clear() + self.emane_config = None # get session data response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) session = response.session + session_state = session.state self.client.events(self.session_id, self.handle_events) # get hooks @@ -140,6 +145,11 @@ class CoreClient: config = {x: node_config[x].value for x in node_config} self.mobilityconfig_management.configurations[node_id] = config + # get emane config + response = self.client.get_emane_config(self.session_id) + logging.info("emane config: %s", response) + self.emane_config = response.config + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: @@ -154,6 +164,14 @@ class CoreClient: # draw session self.app.canvas.canvas_reset_and_redraw(session) + # draw tool bar appropritate with session state + if session_state == core_pb2.SessionState.RUNTIME: + self.app.core_editbar.destroy_children_widgets() + self.app.core_editbar.create_runtime_toolbar() + else: + self.app.core_editbar.destroy_children_widgets() + self.app.core_editbar.create_toolbar() + def create_new_session(self): """ Create a new session @@ -298,6 +316,7 @@ class CoreClient: links, hooks=list(self.hooks.values()), wlan_configs=wlan_configs, + emane_config=emane_config, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index f9d0a461..41c18eed 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -7,18 +7,6 @@ from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from enum import Enum - - -# class SessionStateEnum(Enum): -# NONE = "none" -# DEFINITION = "definition" -# CONFIGURATION = "configuration" -# RUNTIME = "runtime" -# DATACOLLECT = "datacollect" -# SHUTDOWN = "shutdown" -# INSTANTIATION = "instantiation" - class CoreToolbar(object): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index aa53fbbc..d9c7e0bf 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -3,6 +3,8 @@ CoreToolbar help to draw on canvas, and make grpc client call """ from core.api.grpc.client import core_pb2 +# from coretk import configutils + class CoreToolbarHelp: def __init__(self, app): @@ -85,6 +87,15 @@ class CoreToolbarHelp: links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() mobility_configs = self.get_mobility_configuration_list() + + # get emane config + pb_emane_config = self.app.core.emane_config + emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + self.app.core.start_session( - nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs + nodes, + links, + wlan_configs=wlan_configs, + mobility_configs=mobility_configs, + emane_config=emane_config, ) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 256ff69d..d6436deb 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -5,11 +5,15 @@ emane configuration import logging import tkinter as tk import webbrowser +from tkinter import ttk +from coretk import configutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.mobilityconfig import MobilityConfiguration from coretk.images import ImageEnum, Images +PAD_X = 2 +PAD_Y = 2 + class EmaneConfiguration(Dialog): def __init__(self, master, app, canvas_node): @@ -20,6 +24,13 @@ class EmaneConfiguration(Dialog): self.radiovar.set(1) self.columnconfigure(0, weight=1) + # list(string) of emane models + self.emane_models = None + + self.emane_dialog = Dialog(self, app, "emane configuration", modal=False) + self.emane_model_dialog = None + self.emane_model_combobox = None + # draw self.node_name_and_image() self.emane_configuration() @@ -27,8 +38,10 @@ class EmaneConfiguration(Dialog): self.emane_options() self.draw_apply_and_cancel() - def browse_emane_wiki(self): - webbrowser.open_new("https://github.com/adjacentlink/emane/wiki") + self.values = None + self.options = app.core.emane_config + self.model_options = None + self.model_values = None def create_text_variable(self, val): """ @@ -52,20 +65,109 @@ class EmaneConfiguration(Dialog): e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") e.grid(row=0, column=1, padx=2, pady=2) - om = tk.OptionMenu( - f, - self.create_text_variable("None"), - "(none)", - "core1", - "core2", - command=self.choose_core, - ) - om.grid(row=0, column=2, padx=2, pady=2) + cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") + cbb.current(0) + cbb.grid(row=0, column=2, padx=2, pady=2) b = tk.Button(f, image=self.canvas_node.image) b.grid(row=0, column=3, padx=2, pady=2) - f.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + f.grid(row=0, column=0, sticky="nsew") + + def save_emane_option(self): + configutils.parse_config(self.options, self.values) + self.emane_dialog.destroy() + + def draw_emane_options(self): + if not self.emane_dialog.winfo_exists(): + self.emane_dialog = Dialog( + self, self.app, "emane configuration", modal=False + ) + b1 = tk.Button(self.emane_dialog, text="Appy", command=self.save_emane_option) + b2 = tk.Button( + self.emane_dialog, text="Cancel", command=self.emane_dialog.destroy + ) + + if self.options is None: + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_config(session_id) + logging.info("emane config: %s", response) + self.options = response.config + self.values = configutils.create_config( + self.emane_dialog, self.options, PAD_X, PAD_Y + ) + b1.grid(row=1, column=0) + b2.grid(row=1, column=1) + self.emane_dialog.show() + + def save_emane_model_options(self): + """ + configure the node's emane model on the fly + + :return: nothing + """ + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] + + # parse configuration + configutils.parse_config(self.model_options, self.model_values) + config = {x: self.model_options[x].value for x in self.model_options} + + # add string emane_ infront for grpc call + response = self.app.core.client.set_emane_model_config( + self.app.core.session_id, + self.canvas_node.core_id, + "emane_" + model_name, + config, + ) + logging.info( + "emaneconfig.py config emane model (%s), result: %s", + self.canvas_node.core_id, + response, + ) + + self.emane_model_dialog.destroy() + + def draw_model_options(self): + """ + draw emane model configuration + + :return: nothing + """ + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] + + # create the dialog and the necessry widget + if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): + self.emane_model_dialog = Dialog( + self, self.app, model_name + " configuration", modal=False + ) + + b1 = tk.Button( + self.emane_model_dialog, text="Apply", command=self.save_emane_model_options + ) + b2 = tk.Button( + self.emane_model_dialog, + text="Cancel", + command=self.emane_model_dialog.destroy, + ) + + # query for configurations + session_id = self.app.core.session_id + # add string emane_ before model name for grpc call + response = self.app.core.client.get_emane_model_config( + session_id, self.canvas_node.core_id, "emane_" + model_name + ) + logging.info("emane model config %s", response) + + self.model_options = response.config + self.model_values = configutils.create_config( + self.emane_model_dialog, self.model_options, PAD_X, PAD_Y + ) + + b1.grid(row=1, column=0, sticky="nsew") + b2.grid(row=1, column=1, sticky="nsew") + self.emane_model_dialog.show() def draw_option_buttons(self, parent): f = tk.Frame(parent, bg="#d9d9d9") @@ -73,60 +175,53 @@ class EmaneConfiguration(Dialog): f.columnconfigure(1, weight=1) b = tk.Button( f, - text="model options", + text=self.emane_models[0] + " options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, bg="#d9d9d9", - state=tk.DISABLED, + command=self.draw_model_options, ) - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") b = tk.Button( f, text="EMANE options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, bg="#d9d9d9", + command=self.draw_emane_options, ) - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - f.grid(row=4, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + f.grid(row=4, column=0, sticky="nsew") - def radiobutton_text(self, val): + def combobox_select(self, event): """ - get appropriate text based on radio value + update emane model options button - :return: the text value to configure button + :param event: + :return: nothing """ - if val == 1: - return "none" - elif val == 2: - return "rfpipe options" - elif val == 3: - return "ieee80211abg options" - elif val == 4: - return "commeffect options" - elif val == 5: - return "bypass options" - elif val == 6: - return "tdma options" - else: - logging.debug("emaneconfig.py invalid radio value") - return "" + # get model name + model_name = self.emane_models[self.emane_model_combobox.current()] - def click_radio_button(self): - print(type(self.radiovar.get())) + # get the button and configure button text config_frame = self.grid_slaves(row=2, column=0)[0] option_button_frame = config_frame.grid_slaves(row=4, column=0)[0] b = option_button_frame.grid_slaves(row=0, column=0)[0] - text = self.radiobutton_text(self.radiovar.get()) - if text == "none": - state = tk.DISABLED - else: - state = tk.NORMAL - b.config(text=text, state=state) - # b.config(text=) + b.config(text=model_name + " options") def draw_emane_models(self, parent): - models = ["none", "rfpipe", "ieee80211abg", "commeffect", "bypass", "tdma"] + """ + create a combobox that has all the known emane models + + :param parent: parent + :return: nothing + """ + # query for all the known model names + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_models(session_id) + self.emane_models = [x.split("_")[1] for x in response.models] + + # create combo box and its binding f = tk.Frame( parent, bg="#d9d9d9", @@ -135,20 +230,12 @@ class EmaneConfiguration(Dialog): highlightthickness=0.5, bd=0, ) - value = 1 - for m in models: - b = tk.Radiobutton( - f, - text=m, - variable=self.radiovar, - indicatoron=True, - value=value, - bg="#d9d9d9", - highlightthickness=0, - command=self.click_radio_button, - ) - b.grid(sticky=tk.W) - value = value + 1 + self.emane_model_combobox = ttk.Combobox( + f, values=self.emane_models, state="readonly" + ) + self.emane_model_combobox.grid() + self.emane_model_combobox.current(0) + self.emane_model_combobox.bind("<>", self.combobox_select) f.grid(row=3, column=0, sticky=tk.W + tk.E) def draw_text_label_and_entry(self, parent, label_text, entry_text): @@ -164,11 +251,19 @@ class EmaneConfiguration(Dialog): lbl.grid(row=0, column=0) e = tk.Entry(f, textvariable=var, bg="white") e.grid(row=0, column=1) - f.grid(stick=tk.W) + f.grid(stick=tk.W, padx=2, pady=2) def emane_configuration(self): + """ + draw the main frame for emane configuration + + :return: nothing + """ + # draw label lbl = tk.Label(self, text="Emane") lbl.grid(row=1, column=0) + + # main frame that has emane wiki, a short description, emane models and the configure buttons f = tk.Frame( self, bg="#d9d9d9", @@ -187,7 +282,9 @@ class EmaneConfiguration(Dialog): compound=tk.RIGHT, relief=tk.RAISED, bg="#d9d9d9", - command=self.browse_emane_wiki, + command=lambda: webbrowser.open_new( + "https://github.com/adjacentlink/emane/wiki" + ), ) b.grid(row=0, column=0, sticky=tk.W) @@ -197,54 +294,47 @@ class EmaneConfiguration(Dialog): "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", bg="#d9d9d9", ) - lbl.grid(row=1, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + lbl.grid(row=1, column=0, sticky="nsew") lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") lbl.grid(row=2, column=0, sticky=tk.W) - self.draw_option_buttons(f) + self.draw_emane_models(f) - f.grid(row=2, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + self.draw_option_buttons(f) + + f.grid(row=2, column=0, sticky="nsew") def draw_ip_subnets(self): self.draw_text_label_and_entry(self, "IPv4 subnet", "") self.draw_text_label_and_entry(self, "IPv6 subnet", "") - def click_ns2_mobility_script(self): - dialog = MobilityConfiguration(self, self.app, self.canvas_node) - dialog.show() - def emane_options(self): """ create wireless node options :return: """ - f = tk.Frame(self) + f = tk.Frame(self, bg="#d9d9d9") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - f.columnconfigure(2, weight=1) - b = tk.Button( - f, - text="ns-2 mobility script...", - command=lambda: self.click_ns2_mobility_script(), - ) - # b.pack(side=tk.LEFT, padx=1) - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - b = tk.Button(f, text="Link to all routers") - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - # b.pack(side=tk.LEFT, padx=1) - b = tk.Button(f, text="Choose WLAN members") - b.grid(row=0, column=2, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) - # b.pack(side=tk.LEFT, padx=1) - f.grid(row=5, column=0, sticky=tk.N + tk.S + tk.E + tk.W) + b = tk.Button(f, text="Link to all routers", bg="#d9d9d9") + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") + b = tk.Button(f, text="Choose WLAN members", bg="#d9d9d9") + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + f.grid(row=5, column=0, sticky="nsew") + + def apply(self): + # save emane configuration + self.app.core.emane_config = self.options + self.destroy() def draw_apply_and_cancel(self): f = tk.Frame(self, bg="#d9d9d9") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Apply", bg="#d9d9d9") - b.grid(row=0, column=0, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.apply) + b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.destroy) - b.grid(row=0, column=1, padx=10, pady=2, sticky=tk.N + tk.S + tk.E + tk.W) + b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(sticky=tk.N + tk.S + tk.E + tk.W) + f.grid(sticky="nsew") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 2bf02854..448a6f1d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -223,12 +223,6 @@ class CanvasGraph(tk.Canvas): for i in self.find_withtag("node"): self.lift(i) - # def delete_components(self): - # tags = ["node", "edge", "linkinfo", "nodename"] - # for i in tags: - # for id in self.find_withtag(i): - # self.delete(id) - def canvas_xy(self, event): """ Convert window coordinate to canvas coordinate diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 7f3d496f..46de6e92 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "INFO", + "level": "DEBUG", "handlers": ["console"] } } From 6405af24293ff2eca18fae18b798f15da5400297 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 5 Nov 2019 15:15:22 -0800 Subject: [PATCH 068/305] more work on coretk --- coretk/coretk/coretoolbarhelp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index d9c7e0bf..915d3b80 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -3,8 +3,6 @@ CoreToolbar help to draw on canvas, and make grpc client call """ from core.api.grpc.client import core_pb2 -# from coretk import configutils - class CoreToolbarHelp: def __init__(self, app): From 1c36c5e2915542821227fb6f0f53b315bc125f91 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:16:46 -0800 Subject: [PATCH 069/305] startred custom node dialog, update node services dialog to use service data retrieved from grpc --- coretk/coretk/appdirs.py | 14 ++--- coretk/coretk/coreclient.py | 10 +++- coretk/coretk/coretoolbar.py | 17 +----- coretk/coretk/dialogs/customnodes.py | 82 ++++++++++++++++++++++++++++ coretk/coretk/dialogs/nodeservice.py | 71 +++--------------------- 5 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 coretk/coretk/dialogs/customnodes.py diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 80710920..51a78f76 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -42,15 +42,15 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) - with CONFIG_PATH.open("w") as f: - yaml.dump( - {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]}, - f, - Dumper=IndentDumper, - default_flow_style=False, - ) + config = {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]} + save_config(config) def read_config(): with CONFIG_PATH.open("r") as f: return yaml.load(f, Loader=yaml.SafeLoader) + + +def save_config(config): + with CONFIG_PATH.open("w") as f: + yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ccb4c7ee..a5ccc14a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -74,6 +74,7 @@ class CoreClient: self.app = app self.master = app.master self.interface_helper = None + self.services = {} # distributed server data self.servers = {} @@ -188,9 +189,16 @@ class CoreClient: :return: existing sessions """ self.client.connect() - response = self.client.get_sessions() + + # get service information + response = self.client.get_services() + for service in response.services: + group_services = self.services.setdefault(service.group, []) + group_services.append(service) # if there are no sessions, create a new session, else join a session + response = self.client.get_sessions() + logging.info("current sessions: %s", response) sessions = response.sessions if len(sessions) == 0: self.create_new_session() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index 5d7c05ae..f73cc384 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,24 +1,12 @@ import logging import tkinter as tk -# from core.api.grpc import core_pb2 from coretk.coretoolbarhelp import CoreToolbarHelp +from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -# from enum import Enum - - -# class SessionStateEnum(Enum): -# NONE = "none" -# DEFINITION = "definition" -# CONFIGURATION = "configuration" -# RUNTIME = "runtime" -# DATACOLLECT = "datacollect" -# SHUTDOWN = "shutdown" -# INSTANTIATION = "instantiation" - class CoreToolbar(object): """ @@ -245,11 +233,12 @@ class CoreToolbar(object): self.canvas.draw_node_image = Images.get(ImageEnum.OVS) self.canvas.draw_node_name = "OVS" - # TODO what graph node is this def pick_editnode(self, main_button): self.network_layer_option_menu.destroy() main_button.configure(image=Images.get(ImageEnum.EDITNODE)) logging.debug("Pick editnode option") + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() def draw_network_layer_options(self, network_layer_button): """ diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py new file mode 100644 index 00000000..b38196fe --- /dev/null +++ b/coretk/coretk/dialogs/customnodes.py @@ -0,0 +1,82 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class CustomNodesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Custom Nodes", modal=True) + self.save_button = None + self.delete_button = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_node_config() + self.draw_node_buttons() + self.draw_buttons() + + def draw_node_config(self): + frame = tk.Frame(self) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + listbox = tk.Listbox(frame) + listbox.grid( + row=0, + column=0, + selectmode=tk.SINGLE, + yscrollcommand=scrollbar.set, + sticky="nsew", + ) + + scrollbar.config(command=listbox.yview) + + frame = tk.Frame(frame) + frame.grid(row=0, column=2, sticky="nsew") + frame.columnconfigure(0, weight=1) + + def draw_node_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Save Configuration") + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_create(self): + pass + + def click_save(self): + pass + + def click_delete(self): + pass diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 82d36cca..6d92b98e 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -6,58 +6,6 @@ from tkinter import messagebox from coretk.dialogs.dialog import Dialog -CORE_DEFAULT_GROUPS = ["EMANE", "FRR", "ProtoSvc", "Quagga", "Security", "Utility"] -DEFAULT_GROUP_RADIO_VALUE = { - "EMANE": 1, - "FRR": 2, - "ProtoSvc": 3, - "Quagga": 4, - "Security": 5, - "Utility": 6, -} -DEFAULT_GROUP_SERVICES = { - "EMANE": ["transportd"], - "FRR": [ - "FRRBable", - "FRRBGP", - "FRROSPFv2", - "FRROSPFv3", - "FRRpimd", - "FRRRIP", - "FRRRIPNG", - "FRRzebra", - ], - "ProtoSvc": ["MGEN_Sink", "MgenActor", "SMF"], - "Quagga": [ - "Babel", - "BGP", - "OSPFv2", - "OSPFv3", - "OSPFv3MDR", - "RIP", - "RIPNG", - "Xpimd", - "zebra", - ], - "Security": ["Firewall", "IPsec", "NAT", "VPNClient", "VPNServer"], - "Utility": [ - "atd", - "DefaultMulticastRoute", - "DefaultRoute", - "DHCP", - "DHCPClient", - "FTP", - "HTTP", - "IPForward ", - "pcap", - "radvd", - "SSH", - "StaticRoute", - "ucarp", - "UserDefined", - ], -} - class NodeServicesDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -66,6 +14,7 @@ class NodeServicesDialog(Dialog): self.core_groups = [] self.service_to_config = None self.config_frame = None + self.services_list = None self.draw() def draw(self): @@ -110,7 +59,7 @@ class NodeServicesDialog(Dialog): listbox.grid(row=1, column=0, sticky="nsew") listbox.bind("<>", self.handle_group_change) - for group in CORE_DEFAULT_GROUPS: + for group in sorted(self.app.core.services): listbox.insert(tk.END, group) scrollbar.config(command=listbox.yview) @@ -127,7 +76,7 @@ class NodeServicesDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") - listbox = tk.Listbox( + self.services_list = tk.Listbox( frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set, @@ -135,10 +84,10 @@ class NodeServicesDialog(Dialog): highlightthickness=0.5, bd=0, ) - listbox.grid(row=1, column=0, sticky="nsew") - listbox.bind("<>", self.handle_service_change) + self.services_list.grid(row=1, column=0, sticky="nsew") + self.services_list.bind("<>", self.handle_service_change) - scrollbar.config(command=listbox.yview) + scrollbar.config(command=self.services_list.yview) def draw_current_services(self): frame = tk.Frame(self.config_frame) @@ -188,11 +137,9 @@ class NodeServicesDialog(Dialog): self.display_group_services(s) def display_group_services(self, group_name): - group_services_frame = self.config_frame.grid_slaves(row=0, column=1)[0] - listbox = group_services_frame.grid_slaves(row=1, column=0)[0] - listbox.delete(0, tk.END) - for s in DEFAULT_GROUP_SERVICES[group_name]: - listbox.insert(tk.END, s) + self.services_list.delete(0, tk.END) + for service in sorted(self.app.core.services[group_name], key=lambda x: x.name): + self.services_list.insert(tk.END, service.name) def handle_service_change(self, event): print("select group service") From cb72a70d850b9bb8aa06ee070ee56867b406fc21 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:23:47 -0800 Subject: [PATCH 070/305] change to fix coretk action --- .github/workflows/coretk-checks.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coretk-checks.yml b/.github/workflows/coretk-checks.yml index 324babfd..e99ed214 100644 --- a/.github/workflows/coretk-checks.yml +++ b/.github/workflows/coretk-checks.yml @@ -15,7 +15,11 @@ jobs: run: | python -m pip install --upgrade pip pip install pipenv - cd coretk + cd daemon + cp setup.py.in setup.py + cp core/constants.py.in core/constants.py + sed -i 's/True/False/g' core/constants.py + cd ../coretk pipenv install --dev - name: isort run: | From 73b147b1520a2eed0f73d3411575932af1129299 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 16:56:47 -0800 Subject: [PATCH 071/305] updated tk servers dialog to save config --- coretk/coretk/dialogs/servers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index dbc31267..9ab6a694 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,5 +1,6 @@ import tkinter as tk +from coretk import appdirs from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -113,7 +114,14 @@ class ServersDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_save_configuration(self): - pass + servers = [] + for name in sorted(self.app.core.servers): + server = self.app.core.servers[name] + servers.append( + {"name": server.name, "address": server.address, "port": server.port} + ) + self.app.config["servers"] = servers + appdirs.save_config(self.app.config) def click_create(self): name = self.name.get() From 275a03b9e72791473d97a44a75ea45456d39a44a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 17:32:48 -0800 Subject: [PATCH 072/305] added observer widget dialog, still needs to be hooked to where widgets will be kept --- coretk/coretk/coremenubar.py | 4 +- coretk/coretk/dialogs/observerwidgets.py | 148 +++++++++++++++++++++++ coretk/coretk/dialogs/servers.py | 9 +- coretk/coretk/menuaction.py | 5 + 4 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/dialogs/observerwidgets.py diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/coremenubar.py index 5add9790..f720b093 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/coremenubar.py @@ -543,7 +543,9 @@ class CoreMenubar(object): observer_widget_menu.add_command( label="PIM neighbors", command=action.sub_menu_items ) - observer_widget_menu.add_command(label="Edit...", command=action.sub_menu_items) + observer_widget_menu.add_command( + label="Edit...", command=self.menu_action.edit_observer_widgets + ) widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) diff --git a/coretk/coretk/dialogs/observerwidgets.py b/coretk/coretk/dialogs/observerwidgets.py new file mode 100644 index 00000000..c1c4d170 --- /dev/null +++ b/coretk/coretk/dialogs/observerwidgets.py @@ -0,0 +1,148 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog + + +class Widget: + def __init__(self, name, command): + self.name = name + self.command = command + + +class ObserverWidgetsDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Observer Widgets", modal=True) + self.config_widgets = {} + self.widgets = None + self.save_button = None + self.delete_button = None + self.selected = None + self.selected_index = None + self.name = tk.StringVar() + self.command = tk.StringVar() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_widgets() + self.draw_widget_fields() + self.draw_widget_buttons() + self.draw_apply_buttons() + + def draw_widgets(self): + frame = tk.Frame(self) + frame.grid(sticky="nsew") + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky="ns") + + self.widgets = tk.Listbox( + frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set + ) + self.widgets.grid(row=0, column=0, sticky="nsew") + self.widgets.bind("<>", self.handle_widget_change) + + scrollbar.config(command=self.widgets.yview) + + def draw_widget_fields(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + + label = tk.Label(frame, text="Name") + label.grid(row=0, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(row=0, column=1, sticky="ew") + + label = tk.Label(frame, text="Command") + label.grid(row=1, column=0, sticky="w") + entry = tk.Entry(frame, textvariable=self.command) + entry.grid(row=1, column=1, sticky="ew") + + def draw_widget_buttons(self): + frame = tk.Frame(self) + frame.grid(pady=2, sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) + + button = tk.Button(frame, text="Create", command=self.click_create) + button.grid(row=0, column=0, sticky="ew") + + self.save_button = tk.Button( + frame, text="Save", state=tk.DISABLED, command=self.click_save + ) + self.save_button.grid(row=0, column=1, sticky="ew") + + self.delete_button = tk.Button( + frame, text="Delete", state=tk.DISABLED, command=self.click_delete + ) + self.delete_button.grid(row=0, column=2, sticky="ew") + + def draw_apply_buttons(self): + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = tk.Button( + frame, text="Save Configuration", command=self.click_save_configuration + ) + button.grid(row=0, column=0, sticky="ew") + + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save_configuration(self): + pass + + def click_create(self): + name = self.name.get() + if name not in self.config_widgets: + command = self.command.get() + widget = Widget(name, command) + self.config_widgets[name] = widget + self.widgets.insert(tk.END, name) + + def click_save(self): + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + widget = self.config_widgets.pop(previous_name) + widget.name = name + widget.command = self.command.get() + self.config_widgets[name] = widget + self.widgets.delete(self.selected_index) + self.widgets.insert(self.selected_index, name) + self.widgets.selection_set(self.selected_index) + + def click_delete(self): + if self.selected: + self.widgets.delete(self.selected_index) + del self.config_widgets[self.selected] + self.selected = None + self.selected_index = None + self.name.set("") + self.command.set("") + self.widgets.selection_clear(0, tk.END) + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) + + def handle_widget_change(self, event): + selection = self.widgets.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.widgets.get(self.selected_index) + widget = self.config_widgets[self.selected] + self.name.set(widget.name) + self.command.set(widget.command) + self.save_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected_index = None + self.selected = None + self.save_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 9ab6a694..3ff95cde 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -40,12 +40,7 @@ class ServersDialog(Dialog): scrollbar.grid(row=0, column=1, sticky="ns") self.servers = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, + frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set ) self.servers.grid(row=0, column=0, sticky="nsew") self.servers.bind("<>", self.handle_server_change) @@ -134,7 +129,7 @@ class ServersDialog(Dialog): def click_save(self): name = self.name.get() - if self.selected and name not in self.app.core.servers: + if self.selected: previous_name = self.selected self.selected = name server = self.app.core.servers.pop(previous_name) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index a60f7458..391cf765 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,6 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog +from coretk.dialogs.observerwidgets import ObserverWidgetsDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -403,3 +404,7 @@ class MenuAction: logging.debug("Click session emulation servers") dialog = ServersDialog(self.app, self.app) dialog.show() + + def edit_observer_widgets(self): + dialog = ObserverWidgetsDialog(self.app, self.app) + dialog.show() From 2b3e071045f4aa3c4ad1199210c87ec27c7bd915 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 5 Nov 2019 22:44:50 -0800 Subject: [PATCH 073/305] updated nodeicondialog to just icondialog, added custom widgets for convenience, listboxscroll and checkboxlist --- coretk/coretk/dialogs/customnodes.py | 94 +++++++++++++++++++++++++--- coretk/coretk/dialogs/nodeconfig.py | 6 +- coretk/coretk/dialogs/nodeicon.py | 14 ++--- coretk/coretk/dialogs/wlanconfig.py | 6 +- coretk/coretk/widgets.py | 58 +++++++++++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 coretk/coretk/widgets.py diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index b38196fe..841b2dcf 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,6 +1,68 @@ import tkinter as tk from coretk.dialogs.dialog import Dialog +from coretk.dialogs.nodeicon import IconDialog +from coretk.widgets import CheckboxList, ListboxScroll + + +class ServicesSelectDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Node Services", modal=True) + self.groups = None + self.services = None + self.current = None + self.current_services = set() + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + frame = tk.Frame(self) + frame.grid(stick="nsew") + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + self.groups = ListboxScroll(frame, text="Groups") + self.groups.grid(row=0, column=0, sticky="nsew") + for group in sorted(self.app.core.services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + + self.services = CheckboxList( + frame, text="Services", clicked=self.service_clicked + ) + self.services.grid(row=0, column=1, sticky="nsew") + + self.current = ListboxScroll(frame, text="Selected") + self.current.grid(row=0, column=2, sticky="nsew") + + frame = tk.Frame(self) + frame.grid(stick="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save") + button.grid(row=0, column=0, sticky="ew") + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def handle_group_change(self, event): + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for service in sorted(self.app.core.services[group], key=lambda x: x.name): + self.services.add(service.name) + + def service_clicked(self, name, var): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) class CustomNodesDialog(Dialog): @@ -8,6 +70,9 @@ class CustomNodesDialog(Dialog): super().__init__(master, app, "Custom Nodes", modal=True) self.save_button = None self.delete_button = None + self.name = tk.StringVar() + self.image_button = None + self.image = None self.draw() def draw(self): @@ -26,20 +91,20 @@ class CustomNodesDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") - listbox = tk.Listbox(frame) - listbox.grid( - row=0, - column=0, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - sticky="nsew", - ) + listbox = tk.Listbox(frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set) + listbox.grid(row=0, column=0, sticky="nsew") scrollbar.config(command=listbox.yview) frame = tk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=self.name) + entry.grid(sticky="ew") + self.image_button = tk.Button(frame, text="Icon", command=self.click_icon) + self.image_button.grid(sticky="ew") + button = tk.Button(frame, text="Services", command=self.click_services) + button.grid(sticky="ew") def draw_node_buttons(self): frame = tk.Frame(self) @@ -66,12 +131,23 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save Configuration") + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_icon(self): + dialog = IconDialog(self, self.app, self.name.get(), self.image) + dialog.show() + if dialog.image: + self.image = dialog.image + self.image_button.config(image=self.image) + + def click_services(self): + dialog = ServicesSelectDialog(self, self.app) + dialog.show() + def click_create(self): pass diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index b8548c51..bc03dc51 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeicon import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] @@ -91,7 +91,9 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog = IconDialog( + self, self.app, self.canvas_node.name, self.canvas_node.image + ) dialog.show() if dialog.image: self.image = dialog.image diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/nodeicon.py index d82c0756..4d26e29f 100644 --- a/coretk/coretk/dialogs/nodeicon.py +++ b/coretk/coretk/dialogs/nodeicon.py @@ -6,18 +6,12 @@ from coretk.dialogs.dialog import Dialog from coretk.images import Images -class NodeIconDialog(Dialog): - def __init__(self, master, app, canvas_node): - """ - create an instance of ImageModification - :param master: dialog master - :param coretk.app.Application: main app - :param coretk.graph.CanvasNode canvas_node: node object - """ - super().__init__(master, app, f"{canvas_node.name} Icon", modal=True) +class IconDialog(Dialog): + def __init__(self, master, app, name, image): + super().__init__(master, app, f"{name} Icon", modal=True) self.file_path = tk.StringVar() self.image_label = None - self.image = canvas_node.image + self.image = image self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index ad10bf40..d57c8935 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import NodeIconDialog +from coretk.dialogs.nodeicon import IconDialog class WlanConfigDialog(Dialog): @@ -167,7 +167,9 @@ class WlanConfigDialog(Dialog): button.grid(row=0, column=1, padx=2, sticky="ew") def click_icon(self): - dialog = NodeIconDialog(self, self.app, self.canvas_node) + dialog = IconDialog( + self, self.app, self.canvas_node.name, self.canvas_node.image + ) dialog.show() if dialog.image: self.image = dialog.image diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py new file mode 100644 index 00000000..80347e73 --- /dev/null +++ b/coretk/coretk/widgets.py @@ -0,0 +1,58 @@ +import tkinter as tk +from functools import partial + + +class ListboxScroll(tk.LabelFrame): + def __init__(self, master=None, cnf={}, **kw): + super().__init__(master, cnf, **kw) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar.grid(row=0, column=1, sticky="ns") + self.listbox = tk.Listbox( + self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set + ) + self.listbox.grid(row=0, column=0, sticky="nsew") + self.scrollbar.config(command=self.listbox.yview) + + +class CheckboxList(tk.LabelFrame): + def __init__(self, master=None, cnf={}, clicked=None, **kw): + super().__init__(master, cnf, **kw) + self.clicked = clicked + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.canvas = tk.Canvas(self, highlightthickness=0) + self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar( + self, orient="vertical", command=self.canvas.yview + ) + self.scrollbar.grid(row=0, column=2, sticky="ns") + self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame.columnconfigure(0, weight=1) + self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set + ) + self.frame.bind( + "", + lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", + lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + ) + + def clear(self): + for widget in self.frame.winfo_children(): + widget.destroy() + + def add(self, name): + var = tk.BooleanVar() + func = partial(self.clicked, name, var) + checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox.grid(sticky="w") From b71f93e60669cbd90b7c47c500401287982a7bd6 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:22:40 -0800 Subject: [PATCH 074/305] added scrollable frame widget which can re-use code to be the basis for other scrollable frame widgets --- coretk/coretk/widgets.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 80347e73..606f7662 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -2,6 +2,41 @@ import tkinter as tk from functools import partial +class FrameScroll(tk.LabelFrame): + def __init__(self, master=None, cnf={}, **kw): + super().__init__(master, cnf, **kw) + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=1) + self.canvas = tk.Canvas(self, highlightthickness=0) + self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.scrollbar = tk.Scrollbar( + self, orient="vertical", command=self.canvas.yview + ) + self.scrollbar.grid(row=0, column=2, sticky="ns") + self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame.columnconfigure(0, weight=1) + self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set + ) + self.frame.bind( + "", + lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), + ) + self.canvas.bind( + "", + lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + ) + + def clear(self): + for widget in self.frame.winfo_children(): + widget.destroy() + + class ListboxScroll(tk.LabelFrame): def __init__(self, master=None, cnf={}, **kw): super().__init__(master, cnf, **kw) From 40e1bb0374163ff718b2ab3cf127572ee192c475 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 6 Nov 2019 14:34:41 -0800 Subject: [PATCH 075/305] add emane configurations to my start_session() --- coretk/coretk/coreclient.py | 124 +++++++++++++++------------ coretk/coretk/coretoolbarhelp.py | 31 +++++-- coretk/coretk/dialogs/emaneconfig.py | 5 ++ coretk/coretk/emaneodelnodeconfig.py | 78 +++++++++++++++++ 4 files changed, 175 insertions(+), 63 deletions(-) create mode 100644 coretk/coretk/emaneodelnodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 71e37447..ecc35395 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -3,16 +3,16 @@ Incorporate grpc into python tkinter GUI """ import logging import os -from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog +from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] @@ -95,6 +95,7 @@ class CoreClient: self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() + self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None def handle_events(self, event): @@ -165,6 +166,8 @@ class CoreClient: logging.info("emane config: %s", response) self.emane_config = response.config + # get emane model config + # determine next node id and reusable nodes max_id = 1 for node in session.nodes: @@ -332,6 +335,8 @@ class CoreClient: hooks=list(self.hooks.values()), wlan_configs=wlan_configs, emane_config=emane_config, + emane_model_configs=emane_model_configs, + mobility_configs=mobility_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -339,22 +344,22 @@ class CoreClient: response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) - # TODO no need, might get rid of this - def add_link(self, id1, id2, type1, type2, edge): - """ - Grpc client request add link - - :param int session_id: session id - :param int id1: node 1 core id - :param core_pb2.NodeType type1: node 1 core node type - :param int id2: node 2 core id - :param core_pb2.NodeType type2: node 2 core node type - :return: nothing - """ - if1 = self.create_interface(type1, edge.interface_1) - if2 = self.create_interface(type2, edge.interface_2) - response = self.client.add_link(self.session_id, id1, id2, if1, if2) - logging.info("created link: %s", response) + # # TODO no need, might get rid of this + # def add_link(self, id1, id2, type1, type2, edge): + # """ + # Grpc client request add link + # + # :param int session_id: session id + # :param int id1: node 1 core id + # :param core_pb2.NodeType type1: node 1 core node type + # :param int id2: node 2 core id + # :param core_pb2.NodeType type2: node 2 core node type + # :return: nothing + # """ + # if1 = self.create_interface(type1, edge.interface_1) + # if2 = self.create_interface(type2, edge.interface_2) + # response = self.client.add_link(self.session_id, id1, id2, if1, if2) + # logging.info("created link: %s", response) def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) @@ -417,22 +422,22 @@ class CoreClient: else: return self.reusable.pop(0) - def add_node(self, node_type, model, x, y, name, node_id): - position = core_pb2.Position(x=x, y=y) - node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - self.node_ids.append(node_id) - response = self.client.add_node(self.session_id, node) - logging.info("created node: %s", response) - if node_type == core_pb2.NodeType.WIRELESS_LAN: - d = OrderedDict() - d["basic_range"] = "275" - d["bandwidth"] = "54000000" - d["jitter"] = "0" - d["delay"] = "20000" - d["error"] = "0" - r = self.client.set_wlan_config(self.session_id, node_id, d) - logging.debug("set wlan config %s", r) - return response.node_id + # def add_node(self, node_type, model, x, y, name, node_id): + # position = core_pb2.Position(x=x, y=y) + # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) + # self.node_ids.append(node_id) + # response = self.client.add_node(self.session_id, node) + # logging.info("created node: %s", response) + # if node_type == core_pb2.NodeType.WIRELESS_LAN: + # d = OrderedDict() + # d["basic_range"] = "275" + # d["bandwidth"] = "54000000" + # d["jitter"] = "0" + # d["delay"] = "20000" + # d["error"] = "0" + # r = self.client.set_wlan_config(self.session_id, node_id, d) + # logging.debug("set wlan config %s", r) + # return response.node_id def add_graph_node(self, session_id, canvas_id, x, y, name): """ @@ -458,6 +463,8 @@ class CoreClient: node_type = core_pb2.NodeType.RJ45 elif name == "tunnel": node_type = core_pb2.NodeType.TUNNEL + elif name == "emane": + node_type = core_pb2.NodeType.EMANE elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name @@ -468,9 +475,12 @@ class CoreClient: # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) + # set default emane configuration for emane node + if node_type == core_pb2.NodeType.EMANE: + self.emaneconfig_management.set_default_config(nid) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) # self.core_id_to_canvas_id[nid] = canvas_id @@ -628,44 +638,50 @@ class CoreClient: :return: nothing """ + node_one = self.nodes[canvas_id_1] + node_two = self.nodes[canvas_id_2] if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: edge = Edge( session_id, - self.nodes[canvas_id_1].node_id, - self.nodes[canvas_id_1].type, - self.nodes[canvas_id_2].node_id, - self.nodes[canvas_id_2].type, + node_one.node_id, + node_one.type, + node_two.node_id, + node_two.type, ) self.edges[token] = edge src_interface, dst_interface = self.create_edge_interface( edge, canvas_id_1, canvas_id_2 ) - node_one_id = self.nodes[canvas_id_1].node_id - node_two_id = self.nodes[canvas_id_2].node_id + node_one_id = node_one.node_id + node_two_id = node_two.node_id # provide a way to get an edge from a core node and an interface id if src_interface is not None: self.core_mapping.map_node_and_interface_to_canvas_edge( node_one_id, src_interface.id, token ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_one_id, - src_interface.id, - token, - ) if dst_interface is not None: self.core_mapping.map_node_and_interface_to_canvas_edge( node_two_id, dst_interface.id, token ) - logging.debug( - "map node id %s, interface_id %s to edge token %s", - node_two_id, - dst_interface.id, - token, - ) - logging.debug("Adding edge to grpc manager...") + if ( + node_one.type == core_pb2.NodeType.EMANE + and node_two.type == core_pb2.NodeType.DEFAULT + ): + if node_two.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_one.node_id, node_two.node_id, dst_interface.id + ) + elif ( + node_two.type == core_pb2.NodeType.EMANE + and node_one.type == core_pb2.NodeType.DEFAULT + ): + if node_one.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_two.node_id, node_one.node_id, src_interface.id + ) + else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 915d3b80..c650a5bc 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -34,14 +34,6 @@ class CoreToolbarHelp: for edge in self.app.core.edges.values(): interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) - # TODO for now only consider the basic cases - # if ( - # edge.type1 == core_pb2.NodeType.WIRELESS_LAN - # or edge.type2 == core_pb2.NodeType.WIRELESS_LAN - # ): - # link_type = core_pb2.LinkType.WIRELESS - # else: - # link_type = core_pb2.LinkType.WIRED link = core_pb2.Link( node_one_id=edge.id1, node_two_id=edge.id2, @@ -80,20 +72,41 @@ class CoreToolbarHelp: configs.append(cnf) return configs + def get_emane_configuration_list(self): + """ + form a list of emane configuration for the nodes + + :return: nothing + """ + configs = [] + manager_configs = self.app.core.emaneconfig_management.configurations + for key, value in manager_configs.items(): + config = {x: value[1][x].value for x in value[1]} + configs.append( + core_pb2.EmaneModelConfig( + node_id=key[0], interface_id=key[1], model=value[0], config=config + ) + ) + return configs + def gui_start_session(self): nodes = self.get_node_list() links = self.get_link_list() wlan_configs = self.get_wlan_configuration_list() mobility_configs = self.get_mobility_configuration_list() - # get emane config + # get emane config (global configuration) pb_emane_config = self.app.core.emane_config emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + # get emane configuration list + emane_model_configs = self.get_emane_configuration_list() + self.app.core.start_session( nodes, links, wlan_configs=wlan_configs, mobility_configs=mobility_configs, emane_config=emane_config, + emane_model_configs=emane_model_configs, ) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d6436deb..0521d2cf 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -126,6 +126,11 @@ class EmaneConfiguration(Dialog): response, ) + # store the change locally + self.app.core.emaneconfig_management.set_custom_emane_cloud_config( + self.canvas_node.core_id, "emane_" + model_name + ) + self.emane_model_dialog.destroy() def draw_model_options(self): diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py new file mode 100644 index 00000000..e3a9cc33 --- /dev/null +++ b/coretk/coretk/emaneodelnodeconfig.py @@ -0,0 +1,78 @@ +""" +emane model configurations +""" +import logging + + +class EmaneModelNodeConfig: + def __init__(self, app): + """ + create an instance for EmaneModelNodeConfig + + :param app: application + """ + # dict(tuple(node_id, interface_id, model) : config) + self.configurations = {} + + # dict(int, list(int)) stores emane node maps to mdr nodes that are linked to that emane node + self.links = {} + + self.app = app + + def set_default_config(self, node_id): + """ + set a default emane configuration for a newly created emane + + :param int node_id: node id + :return: nothing + """ + session_id = self.app.core.session_id + client = self.app.core.client + default_emane_model = client.get_emane_models(session_id).models[0] + response = client.get_emane_model_config( + session_id, node_id, default_emane_model + ) + logging.info( + "emanemodelnodeconfig.py get emane model config (%s), result: %s", + node_id, + response, + ) + self.configurations[tuple([node_id, None])] = tuple( + [default_emane_model, response.config] + ) + self.links[node_id] = [] + + def set_default_for_mdr(self, emane_node_id, mdr_node_id, interface_id): + """ + set emane configuration of an mdr node on the correct interface + + :param int emane_node_id: emane node id + :param int mdr_node_id: mdr node id + :param int interface_id: interface id + :return: nothing + """ + self.configurations[tuple([mdr_node_id, interface_id])] = self.configurations[ + tuple([emane_node_id, None]) + ] + self.links[emane_node_id].append(tuple([mdr_node_id, interface_id])) + + def set_custom_emane_cloud_config(self, emane_node_id, model_name): + """ + set custom configuration for an emane node, if model is changed, update the nodes connected to that emane node + + :param int emane_node_id: emane node id + :param str model_name: model name + :return: nothing + """ + prev_model_name = self.configurations[tuple([emane_node_id, None])][0] + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_model_config( + session_id, emane_node_id, model_name + ) + self.configurations[tuple([emane_node_id, None])] = tuple( + [model_name, response.config] + ) + + if prev_model_name != model_name: + for k in self.links[emane_node_id]: + self.configurations[k] = tuple([model_name, response.config]) From 99876375647701c5508c8c6d1a26ba01e288da44 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 14:36:36 -0800 Subject: [PATCH 076/305] updated and used common frame scroll class where needed, created common config widget to use where all configurations get drawn --- coretk/coretk/dialogs/customnodes.py | 7 +- coretk/coretk/dialogs/emaneconfig.py | 1 + coretk/coretk/dialogs/sessionoptions.py | 30 +++--- coretk/coretk/widgets.py | 126 ++++++++++++++++-------- 4 files changed, 109 insertions(+), 55 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 841b2dcf..38d59c83 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -28,6 +28,7 @@ class ServicesSelectDialog(Dialog): for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) self.services = CheckboxList( frame, text="Services", clicked=self.service_clicked @@ -46,6 +47,9 @@ class ServicesSelectDialog(Dialog): button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + # trigger group change + self.groups.listbox.event_generate("<>") + def handle_group_change(self, event): selection = self.groups.listbox.curselection() if selection: @@ -53,7 +57,8 @@ class ServicesSelectDialog(Dialog): group = self.groups.listbox.get(index) self.services.clear() for service in sorted(self.app.core.services[group], key=lambda x: x.name): - self.services.add(service.name) + checked = service.name in self.current_services + self.services.add(service.name, checked) def service_clicked(self, name, var): if var.get() and name not in self.current_services: diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d6436deb..b3bca2fd 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -93,6 +93,7 @@ class EmaneConfiguration(Dialog): response = self.app.core.client.get_emane_config(session_id) logging.info("emane config: %s", response) self.options = response.config + self.values = configutils.create_config( self.emane_dialog, self.options, PAD_X, PAD_Y ) diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index e887c2a1..8702d581 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,8 +1,8 @@ import logging import tkinter as tk -from coretk import configutils from coretk.dialogs.dialog import Dialog +from coretk.widgets import ConfigFrame PAD_X = 2 PAD_Y = 2 @@ -11,25 +11,33 @@ PAD_Y = 2 class SessionOptionsDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Session Options", modal=True) - self.options = None - self.values = None - self.save_button = tk.Button(self, text="Save", command=self.save) - self.cancel_button = tk.Button(self, text="Cancel", command=self.destroy) + self.config_frame = None self.draw() def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.options = response.config - self.values = configutils.create_config(self, self.options, PAD_X, PAD_Y) - self.save_button.grid(row=1, pady=PAD_Y, padx=PAD_X, sticky="ew") - self.cancel_button.grid(row=1, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + + self.config_frame = ConfigFrame(self, config=response.config) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew") + + frame = tk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = tk.Button(frame, text="Save", command=self.save) + button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") + button = tk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): - configutils.parse_config(self.options, self.values) + config = self.config_frame.parse_config() session_id = self.app.core.session_id - config = {x: self.options[x].value for x in self.options} response = self.app.core.client.set_session_options(session_id, config) logging.info("saved session config: %s", response) self.destroy() diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 606f7662..cb377dcc 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,5 +1,9 @@ +import logging import tkinter as tk from functools import partial +from tkinter import ttk + +from coretk.configutils import ConfigType class FrameScroll(tk.LabelFrame): @@ -7,29 +11,39 @@ class FrameScroll(tk.LabelFrame): super().__init__(master, cnf, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=1) self.canvas = tk.Canvas(self, highlightthickness=0) - self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) + self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) self.scrollbar = tk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) - self.scrollbar.grid(row=0, column=2, sticky="ns") + self.scrollbar.grid(row=0, column=1, sticky="ns") self.frame = tk.Frame(self.canvas, padx=2, pady=2) - self.frame.columnconfigure(0, weight=1) self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) self.canvas.update_idletasks() self.canvas.configure( scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set ) - self.frame.bind( - "", - lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", - lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), + self.frame.bind("", self._configure_frame) + self.canvas.bind("", self._configure_canvas) + + def _configure_frame(self, event): + req_width = self.frame.winfo_reqwidth() + req_height = self.frame.winfo_reqheight() + if req_width != self.canvas.winfo_reqwidth(): + self.canvas.configure(width=req_width) + if req_height != self.canvas.winfo_reqheight(): + self.canvas.configure(height=req_height) + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + + def _configure_canvas(self, event): + self.canvas.itemconfig(self.frame_id, width=event.width) + + def update_canvas(self): + self.canvas.update_idletasks() + self.canvas.configure( + scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set ) def clear(self): @@ -37,6 +51,61 @@ class FrameScroll(tk.LabelFrame): widget.destroy() +class ConfigFrame(FrameScroll): + def __init__(self, master=None, cnf={}, config=None, **kw): + super().__init__(master, cnf, **kw) + self.frame.columnconfigure(1, weight=1) + if not config: + config = {} + self.config = config + self.values = {} + + def draw_config(self): + padx = 2 + pady = 2 + for index, key in enumerate(sorted(self.config)): + option = self.config[key] + label = tk.Label(self.frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="w") + value = tk.StringVar() + config_type = ConfigType(option.type) + if config_type == ConfigType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox(self.frame, textvariable=value, values=select) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif config_type == ConfigType.STRING: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif config_type == ConfigType.EMANECONFIG: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + else: + logging.error("unhandled config option type: %s", config_type) + self.values[key] = value + + def parse_config(self): + for key in self.config: + option = self.config[key] + value = self.values[key] + config_type = ConfigType(option.type) + config_value = value.get() + if config_type == ConfigType.BOOL: + if config_value == "On": + option.value = "1" + else: + option.value = "0" + else: + option.value = config_value + + return {x: self.config[x].value for x in self.config} + + class ListboxScroll(tk.LabelFrame): def __init__(self, master=None, cnf={}, **kw): super().__init__(master, cnf, **kw) @@ -51,43 +120,14 @@ class ListboxScroll(tk.LabelFrame): self.scrollbar.config(command=self.listbox.yview) -class CheckboxList(tk.LabelFrame): +class CheckboxList(FrameScroll): def __init__(self, master=None, cnf={}, clicked=None, **kw): super().__init__(master, cnf, **kw) self.clicked = clicked - self.rowconfigure(0, weight=1) - self.columnconfigure(0, weight=1) - self.columnconfigure(1, weight=1) - self.canvas = tk.Canvas(self, highlightthickness=0) - self.canvas.grid(row=0, columnspan=2, sticky="nsew", padx=2, pady=2) - self.canvas.columnconfigure(0, weight=1) - self.canvas.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar( - self, orient="vertical", command=self.canvas.yview - ) - self.scrollbar.grid(row=0, column=2, sticky="ns") - self.frame = tk.Frame(self.canvas, padx=2, pady=2) self.frame.columnconfigure(0, weight=1) - self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) - self.canvas.update_idletasks() - self.canvas.configure( - scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set - ) - self.frame.bind( - "", - lambda event: self.canvas.configure(scrollregion=self.canvas.bbox("all")), - ) - self.canvas.bind( - "", - lambda event: self.canvas.itemconfig(self.frame_id, width=event.width), - ) - def clear(self): - for widget in self.frame.winfo_children(): - widget.destroy() - - def add(self, name): - var = tk.BooleanVar() + def add(self, name, checked): + var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 0147bb9988a27e2c750cf6df9222ed5c83070fdc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 20:49:09 -0800 Subject: [PATCH 077/305] updated config generation to use config frame where possible --- coretk/coretk/configutils.py | 99 ---------------------------- coretk/coretk/dialogs/emaneconfig.py | 63 ++++++++++-------- coretk/coretk/widgets.py | 43 ++++++++---- 3 files changed, 65 insertions(+), 140 deletions(-) delete mode 100644 coretk/coretk/configutils.py diff --git a/coretk/coretk/configutils.py b/coretk/coretk/configutils.py deleted file mode 100644 index 267feb73..00000000 --- a/coretk/coretk/configutils.py +++ /dev/null @@ -1,99 +0,0 @@ -import enum -import logging -import tkinter as tk -from tkinter import ttk - - -class ConfigType(enum.Enum): - STRING = 10 - BOOL = 11 - EMANECONFIG = 7 - - -def create_config(master, config, padx=2, pady=2): - """ - Creates a scrollable canvas with an embedded window for displaying configuration - options. Will use grid layout to consume row 0 and columns 0-2. - - :param master: master to add scrollable canvas to - :param dict config: config option mapping keys to config options - :param int padx: x padding for widgets - :param int pady: y padding for widgets - :return: widget value mapping - """ - master.rowconfigure(0, weight=1) - master.columnconfigure(0, weight=1) - master.columnconfigure(1, weight=1) - - canvas = tk.Canvas(master, highlightthickness=0) - canvas.grid(row=0, columnspan=2, sticky="nsew", padx=padx, pady=pady) - canvas.columnconfigure(0, weight=1) - canvas.rowconfigure(0, weight=1) - - scroll_y = tk.Scrollbar(master, orient="vertical", command=canvas.yview) - scroll_y.grid(row=0, column=2, sticky="ns") - - frame = tk.Frame(canvas, padx=padx, pady=pady) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=3) - - values = {} - for index, key in enumerate(sorted(config)): - option = config[key] - label = tk.Label(frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="ew") - value = tk.StringVar() - config_type = ConfigType(option.type) - if config_type == ConfigType.BOOL: - select = tuple(option.select) - combobox = ttk.Combobox(frame, textvariable=value, values=select) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - if option.value == "1": - value.set("On") - else: - value.set("Off") - elif config_type == ConfigType.STRING: - value.set(option.value) - entry = tk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif config_type == ConfigType.EMANECONFIG: - value.set(option.value) - entry = tk.Entry(frame, textvariable=value, bg="white") - entry.grid(row=index, column=1, sticky="ew", pady=pady) - else: - logging.error("unhandled config option type: %s", config_type) - values[key] = value - - frame_id = canvas.create_window(0, 0, anchor="nw", window=frame) - canvas.update_idletasks() - canvas.configure(scrollregion=canvas.bbox("all"), yscrollcommand=scroll_y.set) - - frame.bind( - "", lambda event: canvas.configure(scrollregion=canvas.bbox("all")) - ) - canvas.bind( - "", lambda event: canvas.itemconfig(frame_id, width=event.width) - ) - return values - - -def parse_config(options, values): - """ - Given a set of configurations, parse out values and transform them when needed. - - :param dict options: option key mapping to configuration options - :param dict values: option key mapping to widget values - :return: nothing - """ - for key in options: - option = options[key] - value = values[key] - config_type = ConfigType(option.type) - config_value = value.get() - if config_type == ConfigType.BOOL: - if config_value == "On": - option.value = "1" - else: - option.value = "0" - else: - option.value = config_value diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index b3bca2fd..d90a970c 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -7,9 +7,9 @@ import tkinter as tk import webbrowser from tkinter import ttk -from coretk import configutils from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images +from coretk.widgets import ConfigFrame PAD_X = 2 PAD_Y = 2 @@ -38,10 +38,10 @@ class EmaneConfiguration(Dialog): self.emane_options() self.draw_apply_and_cancel() - self.values = None + self.emane_config_frame = None self.options = app.core.emane_config self.model_options = None - self.model_values = None + self.model_config_frame = None def create_text_variable(self, val): """ @@ -75,7 +75,7 @@ class EmaneConfiguration(Dialog): f.grid(row=0, column=0, sticky="nsew") def save_emane_option(self): - configutils.parse_config(self.options, self.values) + self.emane_config_frame.parse_config() self.emane_dialog.destroy() def draw_emane_options(self): @@ -83,10 +83,6 @@ class EmaneConfiguration(Dialog): self.emane_dialog = Dialog( self, self.app, "emane configuration", modal=False ) - b1 = tk.Button(self.emane_dialog, text="Appy", command=self.save_emane_option) - b2 = tk.Button( - self.emane_dialog, text="Cancel", command=self.emane_dialog.destroy - ) if self.options is None: session_id = self.app.core.session_id @@ -94,11 +90,20 @@ class EmaneConfiguration(Dialog): logging.info("emane config: %s", response) self.options = response.config - self.values = configutils.create_config( - self.emane_dialog, self.options, PAD_X, PAD_Y - ) - b1.grid(row=1, column=0) - b2.grid(row=1, column=1) + self.emane_dialog.columnconfigure(0, weight=1) + self.emane_dialog.rowconfigure(0, weight=1) + self.emane_config_frame = ConfigFrame(self.emane_dialog, config=self.options) + self.emane_config_frame.draw_config() + self.emane_config_frame.grid(sticky="nsew") + + frame = tk.Frame(self.emane_dialog) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + b1 = tk.Button(frame, text="Appy", command=self.save_emane_option) + b1.grid(row=0, column=0, sticky="ew") + b2 = tk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) + b2.grid(row=0, column=1, sticky="ew") self.emane_dialog.show() def save_emane_model_options(self): @@ -111,8 +116,7 @@ class EmaneConfiguration(Dialog): model_name = self.emane_models[self.emane_model_combobox.current()] # parse configuration - configutils.parse_config(self.model_options, self.model_values) - config = {x: self.model_options[x].value for x in self.model_options} + config = self.model_config_frame.parse_config() # add string emane_ infront for grpc call response = self.app.core.client.set_emane_model_config( @@ -141,17 +145,10 @@ class EmaneConfiguration(Dialog): # create the dialog and the necessry widget if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): self.emane_model_dialog = Dialog( - self, self.app, model_name + " configuration", modal=False + self, self.app, f"{model_name} configuration", modal=False ) - - b1 = tk.Button( - self.emane_model_dialog, text="Apply", command=self.save_emane_model_options - ) - b2 = tk.Button( - self.emane_model_dialog, - text="Cancel", - command=self.emane_model_dialog.destroy, - ) + self.emane_model_dialog.columnconfigure(0, weight=1) + self.emane_model_dialog.rowconfigure(0, weight=1) # query for configurations session_id = self.app.core.session_id @@ -162,12 +159,20 @@ class EmaneConfiguration(Dialog): logging.info("emane model config %s", response) self.model_options = response.config - self.model_values = configutils.create_config( - self.emane_model_dialog, self.model_options, PAD_X, PAD_Y + self.model_config_frame = ConfigFrame( + self.emane_model_dialog, config=self.model_options ) + self.model_config_frame.grid(sticky="nsew") + self.model_config_frame.draw_config() - b1.grid(row=1, column=0, sticky="nsew") - b2.grid(row=1, column=1, sticky="nsew") + frame = tk.Frame(self.emane_model_dialog) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + b1 = tk.Button(frame, text="Apply", command=self.save_emane_model_options) + b1.grid(row=0, column=0, sticky="ew") + b2 = tk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) + b2.grid(row=0, column=1, sticky="ew") self.emane_model_dialog.show() def draw_option_buttons(self, parent): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index cb377dcc..9625bc67 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -3,7 +3,18 @@ import tkinter as tk from functools import partial from tkinter import ttk -from coretk.configutils import ConfigType +from core.api.grpc import core_pb2 + +INT_TYPES = { + core_pb2.ConfigOptionType.UINT8, + core_pb2.ConfigOptionType.UINT16, + core_pb2.ConfigOptionType.UINT32, + core_pb2.ConfigOptionType.UINT64, + core_pb2.ConfigOptionType.INT8, + core_pb2.ConfigOptionType.INT16, + core_pb2.ConfigOptionType.INT32, + core_pb2.ConfigOptionType.INT64, +} class FrameScroll(tk.LabelFrame): @@ -30,11 +41,8 @@ class FrameScroll(tk.LabelFrame): def _configure_frame(self, event): req_width = self.frame.winfo_reqwidth() - req_height = self.frame.winfo_reqheight() if req_width != self.canvas.winfo_reqwidth(): self.canvas.configure(width=req_width) - if req_height != self.canvas.winfo_reqheight(): - self.canvas.configure(height=req_height) self.canvas.configure(scrollregion=self.canvas.bbox("all")) def _configure_canvas(self, event): @@ -68,34 +76,45 @@ class ConfigFrame(FrameScroll): label = tk.Label(self.frame, text=option.label) label.grid(row=index, pady=pady, padx=padx, sticky="w") value = tk.StringVar() - config_type = ConfigType(option.type) - if config_type == ConfigType.BOOL: + if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) - combobox = ttk.Combobox(self.frame, textvariable=value, values=select) + combobox = ttk.Combobox( + self.frame, textvariable=value, values=select, state="readonly" + ) combobox.grid(row=index, column=1, sticky="ew", pady=pady) if option.value == "1": value.set("On") else: value.set("Off") - elif config_type == ConfigType.STRING: + elif option.select: + value.set(option.value) + select = tuple(option.select) + combobox = ttk.Combobox( + self.frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) entry = tk.Entry(self.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif config_type == ConfigType.EMANECONFIG: + elif option.type in INT_TYPES: + value.set(option.value) + entry = tk.Entry(self.frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = tk.Entry(self.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: - logging.error("unhandled config option type: %s", config_type) + logging.error("unhandled config option type: %s", option.type) self.values[key] = value def parse_config(self): for key in self.config: option = self.config[key] value = self.values[key] - config_type = ConfigType(option.type) config_value = value.get() - if config_type == ConfigType.BOOL: + if option.type == core_pb2.ConfigOptionType.BOOL: if config_value == "On": option.value = "1" else: From 5d6d22c6eb14e857495dd5cc4214365b9f727fab Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 22:41:05 -0800 Subject: [PATCH 078/305] updated config frame widget to draw tabs for each config group --- coretk/coretk/widgets.py | 95 ++++++++++++++------------- daemon/proto/core/api/grpc/core.proto | 17 +++++ 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9625bc67..236dbc24 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -18,7 +18,7 @@ INT_TYPES = { class FrameScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, **kw): + def __init__(self, master=None, cnf={}, _cls=tk.Frame, **kw): super().__init__(master, cnf, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) @@ -30,7 +30,7 @@ class FrameScroll(tk.LabelFrame): self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") - self.frame = tk.Frame(self.canvas, padx=2, pady=2) + self.frame = _cls(self.canvas) self.frame_id = self.canvas.create_window(0, 0, anchor="nw", window=self.frame) self.canvas.update_idletasks() self.canvas.configure( @@ -48,12 +48,6 @@ class FrameScroll(tk.LabelFrame): def _configure_canvas(self, event): self.canvas.itemconfig(self.frame_id, width=event.width) - def update_canvas(self): - self.canvas.update_idletasks() - self.canvas.configure( - scrollregion=self.canvas.bbox("all"), yscrollcommand=self.scrollbar.set - ) - def clear(self): for widget in self.frame.winfo_children(): widget.destroy() @@ -61,53 +55,60 @@ class FrameScroll(tk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master=None, cnf={}, config=None, **kw): - super().__init__(master, cnf, **kw) - self.frame.columnconfigure(1, weight=1) - if not config: - config = {} + super().__init__(master, cnf, ttk.Notebook, **kw) self.config = config self.values = {} def draw_config(self): padx = 2 pady = 2 - for index, key in enumerate(sorted(self.config)): + group_mapping = {} + for key in self.config: option = self.config[key] - label = tk.Label(self.frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="w") - value = tk.StringVar() - if option.type == core_pb2.ConfigOptionType.BOOL: - select = tuple(option.select) - combobox = ttk.Combobox( - self.frame, textvariable=value, values=select, state="readonly" - ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - if option.value == "1": - value.set("On") + group = group_mapping.setdefault(option.group, []) + group.append(option) + + for group_name in sorted(group_mapping): + group = group_mapping[group_name] + frame = tk.Frame(self.frame) + frame.columnconfigure(1, weight=1) + self.frame.add(frame, text=group_name) + for index, option in enumerate(sorted(group, key=lambda x: x.name)): + label = tk.Label(frame, text=option.label) + label.grid(row=index, pady=pady, padx=padx, sticky="w") + value = tk.StringVar() + if option.type == core_pb2.ConfigOptionType.BOOL: + select = tuple(option.select) + combobox = ttk.Combobox( + frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + if option.value == "1": + value.set("On") + else: + value.set("Off") + elif option.select: + value.set(option.value) + select = tuple(option.select) + combobox = ttk.Combobox( + frame, textvariable=value, values=select, state="readonly" + ) + combobox.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.STRING: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type in INT_TYPES: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type == core_pb2.ConfigOptionType.FLOAT: + value.set(option.value) + entry = tk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) else: - value.set("Off") - elif option.select: - value.set(option.value) - select = tuple(option.select) - combobox = ttk.Combobox( - self.frame, textvariable=value, values=select, state="readonly" - ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type == core_pb2.ConfigOptionType.STRING: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type in INT_TYPES: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - elif option.type == core_pb2.ConfigOptionType.FLOAT: - value.set(option.value) - entry = tk.Entry(self.frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) - else: - logging.error("unhandled config option type: %s", option.type) - self.values[key] = value + logging.error("unhandled config option type: %s", option.type) + self.values[option.name] = value def parse_config(self): for key in self.config: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 19a1ff97..a53a11bb 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -768,6 +768,23 @@ message NodeType { } } +message ConfigOptionType { + enum Enum { + NONE = 0; + UINT8 = 1; + UINT16 = 2; + UINT32 = 3; + UINT64 = 4; + INT8 = 5; + INT16 = 6; + INT32 = 7; + INT64 = 8; + FLOAT = 9; + STRING = 10; + BOOL = 11; + } +} + message ServiceValidationMode { enum Enum { BLOCKING = 0; From 1cf484783567e387a83d11f667ab16a4d7fa5ba0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 23:16:49 -0800 Subject: [PATCH 079/305] removed coreclient set session state --- coretk/coretk/coreclient.py | 46 +++---------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 903d81c0..3fc67254 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -214,7 +214,9 @@ class CoreClient: s = self.client.get_session(sid).session # delete links and nodes from running session if s.state == core_pb2.SessionState.RUNTIME: - self.set_session_state("datacollect", sid) + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DATACOLLECT + ) self.delete_links(sid) self.delete_nodes(sid) self.delete_session(sid) @@ -248,48 +250,6 @@ class CoreClient: # logging.info("get session: %s", response) return response.session.state - def set_session_state(self, state, custom_session_id=None): - """ - Set session state - - :param str state: session state to set - :return: nothing - """ - if custom_session_id is None: - sid = self.session_id - else: - sid = custom_session_id - - response = None - if state == "configuration": - response = self.client.set_session_state( - sid, core_pb2.SessionState.CONFIGURATION - ) - elif state == "instantiation": - response = self.client.set_session_state( - sid, core_pb2.SessionState.INSTANTIATION - ) - elif state == "datacollect": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DATACOLLECT - ) - elif state == "shutdown": - response = self.client.set_session_state( - sid, core_pb2.SessionState.SHUTDOWN - ) - elif state == "runtime": - response = self.client.set_session_state(sid, core_pb2.SessionState.RUNTIME) - elif state == "definition": - response = self.client.set_session_state( - sid, core_pb2.SessionState.DEFINITION - ) - elif state == "none": - response = self.client.set_session_state(sid, core_pb2.SessionState.NONE) - else: - logging.error("coregrpc.py: set_session_state: INVALID STATE") - - logging.info("set session state: %s", response) - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) response = self.client.edit_node(self.session_id, node_id, position) From 613568ca28a517c5e66ed748e421dff7493f1df5 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 6 Nov 2019 23:58:02 -0800 Subject: [PATCH 080/305] updates to get custom nodes dialog to a working state --- coretk/coretk/coreclient.py | 8 +++ coretk/coretk/dialogs/customnodes.py | 85 +++++++++++++++++++++------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 3fc67254..b5114462 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -64,6 +64,13 @@ class CoreServer: self.port = port +class CustomNode: + def __init__(self, name, image, services): + self.name = name + self.image = image + self.services = services + + class CoreClient: def __init__(self, app): """ @@ -76,6 +83,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.custom_nodes = {} # distributed server data self.servers = {} diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 38d59c83..04931034 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,17 +1,18 @@ import tkinter as tk +from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.nodeicon import IconDialog from coretk.widgets import CheckboxList, ListboxScroll class ServicesSelectDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app, current_services): super().__init__(master, app, "Node Services", modal=True) self.groups = None self.services = None self.current = None - self.current_services = set() + self.current_services = current_services self.draw() def draw(self): @@ -37,14 +38,16 @@ class ServicesSelectDialog(Dialog): self.current = ListboxScroll(frame, text="Selected") self.current.grid(row=0, column=2, sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) frame = tk.Frame(self) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save") + button = tk.Button(frame, text="Save", command=self.click_cancel) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") # trigger group change @@ -69,15 +72,23 @@ class ServicesSelectDialog(Dialog): for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) + def click_cancel(self): + self.current_services = None + self.destroy() + class CustomNodesDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Custom Nodes", modal=True) - self.save_button = None + self.edit_button = None self.delete_button = None + self.nodes_list = None self.name = tk.StringVar() self.image_button = None self.image = None + self.services = set() + self.selected = None + self.selected_index = None self.draw() def draw(self): @@ -93,13 +104,11 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - listbox = tk.Listbox(frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set) - listbox.grid(row=0, column=0, sticky="nsew") - - scrollbar.config(command=listbox.yview) + self.nodes_list = ListboxScroll(frame) + self.nodes_list.grid(row=0, column=0, sticky="nsew") + self.nodes_list.listbox.bind("<>", self.handle_node_select) + for name in sorted(self.app.core.custom_nodes): + self.nodes_list.listbox.insert(tk.END, name) frame = tk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") @@ -120,10 +129,10 @@ class CustomNodesDialog(Dialog): button = tk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( - frame, text="Save", state=tk.DISABLED, command=self.click_save + self.edit_button = tk.Button( + frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew") self.delete_button = tk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -136,7 +145,7 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save) + button = tk.Button(frame, text="Save", command=self.click_edit) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) @@ -150,14 +159,50 @@ class CustomNodesDialog(Dialog): self.image_button.config(image=self.image) def click_services(self): - dialog = ServicesSelectDialog(self, self.app) + dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() + if dialog.current_services is not None: + self.services = dialog.current_services def click_create(self): - pass + name = self.name.get() + if name not in self.app.core.custom_nodes: + custom_node = CustomNode(name, self.image, self.services) + self.app.core.custom_nodes[name] = custom_node + self.nodes_list.listbox.insert(tk.END, name) + self.reset_values() - def click_save(self): + def reset_values(self): + self.name.set("") + self.image = None + self.services = set() + self.image_button.config(image="") + + def click_edit(self): pass def click_delete(self): - pass + if self.selected and self.selected in self.app.core.custom_nodes: + self.nodes_list.listbox.delete(self.selected_index) + del self.app.core.custom_nodes[self.selected] + self.reset_values() + self.nodes_list.listbox.selection_clear(0, tk.END) + self.nodes_list.listbox.event_generate("<>") + + def handle_node_select(self, event): + selection = self.nodes_list.listbox.curselection() + if selection: + self.selected_index = selection[0] + self.selected = self.nodes_list.listbox.get(self.selected_index) + custom_node = self.app.core.custom_nodes[self.selected] + self.name.set(custom_node.name) + self.services = custom_node.services + self.image = custom_node.image + self.image_button.config(image=self.image) + self.edit_button.config(state=tk.NORMAL) + self.delete_button.config(state=tk.NORMAL) + else: + self.selected = None + self.selected_index = None + self.edit_button.config(state=tk.DISABLED) + self.delete_button.config(state=tk.DISABLED) From a789498f5c13f6cf7d557975a95dcdba3e199156 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 08:30:49 -0800 Subject: [PATCH 081/305] updates --- daemon/data/logging.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/data/logging.conf b/daemon/data/logging.conf index 46de6e92..7f3d496f 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -14,7 +14,7 @@ } }, "root": { - "level": "DEBUG", + "level": "INFO", "handlers": ["console"] } } From 2873c32c23148c03c3cda2714f616a6be7c13e2a Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 11:33:40 -0800 Subject: [PATCH 082/305] custom nodes dialog works for creating, editing, and saving to config in basic case --- coretk/coretk/appdirs.py | 5 +- coretk/coretk/coreclient.py | 31 ++++++++--- coretk/coretk/dialogs/customnodes.py | 55 +++++++++++++++---- .../dialogs/{nodeicon.py => icondialog.py} | 0 coretk/coretk/dialogs/nodeconfig.py | 2 +- coretk/coretk/dialogs/wlanconfig.py | 2 +- coretk/coretk/images.py | 4 ++ 7 files changed, 78 insertions(+), 21 deletions(-) rename coretk/coretk/dialogs/{nodeicon.py => icondialog.py} (100%) diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 51a78f76..553b0949 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -42,7 +42,10 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) - config = {"servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}]} + config = { + "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], + "nodes": [], + } save_config(config) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b5114462..f4135269 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,6 +8,7 @@ from collections import OrderedDict from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog +from coretk.images import Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -65,9 +66,10 @@ class CoreServer: class CustomNode: - def __init__(self, name, image, services): + def __init__(self, name, image, image_file, services): self.name = name self.image = image + self.image_file = image_file self.services = services @@ -83,15 +85,11 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} - self.custom_nodes = {} - # distributed server data + # loaded configuration data self.servers = {} - for server_config in self.app.config["servers"]: - server = CoreServer( - server_config["name"], server_config["address"], server_config["port"] - ) - self.servers[server.name] = server + self.custom_nodes = {} + self.read_config() # data for managing the current session self.nodes = {} @@ -106,6 +104,23 @@ class CoreClient: self.mobilityconfig_management = MobilityNodeConfig() self.emane_config = None + def read_config(self): + # read distributed server + for server_config in self.app.config["servers"]: + server = CoreServer( + server_config["name"], server_config["address"], server_config["port"] + ) + self.servers[server.name] = server + + # read custom nodes + for node in self.app.config["nodes"]: + image_file = node["image"] + image = Images.get_custom(image_file) + custom_node = CustomNode( + node["name"], image, image_file, set(node["services"]) + ) + self.custom_nodes[custom_node.name] = custom_node + def handle_events(self, event): logging.info("event: %s", event) if event.link_event is not None: diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 04931034..f2423bf0 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,8 +1,11 @@ +import logging import tkinter as tk +from pathlib import Path +from coretk import appdirs from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.widgets import CheckboxList, ListboxScroll @@ -86,6 +89,7 @@ class CustomNodesDialog(Dialog): self.name = tk.StringVar() self.image_button = None self.image = None + self.image_file = None self.services = set() self.selected = None self.selected_index = None @@ -145,17 +149,25 @@ class CustomNodesDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_edit) + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def reset_values(self): + self.name.set("") + self.image = None + self.image_file = None + self.services = set() + self.image_button.config(image="") + def click_icon(self): dialog = IconDialog(self, self.app, self.name.get(), self.image) dialog.show() if dialog.image: self.image = dialog.image + self.image_file = dialog.file_path.get() self.image_button.config(image=self.image) def click_services(self): @@ -164,22 +176,44 @@ class CustomNodesDialog(Dialog): if dialog.current_services is not None: self.services = dialog.current_services + def click_save(self): + self.app.config["nodes"].clear() + for name in sorted(self.app.core.custom_nodes): + custom_node = self.app.core.custom_nodes[name] + self.app.config["nodes"].append( + { + "name": custom_node.name, + "image": custom_node.image_file, + "services": list(custom_node.services), + } + ) + logging.info("saving custom nodes: %s", self.app.config["nodes"]) + appdirs.save_config(self.app.config) + def click_create(self): name = self.name.get() if name not in self.app.core.custom_nodes: - custom_node = CustomNode(name, self.image, self.services) + custom_node = CustomNode( + name, self.image, Path(self.image_file).name, set(self.services) + ) self.app.core.custom_nodes[name] = custom_node self.nodes_list.listbox.insert(tk.END, name) self.reset_values() - def reset_values(self): - self.name.set("") - self.image = None - self.services = set() - self.image_button.config(image="") - def click_edit(self): - pass + name = self.name.get() + if self.selected: + previous_name = self.selected + self.selected = name + custom_node = self.app.core.custom_nodes.pop(previous_name) + custom_node.name = name + custom_node.image = self.image + custom_node.image_file = Path(self.image_file).name + custom_node.services = self.services + self.app.core.custom_nodes[name] = custom_node + self.nodes_list.listbox.delete(self.selected_index) + self.nodes_list.listbox.insert(self.selected_index, name) + self.nodes_list.listbox.selection_set(self.selected_index) def click_delete(self): if self.selected and self.selected in self.app.core.custom_nodes: @@ -198,6 +232,7 @@ class CustomNodesDialog(Dialog): self.name.set(custom_node.name) self.services = custom_node.services self.image = custom_node.image + self.image_file = custom_node.image_file self.image_button.config(image=self.image) self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) diff --git a/coretk/coretk/dialogs/nodeicon.py b/coretk/coretk/dialogs/icondialog.py similarity index 100% rename from coretk/coretk/dialogs/nodeicon.py rename to coretk/coretk/dialogs/icondialog.py diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index bc03dc51..3f13488a 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d57c8935..dc40e6c7 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,7 @@ wlan configuration import tkinter as tk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeicon import IconDialog +from coretk.dialogs.icondialog import IconDialog class WlanConfigDialog(Dialog): diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 768b33ba..f25b0eb1 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -29,6 +29,10 @@ class Images: def get(cls, image): return cls.images[image.value] + @classmethod + def get_custom(cls, name): + return cls.images[name] + @classmethod def convert_type_and_model_to_image(cls, node_type, node_model): """ From dcfd7f879535a331a795c58c7b8fc76ea5c8eded Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 13:23:02 -0800 Subject: [PATCH 083/305] working on delete node --- coretk/coretk/coreclient.py | 49 ++++++++++++++++------- coretk/coretk/coretoolbarhelp.py | 3 ++ coretk/coretk/graph.py | 36 ++++++++++++++++- coretk/coretk/mobilitynodeconfig.py | 4 ++ coretk/coretk/nodedelete.py | 61 +++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 coretk/coretk/nodedelete.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2f6e0b88..ae03922b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -500,6 +500,41 @@ class CoreClient: name, ) + def delete_wanted_graph_nodes(self, canvas_ids, tokens): + """ + remove the nodes selected by the user and anything related to that node + such as link, configurations, interfaces + + :param list(int) canvas_ids: list of canvas node ids + :return: nothing + """ + # keep reference to the core ids + core_node_ids = [self.nodes[x].node_id for x in canvas_ids] + + # delete the nodes + for i in canvas_ids: + try: + self.nodes.pop(i) + self.reusable.append(i) + except KeyError: + logging.error("coreclient.py INVALID NODE CANVAS ID") + + self.reusable.sort() + + # delete the edges and interfaces + for i in tokens: + try: + self.edges.pop(i) + except KeyError: + logging.error("coreclient.py invalid edge token ") + + # delete any configurations + for i in core_node_ids: + if i in self.mobilityconfig_management.configurations: + self.mobilityconfig_management.pop(i) + if i in self.wlanconfig_management.configurations: + self.wlanconfig_management.pop(i) + def add_preexisting_node(self, canvas_node, session_id, core_node, name): """ Add preexisting nodes to grpc manager @@ -553,20 +588,6 @@ class CoreClient: self.preexisting.clear() logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - def delete_node(self, canvas_id): - """ - Delete a node from the session - - :param int canvas_id: node's id in the canvas - :return: thing - """ - try: - self.nodes.pop(canvas_id) - self.reusable.append(canvas_id) - self.reusable.sort() - except KeyError: - logging.error("grpcmanagement.py INVALID NODE CANVAS ID") - def create_interface(self, node_type, gui_interface): """ create a protobuf interface given the interface object stored by the programmer diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index c650a5bc..5192f2c5 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -110,3 +110,6 @@ class CoreToolbarHelp: emane_config=emane_config, emane_model_configs=emane_model_configs, ) + + response = self.app.core.client.get_session(self.app.core.session_id) + print(response) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 448a6f1d..9ab6b956 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -8,6 +8,7 @@ from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.interface import Interface from coretk.linkinfo import LinkInfo, Throughput +from coretk.nodedelete import CanvasComponentManagement from coretk.wirelessconnection import WirelessConnection @@ -41,7 +42,11 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 + + self.canvas_management = CanvasComponentManagement(self, core) + self.canvas_action = CanvasAction(master, self) + self.setup_menus() self.setup_bindings() self.draw_grid() @@ -101,6 +106,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_release) self.bind("", self.click_motion) self.bind("", self.context) + self.bind("", self.press_delete) def draw_grid(self, width=1000, height=800): """ @@ -377,6 +383,25 @@ class CanvasGraph(tk.Canvas): self.node_context.unpost() self.is_node_context_opened = False + def press_delete(self, event): + # hide nodes, links, link information that shows on the GUI + to_delete_nodes, to_delete_edge_tokens = ( + self.canvas_management.delete_selected_nodes() + ) + + # delete nodes and link info stored in CanvasGraph object + for nid in to_delete_nodes: + self.nodes.pop(nid) + for token in to_delete_edge_tokens: + self.edges.pop(token) + + self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) + # delete any configuration related to the links and nodes + + # delete selected node + + # delete links connected to the selected nodes + def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] if self.selected == plot_id: @@ -473,11 +498,15 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) + self.canvas.tag_bind(self.id, "", self.select_multiple) self.edges = set() self.wlans = [] self.moving = None + def click(self, event): + print("click") + def double_click(self, event): node_id = self.canvas.core.nodes[self.id].node_id state = self.canvas.core.get_session_state() @@ -500,7 +529,8 @@ class CanvasNode: def click_press(self, event): logging.debug(f"click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) - # return "break" + + self.canvas.canvas_management.node_select(self) def click_release(self, event): logging.debug(f"click release {self.name}: {event}") @@ -520,6 +550,7 @@ class CanvasNode: self.canvas.move(self.id, offset_x, offset_y) self.canvas.move(self.text_id, offset_x, offset_y) self.antenna_draw.update_antennas_position(offset_x, offset_y) + self.canvas.canvas_management.node_drag(self, offset_x, offset_y) new_x, new_y = self.canvas.coords(self.id) @@ -539,5 +570,8 @@ class CanvasNode: old_x, old_y, new_x, new_y, self.wlans ) + def select_multiple(self, event): + self.canvas.canvas_management.node_select(self, True) + def context(self, event): logging.debug(f"context click {self.name}: {event}") diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py index e79e58ac..4a94f573 100644 --- a/coretk/coretk/mobilitynodeconfig.py +++ b/coretk/coretk/mobilitynodeconfig.py @@ -10,6 +10,10 @@ from core.api.grpc import core_pb2 class MobilityNodeConfig: def __init__(self): + """ + create an instance of MobilityConfig object + """ + # dict that maps node id to mobility configuration self.configurations = {} def set_default_configuration(self, node_type, node_id): diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py new file mode 100644 index 00000000..6ce3a6f9 --- /dev/null +++ b/coretk/coretk/nodedelete.py @@ -0,0 +1,61 @@ +""" +manage deletion +""" + + +class CanvasComponentManagement: + def __init__(self, canvas, core): + self.app = core + self.canvas = canvas + + # dictionary that maps node to box + self.selected = {} + + def node_select(self, canvas_node, choose_multiple=False): + """ + create a bounding box when a node is selected + + :param coretk.graph.CanvasNode canvas_node: canvas node object + :return: nothing + """ + + if not choose_multiple: + self.delete_current_bbox() + + # draw a bounding box if node hasn't been selected yet + if canvas_node.id not in self.selected: + x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) + bbox_id = self.canvas.create_rectangle( + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-" + ) + self.selected[canvas_node.id] = bbox_id + + def node_drag(self, canvas_node, offset_x, offset_y): + self.canvas.move(self.selected[canvas_node.id], offset_x, offset_y) + + def delete_current_bbox(self): + for bbid in self.selected.values(): + self.canvas.delete(bbid) + self.selected.clear() + + def delete_selected_nodes(self): + selected_nodes = list(self.selected.keys()) + edges = set() + for n in selected_nodes: + edges = edges.union(self.canvas.nodes[n].edges) + edge_canvas_ids = [x.id for x in edges] + edge_tokens = [x.token for x in edges] + link_infos = [x.link_info.id1 for x in edges] + [x.link_info.id2 for x in edges] + + for i in edge_canvas_ids: + self.canvas.itemconfig(i, state="hidden") + + for i in link_infos: + self.canvas.itemconfig(i, state="hidden") + + for cnid, bbid in self.selected.items(): + self.canvas.itemconfig(cnid, state="hidden") + self.canvas.itemconfig(bbid, state="hidden") + self.canvas.itemconfig(self.canvas.nodes[cnid].text_id, state="hidden") + self.selected.clear() + return selected_nodes, edge_tokens From 4970fb0d5534540d62255d2d03a0053421e285d2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 14:37:08 -0800 Subject: [PATCH 084/305] cleanup for app toolbar, updated toolbar to use grid layout --- coretk/coretk/app.py | 6 +- coretk/coretk/coreclient.py | 18 +- coretk/coretk/coretoolbar.py | 665 ++++++++++++----------------------- coretk/coretk/graph.py | 7 +- 4 files changed, 234 insertions(+), 462 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 9c9c0d2f..faaf1c20 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -50,10 +50,8 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def draw_toolbar(self): - edit_frame = tk.Frame(self) - edit_frame.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) - self.core_editbar = CoreToolbar(self, edit_frame, self.menubar) - self.core_editbar.create_toolbar() + self.core_editbar = CoreToolbar(self, self) + self.core_editbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) def draw_canvas(self): self.canvas = CanvasGraph( diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f4135269..5369b829 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter", "OVS"] +link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] +network_layer_nodes = ["router", "host", "PC", "mdr", "prouter"] class Node: @@ -205,11 +205,9 @@ class CoreClient: # draw tool bar appropritate with session state if session_state == core_pb2.SessionState.RUNTIME: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_runtime_toolbar() + self.app.core_editbar.runtime_frame.tkraise() else: - self.app.core_editbar.destroy_children_widgets() - self.app.core_editbar.create_toolbar() + self.app.core_editbar.design_frame.tkraise() def create_new_session(self): """ @@ -447,26 +445,26 @@ class CoreClient: node_type = core_pb2.NodeType.WIRELESS_LAN elif name == "rj45": node_type = core_pb2.NodeType.RJ45 + elif name == "emane": + node_type = core_pb2.NodeType.EMANE elif name == "tunnel": node_type = core_pb2.NodeType.TUNNEL elif name in network_layer_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name else: - logging.error("grpcmanagemeny.py INVALID node name") + logging.error("invalid node name: %s", name) nid = self.get_id() create_node = Node(session_id, nid, node_type, node_model, x, y, name) # set default configuration for wireless node self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) - # self.core_id_to_canvas_id[nid] = canvas_id logging.debug( - "Adding node to GrpcManager.. Session id: %s, Coords: (%s, %s), Name: %s", + "Adding node to core.. session id: %s, coords: (%s, %s), name: %s", session_id, x, y, diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/coretoolbar.py index aeccb54f..aaed37a9 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/coretoolbar.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from functools import partial from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.dialogs.customnodes import CustomNodesDialog @@ -8,21 +9,20 @@ from coretk.images import ImageEnum, Images from coretk.tooltip import CreateToolTip -class CoreToolbar(object): +class CoreToolbar(tk.Frame): """ Core toolbar class """ - def __init__(self, app, edit_frame, menubar): + def __init__(self, master, app, cnf={}, **kwargs): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ + super().__init__(master, cnf, **kwargs) self.app = app self.master = app.master - self.edit_frame = edit_frame - self.menubar = menubar self.radio_value = tk.IntVar() self.exec_radio_value = tk.IntVar() @@ -30,44 +30,144 @@ class CoreToolbar(object): self.width = 32 self.height = 32 - self.selection_tool_button = None - # Reference to the option menus + self.selection_tool_button = None self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None - self.canvas = None + self.node_button = None + self.network_button = None + self.annotation_button = None - def destroy_previous_frame(self): - """ - Destroy any extra frame from previous before drawing a new one + # frames + self.design_frame = None + self.runtime_frame = None + self.node_picker = None + self.network_picker = None + self.annotation_picker = None - :return: nothing - """ - if ( - self.network_layer_option_menu - and self.network_layer_option_menu.winfo_exists() - ): - self.network_layer_option_menu.destroy() - if self.link_layer_option_menu and self.link_layer_option_menu.winfo_exists(): - self.link_layer_option_menu.destroy() - if self.marker_option_menu and self.marker_option_menu.winfo_exists(): - self.marker_option_menu.destroy() + # draw components + self.draw() - def destroy_children_widgets(self): - """ - Destroy all children of a parent widget + def draw(self): + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.draw_design_frame() + self.draw_runtime_frame() + self.design_frame.tkraise() - :param tkinter.Frame parent: parent frame - :return: nothing - """ + def draw_design_frame(self): + self.design_frame = tk.Frame(self) + self.design_frame.grid(row=0, column=0, sticky="nsew") + self.design_frame.columnconfigure(0, weight=1) - for i in self.edit_frame.winfo_children(): - if i.winfo_name() != "!frame": - i.destroy() + self.create_regular_button( + self.design_frame, + Images.get(ImageEnum.START), + self.click_start_session_tool, + "start the session", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.radio_value, + 1, + "selection tool", + ) + self.create_radio_button( + self.design_frame, + Images.get(ImageEnum.LINK), + self.click_link_tool, + self.radio_value, + 2, + "link tool", + ) + self.create_node_button() + self.create_link_layer_button() + self.create_marker_button() + self.radio_value.set(1) - def create_button(self, img, func, frame, main_button, btt_message): + def draw_runtime_frame(self): + self.runtime_frame = tk.Frame(self) + self.runtime_frame.grid(row=0, column=0, sticky="nsew") + self.runtime_frame.columnconfigure(0, weight=1) + + self.create_regular_button( + self.runtime_frame, + Images.get(ImageEnum.STOP), + self.click_stop_button, + "stop the session", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.SELECT), + self.click_selection_tool, + self.exec_radio_value, + 1, + "selection tool", + ) + self.create_observe_button() + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.PLOT), + self.click_plot_button, + self.exec_radio_value, + 2, + "plot", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.MARKER), + self.click_marker_button, + self.exec_radio_value, + 3, + "marker", + ) + self.create_radio_button( + self.runtime_frame, + Images.get(ImageEnum.TWONODE), + self.click_two_node_button, + self.exec_radio_value, + 4, + "run command from one node to another", + ) + self.create_regular_button( + self.runtime_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" + ) + self.exec_radio_value.set(1) + + def draw_node_picker(self): + self.hide_pickers() + self.node_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.ROUTER, "router"), + (ImageEnum.HOST, "host"), + (ImageEnum.PC, "PC"), + (ImageEnum.MDR, "mdr"), + (ImageEnum.PROUTER, "prouter"), + (ImageEnum.EDITNODE, "custom node types"), + ] + for image_enum, tooltip in nodes: + self.create_button( + Images.get(image_enum), + partial(self.update_button, self.node_button, image_enum, tooltip), + self.node_picker, + tooltip, + ) + self.show_picker(self.node_button, self.node_picker) + + def show_picker(self, button, picker): + first_button = self.winfo_children()[0] + x = button.winfo_rootx() - first_button.winfo_rootx() + 40 + y = button.winfo_rooty() - first_button.winfo_rooty() - 1 + picker.place(x=x, y=y) + self.app.bind_all("", lambda e: self.hide_pickers()) + self.wait_window(picker) + self.app.unbind_all("") + + def create_button(self, img, func, frame, tooltip): """ Create button and put it on the frame @@ -78,9 +178,9 @@ class CoreToolbar(object): :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=img) + button.bind("", lambda e: func()) button.pack(side=tk.LEFT, pady=1) - CreateToolTip(button, btt_message) - button.bind("", lambda mb: func(main_button)) + CreateToolTip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( @@ -93,326 +193,108 @@ class CoreToolbar(object): variable=variable, command=func, ) - button.pack(side=tk.TOP, pady=1) + button.grid() CreateToolTip(button, tooltip_msg) - def create_regular_button(self, frame, image, func, btt_message): + def create_regular_button(self, frame, image, func, tooltip): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) - button.pack(side=tk.TOP, pady=1) - CreateToolTip(button, btt_message) - - def draw_button_menu_frame(self, edit_frame, option_frame, main_button): - """ - Draw option menu frame right next to the main button - - :param tkinter.Frame edit_frame: parent frame of the main button - :param tkinter.Frame option_frame: option frame to draw - :param tkinter.Radiobutton main_button: the main button - :return: nothing - """ - - first_button = edit_frame.winfo_children()[0] - _x = main_button.winfo_rootx() - first_button.winfo_rootx() + 40 - _y = main_button.winfo_rooty() - first_button.winfo_rooty() - 1 - option_frame.place(x=_x, y=_y) - - def bind_widgets_before_frame_hide(self, frame): - """ - Bind the widgets to a left click, when any of the widgets is clicked, the menu option frame is destroyed before - any further action is performed - - :param tkinter.Frame frame: the frame to be destroyed - :return: nothing - """ - self.menubar.bind("", lambda e: frame.destroy()) - self.master.bind("", lambda e: frame.destroy()) - - def unbind_widgets_after_frame_hide(self): - """ - Unbind the widgets to make sure everything works normally again after the menu option frame is destroyed - - :return: nothing - """ - self.master.unbind("") - self.menubar.unbind("Button-1>") + button.grid() + CreateToolTip(button, tooltip) def click_selection_tool(self): - logging.debug("Click SELECTION TOOL") + logging.debug("clicked selection tool") self.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): """ - Start session handler: redraw buttons, send node and link messages to grpc server + Start session handler redraw buttons, send node and link messages to grpc + server. :return: nothing """ - logging.debug("Click START STOP SESSION button") + logging.debug("clicked start button") helper = CoreToolbarHelp(self.app) - self.destroy_children_widgets() self.canvas.mode = GraphMode.SELECT - - # set configuration state - # state = self.canvas.core_grpc.get_session_state() - # if state == core_pb2.SessionState.SHUTDOWN or self.application.is_open_xml: - # self.canvas.core_grpc.set_session_state(SessionStateEnum.DEFINITION.value) - # self.application.is_open_xml = False - # - # self.canvas.core_grpc.set_session_state(SessionStateEnum.CONFIGURATION.value) - # helper.add_nodes() - # helper.add_edges() - # self.canvas.core_grpc.set_session_state(SessionStateEnum.INSTANTIATION.value) helper.gui_start_session() - self.create_runtime_toolbar() - - # for node in self.canvas.grpc_manager.nodes.values(): - # print(node.type, node.model, int(node.x), int(node.y), node.name, node.node_id) - # self.canvas.core_grpc.add_node( - # node.type, node.model, int(node.x), int(node.y), node.name, node.node_id - # ) - - # print(len(self.canvas.grpc_manager.edges)) - # for edge in self.canvas.grpc_manager.edges.values(): - # print(edge.id1, edge.id2, edge.type1, edge.type2) - # self.canvas.core_grpc.add_link( - # edge.id1, edge.id2, edge.type1, edge.type2, edge - # ) - # self.canvas.core_grpc.get_session() - # self.application.is_open_xml = False + self.runtime_frame.tkraise() def click_link_tool(self): logging.debug("Click LINK button") self.canvas.mode = GraphMode.EDGE - def pick_router(self, main_button): - logging.debug("Pick router option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.ROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.ROUTER) - self.canvas.draw_node_name = "router" + def update_button(self, button, image_enum, name): + logging.info("update button(%s): %s, %s", button, image_enum, name) + self.hide_pickers() + if image_enum == ImageEnum.EDITNODE: + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + else: + image = Images.get(image_enum) + logging.info("updating button(%s): %s", button, name) + button.configure(image=image) + self.canvas.mode = GraphMode.NODE + self.canvas.draw_node_image = image + self.canvas.draw_node_name = name - def pick_host(self, main_button): - logging.debug("Pick host option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HOST)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HOST) - self.canvas.draw_node_name = "host" + def hide_pickers(self): + logging.info("hiding pickers") + if self.node_picker: + self.node_picker.destroy() + self.node_picker = None + if self.network_picker: + self.network_picker.destroy() + self.network_picker = None + if self.annotation_picker: + self.annotation_picker.destroy() + self.annotation_picker = None - def pick_pc(self, main_button): - logging.debug("Pick PC option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PC)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PC) - self.canvas.draw_node_name = "PC" - - def pick_mdr(self, main_button): - logging.debug("Pick MDR option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MDR)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.MDR) - self.canvas.draw_node_name = "mdr" - - def pick_prouter(self, main_button): - logging.debug("Pick prouter option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.PROUTER)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.PROUTER) - self.canvas.draw_node_name = "prouter" - - def pick_ovs(self, main_button): - logging.debug("Pick OVS option") - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVS)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.OVS) - self.canvas.draw_node_name = "OVS" - - def pick_editnode(self, main_button): - self.network_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EDITNODE)) - logging.debug("Pick editnode option") - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - - def draw_network_layer_options(self, network_layer_button): - """ - Draw the options for network-layer button - - :param tkinter.Radiobutton network_layer_button: network-layer button - :return: nothing - """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.ROUTER), - Images.get(ImageEnum.HOST), - Images.get(ImageEnum.PC), - Images.get(ImageEnum.MDR), - Images.get(ImageEnum.PROUTER), - Images.get(ImageEnum.OVS), - Images.get(ImageEnum.EDITNODE), - ] - func_list = [ - self.pick_router, - self.pick_host, - self.pick_pc, - self.pick_mdr, - self.pick_prouter, - self.pick_ovs, - self.pick_editnode, - ] - tooltip_list = [ - "router", - "host", - "PC", - "mdr", - "prouter", - "OVS", - "edit node types", - ] - for i in range(len(img_list)): - self.create_button( - img_list[i], - func_list[i], - option_frame, - network_layer_button, - tooltip_list[i], - ) - - # place frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, network_layer_button) - self.network_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() - - def create_network_layer_button(self): + def create_node_button(self): """ Create network layer button :return: nothing """ router_image = Images.get(ImageEnum.ROUTER) - network_layer_button = tk.Radiobutton( - self.edit_frame, + self.node_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=3, width=self.width, height=self.height, image=router_image, - command=lambda: self.draw_network_layer_options(network_layer_button), + command=self.draw_node_picker, ) - network_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(network_layer_button, "Network-layer virtual nodes") + self.node_button.grid() + CreateToolTip(self.node_button, "Network-layer virtual nodes") - def pick_hub(self, main_button): - logging.debug("Pick link-layer node HUB") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.HUB)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.HUB) - self.canvas.draw_node_name = "hub" - - def pick_switch(self, main_button): - logging.debug("Pick link-layer node SWITCH") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.SWITCH)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.SWITCH) - self.canvas.draw_node_name = "switch" - - def pick_wlan(self, main_button): - logging.debug("Pick link-layer node WLAN") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.WLAN)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.WLAN) - self.canvas.draw_node_name = "wlan" - - def pick_rj45(self, main_button): - logging.debug("Pick link-layer node RJ45") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RJ45)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.RJ45) - self.canvas.draw_node_name = "rj45" - - def pick_tunnel(self, main_button): - logging.debug("Pick link-layer node TUNNEL") - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TUNNEL)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.TUNNEL) - self.canvas.draw_node_name = "tunnel" - - def pick_emane(self, main_button): - self.link_layer_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.EMANE)) - self.canvas.mode = GraphMode.PICKNODE - self.canvas.draw_node_image = Images.get(ImageEnum.EMANE) - self.canvas.draw_node_name = "emane" - - def draw_link_layer_options(self, link_layer_button): + def draw_network_picker(self): """ Draw the options for link-layer button :param tkinter.RadioButton link_layer_button: link-layer button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.HUB), - Images.get(ImageEnum.SWITCH), - Images.get(ImageEnum.WLAN), - Images.get(ImageEnum.EMANE), - Images.get(ImageEnum.RJ45), - Images.get(ImageEnum.TUNNEL), + self.hide_pickers() + self.network_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.HUB, "hub", "ethernet hub"), + (ImageEnum.SWITCH, "switch", "ethernet switch"), + (ImageEnum.WLAN, "wlan", "wireless LAN"), + (ImageEnum.EMANE, "emane", "EMANE"), + (ImageEnum.RJ45, "rj45", "rj45 physical interface tool"), + (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] - func_list = [ - self.pick_hub, - self.pick_switch, - self.pick_wlan, - self.pick_emane, - self.pick_rj45, - self.pick_tunnel, - ] - tooltip_list = [ - "ethernet hub", - "ethernet switch", - "wireless LAN", - "emane", - "rj45 physical interface tool", - "tunnel tool", - ] - for i in range(len(img_list)): + for image_enum, name, tooltip in nodes: self.create_button( - img_list[i], - func_list[i], - option_frame, - link_layer_button, - tooltip_list[i], + Images.get(image_enum), + partial(self.update_button, self.network_button, image_enum, name), + self.network_picker, + tooltip, ) - - # place frame at a calculated position as well as keep a reference of the frame - self.draw_button_menu_frame(self.edit_frame, option_frame, link_layer_button) - self.link_layer_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.network_button, self.network_picker) def create_link_layer_button(self): """ @@ -421,75 +303,42 @@ class CoreToolbar(object): :return: nothing """ hub_image = Images.get(ImageEnum.HUB) - link_layer_button = tk.Radiobutton( - self.edit_frame, + self.network_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=4, width=self.width, height=self.height, image=hub_image, - command=lambda: self.draw_link_layer_options(link_layer_button), + command=self.draw_network_picker, ) - link_layer_button.pack(side=tk.TOP, pady=1) - CreateToolTip(link_layer_button, "link-layer nodes") + self.network_button.grid() + CreateToolTip(self.network_button, "link-layer nodes") - def pick_marker(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.MARKER)) - logging.debug("Pick MARKER") - - def pick_oval(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.OVAL)) - logging.debug("Pick OVAL") - - def pick_rectangle(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.RECTANGLE)) - logging.debug("Pick RECTANGLE") - - def pick_text(self, main_button): - self.marker_option_menu.destroy() - main_button.configure(image=Images.get(ImageEnum.TEXT)) - logging.debug("Pick TEXT") - - def draw_marker_options(self, main_button): + def draw_annotation_picker(self): """ Draw the options for marker button :param tkinter.Radiobutton main_button: the main button :return: nothing """ - # create a frame and add buttons to it - self.destroy_previous_frame() - option_frame = tk.Frame(self.master, padx=1, pady=1) - img_list = [ - Images.get(ImageEnum.MARKER), - Images.get(ImageEnum.OVAL), - Images.get(ImageEnum.RECTANGLE), - Images.get(ImageEnum.TEXT), + self.hide_pickers() + self.annotation_picker = tk.Frame(self.master, padx=1, pady=1) + nodes = [ + (ImageEnum.MARKER, "marker"), + (ImageEnum.OVAL, "oval"), + (ImageEnum.RECTANGLE, "rectangle"), + (ImageEnum.TEXT, "text"), ] - func_list = [ - self.pick_marker, - self.pick_oval, - self.pick_rectangle, - self.pick_text, - ] - tooltip_list = ["marker", "oval", "rectangle", "text"] - for i in range(len(img_list)): + for image_enum, tooltip in nodes: self.create_button( - img_list[i], func_list[i], option_frame, main_button, tooltip_list[i] + Images.get(image_enum), + partial(self.update_annotation, image_enum), + self.annotation_picker, + tooltip, ) - - # place the frame at a calculated position as well as keep a reference of that frame - self.draw_button_menu_frame(self.edit_frame, option_frame, main_button) - self.marker_option_menu = option_frame - - # destroy the frame before any further actions on other widgets - self.bind_widgets_before_frame_hide(option_frame) - option_frame.wait_window(option_frame) - self.unbind_widgets_after_frame_hide() + self.show_picker(self.annotation_button, self.annotation_picker) def create_marker_button(self): """ @@ -498,55 +347,22 @@ class CoreToolbar(object): :return: nothing """ marker_image = Images.get(ImageEnum.MARKER) - marker_main_button = tk.Radiobutton( - self.edit_frame, + self.annotation_button = tk.Radiobutton( + self.design_frame, indicatoron=False, variable=self.radio_value, value=5, width=self.width, height=self.height, image=marker_image, - command=lambda: self.draw_marker_options(marker_main_button), + command=self.draw_annotation_picker, ) - marker_main_button.pack(side=tk.TOP, pady=1) - CreateToolTip(marker_main_button, "background annotation tools") - - def create_toolbar(self): - """ - Create buttons for toolbar in edit mode - - :return: nothing - """ - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.START), - self.click_start_session_tool, - "start the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.radio_value, - 1, - "selection tool", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.LINK), - self.click_link_tool, - self.radio_value, - 2, - "link tool", - ) - self.create_network_layer_button() - self.create_link_layer_button() - self.create_marker_button() - self.radio_value.set(1) + self.annotation_button.grid() + CreateToolTip(self.annotation_button, "background annotation tools") def create_observe_button(self): menu_button = tk.Menubutton( - self.edit_frame, + self.runtime_frame, image=Images.get(ImageEnum.OBSERVE), width=self.width, height=self.height, @@ -555,7 +371,7 @@ class CoreToolbar(object): ) menu_button.menu = tk.Menu(menu_button, tearoff=0) menu_button["menu"] = menu_button.menu - menu_button.pack(side=tk.TOP, pady=1) + menu_button.grid() menu_button.menu.add_command(label="None") menu_button.menu.add_command(label="processes") @@ -581,9 +397,13 @@ class CoreToolbar(object): :return: nothing """ logging.debug("Click on STOP button ") - self.destroy_children_widgets() self.app.core.stop_session() - self.create_toolbar() + self.design_frame.tkraise() + + def update_annotation(self, image_enum): + logging.info("clicked annotation: ") + self.hide_pickers() + self.annotation_button.configure(image=Images.get(image_enum)) def click_run_button(self): logging.debug("Click on RUN button") @@ -596,48 +416,3 @@ class CoreToolbar(object): def click_two_node_button(self): logging.debug("Click TWONODE button") - - def create_runtime_toolbar(self): - self.create_regular_button( - self.edit_frame, - Images.get(ImageEnum.STOP), - self.click_stop_button, - "stop the session", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.exec_radio_value, - 1, - "selection tool", - ) - self.create_observe_button() - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.PLOT), - self.click_plot_button, - self.exec_radio_value, - 2, - "plot", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.MARKER), - self.click_marker_button, - self.exec_radio_value, - 3, - "marker", - ) - self.create_radio_button( - self.edit_frame, - Images.get(ImageEnum.TWONODE), - self.click_two_node_button, - self.exec_radio_value, - 4, - "run command from one node to another", - ) - self.create_regular_button( - self.edit_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" - ) - self.exec_radio_value.set(1) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 448a6f1d..eb2362c6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -272,7 +272,7 @@ class CanvasGraph(tk.Canvas): else: self.focus_set() self.selected = self.get_selected(event) - logging.debug(f"click release selected: {self.selected}") + logging.debug(f"click release selected({self.selected}) mode({self.mode})") if self.mode == GraphMode.EDGE: self.handle_edge_release(event) elif self.mode == GraphMode.NODE: @@ -379,6 +379,7 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] + logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: node = CanvasNode( x=x, @@ -498,12 +499,12 @@ class CanvasNode: self.x_coord, self.y_coord = self.canvas.coords(self.id) def click_press(self, event): - logging.debug(f"click press {self.name}: {event}") + logging.debug(f"node click press {self.name}: {event}") self.moving = self.canvas.canvas_xy(event) # return "break" def click_release(self, event): - logging.debug(f"click release {self.name}: {event}") + logging.debug(f"node click release {self.name}: {event}") self.update_coords() self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None From 707201ce54f791a2093ffb7aa1d4af4ac707feff Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:19:01 -0800 Subject: [PATCH 085/305] work on node deletion --- coretk/coretk/coreclient.py | 48 ++++++++++++++--------------- coretk/coretk/coretoolbarhelp.py | 5 ++- coretk/coretk/dialogs/wlanconfig.py | 2 +- coretk/coretk/graph.py | 22 ++++++++++--- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ae03922b..318ccb99 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -308,7 +308,6 @@ class CoreClient: logging.info("delete nodes %s", response) def delete_links(self, delete_session=None): - # sid = None if delete_session is None: sid = self.session_id else: @@ -430,23 +429,6 @@ class CoreClient: else: return self.reusable.pop(0) - # def add_node(self, node_type, model, x, y, name, node_id): - # position = core_pb2.Position(x=x, y=y) - # node = core_pb2.Node(id=node_id, type=node_type, position=position, model=model) - # self.node_ids.append(node_id) - # response = self.client.add_node(self.session_id, node) - # logging.info("created node: %s", response) - # if node_type == core_pb2.NodeType.WIRELESS_LAN: - # d = OrderedDict() - # d["basic_range"] = "275" - # d["bandwidth"] = "54000000" - # d["jitter"] = "0" - # d["delay"] = "20000" - # d["error"] = "0" - # r = self.client.set_wlan_config(self.session_id, node_id, d) - # logging.debug("set wlan config %s", r) - # return response.node_id - def add_graph_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -510,12 +492,13 @@ class CoreClient: """ # keep reference to the core ids core_node_ids = [self.nodes[x].node_id for x in canvas_ids] + node_interface_pairs = [] # delete the nodes for i in canvas_ids: try: - self.nodes.pop(i) - self.reusable.append(i) + n = self.nodes.pop(i) + self.reusable.append(n.node_id) except KeyError: logging.error("coreclient.py INVALID NODE CANVAS ID") @@ -524,16 +507,33 @@ class CoreClient: # delete the edges and interfaces for i in tokens: try: - self.edges.pop(i) + e = self.edges.pop(i) + if e.interface_1 is not None: + node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) + if e.interface_2 is not None: + node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) + except KeyError: logging.error("coreclient.py invalid edge token ") - # delete any configurations + # delete global emane config if there no longer exist any emane cloud + if core_pb2.NodeType.EMANE not in [x.type for x in self.nodes.values()]: + self.emane_config = None + + # delete any mobility configuration, wlan configuration for i in core_node_ids: if i in self.mobilityconfig_management.configurations: - self.mobilityconfig_management.pop(i) + self.mobilityconfig_management.configurations.pop(i) if i in self.wlanconfig_management.configurations: - self.wlanconfig_management.pop(i) + self.wlanconfig_management.configurations.pop(i) + + # delete emane configurations + for i in node_interface_pairs: + if i in self.emaneconfig_management.configurations: + self.emaneconfig_management.configurations.pop(i) + for i in core_node_ids: + if tuple([i, None]) in self.emaneconfig_management.configurations: + self.emaneconfig_management.configurations.pop(tuple([i, None])) def add_preexisting_node(self, canvas_node, session_id, core_node, name): """ diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/coretoolbarhelp.py index 5192f2c5..c43a3c14 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/coretoolbarhelp.py @@ -97,7 +97,10 @@ class CoreToolbarHelp: # get emane config (global configuration) pb_emane_config = self.app.core.emane_config - emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + if pb_emane_config is not None: + emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} + else: + emane_config = None # get emane configuration list emane_model_configs = self.get_emane_configuration_list() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d57c8935..ecd2610b 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -24,7 +24,7 @@ class WlanConfigDialog(Dialog): self.config = config self.name = tk.StringVar(value=canvas_node.name) - self.range_var = tk.StringVar(value=config["basic_range"]) + self.range_var = tk.StringVar(value=config["range"]) self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) self.delay_var = tk.StringVar(value=config["delay"]) self.loss_var = tk.StringVar(value=config["error"]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 9ab6b956..af769766 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -383,7 +383,14 @@ class CanvasGraph(tk.Canvas): self.node_context.unpost() self.is_node_context_opened = False + # TODO rather than delete, might move the data to somewhere else in order to reuse + # TODO when the user undo def press_delete(self, event): + """ + delete selected nodes and any data that relates to it + :param event: + :return: + """ # hide nodes, links, link information that shows on the GUI to_delete_nodes, to_delete_edge_tokens = ( self.canvas_management.delete_selected_nodes() @@ -395,12 +402,17 @@ class CanvasGraph(tk.Canvas): for token in to_delete_edge_tokens: self.edges.pop(token) + # delete the edge data inside of canvas node + canvas_node_link_to_delete = [] + for canvas_id, node in self.nodes.items(): + for e in node.edges: + if e.token in to_delete_edge_tokens: + canvas_node_link_to_delete.append(tuple([canvas_id, e])) + for nid, edge in canvas_node_link_to_delete: + self.nodes[nid].edges.remove(edge) + + # delete the related data from core self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) - # delete any configuration related to the links and nodes - - # delete selected node - - # delete links connected to the selected nodes def add_node(self, x, y, image, node_name): plot_id = self.find_all()[0] From 173747fd138c9caf90a20a338679ca456303b1d8 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 15:58:02 -0800 Subject: [PATCH 086/305] renamed coretoolbar to just toolbar --- coretk/coretk/app.py | 19 +++++++++++-------- coretk/coretk/coreclient.py | 4 ++-- coretk/coretk/menuaction.py | 4 ---- coretk/coretk/{coretoolbar.py => toolbar.py} | 6 +++--- .../{coretoolbarhelp.py => toolbarhelper.py} | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) rename coretk/coretk/{coretoolbar.py => toolbar.py} (99%) rename coretk/coretk/{coretoolbarhelp.py => toolbarhelper.py} (99%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index faaf1c20..a53b0f8b 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -4,10 +4,10 @@ import tkinter as tk from coretk import appdirs from coretk.coreclient import CoreClient from coretk.coremenubar import CoreMenubar -from coretk.coretoolbar import CoreToolbar from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction +from coretk.toolbar import Toolbar class Application(tk.Frame): @@ -17,7 +17,7 @@ class Application(tk.Frame): self.menubar = None self.core_menu = None self.canvas = None - self.core_editbar = None + self.toolbar = None self.is_open_xml = False self.size_and_scale = None self.set_wallpaper = None @@ -29,9 +29,7 @@ class Application(tk.Frame): self.config = appdirs.read_config() self.core = CoreClient(self) self.setup_app() - self.draw_menu() - self.draw_toolbar() - self.draw_canvas() + self.draw() self.core.set_up() def setup_app(self): @@ -42,6 +40,11 @@ class Application(tk.Frame): self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def draw(self): + self.draw_menu() + self.draw_toolbar() + self.draw_canvas() + def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = tk.Menu(self.master) @@ -50,8 +53,8 @@ class Application(tk.Frame): self.master.config(menu=self.menubar) def draw_toolbar(self): - self.core_editbar = CoreToolbar(self, self) - self.core_editbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.toolbar = Toolbar(self, self) + self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) def draw_canvas(self): self.canvas = CanvasGraph( @@ -59,7 +62,7 @@ class Application(tk.Frame): ) self.canvas.pack(fill=tk.BOTH, expand=True) - self.core_editbar.canvas = self.canvas + self.toolbar.canvas = self.canvas scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 564c13fe..63816cb4 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -208,9 +208,9 @@ class CoreClient: # draw tool bar appropritate with session state if session_state == core_pb2.SessionState.RUNTIME: - self.app.core_editbar.runtime_frame.tkraise() + self.app.toolbar.runtime_frame.tkraise() else: - self.app.core_editbar.design_frame.tkraise() + self.app.toolbar.design_frame.tkraise() def create_new_session(self): """ diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 391cf765..12f81559 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -367,10 +367,6 @@ class MenuAction: self.prompt_save_running_session() self.app.core.open_xml(file_path) - # Todo might not need - # self.application.core_editbar.destroy_children_widgets() - # self.application.core_editbar.create_toolbar() - def canvas_size_and_scale(self): dialog = SizeAndScaleDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/coretoolbar.py b/coretk/coretk/toolbar.py similarity index 99% rename from coretk/coretk/coretoolbar.py rename to coretk/coretk/toolbar.py index aaed37a9..c97a326f 100644 --- a/coretk/coretk/coretoolbar.py +++ b/coretk/coretk/toolbar.py @@ -2,14 +2,14 @@ import logging import tkinter as tk from functools import partial -from coretk.coretoolbarhelp import CoreToolbarHelp from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images +from coretk.toolbarhelper import ToolbarHelper from coretk.tooltip import CreateToolTip -class CoreToolbar(tk.Frame): +class Toolbar(tk.Frame): """ Core toolbar class """ @@ -215,8 +215,8 @@ class CoreToolbar(tk.Frame): :return: nothing """ logging.debug("clicked start button") - helper = CoreToolbarHelp(self.app) self.canvas.mode = GraphMode.SELECT + helper = ToolbarHelper(self.app) helper.gui_start_session() self.runtime_frame.tkraise() diff --git a/coretk/coretk/coretoolbarhelp.py b/coretk/coretk/toolbarhelper.py similarity index 99% rename from coretk/coretk/coretoolbarhelp.py rename to coretk/coretk/toolbarhelper.py index c650a5bc..80b31506 100644 --- a/coretk/coretk/coretoolbarhelp.py +++ b/coretk/coretk/toolbarhelper.py @@ -4,7 +4,7 @@ CoreToolbar help to draw on canvas, and make grpc client call from core.api.grpc.client import core_pb2 -class CoreToolbarHelp: +class ToolbarHelper: def __init__(self, app): self.app = app From c4d2ae599bea53535872ab7d103d63a494de0351 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 16:15:29 -0800 Subject: [PATCH 087/305] removed toolbarhelper, moved logic into coreclient, updated start session click --- coretk/coretk/coreclient.py | 78 +++++++++++++++++++---- coretk/coretk/toolbar.py | 4 +- coretk/coretk/toolbaraction.py | 17 ----- coretk/coretk/toolbarhelper.py | 112 --------------------------------- 4 files changed, 66 insertions(+), 145 deletions(-) delete mode 100644 coretk/coretk/toolbaraction.py delete mode 100644 coretk/coretk/toolbarhelper.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 63816cb4..da75d3bd 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -305,23 +305,19 @@ class CoreClient: ) logging.info("delete links %s", response) - # TODO add location, hooks, emane_config, etc... - def start_session( - self, - nodes, - links, - location=None, - hooks=None, - emane_config=None, - emane_model_configs=None, - wlan_configs=None, - mobility_configs=None, - ): + def start_session(self): + nodes = self.get_nodes_proto() + links = self.get_links_proto() + wlan_configs = self.get_wlan_configs_proto() + mobility_configs = self.get_mobility_configs_proto() + emane_model_configs = self.get_emane_model_configs_proto() + hooks = list(self.hooks.values()) + emane_config = {x: self.emane_config[x].value for x in self.emane_config} response = self.client.start_session( self.session_id, nodes, links, - hooks=list(self.hooks.values()), + hooks=hooks, wlan_configs=wlan_configs, emane_config=emane_config, emane_model_configs=emane_model_configs, @@ -675,3 +671,59 @@ class CoreClient: else: logging.error("grpcmanagement.py INVALID CANVAS NODE ID") + + def get_nodes_proto(self): + nodes = [] + for node in self.nodes.values(): + pos = core_pb2.Position(x=int(node.x), y=int(node.y)) + proto_node = core_pb2.Node( + id=node.node_id, type=node.type, position=pos, model=node.model + ) + nodes.append(proto_node) + return nodes + + def get_links_proto(self): + links = [] + for edge in self.edges.values(): + interface_one = self.create_interface(edge.type1, edge.interface_1) + interface_two = self.create_interface(edge.type2, edge.interface_2) + link = core_pb2.Link( + node_one_id=edge.id1, + node_two_id=edge.id2, + type=core_pb2.LinkType.WIRED, + interface_one=interface_one, + interface_two=interface_two, + ) + links.append(link) + return links + + def get_wlan_configs_proto(self): + configs = [] + wlan_configs = self.wlanconfig_management.configurations + for node_id in wlan_configs: + config = wlan_configs[node_id] + config_proto = core_pb2.WlanConfig(node_id=node_id, config=config) + configs.append(config_proto) + return configs + + def get_mobility_configs_proto(self): + configs = [] + mobility_configs = self.mobilityconfig_management.configurations + for node_id in mobility_configs: + config = mobility_configs[node_id] + config_proto = core_pb2.MobilityConfig(node_id=node_id, config=config) + configs.append(config_proto) + return configs + + def get_emane_model_configs_proto(self): + configs = [] + emane_configs = self.emaneconfig_management.configurations + for key, value in emane_configs.items(): + node_id, interface_id = key + model, options = value + config = {x: options[x].value for x in options} + config_proto = core_pb2.EmaneModelConfig( + node_id=node_id, interface_id=interface_id, model=model, config=config + ) + configs.append(config_proto) + return configs diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c97a326f..ace49796 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,6 @@ from functools import partial from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images -from coretk.toolbarhelper import ToolbarHelper from coretk.tooltip import CreateToolTip @@ -216,8 +215,7 @@ class Toolbar(tk.Frame): """ logging.debug("clicked start button") self.canvas.mode = GraphMode.SELECT - helper = ToolbarHelper(self.app) - helper.gui_start_session() + self.app.core.start_session() self.runtime_frame.tkraise() def click_link_tool(self): diff --git a/coretk/coretk/toolbaraction.py b/coretk/coretk/toolbaraction.py deleted file mode 100644 index bbbc689b..00000000 --- a/coretk/coretk/toolbaraction.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Python file that store button actions -""" - -import logging - - -def click_selection_tool(): - logging.debug("Click SELECTION TOOL") - - -def click_start_stop_session_tool(): - logging.debug("Click START STOP SELECTION TOOL") - - -def click_link_tool(): - logging.debug("Click LINK TOOL") diff --git a/coretk/coretk/toolbarhelper.py b/coretk/coretk/toolbarhelper.py deleted file mode 100644 index 80b31506..00000000 --- a/coretk/coretk/toolbarhelper.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -CoreToolbar help to draw on canvas, and make grpc client call -""" -from core.api.grpc.client import core_pb2 - - -class ToolbarHelper: - def __init__(self, app): - self.app = app - - def get_node_list(self): - """ - form a list node protobuf nodes to pass in start_session in grpc - - :return: nothing - """ - nodes = [] - for node in self.app.core.nodes.values(): - pos = core_pb2.Position(x=int(node.x), y=int(node.y)) - n = core_pb2.Node( - id=node.node_id, type=node.type, position=pos, model=node.model - ) - nodes.append(n) - return nodes - - def get_link_list(self): - """ - form a list of links to pass into grpc start session - - :rtype: list(core_pb2.Link) - :return: list of protobuf links - """ - links = [] - for edge in self.app.core.edges.values(): - interface_one = self.app.core.create_interface(edge.type1, edge.interface_1) - interface_two = self.app.core.create_interface(edge.type2, edge.interface_2) - link = core_pb2.Link( - node_one_id=edge.id1, - node_two_id=edge.id2, - type=core_pb2.LinkType.WIRED, - interface_one=interface_one, - interface_two=interface_two, - ) - links.append(link) - - return links - - def get_wlan_configuration_list(self): - """ - form a list of wlan configuration to pass to start_session - - :return: nothing - """ - configs = [] - manager_configs = self.app.core.wlanconfig_management.configurations - for key in manager_configs: - cnf = core_pb2.WlanConfig(node_id=key, config=manager_configs[key]) - configs.append(cnf) - return configs - - def get_mobility_configuration_list(self): - """ - form a list of mobility configuration to pass to start_session - - :return: nothing - """ - configs = [] - core = self.app.canvas.core - manager_configs = core.mobilityconfig_management.configurations - for key in manager_configs: - cnf = core_pb2.MobilityConfig(node_id=key, config=manager_configs[key]) - configs.append(cnf) - return configs - - def get_emane_configuration_list(self): - """ - form a list of emane configuration for the nodes - - :return: nothing - """ - configs = [] - manager_configs = self.app.core.emaneconfig_management.configurations - for key, value in manager_configs.items(): - config = {x: value[1][x].value for x in value[1]} - configs.append( - core_pb2.EmaneModelConfig( - node_id=key[0], interface_id=key[1], model=value[0], config=config - ) - ) - return configs - - def gui_start_session(self): - nodes = self.get_node_list() - links = self.get_link_list() - wlan_configs = self.get_wlan_configuration_list() - mobility_configs = self.get_mobility_configuration_list() - - # get emane config (global configuration) - pb_emane_config = self.app.core.emane_config - emane_config = {x: pb_emane_config[x].value for x in pb_emane_config} - - # get emane configuration list - emane_model_configs = self.get_emane_configuration_list() - - self.app.core.start_session( - nodes, - links, - wlan_configs=wlan_configs, - mobility_configs=mobility_configs, - emane_config=emane_config, - emane_model_configs=emane_model_configs, - ) From 99678499cec107b41aad7945db8961ea03fd69a2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 21:46:40 -0800 Subject: [PATCH 088/305] some refactoring for menubar code --- coretk/coretk/app.py | 40 ++-- coretk/coretk/menuaction.py | 110 ++------- coretk/coretk/{coremenubar.py => menubar.py} | 238 ++++++------------- coretk/coretk/toolbar.py | 13 +- 4 files changed, 115 insertions(+), 286 deletions(-) rename coretk/coretk/{coremenubar.py => menubar.py} (79%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index a53b0f8b..4bb23bc0 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -3,10 +3,10 @@ import tkinter as tk from coretk import appdirs from coretk.coreclient import CoreClient -from coretk.coremenubar import CoreMenubar from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction +from coretk.menubar import Menubar from coretk.toolbar import Toolbar @@ -15,9 +15,9 @@ class Application(tk.Frame): super().__init__(master) Images.load_all() self.menubar = None - self.core_menu = None - self.canvas = None self.toolbar = None + self.canvas = None + self.statusbar = None self.is_open_xml = False self.size_and_scale = None self.set_wallpaper = None @@ -41,29 +41,18 @@ class Application(tk.Frame): self.pack(fill=tk.BOTH, expand=True) def draw(self): - self.draw_menu() - self.draw_toolbar() - self.draw_canvas() - - def draw_menu(self): self.master.option_add("*tearOff", tk.FALSE) - self.menubar = tk.Menu(self.master) - self.core_menu = CoreMenubar(self, self.master, self.menubar) - self.core_menu.create_core_menubar() - self.master.config(menu=self.menubar) - - def draw_toolbar(self): + self.menubar = Menubar(self.master, self) self.toolbar = Toolbar(self, self) self.toolbar.pack(side=tk.LEFT, fill=tk.Y, ipadx=2, ipady=2) + self.draw_canvas() + self.draw_status() def draw_canvas(self): self.canvas = CanvasGraph( self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - - self.toolbar.canvas = self.canvas - scroll_x = tk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview ) @@ -73,14 +62,15 @@ class Application(tk.Frame): 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) + def draw_status(self): + self.statusbar = tk.Frame(self) + self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) + button = tk.Button(self.statusbar, text="Button 1") + button.pack(side=tk.LEFT, padx=1) + button = tk.Button(self.statusbar, text="Button 2") + button.pack(side=tk.LEFT, padx=1) + button = tk.Button(self.statusbar, text="Button 3") + button.pack(side=tk.LEFT, padx=1) def on_closing(self): menu_action = MenuAction(self, self.master) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 12f81559..88f32bf4 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -21,34 +21,14 @@ def sub_menu_items(): logging.debug("Click on sub menu items") -def file_new(): +def file_new(event=None): logging.debug("Click file New") -def file_new_shortcut(event): - logging.debug("Shortcut for file new shortcut") - - -def file_open(): - logging.debug("Click file Open") - - -def file_open_shortcut(event): - logging.debug("Shortcut for file open") - - def file_reload(): logging.debug("Click file Reload") -# def file_save(): -# logging.debug("Click file save") - - -def file_save_shortcut(event): - logging.debug("Shortcut for file save") - - def file_export_python_script(): logging.debug("Click file export python script") @@ -73,70 +53,38 @@ def file_save_screenshot(): logging.debug("Click file save screenshot") -def edit_undo(): +def edit_undo(event=None): logging.debug("Click edit undo") -def edit_undo_shortcut(event): - logging.debug("Shortcut for edit undo") - - -def edit_redo(): +def edit_redo(event=None): logging.debug("Click edit redo") -def edit_redo_shortcut(event): - logging.debug("Shortcut for edit redo") - - -def edit_cut(): +def edit_cut(event=None): logging.debug("Click edit cut") -def edit_cut_shortcut(event): - logging.debug("Shortcut for edit cut") - - -def edit_copy(): +def edit_copy(event=None): logging.debug("Click edit copy") -def edit_copy_shortcut(event): - logging.debug("Shortcut for edit copy") - - -def edit_paste(): +def edit_paste(event=None): logging.debug("Click edit paste") -def edit_paste_shortcut(event): - logging.debug("Shortcut for edit paste") - - -def edit_select_all(): +def edit_select_all(event=None): logging.debug("Click edit select all") -def edit_select_all_shortcut(event): - logging.debug("Shortcut for edit select all") - - -def edit_select_adjacent(): +def edit_select_adjacent(event=None): logging.debug("Click edit select adjacent") -def edit_select_adjacent_shortcut(event): - logging.debug("Shortcut for edit select adjacent") - - -def edit_find(): +def edit_find(event=None): logging.debug("CLick edit find") -def edit_find_shortcut(event): - logging.debug("Shortcut for edit find") - - def edit_clear_marker(): logging.debug("Click edit clear marker") @@ -157,38 +105,22 @@ def canvas_delete(): logging.debug("Click canvas delete") -def canvas_previous(): +def canvas_previous(event=None): logging.debug("Click canvas previous") -def canvas_previous_shortcut(event): - logging.debug("Shortcut for canvas previous") - - -def canvas_next(): +def canvas_next(event=None): logging.debug("Click canvas next") -def canvas_next_shortcut(event): - logging.debug("Shortcut for canvas next") - - -def canvas_first(): +def canvas_first(event=None): logging.debug("CLick canvas first") -def canvas_first_shortcut(event): - logging.debug("Shortcut for canvas first") - - -def canvas_last(): +def canvas_last(event=None): logging.debug("CLick canvas last") -def canvas_last_shortcut(event): - logging.debug("Shortcut canvas last") - - def view_show(): logging.debug("Click view show") @@ -205,22 +137,14 @@ def view_3d_gui(): logging.debug("CLick view 3D GUI") -def view_zoom_in(): +def view_zoom_in(event=None): logging.debug("Click view zoom in") -def view_zoom_in_shortcut(event): - logging.debug("Shortcut view zoom in") - - -def view_zoom_out(): +def view_zoom_out(event=None): logging.debug("Click view zoom out") -def view_zoom_out_shortcut(event): - logging.debug("Shortcut view zoom out") - - def tools_auto_rearrange_all(): logging.debug("Click tools, auto rearrange all") @@ -343,7 +267,7 @@ class MenuAction: self.prompt_save_running_session() self.app.quit() - def file_save_as_xml(self): + def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( initialdir=str(XML_PATH), @@ -354,7 +278,7 @@ class MenuAction: if file_path: self.app.core.save_xml(file_path) - def file_open_xml(self): + def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") self.app.is_open_xml = True file_path = filedialog.askopenfilename( diff --git a/coretk/coretk/coremenubar.py b/coretk/coretk/menubar.py similarity index 79% rename from coretk/coretk/coremenubar.py rename to coretk/coretk/menubar.py index f720b093..143833b7 100644 --- a/coretk/coretk/coremenubar.py +++ b/coretk/coretk/menubar.py @@ -1,15 +1,14 @@ import tkinter as tk import coretk.menuaction as action -from coretk.menuaction import MenuAction -class CoreMenubar(object): +class Menubar(tk.Menu): """ Core menubar """ - def __init__(self, app, master, menubar): + def __init__(self, master, app, cnf={}, **kwargs): """ Create a CoreMenubar instance @@ -17,63 +16,54 @@ class CoreMenubar(object): :param tkinter.Menu menubar: menubar object :param coretk.app.Application app: application object """ - self.menubar = menubar - self.master = master + super().__init__(master, cnf, **kwargs) + self.master.config(menu=self) self.app = app self.menuaction = action.MenuAction(app, master) - self.menu_action = MenuAction(self.app, self.master) + self.draw() - # def on_quit(self): - # """ - # Prompt use to stop running session before application is closed - # - # :return: nothing - # """ - # state = self.application.core_grpc.get_session_state() - # - # if state == core_pb2.SessionState.SHUTDOWN or state == core_pb2.SessionState.DEFINITION: - # self.application.core_grpc.delete_session() - # self.application.core_grpc.core.close() - # # self.application.quit() - # else: - # msgbox = tk.messagebox.askyesnocancel("stop", "Stop the running session?") - # - # if msgbox or msgbox == False: - # if msgbox: - # self.application.core_grpc.set_session_state("datacollect") - # self.application.core_grpc.delete_links() - # self.application.core_grpc.delete_nodes() - # self.application.core_grpc.delete_session() - # - # self.application.core_grpc.core.close() - # # self.application.quit() + def draw(self): + """ + Create core menubar and bind the hot keys to their matching command - def create_file_menu(self): + :return: nothing + """ + self.draw_file_menu() + self.draw_edit_menu() + self.draw_canvas_menu() + self.draw_view_menu() + self.draw_tools_menu() + self.draw_widgets_menu() + self.draw_session_menu() + self.draw_help_menu() + + def draw_file_menu(self): """ Create file menu :return: nothing """ - file_menu = tk.Menu(self.menubar) - # menu_action = MenuAction(self.application, self.master) + file_menu = tk.Menu(self) file_menu.add_command( - label="New", command=action.file_new, accelerator="Ctrl+N", underline=0 + label="New Session", + command=action.file_new, + accelerator="Ctrl+N", + underline=0, ) + self.app.bind_all("", action.file_new) file_menu.add_command( label="Open...", - command=self.menu_action.file_open_xml, + command=self.menuaction.file_open_xml, accelerator="Ctrl+O", underline=0, ) + self.app.bind_all("", self.menuaction.file_open_xml) file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - # file_menu.add_command( - # label="Save", command=action.file_save, accelerator="Ctrl+S", underline=0 - # ) - # file_menu.add_command(label="Save As XML...", command=action.file_save_as_xml) - file_menu.add_command(label="Save", command=self.menu_action.file_save_as_xml) - + file_menu.add_command( + label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml + ) + self.app.bind_all("", self.menuaction.file_save_as_xml) file_menu.add_separator() - file_menu.add_command( label="Export Python script...", command=action.file_export_python_script ) @@ -85,9 +75,7 @@ class CoreMenubar(object): label="Execute Python script with options...", command=action.file_execute_python_script_with_options, ) - file_menu.add_separator() - file_menu.add_command( label="Open current file in editor...", command=action.file_open_current_file_in_editor, @@ -96,97 +84,95 @@ class CoreMenubar(object): file_menu.add_command( label="Save screenshot...", command=action.file_save_screenshot ) - file_menu.add_separator() - file_menu.add_command( label="Quit", command=self.menuaction.on_quit, underline=0 ) - self.menubar.add_cascade(label="File", menu=file_menu, underline=0) + self.add_cascade(label="File", menu=file_menu, underline=0) - def create_edit_menu(self): + def draw_edit_menu(self): """ Create edit menu :return: nothing """ - edit_menu = tk.Menu(self.menubar) + edit_menu = tk.Menu(self) edit_menu.add_command( label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 ) + self.app.bind_all("", action.edit_undo) edit_menu.add_command( label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 ) - + self.app.bind_all("", action.edit_redo) edit_menu.add_separator() - edit_menu.add_command( label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 ) + self.app.bind_all("", action.edit_cut) edit_menu.add_command( label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 ) + self.app.bind_all("", action.edit_copy) edit_menu.add_command( label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 ) - + self.app.bind_all("", action.edit_paste) edit_menu.add_separator() - edit_menu.add_command( label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" ) + self.app.bind_all("", action.edit_select_all) edit_menu.add_command( label="Select Adjacent", command=action.edit_select_adjacent, accelerator="Ctrl+J", ) - + self.app.bind_all("", action.edit_select_adjacent) edit_menu.add_separator() - edit_menu.add_command( label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 ) + self.app.bind_all("", action.edit_find) edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) edit_menu.add_command(label="Preferences...", command=action.edit_preferences) + self.add_cascade(label="Edit", menu=edit_menu, underline=0) - self.menubar.add_cascade(label="Edit", menu=edit_menu, underline=0) - - def create_canvas_menu(self): + def draw_canvas_menu(self): """ Create canvas menu :return: nothing """ - canvas_menu = tk.Menu(self.menubar) + canvas_menu = tk.Menu(self) canvas_menu.add_command(label="New", command=action.canvas_new) canvas_menu.add_command(label="Manage...", command=action.canvas_manage) canvas_menu.add_command(label="Delete", command=action.canvas_delete) - canvas_menu.add_separator() - canvas_menu.add_command( - label="Size/scale...", command=self.menu_action.canvas_size_and_scale + label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) canvas_menu.add_command( - label="Wallpaper...", command=self.menu_action.canvas_set_wallpaper + label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) - canvas_menu.add_separator() - canvas_menu.add_command( label="Previous", command=action.canvas_previous, accelerator="PgUp" ) + self.app.bind_all("", action.canvas_previous) canvas_menu.add_command( label="Next", command=action.canvas_next, accelerator="PgDown" ) + self.app.bind_all("", action.canvas_next) canvas_menu.add_command( label="First", command=action.canvas_first, accelerator="Home" ) + self.app.bind_all("", action.canvas_first) canvas_menu.add_command( label="Last", command=action.canvas_last, accelerator="End" ) - - self.menubar.add_cascade(label="Canvas", menu=canvas_menu, underline=0) + self.app.bind_all("", action.canvas_last) + self.add_cascade(label="Canvas", menu=canvas_menu, underline=0) def create_show_menu(self, view_menu): """ @@ -206,33 +192,31 @@ class CoreMenubar(object): show_menu.add_command(label="Annotations", command=action.sub_menu_items) show_menu.add_command(label="Grid", command=action.sub_menu_items) show_menu.add_command(label="API Messages", command=action.sub_menu_items) - view_menu.add_cascade(label="Show", menu=show_menu) - def create_view_menu(self): + def draw_view_menu(self): """ Create view menu :return: nothing """ - view_menu = tk.Menu(self.menubar) + view_menu = tk.Menu(self) self.create_show_menu(view_menu) view_menu.add_command( label="Show hidden nodes", command=action.view_show_hidden_nodes ) view_menu.add_command(label="Locked", command=action.view_locked) view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) - view_menu.add_separator() - view_menu.add_command( label="Zoom in", command=action.view_zoom_in, accelerator="+" ) + self.app.bind_all("", action.view_zoom_in) view_menu.add_command( label="Zoom out", command=action.view_zoom_out, accelerator="-" ) - - self.menubar.add_cascade(label="View", menu=view_menu, underline=0) + self.app.bind_all("", action.view_zoom_out) + self.add_cascade(label="View", menu=view_menu, underline=0) def create_experimental_menu(self, tools_menu): """ @@ -251,7 +235,6 @@ class CoreMenubar(object): experimental_menu.add_command( label="Topology partitioning...", command=action.sub_menu_items ) - tools_menu.add_cascade( label="Experimental", menu=experimental_menu, underline=0 ) @@ -269,7 +252,6 @@ class CoreMenubar(object): for i in nums: the_label = "R(" + str(i) + ")" random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( label="Random", menu=random_menu, underline=0 ) @@ -282,14 +264,11 @@ class CoreMenubar(object): :return: nothing """ grid_menu = tk.Menu(topology_generator_menu) - # list of number of nodes to create nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] - for i in nums: the_label = "G(" + str(i) + ")" grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) def create_connected_grid_menu(self, topology_generator_menu): @@ -324,7 +303,6 @@ class CoreMenubar(object): for i in nums: the_label = "P(" + str(i) + ")" chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) def create_star_menu(self, topology_generator_menu): @@ -338,7 +316,6 @@ class CoreMenubar(object): for i in range(3, 26, 1): the_label = "C(" + str(i) + ")" star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) def create_cycle_menu(self, topology_generator_menu): @@ -352,7 +329,6 @@ class CoreMenubar(object): for i in range(3, 25, 1): the_label = "C(" + str(i) + ")" cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) def create_wheel_menu(self, topology_generator_menu): @@ -366,7 +342,6 @@ class CoreMenubar(object): for i in range(4, 26, 1): the_label = "W(" + str(i) + ")" wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) def create_cube_menu(self, topology_generator_menu): @@ -380,7 +355,6 @@ class CoreMenubar(object): for i in range(2, 7, 1): the_label = "Q(" + str(i) + ")" cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) def create_clique_menu(self, topology_generator_menu): @@ -394,7 +368,6 @@ class CoreMenubar(object): for i in range(3, 25, 1): the_label = "K(" + str(i) + ")" clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( label="Clique", menu=clique_menu, underline=0 ) @@ -429,7 +402,6 @@ class CoreMenubar(object): :return: nothing """ topology_generator_menu = tk.Menu(tools_menu) - self.create_random_menu(topology_generator_menu) self.create_grid_menu(topology_generator_menu) self.create_connected_grid_menu(topology_generator_menu) @@ -440,19 +412,17 @@ class CoreMenubar(object): self.create_cube_menu(topology_generator_menu) self.create_clique_menu(topology_generator_menu) self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade( label="Topology generator", menu=topology_generator_menu, underline=0 ) - def create_tools_menu(self): + def draw_tools_menu(self): """ Create tools menu :return: nothing """ - - tools_menu = tk.Menu(self.menubar) + tools_menu = tk.Menu(self) tools_menu.add_command( label="Auto rearrange all", command=action.tools_auto_rearrange_all, @@ -464,13 +434,10 @@ class CoreMenubar(object): underline=0, ) tools_menu.add_separator() - tools_menu.add_command( label="Align to grid", command=action.tools_align_to_grid, underline=0 ) - tools_menu.add_separator() - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) tools_menu.add_command( label="IP addresses...", command=action.tools_ip_addresses, underline=0 @@ -489,8 +456,7 @@ class CoreMenubar(object): self.create_experimental_menu(tools_menu) self.create_topology_generator_menu(tools_menu) tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - - self.menubar.add_cascade(label="Tools", menu=tools_menu, underline=0) + self.add_cascade(label="Tools", menu=tools_menu, underline=0) def create_observer_widgets_menu(self, widget_menu): """ @@ -544,9 +510,8 @@ class CoreMenubar(object): label="PIM neighbors", command=action.sub_menu_items ) observer_widget_menu.add_command( - label="Edit...", command=self.menu_action.edit_observer_widgets + label="Edit...", command=self.menuaction.edit_observer_widgets ) - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) def create_adjacency_menu(self, widget_menu): @@ -561,47 +526,40 @@ class CoreMenubar(object): adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) - def create_widgets_menu(self): + def draw_widgets_menu(self): """ Create widget menu :return: nothing """ - widget_menu = tk.Menu(self.menubar) + widget_menu = tk.Menu(self) self.create_observer_widgets_menu(widget_menu) self.create_adjacency_menu(widget_menu) widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - widget_menu.add_separator() - widget_menu.add_command( label="Configure Adjacency...", command=action.widgets_configure_adjacency ) widget_menu.add_command( label="Configure Throughput...", command=action.widgets_configure_throughput ) + self.add_cascade(label="Widgets", menu=widget_menu, underline=0) - self.menubar.add_cascade(label="Widgets", menu=widget_menu, underline=0) - - def create_session_menu(self): + def draw_session_menu(self): """ Create session menu :return: nothing """ - session_menu = tk.Menu(self.menubar) - + session_menu = tk.Menu(self) session_menu.add_command( label="Change sessions...", - command=self.menu_action.session_change_sessions, + command=self.menuaction.session_change_sessions, underline=0, ) - session_menu.add_separator() - session_menu.add_command( label="Node types...", command=action.session_node_types, underline=0 ) @@ -609,7 +567,7 @@ class CoreMenubar(object): label="Comments...", command=action.session_comments, underline=0 ) session_menu.add_command( - label="Hooks...", command=self.menu_action.session_hooks, underline=0 + label="Hooks...", command=self.menuaction.session_hooks, underline=0 ) session_menu.add_command( label="Reset node positions", @@ -618,69 +576,27 @@ class CoreMenubar(object): ) session_menu.add_command( label="Emulation servers...", - command=self.menu_action.session_servers, + command=self.menuaction.session_servers, underline=0, ) session_menu.add_command( - label="Options...", command=self.menu_action.session_options, underline=0 + label="Options...", command=self.menuaction.session_options, underline=0 ) + self.add_cascade(label="Session", menu=session_menu, underline=0) - self.menubar.add_cascade(label="Session", menu=session_menu, underline=0) - - def create_help_menu(self): + def draw_help_menu(self): """ Create help menu :return: nothing """ - help_menu = tk.Menu(self.menubar) + help_menu = tk.Menu(self) help_menu.add_command( - label="Core Github (www)", command=self.menu_action.help_core_github + label="Core Github (www)", command=self.menuaction.help_core_github ) help_menu.add_command( label="Core Documentation (www)", - command=self.menu_action.help_core_documentation, + command=self.menuaction.help_core_documentation, ) help_menu.add_command(label="About", command=action.help_about) - - self.menubar.add_cascade(label="Help", menu=help_menu) - - def bind_menubar_shortcut(self): - """ - Bind hot keys to matching command - - :return: nothing - """ - self.app.bind_all("", action.file_new_shortcut) - self.app.bind_all("", action.file_open_shortcut) - self.app.bind_all("", action.file_save_shortcut) - self.app.bind_all("", action.edit_undo_shortcut) - self.app.bind_all("", action.edit_redo_shortcut) - self.app.bind_all("", action.edit_cut_shortcut) - self.app.bind_all("", action.edit_copy_shortcut) - self.app.bind_all("", action.edit_paste_shortcut) - self.app.bind_all("", action.edit_select_all_shortcut) - self.app.bind_all("", action.edit_select_adjacent_shortcut) - self.app.bind_all("", action.edit_find_shortcut) - self.app.bind_all("", action.canvas_previous_shortcut) - self.app.bind_all("", action.canvas_next_shortcut) - self.app.bind_all("", action.canvas_first_shortcut) - self.app.bind_all("", action.canvas_last_shortcut) - self.app.bind_all("", action.view_zoom_in_shortcut) - self.app.bind_all("", action.view_zoom_out_shortcut) - - def create_core_menubar(self): - """ - Create core menubar and bind the hot keys to their matching command - - :return: nothing - """ - self.create_file_menu() - self.create_edit_menu() - self.create_canvas_menu() - self.create_view_menu() - self.create_tools_menu() - self.create_widgets_menu() - self.create_session_menu() - self.create_help_menu() - self.bind_menubar_shortcut() + self.add_cascade(label="Help", menu=help_menu) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index ace49796..91e09508 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -34,7 +34,6 @@ class Toolbar(tk.Frame): self.link_layer_option_menu = None self.marker_option_menu = None self.network_layer_option_menu = None - self.canvas = None self.node_button = None self.network_button = None self.annotation_button = None @@ -204,7 +203,7 @@ class Toolbar(tk.Frame): def click_selection_tool(self): logging.debug("clicked selection tool") - self.canvas.mode = GraphMode.SELECT + self.app.canvas.mode = GraphMode.SELECT def click_start_session_tool(self): """ @@ -214,13 +213,13 @@ class Toolbar(tk.Frame): :return: nothing """ logging.debug("clicked start button") - self.canvas.mode = GraphMode.SELECT + self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() def click_link_tool(self): logging.debug("Click LINK button") - self.canvas.mode = GraphMode.EDGE + self.app.canvas.mode = GraphMode.EDGE def update_button(self, button, image_enum, name): logging.info("update button(%s): %s, %s", button, image_enum, name) @@ -232,9 +231,9 @@ class Toolbar(tk.Frame): image = Images.get(image_enum) logging.info("updating button(%s): %s", button, name) button.configure(image=image) - self.canvas.mode = GraphMode.NODE - self.canvas.draw_node_image = image - self.canvas.draw_node_name = name + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.draw_node_image = image + self.app.canvas.draw_node_name = name def hide_pickers(self): logging.info("hiding pickers") From 6357062fecbdadd77dabc702018edf035e000c15 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 22:00:46 -0800 Subject: [PATCH 089/305] removed packing from toolbar --- coretk/coretk/toolbar.py | 4 ++-- coretk/coretk/tooltip.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 91e09508..298cc24a 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -172,12 +172,12 @@ class Toolbar(tk.Frame): :param PIL.Image img: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button - :param tkinter.Radiobutton main_button: main button + :param str tooltip: tooltip text :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=img) button.bind("", lambda e: func()) - button.pack(side=tk.LEFT, pady=1) + button.grid(pady=1) CreateToolTip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index beb68ba3..7ccb505b 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -14,8 +14,6 @@ class CreateToolTip(object): self.tw = None def enter(self, event=None): - x = 0 - y = 0 x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() y += self.widget.winfo_rooty() + 32 From dbaf5dad9177ed1a43ed64c4f51b40c4101ceb38 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 7 Nov 2019 22:11:27 -0800 Subject: [PATCH 090/305] changed tooltip from pack to grid --- coretk/coretk/tooltip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 7ccb505b..6fbbc3c9 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -29,7 +29,7 @@ class CreateToolTip(object): relief="solid", borderwidth=1, ) - label.pack(ipadx=1) + label.grid(padx=1) def close(self, event=None): if self.tw: From 22177def1c3440055da8f0fb43de938d1750edc4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 10:07:23 -0800 Subject: [PATCH 091/305] updates to disable all unimplemented menu options --- coretk/coretk/app.py | 6 - coretk/coretk/menuaction.py | 210 +------------- coretk/coretk/menubar.py | 550 +++++++++++++----------------------- 3 files changed, 201 insertions(+), 565 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4bb23bc0..1297c622 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -65,12 +65,6 @@ class Application(tk.Frame): def draw_status(self): self.statusbar = tk.Frame(self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) - button = tk.Button(self.statusbar, text="Button 1") - button.pack(side=tk.LEFT, padx=1) - button = tk.Button(self.statusbar, text="Button 2") - button.pack(side=tk.LEFT, padx=1) - button = tk.Button(self.statusbar, text="Button 3") - button.pack(side=tk.LEFT, padx=1) def on_closing(self): menu_action = MenuAction(self, self.master) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 88f32bf4..3b33377c 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -17,214 +17,6 @@ from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog -def sub_menu_items(): - logging.debug("Click on sub menu items") - - -def file_new(event=None): - logging.debug("Click file New") - - -def file_reload(): - logging.debug("Click file Reload") - - -def file_export_python_script(): - logging.debug("Click file export python script") - - -def file_execute_xml_or_python_script(): - logging.debug("Execute XML or Python script") - - -def file_execute_python_script_with_options(): - logging.debug("Click execute Python script with options") - - -def file_open_current_file_in_editor(): - logging.debug("Click file open current in editor") - - -def file_print(): - logging.debug("Click file Print") - - -def file_save_screenshot(): - logging.debug("Click file save screenshot") - - -def edit_undo(event=None): - logging.debug("Click edit undo") - - -def edit_redo(event=None): - logging.debug("Click edit redo") - - -def edit_cut(event=None): - logging.debug("Click edit cut") - - -def edit_copy(event=None): - logging.debug("Click edit copy") - - -def edit_paste(event=None): - logging.debug("Click edit paste") - - -def edit_select_all(event=None): - logging.debug("Click edit select all") - - -def edit_select_adjacent(event=None): - logging.debug("Click edit select adjacent") - - -def edit_find(event=None): - logging.debug("CLick edit find") - - -def edit_clear_marker(): - logging.debug("Click edit clear marker") - - -def edit_preferences(): - logging.debug("Click preferences") - - -def canvas_new(): - logging.debug("Click canvas new") - - -def canvas_manage(): - logging.debug("Click canvas manage") - - -def canvas_delete(): - logging.debug("Click canvas delete") - - -def canvas_previous(event=None): - logging.debug("Click canvas previous") - - -def canvas_next(event=None): - logging.debug("Click canvas next") - - -def canvas_first(event=None): - logging.debug("CLick canvas first") - - -def canvas_last(event=None): - logging.debug("CLick canvas last") - - -def view_show(): - logging.debug("Click view show") - - -def view_show_hidden_nodes(): - logging.debug("Click view show hidden nodes") - - -def view_locked(): - logging.debug("Click view locked") - - -def view_3d_gui(): - logging.debug("CLick view 3D GUI") - - -def view_zoom_in(event=None): - logging.debug("Click view zoom in") - - -def view_zoom_out(event=None): - logging.debug("Click view zoom out") - - -def tools_auto_rearrange_all(): - logging.debug("Click tools, auto rearrange all") - - -def tools_auto_rearrange_selected(): - logging.debug("CLick tools auto rearrange selected") - - -def tools_align_to_grid(): - logging.debug("Click tools align to grid") - - -def tools_traffic(): - logging.debug("Click tools traffic") - - -def tools_ip_addresses(): - logging.debug("Click tools ip addresses") - - -def tools_mac_addresses(): - logging.debug("Click tools mac addresses") - - -def tools_build_hosts_file(): - logging.debug("Click tools build hosts file") - - -def tools_renumber_nodes(): - logging.debug("Click tools renumber nodes") - - -def tools_experimental(): - logging.debug("Click tools experimental") - - -def tools_topology_generator(): - logging.debug("Click tools topology generator") - - -def tools_debugger(): - logging.debug("Click tools debugger") - - -def widgets_observer_widgets(): - logging.debug("Click widgets observer widgets") - - -def widgets_adjacency(): - logging.debug("Click widgets adjacency") - - -def widgets_throughput(): - logging.debug("Click widgets throughput") - - -def widgets_configure_adjacency(): - logging.debug("Click widgets configure adjacency") - - -def widgets_configure_throughput(): - logging.debug("Click widgets configure throughput") - - -def session_node_types(): - logging.debug("Click session node types") - - -def session_comments(): - logging.debug("Click session comments") - - -def session_reset_node_positions(): - logging.debug("Click session reset node positions") - - -def help_about(): - logging.debug("Click help About") - - class MenuAction: """ Actions performed when choosing menu items @@ -258,7 +50,7 @@ class MenuAction: self.app.core.stop_session() self.app.core.delete_session() - def on_quit(self): + def on_quit(self, event=None): """ Prompt user whether so save running session, and then close the application diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 143833b7..3ef38d73 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -43,52 +43,33 @@ class Menubar(tk.Menu): :return: nothing """ - file_menu = tk.Menu(self) - file_menu.add_command( - label="New Session", - command=action.file_new, - accelerator="Ctrl+N", - underline=0, - ) - self.app.bind_all("", action.file_new) - file_menu.add_command( - label="Open...", - command=self.menuaction.file_open_xml, - accelerator="Ctrl+O", - underline=0, + menu = tk.Menu(self) + menu.add_command(label="New Session", accelerator="Ctrl+N", state=tk.DISABLED) + menu.add_command( + label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) self.app.bind_all("", self.menuaction.file_open_xml) - file_menu.add_command(label="Reload", command=action.file_reload, underline=0) - file_menu.add_command( + menu.add_command(label="Reload", underline=0, state=tk.DISABLED) + menu.add_command( label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml ) self.app.bind_all("", self.menuaction.file_save_as_xml) - file_menu.add_separator() - file_menu.add_command( - label="Export Python script...", command=action.file_export_python_script + menu.add_separator() + menu.add_command(label="Export Python script...", state=tk.DISABLED) + menu.add_command(label="Execute XML or Python script...", state=tk.DISABLED) + menu.add_command( + label="Execute Python script with options...", state=tk.DISABLED ) - file_menu.add_command( - label="Execute XML or Python script...", - command=action.file_execute_xml_or_python_script, + menu.add_separator() + menu.add_command(label="Open current file in editor...", state=tk.DISABLED) + menu.add_command(label="Print...", underline=0, state=tk.DISABLED) + menu.add_command(label="Save screenshot...", state=tk.DISABLED) + menu.add_separator() + menu.add_command( + label="Quit", accelerator="Ctrl+Q", command=self.menuaction.on_quit ) - file_menu.add_command( - label="Execute Python script with options...", - command=action.file_execute_python_script_with_options, - ) - file_menu.add_separator() - file_menu.add_command( - label="Open current file in editor...", - command=action.file_open_current_file_in_editor, - ) - file_menu.add_command(label="Print...", command=action.file_print, underline=0) - file_menu.add_command( - label="Save screenshot...", command=action.file_save_screenshot - ) - file_menu.add_separator() - file_menu.add_command( - label="Quit", command=self.menuaction.on_quit, underline=0 - ) - self.add_cascade(label="File", menu=file_menu, underline=0) + self.app.bind_all("", self.menuaction.on_quit) + self.add_cascade(label="File", menu=menu) def draw_edit_menu(self): """ @@ -96,47 +77,23 @@ class Menubar(tk.Menu): :return: nothing """ - edit_menu = tk.Menu(self) - edit_menu.add_command( - label="Undo", command=action.edit_undo, accelerator="Ctrl+Z", underline=0 + menu = tk.Menu(self) + menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) + menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Cut", accelerator="Ctrl+X", state=tk.DISABLED) + menu.add_command(label="Copy", accelerator="Ctrl+C", state=tk.DISABLED) + menu.add_command(label="Paste", accelerator="Ctrl+V", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Select all", accelerator="Ctrl+A", state=tk.DISABLED) + menu.add_command( + label="Select Adjacent", accelerator="Ctrl+J", state=tk.DISABLED ) - self.app.bind_all("", action.edit_undo) - edit_menu.add_command( - label="Redo", command=action.edit_redo, accelerator="Ctrl+Y", underline=0 - ) - self.app.bind_all("", action.edit_redo) - edit_menu.add_separator() - edit_menu.add_command( - label="Cut", command=action.edit_cut, accelerator="Ctrl+X", underline=0 - ) - self.app.bind_all("", action.edit_cut) - edit_menu.add_command( - label="Copy", command=action.edit_copy, accelerator="Ctrl+C", underline=0 - ) - self.app.bind_all("", action.edit_copy) - edit_menu.add_command( - label="Paste", command=action.edit_paste, accelerator="Ctrl+V", underline=0 - ) - self.app.bind_all("", action.edit_paste) - edit_menu.add_separator() - edit_menu.add_command( - label="Select all", command=action.edit_select_all, accelerator="Ctrl+A" - ) - self.app.bind_all("", action.edit_select_all) - edit_menu.add_command( - label="Select Adjacent", - command=action.edit_select_adjacent, - accelerator="Ctrl+J", - ) - self.app.bind_all("", action.edit_select_adjacent) - edit_menu.add_separator() - edit_menu.add_command( - label="Find...", command=action.edit_find, accelerator="Ctrl+F", underline=0 - ) - self.app.bind_all("", action.edit_find) - edit_menu.add_command(label="Clear marker", command=action.edit_clear_marker) - edit_menu.add_command(label="Preferences...", command=action.edit_preferences) - self.add_cascade(label="Edit", menu=edit_menu, underline=0) + menu.add_separator() + menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) + menu.add_command(label="Clear marker", state=tk.DISABLED) + menu.add_command(label="Preferences...", state=tk.DISABLED) + self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): """ @@ -144,55 +101,23 @@ class Menubar(tk.Menu): :return: nothing """ - canvas_menu = tk.Menu(self) - canvas_menu.add_command(label="New", command=action.canvas_new) - canvas_menu.add_command(label="Manage...", command=action.canvas_manage) - canvas_menu.add_command(label="Delete", command=action.canvas_delete) - canvas_menu.add_separator() - canvas_menu.add_command( + menu = tk.Menu(self) + menu.add_command(label="New", state=tk.DISABLED) + menu.add_command(label="Manage...", state=tk.DISABLED) + menu.add_command(label="Delete", state=tk.DISABLED) + menu.add_separator() + menu.add_command( label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) - canvas_menu.add_command( + menu.add_command( label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) - canvas_menu.add_separator() - canvas_menu.add_command( - label="Previous", command=action.canvas_previous, accelerator="PgUp" - ) - self.app.bind_all("", action.canvas_previous) - canvas_menu.add_command( - label="Next", command=action.canvas_next, accelerator="PgDown" - ) - self.app.bind_all("", action.canvas_next) - canvas_menu.add_command( - label="First", command=action.canvas_first, accelerator="Home" - ) - self.app.bind_all("", action.canvas_first) - canvas_menu.add_command( - label="Last", command=action.canvas_last, accelerator="End" - ) - self.app.bind_all("", action.canvas_last) - self.add_cascade(label="Canvas", menu=canvas_menu, underline=0) - - def create_show_menu(self, view_menu): - """ - Create the menu items in View/Show - - :param tkinter.Menu view_menu: the view menu - :return: nothing - """ - show_menu = tk.Menu(view_menu) - show_menu.add_command(label="All", command=action.sub_menu_items) - show_menu.add_command(label="None", command=action.sub_menu_items) - show_menu.add_separator() - show_menu.add_command(label="Interface Names", command=action.sub_menu_items) - show_menu.add_command(label="IPv4 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="IPv6 Addresses", command=action.sub_menu_items) - show_menu.add_command(label="Node Labels", command=action.sub_menu_items) - show_menu.add_command(label="Annotations", command=action.sub_menu_items) - show_menu.add_command(label="Grid", command=action.sub_menu_items) - show_menu.add_command(label="API Messages", command=action.sub_menu_items) - view_menu.add_cascade(label="Show", menu=show_menu) + menu.add_separator() + menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) + menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) + menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) + menu.add_command(label="Last", accelerator="End", state=tk.DISABLED) + self.add_cascade(label="Canvas", menu=menu) def draw_view_menu(self): """ @@ -202,21 +127,33 @@ class Menubar(tk.Menu): """ view_menu = tk.Menu(self) self.create_show_menu(view_menu) - view_menu.add_command( - label="Show hidden nodes", command=action.view_show_hidden_nodes - ) - view_menu.add_command(label="Locked", command=action.view_locked) - view_menu.add_command(label="3D GUI...", command=action.view_3d_gui) + view_menu.add_command(label="Show hidden nodes", state=tk.DISABLED) + view_menu.add_command(label="Locked", state=tk.DISABLED) + view_menu.add_command(label="3D GUI...", state=tk.DISABLED) view_menu.add_separator() - view_menu.add_command( - label="Zoom in", command=action.view_zoom_in, accelerator="+" - ) - self.app.bind_all("", action.view_zoom_in) - view_menu.add_command( - label="Zoom out", command=action.view_zoom_out, accelerator="-" - ) - self.app.bind_all("", action.view_zoom_out) - self.add_cascade(label="View", menu=view_menu, underline=0) + view_menu.add_command(label="Zoom in", accelerator="+", state=tk.DISABLED) + view_menu.add_command(label="Zoom out", accelerator="-", state=tk.DISABLED) + self.add_cascade(label="View", menu=view_menu) + + def create_show_menu(self, view_menu): + """ + Create the menu items in View/Show + + :param tkinter.Menu view_menu: the view menu + :return: nothing + """ + menu = tk.Menu(view_menu) + menu.add_command(label="All", state=tk.DISABLED) + menu.add_command(label="None", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Interface Names", state=tk.DISABLED) + menu.add_command(label="IPv4 Addresses", state=tk.DISABLED) + menu.add_command(label="IPv6 Addresses", state=tk.DISABLED) + menu.add_command(label="Node Labels", state=tk.DISABLED) + menu.add_command(label="Annotations", state=tk.DISABLED) + menu.add_command(label="Grid", state=tk.DISABLED) + menu.add_command(label="API Messages", state=tk.DISABLED) + view_menu.add_cascade(label="Show", menu=menu) def create_experimental_menu(self, tools_menu): """ @@ -225,19 +162,11 @@ class Menubar(tk.Menu): :param tkinter.Menu tools_menu: tools menu :return: nothing """ - experimental_menu = tk.Menu(tools_menu) - experimental_menu.add_command( - label="Plugins...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="ns2immunes converter...", command=action.sub_menu_items, underline=0 - ) - experimental_menu.add_command( - label="Topology partitioning...", command=action.sub_menu_items - ) - tools_menu.add_cascade( - label="Experimental", menu=experimental_menu, underline=0 - ) + menu = tk.Menu(tools_menu) + menu.add_command(label="Plugins...", state=tk.DISABLED) + menu.add_command(label="ns2immunes converter...", state=tk.DISABLED) + menu.add_command(label="Topology partitioning...", state=tk.DISABLED) + tools_menu.add_cascade(label="Experimental", menu=menu) def create_random_menu(self, topology_generator_menu): """ @@ -246,15 +175,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - random_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # list of number of random nodes to create nums = [1, 5, 10, 15, 20, 30, 40, 50, 75, 100] for i in nums: - the_label = "R(" + str(i) + ")" - random_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( - label="Random", menu=random_menu, underline=0 - ) + label = f"R({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Random", menu=menu) def create_grid_menu(self, topology_generator_menu): """ @@ -263,13 +190,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - grid_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # list of number of nodes to create nums = [1, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 100] for i in nums: - the_label = "G(" + str(i) + ")" - grid_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Grid", menu=grid_menu, underline=0) + label = f"G({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Grid", menu=menu) def create_connected_grid_menu(self, topology_generator_menu): """ @@ -278,17 +205,15 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - grid_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(1, 11, 1): - i_n_menu = tk.Menu(grid_menu) + submenu = tk.Menu(menu) for j in range(1, 11, 1): - i_j_label = str(i) + " X " + str(j) - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = str(i) + " X N" - grid_menu.add_cascade(label=i_n_label, menu=i_n_menu) - topology_generator_menu.add_cascade( - label="Connected Grid", menu=grid_menu, underline=0 - ) + label = f"{i} X {j}" + submenu.add_command(label=label, state=tk.DISABLED) + label = str(i) + " X N" + menu.add_cascade(label=label, menu=submenu) + topology_generator_menu.add_cascade(label="Connected Grid", menu=menu) def create_chain_menu(self, topology_generator_menu): """ @@ -297,13 +222,13 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - chain_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) # number of nodes to create nums = list(range(2, 25, 1)) + [32, 64, 128] for i in nums: - the_label = "P(" + str(i) + ")" - chain_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Chain", menu=chain_menu, underline=0) + label = f"P({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Chain", menu=menu) def create_star_menu(self, topology_generator_menu): """ @@ -312,11 +237,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - star_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 26, 1): - the_label = "C(" + str(i) + ")" - star_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Star", menu=star_menu, underline=0) + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Star", menu=menu) def create_cycle_menu(self, topology_generator_menu): """ @@ -325,11 +250,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cycle_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): - the_label = "C(" + str(i) + ")" - cycle_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cycle", menu=cycle_menu, underline=0) + label = f"C({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cycle", menu=menu) def create_wheel_menu(self, topology_generator_menu): """ @@ -338,11 +263,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - wheel_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(4, 26, 1): - the_label = "W(" + str(i) + ")" - wheel_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Wheel", menu=wheel_menu, underline=0) + label = f"W({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Wheel", menu=menu) def create_cube_menu(self, topology_generator_menu): """ @@ -351,11 +276,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - cube_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(2, 7, 1): - the_label = "Q(" + str(i) + ")" - cube_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade(label="Cube", menu=cube_menu, underline=0) + label = f"Q({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Cube", menu=menu) def create_clique_menu(self, topology_generator_menu): """ @@ -364,13 +289,11 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology generator menu :return: nothing """ - clique_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) for i in range(3, 25, 1): - the_label = "K(" + str(i) + ")" - clique_menu.add_command(label=the_label, command=action.sub_menu_items) - topology_generator_menu.add_cascade( - label="Clique", menu=clique_menu, underline=0 - ) + label = f"K({i})" + menu.add_command(label=label, state=tk.DISABLED) + topology_generator_menu.add_cascade(label="Clique", menu=menu) def create_bipartite_menu(self, topology_generator_menu): """ @@ -379,19 +302,17 @@ class Menubar(tk.Menu): :param tkinter.Menu topology_generator_menu: topology_generator_menu :return: nothing """ - bipartite_menu = tk.Menu(topology_generator_menu) + menu = tk.Menu(topology_generator_menu) temp = 24 for i in range(1, 13, 1): - i_n_menu = tk.Menu(bipartite_menu) + submenu = tk.Menu(menu) for j in range(i, temp, 1): - i_j_label = "K(" + str(i) + " X " + str(j) + ")" - i_n_menu.add_command(label=i_j_label, command=action.sub_menu_items) - i_n_label = "K(" + str(i) + " X N)" - bipartite_menu.add_cascade(label=i_n_label, menu=i_n_menu) + label = f"K({i} X {j})" + submenu.add_command(label=label, state=tk.DISABLED) + label = f"K({i})" + menu.add_cascade(label=label, menu=submenu) temp = temp - 1 - topology_generator_menu.add_cascade( - label="Bipartite", menu=bipartite_menu, underline=0 - ) + topology_generator_menu.add_cascade(label="Bipartite", menu=menu) def create_topology_generator_menu(self, tools_menu): """ @@ -401,20 +322,18 @@ class Menubar(tk.Menu): :return: nothing """ - topology_generator_menu = tk.Menu(tools_menu) - self.create_random_menu(topology_generator_menu) - self.create_grid_menu(topology_generator_menu) - self.create_connected_grid_menu(topology_generator_menu) - self.create_chain_menu(topology_generator_menu) - self.create_star_menu(topology_generator_menu) - self.create_cycle_menu(topology_generator_menu) - self.create_wheel_menu(topology_generator_menu) - self.create_cube_menu(topology_generator_menu) - self.create_clique_menu(topology_generator_menu) - self.create_bipartite_menu(topology_generator_menu) - tools_menu.add_cascade( - label="Topology generator", menu=topology_generator_menu, underline=0 - ) + menu = tk.Menu(tools_menu) + self.create_random_menu(menu) + self.create_grid_menu(menu) + self.create_connected_grid_menu(menu) + self.create_chain_menu(menu) + self.create_star_menu(menu) + self.create_cycle_menu(menu) + self.create_wheel_menu(menu) + self.create_cube_menu(menu) + self.create_clique_menu(menu) + self.create_bipartite_menu(menu) + tools_menu.add_cascade(label="Topology generator", menu=menu) def draw_tools_menu(self): """ @@ -422,41 +341,21 @@ class Menubar(tk.Menu): :return: nothing """ - tools_menu = tk.Menu(self) - tools_menu.add_command( - label="Auto rearrange all", - command=action.tools_auto_rearrange_all, - underline=0, - ) - tools_menu.add_command( - label="Auto rearrange selected", - command=action.tools_auto_rearrange_selected, - underline=0, - ) - tools_menu.add_separator() - tools_menu.add_command( - label="Align to grid", command=action.tools_align_to_grid, underline=0 - ) - tools_menu.add_separator() - tools_menu.add_command(label="Traffic...", command=action.tools_traffic) - tools_menu.add_command( - label="IP addresses...", command=action.tools_ip_addresses, underline=0 - ) - tools_menu.add_command( - label="MAC addresses...", command=action.tools_mac_addresses, underline=0 - ) - tools_menu.add_command( - label="Build hosts file...", - command=action.tools_build_hosts_file, - underline=0, - ) - tools_menu.add_command( - label="Renumber nodes...", command=action.tools_renumber_nodes, underline=0 - ) - self.create_experimental_menu(tools_menu) - self.create_topology_generator_menu(tools_menu) - tools_menu.add_command(label="Debugger...", command=action.tools_debugger) - self.add_cascade(label="Tools", menu=tools_menu, underline=0) + menu = tk.Menu(self) + menu.add_command(label="Auto rearrange all", state=tk.DISABLED) + menu.add_command(label="Auto rearrange selected", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Align to grid", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Traffic...", state=tk.DISABLED) + menu.add_command(label="IP addresses...", state=tk.DISABLED) + menu.add_command(label="MAC addresses...", state=tk.DISABLED) + menu.add_command(label="Build hosts file...", state=tk.DISABLED) + menu.add_command(label="Renumber nodes...", state=tk.DISABLED) + self.create_experimental_menu(menu) + self.create_topology_generator_menu(menu) + menu.add_command(label="Debugger...", state=tk.DISABLED) + self.add_cascade(label="Tools", menu=menu) def create_observer_widgets_menu(self, widget_menu): """ @@ -465,54 +364,24 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ - observer_widget_menu = tk.Menu(widget_menu) - observer_widget_menu.add_command(label="None", command=action.sub_menu_items) - observer_widget_menu.add_command( - label="processes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="ifconfig", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 routes", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv2 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Listening sockets", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv4 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPv6 MFC entries", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="firewall rules", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="IPsec policies", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="docker logs", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="OSPFv3 MDR level", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="PIM neighbors", command=action.sub_menu_items - ) - observer_widget_menu.add_command( - label="Edit...", command=self.menuaction.edit_observer_widgets - ) - widget_menu.add_cascade(label="Observer Widgets", menu=observer_widget_menu) + menu = tk.Menu(widget_menu) + menu.add_command(label="None", state=tk.DISABLED) + menu.add_command(label="processes", state=tk.DISABLED) + menu.add_command(label="ifconfig", state=tk.DISABLED) + menu.add_command(label="IPv4 routes", state=tk.DISABLED) + menu.add_command(label="IPv6 routes", state=tk.DISABLED) + menu.add_command(label="OSPFv2 neighbors", state=tk.DISABLED) + menu.add_command(label="OSPFv3 neighbors", state=tk.DISABLED) + menu.add_command(label="Listening sockets", state=tk.DISABLED) + menu.add_command(label="IPv4 MFC entries", state=tk.DISABLED) + menu.add_command(label="IPv6 MFC entries", state=tk.DISABLED) + menu.add_command(label="firewall rules", state=tk.DISABLED) + menu.add_command(label="IPsec policies", state=tk.DISABLED) + menu.add_command(label="docker logs", state=tk.DISABLED) + menu.add_command(label="OSPFv3 MDR level", state=tk.DISABLED) + menu.add_command(label="PIM neighbors", state=tk.DISABLED) + menu.add_command(label="Edit...", command=self.menuaction.edit_observer_widgets) + widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): """ @@ -521,12 +390,12 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget menu :return: nothing """ - adjacency_menu = tk.Menu(widget_menu) - adjacency_menu.add_command(label="OSPFv2", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSPFv3", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLR", command=action.sub_menu_items) - adjacency_menu.add_command(label="OSLRv2", command=action.sub_menu_items) - widget_menu.add_cascade(label="Adjacency", menu=adjacency_menu) + menu = tk.Menu(widget_menu) + menu.add_command(label="OSPFv2", state=tk.DISABLED) + menu.add_command(label="OSPFv3", state=tk.DISABLED) + menu.add_command(label="OSLR", state=tk.DISABLED) + menu.add_command(label="OSLRv2", state=tk.DISABLED) + widget_menu.add_cascade(label="Adjacency", menu=menu) def draw_widgets_menu(self): """ @@ -534,18 +403,14 @@ class Menubar(tk.Menu): :return: nothing """ - widget_menu = tk.Menu(self) - self.create_observer_widgets_menu(widget_menu) - self.create_adjacency_menu(widget_menu) - widget_menu.add_command(label="Throughput", command=action.widgets_throughput) - widget_menu.add_separator() - widget_menu.add_command( - label="Configure Adjacency...", command=action.widgets_configure_adjacency - ) - widget_menu.add_command( - label="Configure Throughput...", command=action.widgets_configure_throughput - ) - self.add_cascade(label="Widgets", menu=widget_menu, underline=0) + menu = tk.Menu(self) + self.create_observer_widgets_menu(menu) + self.create_adjacency_menu(menu) + menu.add_command(label="Throughput", state=tk.DISABLED) + menu.add_separator() + menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) + menu.add_command(label="Configure Throughput...", state=tk.DISABLED) + self.add_cascade(label="Widgets", menu=menu) def draw_session_menu(self): """ @@ -553,36 +418,21 @@ class Menubar(tk.Menu): :return: nothing """ - session_menu = tk.Menu(self) - session_menu.add_command( + menu = tk.Menu(self) + menu.add_command( label="Change sessions...", command=self.menuaction.session_change_sessions, underline=0, ) - session_menu.add_separator() - session_menu.add_command( - label="Node types...", command=action.session_node_types, underline=0 + menu.add_separator() + menu.add_command(label="Comments...", state=tk.DISABLED) + menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) + menu.add_command(label="Reset node positions", state=tk.DISABLED) + menu.add_command( + label="Emulation servers...", command=self.menuaction.session_servers ) - session_menu.add_command( - label="Comments...", command=action.session_comments, underline=0 - ) - session_menu.add_command( - label="Hooks...", command=self.menuaction.session_hooks, underline=0 - ) - session_menu.add_command( - label="Reset node positions", - command=action.session_reset_node_positions, - underline=0, - ) - session_menu.add_command( - label="Emulation servers...", - command=self.menuaction.session_servers, - underline=0, - ) - session_menu.add_command( - label="Options...", command=self.menuaction.session_options, underline=0 - ) - self.add_cascade(label="Session", menu=session_menu, underline=0) + menu.add_command(label="Options...", command=self.menuaction.session_options) + self.add_cascade(label="Session", menu=menu) def draw_help_menu(self): """ @@ -590,13 +440,13 @@ class Menubar(tk.Menu): :return: nothing """ - help_menu = tk.Menu(self) - help_menu.add_command( + menu = tk.Menu(self) + menu.add_command( label="Core Github (www)", command=self.menuaction.help_core_github ) - help_menu.add_command( + menu.add_command( label="Core Documentation (www)", command=self.menuaction.help_core_documentation, ) - help_menu.add_command(label="About", command=action.help_about) - self.add_cascade(label="Help", menu=help_menu) + menu.add_command(label="About", state=tk.DISABLED) + self.add_cascade(label="Help", menu=menu) From b0fe5660bd8b607c0fd0925a9cd0a07b33a39ccc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 11:00:22 -0800 Subject: [PATCH 092/305] updates to draw custom nodes on the node picker frame --- coretk/coretk/app.py | 3 +- coretk/coretk/coreclient.py | 12 +++---- coretk/coretk/dialogs/customnodes.py | 2 +- coretk/coretk/toolbar.py | 50 ++++++++++++++++------------ 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 1297c622..7330f21a 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -72,7 +72,8 @@ class Application(tk.Frame): if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) + log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" + logging.basicConfig(level=logging.DEBUG, format=log_format) appdirs.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f018a348..ccb47176 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] -network_layer_nodes = ["router", "host", "PC", "mdr", "prouter"] +NETWORK_NODES = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] +DEFAULT_NODES = ["router", "host", "PC", "mdr", "prouter"] class Node: @@ -422,7 +422,7 @@ class CoreClient: """ node_type = None node_model = None - if name in link_layer_nodes: + if name in NETWORK_NODES: if name == "switch": node_type = core_pb2.NodeType.SWITCH elif name == "hub": @@ -437,7 +437,7 @@ class CoreClient: node_type = core_pb2.NodeType.TUNNEL elif name == "emane": node_type = core_pb2.NodeType.EMANE - elif name in network_layer_nodes: + elif name in DEFAULT_NODES or name in self.custom_nodes: node_type = core_pb2.NodeType.DEFAULT node_model = name else: @@ -606,7 +606,7 @@ class CoreClient: self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] - if src_node.model in network_layer_nodes: + if src_node.model in DEFAULT_NODES: ifid = len(src_node.interfaces) name = "eth" + str(ifid) src_interface = Interface( @@ -620,7 +620,7 @@ class CoreClient: ) dst_node = self.nodes[dst_canvas_id] - if dst_node.model in network_layer_nodes: + if dst_node.model in DEFAULT_NODES: ifid = len(dst_node.interfaces) name = "eth" + str(ifid) dst_interface = Interface( diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f2423bf0..e98b8c77 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -208,7 +208,7 @@ class CustomNodesDialog(Dialog): custom_node = self.app.core.custom_nodes.pop(previous_name) custom_node.name = name custom_node.image = self.image - custom_node.image_file = Path(self.image_file).name + custom_node.image_file = Path(self.image_file).stem custom_node.services = self.services self.app.core.custom_nodes[name] = custom_node self.nodes_list.listbox.delete(self.selected_index) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 298cc24a..ebdb4d39 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -145,15 +145,23 @@ class Toolbar(tk.Frame): (ImageEnum.PC, "PC"), (ImageEnum.MDR, "mdr"), (ImageEnum.PROUTER, "prouter"), - (ImageEnum.EDITNODE, "custom node types"), ] + # draw default nodes for image_enum, tooltip in nodes: - self.create_button( - Images.get(image_enum), - partial(self.update_button, self.node_button, image_enum, tooltip), - self.node_picker, - tooltip, - ) + image = Images.get(image_enum) + func = partial(self.update_button, self.node_button, image, tooltip) + self.create_button(image, func, self.node_picker, tooltip) + # draw custom nodes + for name in sorted(self.app.core.custom_nodes): + custom_node = self.app.core.custom_nodes[name] + image = custom_node.image + func = partial(self.update_button, self.node_button, image, name) + self.create_button(image, func, self.node_picker, name) + # draw edit node + image = Images.get(ImageEnum.EDITNODE) + self.create_button( + image, self.click_edit_node, self.node_picker, "custom nodes" + ) self.show_picker(self.node_button, self.node_picker) def show_picker(self, button, picker): @@ -165,17 +173,17 @@ class Toolbar(tk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_button(self, img, func, frame, tooltip): + def create_button(self, image, func, frame, tooltip): """ Create button and put it on the frame - :param PIL.Image img: button image + :param PIL.Image image: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button :param str tooltip: tooltip text :return: nothing """ - button = tk.Button(frame, width=self.width, height=self.height, image=img) + button = tk.Button(frame, width=self.width, height=self.height, image=image) button.bind("", lambda e: func()) button.grid(pady=1) CreateToolTip(button, tooltip) @@ -221,19 +229,17 @@ class Toolbar(tk.Frame): logging.debug("Click LINK button") self.app.canvas.mode = GraphMode.EDGE - def update_button(self, button, image_enum, name): - logging.info("update button(%s): %s, %s", button, image_enum, name) + def click_edit_node(self): + dialog = CustomNodesDialog(self.app, self.app) + dialog.show() + + def update_button(self, button, image, name): + logging.info("update button(%s): %s", button, name) self.hide_pickers() - if image_enum == ImageEnum.EDITNODE: - dialog = CustomNodesDialog(self.app, self.app) - dialog.show() - else: - image = Images.get(image_enum) - logging.info("updating button(%s): %s", button, name) - button.configure(image=image) - self.app.canvas.mode = GraphMode.NODE - self.app.canvas.draw_node_image = image - self.app.canvas.draw_node_name = name + button.configure(image=image) + self.app.canvas.mode = GraphMode.NODE + self.app.canvas.draw_node_image = image + self.app.canvas.draw_node_name = name def hide_pickers(self): logging.info("hiding pickers") From 3306dbbfae4a66e4bafaedbf10b4c2dc3c302d59 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 15:43:58 -0800 Subject: [PATCH 093/305] updates to allow custom nodes to be linked with interfaces --- coretk/coretk/coreclient.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ccb47176..54e6a75b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,8 +13,8 @@ from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -NETWORK_NODES = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] -DEFAULT_NODES = ["router", "host", "PC", "mdr", "prouter"] +NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} +DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} class Node: @@ -409,6 +409,9 @@ class CoreClient: else: return self.reusable.pop(0) + def is_model_node(self, name): + return name in DEFAULT_NODES or name in self.custom_nodes + def add_graph_node(self, session_id, canvas_id, x, y, name): """ Add node, with information filled in, to grpc manager @@ -437,7 +440,7 @@ class CoreClient: node_type = core_pb2.NodeType.TUNNEL elif name == "emane": node_type = core_pb2.NodeType.EMANE - elif name in DEFAULT_NODES or name in self.custom_nodes: + elif self.is_model_node(name): node_type = core_pb2.NodeType.DEFAULT node_model = name else: @@ -606,7 +609,7 @@ class CoreClient: self.interfaces_manager.new_subnet() src_node = self.nodes[src_canvas_id] - if src_node.model in DEFAULT_NODES: + if self.is_model_node(src_node.model): ifid = len(src_node.interfaces) name = "eth" + str(ifid) src_interface = Interface( @@ -620,7 +623,7 @@ class CoreClient: ) dst_node = self.nodes[dst_canvas_id] - if dst_node.model in DEFAULT_NODES: + if self.is_model_node(dst_node.model): ifid = len(dst_node.interfaces) name = "eth" + str(ifid) dst_interface = Interface( From 78f44e81109c388c0ce759fc3a0f451ab00893e6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 16:21:36 -0800 Subject: [PATCH 094/305] updates to toolbar event handling to eliminate events being sent to other components when displaying pickers --- coretk/coretk/toolbar.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index ebdb4d39..015d2ad6 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -83,8 +83,8 @@ class Toolbar(tk.Frame): "link tool", ) self.create_node_button() - self.create_link_layer_button() - self.create_marker_button() + self.create_network_button() + self.create_annotation_button() self.radio_value.set(1) def draw_runtime_frame(self): @@ -169,9 +169,11 @@ class Toolbar(tk.Frame): x = button.winfo_rootx() - first_button.winfo_rootx() + 40 y = button.winfo_rooty() - first_button.winfo_rooty() - 1 picker.place(x=x, y=y) - self.app.bind_all("", lambda e: self.hide_pickers()) + self.app.bind_all("", lambda e: self.hide_pickers()) + picker.wait_visibility() + picker.grab_set() self.wait_window(picker) - self.app.unbind_all("") + self.app.unbind_all("") def create_button(self, image, func, frame, tooltip): """ @@ -184,7 +186,7 @@ class Toolbar(tk.Frame): :return: nothing """ button = tk.Button(frame, width=self.width, height=self.height, image=image) - button.bind("", lambda e: func()) + button.bind("", lambda e: func()) button.grid(pady=1) CreateToolTip(button, tooltip) @@ -268,8 +270,8 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=router_image, - command=self.draw_node_picker, ) + self.node_button.bind("", lambda e: self.draw_node_picker()) self.node_button.grid() CreateToolTip(self.node_button, "Network-layer virtual nodes") @@ -291,15 +293,16 @@ class Toolbar(tk.Frame): (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] for image_enum, name, tooltip in nodes: + image = Images.get(image_enum) self.create_button( - Images.get(image_enum), - partial(self.update_button, self.network_button, image_enum, name), + image, + partial(self.update_button, self.network_button, image, name), self.network_picker, tooltip, ) self.show_picker(self.network_button, self.network_picker) - def create_link_layer_button(self): + def create_network_button(self): """ Create link-layer node button and the options that represent different link-layer node types @@ -314,7 +317,9 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=hub_image, - command=self.draw_network_picker, + ) + self.network_button.bind( + "", lambda e: self.draw_network_picker() ) self.network_button.grid() CreateToolTip(self.network_button, "link-layer nodes") @@ -343,7 +348,7 @@ class Toolbar(tk.Frame): ) self.show_picker(self.annotation_button, self.annotation_picker) - def create_marker_button(self): + def create_annotation_button(self): """ Create marker button and options that represent different marker types @@ -358,7 +363,9 @@ class Toolbar(tk.Frame): width=self.width, height=self.height, image=marker_image, - command=self.draw_annotation_picker, + ) + self.annotation_button.bind( + "", lambda e: self.draw_annotation_picker() ) self.annotation_button.grid() CreateToolTip(self.annotation_button, "background annotation tools") From 5bf0a2ac05bb0958e48270d1262b769caf31963b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 8 Nov 2019 16:37:00 -0800 Subject: [PATCH 095/305] fixed service editing for custom nodes, fixed hiding picker when clicking edit custom nodes --- coretk/coretk/dialogs/customnodes.py | 7 ++++--- coretk/coretk/toolbar.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index e98b8c77..20c5ae95 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -15,7 +15,7 @@ class ServicesSelectDialog(Dialog): self.groups = None self.services = None self.current = None - self.current_services = current_services + self.current_services = set(current_services) self.draw() def draw(self): @@ -48,7 +48,7 @@ class ServicesSelectDialog(Dialog): frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_cancel) + button = tk.Button(frame, text="Save", command=self.destroy) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -174,7 +174,8 @@ class CustomNodesDialog(Dialog): dialog = ServicesSelectDialog(self, self.app, self.services) dialog.show() if dialog.current_services is not None: - self.services = dialog.current_services + self.services.clear() + self.services.update(dialog.current_services) def click_save(self): self.app.config["nodes"].clear() diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 015d2ad6..5b2c83bd 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -232,6 +232,7 @@ class Toolbar(tk.Frame): self.app.canvas.mode = GraphMode.EDGE def click_edit_node(self): + self.hide_pickers() dialog = CustomNodesDialog(self.app, self.app) dialog.show() From b88abd0f74281b846cdedbd2c35a1a5c54d2e22b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sun, 10 Nov 2019 17:26:38 -0800 Subject: [PATCH 096/305] added initial code that can help support canvas tooltips for observer widgets --- coretk/coretk/canvastooltip.py | 134 +++++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 2 + 2 files changed, 136 insertions(+) create mode 100644 coretk/coretk/canvastooltip.py diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py new file mode 100644 index 00000000..42270809 --- /dev/null +++ b/coretk/coretk/canvastooltip.py @@ -0,0 +1,134 @@ +import tkinter as tk +from tkinter import ttk + + +class CanvasTooltip: + """ + It creates a tooltip for a given canvas tag or id as the mouse is + above it. + + This class has been derived from the original Tooltip class updated + and posted back to StackOverflow at the following link: + + https://stackoverflow.com/questions/3221956/ + what-is-the-simplest-way-to-make-tooltips-in-tkinter/ + 41079350#41079350 + + Alberto Vassena on 2016.12.10. + """ + + def __init__( + self, + canvas, + tag_or_id, + *, + bg="#FFFFEA", + pad=(5, 3, 5, 3), + text="canvas info", + waittime=400, + wraplength=250 + ): + # in miliseconds, originally 500 + self.waittime = waittime + # in pixels, originally 180 + self.wraplength = wraplength + self.canvas = canvas + self.text = text + self.canvas.tag_bind(tag_or_id, "", self.on_enter) + self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.bg = bg + self.pad = pad + self.id = None + self.tw = None + + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.hide() + + def schedule(self): + self.unschedule() + self.id = self.canvas.after(self.waittime, self.show) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.canvas.after_cancel(id_) + + def show(self, event=None): + def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): + + c = canvas + + s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() + + width, height = ( + pad[0] + label.winfo_reqwidth() + pad[2], + pad[1] + label.winfo_reqheight() + pad[3], + ) + + mouse_x, mouse_y = c.winfo_pointerxy() + + x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] + x2, y2 = x1 + width, y1 + height + + x_delta = x2 - s_width + if x_delta < 0: + x_delta = 0 + y_delta = y2 - s_height + if y_delta < 0: + y_delta = 0 + + offscreen = (x_delta, y_delta) != (0, 0) + + if offscreen: + + if x_delta: + x1 = mouse_x - tip_delta[0] - width + + if y_delta: + y1 = mouse_y - tip_delta[1] - height + + offscreen_again = y1 < 0 # out on the top + + if offscreen_again: + y1 = 0 + + return x1, y1 + + bg = self.bg + pad = self.pad + canvas = self.canvas + + # creates a toplevel window + self.tw = tk.Toplevel(canvas.master) + + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + + win = tk.Frame(self.tw, background=bg, borderwidth=0) + label = ttk.Label( + win, + text=self.text, + justify=tk.LEFT, + background=bg, + relief=tk.SOLID, + borderwidth=0, + wraplength=self.wraplength, + ) + + label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) + win.grid() + + x, y = tip_pos_calculator(canvas, label) + + self.tw.wm_geometry("+%d+%d" % (x, y)) + + def hide(self): + if self.tw: + self.tw.destroy() + self.tw = None diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 19491180..80360c44 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -4,6 +4,7 @@ import tkinter as tk from core.api.grpc import core_pb2 from coretk.canvasaction import CanvasAction +from coretk.canvastooltip import CanvasTooltip from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.interface import Interface @@ -512,6 +513,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) + self.tooltip = CanvasTooltip(self.canvas, self.id, text=self.name) self.edges = set() self.wlans = [] From 18c9904d58652491cbb3d30b7fae6ca87f2ad930 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:19:30 -0800 Subject: [PATCH 097/305] modified grpc set node service and node service file to use messages for their config, updated start session to leverage these messages to set them when starting a session --- daemon/core/api/grpc/client.py | 25 ++++++++++------- daemon/core/api/grpc/grpcutils.py | 15 +++++++++++ daemon/core/api/grpc/server.py | 20 +++++++++----- daemon/proto/core/api/grpc/core.proto | 28 ++++++++++++------- daemon/tests/test_grpc.py | 39 ++++++++++++++++++--------- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 01a7b1f6..ceec0448 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -159,6 +159,8 @@ class CoreGrpcClient: emane_model_configs=None, wlan_configs=None, mobility_configs=None, + service_configs=None, + service_file_configs=None, ): """ Start a session. @@ -169,9 +171,11 @@ class CoreGrpcClient: :param core_pb2.SessionLocation location: location to set :param list[core_pb2.Hook] hooks: session hooks to set :param dict emane_config: emane configuration to set - :param list emane_model_configs: emane model configurations to set - :param list wlan_configs: wlan configurations to set - :param list mobility_configs: mobility configurations to set + :param list emane_model_configs: node emane model configurations + :param list wlan_configs: node wlan configurations + :param list mobility_configs: node mobility configurations + :param list service_configs: node service configurations + :param list service_file_configs: node service file configurations :return: start session response :rtype: core_pb2.StartSessionResponse """ @@ -185,6 +189,8 @@ class CoreGrpcClient: emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, mobility_configs=mobility_configs, + service_configs=service_configs, + service_file_configs=service_file_configs, ) return self.stub.StartSession(request) @@ -768,14 +774,14 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceResponse :raises grpc.RpcError: when session or node doesn't exist """ - request = core_pb2.SetNodeServiceRequest( - session_id=session_id, + config = core_pb2.ServiceConfig( node_id=node_id, service=service, startup=startup, validate=validate, shutdown=shutdown, ) + request = core_pb2.SetNodeServiceRequest(session_id=session_id, config=config) return self.stub.SetNodeService(request) def set_node_service_file(self, session_id, node_id, service, file_name, data): @@ -791,12 +797,11 @@ class CoreGrpcClient: :rtype: core_pb2.SetNodeServiceFileResponse :raises grpc.RpcError: when session or node doesn't exist """ + config = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file_name, data=data + ) request = core_pb2.SetNodeServiceFileRequest( - session_id=session_id, - node_id=node_id, - service=service, - file=file_name, - data=data, + session_id=session_id, config=config ) return self.stub.SetNodeServiceFile(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 166807d0..c6b625fb 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -319,3 +319,18 @@ def session_location(session, location): session.location.refxyz = (location.x, location.y, location.z) session.location.setrefgeo(location.lat, location.lon, location.alt) session.location.refscale = location.scale + + +def service_configuration(session, config): + """ + Convenience method for setting a node service configuration. + + :param core.emulator.session.Session session: session for service configuration + :param core_pb2.ServiceConfig config: service configuration + :return: + """ + session.services.set_service(config.node_id, config.service) + service = session.services.get_service(config.node_id, config.service) + service.startup = tuple(config.startup) + service.validate = tuple(config.validate) + service.shutdown = tuple(config.shutdown) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e1e1f24d..c00cc3af 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -153,6 +153,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): config.node_id, Ns2ScriptedMobility.name, config.config ) + # service configs + for config in request.service_configs: + grpcutils.service_configuration(session, config) + + # service file configs + for config in request.service_file_configs: + session.services.set_service_file( + config.node_id, config.service, config.file, config.data + ) + # create links grpcutils.create_links(session, request.links) @@ -1172,11 +1182,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service: %s", request) session = self.get_session(request.session_id, context) - session.services.set_service(request.node_id, request.service) - service = session.services.get_service(request.node_id, request.service) - service.startup = tuple(request.startup) - service.validate = tuple(request.validate) - service.shutdown = tuple(request.shutdown) + config = request.config + grpcutils.service_configuration(session, config) return core_pb2.SetNodeServiceResponse(result=True) def SetNodeServiceFile(self, request, context): @@ -1191,8 +1198,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("set node service file: %s", request) session = self.get_session(request.session_id, context) + config = request.config session.services.set_service_file( - request.node_id, request.service, request.file, request.data + config.node_id, config.service, config.file, config.data ) return core_pb2.SetNodeServiceFileResponse(result=True) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index a53a11bb..ac7cc2ed 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -144,6 +144,8 @@ message StartSessionRequest { repeated WlanConfig wlan_configs = 7; repeated EmaneModelConfig emane_model_configs = 8; repeated MobilityConfig mobility_configs = 9; + repeated ServiceConfig service_configs = 10; + repeated ServiceFileConfig service_file_configs = 11; } message StartSessionResponse { @@ -554,11 +556,7 @@ message GetNodeServiceFileResponse { message SetNodeServiceRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - repeated string startup = 4; - repeated string validate = 5; - repeated string shutdown = 6; + ServiceConfig config = 2; } message SetNodeServiceResponse { @@ -567,10 +565,7 @@ message SetNodeServiceResponse { message SetNodeServiceFileRequest { int32 session_id = 1; - int32 node_id = 2; - string service = 3; - string file = 4; - string data = 5; + ServiceFileConfig config = 2; } message SetNodeServiceFileResponse { @@ -718,6 +713,21 @@ message EmaneModelConfig { map config = 4; } +message ServiceConfig { + int32 node_id = 1; + string service = 2; + repeated string startup = 3; + repeated string validate = 4; + repeated string shutdown = 5; +} + +message ServiceFileConfig { + int32 node_id = 1; + string service = 2; + string file = 3; + string data = 4; +} + message MessageType { enum Enum { NONE = 0; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 62ff3a22..5f934b64 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -27,7 +27,6 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - nodes = [] position = core_pb2.Position(x=50, y=100) node_one = core_pb2.Node(id=1, position=position, model="PC") position = core_pb2.Position(x=100, y=100) @@ -36,8 +35,7 @@ class TestGrpc: wlan_node = core_pb2.Node( id=3, type=NodeTypes.WIRELESS_LAN.value, position=position ) - nodes.extend([node_one, node_two, wlan_node]) - links = [] + nodes = [node_one, node_two, wlan_node] interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") interface_one = interface_helper.create_interface(node_one.id, 0) interface_two = interface_helper.create_interface(node_two.id, 0) @@ -48,12 +46,11 @@ class TestGrpc: interface_one=interface_one, interface_two=interface_two, ) - links.append(link) - hooks = [] + links = [link] hook = core_pb2.Hook( state=core_pb2.SessionState.RUNTIME, file="echo.sh", data="echo hello" ) - hooks.append(hook) + hooks = [hook] location_x = 5 location_y = 10 location_z = 15 @@ -73,7 +70,6 @@ class TestGrpc: emane_config_key = "platform_id_start" emane_config_value = "2" emane_config = {emane_config_key: emane_config_value} - model_configs = [] model_node_id = 20 model_config_key = "bandwidth" model_config_value = "500000" @@ -83,21 +79,30 @@ class TestGrpc: model=EmaneIeee80211abgModel.name, config={model_config_key: model_config_value}, ) - model_configs.append(model_config) - wlan_configs = [] + model_configs = [model_config] wlan_config_key = "range" wlan_config_value = "333" wlan_config = core_pb2.WlanConfig( node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} ) - wlan_configs.append(wlan_config) + wlan_configs = [wlan_config] mobility_config_key = "refresh_ms" mobility_config_value = "60" - mobility_configs = [] mobility_config = core_pb2.MobilityConfig( node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} ) - mobility_configs.append(mobility_config) + mobility_configs = [mobility_config] + service_config = core_pb2.ServiceConfig( + node_id=node_one.id, service="DefaultRoute", validate=["echo hello"] + ) + service_configs = [service_config] + service_file_config = core_pb2.ServiceFileConfig( + node_id=node_one.id, + service="DefaultRoute", + file="defaultroute.sh", + data="echo hello", + ) + service_file_configs = [service_file_config] # when with patch.object(CoreXmlWriter, "write"): @@ -112,6 +117,8 @@ class TestGrpc: model_configs, wlan_configs, mobility_configs, + service_configs, + service_file_configs, ) # then @@ -139,6 +146,14 @@ class TestGrpc: model_node_id, EmaneIeee80211abgModel.name ) assert set_model_config[model_config_key] == model_config_value + service = session.services.get_service( + node_one.id, service_config.service, default_service=True + ) + assert service.validate == tuple(service_config.validate) + service_file = session.services.get_service_file( + node_one, service_file_config.service, service_file_config.file + ) + assert service_file.data == service_file_config.data @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session(self, grpc_server, session_id): From 13a7aa73a1197ff18927b8d427683cd2d64436fb Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:57:26 -0800 Subject: [PATCH 098/305] draw node service configurations --- coretk/coretk/coreclient.py | 15 ++ coretk/coretk/dialogs/nodeconfig.py | 4 +- coretk/coretk/dialogs/nodeservice.py | 202 +++++++----------- coretk/coretk/dialogs/serviceconfiguration.py | 185 ++++++++++++++++ coretk/coretk/servicenodeconfig.py | 37 ++++ 5 files changed, 312 insertions(+), 131 deletions(-) create mode 100644 coretk/coretk/dialogs/serviceconfiguration.py create mode 100644 coretk/coretk/servicenodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f018a348..05a2b1e9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,6 +11,7 @@ from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig link_layer_nodes = ["switch", "hub", "wlan", "rj45", "tunnel", "emane"] @@ -104,6 +105,7 @@ class CoreClient: self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None + self.serviceconfig_manager = ServiceNodeConfig(app) def read_config(self): # read distributed server @@ -327,6 +329,13 @@ class CoreClient: ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + response = self.client.get_service_defaults(self.session_id) + for default in response.defaults: + print(default.node_type) + print(default.services) + response = self.client.get_node_service(self.session_id, 5, "FTP") + print(response) + def stop_session(self): response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) @@ -453,6 +462,12 @@ class CoreClient: if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(nid) + # set default service configurations + if node_type == core_pb2.NodeType.DEFAULT: + self.serviceconfig_manager.node_default_services_configuration( + node_id=nid, node_model=node_model + ) + self.nodes[canvas_id] = create_node self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) logging.debug( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 3f13488a..89dd7d11 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog -from coretk.dialogs.nodeservice import NodeServicesDialog +from coretk.dialogs.nodeservice import NodeService NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] DEFAULTNODES = ["router", "host", "PC"] @@ -87,7 +87,7 @@ class NodeConfigDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_services(self): - dialog = NodeServicesDialog(self, self.app, self.canvas_node) + dialog = NodeService(self, self.app, self.canvas_node) dialog.show() def click_icon(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6d92b98e..52421ca0 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -5,154 +5,98 @@ import tkinter as tk from tkinter import messagebox from coretk.dialogs.dialog import Dialog +from coretk.dialogs.serviceconfiguration import ServiceConfiguration +from coretk.widgets import CheckboxList, ListboxScroll -class NodeServicesDialog(Dialog): - def __init__(self, master, app, canvas_node): +class NodeService(Dialog): + def __init__(self, master, app, canvas_node, current_services=set()): super().__init__(master, app, "Node Services", modal=True) self.canvas_node = canvas_node - self.core_groups = [] - self.service_to_config = None - self.config_frame = None - self.services_list = None + self.groups = None + self.services = None + self.current = None + self.current_services = current_services self.draw() def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.config_frame = tk.Frame(self) - self.config_frame.columnconfigure(0, weight=1) - self.config_frame.columnconfigure(1, weight=1) - self.config_frame.columnconfigure(2, weight=1) - self.config_frame.rowconfigure(0, weight=1) - self.config_frame.grid(row=0, column=0, sticky="nsew") - self.draw_group() - self.draw_services() - self.draw_current_services() - self.draw_buttons() - def draw_group(self): - """ - draw the group tab - - :return: nothing - """ - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Group") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - listbox = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0, sticky="nsew") - listbox.bind("<>", self.handle_group_change) - - for group in sorted(self.app.core.services): - listbox.insert(tk.END, group) - - scrollbar.config(command=listbox.yview) - - def draw_services(self): - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Group services") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - self.services_list = tk.Listbox( - frame, - selectmode=tk.SINGLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - self.services_list.grid(row=1, column=0, sticky="nsew") - self.services_list.bind("<>", self.handle_service_change) - - scrollbar.config(command=self.services_list.yview) - - def draw_current_services(self): - frame = tk.Frame(self.config_frame) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") - - label = tk.Label(frame, text="Current services") - label.grid(row=0, column=0, sticky="ew") - - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=1, column=1, sticky="ns") - - listbox = tk.Listbox( - frame, - selectmode=tk.MULTIPLE, - yscrollcommand=scrollbar.set, - relief=tk.FLAT, - highlightthickness=0.5, - bd=0, - ) - listbox.grid(row=1, column=0, sticky="nsew") - - scrollbar.config(command=listbox.yview) - - def draw_buttons(self): frame = tk.Frame(self) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - frame.grid(row=1, column=0, sticky="ew") + frame.grid(stick="nsew") + frame.rowconfigure(0, weight=1) + for i in range(3): + frame.columnconfigure(i, weight=1) + self.groups = ListboxScroll(frame, text="Groups") + self.groups.grid(row=0, column=0, sticky="nsew") + for group in sorted(self.app.core.services): + self.groups.listbox.insert(tk.END, group) + self.groups.listbox.bind("<>", self.handle_group_change) + self.groups.listbox.selection_set(0) + self.services = CheckboxList( + frame, text="Services", clicked=self.service_clicked + ) + self.services.grid(row=0, column=1, sticky="nsew") + + self.current = ListboxScroll(frame, text="Selected") + self.current.grid(row=0, column=2, sticky="nsew") + for service in sorted(self.current_services): + self.current.listbox.insert(tk.END, service) + + frame = tk.Frame(self) + frame.grid(stick="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) button = tk.Button(frame, text="Configure", command=self.click_configure) button.grid(row=0, column=0, sticky="ew") - - button = tk.Button(frame, text="Apply") + button = tk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=1, sticky="ew") - - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = tk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") + # trigger group change + self.groups.listbox.event_generate("<>") + def handle_group_change(self, event): - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.display_group_services(s) + selection = self.groups.listbox.curselection() + if selection: + index = selection[0] + group = self.groups.listbox.get(index) + self.services.clear() + for service in sorted(self.app.core.services[group], key=lambda x: x.name): + checked = service.name in self.current_services + self.services.add(service.name, checked) - def display_group_services(self, group_name): - self.services_list.delete(0, tk.END) - for service in sorted(self.app.core.services[group_name], key=lambda x: x.name): - self.services_list.insert(tk.END, service.name) - - def handle_service_change(self, event): - print("select group service") - listbox = event.widget - cur_selection = listbox.curselection() - if cur_selection: - s = listbox.get(listbox.curselection()) - self.service_to_config = s - else: - self.service_to_config = None + def service_clicked(self, name, var): + if var.get() and name not in self.current_services: + self.current_services.add(name) + elif not var.get() and name in self.current_services: + self.current_services.remove(name) + self.current.listbox.delete(0, tk.END) + for name in sorted(self.current_services): + self.current.listbox.insert(tk.END, name) def click_configure(self): - if self.service_to_config is None: - messagebox.showinfo("CORE info", "Choose a service to configure.") + current_selection = self.current.listbox.curselection() + if len(current_selection): + dialog = ServiceConfiguration( + master=self, + app=self.app, + service_name=self.current.listbox.get(current_selection[0]), + canvas_node=self.canvas_node, + ) + dialog.show() else: - print(self.service_to_config) + messagebox.showinfo( + "Node service configuration", "Select a service to configure" + ) + + def click_save(self): + print("not implemented") + print(self.current_services) + + def click_cancel(self): + self.current_services = None + self.destroy() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py new file mode 100644 index 00000000..e352a2e8 --- /dev/null +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -0,0 +1,185 @@ +"Service configuration dialog" + +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images +from coretk.widgets import ListboxScroll + + +class ServiceConfiguration(Dialog): + def __init__(self, master, app, service_name, canvas_node): + super().__init__(master, app, service_name + " service", modal=True) + self.app = app + self.service_name = service_name + self.metadata = tk.StringVar() + self.filename = tk.StringVar() + self.radiovar = tk.IntVar() + self.radiovar.set(1) + self.startup_index = tk.IntVar() + self.start_time = tk.IntVar() + self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) + self.editdelete_img = Images.get(ImageEnum.EDITDELETE) + self.draw() + + def draw(self): + # self.columnconfigure(1, weight=1) + frame = tk.Frame(self) + frame1 = tk.Frame(frame) + label = tk.Label(frame1, text=self.service_name) + label.grid(row=0, column=0, sticky="ew") + frame1.grid(row=0, column=0) + frame2 = tk.Frame(frame) + # frame2.columnconfigure(0, weight=1) + # frame2.columnconfigure(1, weight=4) + label = tk.Label(frame2, text="Meta-data") + label.grid(row=0, column=0) + entry = tk.Entry(frame2, textvariable=self.metadata) + entry.grid(row=0, column=1) + frame2.grid(row=1, column=0) + frame.grid(row=0, column=0) + + frame = tk.Frame(self) + tab_parent = ttk.Notebook(frame) + tab1 = ttk.Frame(tab_parent) + tab2 = ttk.Frame(tab_parent) + tab3 = ttk.Frame(tab_parent) + tab1.columnconfigure(0, weight=1) + tab2.columnconfigure(0, weight=1) + tab3.columnconfigure(0, weight=1) + + tab_parent.add(tab1, text="Files", sticky="nsew") + tab_parent.add(tab2, text="Directories", sticky="nsew") + tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") + tab_parent.grid(row=0, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") + + # tab 1 + label = tk.Label( + tab1, text="Config files and scripts that are generated for this service." + ) + label.grid(row=0, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + label = tk.Label(frame, text="File name: ") + label.grid(row=0, column=0) + entry = tk.Entry(frame, textvariable=self.filename) + entry.grid(row=0, column=1) + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=2) + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=3) + frame.grid(row=1, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + button = tk.Radiobutton( + frame, + variable=self.radiovar, + text="Copy this source file:", + indicatoron=True, + ) + button.grid(row=0, column=0) + entry = tk.Entry(frame, state=tk.DISABLED) + entry.grid(row=0, column=1) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=2) + frame.grid(row=2, column=0, sticky="nsew") + + frame = tk.Frame(tab1) + button = tk.Radiobutton( + frame, + variable=self.radiovar, + text="Use text below for file contents:", + indicatoron=True, + ) + button.grid(row=0, column=0) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=1) + button = tk.Button(frame, text="not implemented") + button.grid(row=0, column=2) + frame.grid(row=3, column=0, sticky="nsew") + + # tab 2 + label = tk.Label( + tab2, + text="Directories required by this service that are unique for each node.", + ) + label.grid(row=0, column=0, sticky="nsew") + + # tab 3 + label_frame = tk.LabelFrame(tab3, text="Startup commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=2, column=0, sticky="nsew") + + label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=3, column=0, sticky="nsew") + + label_frame = tk.LabelFrame(tab3, text="Validate commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + label_frame.grid(row=4, column=0, sticky="nsew") + + button = tk.Button( + self, text="onle store values that have changed from their defaults" + ) + button.grid(row=2, column=0) + + frame = tk.Frame(self) + button = tk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="nsew") + button = tk.Button(frame, text="Dafults", command=self.click_defaults) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, text="Copy...", command=self.click_copy) + button.grid(row=0, column=2, sticky="nsew") + button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button.grid(row=0, column=3, sticky="nsew") + frame.grid(row=3, column=0) + + def click_apply(self, event): + print("not implemented") + + def click_defaults(self, event): + print("not implemented") + + def click_copy(self, event): + print("not implemented") + + def click_cancel(self, event): + print("not implemented") diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py new file mode 100644 index 00000000..486e646d --- /dev/null +++ b/coretk/coretk/servicenodeconfig.py @@ -0,0 +1,37 @@ +""" +service node configuration +""" +import logging + + +class ServiceNodeConfig: + def __init__(self, app): + self.app = app + self.configurations = {} + self.default_services = {} + + def node_default_services_configuration(self, node_id, node_model): + """ + set the default configurations for the default services of a node + + :param coretk.graph.CanvasNode canvas_node: canvas node object + :return: nothing + """ + session_id = self.app.core.session_id + client = self.app.core.client + if len(self.default_services) == 0: + response = client.get_service_defaults(session_id) + logging.info("session default services: %s", response) + for default in response.defaults: + self.default_services[default.node_type] = default.services + + self.configurations[node_id] = {} + + for default in self.default_services[node_model]: + response = client.get_node_service(session_id, node_id, default) + logging.info( + "servicenodeconfig.py get node service (%s), result: %s", + node_id, + response, + ) + self.configurations[node_id][default] = response.service From aa718817d08c09c396d7b1ab60b9fe112620206e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 13:23:02 -0800 Subject: [PATCH 099/305] updates to implement working observer widgets --- coretk/coretk/canvastooltip.py | 33 ++++-------------------------- coretk/coretk/coreclient.py | 32 ++++++++++++++++++++++++++--- coretk/coretk/graph.py | 31 ++++++++++++++++------------ coretk/coretk/menubar.py | 37 +++++++++++++++++++--------------- daemon/core/api/grpc/server.py | 4 ++-- 5 files changed, 74 insertions(+), 63 deletions(-) diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py index 42270809..8ace41d0 100644 --- a/coretk/coretk/canvastooltip.py +++ b/coretk/coretk/canvastooltip.py @@ -18,25 +18,14 @@ class CanvasTooltip: """ def __init__( - self, - canvas, - tag_or_id, - *, - bg="#FFFFEA", - pad=(5, 3, 5, 3), - text="canvas info", - waittime=400, - wraplength=250 + self, canvas, *, bg="#FFFFEA", pad=(5, 3, 5, 3), waittime=400, wraplength=600 ): # in miliseconds, originally 500 self.waittime = waittime # in pixels, originally 180 self.wraplength = wraplength self.canvas = canvas - self.text = text - self.canvas.tag_bind(tag_or_id, "", self.on_enter) - self.canvas.tag_bind(tag_or_id, "", self.on_leave) - self.canvas.tag_bind(tag_or_id, "", self.on_leave) + self.text = tk.StringVar() self.bg = bg self.pad = pad self.id = None @@ -61,18 +50,13 @@ class CanvasTooltip: def show(self, event=None): def tip_pos_calculator(canvas, label, *, tip_delta=(10, 5), pad=(5, 3, 5, 3)): - c = canvas - s_width, s_height = c.winfo_screenwidth(), c.winfo_screenheight() - width, height = ( pad[0] + label.winfo_reqwidth() + pad[2], pad[1] + label.winfo_reqheight() + pad[3], ) - mouse_x, mouse_y = c.winfo_pointerxy() - x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] x2, y2 = x1 + width, y1 + height @@ -84,20 +68,14 @@ class CanvasTooltip: y_delta = 0 offscreen = (x_delta, y_delta) != (0, 0) - if offscreen: - if x_delta: x1 = mouse_x - tip_delta[0] - width - if y_delta: y1 = mouse_y - tip_delta[1] - height - offscreen_again = y1 < 0 # out on the top - if offscreen_again: y1 = 0 - return x1, y1 bg = self.bg @@ -111,21 +89,18 @@ class CanvasTooltip: self.tw.wm_overrideredirect(True) win = tk.Frame(self.tw, background=bg, borderwidth=0) + win.grid() label = ttk.Label( win, - text=self.text, + textvariable=self.text, justify=tk.LEFT, background=bg, relief=tk.SOLID, borderwidth=0, wraplength=self.wraplength, ) - label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) - win.grid() - x, y = tip_pos_calculator(canvas, label) - self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 54e6a75b..73a9dd15 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -15,6 +15,17 @@ from coretk.wlannodeconfig import WlanNodeConfig NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} +OBSERVER_WIDGETS = { + "processes": "ps", + "ifconfig": "ifconfig", + "IPV4 Routes": "ip -4 ro", + "IPV6 Routes": "ip -6 ro", + "Listening sockets": "netstat -tuwnl", + "IPv4 MFC entries": "ip -4 mroute show", + "IPv6 MFC entries": "ip -6 mroute show", + "firewall rules": "iptables -L", + "IPSec policies": "setkey -DP", +} class Node: @@ -85,6 +96,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.observer = None # loaded configuration data self.servers = {} @@ -92,6 +104,7 @@ class CoreClient: self.read_config() # data for managing the current session + self.state = None self.nodes = {} self.edges = {} self.hooks = {} @@ -105,6 +118,9 @@ class CoreClient: self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None + def set_observer(self, value): + self.observer = value + def read_config(self): # read distributed server for server_config in self.app.config["servers"]: @@ -124,8 +140,11 @@ class CoreClient: def handle_events(self, event): logging.info("event: %s", event) - if event.link_event is not None: + if event.HasField("link_event"): self.app.canvas.wireless_draw.hangle_link_event(event.link_event) + elif event.HasField("session_event"): + if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: + self.state = event.session_event.event def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs @@ -159,7 +178,7 @@ class CoreClient: response = self.client.get_session(self.session_id) logging.info("joining session(%s): %s", self.session_id, response) session = response.session - session_state = session.state + self.state = session.state self.client.events(self.session_id, self.handle_events) # get hooks @@ -207,11 +226,14 @@ class CoreClient: self.app.canvas.canvas_reset_and_redraw(session) # draw tool bar appropritate with session state - if session_state == core_pb2.SessionState.RUNTIME: + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + def is_runtime(self): + return self.state == core_pb2.SessionState.RUNTIME + def create_new_session(self): """ Create a new session @@ -754,3 +776,7 @@ class CoreClient: ) configs.append(config_proto) return configs + + def run(self, node_id): + logging.info("running node(%s) cmd: %s", node_id, self.observer) + return self.client.node_command(self.session_id, node_id, self.observer).output diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 80360c44..e813ae48 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -150,7 +150,7 @@ class CanvasGraph(tk.Canvas): node.type, node.model ) n = CanvasNode( - node.position.x, node.position.y, image, name, self, node.id + node.position.x, node.position.y, image, name, self.master, node.id ) self.nodes[n.id] = n core_id_to_canvas_id[node.id] = n.id @@ -419,14 +419,7 @@ class CanvasGraph(tk.Canvas): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - node = CanvasNode( - x=x, - y=y, - image=image, - node_type=node_name, - canvas=self, - core_id=self.core.peek_id(), - ) + node = CanvasNode(x, y, image, node_name, self.master, self.core.peek_id()) self.nodes[node.id] = node self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node @@ -491,10 +484,11 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, node_type, canvas, core_id): + def __init__(self, x, y, image, node_type, app, core_id): self.image = image self.node_type = node_type - self.canvas = canvas + self.app = app + self.canvas = app.canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) @@ -506,19 +500,30 @@ class CanvasNode: x, y + 20, text=self.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) - + self.tooltip = CanvasTooltip(self.canvas) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) - self.tooltip = CanvasTooltip(self.canvas, self.id, text=self.name) + self.canvas.tag_bind(self.id, "", self.on_enter) + self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() self.wlans = [] self.moving = None + def on_enter(self, event): + if self.app.core.is_runtime() and self.app.core.observer: + self.tooltip.text.set("waiting...") + self.tooltip.on_enter(event) + output = self.app.core.run(self.core_id) + self.tooltip.text.set(output) + + def on_leave(self, event): + self.tooltip.on_leave(event) + def click(self, event): print("click") diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 3ef38d73..d590c091 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -1,6 +1,8 @@ import tkinter as tk +from functools import partial import coretk.menuaction as action +from coretk.coreclient import OBSERVER_WIDGETS class Menubar(tk.Menu): @@ -364,23 +366,26 @@ class Menubar(tk.Menu): :param tkinter.Menu widget_menu: widget_menu :return: nothing """ + var = tk.StringVar(value="none") menu = tk.Menu(widget_menu) - menu.add_command(label="None", state=tk.DISABLED) - menu.add_command(label="processes", state=tk.DISABLED) - menu.add_command(label="ifconfig", state=tk.DISABLED) - menu.add_command(label="IPv4 routes", state=tk.DISABLED) - menu.add_command(label="IPv6 routes", state=tk.DISABLED) - menu.add_command(label="OSPFv2 neighbors", state=tk.DISABLED) - menu.add_command(label="OSPFv3 neighbors", state=tk.DISABLED) - menu.add_command(label="Listening sockets", state=tk.DISABLED) - menu.add_command(label="IPv4 MFC entries", state=tk.DISABLED) - menu.add_command(label="IPv6 MFC entries", state=tk.DISABLED) - menu.add_command(label="firewall rules", state=tk.DISABLED) - menu.add_command(label="IPsec policies", state=tk.DISABLED) - menu.add_command(label="docker logs", state=tk.DISABLED) - menu.add_command(label="OSPFv3 MDR level", state=tk.DISABLED) - menu.add_command(label="PIM neighbors", state=tk.DISABLED) - menu.add_command(label="Edit...", command=self.menuaction.edit_observer_widgets) + menu.var = var + menu.add_radiobutton( + label="None", + variable=var, + value="none", + command=lambda: self.app.core.set_observer(None), + ) + for name in sorted(OBSERVER_WIDGETS): + cmd = OBSERVER_WIDGETS[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) + menu.add_radiobutton( + label="Edit...", command=self.menuaction.edit_observer_widgets + ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c00cc3af..8c987371 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -184,9 +184,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("stop session: %s", request) session = self.get_session(request.session_id, context) session.data_collect() - session.set_state(EventTypes.DATACOLLECT_STATE) + session.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) session.clear() - session.set_state(EventTypes.SHUTDOWN_STATE) + session.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) return core_pb2.StopSessionResponse(result=True) def CreateSession(self, request, context): From dd73c968301f448b9fe7a81c477c898b13f8d86f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 13:33:37 -0800 Subject: [PATCH 100/305] added observers to gui config and display them within menu --- coretk/coretk/appdirs.py | 1 + coretk/coretk/coreclient.py | 9 +++++++-- coretk/coretk/menubar.py | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appdirs.py index 553b0949..9eebd3a4 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appdirs.py @@ -45,6 +45,7 @@ def check_directory(): config = { "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], + "observers": [], } save_config(config) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 73a9dd15..8ae310c9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -101,6 +101,7 @@ class CoreClient: # loaded configuration data self.servers = {} self.custom_nodes = {} + self.custom_observers = {} self.read_config() # data for managing the current session @@ -123,14 +124,14 @@ class CoreClient: def read_config(self): # read distributed server - for server_config in self.app.config["servers"]: + for server_config in self.app.config.get("servers", []): server = CoreServer( server_config["name"], server_config["address"], server_config["port"] ) self.servers[server.name] = server # read custom nodes - for node in self.app.config["nodes"]: + for node in self.app.config.get("nodes", []): image_file = node["image"] image = Images.get_custom(image_file) custom_node = CustomNode( @@ -138,6 +139,10 @@ class CoreClient: ) self.custom_nodes[custom_node.name] = custom_node + # read observers + for observer in self.app.config.get("observers", []): + self.custom_observers[observer["name"]] = observer["cmd"] + def handle_events(self, event): logging.info("event: %s", event) if event.HasField("link_event"): diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d590c091..c39bd3f8 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -369,6 +369,10 @@ class Menubar(tk.Menu): var = tk.StringVar(value="none") menu = tk.Menu(widget_menu) menu.var = var + menu.add_command( + label="Edit Observers", command=self.menuaction.edit_observer_widgets + ) + menu.add_separator() menu.add_radiobutton( label="None", variable=var, @@ -383,9 +387,14 @@ class Menubar(tk.Menu): value=name, command=partial(self.app.core.set_observer, cmd), ) - menu.add_radiobutton( - label="Edit...", command=self.menuaction.edit_observer_widgets - ) + for name in sorted(self.app.core.custom_observers): + cmd = self.app.core.custom_observers[name] + menu.add_radiobutton( + label=name, + variable=var, + value=name, + command=partial(self.app.core.set_observer, cmd), + ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) def create_adjacency_menu(self, widget_menu): From f5480c8e352cb3b224d1c45628090cdd8ae5f706 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 14:04:50 -0800 Subject: [PATCH 101/305] updated observers dialog to create/edit/delete custom observers and save configuration --- coretk/coretk/coreclient.py | 23 +++-- .../{observerwidgets.py => observers.py} | 97 ++++++++++--------- coretk/coretk/dialogs/servers.py | 1 + coretk/coretk/menuaction.py | 4 +- coretk/coretk/menubar.py | 4 +- 5 files changed, 68 insertions(+), 61 deletions(-) rename coretk/coretk/dialogs/{observerwidgets.py => observers.py} (57%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8ae310c9..0285d60b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -84,6 +84,12 @@ class CustomNode: self.services = services +class Observer: + def __init__(self, name, cmd): + self.name = name + self.cmd = cmd + + class CoreClient: def __init__(self, app): """ @@ -124,24 +130,23 @@ class CoreClient: def read_config(self): # read distributed server - for server_config in self.app.config.get("servers", []): - server = CoreServer( - server_config["name"], server_config["address"], server_config["port"] - ) + for config in self.app.config.get("servers", []): + server = CoreServer(config["name"], config["address"], config["port"]) self.servers[server.name] = server # read custom nodes - for node in self.app.config.get("nodes", []): - image_file = node["image"] + for config in self.app.config.get("nodes", []): + image_file = config["image"] image = Images.get_custom(image_file) custom_node = CustomNode( - node["name"], image, image_file, set(node["services"]) + config["name"], image, image_file, set(config["services"]) ) self.custom_nodes[custom_node.name] = custom_node # read observers - for observer in self.app.config.get("observers", []): - self.custom_observers[observer["name"]] = observer["cmd"] + for config in self.app.config.get("observers", []): + observer = Observer(config["name"], config["cmd"]) + self.custom_observers[observer.name] = observer def handle_events(self, event): logging.info("event: %s", event) diff --git a/coretk/coretk/dialogs/observerwidgets.py b/coretk/coretk/dialogs/observers.py similarity index 57% rename from coretk/coretk/dialogs/observerwidgets.py rename to coretk/coretk/dialogs/observers.py index c1c4d170..406f62be 100644 --- a/coretk/coretk/dialogs/observerwidgets.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,36 +1,31 @@ import tkinter as tk +from coretk import appdirs +from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog -class Widget: - def __init__(self, name, command): - self.name = name - self.command = command - - -class ObserverWidgetsDialog(Dialog): +class ObserverDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Observer Widgets", modal=True) - self.config_widgets = {} - self.widgets = None + self.observers = None self.save_button = None self.delete_button = None self.selected = None self.selected_index = None self.name = tk.StringVar() - self.command = tk.StringVar() + self.cmd = tk.StringVar() self.draw() def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.draw_widgets() - self.draw_widget_fields() - self.draw_widget_buttons() + self.draw_listbox() + self.draw_form_fields() + self.draw_config_buttons() self.draw_apply_buttons() - def draw_widgets(self): + def draw_listbox(self): frame = tk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) @@ -39,15 +34,17 @@ class ObserverWidgetsDialog(Dialog): scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") - self.widgets = tk.Listbox( + self.observers = tk.Listbox( frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set ) - self.widgets.grid(row=0, column=0, sticky="nsew") - self.widgets.bind("<>", self.handle_widget_change) + self.observers.grid(row=0, column=0, sticky="nsew") + self.observers.bind("<>", self.handle_observer_change) + for name in sorted(self.app.core.custom_observers): + self.observers.insert(tk.END, name) - scrollbar.config(command=self.widgets.yview) + scrollbar.config(command=self.observers.yview) - def draw_widget_fields(self): + def draw_form_fields(self): frame = tk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) @@ -59,10 +56,10 @@ class ObserverWidgetsDialog(Dialog): label = tk.Label(frame, text="Command") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.command) + entry = tk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") - def draw_widget_buttons(self): + def draw_config_buttons(self): frame = tk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): @@ -87,58 +84,62 @@ class ObserverWidgetsDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button( - frame, text="Save Configuration", command=self.click_save_configuration - ) + button = tk.Button(frame, text="Save", command=self.click_save_config) button.grid(row=0, column=0, sticky="ew") button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_save_configuration(self): - pass + def click_save_config(self): + observers = [] + for name in sorted(self.app.core.custom_observers): + observer = self.app.core.custom_observers[name] + observers.append({"name": observer.name, "cmd": observer.cmd}) + self.app.config["observers"] = observers + appdirs.save_config(self.app.config) + self.destroy() def click_create(self): name = self.name.get() - if name not in self.config_widgets: - command = self.command.get() - widget = Widget(name, command) - self.config_widgets[name] = widget - self.widgets.insert(tk.END, name) + if name not in self.app.core.custom_observers: + cmd = self.cmd.get() + observer = Observer(name, cmd) + self.app.core.custom_observers[name] = observer + self.observers.insert(tk.END, name) def click_save(self): name = self.name.get() if self.selected: previous_name = self.selected self.selected = name - widget = self.config_widgets.pop(previous_name) - widget.name = name - widget.command = self.command.get() - self.config_widgets[name] = widget - self.widgets.delete(self.selected_index) - self.widgets.insert(self.selected_index, name) - self.widgets.selection_set(self.selected_index) + observer = self.app.core.custom_observers.pop(previous_name) + observer.name = name + observer.cmd = self.cmd.get() + self.app.core.custom_observers[name] = observer + self.observers.delete(self.selected_index) + self.observers.insert(self.selected_index, name) + self.observers.selection_set(self.selected_index) def click_delete(self): if self.selected: - self.widgets.delete(self.selected_index) - del self.config_widgets[self.selected] + self.observers.delete(self.selected_index) + del self.app.core.custom_observers[self.selected] self.selected = None self.selected_index = None self.name.set("") - self.command.set("") - self.widgets.selection_clear(0, tk.END) + self.cmd.set("") + self.observers.selection_clear(0, tk.END) self.save_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) - def handle_widget_change(self, event): - selection = self.widgets.curselection() + def handle_observer_change(self, event): + selection = self.observers.curselection() if selection: self.selected_index = selection[0] - self.selected = self.widgets.get(self.selected_index) - widget = self.config_widgets[self.selected] - self.name.set(widget.name) - self.command.set(widget.command) + self.selected = self.observers.get(self.selected_index) + observer = self.app.core.custom_observers[self.selected] + self.name.set(observer.name) + self.cmd.set(observer.cmd) self.save_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) else: diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 3ff95cde..ce6b9698 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -117,6 +117,7 @@ class ServersDialog(Dialog): ) self.app.config["servers"] = servers appdirs.save_config(self.app.config) + self.destroy() def click_create(self): name = self.name.get() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 3b33377c..f1eb51e3 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -11,7 +11,7 @@ from coretk.appdirs import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog -from coretk.dialogs.observerwidgets import ObserverWidgetsDialog +from coretk.dialogs.observers import ObserverDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -118,5 +118,5 @@ class MenuAction: dialog.show() def edit_observer_widgets(self): - dialog = ObserverWidgetsDialog(self.app, self.app) + dialog = ObserverDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index c39bd3f8..d482045a 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -388,12 +388,12 @@ class Menubar(tk.Menu): command=partial(self.app.core.set_observer, cmd), ) for name in sorted(self.app.core.custom_observers): - cmd = self.app.core.custom_observers[name] + observer = self.app.core.custom_observers[name] menu.add_radiobutton( label=name, variable=var, value=name, - command=partial(self.app.core.set_observer, cmd), + command=partial(self.app.core.set_observer, observer.cmd), ) widget_menu.add_cascade(label="Observer Widgets", menu=menu) From 691013eeb844db6644c837736fd581a8e2bdc611 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 15:35:48 -0800 Subject: [PATCH 102/305] added preferences dialog, updates to try and simplify config saving a bit, adding default preferences configurations --- coretk/coretk/app.py | 9 ++- coretk/coretk/{appdirs.py => appconfig.py} | 37 ++++++++++-- coretk/coretk/coreclient.py | 2 +- coretk/coretk/dialogs/canvasbackground.py | 2 +- coretk/coretk/dialogs/customnodes.py | 4 +- coretk/coretk/dialogs/icondialog.py | 2 +- coretk/coretk/dialogs/observers.py | 3 +- coretk/coretk/dialogs/preferences.py | 67 ++++++++++++++++++++++ coretk/coretk/dialogs/servers.py | 3 +- coretk/coretk/images.py | 2 +- coretk/coretk/menuaction.py | 7 ++- coretk/coretk/menubar.py | 10 ++-- 12 files changed, 126 insertions(+), 22 deletions(-) rename coretk/coretk/{appdirs.py => appconfig.py} (70%) create mode 100644 coretk/coretk/dialogs/preferences.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 7330f21a..276dbd6d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,7 +1,7 @@ import logging import tkinter as tk -from coretk import appdirs +from coretk import appconfig from coretk.coreclient import CoreClient from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images @@ -26,7 +26,7 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.config = appdirs.read_config() + self.config = appconfig.read() self.core = CoreClient(self) self.setup_app() self.draw() @@ -70,10 +70,13 @@ class Application(tk.Frame): menu_action = MenuAction(self, self.master) menu_action.on_quit() + def save_config(self): + appconfig.save(self.config) + if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=log_format) - appdirs.check_directory() + appconfig.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/appdirs.py b/coretk/coretk/appconfig.py similarity index 70% rename from coretk/coretk/appdirs.py rename to coretk/coretk/appconfig.py index 9eebd3a4..67f97181 100644 --- a/coretk/coretk/appdirs.py +++ b/coretk/coretk/appconfig.py @@ -1,4 +1,5 @@ import logging +import os import shutil from pathlib import Path @@ -18,6 +19,20 @@ CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +# configuration data +TERMINALS = [ + "$TERM", + "gnome-terminal --window --", + "lxterminal -e", + "konsole -e", + "xterm -e", + "aterm -e", + "eterm -e", + "rxvt -e", + "xfce4-terminal -x", +] +EDITORS = ["$EDITOR", "vim", "emacs", "gedit", "nano", "vi"] + class IndentDumper(yaml.Dumper): def increase_indent(self, flow=False, indentless=False): @@ -42,19 +57,33 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) + + if "TERM" in os.environ: + terminal = TERMINALS[0] + else: + terminal = TERMINALS[1] + if "EDITOR" in os.environ: + editor = EDITORS[0] + else: + editor = EDITORS[1] config = { + "preferences": { + "editor": editor, + "terminal": terminal, + "gui3d": "/usr/local/bin/std3d.sh", + }, "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], - "observers": [], + "observers": [{"name": "hello", "cmd": "echo hello"}], } - save_config(config) + save(config) -def read_config(): +def read(): with CONFIG_PATH.open("r") as f: return yaml.load(f, Loader=yaml.SafeLoader) -def save_config(config): +def save(config): with CONFIG_PATH.open("w") as f: yaml.dump(config, f, Dumper=IndentDumper, default_flow_style=False) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0285d60b..9e18a413 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -15,7 +15,7 @@ from coretk.wlannodeconfig import WlanNodeConfig NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} -OBSERVER_WIDGETS = { +OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", "IPV4 Routes": "ip -4 ro", diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index fe9b5f6a..5f3b253f 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -8,7 +8,7 @@ from tkinter import filedialog from PIL import Image, ImageTk -from coretk.appdirs import BACKGROUNDS_PATH +from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 20c5ae95..5caa19ff 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -2,7 +2,6 @@ import logging import tkinter as tk from pathlib import Path -from coretk import appdirs from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog @@ -189,7 +188,8 @@ class CustomNodesDialog(Dialog): } ) logging.info("saving custom nodes: %s", self.app.config["nodes"]) - appdirs.save_config(self.app.config) + self.app.save_config() + self.destroy() def click_create(self): name = self.name.get() diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 4d26e29f..bf7b08cf 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import filedialog -from coretk.appdirs import ICONS_PATH +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 406f62be..6546493d 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,6 +1,5 @@ import tkinter as tk -from coretk import appdirs from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog @@ -96,7 +95,7 @@ class ObserverDialog(Dialog): observer = self.app.core.custom_observers[name] observers.append({"name": observer.name, "cmd": observer.cmd}) self.app.config["observers"] = observers - appdirs.save_config(self.app.config) + self.app.save_config() self.destroy() def click_create(self): diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py new file mode 100644 index 00000000..74293a30 --- /dev/null +++ b/coretk/coretk/dialogs/preferences.py @@ -0,0 +1,67 @@ +import tkinter as tk +from tkinter import ttk + +from coretk import appconfig +from coretk.dialogs.dialog import Dialog + + +class PreferencesDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Preferences", modal=True) + preferences = self.app.config["preferences"] + self.editor = tk.StringVar(value=preferences["editor"]) + self.terminal = tk.StringVar(value=preferences["terminal"]) + self.gui3d = tk.StringVar(value=preferences["gui3d"]) + self.draw() + + def draw(self): + self.columnconfigure(0, weight=1) + self.draw_programs() + self.draw_buttons() + + def draw_programs(self): + frame = ttk.LabelFrame(self, text="Programs") + frame.grid(sticky="ew", pady=2) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Editor") + label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + combobox = ttk.Combobox( + frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Terminal") + label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + combobox = ttk.Combobox( + frame, + textvariable=self.terminal, + values=appconfig.TERMINALS, + state="readonly", + ) + combobox.grid(row=1, column=1, sticky="ew") + + label = ttk.Label(frame, text="3D GUI") + label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.gui3d) + entry.grid(row=2, column=1, sticky="ew") + + def draw_buttons(self): + frame = ttk.Frame(self) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + + button = ttk.Button(frame, text="Save", command=self.click_save) + button.grid(row=0, column=0, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_save(self): + preferences = self.app.config["preferences"] + preferences["terminal"] = self.terminal.get() + preferences["editor"] = self.editor.get() + preferences["gui3d"] = self.gui3d.get() + self.app.save_config() + self.destroy() diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index ce6b9698..c917ddf2 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,6 +1,5 @@ import tkinter as tk -from coretk import appdirs from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -116,7 +115,7 @@ class ServersDialog(Dialog): {"name": server.name, "address": server.address, "port": server.port} ) self.app.config["servers"] = servers - appdirs.save_config(self.app.config) + self.app.save_config() self.destroy() def click_create(self): diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f25b0eb1..d064f77e 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -4,7 +4,7 @@ from enum import Enum from PIL import Image, ImageTk from core.api.grpc import core_pb2 -from coretk.appdirs import LOCAL_ICONS_PATH +from coretk.appconfig import LOCAL_ICONS_PATH class Images: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index f1eb51e3..98b3d254 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -7,11 +7,12 @@ import webbrowser from tkinter import filedialog, messagebox from core.api.grpc import core_pb2 -from coretk.appdirs import XML_PATH +from coretk.appconfig import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.observers import ObserverDialog +from coretk.dialogs.preferences import PreferencesDialog from coretk.dialogs.servers import ServersDialog from coretk.dialogs.sessionoptions import SessionOptionsDialog from coretk.dialogs.sessions import SessionsDialog @@ -83,6 +84,10 @@ class MenuAction: self.prompt_save_running_session() self.app.core.open_xml(file_path) + def gui_preferences(self): + dialog = PreferencesDialog(self.app, self.app) + dialog.show() + def canvas_size_and_scale(self): dialog = SizeAndScaleDialog(self.app, self.app) dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d482045a..621510e2 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -2,7 +2,7 @@ import tkinter as tk from functools import partial import coretk.menuaction as action -from coretk.coreclient import OBSERVER_WIDGETS +from coretk.coreclient import OBSERVERS class Menubar(tk.Menu): @@ -94,7 +94,9 @@ class Menubar(tk.Menu): menu.add_separator() menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) menu.add_command(label="Clear marker", state=tk.DISABLED) - menu.add_command(label="Preferences...", state=tk.DISABLED) + menu.add_command( + label="Preferences...", command=self.menuaction.gui_preferences + ) self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): @@ -379,8 +381,8 @@ class Menubar(tk.Menu): value="none", command=lambda: self.app.core.set_observer(None), ) - for name in sorted(OBSERVER_WIDGETS): - cmd = OBSERVER_WIDGETS[name] + for name in sorted(OBSERVERS): + cmd = OBSERVERS[name] menu.add_radiobutton( label=name, variable=var, From a8f06da338466083ab76c94a0786999b19901294 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 15:55:39 -0800 Subject: [PATCH 103/305] converted canvas dialogs to use ttk widgets --- coretk/coretk/dialogs/canvasbackground.py | 45 ++++++------ coretk/coretk/dialogs/canvassizeandscale.py | 76 ++++++++++----------- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 5f3b253f..ed7e44f6 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -4,7 +4,7 @@ set wallpaper import enum import logging import tkinter as tk -from tkinter import filedialog +from tkinter import filedialog, ttk from PIL import Image, ImageTk @@ -39,7 +39,6 @@ class CanvasBackgroundDialog(Dialog): def draw(self): self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) self.draw_image() self.draw_image_label() self.draw_image_selection() @@ -48,65 +47,67 @@ class CanvasBackgroundDialog(Dialog): self.draw_buttons() def draw_image(self): - self.image_label = tk.Label( - self, text="(image preview)", height=8, width=32, bg="white" + self.image_label = ttk.Label( + self, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(row=0, column=0, pady=5, sticky="nsew") + self.image_label.grid(row=0, column=0, pady=5) def draw_image_label(self): - label = tk.Label(self, text="Image filename: ") + label = ttk.Label(self, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") def draw_image_selection(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.grid(row=2, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.file_name) + entry = ttk.Entry(frame, textvariable=self.file_name) entry.focus() entry.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="...", command=self.click_open_image) + button = ttk.Button(frame, text="...", command=self.click_open_image) button.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="Clear", command=self.click_clear) + button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") def draw_options(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) frame.grid(row=3, column=0, sticky="ew") - button = tk.Radiobutton( + button = ttk.Radiobutton( frame, text="upper-left", value=1, variable=self.radiovar ) button.grid(row=0, column=0, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="centered", value=2, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="centered", value=2, variable=self.radiovar + ) button.grid(row=0, column=1, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button = ttk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) button.grid(row=0, column=2, sticky="ew") self.options.append(button) - button = tk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button = ttk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) button.grid(row=0, column=3, sticky="ew") self.options.append(button) def draw_additional_options(self): - checkbutton = tk.Checkbutton( + checkbutton = ttk.Checkbutton( self, text="Show grid", variable=self.show_grid_var ) checkbutton.grid(row=4, column=0, sticky="ew", padx=5) - checkbutton = tk.Checkbutton( + checkbutton = ttk.Checkbutton( self, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, @@ -118,15 +119,15 @@ class CanvasBackgroundDialog(Dialog): self.adjust_to_dim_var.set(0) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=6, column=0, pady=5, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_open_image(self): @@ -144,7 +145,7 @@ class CanvasBackgroundDialog(Dialog): img = Image.open(filename) img = img.resize((width, height), Image.ANTIALIAS) tk_img = ImageTk.PhotoImage(img) - self.image_label.config(image=tk_img, width=width, height=height) + self.image_label.config(image=tk_img, width=width) self.image_label.image = tk_img def click_clear(self): @@ -156,7 +157,7 @@ class CanvasBackgroundDialog(Dialog): # delete entry self.file_name.set("") # delete display image - self.image_label.config(image="", width=32, height=8) + self.image_label.config(image="", width=32) def click_adjust_canvas(self): # deselect all radio buttons and grey them out diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 20b464d0..1d880639 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -2,7 +2,7 @@ size and scale """ import tkinter as tk -from tkinter import font +from tkinter import font, ttk from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog @@ -49,120 +49,120 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label = tk.Label(self, text="Size", font=self.section_font) + label = ttk.Label(self, text="Size", font=self.section_font) label.grid(sticky="w") # draw size row 1 - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="Width") + label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.pixel_width) + entry = ttk.Entry(frame, textvariable=self.pixel_width) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="x Height") + label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.pixel_height) + entry = ttk.Entry(frame, textvariable=self.pixel_height) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Pixels") + label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="Width") + label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.meters_width) + entry = ttk.Entry(frame, textvariable=self.meters_width) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="x Height") + label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.meters_height) + entry = ttk.Entry(frame, textvariable=self.meters_height) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Meters") + label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label = tk.Label(self, text="Scale", font=self.section_font) + label = ttk.Label(self, text="Scale", font=self.section_font) label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = tk.Label(frame, text="100 Pixels =") + label = ttk.Label(frame, text="100 Pixels =") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.scale) + entry = ttk.Entry(frame, textvariable=self.scale) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Meters") + label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label = tk.Label(self, text="Reference point", font=self.section_font) + label = ttk.Label(self, text="Reference point", font=self.section_font) label.grid(sticky="w") - label = tk.Label( + label = ttk.Label( self, text="Default is (0, 0), the upper left corner of the canvas" ) label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="X") + label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w") x_var = tk.StringVar(value=0) - entry = tk.Entry(frame, textvariable=x_var) + entry = ttk.Entry(frame, textvariable=x_var) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Y") + label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w") y_var = tk.StringVar(value=0) - entry = tk.Entry(frame, textvariable=y_var) + entry = ttk.Entry(frame, textvariable=y_var) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(self, text="Translates To") + label = ttk.Label(self, text="Translates To") label.grid(sticky="w") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) - label = tk.Label(frame, text="Lat") + label = ttk.Label(frame, text="Lat") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.lat) + entry = ttk.Entry(frame, textvariable=self.lat) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Lon") + label = ttk.Label(frame, text="Lon") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.lon) + entry = ttk.Entry(frame, textvariable=self.lon) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Alt") + label = ttk.Label(frame, text="Alt") label.grid(row=0, column=4, sticky="w") - entry = tk.Entry(frame, textvariable=self.alt) + entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): - button = tk.Checkbutton( + button = ttk.Checkbutton( self, text="Save as default?", variable=self.save_default ) button.grid(sticky="w", pady=3) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.grid(sticky="ew") - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, pady=5, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, pady=5, sticky="ew") def redraw_grid(self): From 58a4db6050ed8d852c043261bf693f6fc41eb056 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 11 Nov 2019 16:33:51 -0800 Subject: [PATCH 104/305] updates to convert more dialogs to use ttk widgets when possible --- coretk/coretk/dialogs/customnodes.py | 33 +++++----- coretk/coretk/dialogs/emaneconfig.py | 82 ++++++++++--------------- coretk/coretk/dialogs/hooks.py | 26 ++++---- coretk/coretk/dialogs/icondialog.py | 18 +++--- coretk/coretk/dialogs/observers.py | 29 ++++----- coretk/coretk/dialogs/servers.py | 35 ++++++----- coretk/coretk/dialogs/sessionoptions.py | 8 +-- coretk/coretk/dialogs/sessions.py | 28 ++++----- coretk/coretk/widgets.py | 16 ++--- 9 files changed, 127 insertions(+), 148 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 5caa19ff..f5c1ab78 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -1,6 +1,7 @@ import logging import tkinter as tk from pathlib import Path +from tkinter import ttk from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog @@ -21,7 +22,7 @@ class ServicesSelectDialog(Dialog): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -43,13 +44,13 @@ class ServicesSelectDialog(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.destroy) + button = ttk.Button(frame, text="Save", command=self.destroy) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") # trigger group change @@ -102,7 +103,7 @@ class CustomNodesDialog(Dialog): self.draw_buttons() def draw_node_config(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -113,45 +114,45 @@ class CustomNodesDialog(Dialog): for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) - frame = tk.Frame(frame) + frame = ttk.Frame(frame) frame.grid(row=0, column=2, sticky="nsew") frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(sticky="ew") - self.image_button = tk.Button(frame, text="Icon", command=self.click_icon) + self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon) self.image_button.grid(sticky="ew") - button = tk.Button(frame, text="Services", command=self.click_services) + button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") def draw_node_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit_button = tk.Button( + self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save) + button = ttk.Button(frame, text="Save", command=self.click_save) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def reset_values(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 6950dc10..7fd577e2 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -58,18 +58,18 @@ class EmaneConfiguration(Dialog): print("not implemented") def node_name_and_image(self): - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) - lbl = tk.Label(f, text="Node name:", bg="#d9d9d9") + lbl = ttk.Label(f, text="Node name:") lbl.grid(row=0, column=0, padx=2, pady=2) - e = tk.Entry(f, textvariable=self.create_text_variable(""), bg="white") + e = ttk.Entry(f, textvariable=self.create_text_variable("")) e.grid(row=0, column=1, padx=2, pady=2) cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") cbb.current(0) cbb.grid(row=0, column=2, padx=2, pady=2) - b = tk.Button(f, image=self.canvas_node.image) + b = ttk.Button(f, image=self.canvas_node.image) b.grid(row=0, column=3, padx=2, pady=2) f.grid(row=0, column=0, sticky="nsew") @@ -96,13 +96,13 @@ class EmaneConfiguration(Dialog): self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") - frame = tk.Frame(self.emane_dialog) + frame = ttk.Frame(self.emane_dialog) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - b1 = tk.Button(frame, text="Appy", command=self.save_emane_option) + b1 = ttk.Button(frame, text="Appy", command=self.save_emane_option) b1.grid(row=0, column=0, sticky="ew") - b2 = tk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) + b2 = ttk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) b2.grid(row=0, column=1, sticky="ew") self.emane_dialog.show() @@ -170,35 +170,33 @@ class EmaneConfiguration(Dialog): self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() - frame = tk.Frame(self.emane_model_dialog) + frame = ttk.Frame(self.emane_model_dialog) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - b1 = tk.Button(frame, text="Apply", command=self.save_emane_model_options) + b1 = ttk.Button(frame, text="Apply", command=self.save_emane_model_options) b1.grid(row=0, column=0, sticky="ew") - b2 = tk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) + b2 = ttk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) b2.grid(row=0, column=1, sticky="ew") self.emane_model_dialog.show() def draw_option_buttons(self, parent): - f = tk.Frame(parent, bg="#d9d9d9") + f = ttk.Frame(parent) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button( + b = ttk.Button( f, text=self.emane_models[0] + " options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, - bg="#d9d9d9", command=self.draw_model_options, ) b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button( + b = ttk.Button( f, text="EMANE options", image=Images.get(ImageEnum.EDITNODE), compound=tk.RIGHT, - bg="#d9d9d9", command=self.draw_emane_options, ) b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") @@ -233,21 +231,14 @@ class EmaneConfiguration(Dialog): self.emane_models = [x.split("_")[1] for x in response.models] # create combo box and its binding - f = tk.Frame( - parent, - bg="#d9d9d9", - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) + f = ttk.Frame(parent) self.emane_model_combobox = ttk.Combobox( f, values=self.emane_models, state="readonly" ) self.emane_model_combobox.grid() self.emane_model_combobox.current(0) self.emane_model_combobox.bind("<>", self.combobox_select) - f.grid(row=3, column=0, sticky=tk.W + tk.E) + f.grid(row=3, column=0, sticky="ew") def draw_text_label_and_entry(self, parent, label_text, entry_text): """ @@ -257,10 +248,10 @@ class EmaneConfiguration(Dialog): """ var = tk.StringVar() var.set(entry_text) - f = tk.Frame(parent) - lbl = tk.Label(f, text=label_text) + f = ttk.Frame(parent) + lbl = ttk.Label(f, text=label_text) lbl.grid(row=0, column=0) - e = tk.Entry(f, textvariable=var, bg="white") + e = ttk.Entry(f, textvariable=var) e.grid(row=0, column=1) f.grid(stick=tk.W, padx=2, pady=2) @@ -271,44 +262,33 @@ class EmaneConfiguration(Dialog): :return: nothing """ # draw label - lbl = tk.Label(self, text="Emane") + lbl = ttk.Label(self, text="Emane") lbl.grid(row=1, column=0) # main frame that has emane wiki, a short description, emane models and the configure buttons - f = tk.Frame( - self, - bg="#d9d9d9", - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - relief=tk.RAISED, - ) + f = ttk.Frame(self) f.columnconfigure(0, weight=1) - b = tk.Button( + b = ttk.Button( f, image=Images.get(ImageEnum.EDITNODE), text="EMANE Wiki", compound=tk.RIGHT, - relief=tk.RAISED, - bg="#d9d9d9", command=lambda: webbrowser.open_new( "https://github.com/adjacentlink/emane/wiki" ), ) - b.grid(row=0, column=0, sticky=tk.W) + b.grid(row=0, column=0, sticky="w") - lbl = tk.Label( + lbl = ttk.Label( f, text="The EMANE emulation system provides more complex wireless radio emulation " "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", - bg="#d9d9d9", ) lbl.grid(row=1, column=0, sticky="nsew") - lbl = tk.Label(f, text="EMANE Models", bg="#d9d9d9") - lbl.grid(row=2, column=0, sticky=tk.W) + lbl = ttk.Label(f, text="EMANE Models") + lbl.grid(row=2, column=0, sticky="w") self.draw_emane_models(f) self.draw_option_buttons(f) @@ -325,12 +305,12 @@ class EmaneConfiguration(Dialog): :return: """ - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Link to all routers", bg="#d9d9d9") + b = ttk.Button(f, text="Link to all routers") b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button(f, text="Choose WLAN members", bg="#d9d9d9") + b = ttk.Button(f, text="Choose WLAN members") b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") f.grid(row=5, column=0, sticky="nsew") @@ -340,12 +320,12 @@ class EmaneConfiguration(Dialog): self.destroy() def draw_apply_and_cancel(self): - f = tk.Frame(self, bg="#d9d9d9") + f = ttk.Frame(self) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) - b = tk.Button(f, text="Apply", bg="#d9d9d9", command=self.apply) + b = ttk.Button(f, text="Apply", command=self.apply) b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = tk.Button(f, text="Cancel", bg="#d9d9d9", command=self.destroy) + b = ttk.Button(f, text="Cancel", command=self.destroy) b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") f.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 99647635..5ae9da8a 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -19,14 +19,14 @@ class HookDialog(Dialog): self.rowconfigure(1, weight=1) # name and states - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, sticky="ew", pady=2) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) @@ -39,7 +39,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) @@ -53,19 +53,19 @@ class HookDialog(Dialog): ), ) self.data.grid(row=0, column=0, sticky="nsew") - scrollbar = tk.Scrollbar(frame) + scrollbar = ttk.Scrollbar(frame) scrollbar.grid(row=0, column=1, sticky="ns") self.data.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.data.yview) # button row - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, sticky="ew", pady=2) for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=lambda: self.save()) + button = ttk.Button(frame, text="Save", command=lambda: self.save()) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") def state_change(self, event): @@ -106,21 +106,21 @@ class HooksDialog(Dialog): self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.edit_button = tk.Button( + self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) self.edit_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=lambda: self.destroy()) + button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") def click_create(self): diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index bf7b08cf..fb6fb6bb 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import filedialog +from tkinter import filedialog, ttk from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog @@ -18,30 +18,30 @@ class IconDialog(Dialog): self.columnconfigure(0, weight=1) # row one - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) - label = tk.Label(frame, text="Image") + label = ttk.Label(frame, text="Image") label.grid(row=0, column=0, sticky="ew") - entry = tk.Entry(frame, textvariable=self.file_path) + entry = ttk.Entry(frame, textvariable=self.file_path) entry.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="...", command=self.click_file) + button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two - self.image_label = tk.Label(self, image=self.image) + self.image_label = ttk.Label(self, image=self.image, anchor=tk.CENTER) self.image_label.grid(row=1, column=0, pady=2, sticky="ew") # row three - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.destroy) + button = ttk.Button(frame, text="Apply", command=self.destroy) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") def click_file(self): diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 6546493d..6c57f08e 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog @@ -25,12 +26,12 @@ class ObserverDialog(Dialog): self.draw_apply_buttons() def draw_listbox(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") self.observers = tk.Listbox( @@ -44,49 +45,49 @@ class ObserverDialog(Dialog): scrollbar.config(command=self.observers.yview) def draw_form_fields(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Command") + label = ttk.Label(frame, text="Command") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.cmd) + entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( + self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) self.save_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.click_save_config) + button = ttk.Button(frame, text="Save", command=self.click_save_config) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_save_config(self): diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index c917ddf2..25f3e826 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog @@ -30,12 +31,12 @@ class ServersDialog(Dialog): self.draw_apply_buttons() def draw_servers(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=0, column=1, sticky="ns") self.servers = tk.Listbox( @@ -50,61 +51,61 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = tk.Label(self, text="Server Configuration") + label = ttk.Label(self, text="Server Configuration") label.grid(pady=2, sticky="ew") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) - label = tk.Label(frame, text="Name") + label = ttk.Label(frame, text="Name") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="Address") + label = ttk.Label(frame, text="Address") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.address) + entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") - label = tk.Label(frame, text="Port") + label = ttk.Label(frame, text="Port") label.grid(row=0, column=4, sticky="w") - entry = tk.Entry(frame, textvariable=self.port) + entry = ttk.Entry(frame, textvariable=self.port) entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Create", command=self.click_create) + button = ttk.Button(frame, text="Create", command=self.click_create) button.grid(row=0, column=0, sticky="ew") - self.save_button = tk.Button( + self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) self.save_button.grid(row=0, column=1, sticky="ew") - self.delete_button = tk.Button( + self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button( + button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_save_configuration(self): diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 8702d581..45f26a58 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,5 +1,5 @@ import logging -import tkinter as tk +from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.widgets import ConfigFrame @@ -26,13 +26,13 @@ class SessionOptionsDialog(Dialog): self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Save", command=self.save) + button = ttk.Button(frame, text="Save", command=self.save) button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") def save(self): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b7c4d128..40d09c44 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -1,6 +1,6 @@ import logging import tkinter as tk -from tkinter.ttk import Scrollbar, Treeview +from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog @@ -9,12 +9,6 @@ from coretk.images import ImageEnum, Images class SessionsDialog(Dialog): def __init__(self, master, app): - """ - create session table instance - - :param coretk.coreclient.CoreClient grpc: coregrpc - :param root.master master: - """ super().__init__(master, app, "Sessions", modal=True) self.selected = False self.selected_id = None @@ -32,7 +26,7 @@ class SessionsDialog(Dialog): write a short description :return: nothing """ - label = tk.Label( + label = ttk.Label( self, text="Below is a list of active CORE sessions. Double-click to \n" "connect to an existing session. Usually, only sessions in \n" @@ -42,7 +36,9 @@ class SessionsDialog(Dialog): label.grid(row=0, sticky="ew", pady=5) def draw_tree(self): - self.tree = Treeview(self, columns=("id", "state", "nodes"), show="headings") + self.tree = ttk.Treeview( + self, columns=("id", "state", "nodes"), show="headings" + ) self.tree.grid(row=1, sticky="nsew") self.tree.column("id", stretch=tk.YES) self.tree.heading("id", text="ID") @@ -64,20 +60,20 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=1, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) xscrollbar.grid(row=2, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.DOCUMENTNEW), text="New", @@ -85,7 +81,7 @@ class SessionsDialog(Dialog): command=self.click_new, ) b.grid(row=0, padx=2, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.FILEOPEN), text="Connect", @@ -93,7 +89,7 @@ class SessionsDialog(Dialog): command=self.click_connect, ) b.grid(row=0, column=1, padx=2, sticky="ew") - b = tk.Button( + b = ttk.Button( frame, image=Images.get(ImageEnum.EDITDELETE), text="Shutdown", @@ -101,7 +97,7 @@ class SessionsDialog(Dialog): command=self.click_shutdown, ) b.grid(row=0, column=2, padx=2, sticky="ew") - b = tk.Button(frame, text="Cancel", command=self.click_new) + b = ttk.Button(frame, text="Cancel", command=self.click_new) b.grid(row=0, column=3, padx=2, sticky="ew") def click_new(self): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 236dbc24..9ae9e851 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -26,7 +26,7 @@ class FrameScroll(tk.LabelFrame): self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar( + self.scrollbar = ttk.Scrollbar( self, orient="vertical", command=self.canvas.yview ) self.scrollbar.grid(row=0, column=1, sticky="ns") @@ -70,11 +70,11 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = tk.Frame(self.frame) + frame = ttk.Frame(self.frame) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): - label = tk.Label(frame, text=option.label) + label = ttk.Label(frame, text=option.label) label.grid(row=index, pady=pady, padx=padx, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: @@ -96,15 +96,15 @@ class ConfigFrame(FrameScroll): combobox.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = tk.Entry(frame, textvariable=value) + entry = ttk.Entry(frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) @@ -131,7 +131,7 @@ class ListboxScroll(tk.LabelFrame): super().__init__(master, cnf, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) + self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) self.scrollbar.grid(row=0, column=1, sticky="ns") self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set @@ -149,5 +149,5 @@ class CheckboxList(FrameScroll): def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = tk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 3dd3d928dd6bf8a6fd203a77785c5e49f0eb21b6 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 11 Nov 2019 16:34:41 -0800 Subject: [PATCH 105/305] more work on designing node service configuration --- coretk/coretk/dialogs/serviceconfiguration.py | 227 ++++++++++++------ coretk/coretk/icons/document-save.gif | Bin 0 -> 1049 bytes coretk/coretk/images.py | 1 + 3 files changed, 158 insertions(+), 70 deletions(-) create mode 100644 coretk/coretk/icons/document-save.gif diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index e352a2e8..016373c8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -16,11 +16,20 @@ class ServiceConfiguration(Dialog): self.metadata = tk.StringVar() self.filename = tk.StringVar() self.radiovar = tk.IntVar() - self.radiovar.set(1) + self.radiovar.set(2) self.startup_index = tk.IntVar() self.start_time = tk.IntVar() self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) self.editdelete_img = Images.get(ImageEnum.EDITDELETE) + self.tab_parent = None + self.filenames = ["test1", "test2", "test3"] + + self.metadata_entry = None + self.filename_combobox = None + self.startup_commands_listbox = None + self.shutdown_commands_listbox = None + self.validate_commands_listbox = None + self.draw() def draw(self): @@ -35,24 +44,27 @@ class ServiceConfiguration(Dialog): # frame2.columnconfigure(1, weight=4) label = tk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) - entry = tk.Entry(frame2, textvariable=self.metadata) - entry.grid(row=0, column=1) + self.metadata_entry = tk.Entry(frame2, textvariable=self.metadata) + self.metadata_entry.grid(row=0, column=1) frame2.grid(row=1, column=0) frame.grid(row=0, column=0) frame = tk.Frame(self) - tab_parent = ttk.Notebook(frame) - tab1 = ttk.Frame(tab_parent) - tab2 = ttk.Frame(tab_parent) - tab3 = ttk.Frame(tab_parent) + self.tab_parent = ttk.Notebook(frame) + tab1 = ttk.Frame(self.tab_parent) + tab2 = ttk.Frame(self.tab_parent) + tab3 = ttk.Frame(self.tab_parent) + tab4 = ttk.Frame(self.tab_parent) tab1.columnconfigure(0, weight=1) tab2.columnconfigure(0, weight=1) tab3.columnconfigure(0, weight=1) + tab4.columnconfigure(0, weight=1) - tab_parent.add(tab1, text="Files", sticky="nsew") - tab_parent.add(tab2, text="Directories", sticky="nsew") - tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") - tab_parent.grid(row=0, column=0, sticky="nsew") + self.tab_parent.add(tab1, text="Files", sticky="nsew") + self.tab_parent.add(tab2, text="Directories", sticky="nsew") + self.tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") + self.tab_parent.add(tab4, text="Configuration", sticky="nsew") + self.tab_parent.grid(row=0, column=0, sticky="nsew") frame.grid(row=1, column=0, sticky="nsew") # tab 1 @@ -64,11 +76,14 @@ class ServiceConfiguration(Dialog): frame = tk.Frame(tab1) label = tk.Label(frame, text="File name: ") label.grid(row=0, column=0) - entry = tk.Entry(frame, textvariable=self.filename) - entry.grid(row=0, column=1) + self.filename_combobox = ttk.Combobox(frame, values=self.filenames) + self.filename_combobox.grid(row=0, column=1) + self.filename_combobox.current(0) button = tk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_filename) button.grid(row=0, column=2) button = tk.Button(frame, image=self.editdelete_img) + button.bind("", self.delete_filename) button.grid(row=0, column=3) frame.grid(row=1, column=0, sticky="nsew") @@ -78,11 +93,13 @@ class ServiceConfiguration(Dialog): variable=self.radiovar, text="Copy this source file:", indicatoron=True, + value=1, + state="disabled", ) button.grid(row=0, column=0) entry = tk.Entry(frame, state=tk.DISABLED) entry.grid(row=0, column=1) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.FILEOPEN)) button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew") @@ -92,11 +109,12 @@ class ServiceConfiguration(Dialog): variable=self.radiovar, text="Use text below for file contents:", indicatoron=True, + value=2, ) button.grid(row=0, column=0) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.FILEOPEN)) button.grid(row=0, column=1) - button = tk.Button(frame, text="not implemented") + button = tk.Button(frame, image=Images.get(ImageEnum.DOCUMENTSAVE)) button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") @@ -108,53 +126,64 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0, sticky="nsew") # tab 3 - label_frame = tk.LabelFrame(tab3, text="Startup commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, stick="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=2, column=0, sticky="nsew") + for i in range(3): + label_frame = None + if i == 0: + label_frame = tk.LabelFrame(tab3, text="Startup commands") + elif i == 1: + label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + elif i == 2: + label_frame = tk.LabelFrame(tab3, text="Validation commands") + label_frame.columnconfigure(0, weight=1) + frame = tk.Frame(label_frame) + frame.columnconfigure(0, weight=1) + entry = tk.Entry(frame, textvariable=tk.StringVar()) + entry.grid(row=0, column=0, stick="nsew") + button = tk.Button(frame, image=self.documentnew_img) + button.bind("", self.add_command) + button.grid(row=0, column=1, sticky="nsew") + button = tk.Button(frame, image=self.editdelete_img) + button.grid(row=0, column=2, sticky="nsew") + button.bind("", self.delete_command) + frame.grid(row=0, column=0, sticky="nsew") + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.bind("<>", self.update_entry) + listbox_scroll.listbox.config(height=4) + listbox_scroll.grid(row=1, column=0, sticky="nsew") + if i == 0: + self.startup_commands_listbox = listbox_scroll.listbox + elif i == 1: + self.shutdown_commands_listbox = listbox_scroll.listbox + elif i == 2: + self.validate_commands_listbox = listbox_scroll.listbox + label_frame.grid(row=i, column=0, sticky="nsew") - label_frame = tk.LabelFrame(tab3, text="Shutdown commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=3, column=0, sticky="nsew") + # tab 4 + for i in range(2): + if i == 0: + label_frame = tk.LabelFrame(tab4, text="Executables") + elif i == 1: + label_frame = tk.LabelFrame(tab4, text="Dependencies") - label_frame = tk.LabelFrame(tab3, text="Validate commands") - label_frame.columnconfigure(0, weight=1) - frame = tk.Frame(label_frame) - frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, image=self.documentnew_img) - button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=1, column=0, sticky="nsew") - label_frame.grid(row=4, column=0, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.listbox.config(height=4, state="disabled") + listbox_scroll.grid(row=0, column=0, sticky="nsew") + label_frame.grid(row=i, column=0, sticky="nsew") + + for i in range(3): + frame = tk.Frame(tab4) + frame.columnconfigure(0, weight=1) + if i == 0: + label = tk.Label(frame, text="Validation time:") + elif i == 1: + label = tk.Label(frame, text="Validation mode:") + elif i == 2: + label = tk.Label(frame, text="Validation period:") + label.grid(row=i, column=0) + entry = tk.Entry(frame, state="disabled", textvariable=tk.StringVar()) + entry.grid(row=i, column=1) + frame.grid(row=2 + i, column=0, sticky="nsew") button = tk.Button( self, text="onle store values that have changed from their defaults" @@ -164,22 +193,80 @@ class ServiceConfiguration(Dialog): frame = tk.Frame(self) button = tk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="nsew") - button = tk.Button(frame, text="Dafults", command=self.click_defaults) + button = tk.Button( + frame, text="Dafults", command=self.click_defaults, state="disabled" + ) button.grid(row=0, column=1, sticky="nsew") - button = tk.Button(frame, text="Copy...", command=self.click_copy) + button = tk.Button( + frame, text="Copy...", command=self.click_copy, state="disabled" + ) button.grid(row=0, column=2, sticky="nsew") - button = tk.Button(frame, text="Cancel", command=self.click_cancel) + button = tk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=3, sticky="nsew") frame.grid(row=3, column=0) - def click_apply(self, event): + def add_filename(self, event): + frame_contains_button = event.widget.master + combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename not in combobox["values"]: + combobox["values"] += (filename,) + + def delete_filename(self, event): + frame_comntains_button = event.widget.master + combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] + filename = combobox.get() + if filename in combobox["values"]: + combobox["values"] = tuple([x for x in combobox["values"] if x != filename]) + combobox.set("") + + def add_command(self, event): + frame_contains_button = event.widget.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + command_to_add = frame_contains_button.grid_slaves(row=0, column=0)[0].get() + if command_to_add == "": + return + for cmd in listbox.get(0, tk.END): + if cmd == command_to_add: + return + listbox.insert(tk.END, command_to_add) + + def update_entry(self, event): + listbox = event.widget + current_selection = listbox.curselection() + if len(current_selection) > 0: + cmd = listbox.get(current_selection[0]) + entry = listbox.master.master.grid_slaves(row=0, column=0)[0].grid_slaves( + row=0, column=0 + )[0] + entry.delete(0, "end") + entry.insert(0, cmd) + + def delete_command(self, event): + button = event.widget + frame_contains_button = button.master + listbox = frame_contains_button.master.grid_slaves(row=1, column=0)[0].listbox + current_selection = listbox.curselection() + if len(current_selection) > 0: + listbox.delete(current_selection[0]) + entry = frame_contains_button.grid_slaves(row=0, column=0)[0] + entry.delete(0, tk.END) + + def click_apply(self): + metadata = self.metadata_entry.get() + filenames = list(self.filename_combobox["values"]) + startup_commands = self.startup_commands_listbox.get(0, "end") + shutdown_commands = self.shutdown_commands_listbox.get(0, "end") + validate_commands = self.validate_commands_listbox.get(0, "end") + print( + metadata, filenames, startup_commands, shutdown_commands, validate_commands + ) + + def click_defaults(self): print("not implemented") - def click_defaults(self, event): + def click_copy(self): print("not implemented") - def click_copy(self, event): - print("not implemented") - - def click_cancel(self, event): + def click_cancel(self): print("not implemented") diff --git a/coretk/coretk/icons/document-save.gif b/coretk/coretk/icons/document-save.gif new file mode 100644 index 0000000000000000000000000000000000000000..165bcb908bbcf12dfb5d0c06a933898ed8f3944c GIT binary patch literal 1049 zcmZ?wbhEHb6krfw_}@YinU=Z|&e<14NFF4ld44ZmzCg z9-cm4UOwJlzTVz`KE46IegS^|fqwo${sBS$K0$$j!2!Xcfx)3cp)oNjndzz7*?C3z zMP~yN%>eKber00=&?_cmGkk~>bNa@fGjG?NeY^JT=M^U&Y&`d1?~!8%PM$h?>ddk8*N$Dd za{9`Zi`TASzIprlov%mkeLME($C-x@?>~Na|H+SwPk&x|{`1<4pVuBfe(>n=gU62_ zK7R7>$Hy=K} z|M>C!r_Ue1e*OIQ+n2B3zJB}m^~aANzyJLH144iP{{8#^|9^&2K;sZl{K>+|z|h5@ z1F{E{Cm1-sF^F=Ccsy!8$gjkvB_r{uXI8tKnApdQix+hmo2Jb%Ol)TFmv^iC!Pvqf zz}X|^|3g!G#rb}@hBp$Mij5v0GmpRb7dnSnV*-j_uJ)us;Cr9J~LHYW5xwW<>NUDdbt97Hy(V% z$)(wGNaN(?!^}$Z!Q1vIwxqgFGm4t@LgC3m=0&Gx2#Oj_k-W&p?c^l?(@>>DK-r^7 z#8T-<69WeWk4D0U1x##QykZIr7Y;NuvhgYeEGT%$)W&^S(;-8_p_!5K0y9g4-(LnM KMj0k125SJ$J7sYI literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index f25b0eb1..03e7fc01 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -101,6 +101,7 @@ class ImageEnum(Enum): OBSERVE = "observe" RUN = "run" DOCUMENTNEW = "document-new" + DOCUMENTSAVE = "document-save" FILEOPEN = "fileopen" EDITDELETE = "edit-delete" ANTENNA = "antenna" From 469e32b8909053bb4e262f0c892c4edf34447505 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 12:13:53 -0800 Subject: [PATCH 106/305] finished converting dialogs to use ttk --- coretk/coretk/dialogs/mobilityconfig.py | 67 ++++++++++++------------- coretk/coretk/dialogs/nodeconfig.py | 22 ++++---- coretk/coretk/dialogs/nodeservice.py | 30 +++++------ coretk/coretk/dialogs/wlanconfig.py | 64 +++++++++++------------ 4 files changed, 87 insertions(+), 96 deletions(-) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 7f99df18..68c557ee 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -2,11 +2,10 @@ mobility configuration """ -import os import tkinter as tk -from pathlib import Path -from tkinter import filedialog +from tkinter import filedialog, ttk +from coretk import appconfig from coretk.dialogs.dialog import Dialog @@ -40,12 +39,12 @@ class MobilityConfiguration(Dialog): return var def open_file(self, entry): - configs_dir = os.path.join(Path.home(), ".core/configs") - if os.path.isdir(configs_dir): - filename = filedialog.askopenfilename(initialdir=configs_dir, title="Open") - if filename: - entry.delete(0, tk.END) - entry.insert(0, filename) + filename = filedialog.askopenfilename( + initialdir=str(appconfig.MOBILITY_PATH), title="Open" + ) + if filename: + entry.delete(0, tk.END) + entry.insert(0, filename) def set_loop_value(self, value): """ @@ -58,26 +57,26 @@ class MobilityConfiguration(Dialog): def create_label_entry_filebrowser( self, parent_frame, text_label, entry_text, filebrowser=False ): - f = tk.Frame(parent_frame, bg="#d9d9d9") - lbl = tk.Label(f, text=text_label, bg="#d9d9d9") + f = ttk.Frame(parent_frame, bg="#d9d9d9") + lbl = ttk.Label(f, text=text_label, bg="#d9d9d9") lbl.grid(padx=3, pady=3) # f.grid() - e = tk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") + e = ttk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: - b = tk.Button(f, text="...", command=lambda: self.open_file(e)) + b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) b.grid(row=0, column=2, padx=3, pady=3) f.grid(sticky=tk.E) def mobility_script_parameters(self): - lbl = tk.Label(self, text="node ns2script") - lbl.grid(sticky=tk.W + tk.E) + lbl = ttk.Label(self, text="node ns2script") + lbl.grid(sticky="ew") - sb = tk.Scrollbar(self, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky=tk.N + tk.S + tk.E) + sb = ttk.Scrollbar(self, orient=tk.VERTICAL) + sb.grid(row=1, column=1, sticky="ns") - f = tk.Frame(self, bg="#d9d9d9") - lbl = tk.Label( + f = ttk.Frame(self, bg="#d9d9d9") + lbl = ttk.Label( f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED ) lbl.grid(row=0, column=0, sticky=tk.W) @@ -99,21 +98,21 @@ class MobilityConfiguration(Dialog): f1, "Refresh time (ms)", self.node_config["refresh_ms"] ) - # f12 = tk.Frame(f1) + # f12 = ttk.Frame(f1) # - # lbl = tk.Label(f12, text="Refresh time (ms)") + # lbl = ttk.Label(f12, text="Refresh time (ms)") # lbl.grid() # - # e = tk.Entry(f12, textvariable=self.create_string_var("50")) + # e = ttk.Entry(f12, textvariable=self.create_string_var("50")) # e.grid(row=0, column=1) # f12.grid() - f13 = tk.Frame(f1) + f13 = ttk.Frame(f1) - lbl = tk.Label(f13, text="loop") + lbl = ttk.Label(f13, text="loop") lbl.grid() - om = tk.OptionMenu( + om = ttk.OptionMenu( f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value ) om.grid(row=0, column=1) @@ -123,24 +122,24 @@ class MobilityConfiguration(Dialog): self.create_label_entry_filebrowser( f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] ) - # f14 = tk.Frame(f1) + # f14 = ttk.Frame(f1) # - # lbl = tk.Label(f14, text="auto-start seconds (0.0 for runtime)") + # lbl = ttk.Label(f14, text="auto-start seconds (0.0 for runtime)") # lbl.grid() # - # e = tk.Entry(f14, textvariable=self.create_string_var("")) + # e = ttk.Entry(f14, textvariable=self.create_string_var("")) # e.grid(row=0, column=1) # # f14.grid() self.create_label_entry_filebrowser( f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] ) - # f15 = tk.Frame(f1) + # f15 = ttk.Frame(f1) # - # lbl = tk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") + # lbl = ttk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") # lbl.grid() # - # e = tk.Entry(f15, textvariable=self.create_string_var("")) + # e = ttk.Entry(f15, textvariable=self.create_string_var("")) # e.grid(row=0, column=1) # # f15.grid() @@ -230,9 +229,9 @@ class MobilityConfiguration(Dialog): :return: nothing """ - f = tk.Frame(self) - b = tk.Button(f, text="Apply", command=self.ns2script_apply) + f = ttk.Frame(self) + b = ttk.Button(f, text="Apply", command=self.ns2script_apply) b.grid() - b = tk.Button(f, text="Cancel", command=self.destroy) + b = ttk.Button(f, text="Cancel", command=self.destroy) b.grid(row=0, column=1) f.grid() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 3f13488a..be41afb5 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,13 +1,11 @@ import tkinter as tk from tkinter import ttk +from coretk.coreclient import DEFAULT_NODES from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeServicesDialog -NETWORKNODETYPES = ["switch", "hub", "wlan", "rj45", "tunnel"] -DEFAULTNODES = ["router", "host", "PC"] - class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -34,17 +32,17 @@ class NodeConfigDialog(Dialog): self.draw_third_row() def draw_first_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - entry = tk.Entry(frame, textvariable=self.name) + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") combobox = ttk.Combobox( - frame, textvariable=self.type, values=DEFAULTNODES, state="readonly" + frame, textvariable=self.type, values=DEFAULT_NODES, state="readonly" ) combobox.grid(row=0, column=1, padx=2, sticky="ew") @@ -57,15 +55,15 @@ class NodeConfigDialog(Dialog): combobox.grid(row=0, column=2, sticky="ew") def draw_second_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=1, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Services", command=self.click_services) + button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(row=0, column=0, padx=2, sticky="ew") - self.image_button = tk.Button( + self.image_button = ttk.Button( frame, text="Icon", image=self.image, @@ -75,15 +73,15 @@ class NodeConfigDialog(Dialog): self.image_button.grid(row=0, column=1, sticky="ew") def draw_third_row(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = tk.Button(frame, text="Apply", command=self.config_apply) + button = ttk.Button(frame, text="Apply", command=self.config_apply) button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_services(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6d92b98e..fe31457c 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -2,7 +2,7 @@ core node services """ import tkinter as tk -from tkinter import messagebox +from tkinter import messagebox, ttk from coretk.dialogs.dialog import Dialog @@ -20,7 +20,7 @@ class NodeServicesDialog(Dialog): def draw(self): self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) - self.config_frame = tk.Frame(self) + self.config_frame = ttk.Frame(self) self.config_frame.columnconfigure(0, weight=1) self.config_frame.columnconfigure(1, weight=1) self.config_frame.columnconfigure(2, weight=1) @@ -37,15 +37,15 @@ class NodeServicesDialog(Dialog): :return: nothing """ - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=0, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Group") + label = ttk.Label(frame, text="Group") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( @@ -65,15 +65,15 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=listbox.yview) def draw_services(self): - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=1, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Group services") + label = ttk.Label(frame, text="Group services") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") self.services_list = tk.Listbox( @@ -90,15 +90,15 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=self.services_list.yview) def draw_current_services(self): - frame = tk.Frame(self.config_frame) + frame = ttk.Frame(self.config_frame) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) frame.grid(row=0, column=2, padx=3, pady=3, sticky="nsew") - label = tk.Label(frame, text="Current services") + label = ttk.Label(frame, text="Current services") label.grid(row=0, column=0, sticky="ew") - scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) scrollbar.grid(row=1, column=1, sticky="ns") listbox = tk.Listbox( @@ -114,19 +114,19 @@ class NodeServicesDialog(Dialog): scrollbar.config(command=listbox.yview) def draw_buttons(self): - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.grid(row=1, column=0, sticky="ew") - button = tk.Button(frame, text="Configure", command=self.click_configure) + button = ttk.Button(frame, text="Configure", command=self.click_configure) button.grid(row=0, column=0, sticky="ew") - button = tk.Button(frame, text="Apply") + button = ttk.Button(frame, text="Apply") button.grid(row=0, column=1, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=2, sticky="ew") def handle_group_change(self, event): diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 78dc2c22..d33f7ca0 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -3,6 +3,7 @@ wlan configuration """ import tkinter as tk +from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog @@ -10,12 +11,6 @@ from coretk.dialogs.icondialog import IconDialog class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node, config): - """ - create an instance of WlanConfiguration - - :param coretk.grpah.CanvasGraph canvas: canvas object - :param coretk.graph.CanvasNode canvas_node: canvas node object - """ super().__init__( master, app, f"{canvas_node.name} Wlan Configuration", modal=True ) @@ -48,14 +43,14 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") frame.columnconfigure(0, weight=1) - entry = tk.Entry(frame, textvariable=self.name, bg="white") + entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=0, padx=2, sticky="ew") - self.image_button = tk.Button(frame, image=self.image, command=self.click_icon) + self.image_button = ttk.Button(frame, image=self.image, command=self.click_icon) self.image_button.grid(row=0, column=1, padx=3) def draw_wlan_config(self): @@ -64,15 +59,15 @@ class WlanConfigDialog(Dialog): :return: nothing """ - label = tk.Label(self, text="Wireless") + label = ttk.Label(self, text="Wireless") label.grid(sticky="w", pady=2) - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - label = tk.Label( + label = ttk.Label( frame, text=( "The basic range model calculates on/off " @@ -81,29 +76,29 @@ class WlanConfigDialog(Dialog): ) label.grid(row=0, columnspan=2, pady=2, sticky="ew") - label = tk.Label(frame, text="Range") + label = ttk.Label(frame, text="Range") label.grid(row=1, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.range_var) + entry = ttk.Entry(frame, textvariable=self.range_var) entry.grid(row=1, column=1, sticky="ew") - label = tk.Label(frame, text="Bandwidth (bps)") + label = ttk.Label(frame, text="Bandwidth (bps)") label.grid(row=2, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.bandwidth_var) + entry = ttk.Entry(frame, textvariable=self.bandwidth_var) entry.grid(row=2, column=1, sticky="ew") - label = tk.Label(frame, text="Delay (us)") + label = ttk.Label(frame, text="Delay (us)") label.grid(row=3, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.delay_var) + entry = ttk.Entry(frame, textvariable=self.delay_var) entry.grid(row=3, column=1, sticky="ew") - label = tk.Label(frame, text="Loss (%)") + label = ttk.Label(frame, text="Loss (%)") label.grid(row=4, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.loss_var) + entry = ttk.Entry(frame, textvariable=self.loss_var) entry.grid(row=4, column=1, sticky="ew") - label = tk.Label(frame, text="Jitter (us)") + label = ttk.Label(frame, text="Jitter (us)") label.grid(row=5, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.jitter_var) + entry = ttk.Entry(frame, textvariable=self.jitter_var) entry.grid(row=5, column=1, sticky="ew") def draw_subnet(self): @@ -113,19 +108,19 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=3, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) - label = tk.Label(frame, text="IPv4 Subnet") + label = ttk.Label(frame, text="IPv4 Subnet") label.grid(row=0, column=0, sticky="w") - entry = tk.Entry(frame, textvariable=self.ip4_subnet) + entry = ttk.Entry(frame, textvariable=self.ip4_subnet) entry.grid(row=0, column=1, sticky="ew") - label = tk.Label(frame, text="IPv6 Subnet") + label = ttk.Label(frame, text="IPv6 Subnet") label.grid(row=0, column=2, sticky="w") - entry = tk.Entry(frame, textvariable=self.ip6_subnet) + entry = ttk.Entry(frame, textvariable=self.ip6_subnet) entry.grid(row=0, column=3, sticky="ew") def draw_wlan_buttons(self): @@ -135,18 +130,18 @@ class WlanConfigDialog(Dialog): :return: """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="ns-2 mobility script...") + button = ttk.Button(frame, text="ns-2 mobility script...") button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Link to all routers") + button = ttk.Button(frame, text="Link to all routers") button.grid(row=0, column=1, padx=2, sticky="ew") - button = tk.Button(frame, text="Choose WLAN members") + button = ttk.Button(frame, text="Choose WLAN members") button.grid(row=0, column=2, padx=2, sticky="ew") def draw_apply_buttons(self): @@ -155,15 +150,15 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = tk.Frame(self) + frame = ttk.Frame(self) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) - button = tk.Button(frame, text="Apply", command=self.click_apply) + button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, padx=2, sticky="ew") - button = tk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, padx=2, sticky="ew") def click_icon(self): @@ -188,7 +183,6 @@ class WlanConfigDialog(Dialog): jitter = self.jitter_var.get() # set wireless node configuration here - wlanconfig_manager = self.app.core.wlanconfig_management wlanconfig_manager.set_custom_config( node_id=self.canvas_node.core_id, From 96abea311f32601a7771cb35ac3478a6a03b7dfb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 12:47:29 -0800 Subject: [PATCH 107/305] cleanup for padding to canvas dialogs --- coretk/coretk/dialogs/canvasbackground.py | 13 ++-- coretk/coretk/dialogs/canvassizeandscale.py | 83 +++++++++++---------- coretk/coretk/dialogs/dialog.py | 4 +- 3 files changed, 55 insertions(+), 45 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index ed7e44f6..8b1c4cf4 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -11,6 +11,8 @@ from PIL import Image, ImageTk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog +PADX = 5 + class ScaleOption(enum.Enum): NONE = 0 @@ -65,10 +67,10 @@ class CanvasBackgroundDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.file_name) entry.focus() - entry.grid(row=0, column=0, sticky="ew") + entry.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -105,7 +107,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self, text="Show grid", variable=self.show_grid_var ) - checkbutton.grid(row=4, column=0, sticky="ew", padx=5) + checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self, @@ -113,7 +115,7 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim_var, command=self.click_adjust_canvas, ) - checkbutton.grid(row=5, column=0, sticky="ew", padx=5) + checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) self.show_grid_var.set(1) self.adjust_to_dim_var.set(0) @@ -125,7 +127,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -206,7 +208,6 @@ class CanvasBackgroundDialog(Dialog): return def upper_left(self, img): - print("upperleft") tk_img = ImageTk.PhotoImage(img) # crop image if it is bigger than canvas diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1d880639..1af71c93 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -8,6 +8,9 @@ from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] +FRAME_BAD = 5 +PAD = (0, 0, 5, 0) +PADX = 5 class SizeAndScaleDialog(Dialog): @@ -49,101 +52,105 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label = ttk.Label(self, text="Size", font=self.section_font) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Size", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) # draw size row 1 - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.pixel_width) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.pixel_height) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.meters_width) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.meters_height) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label = ttk.Label(self, text="Scale", font=self.section_font) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Scale", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="100 Pixels =") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.scale) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label = ttk.Label(self, text="Reference point", font=self.section_font) - label.grid(sticky="w") - label = ttk.Label( - self, text="Default is (0, 0), the upper left corner of the canvas" - ) - label.grid(sticky="w") + label_frame = ttk.Labelframe(self, text="Reference Point", padding=FRAME_BAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self) + label = ttk.Label( + label_frame, text="Default is (0, 0), the upper left corner of the canvas" + ) + label.grid() + + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) x_var = tk.StringVar(value=0) entry = ttk.Entry(frame, textvariable=x_var) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) y_var = tk.StringVar(value=0) entry = ttk.Entry(frame, textvariable=y_var) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) - label = ttk.Label(self, text="Translates To") - label.grid(sticky="w") + label = ttk.Label(label_frame, text="Translates To") + label.grid() - frame = ttk.Frame(self) + frame = ttk.Frame(label_frame) frame.grid(sticky="ew", pady=3) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.lat) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.lon) - entry.grid(row=0, column=3, sticky="ew") + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") @@ -160,10 +167,10 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, pady=5, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, pady=5, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def redraw_grid(self): """ diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index f9cfcabe..908523f2 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -2,10 +2,12 @@ import tkinter as tk from coretk.images import ImageEnum, Images +DIALOG_PAD = 5 + class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): - super().__init__(master, padx=5, pady=5) + super().__init__(master, padx=DIALOG_PAD, pady=DIALOG_PAD) self.withdraw() self.app = app self.modal = modal From b96f8ff999f0fc9194d5cd7e78b010e30d722652 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 13:01:58 -0800 Subject: [PATCH 108/305] updated standard tooltip to use delays and have the same style of the canvas tooltip --- coretk/coretk/toolbar.py | 14 +++++++------- coretk/coretk/tooltip.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 5b2c83bd..752b1000 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,7 @@ from functools import partial from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images -from coretk.tooltip import CreateToolTip +from coretk.tooltip import Tooltip class Toolbar(tk.Frame): @@ -188,7 +188,7 @@ class Toolbar(tk.Frame): button = tk.Button(frame, width=self.width, height=self.height, image=image) button.bind("", lambda e: func()) button.grid(pady=1) - CreateToolTip(button, tooltip) + Tooltip(button, tooltip) def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): button = tk.Radiobutton( @@ -202,14 +202,14 @@ class Toolbar(tk.Frame): command=func, ) button.grid() - CreateToolTip(button, tooltip_msg) + Tooltip(button, tooltip_msg) def create_regular_button(self, frame, image, func, tooltip): button = tk.Button( frame, width=self.width, height=self.height, image=image, command=func ) button.grid() - CreateToolTip(button, tooltip) + Tooltip(button, tooltip) def click_selection_tool(self): logging.debug("clicked selection tool") @@ -274,7 +274,7 @@ class Toolbar(tk.Frame): ) self.node_button.bind("", lambda e: self.draw_node_picker()) self.node_button.grid() - CreateToolTip(self.node_button, "Network-layer virtual nodes") + Tooltip(self.node_button, "Network-layer virtual nodes") def draw_network_picker(self): """ @@ -323,7 +323,7 @@ class Toolbar(tk.Frame): "", lambda e: self.draw_network_picker() ) self.network_button.grid() - CreateToolTip(self.network_button, "link-layer nodes") + Tooltip(self.network_button, "link-layer nodes") def draw_annotation_picker(self): """ @@ -369,7 +369,7 @@ class Toolbar(tk.Frame): "", lambda e: self.draw_annotation_picker() ) self.annotation_button.grid() - CreateToolTip(self.annotation_button, "background annotation tools") + Tooltip(self.annotation_button, "background annotation tools") def create_observe_button(self): menu_button = tk.Menubutton( diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 6fbbc3c9..1877fd24 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -1,7 +1,8 @@ import tkinter as tk +from tkinter import ttk -class CreateToolTip(object): +class Tooltip(object): """ Create tool tip for a given widget """ @@ -9,10 +10,29 @@ class CreateToolTip(object): def __init__(self, widget, text="widget info"): self.widget = widget self.text = text - self.widget.bind("", self.enter) - self.widget.bind("", self.close) + self.widget.bind("", self.on_enter) + self.widget.bind("", self.on_leave) + self.waittime = 400 + self.id = None self.tw = None + def on_enter(self, event=None): + self.schedule() + + def on_leave(self, event=None): + self.unschedule() + self.close(event) + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.waittime, self.enter) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.widget.after_cancel(id_) + def enter(self, event=None): x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() @@ -21,13 +41,13 @@ class CreateToolTip(object): self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) - label = tk.Label( + label = ttk.Label( self.tw, text=self.text, justify=tk.LEFT, - background="#ffffe6", - relief="solid", - borderwidth=1, + background="#FFFFEA", + relief=tk.SOLID, + borderwidth=0, ) label.grid(padx=1) From 14187ba79c4471302a53660e248242668e139268 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 17:32:34 -0800 Subject: [PATCH 109/305] tweaks to retrieving images/icons, updates to toolbar to use ttk widgets --- coretk/coretk/app.py | 9 +- coretk/coretk/coreclient.py | 4 +- coretk/coretk/dialogs/dialog.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 14 +- coretk/coretk/dialogs/icondialog.py | 2 +- coretk/coretk/dialogs/sessions.py | 20 +- coretk/coretk/graph.py | 4 +- coretk/coretk/graph_helper.py | 3 +- coretk/coretk/images.py | 90 ++++---- coretk/coretk/toolbar.py | 307 ++++++++++++--------------- 10 files changed, 221 insertions(+), 234 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 276dbd6d..452d5397 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from tkinter import ttk from coretk import appconfig from coretk.coreclient import CoreClient @@ -36,7 +37,7 @@ class Application(tk.Frame): self.master.title("CORE") self.master.geometry("1000x800") self.master.protocol("WM_DELETE_WINDOW", self.on_closing) - image = Images.get(ImageEnum.CORE) + image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) @@ -53,17 +54,17 @@ class Application(tk.Frame): self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) ) self.canvas.pack(fill=tk.BOTH, expand=True) - scroll_x = tk.Scrollbar( + scroll_x = ttk.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 = ttk.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) def draw_status(self): - self.statusbar = tk.Frame(self) + self.statusbar = ttk.Frame(self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) def on_closing(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 9e18a413..d28ad49e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,7 @@ from core.api.grpc import client, core_pb2 from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig -from coretk.images import Images +from coretk.images import NODE_WIDTH, Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -137,7 +137,7 @@ class CoreClient: # read custom nodes for config in self.app.config.get("nodes", []): image_file = config["image"] - image = Images.get_custom(image_file) + image = Images.get_custom(image_file, NODE_WIDTH) custom_node = CustomNode( config["name"], image, image_file, set(config["services"]) ) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 908523f2..c043a47a 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -13,7 +13,7 @@ class Dialog(tk.Toplevel): self.modal = modal self.title(title) self.protocol("WM_DELETE_WINDOW", self.destroy) - image = Images.get(ImageEnum.CORE) + image = Images.get(ImageEnum.CORE, 16) self.tk.call("wm", "iconphoto", self._w, image) def show(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 7fd577e2..8673d3fc 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -182,25 +182,31 @@ class EmaneConfiguration(Dialog): def draw_option_buttons(self, parent): f = ttk.Frame(parent) + f.grid(row=4, column=0, sticky="nsew") f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) + + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, text=self.emane_models[0] + " options", - image=Images.get(ImageEnum.EDITNODE), + image=image, compound=tk.RIGHT, command=self.draw_model_options, ) + b.image = image b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") + + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, text="EMANE options", - image=Images.get(ImageEnum.EDITNODE), + image=image, compound=tk.RIGHT, command=self.draw_emane_options, ) + b.image = image b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(row=4, column=0, sticky="nsew") def combobox_select(self, event): """ @@ -271,7 +277,7 @@ class EmaneConfiguration(Dialog): b = ttk.Button( f, - image=Images.get(ImageEnum.EDITNODE), + image=Images.get(ImageEnum.EDITNODE, 8), text="EMANE Wiki", compound=tk.RIGHT, command=lambda: webbrowser.open_new( diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index fb6fb6bb..15d8000b 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -54,7 +54,7 @@ class IconDialog(Dialog): ), ) if file_path: - self.image = Images.create(file_path) + self.image = Images.create(file_path, 32, 32) self.image_label.config(image=self.image) self.file_path.set(file_path) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 40d09c44..b9b7d77b 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -73,30 +73,36 @@ class SessionsDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") + + image = Images.get(ImageEnum.DOCUMENTNEW, 16) b = ttk.Button( - frame, - image=Images.get(ImageEnum.DOCUMENTNEW), - text="New", - compound=tk.LEFT, - command=self.click_new, + frame, image=image, text="New", compound=tk.LEFT, command=self.click_new ) + b.image = image b.grid(row=0, padx=2, sticky="ew") + + image = Images.get(ImageEnum.FILEOPEN, 16) b = ttk.Button( frame, - image=Images.get(ImageEnum.FILEOPEN), + image=image, text="Connect", compound=tk.LEFT, command=self.click_connect, ) + b.image = image b.grid(row=0, column=1, padx=2, sticky="ew") + + image = Images.get(ImageEnum.EDITDELETE, 16) b = ttk.Button( frame, - image=Images.get(ImageEnum.EDITDELETE), + image=image, text="Shutdown", compound=tk.LEFT, command=self.click_shutdown, ) + b.image = image b.grid(row=0, column=2, padx=2, sticky="ew") + b = ttk.Button(frame, text="Cancel", command=self.click_new) b.grid(row=0, column=3, padx=2, sticky="ew") diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e813ae48..5cdd2eec 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -146,9 +146,7 @@ class CanvasGraph(tk.Canvas): # peer to peer node is not drawn on the GUI if node.type != core_pb2.NodeType.PEER_TO_PEER: # draw nodes on the canvas - image, name = Images.convert_type_and_model_to_image( - node.type, node.model - ) + image, name = Images.node_icon(node.type, node.model) n = CanvasNode( node.position.x, node.position.y, image, name, self.master, node.id ) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index bdedd65e..c1f59815 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -77,6 +77,7 @@ class WlanAntennaManager: self.quantity = 0 self._max = 5 self.antennas = [] + self.image = Images.get(ImageEnum.ANTENNA, 32) # distance between each antenna self.offset = 0 @@ -94,7 +95,7 @@ class WlanAntennaManager: x - 16 + self.offset, y - 16, anchor=tk.CENTER, - image=Images.get(ImageEnum.ANTENNA), + image=self.image, tags="antenna", ) ) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index d064f77e..cebcb49c 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -6,35 +6,37 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from coretk.appconfig import LOCAL_ICONS_PATH +NODE_WIDTH = 32 + class Images: images = {} @classmethod - def create(cls, file_path): + def create(cls, file_path, width, height=None): + if height is None: + height = width image = Image.open(file_path) + image = image.resize((width, height), Image.ANTIALIAS) return ImageTk.PhotoImage(image) @classmethod def load_all(cls): for image in LOCAL_ICONS_PATH.glob("*"): - cls.load(image.stem, str(image)) + cls.images[image.stem] = str(image) @classmethod - def load(cls, name, file_path): - tk_image = cls.create(file_path) - cls.images[name] = tk_image + def get(cls, image_enum, width, height=None): + file_path = cls.images[image_enum.value] + return cls.create(file_path, width, height) @classmethod - def get(cls, image): - return cls.images[image.value] + def get_custom(cls, name, width, height): + file_path = cls.images[name] + return cls.create(file_path, width, height) @classmethod - def get_custom(cls, name): - return cls.images[name] - - @classmethod - def convert_type_and_model_to_image(cls, node_type, node_model): + def node_icon(cls, node_type, node_model): """ Retrieve image based on type and model :param core_pb2.NodeType node_type: core node type @@ -43,34 +45,48 @@ class Images: :rtype: tuple(PhotoImage, str) :return: the matching image and its name """ + image_enum = ImageEnum.ROUTER + name = "unknown" if node_type == core_pb2.NodeType.SWITCH: - return Images.get(ImageEnum.SWITCH), "switch" - if node_type == core_pb2.NodeType.HUB: - return Images.get(ImageEnum.HUB), "hub" - if node_type == core_pb2.NodeType.WIRELESS_LAN: - return Images.get(ImageEnum.WLAN), "wlan" - if node_type == core_pb2.NodeType.EMANE: - return Images.get(ImageEnum.EMANE), "emane" - - if node_type == core_pb2.NodeType.RJ45: - return Images.get(ImageEnum.RJ45), "rj45" - if node_type == core_pb2.NodeType.TUNNEL: - return Images.get(ImageEnum.TUNNEL), "tunnel" - if node_type == core_pb2.NodeType.DEFAULT: + image_enum = ImageEnum.SWITCH + name = "switch" + elif node_type == core_pb2.NodeType.HUB: + image_enum = ImageEnum.HUB + name = "hub" + elif node_type == core_pb2.NodeType.WIRELESS_LAN: + image_enum = ImageEnum.WLAN + name = "wlan" + elif node_type == core_pb2.NodeType.EMANE: + image_enum = ImageEnum.EMANE + name = "emane" + elif node_type == core_pb2.NodeType.RJ45: + image_enum = ImageEnum.RJ45 + name = "rj45" + elif node_type == core_pb2.NodeType.TUNNEL: + image_enum = ImageEnum.TUNNEL + name = "tunnel" + elif node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": - return Images.get(ImageEnum.ROUTER), "router" - if node_model == "host": - return Images.get(ImageEnum.HOST), "host" - if node_model == "PC": - return Images.get(ImageEnum.PC), "PC" - if node_model == "mdr": - return Images.get(ImageEnum.MDR), "mdr" - if node_model == "prouter": - return Images.get(ImageEnum.PROUTER), "prouter" - if node_model == "OVS": - return Images.get(ImageEnum.OVS), "ovs" + image_enum = ImageEnum.ROUTER + name = "router" + elif node_model == "host": + image_enum = ImageEnum.HOST + name = "host" + elif node_model == "PC": + image_enum = ImageEnum.PC + name = "PC" + elif node_model == "mdr": + image_enum = ImageEnum.MDR + name = "mdr" + elif node_model == "prouter": + image_enum = ImageEnum.PROUTER + name = "prouter" + else: + logging.error("invalid node model: %s", node_model) else: - logging.debug("INVALID INPUT OR NOT CONSIDERED YET") + logging.error("invalid node type: %s", node_type) + + return Images.get(image_enum, NODE_WIDTH), name class ImageEnum(Enum): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 752b1000..89393c3e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,43 +1,44 @@ import logging import tkinter as tk from functools import partial +from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images from coretk.tooltip import Tooltip +WIDTH = 32 -class Toolbar(tk.Frame): + +def icon(image_enum): + return Images.get(image_enum, WIDTH) + + +class Toolbar(ttk.Frame): """ Core toolbar class """ - def __init__(self, master, app, cnf={}, **kwargs): + def __init__(self, master, app, **kwargs): """ Create a CoreToolbar instance :param tkinter.Frame edit_frame: edit frame """ - super().__init__(master, cnf, **kwargs) + super().__init__(master, **kwargs) self.app = app self.master = app.master - self.radio_value = tk.IntVar() - self.exec_radio_value = tk.IntVar() - # button dimension - self.width = 32 - self.height = 32 - - # Reference to the option menus - self.selection_tool_button = None - self.link_layer_option_menu = None - self.marker_option_menu = None - self.network_layer_option_menu = None + # design buttons + self.select_button = None + self.link_button = None self.node_button = None self.network_button = None self.annotation_button = None + # runtime buttons + # frames self.design_frame = None self.runtime_frame = None @@ -56,89 +57,77 @@ class Toolbar(tk.Frame): self.design_frame.tkraise() def draw_design_frame(self): - self.design_frame = tk.Frame(self) + self.design_frame = ttk.Frame(self) self.design_frame.grid(row=0, column=0, sticky="nsew") self.design_frame.columnconfigure(0, weight=1) - - self.create_regular_button( + self.create_button( self.design_frame, - Images.get(ImageEnum.START), - self.click_start_session_tool, + icon(ImageEnum.START), + self.click_start, "start the session", ) - self.create_radio_button( + self.select_button = self.create_button( self.design_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.radio_value, - 1, + icon(ImageEnum.SELECT), + self.click_selection, "selection tool", ) - self.create_radio_button( - self.design_frame, - Images.get(ImageEnum.LINK), - self.click_link_tool, - self.radio_value, - 2, - "link tool", + self.link_button = self.create_button( + self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool" ) self.create_node_button() self.create_network_button() self.create_annotation_button() - self.radio_value.set(1) + + def design_select(self, button): + logging.info("selecting design button: %s", button) + self.select_button.state(["!pressed"]) + self.link_button.state(["!pressed"]) + self.node_button.state(["!pressed"]) + self.network_button.state(["!pressed"]) + self.annotation_button.state(["!pressed"]) + button.state(["pressed"]) def draw_runtime_frame(self): - self.runtime_frame = tk.Frame(self) + self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") self.runtime_frame.columnconfigure(0, weight=1) - self.create_regular_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.STOP), - self.click_stop_button, + icon(ImageEnum.STOP), + self.click_stop, "stop the session", ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.SELECT), - self.click_selection_tool, - self.exec_radio_value, - 1, + icon(ImageEnum.SELECT), + self.click_selection, "selection tool", ) - self.create_observe_button() - self.create_radio_button( - self.runtime_frame, - Images.get(ImageEnum.PLOT), - self.click_plot_button, - self.exec_radio_value, - 2, - "plot", + # self.create_observe_button() + self.create_button( + self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot" ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.MARKER), + icon(ImageEnum.MARKER), self.click_marker_button, - self.exec_radio_value, - 3, "marker", ) - self.create_radio_button( + self.create_button( self.runtime_frame, - Images.get(ImageEnum.TWONODE), + icon(ImageEnum.TWONODE), self.click_two_node_button, - self.exec_radio_value, - 4, "run command from one node to another", ) - self.create_regular_button( - self.runtime_frame, Images.get(ImageEnum.RUN), self.click_run_button, "run" + self.create_button( + self.runtime_frame, icon(ImageEnum.RUN), self.click_run_button, "run" ) - self.exec_radio_value.set(1) def draw_node_picker(self): self.hide_pickers() - self.node_picker = tk.Frame(self.master, padx=1, pady=1) + self.node_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.ROUTER, "router"), (ImageEnum.HOST, "host"), @@ -148,26 +137,28 @@ class Toolbar(tk.Frame): ] # draw default nodes for image_enum, tooltip in nodes: - image = Images.get(image_enum) + image = icon(image_enum) func = partial(self.update_button, self.node_button, image, tooltip) - self.create_button(image, func, self.node_picker, tooltip) + self.create_picker_button(image, func, self.node_picker, tooltip) # draw custom nodes for name in sorted(self.app.core.custom_nodes): custom_node = self.app.core.custom_nodes[name] image = custom_node.image func = partial(self.update_button, self.node_button, image, name) - self.create_button(image, func, self.node_picker, name) + self.create_picker_button(image, func, self.node_picker, name) # draw edit node - image = Images.get(ImageEnum.EDITNODE) - self.create_button( + image = icon(ImageEnum.EDITNODE) + self.create_picker_button( image, self.click_edit_node, self.node_picker, "custom nodes" ) - self.show_picker(self.node_button, self.node_picker) + self.design_select(self.node_button) + self.node_button.after( + 0, lambda: self.show_picker(self.node_button, self.node_picker) + ) def show_picker(self, button, picker): - first_button = self.winfo_children()[0] - x = button.winfo_rootx() - first_button.winfo_rootx() + 40 - y = button.winfo_rooty() - first_button.winfo_rooty() - 1 + x = self.winfo_width() + 1 + y = button.winfo_rooty() - picker.master.winfo_rooty() - 1 picker.place(x=x, y=y) self.app.bind_all("", lambda e: self.hide_pickers()) picker.wait_visibility() @@ -175,7 +166,7 @@ class Toolbar(tk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_button(self, image, func, frame, tooltip): + def create_picker_button(self, image, func, frame, tooltip): """ Create button and put it on the frame @@ -185,37 +176,25 @@ class Toolbar(tk.Frame): :param str tooltip: tooltip text :return: nothing """ - button = tk.Button(frame, width=self.width, height=self.height, image=image) + button = ttk.Button(frame, image=image) + button.image = image button.bind("", lambda e: func()) button.grid(pady=1) Tooltip(button, tooltip) - def create_radio_button(self, frame, image, func, variable, value, tooltip_msg): - button = tk.Radiobutton( - frame, - indicatoron=False, - width=self.width, - height=self.height, - image=image, - value=value, - variable=variable, - command=func, - ) - button.grid() - Tooltip(button, tooltip_msg) - - def create_regular_button(self, frame, image, func, tooltip): - button = tk.Button( - frame, width=self.width, height=self.height, image=image, command=func - ) - button.grid() + def create_button(self, frame, image, func, tooltip): + button = ttk.Button(frame, image=image, command=func) + button.image = image + button.grid(sticky="ew") Tooltip(button, tooltip) + return button - def click_selection_tool(self): + def click_selection(self): logging.debug("clicked selection tool") + self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT - def click_start_session_tool(self): + def click_start(self): """ Start session handler redraw buttons, send node and link messages to grpc server. @@ -227,8 +206,9 @@ class Toolbar(tk.Frame): self.app.core.start_session() self.runtime_frame.tkraise() - def click_link_tool(self): + def click_link(self): logging.debug("Click LINK button") + self.design_select(self.link_button) self.app.canvas.mode = GraphMode.EDGE def click_edit_node(self): @@ -240,6 +220,7 @@ class Toolbar(tk.Frame): logging.info("update button(%s): %s", button, name) self.hide_pickers() button.configure(image=image) + button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.draw_node_image = image self.app.canvas.draw_node_name = name @@ -262,29 +243,22 @@ class Toolbar(tk.Frame): :return: nothing """ - router_image = Images.get(ImageEnum.ROUTER) - self.node_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=3, - width=self.width, - height=self.height, - image=router_image, + image = icon(ImageEnum.ROUTER) + self.node_button = ttk.Button( + self.design_frame, image=image, command=self.draw_node_picker ) - self.node_button.bind("", lambda e: self.draw_node_picker()) - self.node_button.grid() + self.node_button.image = image + self.node_button.grid(sticky="ew") Tooltip(self.node_button, "Network-layer virtual nodes") def draw_network_picker(self): """ - Draw the options for link-layer button + Draw the options for link-layer button. - :param tkinter.RadioButton link_layer_button: link-layer button :return: nothing """ self.hide_pickers() - self.network_picker = tk.Frame(self.master, padx=1, pady=1) + self.network_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.HUB, "hub", "ethernet hub"), (ImageEnum.SWITCH, "switch", "ethernet switch"), @@ -294,14 +268,17 @@ class Toolbar(tk.Frame): (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), ] for image_enum, name, tooltip in nodes: - image = Images.get(image_enum) - self.create_button( + image = icon(image_enum) + self.create_picker_button( image, partial(self.update_button, self.network_button, image, name), self.network_picker, tooltip, ) - self.show_picker(self.network_button, self.network_picker) + self.design_select(self.network_button) + self.network_button.after( + 0, lambda: self.show_picker(self.network_button, self.network_picker) + ) def create_network_button(self): """ @@ -309,31 +286,22 @@ class Toolbar(tk.Frame): :return: nothing """ - hub_image = Images.get(ImageEnum.HUB) - self.network_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=4, - width=self.width, - height=self.height, - image=hub_image, + image = icon(ImageEnum.HUB) + self.network_button = ttk.Button( + self.design_frame, image=image, command=self.draw_network_picker ) - self.network_button.bind( - "", lambda e: self.draw_network_picker() - ) - self.network_button.grid() + self.network_button.image = image + self.network_button.grid(sticky="ew") Tooltip(self.network_button, "link-layer nodes") def draw_annotation_picker(self): """ - Draw the options for marker button + Draw the options for marker button. - :param tkinter.Radiobutton main_button: the main button :return: nothing """ self.hide_pickers() - self.annotation_picker = tk.Frame(self.master, padx=1, pady=1) + self.annotation_picker = ttk.Frame(self.master) nodes = [ (ImageEnum.MARKER, "marker"), (ImageEnum.OVAL, "oval"), @@ -341,13 +309,17 @@ class Toolbar(tk.Frame): (ImageEnum.TEXT, "text"), ] for image_enum, tooltip in nodes: - self.create_button( - Images.get(image_enum), - partial(self.update_annotation, image_enum), + image = icon(image_enum) + self.create_picker_button( + image, + partial(self.update_annotation, image), self.annotation_picker, tooltip, ) - self.show_picker(self.annotation_button, self.annotation_picker) + self.design_select(self.annotation_button) + self.annotation_button.after( + 0, lambda: self.show_picker(self.annotation_button, self.annotation_picker) + ) def create_annotation_button(self): """ @@ -355,53 +327,39 @@ class Toolbar(tk.Frame): :return: nothing """ - marker_image = Images.get(ImageEnum.MARKER) - self.annotation_button = tk.Radiobutton( - self.design_frame, - indicatoron=False, - variable=self.radio_value, - value=5, - width=self.width, - height=self.height, - image=marker_image, + image = icon(ImageEnum.MARKER) + self.annotation_button = ttk.Button( + self.design_frame, image=image, command=self.draw_annotation_picker ) - self.annotation_button.bind( - "", lambda e: self.draw_annotation_picker() - ) - self.annotation_button.grid() + self.annotation_button.image = image + self.annotation_button.grid(sticky="ew") Tooltip(self.annotation_button, "background annotation tools") def create_observe_button(self): - menu_button = tk.Menubutton( - self.runtime_frame, - image=Images.get(ImageEnum.OBSERVE), - width=self.width, - height=self.height, - direction=tk.RIGHT, - relief=tk.RAISED, + menu_button = ttk.Menubutton( + self.runtime_frame, image=icon(ImageEnum.OBSERVE), direction=tk.RIGHT ) - menu_button.menu = tk.Menu(menu_button, tearoff=0) - menu_button["menu"] = menu_button.menu - menu_button.grid() + menu_button.grid(sticky="ew") + menu = tk.Menu(menu_button, tearoff=0) + menu_button["menu"] = menu + menu.add_command(label="None") + menu.add_command(label="processes") + menu.add_command(label="ifconfig") + menu.add_command(label="IPv4 routes") + menu.add_command(label="IPv6 routes") + menu.add_command(label="OSPFv2 neighbors") + menu.add_command(label="OSPFv3 neighbors") + menu.add_command(label="Listening sockets") + menu.add_command(label="IPv4 MFC entries") + menu.add_command(label="IPv6 MFC entries") + menu.add_command(label="firewall rules") + menu.add_command(label="IPSec policies") + menu.add_command(label="docker logs") + menu.add_command(label="OSPFv3 MDR level") + menu.add_command(label="PIM neighbors") + menu.add_command(label="Edit...") - menu_button.menu.add_command(label="None") - menu_button.menu.add_command(label="processes") - menu_button.menu.add_command(label="ifconfig") - menu_button.menu.add_command(label="IPv4 routes") - menu_button.menu.add_command(label="IPv6 routes") - menu_button.menu.add_command(label="OSPFv2 neighbors") - menu_button.menu.add_command(label="OSPFv3 neighbors") - menu_button.menu.add_command(label="Listening sockets") - menu_button.menu.add_command(label="IPv4 MFC entries") - menu_button.menu.add_command(label="IPv6 MFC entries") - menu_button.menu.add_command(label="firewall rules") - menu_button.menu.add_command(label="IPSec policies") - menu_button.menu.add_command(label="docker logs") - menu_button.menu.add_command(label="OSPFv3 MDR level") - menu_button.menu.add_command(label="PIM neighbors") - menu_button.menu.add_command(label="Edit...") - - def click_stop_button(self): + def click_stop(self): """ redraw buttons on the toolbar, send node and link messages to grpc server @@ -411,10 +369,11 @@ class Toolbar(tk.Frame): self.app.core.stop_session() self.design_frame.tkraise() - def update_annotation(self, image_enum): + def update_annotation(self, image): logging.info("clicked annotation: ") self.hide_pickers() - self.annotation_button.configure(image=Images.get(image_enum)) + self.annotation_button.configure(image=image) + self.annotation_button.image = image def click_run_button(self): logging.debug("Click on RUN button") From 1693c5942c8818922b9a8ce9070853bb288bc351 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 12 Nov 2019 22:42:55 -0800 Subject: [PATCH 110/305] added ttk theme based on opensource dark theme with minor tweaks for button presses and menu coloring --- coretk/coretk/theme.py | 162 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 coretk/coretk/theme.py diff --git a/coretk/coretk/theme.py b/coretk/coretk/theme.py new file mode 100644 index 00000000..9525d35e --- /dev/null +++ b/coretk/coretk/theme.py @@ -0,0 +1,162 @@ +import tkinter as tk +from tkinter import ttk + + +class Colors: + disabledfg = "DarkGrey" + frame = "#424242" + dark = "#222222" + darker = "#121212" + darkest = "black" + lighter = "#626262" + lightest = "#ffffff" + selectbg = "#4a6984" + selectfg = "#ffffff" + white = "white" + black = "black" + + +style = ttk.Style() +style.theme_create( + "black", + "clam", + { + ".": { + "configure": { + "background": Colors.frame, + "foreground": Colors.white, + "bordercolor": Colors.darkest, + "darkcolor": Colors.dark, + "lightcolor": Colors.lighter, + "troughcolor": Colors.darker, + "selectbackground": Colors.selectbg, + "selectforeground": Colors.selectfg, + "selectborderwidth": 0, + "font": "TkDefaultFont", + }, + "map": { + "background": [("disabled", Colors.frame), ("active", Colors.lighter)], + "foreground": [("disabled", Colors.disabledfg)], + "selectbackground": [("!focus", Colors.darkest)], + "selectforeground": [("!focus", Colors.white)], + }, + }, + "TButton": { + "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "map": { + "relief": [("pressed", tk.SUNKEN)], + "shiftrelief": [("pressed", 1)], + }, + }, + "TMenubutton": { + "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} + }, + "TCheckbutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TRadiobutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TEntry": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TCombobox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TNotebook.Tab": { + "configure": {"padding": (6, 2, 6, 2)}, + "map": {"background": [("selected", Colors.lighter)]}, + }, + "Treeview": { + "configure": { + "fieldbackground": Colors.white, + "background": Colors.white, + "foreground": Colors.black, + }, + "map": { + "background": [("selected", Colors.selectbg)], + "foreground": [("selected", Colors.selectfg)], + }, + }, + }, +) +style.theme_use("black") + + +def update_menu(event): + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + abg = style.lookup(".", "lightcolor") + event.widget.config( + background=bg, foreground=fg, activebackground=abg, activeforeground=fg + ) + + +class Application(ttk.Frame): + def __init__(self, master=None): + super().__init__(master) + self.master.bind_class("Menu", "<>", update_menu) + self.master.geometry("800x600") + menu = tk.Menu(self.master) + menu.add_command(label="Command1") + menu.add_command(label="Command2") + submenu = tk.Menu(menu, tearoff=False) + submenu.add_command(label="Command1") + submenu.add_command(label="Command2") + menu.add_cascade(label="Submenu", menu=submenu) + self.master.config(menu=menu) + self.master.columnconfigure(0, weight=1) + self.master.rowconfigure(0, weight=1) + notebook = ttk.Notebook(self.master) + notebook.grid(sticky="nsew") + frame = ttk.Frame(notebook) + frame.grid(sticky="nsew") + ttk.Label(frame, text="Label").grid() + ttk.Entry(frame).grid() + ttk.Button(frame, text="Button").grid() + ttk.Combobox(frame, values=("one", "two", "three")).grid() + menubutton = ttk.Menubutton(frame, text="MenuButton") + menubutton.grid() + mbmenu = tk.Menu(menubutton, tearoff=False) + menubutton.config(menu=mbmenu) + mbmenu.add_command(label="Menu1") + mbmenu.add_command(label="Menu2") + submenu = tk.Menu(mbmenu, tearoff=False) + submenu.add_command(label="Command1") + submenu.add_command(label="Command2") + mbmenu.add_cascade(label="Submenu", menu=submenu) + ttk.Radiobutton(frame, text="Radio Button").grid() + ttk.Checkbutton(frame, text="Check Button").grid() + tv = ttk.Treeview(frame, columns=("one", "two", "three"), show="headings") + tv.grid() + tv.column("one", stretch=tk.YES) + tv.heading("one", text="ID") + tv.column("two", stretch=tk.YES) + tv.heading("two", text="State") + tv.column("three", stretch=tk.YES) + tv.heading("three", text="Node Count") + tv.insert("", tk.END, text="1", values=("v1", "v2", "v3")) + tv.insert("", tk.END, text="2", values=("v1", "v2", "v3")) + notebook.add(frame, text="Tab1") + frame = ttk.Frame(notebook) + frame.grid(sticky="nsew") + notebook.add(frame, text="Tab2") + + +if __name__ == "__main__": + app = Application() + app.mainloop() From b69b24d9fc8e912aa8b868128e39717bbd137c95 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 13 Nov 2019 09:09:53 -0800 Subject: [PATCH 111/305] continued work on node service configuration --- coretk/coretk/coreclient.py | 29 +++++ coretk/coretk/dialogs/serviceconfiguration.py | 109 +++++++++++++++--- coretk/coretk/servicenodeconfig.py | 12 ++ 3 files changed, 134 insertions(+), 16 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d924e305..13b1c1cf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -416,6 +416,35 @@ class CoreClient: logging.debug("open xml: %s", response) self.join_session(response.session_id) + def get_node_service(self, node_id, service_name): + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service %s", response) + return response.service + + def set_node_service(self, node_id, service_name, startups, validations, shutdowns): + response = self.client.set_node_service( + self.session_id, node_id, service_name, startups, validations, shutdowns + ) + logging.debug("set node service %s", response) + + def create_nodes_and_links(self): + node_protos = self.get_nodes_proto() + link_protos = self.get_links_proto() + self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + for node_proto in node_protos: + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) + for link_proto in link_protos: + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) + def close(self): """ Clean ups when done using grpc diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 016373c8..7cd8e501 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -3,6 +3,7 @@ import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.widgets import ListboxScroll @@ -12,26 +13,51 @@ class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, canvas_node): super().__init__(master, app, service_name + " service", modal=True) self.app = app + self.canvas_node = canvas_node self.service_name = service_name - self.metadata = tk.StringVar() - self.filename = tk.StringVar() self.radiovar = tk.IntVar() self.radiovar.set(2) - self.startup_index = tk.IntVar() - self.start_time = tk.IntVar() self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW) self.editdelete_img = Images.get(ImageEnum.EDITDELETE) - self.tab_parent = None - self.filenames = ["test1", "test2", "test3"] + self.metadata = "" + self.filenames = [] + self.dependencies = [] + self.executables = [] + self.startup_commands = [] + self.validation_commands = [] + self.shutdown_commands = [] + self.validation_mode = None + self.validation_time = None + self.validation_period = None + self.tab_parent = None self.metadata_entry = None self.filename_combobox = None self.startup_commands_listbox = None self.shutdown_commands_listbox = None self.validate_commands_listbox = None - + self.validation_time_entry = None + self.validation_mode_entry = None + self.load() self.draw() + def load(self): + # create nodes and links in definition state for getting and setting service file + self.app.core.create_nodes_and_links() + # load data from local memory + service_config = self.app.core.serviceconfig_manager.configurations[ + self.canvas_node.core_id + ][self.service_name] + self.dependencies = [x for x in service_config.dependencies] + self.executables = [x for x in service_config.executables] + self.metadata = service_config.meta + self.filenames = [x for x in service_config.configs] + self.startup_commands = [x for x in service_config.startup] + self.validation_commands = [x for x in service_config.validate] + self.shutdown_commands = [x for x in service_config.shutdown] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + def draw(self): # self.columnconfigure(1, weight=1) frame = tk.Frame(self) @@ -44,7 +70,9 @@ class ServiceConfiguration(Dialog): # frame2.columnconfigure(1, weight=4) label = tk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) - self.metadata_entry = tk.Entry(frame2, textvariable=self.metadata) + self.metadata_entry = tk.Entry( + frame2, textvariable=tk.StringVar(value=self.metadata) + ) self.metadata_entry.grid(row=0, column=1) frame2.grid(row=1, column=0) frame.grid(row=0, column=0) @@ -78,7 +106,12 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0) self.filename_combobox = ttk.Combobox(frame, values=self.filenames) self.filename_combobox.grid(row=0, column=1) - self.filename_combobox.current(0) + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.filename_combobox.bind( + "<>", self.display_service_file_data + ) + button = tk.Button(frame, image=self.documentnew_img) button.bind("", self.add_filename) button.grid(row=0, column=2) @@ -130,10 +163,13 @@ class ServiceConfiguration(Dialog): label_frame = None if i == 0: label_frame = tk.LabelFrame(tab3, text="Startup commands") + commands = self.startup_commands elif i == 1: label_frame = tk.LabelFrame(tab3, text="Shutdown commands") + commands = self.shutdown_commands elif i == 2: label_frame = tk.LabelFrame(tab3, text="Validation commands") + commands = self.validation_commands label_frame.columnconfigure(0, weight=1) frame = tk.Frame(label_frame) frame.columnconfigure(0, weight=1) @@ -148,6 +184,8 @@ class ServiceConfiguration(Dialog): frame.grid(row=0, column=0, sticky="nsew") listbox_scroll = ListboxScroll(label_frame) listbox_scroll.listbox.bind("<>", self.update_entry) + for command in commands: + listbox_scroll.listbox.insert("end", command) listbox_scroll.listbox.config(height=4) listbox_scroll.grid(row=1, column=0, sticky="nsew") if i == 0: @@ -160,33 +198,60 @@ class ServiceConfiguration(Dialog): # tab 4 for i in range(2): + label_frame = None if i == 0: label_frame = tk.LabelFrame(tab4, text="Executables") elif i == 1: label_frame = tk.LabelFrame(tab4, text="Dependencies") - label_frame.columnconfigure(0, weight=1) listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4, state="disabled") + listbox_scroll.listbox.config(height=4) listbox_scroll.grid(row=0, column=0, sticky="nsew") label_frame.grid(row=i, column=0, sticky="nsew") + if i == 0: + for executable in self.executables: + print(executable) + listbox_scroll.listbox.insert("end", executable) + if i == 1: + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) for i in range(3): frame = tk.Frame(tab4) frame.columnconfigure(0, weight=1) if i == 0: label = tk.Label(frame, text="Validation time:") + self.validation_time_entry = tk.Entry( + frame, + state="disabled", + textvariable=tk.StringVar(value=self.validation_time), + ) + self.validation_time_entry.grid(row=i, column=1) elif i == 1: label = tk.Label(frame, text="Validation mode:") + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif ( + self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING + ): + mode = "NON_BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: + mode = "TIMER" + self.validation_mode_entry = tk.Entry( + frame, state="disabled", textvariable=tk.StringVar(value=mode) + ) + self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = tk.Label(frame, text="Validation period:") + self.validation_period_entry = tk.Entry( + frame, state="disabled", textvariable=tk.StringVar() + ) + self.validation_period_entry.grid(row=i, column=1) label.grid(row=i, column=0) - entry = tk.Entry(frame, state="disabled", textvariable=tk.StringVar()) - entry.grid(row=i, column=1) frame.grid(row=2 + i, column=0, sticky="nsew") button = tk.Button( - self, text="onle store values that have changed from their defaults" + self, text="only store values that have changed from their defaults" ) button.grid(row=2, column=0) @@ -258,9 +323,21 @@ class ServiceConfiguration(Dialog): startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") - print( - metadata, filenames, startup_commands, shutdown_commands, validate_commands + self.app.core.serviceconfig_manager.node_service_custom_configuration( + self.canvas_node.core_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, ) + # wipe nodes and links when finished by setting to DEFINITION state + self.app.core.client.set_session_state( + self.app.core.session_id, core_pb2.SessionState.DEFINITION + ) + print(metadata, filenames) + + def display_service_file_data(self, event): + print("not implemented") def click_defaults(self): print("not implemented") diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index 486e646d..f9c2ee88 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -35,3 +35,15 @@ class ServiceNodeConfig: response, ) self.configurations[node_id][default] = response.service + + def node_custom_service_configuration(self, node_id, service_name): + return + + def node_service_custom_configuration( + self, node_id, service_name, startups, validates, shutdowns + ): + self.app.core.set_node_service( + node_id, service_name, startups, validates, shutdowns + ) + config = self.app.core.get_node_service(node_id, service_name) + self.configurations[node_id][service_name] = config From ca798215e4ea66dcf1fdc0adc8072d18a4f351a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 10:45:43 -0800 Subject: [PATCH 112/305] updates to leverage theming, initially using a dark theme, updated all dialogs to be contained within a frame to help provide consistent theming --- coretk/coretk/app.py | 15 +- coretk/coretk/canvastooltip.py | 18 +- coretk/coretk/dialogs/canvasbackground.py | 16 +- coretk/coretk/dialogs/canvassizeandscale.py | 14 +- coretk/coretk/dialogs/customnodes.py | 20 +-- coretk/coretk/dialogs/dialog.py | 7 +- coretk/coretk/dialogs/emaneconfig.py | 38 ++-- coretk/coretk/dialogs/hooks.py | 20 ++- coretk/coretk/dialogs/icondialog.py | 8 +- coretk/coretk/dialogs/mobilityconfig.py | 24 ++- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/nodeservice.py | 8 +- coretk/coretk/dialogs/observers.py | 12 +- coretk/coretk/dialogs/preferences.py | 6 +- coretk/coretk/dialogs/servers.py | 14 +- coretk/coretk/dialogs/serviceconfiguration.py | 8 +- coretk/coretk/dialogs/sessionoptions.py | 8 +- coretk/coretk/dialogs/sessions.py | 14 +- coretk/coretk/dialogs/wlanconfig.py | 23 ++- coretk/coretk/theme.py | 162 ------------------ coretk/coretk/themes.py | 127 ++++++++++++++ coretk/coretk/tooltip.py | 17 +- coretk/coretk/widgets.py | 20 +-- 23 files changed, 298 insertions(+), 309 deletions(-) delete mode 100644 coretk/coretk/theme.py create mode 100644 coretk/coretk/themes.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 452d5397..bc3ac9dd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,8 +1,9 @@ import logging import tkinter as tk +from functools import partial from tkinter import ttk -from coretk import appconfig +from coretk import appconfig, themes from coretk.coreclient import CoreClient from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images @@ -14,7 +15,8 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - Images.load_all() + self.style = ttk.Style() + self.setup_theme() self.menubar = None self.toolbar = None self.canvas = None @@ -33,6 +35,14 @@ class Application(tk.Frame): self.draw() self.core.set_up() + def setup_theme(self): + themes.load(self.style) + self.style.theme_use(themes.DARK) + func = partial(themes.update_menu, self.style) + self.master.bind_class("Menu", "<>", func) + func = partial(themes.update_toplevel, self.style) + self.master.bind_class("Toplevel", "<>", func) + def setup_app(self): self.master.title("CORE") self.master.geometry("1000x800") @@ -78,6 +88,7 @@ class Application(tk.Frame): if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=log_format) + Images.load_all() appconfig.check_directory() app = Application() app.mainloop() diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/canvastooltip.py index 8ace41d0..8ae528d8 100644 --- a/coretk/coretk/canvastooltip.py +++ b/coretk/coretk/canvastooltip.py @@ -1,6 +1,8 @@ import tkinter as tk from tkinter import ttk +from coretk.themes import Styles + class CanvasTooltip: """ @@ -17,16 +19,13 @@ class CanvasTooltip: Alberto Vassena on 2016.12.10. """ - def __init__( - self, canvas, *, bg="#FFFFEA", pad=(5, 3, 5, 3), waittime=400, wraplength=600 - ): + def __init__(self, canvas, *, pad=(5, 3, 5, 3), waittime=400, wraplength=600): # in miliseconds, originally 500 self.waittime = waittime # in pixels, originally 180 self.wraplength = wraplength self.canvas = canvas self.text = tk.StringVar() - self.bg = bg self.pad = pad self.id = None self.tw = None @@ -78,7 +77,6 @@ class CanvasTooltip: y1 = 0 return x1, y1 - bg = self.bg pad = self.pad canvas = self.canvas @@ -87,20 +85,16 @@ class CanvasTooltip: # Leaves only the label and removes the app window self.tw.wm_overrideredirect(True) - - win = tk.Frame(self.tw, background=bg, borderwidth=0) + win = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) win.grid() label = ttk.Label( win, textvariable=self.text, - justify=tk.LEFT, - background=bg, - relief=tk.SOLID, - borderwidth=0, wraplength=self.wraplength, + style=Styles.tooltip, ) label.grid(padx=(pad[0], pad[2]), pady=(pad[1], pad[3]), sticky=tk.NSEW) - x, y = tip_pos_calculator(canvas, label) + x, y = tip_pos_calculator(canvas, label, pad=pad) self.tw.wm_geometry("+%d+%d" % (x, y)) def hide(self): diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 8b1c4cf4..7b1dd186 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -40,7 +40,7 @@ class CanvasBackgroundDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_image() self.draw_image_label() self.draw_image_selection() @@ -50,16 +50,16 @@ class CanvasBackgroundDialog(Dialog): def draw_image(self): self.image_label = ttk.Label( - self, text="(image preview)", width=32, anchor=tk.CENTER + self.top, text="(image preview)", width=32, anchor=tk.CENTER ) self.image_label.grid(row=0, column=0, pady=5) def draw_image_label(self): - label = ttk.Label(self, text="Image filename: ") + label = ttk.Label(self.top, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") def draw_image_selection(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -76,7 +76,7 @@ class CanvasBackgroundDialog(Dialog): button.grid(row=0, column=2, sticky="ew") def draw_options(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -105,12 +105,12 @@ class CanvasBackgroundDialog(Dialog): def draw_additional_options(self): checkbutton = ttk.Checkbutton( - self, text="Show grid", variable=self.show_grid_var + self.top, text="Show grid", variable=self.show_grid_var ) checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( - self, + self.top, text="Adjust canvas size to image dimensions", variable=self.adjust_to_dim_var, command=self.click_adjust_canvas, @@ -121,7 +121,7 @@ class CanvasBackgroundDialog(Dialog): self.adjust_to_dim_var.set(0) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=6, column=0, pady=5, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1af71c93..09132aef 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -44,7 +44,7 @@ class SizeAndScaleDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_size() self.draw_scale() self.draw_reference_point() @@ -52,7 +52,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self, text="Size", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_BAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -89,7 +89,7 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self, text="Scale", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_BAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -104,7 +104,9 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe(self, text="Reference Point", padding=FRAME_BAD) + label_frame = ttk.Labelframe( + self.top, text="Reference Point", padding=FRAME_BAD + ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -156,12 +158,12 @@ class SizeAndScaleDialog(Dialog): def draw_save_as_default(self): button = ttk.Checkbutton( - self, text="Save as default?", variable=self.save_default + self.top, text="Save as default?", variable=self.save_default ) button.grid(sticky="w", pady=3) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.grid(sticky="ew") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f5c1ab78..154b7868 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -19,10 +19,10 @@ class ServicesSelectDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -44,7 +44,7 @@ class ServicesSelectDialog(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -96,19 +96,19 @@ class CustomNodesDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_node_config() self.draw_node_buttons() self.draw_buttons() def draw_node_config(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame) + self.nodes_list = ListboxScroll(frame, text="Nodes") self.nodes_list.grid(row=0, column=0, sticky="nsew") self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): @@ -125,7 +125,7 @@ class CustomNodesDialog(Dialog): button.grid(sticky="ew") def draw_node_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -144,7 +144,7 @@ class CustomNodesDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index c043a47a..769fd176 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -1,4 +1,5 @@ import tkinter as tk +from tkinter import ttk from coretk.images import ImageEnum, Images @@ -7,7 +8,7 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): - super().__init__(master, padx=DIALOG_PAD, pady=DIALOG_PAD) + super().__init__(master) self.withdraw() self.app = app self.modal = modal @@ -15,6 +16,10 @@ class Dialog(tk.Toplevel): self.protocol("WM_DELETE_WINDOW", self.destroy) image = Images.get(ImageEnum.CORE, 16) self.tk.call("wm", "iconphoto", self._w, image) + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + self.top = ttk.Frame(self, padding=DIALOG_PAD) + self.top.grid(sticky="nsew") def show(self): self.transient(self.master) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 8673d3fc..181e1264 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -55,10 +55,10 @@ class EmaneConfiguration(Dialog): return var def choose_core(self): - print("not implemented") + logging.info("not implemented") def node_name_and_image(self): - f = ttk.Frame(self) + f = ttk.Frame(self.top) lbl = ttk.Label(f, text="Node name:") lbl.grid(row=0, column=0, padx=2, pady=2) @@ -90,13 +90,15 @@ class EmaneConfiguration(Dialog): logging.info("emane config: %s", response) self.options = response.config - self.emane_dialog.columnconfigure(0, weight=1) - self.emane_dialog.rowconfigure(0, weight=1) - self.emane_config_frame = ConfigFrame(self.emane_dialog, config=self.options) + self.emane_dialog.top.columnconfigure(0, weight=1) + self.emane_dialog.top.rowconfigure(0, weight=1) + self.emane_config_frame = ConfigFrame( + self.emane_dialog.top, config=self.options + ) self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") - frame = ttk.Frame(self.emane_dialog) + frame = ttk.Frame(self.emane_dialog.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -152,8 +154,8 @@ class EmaneConfiguration(Dialog): self.emane_model_dialog = Dialog( self, self.app, f"{model_name} configuration", modal=False ) - self.emane_model_dialog.columnconfigure(0, weight=1) - self.emane_model_dialog.rowconfigure(0, weight=1) + self.emane_model_dialog.top.columnconfigure(0, weight=1) + self.emane_model_dialog.top.rowconfigure(0, weight=1) # query for configurations session_id = self.app.core.session_id @@ -165,12 +167,12 @@ class EmaneConfiguration(Dialog): self.model_options = response.config self.model_config_frame = ConfigFrame( - self.emane_model_dialog, config=self.model_options + self.emane_model_dialog.top, config=self.model_options ) self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() - frame = ttk.Frame(self.emane_model_dialog) + frame = ttk.Frame(self.emane_model_dialog.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -268,22 +270,24 @@ class EmaneConfiguration(Dialog): :return: nothing """ # draw label - lbl = ttk.Label(self, text="Emane") + lbl = ttk.Label(self.top, text="Emane") lbl.grid(row=1, column=0) # main frame that has emane wiki, a short description, emane models and the configure buttons - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) + image = Images.get(ImageEnum.EDITNODE, 16) b = ttk.Button( f, - image=Images.get(ImageEnum.EDITNODE, 8), + image=image, text="EMANE Wiki", compound=tk.RIGHT, command=lambda: webbrowser.open_new( "https://github.com/adjacentlink/emane/wiki" ), ) + b.image = image b.grid(row=0, column=0, sticky="w") lbl = ttk.Label( @@ -302,8 +306,8 @@ class EmaneConfiguration(Dialog): f.grid(row=2, column=0, sticky="nsew") def draw_ip_subnets(self): - self.draw_text_label_and_entry(self, "IPv4 subnet", "") - self.draw_text_label_and_entry(self, "IPv6 subnet", "") + self.draw_text_label_and_entry(self.top, "IPv4 subnet", "") + self.draw_text_label_and_entry(self.top, "IPv6 subnet", "") def emane_options(self): """ @@ -311,7 +315,7 @@ class EmaneConfiguration(Dialog): :return: """ - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) b = ttk.Button(f, text="Link to all routers") @@ -326,7 +330,7 @@ class EmaneConfiguration(Dialog): self.destroy() def draw_apply_and_cancel(self): - f = ttk.Frame(self) + f = ttk.Frame(self.top) f.columnconfigure(0, weight=1) f.columnconfigure(1, weight=1) b = ttk.Button(f, text="Apply", command=self.apply) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 5ae9da8a..dc677e12 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -15,11 +15,11 @@ class HookDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(1, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) # name and states - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, sticky="ew", pady=2) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) @@ -39,7 +39,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) @@ -59,7 +59,7 @@ class HookDialog(Dialog): scrollbar.config(command=self.data.yview) # button row - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, sticky="ew", pady=2) for i in range(2): frame.columnconfigure(i, weight=1) @@ -99,14 +99,16 @@ class HooksDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) - self.listbox = tk.Listbox(self) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + self.listbox = tk.Listbox(self.top) self.listbox.grid(row=0, sticky="nsew") self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) - frame = ttk.Frame(self) + + frame = ttk.Frame(self.top) frame.grid(row=1, sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 15d8000b..c5518f28 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -15,10 +15,10 @@ class IconDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) # row one - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) @@ -30,11 +30,11 @@ class IconDialog(Dialog): button.grid(row=0, column=2) # row two - self.image_label = ttk.Label(self, image=self.image, anchor=tk.CENTER) + self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) self.image_label.grid(row=1, column=0, pady=2, sticky="ew") # row three - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 68c557ee..1f53b4a0 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -1,7 +1,7 @@ """ mobility configuration """ - +import logging import tkinter as tk from tkinter import filedialog, ttk @@ -9,7 +9,7 @@ from coretk import appconfig from coretk.dialogs.dialog import Dialog -class MobilityConfiguration(Dialog): +class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): """ create an instance of mobility configuration @@ -19,7 +19,7 @@ class MobilityConfiguration(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node - print(app.canvas.core.mobilityconfig_management.configurations) + logging.info(app.canvas.core.mobilityconfig_management.configurations) self.node_config = app.canvas.core.mobilityconfig_management.configurations[ canvas_node.core_id ] @@ -57,11 +57,11 @@ class MobilityConfiguration(Dialog): def create_label_entry_filebrowser( self, parent_frame, text_label, entry_text, filebrowser=False ): - f = ttk.Frame(parent_frame, bg="#d9d9d9") - lbl = ttk.Label(f, text=text_label, bg="#d9d9d9") + f = ttk.Frame(parent_frame) + lbl = ttk.Label(f, text=text_label) lbl.grid(padx=3, pady=3) # f.grid() - e = ttk.Entry(f, textvariable=self.create_string_var(entry_text), bg="#ffffff") + e = ttk.Entry(f, textvariable=self.create_string_var(entry_text)) e.grid(row=0, column=1, padx=3, pady=3) if filebrowser: b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) @@ -69,16 +69,14 @@ class MobilityConfiguration(Dialog): f.grid(sticky=tk.E) def mobility_script_parameters(self): - lbl = ttk.Label(self, text="node ns2script") + lbl = ttk.Label(self.top, text="node ns2script") lbl.grid(sticky="ew") - sb = ttk.Scrollbar(self, orient=tk.VERTICAL) + sb = ttk.Scrollbar(self.top, orient=tk.VERTICAL) sb.grid(row=1, column=1, sticky="ns") - f = ttk.Frame(self, bg="#d9d9d9") - lbl = ttk.Label( - f, text="ns-2 Mobility Scripts Parameters", bg="#d9d9d9", relief=tk.RAISED - ) + f = ttk.Frame(self.top) + lbl = ttk.Label(f, text="ns-2 Mobility Scripts Parameters") lbl.grid(row=0, column=0, sticky=tk.W) f1 = tk.Canvas( @@ -229,7 +227,7 @@ class MobilityConfiguration(Dialog): :return: nothing """ - f = ttk.Frame(self) + f = ttk.Frame(self.top) b = ttk.Button(f, text="Apply", command=self.ns2script_apply) b.grid() b = ttk.Button(f, text="Cancel", command=self.destroy) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index ce7261e2..48233762 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -26,13 +26,13 @@ class NodeConfigDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_first_row() self.draw_second_row() self.draw_third_row() def draw_first_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=0, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -55,7 +55,7 @@ class NodeConfigDialog(Dialog): combobox.grid(row=0, column=2, sticky="ew") def draw_second_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=1, column=0, pady=2, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -73,7 +73,7 @@ class NodeConfigDialog(Dialog): self.image_button.grid(row=0, column=1, sticky="ew") def draw_third_row(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(row=2, column=0, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 49cc56d8..ce5a4715 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -22,10 +22,10 @@ class NodeService(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="nsew") frame.rowconfigure(0, weight=1) for i in range(3): @@ -47,7 +47,7 @@ class NodeService(Dialog): for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(stick="ew") for i in range(3): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 6c57f08e..78c5811f 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -18,15 +18,15 @@ class ObserverDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_listbox() self.draw_form_fields() self.draw_config_buttons() self.draw_apply_buttons() def draw_listbox(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -45,7 +45,7 @@ class ObserverDialog(Dialog): scrollbar.config(command=self.observers.yview) def draw_form_fields(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) @@ -60,7 +60,7 @@ class ObserverDialog(Dialog): entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -79,7 +79,7 @@ class ObserverDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 74293a30..0c426d3c 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -15,12 +15,12 @@ class PreferencesDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_programs() self.draw_buttons() def draw_programs(self): - frame = ttk.LabelFrame(self, text="Programs") + frame = ttk.LabelFrame(self.top, text="Programs") frame.grid(sticky="ew", pady=2) frame.columnconfigure(1, weight=1) @@ -47,7 +47,7 @@ class PreferencesDialog(Dialog): entry.grid(row=2, column=1, sticky="ew") def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 25f3e826..d3db22e7 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -23,15 +23,15 @@ class ServersDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_servers() self.draw_server_configuration() self.draw_servers_buttons() self.draw_apply_buttons() def draw_servers(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -51,10 +51,10 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = ttk.Label(self, text="Server Configuration") + label = ttk.Label(self.top, text="Server Configuration") label.grid(pady=2, sticky="ew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) @@ -76,7 +76,7 @@ class ServersDialog(Dialog): entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) @@ -95,7 +95,7 @@ class ServersDialog(Dialog): self.delete_button.grid(row=0, column=2, sticky="ew") def draw_apply_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index b0b35db9..29f844b5 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -34,7 +34,7 @@ class ServiceConfiguration(Dialog): def draw(self): # self.columnconfigure(1, weight=1) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame1 = ttk.Frame(frame) label = ttk.Label(frame1, text=self.service_name) label.grid(row=0, column=0, sticky="ew") @@ -49,7 +49,7 @@ class ServiceConfiguration(Dialog): frame2.grid(row=1, column=0) frame.grid(row=0, column=0) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) self.tab_parent = ttk.Notebook(frame) tab1 = ttk.Frame(self.tab_parent) tab2 = ttk.Frame(self.tab_parent) @@ -190,11 +190,11 @@ class ServiceConfiguration(Dialog): frame.grid(row=2 + i, column=0, sticky="nsew") button = ttk.Button( - self, text="onle store values that have changed from their defaults" + self.top, text="onle store values that have changed from their defaults" ) button.grid(row=2, column=0) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="nsew") button = ttk.Button( diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 45f26a58..8cd7ad68 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -15,18 +15,18 @@ class SessionOptionsDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) - self.rowconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) session_id = self.app.core.session_id response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.config_frame = ConfigFrame(self, config=response.config) + self.config_frame = ConfigFrame(self.top, config=response.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index b9b7d77b..6ea66973 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -16,7 +16,7 @@ class SessionsDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_description() self.draw_tree() self.draw_buttons() @@ -27,7 +27,7 @@ class SessionsDialog(Dialog): :return: nothing """ label = ttk.Label( - self, + self.top, text="Below is a list of active CORE sessions. Double-click to \n" "connect to an existing session. Usually, only sessions in \n" "the RUNTIME state persist in the daemon, except for the \n" @@ -37,7 +37,7 @@ class SessionsDialog(Dialog): def draw_tree(self): self.tree = ttk.Treeview( - self, columns=("id", "state", "nodes"), show="headings" + self.top, columns=("id", "state", "nodes"), show="headings" ) self.tree.grid(row=1, sticky="nsew") self.tree.column("id", stretch=tk.YES) @@ -60,16 +60,18 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) + yscrollbar = ttk.Scrollbar(self.top, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=1, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) + xscrollbar = ttk.Scrollbar( + self.top, orient="horizontal", command=self.tree.xview + ) xscrollbar.grid(row=2, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) for i in range(4): frame.columnconfigure(i, weight=1) frame.grid(row=3, sticky="ew") diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index d33f7ca0..85a2a18f 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -7,6 +7,7 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog +from coretk.dialogs.mobilityconfig import MobilityConfigDialog class WlanConfigDialog(Dialog): @@ -30,7 +31,7 @@ class WlanConfigDialog(Dialog): self.draw() def draw(self): - self.columnconfigure(0, weight=1) + self.top.columnconfigure(0, weight=1) self.draw_name_config() self.draw_wlan_config() self.draw_subnet() @@ -43,7 +44,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") frame.columnconfigure(0, weight=1) @@ -59,10 +60,10 @@ class WlanConfigDialog(Dialog): :return: nothing """ - label = ttk.Label(self, text="Wireless") + label = ttk.Label(self.top, text="Wireless") label.grid(sticky="w", pady=2) - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -108,7 +109,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=3, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) @@ -130,12 +131,14 @@ class WlanConfigDialog(Dialog): :return: """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(pady=2, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) - button = ttk.Button(frame, text="ns-2 mobility script...") + button = ttk.Button( + frame, text="ns-2 mobility script...", command=self.click_mobility + ) button.grid(row=0, column=0, padx=2, sticky="ew") button = ttk.Button(frame, text="Link to all routers") @@ -150,7 +153,7 @@ class WlanConfigDialog(Dialog): :return: nothing """ - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) @@ -161,6 +164,10 @@ class WlanConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, padx=2, sticky="ew") + def click_mobility(self): + dialog = MobilityConfigDialog(self, self.app, self.canvas_node) + dialog.show() + def click_icon(self): dialog = IconDialog( self, self.app, self.canvas_node.name, self.canvas_node.image diff --git a/coretk/coretk/theme.py b/coretk/coretk/theme.py deleted file mode 100644 index 9525d35e..00000000 --- a/coretk/coretk/theme.py +++ /dev/null @@ -1,162 +0,0 @@ -import tkinter as tk -from tkinter import ttk - - -class Colors: - disabledfg = "DarkGrey" - frame = "#424242" - dark = "#222222" - darker = "#121212" - darkest = "black" - lighter = "#626262" - lightest = "#ffffff" - selectbg = "#4a6984" - selectfg = "#ffffff" - white = "white" - black = "black" - - -style = ttk.Style() -style.theme_create( - "black", - "clam", - { - ".": { - "configure": { - "background": Colors.frame, - "foreground": Colors.white, - "bordercolor": Colors.darkest, - "darkcolor": Colors.dark, - "lightcolor": Colors.lighter, - "troughcolor": Colors.darker, - "selectbackground": Colors.selectbg, - "selectforeground": Colors.selectfg, - "selectborderwidth": 0, - "font": "TkDefaultFont", - }, - "map": { - "background": [("disabled", Colors.frame), ("active", Colors.lighter)], - "foreground": [("disabled", Colors.disabledfg)], - "selectbackground": [("!focus", Colors.darkest)], - "selectforeground": [("!focus", Colors.white)], - }, - }, - "TButton": { - "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, - "map": { - "relief": [("pressed", tk.SUNKEN)], - "shiftrelief": [("pressed", 1)], - }, - }, - "TMenubutton": { - "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} - }, - "TCheckbutton": { - "configure": { - "indicatorbackground": Colors.white, - "indicatormargin": (1, 1, 4, 1), - } - }, - "TRadiobutton": { - "configure": { - "indicatorbackground": Colors.white, - "indicatormargin": (1, 1, 4, 1), - } - }, - "TEntry": { - "configure": { - "fieldbackground": Colors.white, - "foreground": Colors.black, - "padding": (2, 0), - } - }, - "TCombobox": { - "configure": { - "fieldbackground": Colors.white, - "foreground": Colors.black, - "padding": (2, 0), - } - }, - "TNotebook.Tab": { - "configure": {"padding": (6, 2, 6, 2)}, - "map": {"background": [("selected", Colors.lighter)]}, - }, - "Treeview": { - "configure": { - "fieldbackground": Colors.white, - "background": Colors.white, - "foreground": Colors.black, - }, - "map": { - "background": [("selected", Colors.selectbg)], - "foreground": [("selected", Colors.selectfg)], - }, - }, - }, -) -style.theme_use("black") - - -def update_menu(event): - bg = style.lookup(".", "background") - fg = style.lookup(".", "foreground") - abg = style.lookup(".", "lightcolor") - event.widget.config( - background=bg, foreground=fg, activebackground=abg, activeforeground=fg - ) - - -class Application(ttk.Frame): - def __init__(self, master=None): - super().__init__(master) - self.master.bind_class("Menu", "<>", update_menu) - self.master.geometry("800x600") - menu = tk.Menu(self.master) - menu.add_command(label="Command1") - menu.add_command(label="Command2") - submenu = tk.Menu(menu, tearoff=False) - submenu.add_command(label="Command1") - submenu.add_command(label="Command2") - menu.add_cascade(label="Submenu", menu=submenu) - self.master.config(menu=menu) - self.master.columnconfigure(0, weight=1) - self.master.rowconfigure(0, weight=1) - notebook = ttk.Notebook(self.master) - notebook.grid(sticky="nsew") - frame = ttk.Frame(notebook) - frame.grid(sticky="nsew") - ttk.Label(frame, text="Label").grid() - ttk.Entry(frame).grid() - ttk.Button(frame, text="Button").grid() - ttk.Combobox(frame, values=("one", "two", "three")).grid() - menubutton = ttk.Menubutton(frame, text="MenuButton") - menubutton.grid() - mbmenu = tk.Menu(menubutton, tearoff=False) - menubutton.config(menu=mbmenu) - mbmenu.add_command(label="Menu1") - mbmenu.add_command(label="Menu2") - submenu = tk.Menu(mbmenu, tearoff=False) - submenu.add_command(label="Command1") - submenu.add_command(label="Command2") - mbmenu.add_cascade(label="Submenu", menu=submenu) - ttk.Radiobutton(frame, text="Radio Button").grid() - ttk.Checkbutton(frame, text="Check Button").grid() - tv = ttk.Treeview(frame, columns=("one", "two", "three"), show="headings") - tv.grid() - tv.column("one", stretch=tk.YES) - tv.heading("one", text="ID") - tv.column("two", stretch=tk.YES) - tv.heading("two", text="State") - tv.column("three", stretch=tk.YES) - tv.heading("three", text="Node Count") - tv.insert("", tk.END, text="1", values=("v1", "v2", "v3")) - tv.insert("", tk.END, text="2", values=("v1", "v2", "v3")) - notebook.add(frame, text="Tab1") - frame = ttk.Frame(notebook) - frame.grid(sticky="nsew") - notebook.add(frame, text="Tab2") - - -if __name__ == "__main__": - app = Application() - app.mainloop() diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py new file mode 100644 index 00000000..b661b644 --- /dev/null +++ b/coretk/coretk/themes.py @@ -0,0 +1,127 @@ +import tkinter as tk + +DARK = "black" + + +class Styles: + tooltip = "Tooltip.TLabel" + tooltip_frame = "Tooltip.TFrame" + + +class Colors: + disabledfg = "DarkGrey" + frame = "#424242" + dark = "#222222" + darker = "#121212" + darkest = "black" + lighter = "#626262" + lightest = "#ffffff" + selectbg = "#4a6984" + selectfg = "#ffffff" + white = "white" + black = "black" + + +def load(style): + style.theme_create( + DARK, + "clam", + { + ".": { + "configure": { + "background": Colors.frame, + "foreground": Colors.white, + "bordercolor": Colors.darkest, + "darkcolor": Colors.dark, + "lightcolor": Colors.lighter, + "troughcolor": Colors.darker, + "selectbackground": Colors.selectbg, + "selectforeground": Colors.selectfg, + "selectborderwidth": 0, + "font": "TkDefaultFont", + }, + "map": { + "background": [ + ("disabled", Colors.frame), + ("active", Colors.lighter), + ], + "foreground": [("disabled", Colors.disabledfg)], + "selectbackground": [("!focus", Colors.darkest)], + "selectforeground": [("!focus", Colors.white)], + }, + }, + "TButton": { + "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "map": { + "relief": [("pressed", tk.SUNKEN)], + "shiftrelief": [("pressed", 1)], + }, + }, + "TMenubutton": { + "configure": {"width": 11, "padding": (5, 1), "relief": tk.RAISED} + }, + "TCheckbutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TRadiobutton": { + "configure": { + "indicatorbackground": Colors.white, + "indicatormargin": (1, 1, 4, 1), + } + }, + "TEntry": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TCombobox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + } + }, + "TNotebook.Tab": { + "configure": {"padding": (6, 2, 6, 2)}, + "map": {"background": [("selected", Colors.lighter)]}, + }, + "Treeview": { + "configure": { + "fieldbackground": Colors.white, + "background": Colors.white, + "foreground": Colors.black, + }, + "map": { + "background": [("selected", Colors.selectbg)], + "foreground": [("selected", Colors.selectfg)], + }, + }, + Styles.tooltip: { + "configure": {"justify": tk.LEFT, "relief": tk.SOLID, "borderwidth": 0} + }, + Styles.tooltip_frame: {"configure": {}}, + }, + ) + + +def update_toplevel(style, event): + if not isinstance(event.widget, tk.Toplevel): + return + bg = style.lookup(".", "background") + event.widget.config(background=bg) + + +def update_menu(style, event): + if not isinstance(event.widget, tk.Menu): + return + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + abg = style.lookup(".", "lightcolor") + event.widget.config( + background=bg, foreground=fg, activebackground=abg, activeforeground=fg + ) diff --git a/coretk/coretk/tooltip.py b/coretk/coretk/tooltip.py index 1877fd24..9a3f7ade 100644 --- a/coretk/coretk/tooltip.py +++ b/coretk/coretk/tooltip.py @@ -1,6 +1,8 @@ import tkinter as tk from tkinter import ttk +from coretk.themes import Styles + class Tooltip(object): """ @@ -41,15 +43,12 @@ class Tooltip(object): self.tw = tk.Toplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry("+%d+%d" % (x, y)) - label = ttk.Label( - self.tw, - text=self.text, - justify=tk.LEFT, - background="#FFFFEA", - relief=tk.SOLID, - borderwidth=0, - ) - label.grid(padx=1) + self.tw.rowconfigure(0, weight=1) + self.tw.columnconfigure(0, weight=1) + frame = ttk.Frame(self.tw, style=Styles.tooltip_frame, padding=3) + frame.grid(sticky="nsew") + label = ttk.Label(frame, text=self.text, style=Styles.tooltip) + label.grid() def close(self, event=None): if self.tw: diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9ae9e851..87651701 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -17,9 +17,9 @@ INT_TYPES = { } -class FrameScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, _cls=tk.Frame, **kw): - super().__init__(master, cnf, **kw) +class FrameScroll(ttk.LabelFrame): + def __init__(self, master=None, _cls=tk.Frame, **kw): + super().__init__(master, **kw) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.canvas = tk.Canvas(self, highlightthickness=0) @@ -54,8 +54,8 @@ class FrameScroll(tk.LabelFrame): class ConfigFrame(FrameScroll): - def __init__(self, master=None, cnf={}, config=None, **kw): - super().__init__(master, cnf, ttk.Notebook, **kw) + def __init__(self, master=None, config=None, **kw): + super().__init__(master, ttk.Notebook, **kw) self.config = config self.values = {} @@ -126,9 +126,9 @@ class ConfigFrame(FrameScroll): return {x: self.config[x].value for x in self.config} -class ListboxScroll(tk.LabelFrame): - def __init__(self, master=None, cnf={}, **kw): - super().__init__(master, cnf, **kw) +class ListboxScroll(ttk.LabelFrame): + def __init__(self, master=None, **kw): + super().__init__(master, **kw) self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) @@ -141,8 +141,8 @@ class ListboxScroll(tk.LabelFrame): class CheckboxList(FrameScroll): - def __init__(self, master=None, cnf={}, clicked=None, **kw): - super().__init__(master, cnf, **kw) + def __init__(self, master=None, clicked=None, **kw): + super().__init__(master, **kw) self.clicked = clicked self.frame.columnconfigure(0, weight=1) From f01a8a4cb2789b9fa8aef8da90a08828da83c5ed Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 13 Nov 2019 10:51:16 -0800 Subject: [PATCH 113/305] node service config --- coretk/coretk/coreclient.py | 7 +++++++ coretk/coretk/dialogs/serviceconfiguration.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0d562f8a..51aa8e46 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -427,6 +427,13 @@ class CoreClient: ) logging.debug("set node service %s", response) + def get_node_service_file(self, node_id, service_name, file_name): + response = self.client.get_node_service_file( + self.session_id, node_id, service_name, file_name + ) + logging.debug("get service file %s", response) + return response.data + def create_nodes_and_links(self): node_protos = self.get_nodes_proto() link_protos = self.get_links_proto() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 5cc41116..c1687cd8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -2,6 +2,7 @@ import logging import tkinter as tk from tkinter import ttk +from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog @@ -38,6 +39,7 @@ class ServiceConfiguration(Dialog): self.validate_commands_listbox = None self.validation_time_entry = None self.validation_mode_entry = None + self.service_file_data = None self.load() self.draw() @@ -153,6 +155,9 @@ class ServiceConfiguration(Dialog): button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") + self.service_file_data = ScrolledText(tab1) + self.service_file_data.grid(row=4, column=0, sticky="nsew") + # tab 2 label = ttk.Label( tab2, @@ -333,6 +338,9 @@ class ServiceConfiguration(Dialog): validate_commands, shutdown_commands, ) + filename = self.filename_combobox.get() + file_data = self.service_file_data.get() + print(filename, file_data) logging.info( "%s, %s, %s, %s, %s", metadata, @@ -347,7 +355,14 @@ class ServiceConfiguration(Dialog): ) def display_service_file_data(self, event): - print("not implemented") + combobox = event.widget + filename = combobox.get() + print(filename) + file_data = self.app.core.get_node_service_file( + self.canvas_node.core_id, self.service_name, filename + ) + self.service_file_data.delete(1.0, "end") + self.service_file_data.insert("end", file_data) def click_defaults(self): logging.info("not implemented") From 40b2c270e49684bae2692ffc2b9fc4e318b09ca6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 12:11:37 -0800 Subject: [PATCH 114/305] fixed typo in canvas dialog variable, updated theming to make label frame have a relief groove border --- coretk/coretk/dialogs/canvassizeandscale.py | 8 ++++---- coretk/coretk/themes.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 09132aef..5bbd2ec2 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -8,7 +8,7 @@ from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] -FRAME_BAD = 5 +FRAME_PAD = 5 PAD = (0, 0, 5, 0) PADX = 5 @@ -52,7 +52,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -89,7 +89,7 @@ class SizeAndScaleDialog(Dialog): label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_BAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -105,7 +105,7 @@ class SizeAndScaleDialog(Dialog): def draw_reference_point(self): label_frame = ttk.Labelframe( - self.top, text="Reference Point", padding=FRAME_BAD + self.top, text="Reference Point", padding=FRAME_PAD ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index b661b644..8eefa35e 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -86,6 +86,7 @@ def load(style): "padding": (2, 0), } }, + "TLabelframe": {"configure": {"relief": tk.GROOVE}}, "TNotebook.Tab": { "configure": {"padding": (6, 2, 6, 2)}, "map": {"background": [("selected", Colors.lighter)]}, From 08927b180a37488868050eaa7fa1cff1e06bbe6b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 12:42:16 -0800 Subject: [PATCH 115/305] changed service checklist style --- coretk/coretk/themes.py | 8 ++++++++ coretk/coretk/widgets.py | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 8eefa35e..8bbdb545 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -6,6 +6,7 @@ DARK = "black" class Styles: tooltip = "Tooltip.TLabel" tooltip_frame = "Tooltip.TFrame" + service_checkbutton = "Service.TCheckbutton" class Colors: @@ -20,6 +21,7 @@ class Colors: selectfg = "#ffffff" white = "white" black = "black" + listboxbg = "#f2f1f0" def load(style): @@ -106,6 +108,12 @@ def load(style): "configure": {"justify": tk.LEFT, "relief": tk.SOLID, "borderwidth": 0} }, Styles.tooltip_frame: {"configure": {}}, + Styles.service_checkbutton: { + "configure": { + "background": Colors.listboxbg, + "foreground": Colors.black, + } + }, }, ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 87651701..d91f7f8a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -4,6 +4,7 @@ from functools import partial from tkinter import ttk from core.api.grpc import core_pb2 +from coretk.themes import Styles INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -136,6 +137,7 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) + logging.info("listbox background: %s", self.listbox.cget("background")) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) @@ -149,5 +151,11 @@ class CheckboxList(FrameScroll): def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) + checkbox = ttk.Checkbutton( + self.frame, + text=name, + variable=var, + command=func, + style=Styles.service_checkbutton, + ) checkbox.grid(sticky="w") From d63da73581174b93e979aecf5199ca5e6d8b4d67 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 13:19:18 -0800 Subject: [PATCH 116/305] updated framescroll to dynamically set bg based on current style --- coretk/coretk/app.py | 2 -- coretk/coretk/dialogs/customnodes.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 4 ++-- coretk/coretk/dialogs/nodeservice.py | 2 +- coretk/coretk/dialogs/sessionoptions.py | 2 +- coretk/coretk/themes.py | 6 +++--- coretk/coretk/widgets.py | 23 +++++++++-------------- 7 files changed, 17 insertions(+), 24 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index bc3ac9dd..b66f1388 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -40,8 +40,6 @@ class Application(tk.Frame): self.style.theme_use(themes.DARK) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) - func = partial(themes.update_toplevel, self.style) - self.master.bind_class("Toplevel", "<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 154b7868..f427f08b 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -35,7 +35,7 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked ) self.services.grid(row=0, column=1, sticky="nsew") diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 181e1264..d64e9089 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -93,7 +93,7 @@ class EmaneConfiguration(Dialog): self.emane_dialog.top.columnconfigure(0, weight=1) self.emane_dialog.top.rowconfigure(0, weight=1) self.emane_config_frame = ConfigFrame( - self.emane_dialog.top, config=self.options + self.emane_dialog.top, self.app, config=self.options ) self.emane_config_frame.draw_config() self.emane_config_frame.grid(sticky="nsew") @@ -167,7 +167,7 @@ class EmaneConfiguration(Dialog): self.model_options = response.config self.model_config_frame = ConfigFrame( - self.emane_model_dialog.top, config=self.model_options + self.emane_model_dialog.top, self.app, config=self.model_options ) self.model_config_frame.grid(sticky="nsew") self.model_config_frame.draw_config() diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index ce5a4715..9ced9205 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -38,7 +38,7 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked ) self.services.grid(row=0, column=1, sticky="nsew") diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 8cd7ad68..b2666015 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -22,7 +22,7 @@ class SessionOptionsDialog(Dialog): response = self.app.core.client.get_session_options(session_id) logging.info("session options: %s", response) - self.config_frame = ConfigFrame(self.top, config=response.config) + self.config_frame = ConfigFrame(self.top, self.app, config=response.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 8bbdb545..f6dced19 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk DARK = "black" @@ -118,9 +119,8 @@ def load(style): ) -def update_toplevel(style, event): - if not isinstance(event.widget, tk.Toplevel): - return +def update_bg(style, event): + logging.info("updating background: %s", event.widget) bg = style.lookup(".", "background") event.widget.config(background=bg) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index d91f7f8a..e825fdb4 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -4,7 +4,6 @@ from functools import partial from tkinter import ttk from core.api.grpc import core_pb2 -from coretk.themes import Styles INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -19,11 +18,13 @@ INT_TYPES = { class FrameScroll(ttk.LabelFrame): - def __init__(self, master=None, _cls=tk.Frame, **kw): + def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) + self.app = app self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) - self.canvas = tk.Canvas(self, highlightthickness=0) + bg = self.app.style.lookup(".", "background") + self.canvas = tk.Canvas(self, highlightthickness=0, background=bg) self.canvas.grid(row=0, sticky="nsew", padx=2, pady=2) self.canvas.columnconfigure(0, weight=1) self.canvas.rowconfigure(0, weight=1) @@ -55,8 +56,8 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): - def __init__(self, master=None, config=None, **kw): - super().__init__(master, ttk.Notebook, **kw) + def __init__(self, master, app, config, **kw): + super().__init__(master, app, ttk.Notebook, **kw) self.config = config self.values = {} @@ -143,19 +144,13 @@ class ListboxScroll(ttk.LabelFrame): class CheckboxList(FrameScroll): - def __init__(self, master=None, clicked=None, **kw): - super().__init__(master, **kw) + def __init__(self, master, app, clicked=None, **kw): + super().__init__(master, app, **kw) self.clicked = clicked self.frame.columnconfigure(0, weight=1) def add(self, name, checked): var = tk.BooleanVar(value=checked) func = partial(self.clicked, name, var) - checkbox = ttk.Checkbutton( - self.frame, - text=name, - variable=var, - command=func, - style=Styles.service_checkbutton, - ) + checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") From 145abca8632dcf1f858b8182683a44567b5dedb4 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 22:49:32 -0800 Subject: [PATCH 117/305] added theme configuration to preferences dialog --- coretk/coretk/app.py | 4 ++-- coretk/coretk/appconfig.py | 3 +++ coretk/coretk/dialogs/preferences.py | 36 +++++++++++++++++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index b66f1388..5481ad19 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,6 +15,7 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + self.config = appconfig.read() self.style = ttk.Style() self.setup_theme() self.menubar = None @@ -29,7 +30,6 @@ class Application(tk.Frame): self.radiovar = tk.IntVar(value=1) self.show_grid_var = tk.IntVar(value=1) self.adjust_to_dim_var = tk.IntVar(value=0) - self.config = appconfig.read() self.core = CoreClient(self) self.setup_app() self.draw() @@ -37,7 +37,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) - self.style.theme_use(themes.DARK) + self.style.theme_use(self.config["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 67f97181..e8f5db6e 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -6,6 +6,8 @@ from pathlib import Path import yaml # gui home paths +from coretk import themes + HOME_PATH = Path.home().joinpath(".coretk") BACKGROUNDS_PATH = HOME_PATH.joinpath("backgrounds") CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") @@ -68,6 +70,7 @@ def check_directory(): editor = EDITORS[1] config = { "preferences": { + "theme": themes.DARK, "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 0c426d3c..148cb6bb 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -1,3 +1,4 @@ +import logging import tkinter as tk from tkinter import ttk @@ -10,41 +11,52 @@ class PreferencesDialog(Dialog): super().__init__(master, app, "Preferences", modal=True) preferences = self.app.config["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) + self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) self.draw() def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_programs() + self.draw_preferences() self.draw_buttons() - def draw_programs(self): - frame = ttk.LabelFrame(self.top, text="Programs") + def draw_preferences(self): + frame = ttk.LabelFrame(self.top, text="Preferences") frame.grid(sticky="ew", pady=2) frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Editor") + label = ttk.Label(frame, text="Theme") label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + themes = self.app.style.theme_names() + combobox = ttk.Combobox( + frame, textvariable=self.theme, values=themes, state="readonly" + ) + combobox.set(self.theme.get()) + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.theme_change) + + label = ttk.Label(frame, text="Editor") + label.grid(row=1, column=0, pady=2, padx=2, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) - combobox.grid(row=0, column=1, sticky="ew") + combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + label.grid(row=2, column=0, pady=2, padx=2, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, values=appconfig.TERMINALS, state="readonly", ) - combobox.grid(row=1, column=1, sticky="ew") + combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + label.grid(row=3, column=0, pady=2, padx=2, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) - entry.grid(row=2, column=1, sticky="ew") + entry.grid(row=3, column=1, sticky="ew") def draw_buttons(self): frame = ttk.Frame(self.top) @@ -58,10 +70,16 @@ class PreferencesDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def theme_change(self, event): + theme = self.theme.get() + logging.info("changing theme: %s", theme) + self.app.style.theme_use(theme) + def click_save(self): preferences = self.app.config["preferences"] preferences["terminal"] = self.terminal.get() preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() + preferences["theme"] = self.theme.get() self.app.save_config() self.destroy() From 31d87810086f1482715cea1d619c6499d97b130e Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 13 Nov 2019 23:03:12 -0800 Subject: [PATCH 118/305] fixed emane icon background to alpha, updated wlan icon to have a white cloud instead of alpha --- coretk/coretk/icons/emane.gif | Bin 1111 -> 337 bytes coretk/coretk/icons/wlan.gif | Bin 146 -> 173 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/icons/emane.gif index 8a3d3850d4c01d35e3322c97b8cd0ab00251ecd8..0531a932532f4573b3f581364a01a059a7a54e26 100644 GIT binary patch literal 337 zcmZ?wbhEHbRA5kGc+A591jgD7#-R)|uL6w9SoqqGI`S+J)Z}3~{55m3>-wv~T?H#< zW@b2-6z*ZzwOP^2gZrP5;o(z43Q~UM#g#@z4%MQzE@c_^bw;Lk?IH|)m32vxaa}CA z3fvO{o3pwW8foTD(O4XmndY#7V{QjS_&47}7hO2# Z!QgRoI`a`OK~`a{m=hZowsSC80|0bwhHwA? literal 1111 zcmW+!Uue)(7(D}p89_BM)XmtxBE44IKCNjFQrT)0L!wKI3bD;^%+_DXryeeCA)}Qv zqWBE4bQrr}ZY?5waS`Q$uZ-BG@MV#(psWa4Qc771P(TF@bg;Tq+#*nc3B1B2 zK!Qk62`0fMP$DFXM3ra~T_Pnxl1NfXCdnmHG9-&+m28q-GNnL@NKq*!#idXPp$JuI zLKjv#sxO zw1^hfVp?1al~9UOm8NuMWkKb*7}Q_}uN<)e3t~Ynm<6{$i?Apb)uLH+i?jqwVo5ET zCAUP&uq>9w?ZR~VpO9U-B`I_g)I(sn8T|uJivo^P!Hz8JGnl_iy~ z3aqLSz#t6DU<}ScMlcGaG8&^Zk_k-0q)f)-Ok@VLFe|e$J2P3pA}q>cEY7k_Ru!#E zyRy8RdxdK$1^z}{u4nEZX z&53(&(@U*+B98YSUj5?1%Z=$|%fORwc6Tg3piPIL-c~!jIQ{Ctxs&TQ*DYRn`If;W zpa1>ssk!l6f3I8h!yTs{o9sUN^RM&kj?3&_KOHFbkM?(+9%=8n{z%#K;M|Fg7gt~V z_J{pnZhvj`w@=o8FnDHP`_7B4y*Cbh-@5Cav4)wCXW!~s7+)Ie%@;jEB@otNY*qmFfdba%1_PAOJ`90$->CRz{sEjQUOxS zz!cuozw-23{>32+u5qs4D3yPjAx>eEpJZvqkAmdpb(+5?eJb>ho%FhP{o<=WA`fyl xetyz3Q~L84X{Px{Rg%)5F0}|bV|49q%MP!#xfR=XUU=m{gSY?m^L8c%YXB@FJ0t)A From 4d2b84b107cff0fce3e943ca8dfd8d9bb36b420c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 11:26:20 -0800 Subject: [PATCH 119/305] updated wallpaper dialog to now save and redisplay current wallpaper, updated app to display in center of screen on launch --- coretk/coretk/app.py | 29 +++-- coretk/coretk/dialogs/canvasbackground.py | 132 ++++++++++---------- coretk/coretk/dialogs/canvassizeandscale.py | 66 +++++----- coretk/coretk/graph.py | 14 ++- coretk/coretk/menuaction.py | 1 - 5 files changed, 124 insertions(+), 118 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 5481ad19..11fad378 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -15,21 +15,19 @@ from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) - self.config = appconfig.read() - self.style = ttk.Style() - self.setup_theme() + # widgets self.menubar = None self.toolbar = None self.canvas = None self.statusbar = None - self.is_open_xml = False - self.size_and_scale = None + + # variables self.set_wallpaper = None - self.wallpaper_id = None - self.current_wallpaper = None - self.radiovar = tk.IntVar(value=1) - self.show_grid_var = tk.IntVar(value=1) - self.adjust_to_dim_var = tk.IntVar(value=0) + + # setup + self.config = appconfig.read() + self.style = ttk.Style() + self.setup_theme() self.core = CoreClient(self) self.setup_app() self.draw() @@ -43,12 +41,21 @@ class Application(tk.Frame): def setup_app(self): self.master.title("CORE") - self.master.geometry("1000x800") + self.center() self.master.protocol("WM_DELETE_WINDOW", self.on_closing) image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + def center(self): + width = 1000 + height = 800 + screen_width = self.master.winfo_screenwidth() + screen_height = self.master.winfo_screenheight() + x = int((screen_width / 2) - (width / 2)) + y = int((screen_height / 2) - (height / 2)) + self.master.geometry(f"{width}x{height}+{x}+{y}") + def draw(self): self.master.option_add("*tearOff", tk.FALSE) self.menubar = Menubar(self.master, self) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 7b1dd186..80b1ae5f 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -10,6 +10,7 @@ from PIL import Image, ImageTk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog +from coretk.images import Images PADX = 5 @@ -31,11 +32,11 @@ class CanvasBackgroundDialog(Dialog): """ super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas - self.radiovar = tk.IntVar(value=self.app.radiovar.get()) - self.show_grid_var = tk.IntVar(value=self.app.show_grid_var.get()) - self.adjust_to_dim_var = tk.IntVar(value=self.app.adjust_to_dim_var.get()) + self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) + self.show_grid = tk.IntVar(value=self.canvas.show_grid.get()) + self.adjust_to_dim = tk.IntVar(value=self.canvas.adjust_to_dim.get()) + self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.image_label = None - self.file_name = tk.StringVar() self.options = [] self.draw() @@ -57,6 +58,8 @@ class CanvasBackgroundDialog(Dialog): def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") label.grid(row=1, column=0, sticky="ew") + if self.filename.get(): + self.draw_preview() def draw_image_selection(self): frame = ttk.Frame(self.top) @@ -65,7 +68,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(2, weight=1) frame.grid(row=2, column=0, sticky="ew") - entry = ttk.Entry(frame, textvariable=self.file_name) + entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() entry.grid(row=0, column=0, sticky="ew", padx=PADX) @@ -84,41 +87,45 @@ class CanvasBackgroundDialog(Dialog): frame.grid(row=3, column=0, sticky="ew") button = ttk.Radiobutton( - frame, text="upper-left", value=1, variable=self.radiovar + frame, text="upper-left", value=1, variable=self.scale_option ) button.grid(row=0, column=0, sticky="ew") self.options.append(button) button = ttk.Radiobutton( - frame, text="centered", value=2, variable=self.radiovar + frame, text="centered", value=2, variable=self.scale_option ) button.grid(row=0, column=1, sticky="ew") self.options.append(button) - button = ttk.Radiobutton(frame, text="scaled", value=3, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="scaled", value=3, variable=self.scale_option + ) button.grid(row=0, column=2, sticky="ew") self.options.append(button) - button = ttk.Radiobutton(frame, text="titled", value=4, variable=self.radiovar) + button = ttk.Radiobutton( + frame, text="titled", value=4, variable=self.scale_option + ) button.grid(row=0, column=3, sticky="ew") self.options.append(button) def draw_additional_options(self): checkbutton = ttk.Checkbutton( - self.top, text="Show grid", variable=self.show_grid_var + self.top, text="Show grid", variable=self.show_grid ) checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self.top, text="Adjust canvas size to image dimensions", - variable=self.adjust_to_dim_var, + variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) - self.show_grid_var.set(1) - self.adjust_to_dim_var.set(0) + self.show_grid.set(1) + self.adjust_to_dim.set(0) def draw_buttons(self): frame = ttk.Frame(self.top) @@ -142,13 +149,13 @@ class CanvasBackgroundDialog(Dialog): ), ) if filename: - self.file_name.set(filename) - width, height = 250, 135 - img = Image.open(filename) - img = img.resize((width, height), Image.ANTIALIAS) - tk_img = ImageTk.PhotoImage(img) - self.image_label.config(image=tk_img, width=width) - self.image_label.image = tk_img + self.filename.set(filename) + self.draw_preview() + + def draw_preview(self): + image = Images.create(self.filename.get(), 250, 135) + self.image_label.config(image=image) + self.image_label.image = image def click_clear(self): """ @@ -157,19 +164,20 @@ class CanvasBackgroundDialog(Dialog): :return: nothing """ # delete entry - self.file_name.set("") + self.filename.set("") # delete display image self.image_label.config(image="", width=32) + self.image_label.image = None def click_adjust_canvas(self): # deselect all radio buttons and grey them out - if self.adjust_to_dim_var.get() == 1: - self.radiovar.set(0) + if self.adjust_to_dim.get() == 1: + self.scale_option.set(0) for option in self.options: option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again - elif self.adjust_to_dim_var.get() == 0: - self.radiovar.set(1) + elif self.adjust_to_dim.get() == 0: + self.scale_option.set(1) for option in self.options: option.config(state=tk.NORMAL) else: @@ -192,9 +200,8 @@ class CanvasBackgroundDialog(Dialog): :return: nothing """ - canvas = self.app.canvas - grid = canvas.find_withtag("rectangle")[0] - x0, y0, x1, y1 = canvas.coords(grid) + grid = self.canvas.find_withtag("rectangle")[0] + x0, y0, x1, y1 = self.canvas.coords(grid) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h @@ -224,15 +231,12 @@ class CanvasBackgroundDialog(Dialog): cropped_tk = ImageTk.PhotoImage(cropped) # place left corner of image to the left corner of the canvas - self.app.croppedwallpaper = cropped_tk - + self.canvas.wallpaper_drawn = cropped_tk self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid def center(self, img): """ @@ -261,13 +265,13 @@ class CanvasBackgroundDialog(Dialog): cropped_tk = ImageTk.PhotoImage(cropped) # place the center of the image at the center of the canvas - self.app.croppedwallpaper = cropped_tk self.delete_canvas_components(["wallpaper"]) # self.delete_previous_wallpaper() wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = cropped_tk def scaled(self, img): """ @@ -279,15 +283,12 @@ class CanvasBackgroundDialog(Dialog): canvas_w, canvas_h = self.get_canvas_width_and_height() resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) image_tk = ImageTk.PhotoImage(resized_image) - self.app.croppedwallpaper = image_tk - self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" ) - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = image_tk def tiled(self, img): return @@ -310,20 +311,15 @@ class CanvasBackgroundDialog(Dialog): self.delete_canvas_components(["wallpaper"]) self.draw_new_canvas(img_w, img_h) wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.app.croppedwallpaper = image_tk - self.app.wallpaper_id = wid + self.canvas.wallpaper_id = wid + self.canvas.wallpaper_drawn = image_tk - def show_grid(self): - """ - - :return: nothing - """ - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) - - if self.show_grid_var.get() == 0: + def draw_grid(self): + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + if self.show_grid.get() == 0: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.HIDDEN) - elif self.show_grid_var.get() == 1: + elif self.show_grid.get() == 1: for i in self.canvas.find_withtag("gridline"): self.canvas.itemconfig(i, state=tk.NORMAL) self.canvas.lift(i) @@ -331,33 +327,36 @@ class CanvasBackgroundDialog(Dialog): logging.error("canvasbackground.py show_grid invalid value") def save_wallpaper_options(self): - self.app.radiovar.set(self.radiovar.get()) - self.app.show_grid_var.set(self.show_grid_var.get()) - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) + self.canvas.scale_option.set(self.scale_option.get()) + self.canvas.show_grid.set(self.show_grid.get()) + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) def click_apply(self): - filename = self.file_name.get() + filename = self.filename.get() if not filename: self.delete_canvas_components(["wallpaper"]) self.destroy() - self.app.current_wallpaper = None + self.canvas.wallpaper = None + self.canvas.wallpaper_file = None self.save_wallpaper_options() return try: img = Image.open(filename) - self.app.current_wallpaper = img + self.canvas.wallpaper = img + self.canvas.wallpaper_file = filename except FileNotFoundError: logging.error("invalid background: %s", filename) - if self.app.wallpaper_id: - self.canvas.delete(self.app.wallpaper_id) + if self.canvas.wallpaper_id: + self.canvas.delete(self.canvas.wallpaper_id) + self.canvas.wallpaper_id = None self.destroy() return - self.app.adjust_to_dim_var.set(self.adjust_to_dim_var.get()) - if self.adjust_to_dim_var.get() == 0: - self.app.radiovar.set(self.radiovar.get()) - option = ScaleOption(self.radiovar.get()) + self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + if self.adjust_to_dim.get() == 0: + self.canvas.scale_option.set(self.scale_option.get()) + option = ScaleOption(self.scale_option.get()) if option == ScaleOption.UPPER_LEFT: self.upper_left(img) elif option == ScaleOption.CENTERED: @@ -365,10 +364,9 @@ class CanvasBackgroundDialog(Dialog): elif option == ScaleOption.SCALED: self.scaled(img) elif option == ScaleOption.TILED: - print("not implemented yet") - - elif self.adjust_to_dim_var.get() == 1: + logging.warning("tiled background not implemented yet") + elif self.adjust_to_dim.get() == 1: self.canvas_to_image_dimension(img) - self.show_grid() + self.draw_grid() self.destroy() diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 5bbd2ec2..598d8d37 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -1,6 +1,7 @@ """ size and scale """ +import logging import tkinter as tk from tkinter import font, ttk @@ -9,7 +10,6 @@ from coretk.dialogs.dialog import Dialog DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] FRAME_PAD = 5 -PAD = (0, 0, 5, 0) PADX = 5 @@ -21,13 +21,13 @@ class SizeAndScaleDialog(Dialog): :param app: main application """ super().__init__(master, app, "Canvas Size and Scale", modal=True) - self.meter_per_pixel = self.app.canvas.meters_per_pixel + self.canvas = self.app.canvas + self.meter_per_pixel = self.canvas.meters_per_pixel self.section_font = font.Font(weight="bold") # get current canvas dimensions - canvas = self.app.canvas - plot = canvas.find_withtag("rectangle") - x0, y0, x1, y1 = canvas.bbox(plot[0]) + plot = self.canvas.find_withtag("rectangle") + x0, y0, x1, y1 = self.canvas.bbox(plot[0]) width = abs(x0 - x1) - 2 height = abs(y0 - y1) - 2 self.pixel_width = tk.IntVar(value=width) @@ -122,14 +122,12 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w", padx=PADX) - x_var = tk.StringVar(value=0) - entry = ttk.Entry(frame, textvariable=x_var) + entry = ttk.Entry(frame, textvariable=self.x) entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w", padx=PADX) - y_var = tk.StringVar(value=0) - entry = ttk.Entry(frame, textvariable=y_var) + entry = ttk.Entry(frame, textvariable=self.y) entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(label_frame, text="Translates To") @@ -181,40 +179,40 @@ class SizeAndScaleDialog(Dialog): :return: nothing """ width, height = self.pixel_width.get(), self.pixel_height.get() - - canvas = self.app.canvas - canvas.config(scrollregion=(0, 0, width + 200, height + 200)) + self.canvas.config(scrollregion=(0, 0, width + 200, height + 200)) # delete old plot and redraw - for i in canvas.find_withtag("gridline"): - canvas.delete(i) - for i in canvas.find_withtag("rectangle"): - canvas.delete(i) + for i in self.canvas.find_withtag("gridline"): + self.canvas.delete(i) + for i in self.canvas.find_withtag("rectangle"): + self.canvas.delete(i) - canvas.draw_grid(width=width, height=height) + self.canvas.draw_grid(width=width, height=height) # lift anything that is drawn on the plot before for tag in DRAW_OBJECT_TAGS: - for i in canvas.find_withtag(tag): - canvas.lift(i) + for i in self.canvas.find_withtag(tag): + self.canvas.lift(i) def click_apply(self): meter_per_pixel = float(self.scale.get()) / 100 - self.app.canvas.meters_per_pixel = meter_per_pixel + self.canvas.meters_per_pixel = meter_per_pixel self.redraw_grid() - # if there is a current wallpaper showing, redraw it based on current wallpaper options + # if there is a current wallpaper showing, redraw it based on current + # wallpaper options wallpaper_tool = self.app.set_wallpaper - current_wallpaper = self.app.current_wallpaper - if current_wallpaper: - if self.app.adjust_to_dim_var.get() == 0: - if self.app.radiovar.get() == ScaleOption.UPPER_LEFT.value: - wallpaper_tool.upper_left(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.CENTERED.value: - wallpaper_tool.center(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.SCALED.value: - wallpaper_tool.scaled(current_wallpaper) - elif self.app.radiovar.get() == ScaleOption.TILED.value: - print("not implemented") - elif self.app.adjust_to_dim_var.get() == 1: - wallpaper_tool.canvas_to_image_dimension(current_wallpaper) + wallpaper = self.canvas.wallpaper + if wallpaper: + if self.canvas.adjust_to_dim.get() == 0: + scale_option = ScaleOption(self.canvas.scale_option.get()) + if scale_option == ScaleOption.UPPER_LEFT: + wallpaper_tool.upper_left(wallpaper) + elif scale_option == ScaleOption.CENTERED: + wallpaper_tool.center(wallpaper) + elif scale_option == ScaleOption.SCALED: + wallpaper_tool.scaled(wallpaper) + elif scale_option == ScaleOption.TILED: + logging.warning("tiled background not implemented") + elif self.canvas.adjust_to_dim.get() == 1: + wallpaper_tool.canvas_to_image_dimension(wallpaper) wallpaper_tool.show_grid() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 5cdd2eec..1bec481b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -43,11 +43,8 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 - self.canvas_management = CanvasComponentManagement(self, core) - self.canvas_action = CanvasAction(master, self) - self.setup_menus() self.setup_bindings() self.draw_grid() @@ -57,6 +54,15 @@ class CanvasGraph(tk.Canvas): self.wireless_draw = WirelessConnection(self, core) self.is_node_context_opened = False + # background related + self.wallpaper_id = None + self.wallpaper = None + self.wallpaper_drawn = None + self.wallpaper_file = "" + self.scale_option = tk.IntVar(value=1) + self.show_grid = tk.IntVar(value=1) + self.adjust_to_dim = tk.IntVar(value=0) + def setup_menus(self): self.node_context = tk.Menu(self.master) self.node_context.add_command( @@ -95,8 +101,6 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.draw_existing_component(session) - # self.grpc_manager.wlanconfig_management.load_wlan_configurations(self.core_grpc) - def setup_bindings(self): """ Bind any mouse events or hot keys to the matching action diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 98b3d254..b47c6ef2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -73,7 +73,6 @@ class MenuAction: def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") - self.app.is_open_xml = True file_path = filedialog.askopenfilename( initialdir=str(XML_PATH), title="Open", From aec1126a14cca6e1eaaf8394f498cadee13e142d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 12:58:27 -0800 Subject: [PATCH 120/305] updated canvas dialogs to make use of common canvas redraw/wallpaper logic --- coretk/coretk/app.py | 3 - coretk/coretk/dialogs/canvasbackground.py | 196 ++------------------ coretk/coretk/dialogs/canvassizeandscale.py | 47 +---- coretk/coretk/graph.py | 154 ++++++++++++++- 4 files changed, 159 insertions(+), 241 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 11fad378..185e5968 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -21,9 +21,6 @@ class Application(tk.Frame): self.canvas = None self.statusbar = None - # variables - self.set_wallpaper = None - # setup self.config = appconfig.read() self.style = ttk.Style() diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 80b1ae5f..8fb92c8b 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -1,12 +1,11 @@ """ set wallpaper """ -import enum import logging import tkinter as tk from tkinter import filedialog, ttk -from PIL import Image, ImageTk +from PIL import Image from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog @@ -15,14 +14,6 @@ from coretk.images import Images PADX = 5 -class ScaleOption(enum.Enum): - NONE = 0 - UPPER_LEFT = 1 - CENTERED = 2 - SCALED = 3 - TILED = 4 - - class CanvasBackgroundDialog(Dialog): def __init__(self, master, app): """ @@ -33,8 +24,8 @@ class CanvasBackgroundDialog(Dialog): super().__init__(master, app, "Canvas Background", modal=True) self.canvas = self.app.canvas self.scale_option = tk.IntVar(value=self.canvas.scale_option.get()) - self.show_grid = tk.IntVar(value=self.canvas.show_grid.get()) - self.adjust_to_dim = tk.IntVar(value=self.canvas.adjust_to_dim.get()) + self.show_grid = tk.BooleanVar(value=self.canvas.show_grid.get()) + self.adjust_to_dim = tk.BooleanVar(value=self.canvas.adjust_to_dim.get()) self.filename = tk.StringVar(value=self.canvas.wallpaper_file) self.image_label = None self.options = [] @@ -124,9 +115,6 @@ class CanvasBackgroundDialog(Dialog): ) checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) - self.show_grid.set(1) - self.adjust_to_dim.set(0) - def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(row=6, column=0, pady=5, sticky="ew") @@ -171,202 +159,40 @@ class CanvasBackgroundDialog(Dialog): def click_adjust_canvas(self): # deselect all radio buttons and grey them out - if self.adjust_to_dim.get() == 1: + if self.adjust_to_dim.get(): self.scale_option.set(0) for option in self.options: option.config(state=tk.DISABLED) # turn back the radio button to active state so that user can choose again - elif self.adjust_to_dim.get() == 0: + else: self.scale_option.set(1) for option in self.options: option.config(state=tk.NORMAL) - else: - logging.error("canvasbackground.py adjust_canvas_size invalid value") - def delete_canvas_components(self, tag_list): - """ - delete canvas items whose tag is in the tag list - - :param list[string] tag_list: list of tags - :return: nothing - """ - for tag in tag_list: - for i in self.canvas.find_withtag(tag): - self.canvas.delete(i) - - def get_canvas_width_and_height(self): - """ - retrieve canvas width and height in pixels - - :return: nothing - """ - grid = self.canvas.find_withtag("rectangle")[0] - x0, y0, x1, y1 = self.canvas.coords(grid) - canvas_w = abs(x0 - x1) - canvas_h = abs(y0 - y1) - return canvas_w, canvas_h - - def determine_cropped_image_dimension(self): - """ - determine the dimension of the image after being cropped - - :return: nothing - """ - return - - def upper_left(self, img): - tk_img = ImageTk.PhotoImage(img) - - # crop image if it is bigger than canvas - canvas_w, canvas_h = self.get_canvas_width_and_height() - - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - cropped = img.crop((0, 0, cropx, cropy)) - cropped_tk = ImageTk.PhotoImage(cropped) - - # place left corner of image to the left corner of the canvas - self.canvas.wallpaper_drawn = cropped_tk - self.delete_canvas_components(["wallpaper"]) - wid = self.canvas.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - - def center(self, img): - """ - place the image at the center of canvas - - :param Image img: image object - :return: nothing - """ - tk_img = ImageTk.PhotoImage(img) - canvas_w, canvas_h = self.get_canvas_width_and_height() - - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - - # dimension of the cropped image - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - - x0 = (img_w - cropx) / 2 - y0 = (img_h - cropy) / 2 - x1 = x0 + cropx - y1 = y0 + cropy - cropped = img.crop((x0, y0, x1, y1)) - cropped_tk = ImageTk.PhotoImage(cropped) - - # place the center of the image at the center of the canvas - self.delete_canvas_components(["wallpaper"]) - # self.delete_previous_wallpaper() - wid = self.canvas.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = cropped_tk - - def scaled(self, img): - """ - scale image based on canvas dimension - - :param Image img: image object - :return: nothing - """ - canvas_w, canvas_h = self.get_canvas_width_and_height() - resized_image = img.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) - image_tk = ImageTk.PhotoImage(resized_image) - self.delete_canvas_components(["wallpaper"]) - wid = self.canvas.create_image( - (canvas_w / 2, canvas_h / 2), image=image_tk, tags="wallpaper" - ) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = image_tk - - def tiled(self, img): - return - - def draw_new_canvas(self, canvas_width, canvas_height): - """ - delete the old canvas and draw a new one - - :param int canvas_width: canvas width in pixel - :param int canvas_height: canvas height in pixel - :return: - """ - self.delete_canvas_components(["rectangle", "gridline"]) - self.canvas.draw_grid(canvas_width, canvas_height) - - def canvas_to_image_dimension(self, img): - image_tk = ImageTk.PhotoImage(img) - img_w = image_tk.width() - img_h = image_tk.height() - self.delete_canvas_components(["wallpaper"]) - self.draw_new_canvas(img_w, img_h) - wid = self.canvas.create_image((img_w / 2, img_h / 2), image=image_tk) - self.canvas.wallpaper_id = wid - self.canvas.wallpaper_drawn = image_tk - - def draw_grid(self): - self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - if self.show_grid.get() == 0: - for i in self.canvas.find_withtag("gridline"): - self.canvas.itemconfig(i, state=tk.HIDDEN) - elif self.show_grid.get() == 1: - for i in self.canvas.find_withtag("gridline"): - self.canvas.itemconfig(i, state=tk.NORMAL) - self.canvas.lift(i) - else: - logging.error("canvasbackground.py show_grid invalid value") - - def save_wallpaper_options(self): + def click_apply(self): self.canvas.scale_option.set(self.scale_option.get()) self.canvas.show_grid.set(self.show_grid.get()) self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) + self.canvas.update_grid() - def click_apply(self): filename = self.filename.get() if not filename: - self.delete_canvas_components(["wallpaper"]) - self.destroy() + self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper = None self.canvas.wallpaper_file = None - self.save_wallpaper_options() + self.destroy() return try: img = Image.open(filename) self.canvas.wallpaper = img self.canvas.wallpaper_file = filename + self.canvas.redraw() except FileNotFoundError: logging.error("invalid background: %s", filename) if self.canvas.wallpaper_id: self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper_id = None - self.destroy() - return + self.canvas.wallpaper_file = None - self.canvas.adjust_to_dim.set(self.adjust_to_dim.get()) - if self.adjust_to_dim.get() == 0: - self.canvas.scale_option.set(self.scale_option.get()) - option = ScaleOption(self.scale_option.get()) - if option == ScaleOption.UPPER_LEFT: - self.upper_left(img) - elif option == ScaleOption.CENTERED: - self.center(img) - elif option == ScaleOption.SCALED: - self.scaled(img) - elif option == ScaleOption.TILED: - logging.warning("tiled background not implemented yet") - elif self.adjust_to_dim.get() == 1: - self.canvas_to_image_dimension(img) - - self.draw_grid() self.destroy() diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 598d8d37..3a72389d 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -1,14 +1,11 @@ """ size and scale """ -import logging import tkinter as tk from tkinter import font, ttk -from coretk.dialogs.canvasbackground import ScaleOption from coretk.dialogs.dialog import Dialog -DRAW_OBJECT_TAGS = ["edge", "node", "nodename", "linkinfo", "antenna"] FRAME_PAD = 5 PADX = 5 @@ -172,47 +169,11 @@ class SizeAndScaleDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def redraw_grid(self): - """ - redraw grid with new dimension - - :return: nothing - """ - width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.config(scrollregion=(0, 0, width + 200, height + 200)) - - # delete old plot and redraw - for i in self.canvas.find_withtag("gridline"): - self.canvas.delete(i) - for i in self.canvas.find_withtag("rectangle"): - self.canvas.delete(i) - - self.canvas.draw_grid(width=width, height=height) - # lift anything that is drawn on the plot before - for tag in DRAW_OBJECT_TAGS: - for i in self.canvas.find_withtag(tag): - self.canvas.lift(i) - def click_apply(self): meter_per_pixel = float(self.scale.get()) / 100 + width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.meters_per_pixel = meter_per_pixel - self.redraw_grid() - # if there is a current wallpaper showing, redraw it based on current - # wallpaper options - wallpaper_tool = self.app.set_wallpaper - wallpaper = self.canvas.wallpaper - if wallpaper: - if self.canvas.adjust_to_dim.get() == 0: - scale_option = ScaleOption(self.canvas.scale_option.get()) - if scale_option == ScaleOption.UPPER_LEFT: - wallpaper_tool.upper_left(wallpaper) - elif scale_option == ScaleOption.CENTERED: - wallpaper_tool.center(wallpaper) - elif scale_option == ScaleOption.SCALED: - wallpaper_tool.scaled(wallpaper) - elif scale_option == ScaleOption.TILED: - logging.warning("tiled background not implemented") - elif self.canvas.adjust_to_dim.get() == 1: - wallpaper_tool.canvas_to_image_dimension(wallpaper) - wallpaper_tool.show_grid() + self.canvas.redraw_grid(width, height) + if self.canvas.wallpaper: + self.canvas.redraw() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1bec481b..e5bab65b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -2,6 +2,8 @@ import enum import logging import tkinter as tk +from PIL import ImageTk + from core.api.grpc import core_pb2 from coretk.canvasaction import CanvasAction from coretk.canvastooltip import CanvasTooltip @@ -21,6 +23,14 @@ class GraphMode(enum.Enum): OTHER = 4 +class ScaleOption(enum.Enum): + NONE = 0 + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + CORE_NODES = ["router"] CORE_WIRED_NETWORK_NODES = [] CORE_WIRELESS_NODE = ["wlan"] @@ -60,8 +70,8 @@ class CanvasGraph(tk.Canvas): self.wallpaper_drawn = None self.wallpaper_file = "" self.scale_option = tk.IntVar(value=1) - self.show_grid = tk.IntVar(value=1) - self.adjust_to_dim = tk.IntVar(value=0) + self.show_grid = tk.BooleanVar(value=True) + self.adjust_to_dim = tk.BooleanVar(value=False) def setup_menus(self): self.node_context = tk.Menu(self.master) @@ -132,11 +142,12 @@ class CanvasGraph(tk.Canvas): width=1, tags="rectangle", ) - self.tag_lower(self.grid) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") + self.tag_lower("gridline") + self.tag_lower(self.grid) def draw_existing_component(self, session): """ @@ -228,9 +239,8 @@ class CanvasGraph(tk.Canvas): if2 ) - # lift the nodes so they on top of the links - for i in self.find_withtag("node"): - self.lift(i) + # raise the nodes so they on top of the links + self.tag_raise("node") def canvas_xy(self, event): """ @@ -426,6 +436,132 @@ class CanvasGraph(tk.Canvas): self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node + def width_and_height(self): + """ + retrieve canvas width and height in pixels + + :return: nothing + """ + grid = self.find_withtag("rectangle")[0] + x0, y0, x1, y1 = self.coords(grid) + canvas_w = abs(x0 - x1) + canvas_h = abs(y0 - y1) + return canvas_w, canvas_h + + def wallpaper_upper_left(self): + tk_img = ImageTk.PhotoImage(self.wallpaper) + # crop image if it is bigger than canvas + canvas_w, canvas_h = self.width_and_height() + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + cropped_tk = ImageTk.PhotoImage(cropped) + self.delete(self.wallpaper_id) + # place left corner of image to the left corner of the canvas + self.wallpaper_id = self.create_image( + (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + ) + self.wallpaper_drawn = cropped_tk + + def wallpaper_center(self): + """ + place the image at the center of canvas + + :param Image img: image object + :return: nothing + """ + tk_img = ImageTk.PhotoImage(self.wallpaper) + canvas_w, canvas_h = self.width_and_height() + cropx = img_w = tk_img.width() + cropy = img_h = tk_img.height() + # dimension of the cropped image + if img_w > canvas_w: + cropx -= img_w - canvas_w + if img_h > canvas_h: + cropy -= img_h - canvas_h + x0 = (img_w - cropx) / 2 + y0 = (img_h - cropy) / 2 + x1 = x0 + cropx + y1 = y0 + cropy + cropped = self.wallpaper.crop((x0, y0, x1, y1)) + cropped_tk = ImageTk.PhotoImage(cropped) + # place the center of the image at the center of the canvas + self.delete(self.wallpaper_id) + self.wallpaper_id = self.create_image( + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + ) + self.wallpaper_drawn = cropped_tk + + def wallpaper_scaled(self): + """ + scale image based on canvas dimension + + :param Image img: image object + :return: nothing + """ + canvas_w, canvas_h = self.width_and_height() + image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) + self.delete(self.wallpaper_id) + self.wallpaper_id = self.create_image( + (canvas_w / 2, canvas_h / 2), image=image, tags="wallpaper" + ) + self.wallpaper_drawn = image + + def resize_to_wallpaper(self): + image_tk = ImageTk.PhotoImage(self.wallpaper) + img_w = image_tk.width() + img_h = image_tk.height() + self.delete(self.wallpaper_id) + self.delete("rectangle") + self.delete("gridline") + self.draw_grid(img_w, img_h) + self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) + self.wallpaper_drawn = image_tk + + def redraw_grid(self, width, height): + """ + redraw grid with new dimension + + :return: nothing + """ + self.config(scrollregion=(0, 0, width + 200, height + 200)) + + # delete previous grid + self.delete("rectangle") + self.delete("gridline") + + # redraw + self.draw_grid(width=width, height=height) + + # hide/show grid + self.update_grid() + + def redraw(self): + if self.adjust_to_dim.get(): + self.resize_to_wallpaper() + else: + option = ScaleOption(self.scale_option.get()) + if option == ScaleOption.UPPER_LEFT: + self.wallpaper_upper_left() + elif option == ScaleOption.CENTERED: + self.wallpaper_center() + elif option == ScaleOption.SCALED: + self.wallpaper_scaled() + elif option == ScaleOption.TILED: + logging.warning("tiled background not implemented yet") + + def update_grid(self): + logging.info("updating grid show: %s", self.show_grid.get()) + if self.show_grid.get(): + self.itemconfig("gridline", state=tk.NORMAL) + self.tag_raise("gridline") + else: + self.itemconfig("gridline", state=tk.HIDDEN) + class CanvasEdge: """ @@ -469,8 +605,6 @@ class CanvasEdge: self.link_info = None self.throughput = None self.wired = is_wired - # TODO resolve this - # self.canvas.tag_lower(self.id) def complete(self, dst, x, y): self.dst = dst @@ -478,8 +612,8 @@ class CanvasEdge: x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) self.canvas.helper.draw_wireless_case(self.src, self.dst, self) - self.canvas.lift(self.src) - self.canvas.lift(self.dst) + self.canvas.tag_raise(self.src) + self.canvas.tag_raise(self.dst) def delete(self): self.canvas.delete(self.id) From 940281a3eef564aad4896c6ad6454e1931b9c184 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:20:07 -0800 Subject: [PATCH 121/305] save work --- coretk/coretk/dialogs/serviceconfiguration.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index ceb3ece1..360e9561 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -40,6 +40,8 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None + self.service_files = {} + self.temp_service_files = {} self.load() self.draw() @@ -59,6 +61,13 @@ class ServiceConfiguration(Dialog): self.shutdown_commands = [x for x in service_config.shutdown] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer + self.service_files = { + x: self.app.core.get_node_service_file( + self.canvas_node.core_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = self.service_files def draw(self): # self.columnconfigure(1, weight=1) @@ -105,17 +114,17 @@ class ServiceConfiguration(Dialog): frame = ttk.Frame(tab1) label = ttk.Label(frame, text="File name: ") label.grid(row=0, column=0) - self.filename_combobox = ttk.Combobox(frame, values=self.filenames) + self.filename_combobox = ttk.Combobox( + frame, values=self.filenames, state="readonly" + ) self.filename_combobox.grid(row=0, column=1) - if len(self.filenames) > 0: - self.filename_combobox.current(0) self.filename_combobox.bind( "<>", self.display_service_file_data ) - button = ttk.Button(frame, image=self.documentnew_img) + button = ttk.Button(frame, image=self.documentnew_img, state="disabled") button.bind("", self.add_filename) button.grid(row=0, column=2) - button = ttk.Button(frame, image=self.editdelete_img) + button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button.bind("", self.delete_filename) button.grid(row=0, column=3) frame.grid(row=1, column=0, sticky="nsew") @@ -157,6 +166,10 @@ class ServiceConfiguration(Dialog): self.service_file_data = ScrolledText(tab1) self.service_file_data.grid(row=4, column=0, sticky="nsew") + if len(self.filenames) > 0: + self.filename_combobox.current(0) + self.service_file_data.delete(1.0, "end") + self.service_file_data.insert("end", self.service_files[self.filenames[0]]) # tab 2 label = ttk.Label( @@ -245,9 +258,13 @@ class ServiceConfiguration(Dialog): mode = "NON_BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: mode = "TIMER" + print("the mode is", mode) self.validation_mode_entry = ttk.Entry( - frame, state="disabled", textvariable=tk.StringVar(value=mode) + frame, textvariable=tk.StringVar(value=mode) ) + self.validation_mode_entry.insert("end", mode) + print("get mode") + print(self.validation_mode_entry.get()) self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = ttk.Label(frame, text="Validation period:") @@ -279,6 +296,8 @@ class ServiceConfiguration(Dialog): frame.grid(row=3, column=0) def add_filename(self, event): + # not worry about it for now + return frame_contains_button = event.widget.master combobox = frame_contains_button.grid_slaves(row=0, column=1)[0] filename = combobox.get() @@ -286,6 +305,8 @@ class ServiceConfiguration(Dialog): combobox["values"] += (filename,) def delete_filename(self, event): + # not worry about it for now + return frame_comntains_button = event.widget.master combobox = frame_comntains_button.grid_slaves(row=0, column=1)[0] filename = combobox.get() @@ -357,12 +378,8 @@ class ServiceConfiguration(Dialog): def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() - print(filename) - file_data = self.app.core.get_node_service_file( - self.canvas_node.core_id, self.service_name, filename - ) self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", file_data) + self.service_file_data.insert("end", self.service_files[filename]) def click_defaults(self): logging.info("not implemented") From fa76bbf01b55bc9a1e28c4e3fe098e288600cf9c Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 14 Nov 2019 21:39:44 -0800 Subject: [PATCH 122/305] removed unused code in graph and coreclient --- coretk/coretk/coreclient.py | 17 ----------------- coretk/coretk/graph.py | 14 -------------- 2 files changed, 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0d562f8a..2bf047d7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -372,23 +372,6 @@ class CoreClient: response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) - # # TODO no need, might get rid of this - # def add_link(self, id1, id2, type1, type2, edge): - # """ - # Grpc client request add link - # - # :param int session_id: session id - # :param int id1: node 1 core id - # :param core_pb2.NodeType type1: node 1 core node type - # :param int id2: node 2 core id - # :param core_pb2.NodeType type2: node 2 core node type - # :return: nothing - # """ - # if1 = self.create_interface(type1, edge.interface_1) - # if2 = self.create_interface(type2, edge.interface_2) - # response = self.client.add_link(self.session_id, id1, id2, if1, if2) - # logging.info("created link: %s", response) - def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e5bab65b..f7c45e63 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -31,12 +31,6 @@ class ScaleOption(enum.Enum): TILED = 4 -CORE_NODES = ["router"] -CORE_WIRED_NETWORK_NODES = [] -CORE_WIRELESS_NODE = ["wlan"] -CORE_EMANE = ["emane"] - - class CanvasGraph(tk.Canvas): def __init__(self, master, core, cnf=None, **kwargs): if cnf is None: @@ -670,14 +664,6 @@ class CanvasNode: self.canvas.core.launch_terminal(node_id) else: self.canvas.canvas_action.display_configuration(self) - # if self.node_type in CORE_NODES: - # self.canvas.canvas_action.node_to_show_config = self - # self.canvas.canvas_action.display_node_configuration() - # elif self.node_type in CORE_WIRED_NETWORK_NODES: - # return - # elif self.node_type in CORE_WIRELESS_NODE: - # return - # elif self def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) From 6d38058887b9f8d9af47850d6fb1f8c019766c64 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 10:22:30 -0800 Subject: [PATCH 123/305] update node creation and storage to leverage protobufs directly --- coretk/coretk/canvasaction.py | 2 - coretk/coretk/coreclient.py | 343 ++++++++-------------------- coretk/coretk/coretocanvas.py | 34 --- coretk/coretk/dialogs/nodeconfig.py | 2 +- coretk/coretk/graph.py | 154 ++++++------- coretk/coretk/graph_helper.py | 110 +-------- coretk/coretk/images.py | 22 +- coretk/coretk/linkinfo.py | 3 +- coretk/coretk/toolbar.py | 36 +-- coretk/coretk/wirelessconnection.py | 26 +-- 10 files changed, 216 insertions(+), 516 deletions(-) delete mode 100644 coretk/coretk/coretocanvas.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index decf43f3..8f217833 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -36,8 +36,6 @@ class CanvasAction: self.node_to_show_config = None def display_wlan_configuration(self, canvas_node): - - # print(self.canvas.grpc_manager.wlanconfig_management.configurations) wlan_config = self.master.core.wlanconfig_management.configurations[ canvas_node.core_id ] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2bf047d7..d80f6e5c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,7 +5,6 @@ import logging import os from core.api.grpc import client, core_pb2 -from coretk.coretocanvas import CoreToCanvasMapping from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images @@ -29,28 +28,6 @@ OBSERVERS = { } -class Node: - def __init__(self, session_id, node_id, node_type, model, x, y, name): - """ - Create an instance of a node - - :param int session_id: session id - :param int node_id: node id - :param core_pb2.NodeType node_type: node type - :param int x: x coordinate - :param int y: coordinate - :param str name: node name - """ - self.session_id = session_id - self.node_id = node_id - self.type = node_type - self.x = x - self.y = y - self.model = model - self.name = name - self.interfaces = [] - - class Edge: def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): """ @@ -112,15 +89,15 @@ class CoreClient: self.read_config() # data for managing the current session + self.canvas_nodes = {} + self.interface_to_edge = {} self.state = None - self.nodes = {} self.edges = {} self.hooks = {} self.id = 1 self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager() - self.core_mapping = CoreToCanvasMapping() self.wlanconfig_management = WlanNodeConfig() self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) @@ -179,7 +156,7 @@ class CoreClient: # clear session data self.reusable.clear() self.preexisting.clear() - self.nodes.clear() + self.canvas_nodes.clear() self.edges.clear() self.hooks.clear() self.wlanconfig_management.configurations.clear() @@ -203,14 +180,14 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) - logging.info("wlan config(%s): %s", node.id, response) + logging.debug("wlan config(%s): %s", node.id, response) node_config = response.config config = {x: node_config[x].value for x in node_config} self.wlanconfig_management.configurations[node.id] = config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - logging.info("mobility configs: %s", response) + logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config config = {x: node_config[x].value for x in node_config} @@ -218,7 +195,7 @@ class CoreClient: # get emane config response = self.client.get_emane_config(self.session_id) - logging.info("emane config: %s", response) + logging.debug("emane config: %s", response) self.emane_config = response.config # get emane model config @@ -339,7 +316,7 @@ class CoreClient: logging.info("delete links %s", response) def start_session(self): - nodes = self.get_nodes_proto() + nodes = [x.core_node for x in self.canvas_nodes.values()] links = self.get_links_proto() wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() @@ -411,7 +388,7 @@ class CoreClient: logging.debug("set node service %s", response) def create_nodes_and_links(self): - node_protos = self.get_nodes_proto() + node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = self.get_links_proto() self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: @@ -437,17 +414,6 @@ class CoreClient: logging.debug("Close grpc") self.client.close() - def peek_id(self): - """ - Peek the next id to be used - - :return: nothing - """ - if len(self.reusable) == 0: - return self.id - else: - return self.reusable[0] - def get_id(self): """ Get the next node id as well as update id status and reusable ids @@ -465,106 +431,87 @@ class CoreClient: def is_model_node(self, name): return name in DEFAULT_NODES or name in self.custom_nodes - def add_graph_node(self, session_id, canvas_id, x, y, name): + def create_node(self, x, y, node_type, model): """ Add node, with information filled in, to grpc manager - :param int session_id: session id - :param int canvas_id: node's canvas id :param int x: x coord :param int y: y coord - :param str name: node type + :param core_pb2.NodeType node_type: node type + :param str model: node model :return: nothing """ - node_type = None - node_model = None - if name in NETWORK_NODES: - if name == "switch": - node_type = core_pb2.NodeType.SWITCH - elif name == "hub": - node_type = core_pb2.NodeType.HUB - elif name == "wlan": - node_type = core_pb2.NodeType.WIRELESS_LAN - elif name == "rj45": - node_type = core_pb2.NodeType.RJ45 - elif name == "emane": - node_type = core_pb2.NodeType.EMANE - elif name == "tunnel": - node_type = core_pb2.NodeType.TUNNEL - elif name == "emane": - node_type = core_pb2.NodeType.EMANE - elif self.is_model_node(name): - node_type = core_pb2.NodeType.DEFAULT - node_model = name - else: - logging.error("invalid node name: %s", name) - nid = self.get_id() - create_node = Node(session_id, nid, node_type, node_model, x, y, name) + node_id = self.get_id() + position = core_pb2.Position(x=x, y=y) + node = core_pb2.Node( + id=node_id, + type=node_type, + name=f"n{node_id}", + model=model, + position=position, + ) # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, nid) - self.mobilityconfig_management.set_default_configuration(node_type, nid) + self.wlanconfig_management.set_default_config(node_type, node_id) + self.mobilityconfig_management.set_default_configuration(node_type, node_id) # set default emane configuration for emane node if node_type == core_pb2.NodeType.EMANE: - self.emaneconfig_management.set_default_config(nid) + self.emaneconfig_management.set_default_config(node_id) # set default service configurations if node_type == core_pb2.NodeType.DEFAULT: self.serviceconfig_manager.node_default_services_configuration( - node_id=nid, node_model=node_model + node_id=node_id, node_model=model ) - self.nodes[canvas_id] = create_node - self.core_mapping.map_core_id_to_canvas_id(nid, canvas_id) logging.debug( - "Adding node to core.. session id: %s, coords: (%s, %s), name: %s", - session_id, + "adding node to core session: %s, coords: (%s, %s), name: %s", + self.session_id, x, y, - name, + node.name, ) + return node - def delete_wanted_graph_nodes(self, canvas_ids, tokens): + def delete_wanted_graph_nodes(self, node_ids, edge_tokens): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces - :param list(int) canvas_ids: list of canvas node ids + :param list[int] node_ids: list of nodes to delete + :param list edge_tokens: list of edges to delete :return: nothing """ - # keep reference to the core ids - core_node_ids = [self.nodes[x].node_id for x in canvas_ids] - node_interface_pairs = [] - # delete the nodes - for i in canvas_ids: + for node_id in node_ids: try: - n = self.nodes.pop(i) - self.reusable.append(n.node_id) + del self.canvas_nodes[node_id] + self.reusable.append(node_id) except KeyError: - logging.error("coreclient.py INVALID NODE CANVAS ID") - + logging.error("invalid canvas id: %s", node_id) self.reusable.sort() # delete the edges and interfaces - for i in tokens: + node_interface_pairs = [] + for i in edge_tokens: try: e = self.edges.pop(i) if e.interface_1 is not None: node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) if e.interface_2 is not None: node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) - except KeyError: logging.error("coreclient.py invalid edge token ") # delete global emane config if there no longer exist any emane cloud - if core_pb2.NodeType.EMANE not in [x.type for x in self.nodes.values()]: + # TODO: should not need to worry about this + node_types = [x.core_node.type for x in self.canvas_nodes.values()] + if core_pb2.NodeType.EMANE not in node_types: self.emane_config = None # delete any mobility configuration, wlan configuration - for i in core_node_ids: + for i in node_ids: if i in self.mobilityconfig_management.configurations: self.mobilityconfig_management.configurations.pop(i) if i in self.wlanconfig_management.configurations: @@ -574,69 +521,16 @@ class CoreClient: for i in node_interface_pairs: if i in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(i) - for i in core_node_ids: + for i in node_ids: if tuple([i, None]) in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(tuple([i, None])) - def add_preexisting_node(self, canvas_node, session_id, core_node, name): - """ - Add preexisting nodes to grpc manager - - :param str name: node_type - :param core_pb2.Node core_node: core node grpc message - :param coretk.graph.CanvasNode canvas_node: canvas node - :param int session_id: session id - :return: nothing - """ - - # update the next available id - core_id = core_node.id - if self.id is None or core_id >= self.id: - self.id = core_id + 1 - self.preexisting.add(core_id) - n = Node( - session_id, - core_id, - core_node.type, - core_node.model, - canvas_node.x_coord, - canvas_node.y_coord, - name, - ) - self.nodes[canvas_node.id] = n - - def update_node_location(self, canvas_id, new_x, new_y): - """ - update node - - :param int canvas_id: canvas id of that node - :param int new_x: new x coord - :param int new_y: new y coord - :return: nothing - """ - self.nodes[canvas_id].x = new_x - self.nodes[canvas_id].y = new_y - - def update_reusable_id(self): - """ - Update available id for reuse - - :return: nothing - """ - if len(self.preexisting) > 0: - for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) - - self.preexisting.clear() - logging.debug("Next id: %s, Reusable: %s", self.id, self.reusable) - def create_interface(self, node_type, gui_interface): """ create a protobuf interface given the interface object stored by the programmer - :param core_bp2.NodeType type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface object + :param core_bp2.NodeType node_type: node type + :param coretk.interface.Interface gui_interface: the programmer's interface :rtype: core_bp2.Interface :return: protobuf interface object """ @@ -653,120 +547,71 @@ class CoreClient: logging.debug("create interface: %s", interface) return interface - def create_edge_interface(self, edge, src_canvas_id, dst_canvas_id): - """ - Create the interface for the two end of an edge, add a copy to node's interfaces - - :param coretk.coreclient.Edge edge: edge to add interfaces to - :param int src_canvas_id: canvas id for the source node - :param int dst_canvas_id: canvas id for the destination node - :return: nothing - """ - src_interface = None - dst_interface = None - print("create interface") - self.interfaces_manager.new_subnet() - - src_node = self.nodes[src_canvas_id] - if self.is_model_node(src_node.model): - ifid = len(src_node.interfaces) - name = "eth" + str(ifid) - src_interface = Interface( + def create_edge_interface(self, canvas_node): + interface = None + core_node = canvas_node.core_node + if self.is_model_node(core_node.model): + ifid = len(canvas_node.interfaces) + name = f"eth{ifid}" + interface = Interface( name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) ) - self.nodes[src_canvas_id].interfaces.append(src_interface) + canvas_node.interfaces.append(interface) logging.debug( - "Create source interface 1... IP: %s, name: %s", - src_interface.ipv4, - src_interface.name, + "create node(%s) interface IP: %s, name: %s", + core_node.name, + interface.ipv4, + interface.name, ) + return interface - dst_node = self.nodes[dst_canvas_id] - if self.is_model_node(dst_node.model): - ifid = len(dst_node.interfaces) - name = "eth" + str(ifid) - dst_interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) - ) - self.nodes[dst_canvas_id].interfaces.append(dst_interface) - logging.debug( - "Create destination interface... IP: %s, name: %s", - dst_interface.ipv4, - dst_interface.name, - ) - - edge.interface_1 = src_interface - edge.interface_2 = dst_interface - return src_interface, dst_interface - - def add_edge(self, session_id, token, canvas_id_1, canvas_id_2): + def create_edge(self, token, canvas_node_one, canvas_node_two): """ Add an edge to grpc manager - :param int session_id: core session id :param tuple(int, int) token: edge's identification in the canvas - :param int canvas_id_1: canvas id of source node - :param int canvas_id_2: canvas_id of destination node + :param canvas_node_one: canvas node one + :param canvas_node_two: canvas node two :return: nothing """ - node_one = self.nodes[canvas_id_1] - node_two = self.nodes[canvas_id_2] - if canvas_id_1 in self.nodes and canvas_id_2 in self.nodes: - edge = Edge( - session_id, - node_one.node_id, - node_one.type, - node_two.node_id, - node_two.type, - ) - self.edges[token] = edge - src_interface, dst_interface = self.create_edge_interface( - edge, canvas_id_1, canvas_id_2 - ) - node_one_id = node_one.node_id - node_two_id = node_two.node_id + node_one = canvas_node_one.core_node + node_two = canvas_node_two.core_node - # provide a way to get an edge from a core node and an interface id - if src_interface is not None: - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_one_id, src_interface.id, token + # create interfaces + self.interfaces_manager.new_subnet() + interface_one = self.create_edge_interface(canvas_node_one) + if interface_one is not None: + self.interface_to_edge[(node_one.id, interface_one.id)] = token + interface_two = self.create_edge_interface(canvas_node_two) + if interface_two is not None: + self.interface_to_edge[(node_two.id, interface_two.id)] = token + + # emane setup + if ( + node_one.type == core_pb2.NodeType.EMANE + and node_two.type == core_pb2.NodeType.DEFAULT + ): + if node_two.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_one.node_id, node_two.node_id, interface_two.id + ) + elif ( + node_two.type == core_pb2.NodeType.EMANE + and node_one.type == core_pb2.NodeType.DEFAULT + ): + if node_one.model == "mdr": + self.emaneconfig_management.set_default_for_mdr( + node_two.node_id, node_one.node_id, interface_one.id ) - if dst_interface is not None: - self.core_mapping.map_node_and_interface_to_canvas_edge( - node_two_id, dst_interface.id, token - ) - - if ( - node_one.type == core_pb2.NodeType.EMANE - and node_two.type == core_pb2.NodeType.DEFAULT - ): - if node_two.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_one.node_id, node_two.node_id, dst_interface.id - ) - elif ( - node_two.type == core_pb2.NodeType.EMANE - and node_one.type == core_pb2.NodeType.DEFAULT - ): - if node_one.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_two.node_id, node_one.node_id, src_interface.id - ) - - else: - logging.error("grpcmanagement.py INVALID CANVAS NODE ID") - - def get_nodes_proto(self): - nodes = [] - for node in self.nodes.values(): - pos = core_pb2.Position(x=int(node.x), y=int(node.y)) - proto_node = core_pb2.Node( - id=node.node_id, type=node.type, position=pos, model=node.model - ) - nodes.append(proto_node) - return nodes + edge = Edge( + self.session_id, node_one.id, node_one.type, node_two.id, node_two.type + ) + edge.interface_1 = interface_one + edge.interface_2 = interface_two + self.edges[token] = edge + return edge def get_links_proto(self): links = [] diff --git a/coretk/coretk/coretocanvas.py b/coretk/coretk/coretocanvas.py deleted file mode 100644 index 6e767db3..00000000 --- a/coretk/coretk/coretocanvas.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -provide mapping from core to canvas -""" -import logging - - -class CoreToCanvasMapping: - def __init__(self): - self.core_id_to_canvas_id = {} - self.core_node_and_interface_to_canvas_edge = {} - - def map_node_and_interface_to_canvas_edge(self, nid, iid, edge_token): - self.core_node_and_interface_to_canvas_edge[tuple([nid, iid])] = edge_token - - def get_token_from_node_and_interface(self, nid, iid): - key = tuple([nid, iid]) - if key in self.core_node_and_interface_to_canvas_edge: - return self.core_node_and_interface_to_canvas_edge[key] - else: - logging.error("invalid key") - return None - - def map_core_id_to_canvas_id(self, core_nid, canvas_nid): - if core_nid not in self.core_id_to_canvas_id: - self.core_id_to_canvas_id[core_nid] = canvas_nid - else: - logging.debug("key already existed") - - def get_canvas_id_from_core_id(self, core_id): - if core_id in self.core_id_to_canvas_id: - return self.core_id_to_canvas_id[core_id] - else: - logging.debug("invalid key") - return None diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 48233762..14e9de03 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -21,7 +21,7 @@ class NodeConfigDialog(Dialog): self.image = canvas_node.image self.image_button = None self.name = tk.StringVar(value=canvas_node.name) - self.type = tk.StringVar(value=canvas_node.node_type) + self.type = tk.StringVar(value=canvas_node.core_node.model) self.server = tk.StringVar() self.draw() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index f7c45e63..1fcafac1 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -39,7 +39,8 @@ class CanvasGraph(tk.Canvas): super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT self.draw_node_image = None - self.draw_node_name = None + self.draw_node_type = None + self.draw_node_model = None self.selected = None self.node_context = None self.nodes = {} @@ -97,13 +98,14 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT self.draw_node_image = None - self.draw_node_name = None + self.draw_node_type = None + self.draw_node_model = None self.selected = None self.node_context = None self.nodes.clear() self.edges.clear() self.drawing_edge = None - self.draw_existing_component(session) + self.draw_session(session) def setup_bindings(self): """ @@ -143,61 +145,46 @@ class CanvasGraph(tk.Canvas): self.tag_lower("gridline") self.tag_lower(self.grid) - def draw_existing_component(self, session): + def draw_session(self, session): """ - Draw existing node and update the information in grpc manager to match + Draw existing session. :return: nothing """ - core_id_to_canvas_id = {} - # redraw existing nodes - for node in session.nodes: + # draw existing nodes + for core_node in session.nodes: # peer to peer node is not drawn on the GUI - if node.type != core_pb2.NodeType.PEER_TO_PEER: - # draw nodes on the canvas - image, name = Images.node_icon(node.type, node.model) - n = CanvasNode( - node.position.x, node.position.y, image, name, self.master, node.id - ) - self.nodes[n.id] = n - core_id_to_canvas_id[node.id] = n.id + if core_node.type == core_pb2.NodeType.PEER_TO_PEER: + continue - # store the node in grpc manager - self.core.add_preexisting_node(n, session.id, node, name) + # draw nodes on the canvas + image = Images.node_icon(core_node.type, core_node.model) + position = core_node.position + node = CanvasNode(position.x, position.y, image, self.master, core_node) + self.nodes[node.id] = node + self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: - n1 = self.nodes[core_id_to_canvas_id[link.node_one_id]] - n2 = self.nodes[core_id_to_canvas_id[link.node_two_id]] - if link.type == core_pb2.LinkType.WIRED: - e = CanvasEdge( - n1.x_coord, - n1.y_coord, - n2.x_coord, - n2.y_coord, - n1.id, - self, - is_wired=True, - ) - elif link.type == core_pb2.LinkType.WIRELESS: - e = CanvasEdge( - n1.x_coord, - n1.y_coord, - n2.x_coord, - n2.y_coord, - n1.id, - self, - is_wired=False, - ) - edge_token = tuple(sorted((n1.id, n2.id))) - e.token = edge_token - e.dst = n2.id - n1.edges.add(e) - n2.edges.add(e) - self.edges[e.token] = e - self.core.add_edge(session.id, e.token, n1.id, n2.id) - - self.helper.redraw_antenna(link, n1, n2) + canvas_node_one = self.core.canvas_nodes[link.node_one_id] + canvas_node_two = self.core.canvas_nodes[link.node_two_id] + is_wired = link.type == core_pb2.LinkType.WIRED + edge = CanvasEdge( + canvas_node_one.x_coord, + canvas_node_one.y_coord, + canvas_node_two.x_coord, + canvas_node_two.y_coord, + canvas_node_one.id, + self, + is_wired=is_wired, + ) + edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + edge.dst = canvas_node_two.id + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.create_edge(edge.token, canvas_node_one, canvas_node_two) + self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) # TODO add back the link info to grpc manager also redraw grpc_if1 = link.interface_one @@ -212,9 +199,9 @@ class CanvasGraph(tk.Canvas): if grpc_if2 is not None: ip4_dst = grpc_if2.ip4 ip6_dst = grpc_if2.ip6 - e.link_info = LinkInfo( + edge.link_info = LinkInfo( canvas=self, - edge=e, + edge=edge, ip4_src=ip4_src, ip6_src=ip6_src, ip4_dst=ip4_dst, @@ -224,14 +211,10 @@ class CanvasGraph(tk.Canvas): # TODO will include throughput and ipv6 in the future if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core.edges[e.token].interface_1 = if1 - self.core.edges[e.token].interface_2 = if2 - self.core.nodes[core_id_to_canvas_id[link.node_one_id]].interfaces.append( - if1 - ) - self.core.nodes[core_id_to_canvas_id[link.node_two_id]].interfaces.append( - if2 - ) + self.core.edges[edge.token].interface_1 = if1 + self.core.edges[edge.token].interface_2 = if2 + canvas_node_one.interfaces.append(if1) + canvas_node_two.interfaces.append(if2) # raise the nodes so they on top of the links self.tag_raise("node") @@ -290,7 +273,13 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node(x, y, self.draw_node_image, self.draw_node_name) + self.add_node( + x, + y, + self.draw_node_image, + self.draw_node_type, + self.draw_node_model, + ) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -313,6 +302,7 @@ class CanvasGraph(tk.Canvas): # edge dst is same as src, delete edge if edge.src == self.selected: edge.delete() + return # set dst node and snap edge to center x, y = self.coords(self.selected) @@ -326,14 +316,11 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - - self.core.add_edge( - self.core.session_id, edge.token, node_src.id, node_dst.id - ) + core_edge = self.core.create_edge(edge.token, node_src, node_dst) # draw link info on the edge - if1 = self.core.edges[edge.token].interface_1 - if2 = self.core.edges[edge.token].interface_2 + if1 = core_edge.interface_1 + if2 = core_edge.interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None if if1 is not None: @@ -404,8 +391,10 @@ class CanvasGraph(tk.Canvas): ) # delete nodes and link info stored in CanvasGraph object + node_ids = [] for nid in to_delete_nodes: - self.nodes.pop(nid) + canvas_node = self.nodes.pop(nid) + node_ids.append(canvas_node.core_node.id) for token in to_delete_edge_tokens: self.edges.pop(token) @@ -419,15 +408,16 @@ class CanvasGraph(tk.Canvas): self.nodes[nid].edges.remove(edge) # delete the related data from core - self.core.delete_wanted_graph_nodes(to_delete_nodes, to_delete_edge_tokens) + self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) - def add_node(self, x, y, image, node_name): + def add_node(self, x, y, image, node_type, model): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - node = CanvasNode(x, y, image, node_name, self.master, self.core.peek_id()) + core_node = self.core.create_node(int(x), int(y), node_type, model) + node = CanvasNode(x, y, image, self.master, core_node) + self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node - self.core.add_graph_node(self.core.session_id, node.id, x, y, node_name) return node def width_and_height(self): @@ -494,7 +484,6 @@ class CanvasGraph(tk.Canvas): """ scale image based on canvas dimension - :param Image img: image object :return: nothing """ canvas_w, canvas_h = self.width_and_height() @@ -614,18 +603,17 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, node_type, app, core_id): + def __init__(self, x, y, image, app, core_node): self.image = image - self.node_type = node_type self.app = app self.canvas = app.canvas self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.core_id = core_id + self.core_node = core_node + self.name = core_node.name self.x_coord = x self.y_coord = y - self.name = f"N{self.core_id}" self.text_id = self.canvas.create_text( x, y + 20, text=self.name, tags="nodename" ) @@ -641,6 +629,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() + self.interfaces = [] self.wlans = [] self.moving = None @@ -648,7 +637,7 @@ class CanvasNode: if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) - output = self.app.core.run(self.core_id) + output = self.app.core.run(self.core_node.id) self.tooltip.text.set(output) def on_leave(self, event): @@ -658,15 +647,15 @@ class CanvasNode: print("click") def double_click(self, event): - node_id = self.canvas.core.nodes[self.id].node_id - state = self.canvas.core.get_session_state() - if state == core_pb2.SessionState.RUNTIME: - self.canvas.core.launch_terminal(node_id) + if self.app.core.is_runtime(): + self.canvas.core.launch_terminal(self.core_node.id) else: self.canvas.canvas_action.display_configuration(self) def update_coords(self): self.x_coord, self.y_coord = self.canvas.coords(self.id) + self.core_node.position.x = int(self.x_coord) + self.core_node.position.y = int(self.y_coord) def click_press(self, event): logging.debug(f"node click press {self.name}: {event}") @@ -677,7 +666,6 @@ class CanvasNode: def click_release(self, event): logging.debug(f"node click release {self.name}: {event}") self.update_coords() - self.canvas.core.update_node_location(self.id, self.x_coord, self.y_coord) self.moving = None def motion(self, event): @@ -697,7 +685,7 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: - self.canvas.core.edit_node(self.core_id, int(new_x), int(new_y)) + self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index c1f59815..476c0182 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -29,29 +29,31 @@ class GraphHelper: self.canvas.delete(i) def draw_wireless_case(self, src_id, dst_id, edge): - src_node_name = self.canvas.nodes[src_id].node_type - dst_node_name = self.canvas.nodes[dst_id].node_type - - if src_node_name == "wlan" or dst_node_name == "wlan": + src_node_type = self.canvas.nodes[src_id].core_node.type + dst_node_type = self.canvas.nodes[dst_id].core_node.type + is_src_wlan = src_node_type == core_pb2.NodeType.WIRELESS_LAN + is_dst_wlan = dst_node_type == core_pb2.NodeType.WIRELESS_LAN + if is_src_wlan or is_dst_wlan: self.canvas.itemconfig(edge.id, state=tk.HIDDEN) edge.wired = False if edge.token not in self.canvas.edges: - if src_node_name == "wlan" and dst_node_name == "wlan": + if is_src_wlan and is_dst_wlan: self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif src_node_name == "wlan": + elif is_src_wlan: self.canvas.nodes[dst_id].antenna_draw.add_antenna() else: self.canvas.nodes[src_id].antenna_draw.add_antenna() - edge.wired = True def redraw_antenna(self, link, node_one, node_two): + is_node_one_wlan = node_one.core_node.type == core_pb2.NodeType.WIRELESS_LAN + is_node_two_wlan = node_two.core_node.type == core_pb2.NodeType.WIRELESS_LAN if link.type == core_pb2.LinkType.WIRELESS: - if node_one.node_type == "wlan" and node_two.node_type == "wlan": + if is_node_one_wlan and is_node_two_wlan: node_one.antenna_draw.add_antenna() - elif node_one.node_type == "wlan" and node_two.node_type != "wlan": + elif is_node_one_wlan and not is_node_two_wlan: node_two.antenna_draw.add_antenna() - elif node_one.node_type != "wlan" and node_two.node_type == "wlan": + elif not is_node_one_wlan and is_node_two_wlan: node_one.antenna_draw.add_antenna() else: logging.error( @@ -122,91 +124,3 @@ class WlanAntennaManager: """ for i in self.antennas: self.canvas.delete(i) - - -# class WlanConnection: -# def __init__(self, canvas, grpc): -# """ -# create in -# :param canvas: -# """ -# self.canvas = canvas -# self.core_grpc = grpc -# self.throughput_on = False -# self.map_node_link = {} -# self.links = [] -# -# def wireless_nodes(self): -# """ -# retrieve all the wireless clouds in the canvas -# -# :return: list(coretk.graph.CanvasNode) -# """ -# wireless_nodes = [] -# for n in self.canvas.nodes.values(): -# if n.node_type == "wlan": -# wireless_nodes.append(n) -# return wireless_nodes -# -# def draw_wireless_link(self, src, dst): -# """ -# draw a line between 2 nodes that are connected to the same wireless cloud -# -# :param coretk.graph.CanvasNode src: source node -# :param coretk.graph.CanvasNode dst: destination node -# :return: nothing -# """ -# cid = self.canvas.create_line(src.x_coord, src.y_coord, dst.x_coord, dst.y_coord, tags="wlanconnection") -# if src.id not in self.map_node_link: -# self.map_node_link[src.id] = [] -# if dst.id not in self.map_node_link: -# self.map_node_link[dst.id] = [] -# self.map_node_link[src.id].append(cid) -# self.map_node_link[dst.id].append(cid) -# self.links.append(cid) -# -# def subnet_wireless_connection(self, wlan_node): -# """ -# retrieve all the non-wireless nodes connected to wireless_node and create line (represent wireless connection) between each pair of nodes -# :param coretk.grpah.CanvasNode wlan_node: wireless node -# -# :return: nothing -# """ -# non_wlan_nodes = [] -# for e in wlan_node.edges: -# src = self.canvas.nodes[e.src] -# dst = self.canvas.nodes[e.dst] -# if src.node_type == "wlan" and dst.node_type != "wlan": -# non_wlan_nodes.append(dst) -# elif src.node_type != "wlan" and dst.node_type == "wlan": -# non_wlan_nodes.append(src) -# -# size = len(non_wlan_nodes) -# for i in range(size): -# for j in range(i+1, size): -# self.draw_wireless_link(non_wlan_nodes[i], non_wlan_nodes[j]) -# -# def session_wireless_connection(self): -# """ -# draw all the wireless connection in the canvas -# -# :return: nothing -# """ -# wlan_nodes = self.wireless_nodes() -# for n in wlan_nodes: -# self.subnet_wireless_connection(n) -# -# def show_links(self): -# """ -# show all the links -# """ -# for l in self.links: -# self.canvas.itemconfig(l, state=tk.NORMAL) -# -# def hide_links(self): -# """ -# hide all the links -# :return: -# """ -# for l in self.links: -# self.canvas.itemconfig(l, state=tk.HIDDEN) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 7335e14e..6ce4732f 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -31,7 +31,7 @@ class Images: return cls.create(file_path, width, height) @classmethod - def get_custom(cls, name, width, height): + def get_custom(cls, name, width, height=None): file_path = cls.images[name] return cls.create(file_path, width, height) @@ -41,52 +41,38 @@ class Images: Retrieve image based on type and model :param core_pb2.NodeType node_type: core node type :param string node_model: the node model - - :rtype: tuple(PhotoImage, str) - :return: the matching image and its name + :return: core node icon + :rtype: PhotoImage """ image_enum = ImageEnum.ROUTER - name = "unknown" if node_type == core_pb2.NodeType.SWITCH: image_enum = ImageEnum.SWITCH - name = "switch" elif node_type == core_pb2.NodeType.HUB: image_enum = ImageEnum.HUB - name = "hub" elif node_type == core_pb2.NodeType.WIRELESS_LAN: image_enum = ImageEnum.WLAN - name = "wlan" elif node_type == core_pb2.NodeType.EMANE: image_enum = ImageEnum.EMANE - name = "emane" elif node_type == core_pb2.NodeType.RJ45: image_enum = ImageEnum.RJ45 - name = "rj45" elif node_type == core_pb2.NodeType.TUNNEL: image_enum = ImageEnum.TUNNEL - name = "tunnel" elif node_type == core_pb2.NodeType.DEFAULT: if node_model == "router": image_enum = ImageEnum.ROUTER - name = "router" elif node_model == "host": image_enum = ImageEnum.HOST - name = "host" elif node_model == "PC": image_enum = ImageEnum.PC - name = "PC" elif node_model == "mdr": image_enum = ImageEnum.MDR - name = "mdr" elif node_model == "prouter": image_enum = ImageEnum.PROUTER - name = "prouter" else: logging.error("invalid node model: %s", node_model) else: logging.error("invalid node type: %s", node_type) - - return Images.get(image_enum, NODE_WIDTH), name + return Images.get(image_enum, NODE_WIDTH) class ImageEnum(Enum): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 916e8dfb..29430deb 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -129,8 +129,7 @@ class Throughput: nid = t.node_id iid = t.interface_id tp = t.throughput - # token = self.grpc_manager.node_id_and_interface_to_edge_token[nid, iid] - token = self.core.core_mapping.get_token_from_node_and_interface(nid, iid) + token = self.core.interface_to_edge[(nid, iid)] print(token) edge_id = self.canvas.edges[token].id diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 89393c3e..a049c321 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,6 +3,7 @@ import tkinter as tk from functools import partial from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images @@ -136,10 +137,16 @@ class Toolbar(ttk.Frame): (ImageEnum.PROUTER, "prouter"), ] # draw default nodes - for image_enum, tooltip in nodes: + for image_enum, model in nodes: image = icon(image_enum) - func = partial(self.update_button, self.node_button, image, tooltip) - self.create_picker_button(image, func, self.node_picker, tooltip) + func = partial( + self.update_button, + self.node_button, + image, + core_pb2.NodeType.DEFAULT, + model, + ) + self.create_picker_button(image, func, self.node_picker, model) # draw custom nodes for name in sorted(self.app.core.custom_nodes): custom_node = self.app.core.custom_nodes[name] @@ -216,14 +223,15 @@ class Toolbar(ttk.Frame): dialog = CustomNodesDialog(self.app, self.app) dialog.show() - def update_button(self, button, image, name): - logging.info("update button(%s): %s", button, name) + def update_button(self, button, image, node_type, model=None): + logging.info("update button(%s): %s", button, node_type) self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE self.app.canvas.draw_node_image = image - self.app.canvas.draw_node_name = name + self.app.canvas.draw_node_type = node_type + self.app.canvas.draw_node_model = model def hide_pickers(self): logging.info("hiding pickers") @@ -260,18 +268,18 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) nodes = [ - (ImageEnum.HUB, "hub", "ethernet hub"), - (ImageEnum.SWITCH, "switch", "ethernet switch"), - (ImageEnum.WLAN, "wlan", "wireless LAN"), - (ImageEnum.EMANE, "emane", "EMANE"), - (ImageEnum.RJ45, "rj45", "rj45 physical interface tool"), - (ImageEnum.TUNNEL, "tunnel", "tunnel tool"), + (ImageEnum.HUB, core_pb2.NodeType.HUB, "ethernet hub"), + (ImageEnum.SWITCH, core_pb2.NodeType.SWITCH, "ethernet switch"), + (ImageEnum.WLAN, core_pb2.NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.EMANE, core_pb2.NodeType.EMANE, "EMANE"), + (ImageEnum.RJ45, core_pb2.NodeType.RJ45, "rj45 physical interface tool"), + (ImageEnum.TUNNEL, core_pb2.NodeType.TUNNEL, "tunnel tool"), ] - for image_enum, name, tooltip in nodes: + for image_enum, node_type, tooltip in nodes: image = icon(image_enum) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, name), + partial(self.update_button, self.network_button, image, node_type), self.network_picker, tooltip, ) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index d1e17bc5..b864ce38 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -7,35 +7,31 @@ from core.api.grpc import core_pb2 class WirelessConnection: def __init__(self, canvas, core): self.canvas = canvas - self.core_mapping = core.core_mapping + self.core = core # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} def add_wlan_connection(self, node_one_id, node_two_id): - canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) - canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) + canvas_node_one = self.core.canvas_nodes[node_one_id] + canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) - if key not in self.map: - x1, y1 = self.canvas.coords(canvas_id_one) - x2, y2 = self.canvas.coords(canvas_id_two) + x1, y1 = self.canvas.coords(canvas_node_one.id) + x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 ) self.map[key] = wlan_canvas_id - self.canvas.nodes[canvas_id_one].wlans.append(wlan_canvas_id) - self.canvas.nodes[canvas_id_two].wlans.append(wlan_canvas_id) + canvas_node_one.wlans.append(wlan_canvas_id) + canvas_node_two.wlans.append(wlan_canvas_id) def delete_wlan_connection(self, node_one_id, node_two_id): - canvas_id_one = self.core_mapping.get_canvas_id_from_core_id(node_one_id) - canvas_id_two = self.core_mapping.get_canvas_id_from_core_id(node_two_id) - + canvas_node_one = self.core.canvas_nodes[node_one_id] + canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) wlan_canvas_id = self.map[key] - - self.canvas.nodes[canvas_id_one].wlans.remove(wlan_canvas_id) - self.canvas.nodes[canvas_id_two].wlans.remove(wlan_canvas_id) - + canvas_node_one.wlans.remove(wlan_canvas_id) + canvas_node_two.wlans.remove(wlan_canvas_id) self.canvas.delete(wlan_canvas_id) self.map.pop(key, None) From 814d35d7dda1e2a774ab45663d4a503dec93d148 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 12:39:08 -0800 Subject: [PATCH 124/305] removed coreclient edge and updated code to save and use link protobuf data structures --- coretk/coretk/canvasaction.py | 15 +--- coretk/coretk/coreclient.py | 119 +++++++++------------------- coretk/coretk/graph.py | 46 +++++------ coretk/coretk/interface.py | 50 ------------ coretk/coretk/linkinfo.py | 2 - coretk/coretk/toolbar.py | 1 + coretk/coretk/wirelessconnection.py | 2 +- 7 files changed, 63 insertions(+), 172 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 8f217833..5b9c08c3 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -6,13 +6,6 @@ from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog -# TODO, finish classifying node types -NODE_TO_TYPE = { - "router": core_pb2.NodeType.DEFAULT, - "wlan": core_pb2.NodeType.WIRELESS_LAN, - "emane": core_pb2.NodeType.EMANE, -} - class CanvasAction: def __init__(self, master, canvas): @@ -21,13 +14,13 @@ class CanvasAction: self.node_to_show_config = None def display_configuration(self, canvas_node): - pb_type = NODE_TO_TYPE[canvas_node.node_type] + node_type = canvas_node.core_node.type self.node_to_show_config = canvas_node - if pb_type == core_pb2.NodeType.DEFAULT: + if node_type == core_pb2.NodeType.DEFAULT: self.display_node_configuration() - elif pb_type == core_pb2.NodeType.WIRELESS_LAN: + elif node_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) - elif pb_type == core_pb2.NodeType.EMANE: + elif node_type == core_pb2.NodeType.EMANE: self.display_emane_configuration() def display_node_configuration(self): diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d80f6e5c..5dd0a621 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,7 @@ from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images -from coretk.interface import Interface, InterfaceManager +from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -28,25 +28,6 @@ OBSERVERS = { } -class Edge: - def __init__(self, session_id, node_id_1, node_type_1, node_id_2, node_type_2): - """ - Create an instance of an edge - :param int session_id: session id - :param int node_id_1: node 1 id - :param int node_type_1: node 1 type - :param core_pb2.NodeType node_id_2: node 2 id - :param core_pb2.NodeType node_type_2: node 2 type - """ - self.session_id = session_id - self.id1 = node_id_1 - self.id2 = node_id_2 - self.type1 = node_type_1 - self.type2 = node_type_2 - self.interface_1 = None - self.interface_2 = None - - class CoreServer: def __init__(self, name, address, port): self.name = name @@ -92,7 +73,7 @@ class CoreClient: self.canvas_nodes = {} self.interface_to_edge = {} self.state = None - self.edges = {} + self.links = {} self.hooks = {} self.id = 1 self.reusable = [] @@ -157,7 +138,7 @@ class CoreClient: self.reusable.clear() self.preexisting.clear() self.canvas_nodes.clear() - self.edges.clear() + self.links.clear() self.hooks.clear() self.wlanconfig_management.configurations.clear() self.mobilityconfig_management.configurations.clear() @@ -212,7 +193,7 @@ class CoreClient: self.reusable.append(i) # draw session - self.app.canvas.canvas_reset_and_redraw(session) + self.app.canvas.reset_and_redraw(session) # draw tool bar appropritate with session state if self.is_runtime(): @@ -317,7 +298,7 @@ class CoreClient: def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] - links = self.get_links_proto() + links = list(self.links.values()) wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() @@ -389,7 +370,7 @@ class CoreClient: def create_nodes_and_links(self): node_protos = [x.core_node for x in self.canvas_nodes.values()] - link_protos = self.get_links_proto() + link_protos = list(self.links.values()) self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: response = self.client.add_node(self.session_id, node_proto) @@ -496,11 +477,15 @@ class CoreClient: node_interface_pairs = [] for i in edge_tokens: try: - e = self.edges.pop(i) - if e.interface_1 is not None: - node_interface_pairs.append(tuple([e.id1, e.interface_1.id])) - if e.interface_2 is not None: - node_interface_pairs.append(tuple([e.id2, e.interface_2.id])) + link = self.links.pop(i) + if link.interface_one is not None: + node_interface_pairs.append( + (link.node_one_id, link.interface_one.id) + ) + if link.interface_two is not None: + node_interface_pairs.append( + (link.node_two_id, link.interface_two.id) + ) except KeyError: logging.error("coreclient.py invalid edge token ") @@ -525,49 +510,31 @@ class CoreClient: if tuple([i, None]) in self.emaneconfig_management.configurations: self.emaneconfig_management.configurations.pop(tuple([i, None])) - def create_interface(self, node_type, gui_interface): - """ - create a protobuf interface given the interface object stored by the programmer - - :param core_bp2.NodeType node_type: node type - :param coretk.interface.Interface gui_interface: the programmer's interface - :rtype: core_bp2.Interface - :return: protobuf interface object - """ - if node_type != core_pb2.NodeType.DEFAULT: - return None - else: - interface = core_pb2.Interface( - id=gui_interface.id, - name=gui_interface.name, - mac=gui_interface.mac, - ip4=gui_interface.ipv4, - ip4mask=gui_interface.ip4prefix, - ) - logging.debug("create interface: %s", interface) - return interface - - def create_edge_interface(self, canvas_node): + def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node if self.is_model_node(core_node.model): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" - interface = Interface( - name=name, ifid=ifid, ipv4=str(self.interfaces_manager.get_address()) + interface = core_pb2.Interface( + id=ifid, + name=name, + ip4=str(self.interfaces_manager.get_address()), + ip4mask=24, ) canvas_node.interfaces.append(interface) logging.debug( - "create node(%s) interface IP: %s, name: %s", + "create node(%s) interface IPv4: %s, name: %s", core_node.name, - interface.ipv4, + interface.ip4, interface.name, ) return interface - def create_edge(self, token, canvas_node_one, canvas_node_two): + def create_link(self, token, canvas_node_one, canvas_node_two): """ - Add an edge to grpc manager + Create core link for a pair of canvas nodes, with token referencing + the canvas edge. :param tuple(int, int) token: edge's identification in the canvas :param canvas_node_one: canvas node one @@ -580,14 +547,15 @@ class CoreClient: # create interfaces self.interfaces_manager.new_subnet() - interface_one = self.create_edge_interface(canvas_node_one) + interface_one = self.create_interface(canvas_node_one) if interface_one is not None: self.interface_to_edge[(node_one.id, interface_one.id)] = token - interface_two = self.create_edge_interface(canvas_node_two) + interface_two = self.create_interface(canvas_node_two) if interface_two is not None: self.interface_to_edge[(node_two.id, interface_two.id)] = token # emane setup + # TODO: determine if this is needed if ( node_one.type == core_pb2.NodeType.EMANE and node_two.type == core_pb2.NodeType.DEFAULT @@ -605,28 +573,15 @@ class CoreClient: node_two.node_id, node_one.node_id, interface_one.id ) - edge = Edge( - self.session_id, node_one.id, node_one.type, node_two.id, node_two.type + link = core_pb2.Link( + type=core_pb2.LinkType.WIRED, + node_one_id=node_one.id, + node_two_id=node_two.id, + interface_one=interface_one, + interface_two=interface_two, ) - edge.interface_1 = interface_one - edge.interface_2 = interface_two - self.edges[token] = edge - return edge - - def get_links_proto(self): - links = [] - for edge in self.edges.values(): - interface_one = self.create_interface(edge.type1, edge.interface_1) - interface_two = self.create_interface(edge.type2, edge.interface_2) - link = core_pb2.Link( - node_one_id=edge.id1, - node_two_id=edge.id2, - type=core_pb2.LinkType.WIRED, - interface_one=interface_one, - interface_two=interface_two, - ) - links.append(link) - return links + self.links[token] = link + return link def get_wlan_configs_proto(self): configs = [] diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1fcafac1..c1be859d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -9,7 +9,6 @@ from coretk.canvasaction import CanvasAction from coretk.canvastooltip import CanvasTooltip from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images -from coretk.interface import Interface from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.wirelessconnection import WirelessConnection @@ -84,7 +83,7 @@ class CanvasGraph(tk.Canvas): self.node_context.add_command(label="Hide") self.node_context.add_command(label="Services") - def canvas_reset_and_redraw(self, session): + def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc client. @@ -183,22 +182,23 @@ class CanvasGraph(tk.Canvas): canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge - self.core.create_edge(edge.token, canvas_node_one, canvas_node_two) + self.core.links[edge.token] = link self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) # TODO add back the link info to grpc manager also redraw - grpc_if1 = link.interface_one - grpc_if2 = link.interface_two + # TODO will include throughput and ipv6 in the future + interface_one = link.interface_one + interface_two = link.interface_two ip4_src = None ip4_dst = None ip6_src = None ip6_dst = None - if grpc_if1 is not None: - ip4_src = grpc_if1.ip4 - ip6_src = grpc_if1.ip6 - if grpc_if2 is not None: - ip4_dst = grpc_if2.ip4 - ip6_dst = grpc_if2.ip6 + if interface_one is not None: + ip4_src = interface_one.ip4 + ip6_src = interface_one.ip6 + if interface_two is not None: + ip4_dst = interface_two.ip4 + ip6_dst = interface_two.ip6 edge.link_info = LinkInfo( canvas=self, edge=edge, @@ -207,14 +207,8 @@ class CanvasGraph(tk.Canvas): ip4_dst=ip4_dst, ip6_dst=ip6_dst, ) - - # TODO will include throughput and ipv6 in the future - if1 = Interface(grpc_if1.name, grpc_if1.ip4, ifid=grpc_if1.id) - if2 = Interface(grpc_if2.name, grpc_if2.ip4, ifid=grpc_if2.id) - self.core.edges[edge.token].interface_1 = if1 - self.core.edges[edge.token].interface_2 = if2 - canvas_node_one.interfaces.append(if1) - canvas_node_two.interfaces.append(if2) + canvas_node_one.interfaces.append(interface_one) + canvas_node_two.interfaces.append(interface_two) # raise the nodes so they on top of the links self.tag_raise("node") @@ -316,17 +310,17 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - core_edge = self.core.create_edge(edge.token, node_src, node_dst) + link = self.core.create_link(edge.token, node_src, node_dst) # draw link info on the edge - if1 = core_edge.interface_1 - if2 = core_edge.interface_2 ip4_and_prefix_1 = None ip4_and_prefix_2 = None - if if1 is not None: - ip4_and_prefix_1 = if1.ip4_and_prefix - if if2 is not None: - ip4_and_prefix_2 = if2.ip4_and_prefix + if link.HasField("interface_one"): + if1 = link.interface_one + ip4_and_prefix_1 = f"{if1.ip4}/{if1.ip4mask}" + if link.HasField("interface_two"): + if2 = link.interface_two + ip4_and_prefix_2 = f"{if2.ip4}/{if2.ip4mask}" edge.link_info = LinkInfo( self, edge, diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 5df26e26..b0fcd346 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -1,35 +1,4 @@ import ipaddress -import random - - -class Interface: - def __init__(self, name, ipv4, ifid=None): - """ - Create an interface instance - - :param str name: interface name - :param str ip4: IPv4 - :param str mac: MAC address - :param int ifid: interface id - """ - self.name = name - self.ipv4 = ipv4 - self.ip4prefix = 24 - self.ip4_and_prefix = ipv4 + "/" + str(self.ip4prefix) - self.mac = self.random_mac_address() - self.id = ifid - - def random_mac_address(self): - """ - create a random MAC address for an interface - - :return: nothing - """ - return "02:00:00:%02x:%02x:%02x" % ( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 225), - ) class SubnetAddresses: @@ -46,18 +15,13 @@ class SubnetAddresses: class InterfaceManager: def __init__(self): - # self.prefix = None self.core_subnets = list( ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) ) self.subnet_index = 0 self.address_index = 0 - - # self.network = ipaddress.ip_network("10.0.0.0/24") - # self.addresses = list(self.network.hosts()) self.network = None self.addresses = None - # self.start_interface_manager() def start_interface_manager(self): self.subnet_index = 0 @@ -72,24 +36,10 @@ class InterfaceManager: :return: """ - # i = self.index - # self.address_index = self.index + 1 - # return self.addresses[i] ipaddr = self.addresses[self.address_index] self.address_index = self.address_index + 1 return ipaddr def new_subnet(self): self.network = self.core_subnets[self.subnet_index] - # self.subnet_index = self.subnet_index + 1 self.addresses = list(self.network.hosts()) - # self.address_index = 0 - - # def new_subnet(self): - # """ - # retrieve a new subnet - # :return: - # """ - # if self.prefix is None: - # self.prefix = - # self.addresses = list(ipaddress.ip_network("10.0.0.0/24").hosts()) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 29430deb..1e0363cf 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -4,8 +4,6 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas import logging import math -WIRELESS_DEF = ["mdr", "wlan"] - class LinkInfo: def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index a049c321..6017cbb1 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -375,6 +375,7 @@ class Toolbar(ttk.Frame): """ logging.debug("Click on STOP button ") self.app.core.stop_session() + self.app.canvas.delete("wireless") self.design_frame.tkraise() def update_annotation(self, image): diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index b864ce38..e8d03126 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -19,7 +19,7 @@ class WirelessConnection: x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( - x1, y1, x2, y2, fill="#009933", tags="wlan", width=1.5 + x1, y1, x2, y2, fill="#009933", tags="wireless", width=1.5 ) self.map[key] = wlan_canvas_id canvas_node_one.wlans.append(wlan_canvas_id) From 7981340b13f85c37ccfc90dfe20359d209d03ddc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 13:09:53 -0800 Subject: [PATCH 125/305] added path for creating non core container based nodes --- coretk/coretk/icons/docker.gif | Bin 0 -> 719 bytes coretk/coretk/icons/lxc.gif | Bin 0 -> 724 bytes coretk/coretk/images.py | 2 ++ coretk/coretk/toolbar.py | 28 ++++++++++++++++------------ 4 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 coretk/coretk/icons/docker.gif create mode 100644 coretk/coretk/icons/lxc.gif diff --git a/coretk/coretk/icons/docker.gif b/coretk/coretk/icons/docker.gif new file mode 100644 index 0000000000000000000000000000000000000000..dde25750d31c0f3ce9329dd5dd26f33ec3ebd0bf GIT binary patch literal 719 zcmZ?wbhEHblxGlSIOfgZtdQWYnC7jV<)f10tCAb2Rve;H5w2Mit=$x>)10K&ovh!R zYA_+qU}C1>v@E0P*+w&SjA!MW%q=jTUu3qZ#C&O)#fox^m6eujsw~&lSZ}Pg-qdKj zz1eP8i{0*4`@QWB`@38YcDo(y@i^4ud8p6l@Wj9)lY)*;4mma@^w`w!+e$?woLI z*Th?Ur{38&@hcMr_DcW~~#!wc>oS@huevWF*EK03YT@!9oHE^K{var^WCgBvLR zWMSlDsAte&00K~)FtGn?sBda+X>DuoXcp@3>Fw)m>6&ZbT_)L+Qh)j+bfo|;6p&-%r>DD zDiID3o8lQ5*(7wL%@~_IM0KS8F@Bifz{t!gX4A2u$BC6!)@_DIW1zA-qa3e{!vu$e zOziwBGP@@vGPm<9xJ`(-c}ek1zj5}xGnqooZ1Wvjh0bP}qy^2kC=Dy%GJO#+EyTRc z#Y>7;s=2E^o{yNRn>sz~>8dcT8{6vp4u#Iznt5wQyrI-4sqOl=)|Dy#-Klk5 SKj~;kf!l(FgPKnq7_0#ge~iHZ literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/lxc.gif b/coretk/coretk/icons/lxc.gif new file mode 100644 index 0000000000000000000000000000000000000000..e9f57aa30d3e2092b7de3f074146985272accd7b GIT binary patch literal 724 zcmZ?wbhEHblxGlSI2OW?Y^9KCr=07kQsAsw=&V-ere5K$QSGTw@2%D9uhS8z+ZUog zG0b3cxWTkY!2~R9E8Z-jp+asb?o-oSm3;Zc@(qDf#E87F?K7 zd~s&Ur8$+C=ha+W(sF%y$E~#!ZmpYmd&87F8>imgGV9*fIrp~Dy}x7r{hbRQ?pgA1 z@3KexS3W+t`pJ>?&yH_-erm^yGrRr|0-*Slg^`P)o2=Jx;6p9&Kkl-LBYXZ8aFmIu}pS- z7G&0Ipkv`46kvTrw~2w7uh-~BgCi5OF~6>s&MAh+Zu=RS_)HRx^)R)Im}FV}VP;Tj zWMUUFXi#X3WaW`_TT>vY{PFMDa3YoW# ztlWHvl~1mYO*Sgzao|j^{AYK5nrH Date: Fri, 15 Nov 2019 17:05:03 -0800 Subject: [PATCH 126/305] save service file data change locally and use that for display and for start session --- coretk/coretk/coreclient.py | 90 ++++++++++++++---- coretk/coretk/dialogs/nodeservice.py | 6 +- coretk/coretk/dialogs/serviceconfiguration.py | 94 ++++++++++++------- coretk/coretk/servicefileconfig.py | 37 ++++++++ coretk/coretk/servicenodeconfig.py | 5 +- 5 files changed, 177 insertions(+), 55 deletions(-) create mode 100644 coretk/coretk/servicefileconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 51aa8e46..52a45140 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,6 +11,7 @@ from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.images import NODE_WIDTH, Images from coretk.interface import Interface, InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig @@ -126,6 +127,9 @@ class CoreClient: self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) + self.servicefileconfig_manager = ServiceFileConfig() + self.created_nodes = set() + self.created_links = set() def set_observer(self, value): self.observer = value @@ -345,6 +349,10 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) + # service_configs = self.get_service_config_proto() + # service_file_configs = self.get_service_file_config_proto() + self.created_links.clear() + self.created_nodes.clear() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -358,16 +366,10 @@ class CoreClient: emane_config=emane_config, emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, + # service_configs=service_configs ) logging.debug("Start session %s, result: %s", self.session_id, response.result) - response = self.client.get_service_defaults(self.session_id) - for default in response.defaults: - print(default.node_type) - print(default.services) - response = self.client.get_node_service(self.session_id, 5, "FTP") - print(response) - def stop_session(self): response = self.client.stop_session(session_id=self.session_id) logging.debug("coregrpc.py Stop session, result: %s", response.result) @@ -434,23 +436,42 @@ class CoreClient: logging.debug("get service file %s", response) return response.data + def set_node_service_file(self, node_id, service_name, file_name, data): + response = self.client.set_node_service_file( + self.session_id, node_id, service_name, file_name, data + ) + logging.debug("set node service file %s", response) + def create_nodes_and_links(self): + """ + create nodes and links that have not been created yet + + :return: nothing + """ node_protos = self.get_nodes_proto() link_protos = self.get_links_proto() - self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - response = self.client.add_node(self.session_id, node_proto) - logging.debug("create node: %s", response) + if node_proto.id not in self.created_nodes: + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) + self.created_nodes.add(node_proto.id) for link_proto in link_protos: - response = self.client.add_link( - self.session_id, - link_proto.node_one_id, - link_proto.node_two_id, - link_proto.interface_one, - link_proto.interface_two, - link_proto.options, - ) - logging.debug("create link: %s", response) + if ( + tuple([link_proto.node_one_id, link_proto.node_two_id]) + not in self.created_links + ): + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) + self.created_links.add( + tuple([link_proto.node_one_id, link_proto.node_two_id]) + ) def close(self): """ @@ -838,6 +859,37 @@ class CoreClient: configs.append(config_proto) return configs + def get_service_config_proto(self): + configs = [] + for ( + node_id, + service_configs, + ) in self.serviceconfig_manager.configurations.items(): + for service, config in service_configs.items(): + config = core_pb2.ServiceConfig( + node_id=node_id, + service=service, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config) + return configs + + def get_service_file_config_proto(self): + configs = [] + for ( + node_id, + service_file_configs, + ) in self.servicefileconfig_manager.configurations.items(): + for service, file_configs in service_file_configs.items(): + for file, data in file_configs.items(): + config = core_pb2.ServiceFileConfig( + node_id=node_id, service=service, file=file, data=data + ) + configs.append(config) + return configs + def run(self, node_id): logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 9ced9205..d791cc0e 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -17,7 +17,11 @@ class NodeService(Dialog): self.services = None self.current = None if services is None: - services = set() + services = set( + app.core.serviceconfig_manager.configurations[ + canvas_node.core_id + ].keys() + ) self.current_services = services self.draw() diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 360e9561..da0c0736 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -15,6 +15,7 @@ class ServiceConfiguration(Dialog): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app self.canvas_node = canvas_node + self.node_id = canvas_node.core_id self.service_name = service_name self.radiovar = tk.IntVar() self.radiovar.set(2) @@ -40,8 +41,9 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None - self.service_files = {} + self.original_service_files = {} self.temp_service_files = {} + self.modified_files = set() self.load() self.draw() @@ -49,9 +51,18 @@ class ServiceConfiguration(Dialog): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() # load data from local memory - service_config = self.app.core.serviceconfig_manager.configurations[ - self.canvas_node.core_id - ][self.service_name] + serviceconfig_manager = self.app.core.serviceconfig_manager + if self.service_name in serviceconfig_manager.configurations[self.node_id]: + service_config = serviceconfig_manager.configurations[self.node_id][ + self.service_name + ] + else: + serviceconfig_manager.node_custom_service_configuration( + self.node_id, self.service_name + ) + service_config = serviceconfig_manager.configurations[self.node_id][ + self.service_name + ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -61,13 +72,24 @@ class ServiceConfiguration(Dialog): self.shutdown_commands = [x for x in service_config.shutdown] self.validation_mode = service_config.validation_mode self.validation_time = service_config.validation_timer - self.service_files = { + self.original_service_files = { x: self.app.core.get_node_service_file( self.canvas_node.core_id, self.service_name, x ) for x in self.filenames } - self.temp_service_files = self.service_files + self.temp_service_files = { + x: self.original_service_files[x] for x in self.original_service_files + } + configs = self.app.core.servicefileconfig_manager.configurations + if ( + self.canvas_node.core_id in configs + and self.service_name in configs[self.canvas_node.core_id] + ): + for file, data in configs[self.canvas_node.core_id][ + self.service_name + ].items(): + self.temp_service_files[file] = data def draw(self): # self.columnconfigure(1, weight=1) @@ -169,7 +191,10 @@ class ServiceConfiguration(Dialog): if len(self.filenames) > 0: self.filename_combobox.current(0) self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.service_files[self.filenames[0]]) + self.service_file_data.insert( + "end", self.temp_service_files[self.filenames[0]] + ) + self.service_file_data.bind("", self.update_temp_service_file_data) # tab 2 label = ttk.Label( @@ -184,7 +209,6 @@ class ServiceConfiguration(Dialog): if i == 0: label_frame = ttk.LabelFrame(tab3, text="Startup commands") commands = self.startup_commands - elif i == 1: label_frame = ttk.LabelFrame(tab3, text="Shutdown commands") commands = self.shutdown_commands @@ -242,11 +266,9 @@ class ServiceConfiguration(Dialog): frame.columnconfigure(0, weight=1) if i == 0: label = ttk.Label(frame, text="Validation time:") - self.validation_time_entry = ttk.Entry( - frame, - state="disabled", - textvariable=tk.StringVar(value=self.validation_time), - ) + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state="disabled") self.validation_time_entry.grid(row=i, column=1) elif i == 1: label = ttk.Label(frame, text="Validation mode:") @@ -258,13 +280,11 @@ class ServiceConfiguration(Dialog): mode = "NON_BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: mode = "TIMER" - print("the mode is", mode) self.validation_mode_entry = ttk.Entry( frame, textvariable=tk.StringVar(value=mode) ) self.validation_mode_entry.insert("end", mode) - print("get mode") - print(self.validation_mode_entry.get()) + self.validation_mode_entry.config(state="disabled") self.validation_mode_entry.grid(row=i, column=1) elif i == 2: label = ttk.Label(frame, text="Validation period:") @@ -347,8 +367,6 @@ class ServiceConfiguration(Dialog): entry.delete(0, tk.END) def click_apply(self): - metadata = self.metadata_entry.get() - filenames = list(self.filename_combobox["values"]) startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") @@ -359,27 +377,35 @@ class ServiceConfiguration(Dialog): validate_commands, shutdown_commands, ) - filename = self.filename_combobox.get() - file_data = self.service_file_data.get() - print(filename, file_data) - logging.info( - "%s, %s, %s, %s, %s", - metadata, - filenames, - startup_commands, - shutdown_commands, - validate_commands, - ) - # wipe nodes and links when finished by setting to DEFINITION state - self.app.core.client.set_session_state( - self.app.core.session_id, core_pb2.SessionState.DEFINITION - ) + for file in self.modified_files: + self.app.core.servicefileconfig_manager.set_custom_service_file_config( + self.canvas_node.core_id, + self.service_name, + file, + self.temp_service_files[file], + ) + self.app.core.set_node_service_file( + self.canvas_node.core_id, + self.service_name, + file, + self.temp_service_files[file], + ) + self.destroy() def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.service_files[filename]) + self.service_file_data.insert("end", self.temp_service_files[filename]) + + def update_temp_service_file_data(self, event): + scrolledtext = event.widget + filename = self.filename_combobox.get() + self.temp_service_files[filename] = scrolledtext.get(1.0, "end") + if self.temp_service_files[filename] != self.original_service_files[filename]: + self.modified_files.add(filename) + else: + self.modified_files.discard(filename) def click_defaults(self): logging.info("not implemented") diff --git a/coretk/coretk/servicefileconfig.py b/coretk/coretk/servicefileconfig.py new file mode 100644 index 00000000..06a134cd --- /dev/null +++ b/coretk/coretk/servicefileconfig.py @@ -0,0 +1,37 @@ +""" +service file configuration +""" + + +class ServiceFileConfig: + def __init__(self): + # dict(node_id:dict(service:dict(filename, data))) + self.configurations = {} + + # def set_service_configs(self, node_id, service_name, file_configs): + # """ + # store file configs + # + # :param int node_id: node id + # :param str service_name: service name + # :param dict(str, str) file_configs: map of service file to its data + # :return: nothing + # """ + # for key, value in file_configs.items(): + # self.configurations[node_id][service_name][key] = value + + def set_custom_service_file_config(self, node_id, service_name, file_name, data): + """ + store file config + + :param int node_id: node id + :param str service_name: service name + :param str file_name: file name + :param str data: data + :return: nothing + """ + if node_id not in self.configurations: + self.configurations[node_id] = {} + if service_name not in self.configurations[node_id]: + self.configurations[node_id][service_name] = {} + self.configurations[node_id][service_name][file_name] = data diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index f9c2ee88..98de9034 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -7,6 +7,7 @@ import logging class ServiceNodeConfig: def __init__(self, app): self.app = app + # dict(node_id:dict(service:node_service_config_proto)) self.configurations = {} self.default_services = {} @@ -37,7 +38,9 @@ class ServiceNodeConfig: self.configurations[node_id][default] = response.service def node_custom_service_configuration(self, node_id, service_name): - return + self.configurations[node_id][service_name] = self.app.core.get_node_service( + node_id, service_name + ) def node_service_custom_configuration( self, node_id, service_name, startups, validates, shutdowns From 8ad9b7d7286bd17e4899ecd331c9ff56dce6713b Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 23:31:41 -0800 Subject: [PATCH 127/305] removed CustomNode class, added nodeutils and NodeDraw to support defining the current node type to draw and reuse for custom nodes as well --- coretk/coretk/app.py | 4 ++ coretk/coretk/coreclient.py | 21 +++----- coretk/coretk/dialogs/customnodes.py | 43 +++++++-------- coretk/coretk/dialogs/icondialog.py | 3 +- coretk/coretk/graph.py | 27 ++++------ coretk/coretk/images.py | 41 --------------- coretk/coretk/nodeutils.py | 79 ++++++++++++++++++++++++++++ coretk/coretk/themes.py | 7 ++- coretk/coretk/toolbar.py | 60 ++++++--------------- 9 files changed, 146 insertions(+), 139 deletions(-) create mode 100644 coretk/coretk/nodeutils.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 185e5968..fbce02e1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -9,12 +9,16 @@ from coretk.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar +from coretk.nodeutils import NodeUtils from coretk.toolbar import Toolbar class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) + # load node icons + NodeUtils.setup() + # widgets self.menubar = None self.toolbar = None diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5ed35b57..c57a0b42 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,13 +7,12 @@ import os from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig -from coretk.images import NODE_WIDTH, Images from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig +from coretk.nodeutils import NodeDraw from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -NETWORK_NODES = {"switch", "hub", "wlan", "rj45", "tunnel", "emane"} DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} OBSERVERS = { "processes": "ps", @@ -35,14 +34,6 @@ class CoreServer: self.port = port -class CustomNode: - def __init__(self, name, image, image_file, services): - self.name = name - self.image = image - self.image_file = image_file - self.services = services - - class Observer: def __init__(self, name, cmd): self.name = name @@ -96,12 +87,11 @@ class CoreClient: # read custom nodes for config in self.app.config.get("nodes", []): + name = config["name"] image_file = config["image"] - image = Images.get_custom(image_file, NODE_WIDTH) - custom_node = CustomNode( - config["name"], image, image_file, set(config["services"]) - ) - self.custom_nodes[custom_node.name] = custom_node + services = set(config["services"]) + node_draw = NodeDraw.from_custom(name, image_file, services) + self.custom_nodes[name] = node_draw # read observers for config in self.app.config.get("observers", []): @@ -448,6 +438,7 @@ class CoreClient: self.emaneconfig_management.set_default_config(node_id) # set default service configurations + # TODO: need to deal with this and custom node cases if node_type == core_pb2.NodeType.DEFAULT: self.serviceconfig_manager.node_default_services_configuration( node_id=node_id, node_model=model diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index f427f08b..da7a1fb0 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -3,9 +3,9 @@ import tkinter as tk from pathlib import Path from tkinter import ttk -from coretk.coreclient import CustomNode from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog +from coretk.nodeutils import NodeDraw from coretk.widgets import CheckboxList, ListboxScroll @@ -119,7 +119,9 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(sticky="ew") - self.image_button = ttk.Button(frame, text="Icon", command=self.click_icon) + self.image_button = ttk.Button( + frame, text="Icon", compound=tk.LEFT, command=self.click_icon + ) self.image_button.grid(sticky="ew") button = ttk.Button(frame, text="Services", command=self.click_services) button.grid(sticky="ew") @@ -180,12 +182,12 @@ class CustomNodesDialog(Dialog): def click_save(self): self.app.config["nodes"].clear() for name in sorted(self.app.core.custom_nodes): - custom_node = self.app.core.custom_nodes[name] + node_draw = self.app.core.custom_nodes[name] self.app.config["nodes"].append( { - "name": custom_node.name, - "image": custom_node.image_file, - "services": list(custom_node.services), + "name": name, + "image": node_draw.image_file, + "services": list(node_draw.services), } ) logging.info("saving custom nodes: %s", self.app.config["nodes"]) @@ -195,10 +197,9 @@ class CustomNodesDialog(Dialog): def click_create(self): name = self.name.get() if name not in self.app.core.custom_nodes: - custom_node = CustomNode( - name, self.image, Path(self.image_file).name, set(self.services) - ) - self.app.core.custom_nodes[name] = custom_node + image_file = Path(self.image_file).stem + node_draw = NodeDraw.from_custom(name, image_file, set(self.services)) + self.app.core.custom_nodes[name] = node_draw self.nodes_list.listbox.insert(tk.END, name) self.reset_values() @@ -207,12 +208,12 @@ class CustomNodesDialog(Dialog): if self.selected: previous_name = self.selected self.selected = name - custom_node = self.app.core.custom_nodes.pop(previous_name) - custom_node.name = name - custom_node.image = self.image - custom_node.image_file = Path(self.image_file).stem - custom_node.services = self.services - self.app.core.custom_nodes[name] = custom_node + node_draw = self.app.core.custom_nodes.pop(previous_name) + node_draw.model = name + node_draw.image_file = Path(self.image_file).stem + node_draw.image = self.image + node_draw.services = self.services + self.app.core.custom_nodes[name] = node_draw self.nodes_list.listbox.delete(self.selected_index) self.nodes_list.listbox.insert(self.selected_index, name) self.nodes_list.listbox.selection_set(self.selected_index) @@ -230,11 +231,11 @@ class CustomNodesDialog(Dialog): if selection: self.selected_index = selection[0] self.selected = self.nodes_list.listbox.get(self.selected_index) - custom_node = self.app.core.custom_nodes[self.selected] - self.name.set(custom_node.name) - self.services = custom_node.services - self.image = custom_node.image - self.image_file = custom_node.image_file + node_draw = self.app.core.custom_nodes[self.selected] + self.name.set(node_draw.model) + self.services = node_draw.services + self.image = node_draw.image + self.image_file = node_draw.image_file self.image_button.config(image=self.image) self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index c5518f28..98a4ed55 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -1,6 +1,7 @@ import tkinter as tk from tkinter import filedialog, ttk +from coretk import nodeutils from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images @@ -54,7 +55,7 @@ class IconDialog(Dialog): ), ) if file_path: - self.image = Images.create(file_path, 32, 32) + self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_label.config(image=self.image) self.file_path.set(file_path) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c1be859d..38d67079 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -11,6 +11,7 @@ from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement +from coretk.nodeutils import NodeUtils from coretk.wirelessconnection import WirelessConnection @@ -37,9 +38,7 @@ class CanvasGraph(tk.Canvas): kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT - self.draw_node_image = None - self.draw_node_type = None - self.draw_node_model = None + self.node_draw = None self.selected = None self.node_context = None self.nodes = {} @@ -96,9 +95,7 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT - self.draw_node_image = None - self.draw_node_type = None - self.draw_node_model = None + self.node_draw = None self.selected = None self.node_context = None self.nodes.clear() @@ -157,7 +154,7 @@ class CanvasGraph(tk.Canvas): continue # draw nodes on the canvas - image = Images.node_icon(core_node.type, core_node.model) + image = NodeUtils.node_icon(core_node.type, core_node.model) position = core_node.position node = CanvasNode(position.x, position.y, image, self.master, core_node) self.nodes[node.id] = node @@ -267,13 +264,7 @@ class CanvasGraph(tk.Canvas): self.handle_edge_release(event) elif self.mode == GraphMode.NODE: x, y = self.canvas_xy(event) - self.add_node( - x, - y, - self.draw_node_image, - self.draw_node_type, - self.draw_node_model, - ) + self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -404,12 +395,14 @@ class CanvasGraph(tk.Canvas): # delete the related data from core self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) - def add_node(self, x, y, image, node_type, model): + def add_node(self, x, y): plot_id = self.find_all()[0] logging.info("add node event: %s - %s", plot_id, self.selected) if self.selected == plot_id: - core_node = self.core.create_node(int(x), int(y), node_type, model) - node = CanvasNode(x, y, image, self.master, core_node) + core_node = self.core.create_node( + int(x), int(y), self.node_draw.node_type, self.node_draw.model + ) + node = CanvasNode(x, y, self.node_draw.image, self.master, core_node) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 1a377253..8f13c593 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -1,9 +1,7 @@ -import logging from enum import Enum from PIL import Image, ImageTk -from core.api.grpc import core_pb2 from coretk.appconfig import LOCAL_ICONS_PATH NODE_WIDTH = 32 @@ -35,45 +33,6 @@ class Images: file_path = cls.images[name] return cls.create(file_path, width, height) - @classmethod - def node_icon(cls, node_type, node_model): - """ - Retrieve image based on type and model - :param core_pb2.NodeType node_type: core node type - :param string node_model: the node model - :return: core node icon - :rtype: PhotoImage - """ - image_enum = ImageEnum.ROUTER - if node_type == core_pb2.NodeType.SWITCH: - image_enum = ImageEnum.SWITCH - elif node_type == core_pb2.NodeType.HUB: - image_enum = ImageEnum.HUB - elif node_type == core_pb2.NodeType.WIRELESS_LAN: - image_enum = ImageEnum.WLAN - elif node_type == core_pb2.NodeType.EMANE: - image_enum = ImageEnum.EMANE - elif node_type == core_pb2.NodeType.RJ45: - image_enum = ImageEnum.RJ45 - elif node_type == core_pb2.NodeType.TUNNEL: - image_enum = ImageEnum.TUNNEL - elif node_type == core_pb2.NodeType.DEFAULT: - if node_model == "router": - image_enum = ImageEnum.ROUTER - elif node_model == "host": - image_enum = ImageEnum.HOST - elif node_model == "PC": - image_enum = ImageEnum.PC - elif node_model == "mdr": - image_enum = ImageEnum.MDR - elif node_model == "prouter": - image_enum = ImageEnum.PROUTER - else: - logging.error("invalid node model: %s", node_model) - else: - logging.error("invalid node type: %s", node_type) - return Images.get(image_enum, NODE_WIDTH) - class ImageEnum(Enum): SWITCH = "lanswitch" diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py new file mode 100644 index 00000000..df483ba6 --- /dev/null +++ b/coretk/coretk/nodeutils.py @@ -0,0 +1,79 @@ +from core.api.grpc.core_pb2 import NodeType +from coretk.images import ImageEnum, Images + +ICON_SIZE = 32 + + +class NodeDraw: + def __init__(self): + self.custom = False + self.image = None + self.image_enum = None + self.image_file = None + self.node_type = None + self.model = None + self.tooltip = None + self.services = set() + + @classmethod + def from_setup(cls, image_enum, node_type, model=None, tooltip=None): + node_draw = NodeDraw() + node_draw.image_enum = image_enum + node_draw.image = Images.get(image_enum, ICON_SIZE) + node_draw.node_type = node_type + node_draw.model = model + if tooltip is None: + tooltip = model + node_draw.tooltip = tooltip + return node_draw + + @classmethod + def from_custom(cls, name, image_file, services): + node_draw = NodeDraw() + node_draw.custom = True + node_draw.image_file = image_file + node_draw.image = Images.get_custom(image_file, ICON_SIZE) + node_draw.node_type = NodeType.DEFAULT + node_draw.services = services + node_draw.model = name + node_draw.tooltip = name + return node_draw + + +class NodeUtils: + NODES = [] + NETWORK_NODES = [] + NODE_ICONS = {} + + @classmethod + def node_icon(cls, node_type, model): + return cls.NODE_ICONS[(node_type, model)] + + @classmethod + def setup(cls): + nodes = [ + (ImageEnum.ROUTER, NodeType.DEFAULT, "router"), + (ImageEnum.HOST, NodeType.DEFAULT, "host"), + (ImageEnum.PC, NodeType.DEFAULT, "PC"), + (ImageEnum.MDR, NodeType.DEFAULT, "mdr"), + (ImageEnum.PROUTER, NodeType.DEFAULT, "prouter"), + (ImageEnum.DOCKER, NodeType.DOCKER, "Docker"), + (ImageEnum.LXC, NodeType.LXC, "LXC"), + ] + for image_enum, node_type, model in nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, model) + cls.NODES.append(node_draw) + cls.NODE_ICONS[(node_type, model)] = node_draw.image + + network_nodes = [ + (ImageEnum.HUB, NodeType.HUB, "ethernet hub"), + (ImageEnum.SWITCH, NodeType.SWITCH, "ethernet switch"), + (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), + (ImageEnum.RJ45, NodeType.RJ45, "rj45 physical interface tool"), + (ImageEnum.TUNNEL, NodeType.TUNNEL, "tunnel tool"), + ] + for image_enum, node_type, tooltip in network_nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) + cls.NETWORK_NODES.append(node_draw) + cls.NODE_ICONS[(node_type, None)] = node_draw.image diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index f6dced19..0cf54eb8 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -54,7 +54,12 @@ def load(style): }, }, "TButton": { - "configure": {"width": 8, "padding": (5, 1), "relief": tk.RAISED}, + "configure": { + "width": 8, + "padding": (5, 1), + "relief": tk.RAISED, + "anchor": tk.CENTER, + }, "map": { "relief": [("pressed", tk.SUNKEN)], "shiftrelief": [("pressed", 1)], diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 7fe675a4..bb1803ea 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,10 +3,10 @@ import tkinter as tk from functools import partial from tkinter import ttk -from core.api.grpc import core_pb2 from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import GraphMode from coretk.images import ImageEnum, Images +from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip WIDTH = 32 @@ -129,33 +129,16 @@ class Toolbar(ttk.Frame): def draw_node_picker(self): self.hide_pickers() self.node_picker = ttk.Frame(self.master) - nodes = [ - (ImageEnum.ROUTER, core_pb2.NodeType.DEFAULT, "router"), - (ImageEnum.HOST, core_pb2.NodeType.DEFAULT, "host"), - (ImageEnum.PC, core_pb2.NodeType.DEFAULT, "PC"), - (ImageEnum.MDR, core_pb2.NodeType.DEFAULT, "mdr"), - (ImageEnum.PROUTER, core_pb2.NodeType.DEFAULT, "prouter"), - (ImageEnum.DOCKER, core_pb2.NodeType.DOCKER, "Docker"), - (ImageEnum.LXC, core_pb2.NodeType.LXC, "LXC"), - ] # draw default nodes - for image_enum, node_type, model in nodes: - image = icon(image_enum) - func = partial( - self.update_button, self.node_button, image, node_type, model - ) - self.create_picker_button(image, func, self.node_picker, model) + for node_draw in NodeUtils.NODES: + image = icon(node_draw.image_enum) + func = partial(self.update_button, self.node_button, image, node_draw) + self.create_picker_button(image, func, self.node_picker, node_draw.tooltip) # draw custom nodes for name in sorted(self.app.core.custom_nodes): - custom_node = self.app.core.custom_nodes[name] - image = custom_node.image - func = partial( - self.update_button, - self.node_button, - image, - core_pb2.NodeType.DEFAULT, - name, - ) + node_draw = self.app.core.custom_nodes[name] + image = Images.get_custom(node_draw.image_file, WIDTH) + func = partial(self.update_button, self.node_button, image, node_draw) self.create_picker_button(image, func, self.node_picker, name) # draw edit node image = icon(ImageEnum.EDITNODE) @@ -227,15 +210,13 @@ class Toolbar(ttk.Frame): dialog = CustomNodesDialog(self.app, self.app) dialog.show() - def update_button(self, button, image, node_type, model=None): - logging.info("update button(%s): %s", button, node_type) + def update_button(self, button, image, node_draw): + logging.info("update button(%s): %s", button, node_draw) self.hide_pickers() button.configure(image=image) button.image = image self.app.canvas.mode = GraphMode.NODE - self.app.canvas.draw_node_image = image - self.app.canvas.draw_node_type = node_type - self.app.canvas.draw_node_model = model + self.app.canvas.node_draw = node_draw def hide_pickers(self): logging.info("hiding pickers") @@ -271,21 +252,13 @@ class Toolbar(ttk.Frame): """ self.hide_pickers() self.network_picker = ttk.Frame(self.master) - nodes = [ - (ImageEnum.HUB, core_pb2.NodeType.HUB, "ethernet hub"), - (ImageEnum.SWITCH, core_pb2.NodeType.SWITCH, "ethernet switch"), - (ImageEnum.WLAN, core_pb2.NodeType.WIRELESS_LAN, "wireless LAN"), - (ImageEnum.EMANE, core_pb2.NodeType.EMANE, "EMANE"), - (ImageEnum.RJ45, core_pb2.NodeType.RJ45, "rj45 physical interface tool"), - (ImageEnum.TUNNEL, core_pb2.NodeType.TUNNEL, "tunnel tool"), - ] - for image_enum, node_type, tooltip in nodes: - image = icon(image_enum) + for node_draw in NodeUtils.NETWORK_NODES: + image = icon(node_draw.image_enum) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, node_type), + partial(self.update_button, self.network_button, image, node_draw), self.network_picker, - tooltip, + node_draw.tooltip, ) self.design_select(self.network_button) self.network_button.after( @@ -294,7 +267,8 @@ class Toolbar(ttk.Frame): def create_network_button(self): """ - Create link-layer node button and the options that represent different link-layer node types + Create link-layer node button and the options that represent different + link-layer node types. :return: nothing """ From 95cd7926756b7edb6b86df4f69dd2458985b1e77 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 15 Nov 2019 23:45:01 -0800 Subject: [PATCH 128/305] updated check for creating interfaces on nodes --- coretk/coretk/coreclient.py | 8 ++------ coretk/coretk/dialogs/nodeconfig.py | 3 ++- coretk/coretk/nodeutils.py | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c57a0b42..3c63e6bc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -9,11 +9,10 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.mobilitynodeconfig import MobilityNodeConfig -from coretk.nodeutils import NodeDraw +from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicenodeconfig import ServiceNodeConfig from coretk.wlannodeconfig import WlanNodeConfig -DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -406,9 +405,6 @@ class CoreClient: else: return self.reusable.pop(0) - def is_model_node(self, name): - return name in DEFAULT_NODES or name in self.custom_nodes - def create_node(self, x, y, node_type, model): """ Add node, with information filled in, to grpc manager @@ -511,7 +507,7 @@ class CoreClient: def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node - if self.is_model_node(core_node.model): + if NodeUtils.is_interface_node(core_node.type): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" interface = core_pb2.Interface( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 14e9de03..57c2fcfb 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,11 +1,12 @@ import tkinter as tk from tkinter import ttk -from coretk.coreclient import DEFAULT_NODES from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} + class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index df483ba6..6777718a 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -44,6 +44,11 @@ class NodeUtils: NODES = [] NETWORK_NODES = [] NODE_ICONS = {} + INTERFACE_NODE = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + + @classmethod + def is_interface_node(cls, node_type): + return node_type in cls.INTERFACE_NODE @classmethod def node_icon(cls, node_type, model): From 695f5c3e66557eb3e497b7ddcf2224df613f2eba Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Sat, 16 Nov 2019 08:54:15 -0800 Subject: [PATCH 129/305] updated node config layout --- coretk/coretk/dialogs/nodeconfig.py | 79 ++++++++++++++++------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 57c2fcfb..a8252336 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -6,6 +6,7 @@ from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} +PAD = 5 class NodeConfigDialog(Dialog): @@ -28,54 +29,64 @@ class NodeConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_first_row() - self.draw_second_row() - self.draw_third_row() + row = 0 - def draw_first_row(self): + # field frame frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) + frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) + # name field + label = ttk.Label(frame, text="Name") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=0, padx=2, sticky="ew") - - combobox = ttk.Combobox( - frame, textvariable=self.type, values=DEFAULT_NODES, state="readonly" - ) - combobox.grid(row=0, column=1, padx=2, sticky="ew") - - servers = [""] - servers.extend(list(sorted(self.app.core.servers.keys()))) - combobox = ttk.Combobox( - frame, textvariable=self.server, values=servers, state="readonly" - ) - combobox.current(0) - combobox.grid(row=0, column=2, sticky="ew") - - def draw_second_row(self): - frame = ttk.Frame(self.top) - frame.grid(row=1, column=0, pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - - button = ttk.Button(frame, text="Services", command=self.click_services) - button.grid(row=0, column=0, padx=2, sticky="ew") + entry.grid(row=row, column=1, sticky="ew") + row += 1 + # icon field + label = ttk.Label(frame, text="Icon") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) self.image_button = ttk.Button( frame, text="Icon", image=self.image, - compound=tk.LEFT, + compound=tk.NONE, command=self.click_icon, ) - self.image_button.grid(row=0, column=1, sticky="ew") + self.image_button.grid(row=row, column=1, sticky="ew") + row += 1 - def draw_third_row(self): + # node type field + label = ttk.Label(frame, text="Type") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + combobox = ttk.Combobox( + frame, textvariable=self.type, values=list(DEFAULT_NODES), state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # server + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Server") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + servers = ["localhost"] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # services + button = ttk.Button(self.top, text="Services", command=self.click_services) + button.grid(sticky="ew", pady=PAD) + + self.draw_buttons() + + def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) From c43e5f999ca27948a2dee8b1b6e22e2dcd9895a9 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 10:59:30 -0800 Subject: [PATCH 130/305] added node config interface display and updated canvas nodes to use core node porotbuf directly for display and saving data --- coretk/coretk/canvasaction.py | 2 +- coretk/coretk/dialogs/emaneconfig.py | 14 ++-- coretk/coretk/dialogs/mobilityconfig.py | 5 +- coretk/coretk/dialogs/nodeconfig.py | 93 ++++++++++++++++++++++--- coretk/coretk/dialogs/wlanconfig.py | 11 ++- coretk/coretk/graph.py | 45 ++++++------ coretk/coretk/themes.py | 3 +- 7 files changed, 124 insertions(+), 49 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 5b9c08c3..08aa8c3d 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -30,7 +30,7 @@ class CanvasAction: def display_wlan_configuration(self, canvas_node): wlan_config = self.master.core.wlanconfig_management.configurations[ - canvas_node.core_id + canvas_node.core_node.id ] dialog = WlanConfigDialog( self.master, self.master, self.node_to_show_config, wlan_config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index d64e9089..2c885511 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -20,6 +20,7 @@ class EmaneConfiguration(Dialog): super().__init__(master, app, "emane configuration", modal=False) self.app = app self.canvas_node = canvas_node + self.node = canvas_node.core_node self.radiovar = tk.IntVar() self.radiovar.set(1) self.columnconfigure(0, weight=1) @@ -122,20 +123,15 @@ class EmaneConfiguration(Dialog): # add string emane_ infront for grpc call response = self.app.core.client.set_emane_model_config( - self.app.core.session_id, - self.canvas_node.core_id, - "emane_" + model_name, - config, + self.app.core.session_id, self.node.id, f"emane_{model_name}", config ) logging.info( - "emaneconfig.py config emane model (%s), result: %s", - self.canvas_node.core_id, - response, + "emaneconfig.py config emane model (%s), result: %s", self.node.id, response ) # store the change locally self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.canvas_node.core_id, "emane_" + model_name + self.node.id, f"emane_{model_name}" ) self.emane_model_dialog.destroy() @@ -161,7 +157,7 @@ class EmaneConfiguration(Dialog): session_id = self.app.core.session_id # add string emane_ before model name for grpc call response = self.app.core.client.get_emane_model_config( - session_id, self.canvas_node.core_id, "emane_" + model_name + session_id, self.node.id, f"emane_{model_name}" ) logging.info("emane model config %s", response) diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 1f53b4a0..2c20229a 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -19,9 +19,10 @@ class MobilityConfigDialog(Dialog): """ super().__init__(master, app, "ns2script configuration", modal=True) self.canvas_node = canvas_node + self.node = canvas_node.core_node logging.info(app.canvas.core.mobilityconfig_management.configurations) self.node_config = app.canvas.core.mobilityconfig_management.configurations[ - canvas_node.core_id + self.node.id ] self.mobility_script_parameters() @@ -208,7 +209,7 @@ class MobilityConfigDialog(Dialog): else: loop = "0" self.app.canvas.core.mobilityconfig_management.set_custom_configuration( - node_id=self.canvas_node.core_id, + node_id=self.node.id, file=file, refresh_ms=refresh_time, loop=loop, diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index a8252336..8a0d4522 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -1,14 +1,36 @@ +import logging import tkinter as tk +from functools import partial from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.widgets import FrameScroll DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} PAD = 5 +def mac_auto(is_auto, entry): + logging.info("mac auto clicked") + if is_auto.get(): + logging.info("disabling mac") + entry.var.set("") + entry.config(state=tk.DISABLED) + else: + entry.var.set("00:00:00:00:00:00") + entry.config(state=tk.NORMAL) + + +class InterfaceData: + def __init__(self, is_auto, mac, ip4, ip6): + self.is_auto = is_auto + self.mac = mac + self.ip4 = ip4 + self.ip6 = ip6 + + class NodeConfigDialog(Dialog): def __init__(self, master, app, canvas_node): """ @@ -18,13 +40,20 @@ class NodeConfigDialog(Dialog): :param coretk.app.Application: main app :param coretk.graph.CanvasNode canvas_node: canvas node object """ - super().__init__(master, app, f"{canvas_node.name} Configuration", modal=True) + super().__init__( + master, app, f"{canvas_node.core_node.name} Configuration", modal=True + ) self.canvas_node = canvas_node + self.node = canvas_node.core_node self.image = canvas_node.image self.image_button = None - self.name = tk.StringVar(value=canvas_node.name) - self.type = tk.StringVar(value=canvas_node.core_node.model) - self.server = tk.StringVar() + self.name = tk.StringVar(value=self.node.name) + self.type = tk.StringVar(value=self.node.model) + server = "localhost" + if self.node.server: + server = self.node.server + self.server = tk.StringVar(value=server) + self.interfaces = {} self.draw() def draw(self): @@ -82,8 +111,50 @@ class NodeConfigDialog(Dialog): button = ttk.Button(self.top, text="Services", command=self.click_services) button.grid(sticky="ew", pady=PAD) + # interfaces + if self.canvas_node.interfaces: + self.draw_interfaces() + self.draw_buttons() + def draw_interfaces(self): + scroll = FrameScroll(self.top, self.app, text="Interfaces") + scroll.grid(sticky="nsew") + scroll.frame.columnconfigure(0, weight=1) + scroll.frame.rowconfigure(0, weight=1) + for interface in self.canvas_node.interfaces: + logging.info("interface: %s", interface) + frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=PAD) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + + label = ttk.Label(frame, text="MAC") + label.grid(row=0, column=0, padx=PAD, pady=PAD) + is_auto = tk.BooleanVar(value=True) + checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) + checkbutton.var = is_auto + checkbutton.grid(row=0, column=1, padx=PAD) + mac = tk.StringVar(value=interface.mac) + entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) + entry.grid(row=0, column=2, sticky="ew") + func = partial(mac_auto, is_auto, entry) + checkbutton.config(command=func) + + label = ttk.Label(frame, text="IPv4") + label.grid(row=1, column=0, padx=PAD, pady=PAD) + ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") + entry = ttk.Entry(frame, textvariable=ip4) + entry.grid(row=1, column=1, columnspan=2, sticky="ew") + + label = ttk.Label(frame, text="IPv6") + label.grid(row=2, column=0, padx=PAD, pady=PAD) + ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") + entry = ttk.Entry(frame, textvariable=ip6) + entry.grid(row=2, column=1, columnspan=2, sticky="ew") + + self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -101,16 +172,20 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog( - self, self.app, self.canvas_node.name, self.canvas_node.image - ) + dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) dialog.show() if dialog.image: self.image = dialog.image self.image_button.config(image=self.image) def config_apply(self): - self.canvas_node.name = self.name.get() + # update core node + self.node.name = self.name.get() + + # update canvas node self.canvas_node.image = self.image - self.canvas_node.canvas.itemconfig(self.canvas_node.id, image=self.image) + + # redraw + self.canvas_node.redraw() + self.destroy() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 85a2a18f..4f213e68 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -13,13 +13,14 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node, config): super().__init__( - master, app, f"{canvas_node.name} Wlan Configuration", modal=True + master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True ) self.image = canvas_node.image self.canvas_node = canvas_node + self.node = canvas_node.core_node self.config = config - self.name = tk.StringVar(value=canvas_node.name) + self.name = tk.StringVar(value=self.node.name) self.range_var = tk.StringVar(value=config["range"]) self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) self.delay_var = tk.StringVar(value=config["delay"]) @@ -169,9 +170,7 @@ class WlanConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog( - self, self.app, self.canvas_node.name, self.canvas_node.image - ) + dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) dialog.show() if dialog.image: self.image = dialog.image @@ -192,7 +191,7 @@ class WlanConfigDialog(Dialog): # set wireless node configuration here wlanconfig_manager = self.app.core.wlanconfig_management wlanconfig_manager.set_custom_config( - node_id=self.canvas_node.core_id, + node_id=self.node.id, range=basic_range, bandwidth=bandwidth, jitter=jitter, diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 38d67079..3eb002e6 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -155,21 +155,22 @@ class CanvasGraph(tk.Canvas): # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) - position = core_node.position - node = CanvasNode(position.x, position.y, image, self.master, core_node) + node = CanvasNode(self.master, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node # draw existing links for link in session.links: canvas_node_one = self.core.canvas_nodes[link.node_one_id] + node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] + node_two = canvas_node_two.core_node is_wired = link.type == core_pb2.LinkType.WIRED edge = CanvasEdge( - canvas_node_one.x_coord, - canvas_node_one.y_coord, - canvas_node_two.x_coord, - canvas_node_two.y_coord, + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, canvas_node_one.id, self, is_wired=is_wired, @@ -402,7 +403,7 @@ class CanvasGraph(tk.Canvas): core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) - node = CanvasNode(x, y, self.node_draw.image, self.master, core_node) + node = CanvasNode(self.master, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node @@ -590,19 +591,18 @@ class CanvasEdge: class CanvasNode: - def __init__(self, x, y, image, app, core_node): - self.image = image + def __init__(self, app, core_node, image): self.app = app self.canvas = app.canvas + self.image = image + self.core_node = core_node + x = self.core_node.position.x + y = self.core_node.position.y self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) - self.core_node = core_node - self.name = core_node.name - self.x_coord = x - self.y_coord = y self.text_id = self.canvas.create_text( - x, y + 20, text=self.name, tags="nodename" + x, y + 20, text=self.core_node.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) @@ -620,6 +620,10 @@ class CanvasNode: self.wlans = [] self.moving = None + def redraw(self): + self.canvas.itemconfig(self.id, image=self.image) + self.canvas.itemconfig(self.text_id, text=self.core_node.name) + def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") @@ -640,18 +644,18 @@ class CanvasNode: self.canvas.canvas_action.display_configuration(self) def update_coords(self): - self.x_coord, self.y_coord = self.canvas.coords(self.id) - self.core_node.position.x = int(self.x_coord) - self.core_node.position.y = int(self.y_coord) + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) def click_press(self, event): - logging.debug(f"node click press {self.name}: {event}") + logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) self.canvas.canvas_management.node_select(self) def click_release(self, event): - logging.debug(f"node click release {self.name}: {event}") + logging.debug(f"node click release {self.core_node.name}: {event}") self.update_coords() self.moving = None @@ -681,7 +685,6 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, new_x, new_y) edge.link_info.recalculate_info() - # self.canvas.core_grpc.throughput_draw.update_throughtput_location(edge) self.canvas.helper.update_wlan_connection( old_x, old_y, new_x, new_y, self.wlans @@ -691,4 +694,4 @@ class CanvasNode: self.canvas.canvas_management.node_select(self, True) def context(self, event): - logging.debug(f"context click {self.name}: {event}") + logging.debug(f"context click {self.core_node.name}: {event}") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 0cf54eb8..267c3a3c 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -85,7 +85,8 @@ def load(style): "fieldbackground": Colors.white, "foreground": Colors.black, "padding": (2, 0), - } + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, }, "TCombobox": { "configure": { From 07c07da0996eb911e14e5c9ff2e6b783630b9bb0 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 11:20:08 -0800 Subject: [PATCH 131/305] update node config dialog to display fields based on node type, added field for nodes with images --- coretk/coretk/canvasaction.py | 3 +- coretk/coretk/coreclient.py | 6 +++- coretk/coretk/dialogs/nodeconfig.py | 56 +++++++++++++++++++---------- coretk/coretk/nodeutils.py | 16 +++++++-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py index 08aa8c3d..509b9723 100644 --- a/coretk/coretk/canvasaction.py +++ b/coretk/coretk/canvasaction.py @@ -5,6 +5,7 @@ from core.api.grpc import core_pb2 from coretk.dialogs.emaneconfig import EmaneConfiguration from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.nodeutils import NodeUtils class CanvasAction: @@ -16,7 +17,7 @@ class CanvasAction: def display_configuration(self, canvas_node): node_type = canvas_node.core_node.type self.node_to_show_config = canvas_node - if node_type == core_pb2.NodeType.DEFAULT: + if NodeUtils.is_container_node(node_type): self.display_node_configuration() elif node_type == core_pb2.NodeType.WIRELESS_LAN: self.display_wlan_configuration(canvas_node) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 14cc3a11..d9bbd659 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -440,12 +440,16 @@ class CoreClient: """ node_id = self.get_id() position = core_pb2.Position(x=x, y=y) + image = None + if NodeUtils.is_image_node(node_type): + image = "ubuntu:latest" node = core_pb2.Node( id=node_id, type=node_type, name=f"n{node_id}", model=model, position=position, + image=image, ) # set default configuration for wireless node @@ -530,7 +534,7 @@ class CoreClient: def create_interface(self, canvas_node): interface = None core_node = canvas_node.core_node - if NodeUtils.is_interface_node(core_node.type): + if NodeUtils.is_container_node(core_node.type): ifid = len(canvas_node.interfaces) name = f"eth{ifid}" interface = core_pb2.Interface( diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8a0d4522..4ed4baa6 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -6,9 +6,9 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.nodeutils import NodeUtils from coretk.widgets import FrameScroll -DEFAULT_NODES = {"router", "host", "PC", "mdr", "prouter"} PAD = 5 @@ -49,6 +49,7 @@ class NodeConfigDialog(Dialog): self.image_button = None self.name = tk.StringVar(value=self.node.name) self.type = tk.StringVar(value=self.node.model) + self.container_image = tk.StringVar(value=self.node.image) server = "localhost" if self.node.server: server = self.node.server @@ -86,26 +87,39 @@ class NodeConfigDialog(Dialog): row += 1 # node type field - label = ttk.Label(frame, text="Type") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - combobox = ttk.Combobox( - frame, textvariable=self.type, values=list(DEFAULT_NODES), state="readonly" - ) - combobox.grid(row=row, column=1, sticky="ew") - row += 1 + if NodeUtils.is_model_node(self.node.type): + label = ttk.Label(frame, text="Type") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + combobox = ttk.Combobox( + frame, + textvariable=self.type, + values=list(NodeUtils.NODE_MODELS), + state="readonly", + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 + + # container image field + if NodeUtils.is_image_node(self.node.type): + label = ttk.Label(frame, text="Image") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + entry = ttk.Entry(frame, textvariable=self.container_image) + entry.grid(row=row, column=1, sticky="ew") + row += 1 # server - frame.grid(sticky="ew") - frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="Server") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - servers = ["localhost"] - servers.extend(list(sorted(self.app.core.servers.keys()))) - combobox = ttk.Combobox( - frame, textvariable=self.server, values=servers, state="readonly" - ) - combobox.grid(row=row, column=1, sticky="ew") - row += 1 + if NodeUtils.is_container_node(self.node.type): + frame.grid(sticky="ew") + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Server") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + servers = ["localhost"] + servers.extend(list(sorted(self.app.core.servers.keys()))) + combobox = ttk.Combobox( + frame, textvariable=self.server, values=servers, state="readonly" + ) + combobox.grid(row=row, column=1, sticky="ew") + row += 1 # services button = ttk.Button(self.top, text="Services", command=self.click_services) @@ -181,6 +195,10 @@ class NodeConfigDialog(Dialog): def config_apply(self): # update core node self.node.name = self.name.get() + if NodeUtils.is_image_node(self.node.type): + self.node.image = self.container_image.get() + if NodeUtils.is_container_node(self.node.type): + self.node.server = self.server.get() # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 6777718a..380c9a0b 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -44,11 +44,21 @@ class NodeUtils: NODES = [] NETWORK_NODES = [] NODE_ICONS = {} - INTERFACE_NODE = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} + IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} + NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} @classmethod - def is_interface_node(cls, node_type): - return node_type in cls.INTERFACE_NODE + def is_container_node(cls, node_type): + return node_type in cls.CONTAINER_NODES + + @classmethod + def is_model_node(cls, node_type): + return node_type == NodeType.DEFAULT + + @classmethod + def is_image_node(cls, node_type): + return node_type in cls.IMAGE_NODES @classmethod def node_icon(cls, node_type, model): From 2e121a334412ec5c69178fb95a50996ed3e0e889 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 20 Nov 2019 16:52:02 -0800 Subject: [PATCH 132/305] service config, trying out some new icons --- coretk/coretk/coreclient.py | 26 ++++++++------ coretk/coretk/dialogs/nodeservice.py | 15 ++++++-- coretk/coretk/dialogs/serviceconfiguration.py | 17 +++++---- coretk/coretk/icons/host.png | Bin 0 -> 642 bytes coretk/coretk/icons/hub.png | Bin 0 -> 1108 bytes coretk/coretk/icons/lanswitch.png | Bin 0 -> 669 bytes coretk/coretk/icons/marker.png | Bin 0 -> 667 bytes coretk/coretk/icons/oval.png | Bin 0 -> 1236 bytes coretk/coretk/icons/pc.png | Bin 0 -> 517 bytes coretk/coretk/icons/rectangle.png | Bin 0 -> 157 bytes coretk/coretk/icons/rj45.png | Bin 0 -> 696 bytes coretk/coretk/icons/router.png | Bin 0 -> 1462 bytes coretk/coretk/icons/select.png | Bin 0 -> 348 bytes coretk/coretk/icons/start.png | Bin 0 -> 588 bytes coretk/coretk/icons/stop.png | Bin 0 -> 145 bytes coretk/coretk/icons/text.png | Bin 0 -> 146 bytes coretk/coretk/{icons => oldicons}/host.gif | Bin coretk/coretk/{icons => oldicons}/hub.gif | Bin .../coretk/{icons => oldicons}/lanswitch.gif | Bin coretk/coretk/{icons => oldicons}/oval.gif | Bin coretk/coretk/{icons => oldicons}/pc.gif | Bin .../coretk/{icons => oldicons}/rectangle.gif | Bin coretk/coretk/{icons => oldicons}/rj45.gif | Bin coretk/coretk/{icons => oldicons}/router.gif | Bin coretk/coretk/{icons => oldicons}/select.gif | Bin coretk/coretk/{icons => oldicons}/start.gif | Bin coretk/coretk/{icons => oldicons}/stop.gif | Bin coretk/coretk/{icons => oldicons}/text.gif | Bin coretk/coretk/servicenodeconfig.py | 33 ++++++++++++++++++ coretk/coretk/toolbar.py | 3 ++ 30 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 coretk/coretk/icons/host.png create mode 100644 coretk/coretk/icons/hub.png create mode 100644 coretk/coretk/icons/lanswitch.png create mode 100644 coretk/coretk/icons/marker.png create mode 100644 coretk/coretk/icons/oval.png create mode 100644 coretk/coretk/icons/pc.png create mode 100644 coretk/coretk/icons/rectangle.png create mode 100644 coretk/coretk/icons/rj45.png create mode 100644 coretk/coretk/icons/router.png create mode 100644 coretk/coretk/icons/select.png create mode 100644 coretk/coretk/icons/start.png create mode 100644 coretk/coretk/icons/stop.png create mode 100644 coretk/coretk/icons/text.png rename coretk/coretk/{icons => oldicons}/host.gif (100%) rename coretk/coretk/{icons => oldicons}/hub.gif (100%) rename coretk/coretk/{icons => oldicons}/lanswitch.gif (100%) rename coretk/coretk/{icons => oldicons}/oval.gif (100%) rename coretk/coretk/{icons => oldicons}/pc.gif (100%) rename coretk/coretk/{icons => oldicons}/rectangle.gif (100%) rename coretk/coretk/{icons => oldicons}/rj45.gif (100%) rename coretk/coretk/{icons => oldicons}/router.gif (100%) rename coretk/coretk/{icons => oldicons}/select.gif (100%) rename coretk/coretk/{icons => oldicons}/start.gif (100%) rename coretk/coretk/{icons => oldicons}/stop.gif (100%) rename coretk/coretk/{icons => oldicons}/text.gif (100%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 14cc3a11..9b486ccf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -123,6 +123,9 @@ class CoreClient: ) def join_session(self, session_id): + self.master.config(cursor="watch") + self.master.update() + # update session and title self.session_id = session_id self.master.title(f"CORE Session({self.session_id})") @@ -193,6 +196,7 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + self.master.config(cursor="") def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME @@ -296,7 +300,8 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) - # service_configs = self.get_service_config_proto() + service_configs = self.get_service_config_proto() + print(service_configs) # service_file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() @@ -313,7 +318,7 @@ class CoreClient: emane_config=emane_config, emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, - # service_configs=service_configs + service_configs=service_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -638,14 +643,15 @@ class CoreClient: service_configs, ) in self.serviceconfig_manager.configurations.items(): for service, config in service_configs.items(): - config = core_pb2.ServiceConfig( - node_id=node_id, - service=service, - startup=config.startup, - validate=config.validate, - shutdown=config.shutdown, - ) - configs.append(config) + if service in self.serviceconfig_manager.current_services[node_id]: + config = core_pb2.ServiceConfig( + node_id=node_id, + service=service, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config) return configs def get_service_file_config_proto(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 67d9a0b3..35c55b75 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -19,9 +19,11 @@ class NodeService(Dialog): self.current = None if services is None: services = set( - app.core.serviceconfig_manager.configurations[self.node_id].keys() + app.core.serviceconfig_manager.current_services[self.node_id] ) self.current_services = services + self.service_manager = self.app.core.serviceconfig_manager + self.service_file_manager = self.app.core.servicefileconfig_manager self.draw() def draw(self): @@ -76,9 +78,16 @@ class NodeService(Dialog): def service_clicked(self, name, var): if var.get() and name not in self.current_services: - self.current_services.add(name) + if self.service_manager.node_new_service_configuration(self.node_id, name): + self.current_services.add(name) + else: + for checkbutton in self.services.frame.winfo_children(): + if name == checkbutton.cget("text"): + checkbutton.config(variable=tk.BooleanVar(value=False)) + elif not var.get() and name in self.current_services: self.current_services.remove(name) + self.service_manager.current_services[self.node_id].remove(name) self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) @@ -90,7 +99,7 @@ class NodeService(Dialog): master=self, app=self.app, service_name=self.current.listbox.get(current_selection[0]), - canvas_node=self.canvas_node, + node_id=self.node_id, ) dialog.show() else: diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 195dc913..bb1fd858 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -11,11 +11,11 @@ from coretk.widgets import ListboxScroll class ServiceConfiguration(Dialog): - def __init__(self, master, app, service_name, canvas_node): + def __init__(self, master, app, service_name, node_id): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app - self.canvas_node = canvas_node - self.node_id = canvas_node.core_node.id + self.service_manager = app.core.serviceconfig_manager + self.node_id = node_id self.service_name = service_name self.radiovar = tk.IntVar() self.radiovar.set(2) @@ -51,16 +51,15 @@ class ServiceConfiguration(Dialog): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() # load data from local memory - serviceconfig_manager = self.app.core.serviceconfig_manager - if self.service_name in serviceconfig_manager.configurations[self.node_id]: - service_config = serviceconfig_manager.configurations[self.node_id][ + if self.service_name in self.service_manager.configurations[self.node_id]: + service_config = self.service_manager.configurations[self.node_id][ self.service_name ] else: - serviceconfig_manager.node_custom_service_configuration( + self.service_manager.node_custom_service_configuration( self.node_id, self.service_name ) - service_config = serviceconfig_manager.configurations[self.node_id][ + service_config = self.service_manager.configurations[self.node_id][ self.service_name ] self.dependencies = [x for x in service_config.dependencies] @@ -363,7 +362,7 @@ class ServiceConfiguration(Dialog): startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") - self.app.core.serviceconfig_manager.node_service_custom_configuration( + self.service_manager.node_service_custom_configuration( self.node_id, self.service_name, startup_commands, diff --git a/coretk/coretk/icons/host.png b/coretk/coretk/icons/host.png new file mode 100644 index 0000000000000000000000000000000000000000..e6efda088f82d791ba5e3122f707581e05644165 GIT binary patch literal 642 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/icons/rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..f8a9581ee1df0f3e5795b74658e731af2f23f45b GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}}HPZ!6K zjK;U;H*z*OFt8lFek|vZQSPBr_k0XQT6pK2)Y~n~%U-Q9ov({XW@ gD>xtnLwy-z)n8@{hR1x1fTlBey85}Sb4q9e09ofZqyPW_ literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/icons/rj45.png new file mode 100644 index 0000000000000000000000000000000000000000..baf4f9e80fb8632f29c5951d6ed6dbc32a1c5a98 GIT binary patch literal 696 zcmV;p0!RIcP)CJt(WY!s>`r^`w0ld50S0zSU5Da)CZ$p8R)W!aUq{-7&ru_QNO*_@KJhFH{MNp7G+xwy+Y z+QpxM$x&?5aA)xi5P*10FqgGhk{ig#$P*x1{0T5*Vw1$O7EAgG82OlFf#VfRdIQYj zE@wv8VmX8(B=0gG!V!x4E3xrv|5jDoU7t4jx(CxA-@I`rFJFO`wX0sO*IU{8@^bZ) zwwI_0aJ_ z29g#uK(c00&KW{&VDoi#)*%&=9x?z<)|}(w#+!q4+J4jq3^mUeB3>3KF0Dwf{+#b{hh*eq+R@WET@BR))-LqDhClCVvixQJ z;u+tv`L4*3{Eh5?v-Aekzr;Rn4pk41q%8deS_4N-w1J)NA?$1qlYWlj_)cI^Xbl`S et$~)7CFc*V=k<6^q$m~u0000b3C7hBeV5RHriLh{c1wR?Td-(=S4m4SE3w;XYOxmiS;|kf%kIt`AKF6Q znVq?_Z3&6}-u9ex&-u>Yx%ZrVFEEEW{J%jn;UpLd%43eqD=cXnBqpB*R1i=`zyf+W#3xi$K619cZ75$3 zc_mO436?sP^dSaz190XWd;%!~8XZg@wAZyK><+e_Ky^pWc4j&Zpv1~{&EnNyCiLFu z6>*&a@&3-*Q_Xhx2q)k>wofT5z10BXy(0FWA`iAzga80deGv@Y9}r~3M@OHz_wL0F z7c)ZUB1YBG>Uo@zPXeeCv3I*luxHg@A$vYy$LBw5opIO<-W=elsUKh4Jt03^tv zRz$0#w~drJ=QBh63GyUA!*H`Ig>xwl6R2vfTQX6ko(?eW^+)}NmEJG`2k`++zrxNW z3XpN^H&W;l2#3QG6YrRPPBy``D;NptHXU68J(WML1F+4jI(J@~!39n(etm!`^eGeDZO#wy_IXJ5uvZ%DYZ zn!zQzu)Sgn)-JXT3kGyEV6=#riv;!*d9Y{IZY(Nvi*#fc7>M4d%yE+Pi)7B*N`GhXtyn>PBJp{e=a7v2sH~fJSV20`I-ii>AH^l4?>cunCfC5>0&(p@Y8$ zJvZGNkW~3ROXl}YXMaCu8O+XkKx0kFr=-f~b+T-MFIIU8z#o?0o;FO_n`T&tI4E-C(zV`RW+xGMi9}7RNkrl;NNMRuvXquaB;I1D zaz_i(N=W2}+VpC168%m6>F3`mixebXr2y7i*^gz$Fg$u4FD_aE2BSHC62pIA7YPLd z4v#Lp`}P_C8KG_5{sTvQZHR#*5V*CQT+o6K95*K(I849!Y~=OC^m-&%qBv6@gJ~~- zf_!{DX23~e`a)CEA9ne>Mr;qCPHcbFUp{eTwiCcoo5Qa$@z*kO!|9sS!})TUk!RL$ zI4t$NbY%@OZDhbl#9ju<@%Vf_3Z@|d{VZuck}99~#VUJe;+b_0bNIjGU*+KI{#Xi` Q*8l(j07*qoM6N<$g0Q%$(EtDd literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/icons/select.png new file mode 100644 index 0000000000000000000000000000000000000000..9063d308a02c5693c78f9cf680d8a153bbcf7d36 GIT binary patch literal 348 zcmV-i0i*tjP)# z55NoPH5!SIN<`u(k+5s+Rm{jtWM+0Jv-egv=T6RdPUdFLP`=t|Rh&0o0q2;A2?TU7 z9RnETkW0+O1Ol!w7ZV7$#^Shup8_D@7Rxb#fP1XP1Ogth-UR3m0RmpI5fcdbz*bBk z;0rr-f$thXNDJEgpJb{62tx|!;aT#spVIvb7VtVs2fd1FJEJ713!LFD-FLCn6zJxX zpQN$FT&z6~V1VOXY!+`R@Q(Q=z$f+#;-|7Mj+y{v_j*(C7+uJ^*o^^f=YSg# uehylax$uWVziWwSc2W_bcR+r-7<`5MpOLY(tM5>z<9W>Gwf-lK?pMy9Ex!B0vJuUZJ|9SuW ze(=3}1OkCTAWKxYurX8~6fISOQ^0mlGyP&+iK?|VmrJ#!_og2i1sX%;!Bop@V8F?E z8>b$LKD_nm;!3A5GCd_)s$@Srz(7heX$s8(KiB5pK$&MT3gCa|yHPR)-30y%f; zfvmAEuw`@twawoJW?dk6;v~ge^F#*^*{<%0lND0C_fV|P&^LSml-n(J*RD1B;qw$K zQ>dN0oE_u*CDuoeQJlSpE+2Q&@8uR!rG5%mC+WL*337R>-TtmorO2N>M{#P7XzxC2 zwcfu2(f$JzZr&w#`iz}ED=mLRGqzHbv(nN7>GC_`Ygb5DK3eJXCm=%W;R4N>X=t~s z)p`+-)o;X=agwFy*1MexB(Gi)Umrv2U!3&26G#$T_wUf0zYUT)JI0BCeE!7uiBZzG zZ(P*>hIN5UWEYw0000#8 b_z*AuI)zzdTjAtdpot8gu6{1-oD!M<$_gx= literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/icons/text.png new file mode 100644 index 0000000000000000000000000000000000000000..4933dabbc40d0694ad46369a884685bb463f1f04 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjY)RhkE)4%caKYZ?lYt`co-U3d z9-YYv609r?%3>S;Uon~7>2#QbNzA6`M~(ZI^O^6MgAUfS2|ez3W+$;V;Yw2=cUv>- p4KLRmhVY-x0sj|h`?@PIFf7}|rg+lx(=(uv44$rjF6*2UngH*%FU0@= literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/host.gif b/coretk/coretk/oldicons/host.gif similarity index 100% rename from coretk/coretk/icons/host.gif rename to coretk/coretk/oldicons/host.gif diff --git a/coretk/coretk/icons/hub.gif b/coretk/coretk/oldicons/hub.gif similarity index 100% rename from coretk/coretk/icons/hub.gif rename to coretk/coretk/oldicons/hub.gif diff --git a/coretk/coretk/icons/lanswitch.gif b/coretk/coretk/oldicons/lanswitch.gif similarity index 100% rename from coretk/coretk/icons/lanswitch.gif rename to coretk/coretk/oldicons/lanswitch.gif diff --git a/coretk/coretk/icons/oval.gif b/coretk/coretk/oldicons/oval.gif similarity index 100% rename from coretk/coretk/icons/oval.gif rename to coretk/coretk/oldicons/oval.gif diff --git a/coretk/coretk/icons/pc.gif b/coretk/coretk/oldicons/pc.gif similarity index 100% rename from coretk/coretk/icons/pc.gif rename to coretk/coretk/oldicons/pc.gif diff --git a/coretk/coretk/icons/rectangle.gif b/coretk/coretk/oldicons/rectangle.gif similarity index 100% rename from coretk/coretk/icons/rectangle.gif rename to coretk/coretk/oldicons/rectangle.gif diff --git a/coretk/coretk/icons/rj45.gif b/coretk/coretk/oldicons/rj45.gif similarity index 100% rename from coretk/coretk/icons/rj45.gif rename to coretk/coretk/oldicons/rj45.gif diff --git a/coretk/coretk/icons/router.gif b/coretk/coretk/oldicons/router.gif similarity index 100% rename from coretk/coretk/icons/router.gif rename to coretk/coretk/oldicons/router.gif diff --git a/coretk/coretk/icons/select.gif b/coretk/coretk/oldicons/select.gif similarity index 100% rename from coretk/coretk/icons/select.gif rename to coretk/coretk/oldicons/select.gif diff --git a/coretk/coretk/icons/start.gif b/coretk/coretk/oldicons/start.gif similarity index 100% rename from coretk/coretk/icons/start.gif rename to coretk/coretk/oldicons/start.gif diff --git a/coretk/coretk/icons/stop.gif b/coretk/coretk/oldicons/stop.gif similarity index 100% rename from coretk/coretk/icons/stop.gif rename to coretk/coretk/oldicons/stop.gif diff --git a/coretk/coretk/icons/text.gif b/coretk/coretk/oldicons/text.gif similarity index 100% rename from coretk/coretk/icons/text.gif rename to coretk/coretk/oldicons/text.gif diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/servicenodeconfig.py index 98de9034..3473e1d8 100644 --- a/coretk/coretk/servicenodeconfig.py +++ b/coretk/coretk/servicenodeconfig.py @@ -2,15 +2,23 @@ service node configuration """ import logging +from tkinter import messagebox + +import grpc class ServiceNodeConfig: def __init__(self, app): self.app = app # dict(node_id:dict(service:node_service_config_proto)) + # maps node to all of its service configuration self.configurations = {} + # dict(node_id:set(str)) + # maps node to current configurations + self.current_services = {} self.default_services = {} + # todo rewrite, no need self.default services def node_default_services_configuration(self, node_id, node_model): """ set the default configurations for the default services of a node @@ -20,6 +28,7 @@ class ServiceNodeConfig: """ session_id = self.app.core.session_id client = self.app.core.client + if len(self.default_services) == 0: response = client.get_service_defaults(session_id) logging.info("session default services: %s", response) @@ -28,6 +37,7 @@ class ServiceNodeConfig: self.configurations[node_id] = {} + self.current_services[node_id] = set() for default in self.default_services[node_model]: response = client.get_node_service(session_id, node_id, default) logging.info( @@ -36,6 +46,29 @@ class ServiceNodeConfig: response, ) self.configurations[node_id][default] = response.service + self.current_services[node_id].add(default) + + def node_new_service_configuration(self, node_id, service_name): + """ + store node's configuration if a new service is added from the GUI + + :param int node_id: node id + :param str service_name: service name + :return: nothing + """ + try: + config = self.app.core.get_node_service(node_id, service_name) + except grpc.RpcError: + messagebox.showerror("Service problem", "Service not found") + return False + if node_id not in self.configurations: + self.configurations[node_id] = {} + if node_id not in self.current_services: + self.current_services[node_id] = set() + if service_name not in self.configurations[node_id]: + self.configurations[node_id][service_name] = config + self.current_services[node_id].add(service_name) + return True def node_custom_service_configuration(self, node_id, service_name): self.configurations[node_id][service_name] = self.app.core.get_node_service( diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index bb1803ea..834f7b39 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -196,9 +196,12 @@ class Toolbar(ttk.Frame): :return: nothing """ logging.debug("clicked start button") + self.master.config(cursor="watch") + self.master.update() self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() + self.master.config(cursor="") def click_link(self): logging.debug("Click LINK button") From 6035032e960617ce73143a66d5a0bf754342c9be Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 20 Nov 2019 23:16:04 -0800 Subject: [PATCH 133/305] removed wlan config class and canvas action class --- coretk/coretk/canvasaction.py | 45 -------- coretk/coretk/coreclient.py | 31 +++--- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/dialogs/wlanconfig.py | 166 +++------------------------- coretk/coretk/graph.py | 96 +++++++++------- coretk/coretk/widgets.py | 3 +- coretk/coretk/wlannodeconfig.py | 37 ------- 7 files changed, 92 insertions(+), 294 deletions(-) delete mode 100644 coretk/coretk/canvasaction.py delete mode 100644 coretk/coretk/wlannodeconfig.py diff --git a/coretk/coretk/canvasaction.py b/coretk/coretk/canvasaction.py deleted file mode 100644 index 509b9723..00000000 --- a/coretk/coretk/canvasaction.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -canvas graph action -""" -from core.api.grpc import core_pb2 -from coretk.dialogs.emaneconfig import EmaneConfiguration -from coretk.dialogs.nodeconfig import NodeConfigDialog -from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.nodeutils import NodeUtils - - -class CanvasAction: - def __init__(self, master, canvas): - self.master = master - self.canvas = canvas - self.node_to_show_config = None - - def display_configuration(self, canvas_node): - node_type = canvas_node.core_node.type - self.node_to_show_config = canvas_node - if NodeUtils.is_container_node(node_type): - self.display_node_configuration() - elif node_type == core_pb2.NodeType.WIRELESS_LAN: - self.display_wlan_configuration(canvas_node) - elif node_type == core_pb2.NodeType.EMANE: - self.display_emane_configuration() - - def display_node_configuration(self): - dialog = NodeConfigDialog(self.master, self.master, self.node_to_show_config) - dialog.show() - self.node_to_show_config = None - - def display_wlan_configuration(self, canvas_node): - wlan_config = self.master.core.wlanconfig_management.configurations[ - canvas_node.core_node.id - ] - dialog = WlanConfigDialog( - self.master, self.master, self.node_to_show_config, wlan_config - ) - dialog.show() - self.node_to_show_config = None - - def display_emane_configuration(self): - app = self.canvas.core.app - dialog = EmaneConfiguration(self.master, app, self.node_to_show_config) - dialog.show() diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d9bbd659..44b8e38a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -12,7 +12,6 @@ from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig -from coretk.wlannodeconfig import WlanNodeConfig OBSERVERS = { "processes": "ps", @@ -70,7 +69,7 @@ class CoreClient: self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager() - self.wlanconfig_management = WlanNodeConfig() + self.wlan_configs = {} self.mobilityconfig_management = MobilityNodeConfig() self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None @@ -133,7 +132,7 @@ class CoreClient: self.canvas_nodes.clear() self.links.clear() self.hooks.clear() - self.wlanconfig_management.configurations.clear() + self.wlan_configs.clear() self.mobilityconfig_management.configurations.clear() self.emane_config = None @@ -155,9 +154,7 @@ class CoreClient: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) logging.debug("wlan config(%s): %s", node.id, response) - node_config = response.config - config = {x: node_config[x].value for x in node_config} - self.wlanconfig_management.configurations[node.id] = config + self.wlan_configs[node.id] = response.config # get mobility configs response = self.client.get_mobility_configs(self.session_id) @@ -453,7 +450,6 @@ class CoreClient: ) # set default configuration for wireless node - self.wlanconfig_management.set_default_config(node_type, node_id) self.mobilityconfig_management.set_default_configuration(node_type, node_id) # set default emane configuration for emane node @@ -520,8 +516,8 @@ class CoreClient: for i in node_ids: if i in self.mobilityconfig_management.configurations: self.mobilityconfig_management.configurations.pop(i) - if i in self.wlanconfig_management.configurations: - self.wlanconfig_management.configurations.pop(i) + if i in self.wlan_configs: + del self.wlan_configs[i] # delete emane configurations for i in node_interface_pairs: @@ -606,11 +602,10 @@ class CoreClient: def get_wlan_configs_proto(self): configs = [] - wlan_configs = self.wlanconfig_management.configurations - for node_id in wlan_configs: - config = wlan_configs[node_id] - config_proto = core_pb2.WlanConfig(node_id=node_id, config=config) - configs.append(config_proto) + for node_id, config in self.wlan_configs.items(): + config = {x: config[x].value for x in config} + wlan_config = core_pb2.WlanConfig(node_id=node_id, config=config) + configs.append(wlan_config) return configs def get_mobility_configs_proto(self): @@ -669,3 +664,11 @@ class CoreClient: def run(self, node_id): logging.info("running node(%s) cmd: %s", node_id, self.observer) return self.client.node_command(self.session_id, node_id, self.observer).output + + def get_wlan_config(self, node_id): + config = self.wlan_configs.get(node_id) + if not config: + response = self.client.get_wlan_config(self.session_id, node_id) + config = response.config + self.wlan_configs[node_id] = config + return config diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 4ed4baa6..8b9fcfab 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -107,8 +107,8 @@ class NodeConfigDialog(Dialog): entry.grid(row=row, column=1, sticky="ew") row += 1 - # server if NodeUtils.is_container_node(self.node.type): + # server frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") @@ -121,9 +121,9 @@ class NodeConfigDialog(Dialog): combobox.grid(row=row, column=1, sticky="ew") row += 1 - # services - button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PAD) + # services + button = ttk.Button(self.top, text="Services", command=self.click_services) + button.grid(sticky="ew", pady=PAD) # interfaces if self.canvas_node.interfaces: diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 4f213e68..3973efb5 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -2,152 +2,33 @@ wlan configuration """ -import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.widgets import ConfigFrame + +PAD = 5 class WlanConfigDialog(Dialog): - def __init__(self, master, app, canvas_node, config): + def __init__(self, master, app, canvas_node): super().__init__( master, app, f"{canvas_node.core_node.name} Wlan Configuration", modal=True ) - self.image = canvas_node.image self.canvas_node = canvas_node self.node = canvas_node.core_node - self.config = config - - self.name = tk.StringVar(value=self.node.name) - self.range_var = tk.StringVar(value=config["range"]) - self.bandwidth_var = tk.StringVar(value=config["bandwidth"]) - self.delay_var = tk.StringVar(value=config["delay"]) - self.loss_var = tk.StringVar(value=config["error"]) - self.jitter_var = tk.StringVar(value=config["jitter"]) - self.ip4_subnet = tk.StringVar() - self.ip6_subnet = tk.StringVar() - self.image_button = None + self.config_frame = None + self.config = self.app.core.get_wlan_config(self.node.id) self.draw() def draw(self): self.top.columnconfigure(0, weight=1) - self.draw_name_config() - self.draw_wlan_config() - self.draw_subnet() - self.draw_wlan_buttons() + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() - def draw_name_config(self): - """ - draw image modification part - - :return: nothing - """ - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - frame.columnconfigure(0, weight=1) - - entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=0, padx=2, sticky="ew") - - self.image_button = ttk.Button(frame, image=self.image, command=self.click_icon) - self.image_button.grid(row=0, column=1, padx=3) - - def draw_wlan_config(self): - """ - create wireless configuration table - - :return: nothing - """ - label = ttk.Label(self.top, text="Wireless") - label.grid(sticky="w", pady=2) - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - - label = ttk.Label( - frame, - text=( - "The basic range model calculates on/off " - "connectivity based on pixel distance between nodes." - ), - ) - label.grid(row=0, columnspan=2, pady=2, sticky="ew") - - label = ttk.Label(frame, text="Range") - label.grid(row=1, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.range_var) - entry.grid(row=1, column=1, sticky="ew") - - label = ttk.Label(frame, text="Bandwidth (bps)") - label.grid(row=2, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.bandwidth_var) - entry.grid(row=2, column=1, sticky="ew") - - label = ttk.Label(frame, text="Delay (us)") - label.grid(row=3, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.delay_var) - entry.grid(row=3, column=1, sticky="ew") - - label = ttk.Label(frame, text="Loss (%)") - label.grid(row=4, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.loss_var) - entry.grid(row=4, column=1, sticky="ew") - - label = ttk.Label(frame, text="Jitter (us)") - label.grid(row=5, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.jitter_var) - entry.grid(row=5, column=1, sticky="ew") - - def draw_subnet(self): - """ - create the entries for ipv4 subnet and ipv6 subnet - - :return: nothing - """ - - frame = ttk.Frame(self.top) - frame.grid(pady=3, sticky="ew") - frame.columnconfigure(1, weight=1) - frame.columnconfigure(3, weight=1) - - label = ttk.Label(frame, text="IPv4 Subnet") - label.grid(row=0, column=0, sticky="w") - entry = ttk.Entry(frame, textvariable=self.ip4_subnet) - entry.grid(row=0, column=1, sticky="ew") - - label = ttk.Label(frame, text="IPv6 Subnet") - label.grid(row=0, column=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.ip6_subnet) - entry.grid(row=0, column=3, sticky="ew") - - def draw_wlan_buttons(self): - """ - create wireless node options - - :return: - """ - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") - for i in range(3): - frame.columnconfigure(i, weight=1) - - button = ttk.Button( - frame, text="ns-2 mobility script...", command=self.click_mobility - ) - button.grid(row=0, column=0, padx=2, sticky="ew") - - button = ttk.Button(frame, text="Link to all routers") - button.grid(row=0, column=1, padx=2, sticky="ew") - - button = ttk.Button(frame, text="Choose WLAN members") - button.grid(row=0, column=2, padx=2, sticky="ew") - def draw_apply_buttons(self): """ create node configuration options @@ -160,42 +41,21 @@ class WlanConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=2, sticky="ew") + button.grid(row=0, column=0, padx=PAD, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, padx=2, sticky="ew") + button.grid(row=0, column=1, sticky="ew") def click_mobility(self): dialog = MobilityConfigDialog(self, self.app, self.canvas_node) dialog.show() - def click_icon(self): - dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) - dialog.show() - if dialog.image: - self.image = dialog.image - self.image_button.config(image=self.image) - def click_apply(self): """ retrieve user's wlan configuration and store the new configuration values :return: nothing """ - basic_range = self.range_var.get() - bandwidth = self.bandwidth_var.get() - delay = self.delay_var.get() - loss = self.loss_var.get() - jitter = self.jitter_var.get() - - # set wireless node configuration here - wlanconfig_manager = self.app.core.wlanconfig_management - wlanconfig_manager.set_custom_config( - node_id=self.node.id, - range=basic_range, - bandwidth=bandwidth, - jitter=jitter, - delay=delay, - error=loss, - ) + self.config_frame.parse_config() + self.app.core.wlan_configs[self.node.id] = self.config self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3eb002e6..86ce617e 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -5,8 +5,11 @@ import tkinter as tk from PIL import ImageTk from core.api.grpc import core_pb2 -from coretk.canvasaction import CanvasAction +from core.api.grpc.core_pb2 import NodeType from coretk.canvastooltip import CanvasTooltip +from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput @@ -38,24 +41,21 @@ class CanvasGraph(tk.Canvas): kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) self.mode = GraphMode.SELECT - self.node_draw = None self.selected = None - self.node_context = None + self.node_draw = None + self.context = None self.nodes = {} self.edges = {} self.drawing_edge = None self.grid = None self.meters_per_pixel = 1.5 self.canvas_management = CanvasComponentManagement(self, core) - self.canvas_action = CanvasAction(master, self) - self.setup_menus() self.setup_bindings() self.draw_grid() self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) self.wireless_draw = WirelessConnection(self, core) - self.is_node_context_opened = False # background related self.wallpaper_id = None @@ -66,21 +66,28 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) - def setup_menus(self): - self.node_context = tk.Menu(self.master) - self.node_context.add_command( - label="Configure", command=self.canvas_action.display_node_configuration - ) - self.node_context.add_command(label="Select adjacent") - self.node_context.add_command(label="Create link to") - self.node_context.add_command(label="Assign to") - self.node_context.add_command(label="Move to") - self.node_context.add_command(label="Cut") - self.node_context.add_command(label="Copy") - self.node_context.add_command(label="Paste") - self.node_context.add_command(label="Delete") - self.node_context.add_command(label="Hide") - self.node_context.add_command(label="Services") + def create_node_context(self, canvas_node): + node = canvas_node.core_node + context = tk.Menu(self.master) + context.add_command(label="Configure", command=canvas_node.show_config) + if node.type == NodeType.WIRELESS_LAN: + context.add_command( + label="WLAN Config", command=canvas_node.show_wlan_config + ) + context.add_command( + label="Mobility Config", command=canvas_node.show_mobility_config + ) + context.add_command(label="Select adjacent", state=tk.DISABLED) + context.add_command(label="Create link to", state=tk.DISABLED) + context.add_command(label="Assign to", state=tk.DISABLED) + context.add_command(label="Move to", state=tk.DISABLED) + context.add_command(label="Cut", state=tk.DISABLED) + context.add_command(label="Copy", state=tk.DISABLED) + context.add_command(label="Paste", state=tk.DISABLED) + context.add_command(label="Delete", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + return context def reset_and_redraw(self, session): """ @@ -97,7 +104,6 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.SELECT self.node_draw = None self.selected = None - self.node_context = None self.nodes.clear() self.edges.clear() self.drawing_edge = None @@ -112,7 +118,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.context) + self.bind("", self.click_context) self.bind("", self.press_delete) def draw_grid(self, width=1000, height=800): @@ -254,9 +260,9 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - if self.is_node_context_opened: - self.node_context.unpost() - self.is_node_context_opened = False + if self.context: + self.context.unpost() + self.context = None else: self.focus_set() self.selected = self.get_selected(event) @@ -350,18 +356,18 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) - def context(self, event): - if not self.is_node_context_opened: + def click_context(self, event): + logging.info("context event: %s", self.context) + if not self.context: selected = self.get_selected(event) - nodes = self.find_withtag("node") - if selected in nodes: + canvas_node = self.nodes.get(selected) + if canvas_node: logging.debug(f"node context: {selected}") - self.node_context.post(event.x_root, event.y_root) - self.canvas_action.node_to_show_config = self.nodes[selected] - self.is_node_context_opened = True + self.context = self.create_node_context(canvas_node) + self.context.post(event.x_root, event.y_root) else: - self.node_context.unpost() - self.is_node_context_opened = False + self.context.unpost() + self.context = None # TODO rather than delete, might move the data to somewhere else in order to reuse # TODO when the user undo @@ -443,7 +449,6 @@ class CanvasGraph(tk.Canvas): """ place the image at the center of canvas - :param Image img: image object :return: nothing """ tk_img = ImageTk.PhotoImage(self.wallpaper) @@ -609,7 +614,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) - self.canvas.tag_bind(self.id, "", self.context) self.canvas.tag_bind(self.id, "", self.double_click) self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) @@ -641,7 +645,7 @@ class CanvasNode: if self.app.core.is_runtime(): self.canvas.core.launch_terminal(self.core_node.id) else: - self.canvas.canvas_action.display_configuration(self) + self.show_config() def update_coords(self): x, y = self.canvas.coords(self.id) @@ -693,5 +697,17 @@ class CanvasNode: def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) - def context(self, event): - logging.debug(f"context click {self.core_node.name}: {event}") + def show_config(self): + self.canvas.context = None + dialog = NodeConfigDialog(self.app, self.app, self) + dialog.show() + + def show_wlan_config(self): + self.canvas.context = None + dialog = WlanConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_config(self): + self.canvas.context = None + dialog = MobilityConfigDialog(self.app, self.app, self) + dialog.show() diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index e825fdb4..bd6904b2 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -15,6 +15,7 @@ INT_TYPES = { core_pb2.ConfigOptionType.INT32, core_pb2.ConfigOptionType.INT64, } +PAD = 5 class FrameScroll(ttk.LabelFrame): @@ -72,7 +73,7 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame) + frame = ttk.Frame(self.frame, padding=PAD) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): diff --git a/coretk/coretk/wlannodeconfig.py b/coretk/coretk/wlannodeconfig.py deleted file mode 100644 index 1ebfb771..00000000 --- a/coretk/coretk/wlannodeconfig.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -wireless node configuration for all the wireless node -""" -from collections import OrderedDict - -from core.api.grpc import core_pb2 - - -class WlanNodeConfig: - def __init__(self): - # maps node id to wlan configuration - self.configurations = {} - - def set_default_config(self, node_type, node_id): - if node_type == core_pb2.NodeType.WIRELESS_LAN: - config = OrderedDict() - config["range"] = "275" - config["bandwidth"] = "54000000" - config["jitter"] = "0" - config["delay"] = "20000" - config["error"] = "0" - self.configurations[node_id] = config - - def set_custom_config(self, node_id, range, bandwidth, jitter, delay, error): - self.configurations[node_id]["range"] = range - self.configurations[node_id]["bandwidth"] = bandwidth - self.configurations[node_id]["jitter"] = jitter - self.configurations[node_id]["delay"] = delay - self.configurations[node_id]["error"] = error - - def delete_node_config(self, node_id): - """ - not implemented - :param node_id: - :return: - """ - return From eced9863ad488928024edecbc839728577a4600d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 09:40:57 -0800 Subject: [PATCH 134/305] removed mobility config class to try and help simplify saving configs --- coretk/coretk/coreclient.py | 32 +-- coretk/coretk/dialogs/mobilityconfig.py | 250 +++--------------------- coretk/coretk/dialogs/wlanconfig.py | 5 - coretk/coretk/mobilitynodeconfig.py | 75 ------- 4 files changed, 48 insertions(+), 314 deletions(-) delete mode 100644 coretk/coretk/mobilitynodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index fed8040a..93ebba4c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -8,7 +8,6 @@ from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager -from coretk.mobilitynodeconfig import MobilityNodeConfig from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig from coretk.servicenodeconfig import ServiceNodeConfig @@ -70,7 +69,7 @@ class CoreClient: self.preexisting = set() self.interfaces_manager = InterfaceManager() self.wlan_configs = {} - self.mobilityconfig_management = MobilityNodeConfig() + self.mobility_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) @@ -136,7 +135,7 @@ class CoreClient: self.links.clear() self.hooks.clear() self.wlan_configs.clear() - self.mobilityconfig_management.configurations.clear() + self.mobility_configs.clear() self.emane_config = None # get session data @@ -164,8 +163,7 @@ class CoreClient: logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config - config = {x: node_config[x].value for x in node_config} - self.mobilityconfig_management.configurations[node_id] = config + self.mobility_configs[node_id] = node_config # get emane config response = self.client.get_emane_config(self.session_id) @@ -454,9 +452,6 @@ class CoreClient: image=image, ) - # set default configuration for wireless node - self.mobilityconfig_management.set_default_configuration(node_type, node_id) - # set default emane configuration for emane node if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(node_id) @@ -519,8 +514,8 @@ class CoreClient: # delete any mobility configuration, wlan configuration for i in node_ids: - if i in self.mobilityconfig_management.configurations: - self.mobilityconfig_management.configurations.pop(i) + if i in self.mobility_configs: + del self.mobility_configs[i] if i in self.wlan_configs: del self.wlan_configs[i] @@ -615,11 +610,10 @@ class CoreClient: def get_mobility_configs_proto(self): configs = [] - mobility_configs = self.mobilityconfig_management.configurations - for node_id in mobility_configs: - config = mobility_configs[node_id] - config_proto = core_pb2.MobilityConfig(node_id=node_id, config=config) - configs.append(config_proto) + for node_id, config in self.mobility_configs.items(): + config = {x: config[x].value for x in config} + mobility_config = core_pb2.MobilityConfig(node_id=node_id, config=config) + configs.append(mobility_config) return configs def get_emane_model_configs_proto(self): @@ -678,3 +672,11 @@ class CoreClient: config = response.config self.wlan_configs[node_id] = config return config + + def get_mobility_config(self, node_id): + config = self.mobility_configs.get(node_id) + if not config: + response = self.client.get_mobility_config(self.session_id, node_id) + config = response.config + self.mobility_configs[node_id] = config + return config diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 2c20229a..21782ee6 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -1,236 +1,48 @@ """ mobility configuration """ -import logging -import tkinter as tk -from tkinter import filedialog, ttk +from tkinter import ttk -from coretk import appconfig from coretk.dialogs.dialog import Dialog +from coretk.widgets import ConfigFrame + +PAD = 5 class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): - """ - create an instance of mobility configuration - - :param app: core application - :param root.master master: - """ - super().__init__(master, app, "ns2script configuration", modal=True) + super().__init__( + master, + app, + f"{canvas_node.core_node.name} Mobility Configuration", + modal=True, + ) self.canvas_node = canvas_node self.node = canvas_node.core_node - logging.info(app.canvas.core.mobilityconfig_management.configurations) - self.node_config = app.canvas.core.mobilityconfig_management.configurations[ - self.node.id - ] + self.config_frame = None + self.config = self.app.core.get_mobility_config(self.node.id) + self.draw() - self.mobility_script_parameters() - self.ns2script_options() - self.loop = "On" + def draw(self): + self.top.columnconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_apply_buttons() - def create_string_var(self, val): - """ - create string variable for entry widget + def draw_apply_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) - :return: nothing - """ - var = tk.StringVar() - var.set(val) - return var + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PAD, sticky="ew") - def open_file(self, entry): - filename = filedialog.askopenfilename( - initialdir=str(appconfig.MOBILITY_PATH), title="Open" - ) - if filename: - entry.delete(0, tk.END) - entry.insert(0, filename) - - def set_loop_value(self, value): - """ - set loop value when user changes the option - :param value: - :return: - """ - self.loop = value - - def create_label_entry_filebrowser( - self, parent_frame, text_label, entry_text, filebrowser=False - ): - f = ttk.Frame(parent_frame) - lbl = ttk.Label(f, text=text_label) - lbl.grid(padx=3, pady=3) - # f.grid() - e = ttk.Entry(f, textvariable=self.create_string_var(entry_text)) - e.grid(row=0, column=1, padx=3, pady=3) - if filebrowser: - b = ttk.Button(f, text="...", command=lambda: self.open_file(e)) - b.grid(row=0, column=2, padx=3, pady=3) - f.grid(sticky=tk.E) - - def mobility_script_parameters(self): - lbl = ttk.Label(self.top, text="node ns2script") - lbl.grid(sticky="ew") - - sb = ttk.Scrollbar(self.top, orient=tk.VERTICAL) - sb.grid(row=1, column=1, sticky="ns") - - f = ttk.Frame(self.top) - lbl = ttk.Label(f, text="ns-2 Mobility Scripts Parameters") - lbl.grid(row=0, column=0, sticky=tk.W) - - f1 = tk.Canvas( - f, - yscrollcommand=sb.set, - bg="#d9d9d9", - relief=tk.RAISED, - highlightbackground="#b3b3b3", - highlightcolor="#b3b3b3", - highlightthickness=0.5, - bd=0, - ) - self.create_label_entry_filebrowser( - f1, "mobility script file", self.node_config["file"], filebrowser=True - ) - self.create_label_entry_filebrowser( - f1, "Refresh time (ms)", self.node_config["refresh_ms"] - ) - - # f12 = ttk.Frame(f1) - # - # lbl = ttk.Label(f12, text="Refresh time (ms)") - # lbl.grid() - # - # e = ttk.Entry(f12, textvariable=self.create_string_var("50")) - # e.grid(row=0, column=1) - # f12.grid() - - f13 = ttk.Frame(f1) - - lbl = ttk.Label(f13, text="loop") - lbl.grid() - - om = ttk.OptionMenu( - f13, self.create_string_var("On"), "On", "Off", command=self.set_loop_value - ) - om.grid(row=0, column=1) - - f13.grid(sticky=tk.E) - - self.create_label_entry_filebrowser( - f1, "auto-start seconds (0.0 for runtime)", self.node_config["autostart"] - ) - # f14 = ttk.Frame(f1) - # - # lbl = ttk.Label(f14, text="auto-start seconds (0.0 for runtime)") - # lbl.grid() - # - # e = ttk.Entry(f14, textvariable=self.create_string_var("")) - # e.grid(row=0, column=1) - # - # f14.grid() - self.create_label_entry_filebrowser( - f1, "node mapping (optional, e.g. 0:1, 1:2, 2:3)", self.node_config["map"] - ) - # f15 = ttk.Frame(f1) - # - # lbl = ttk.Label(f15, text="node mapping (optional, e.g. 0:1, 1:2, 2:3)") - # lbl.grid() - # - # e = ttk.Entry(f15, textvariable=self.create_string_var("")) - # e.grid(row=0, column=1) - # - # f15.grid() - - self.create_label_entry_filebrowser( - f1, - "script file to run upon start", - self.node_config["script_start"], - filebrowser=True, - ) - self.create_label_entry_filebrowser( - f1, - "script file to run upon pause", - self.node_config["script_pause"], - filebrowser=True, - ) - self.create_label_entry_filebrowser( - f1, - "script file to run upon stop", - self.node_config["script_stop"], - filebrowser=True, - ) - f1.grid() - sb.config(command=f1.yview) - f.grid(row=1, column=0) - - def ns2script_apply(self): - """ - - :return: - """ - config_frame = self.grid_slaves(row=1, column=0)[0] - canvas = config_frame.grid_slaves(row=1, column=0)[0] - file = ( - canvas.grid_slaves(row=0, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - refresh_time = ( - canvas.grid_slaves(row=1, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - auto_start_seconds = ( - canvas.grid_slaves(row=3, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - node_mapping = ( - canvas.grid_slaves(row=4, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - file_upon_start = ( - canvas.grid_slaves(row=5, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - file_upon_pause = ( - canvas.grid_slaves(row=6, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - file_upon_stop = ( - canvas.grid_slaves(row=7, column=0)[0].grid_slaves(row=0, column=1)[0].get() - ) - - # print("mobility script file: ", file) - # print("refresh time: ", refresh_time) - # print("auto start seconds: ", auto_start_seconds) - # print("node mapping: ", node_mapping) - # print("script file to run upon start: ", file_upon_start) - # print("file upon pause: ", file_upon_pause) - # print("file upon stop: ", file_upon_stop) - if self.loop == "On": - loop = "1" - else: - loop = "0" - self.app.canvas.core.mobilityconfig_management.set_custom_configuration( - node_id=self.node.id, - file=file, - refresh_ms=refresh_time, - loop=loop, - autostart=auto_start_seconds, - node_mapping=node_mapping, - script_start=file_upon_start, - script_pause=file_upon_pause, - script_stop=file_upon_stop, - ) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + def click_apply(self): + self.config_frame.parse_config() + self.app.core.mobility_configs[self.node.id] = self.config self.destroy() - - def ns2script_options(self): - """ - create the options for ns2script configuration - - :return: nothing - """ - f = ttk.Frame(self.top) - b = ttk.Button(f, text="Apply", command=self.ns2script_apply) - b.grid() - b = ttk.Button(f, text="Cancel", command=self.destroy) - b.grid(row=0, column=1) - f.grid() diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 3973efb5..3c118834 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -5,7 +5,6 @@ wlan configuration from tkinter import ttk from coretk.dialogs.dialog import Dialog -from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.widgets import ConfigFrame PAD = 5 @@ -46,10 +45,6 @@ class WlanConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_mobility(self): - dialog = MobilityConfigDialog(self, self.app, self.canvas_node) - dialog.show() - def click_apply(self): """ retrieve user's wlan configuration and store the new configuration values diff --git a/coretk/coretk/mobilitynodeconfig.py b/coretk/coretk/mobilitynodeconfig.py deleted file mode 100644 index 4a94f573..00000000 --- a/coretk/coretk/mobilitynodeconfig.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -mobility configurations for all the nodes -""" - -import logging -from collections import OrderedDict - -from core.api.grpc import core_pb2 - - -class MobilityNodeConfig: - def __init__(self): - """ - create an instance of MobilityConfig object - """ - # dict that maps node id to mobility configuration - self.configurations = {} - - def set_default_configuration(self, node_type, node_id): - """ - set default mobility configuration for a node - - :param core_pb2.NodeType node_type: protobuf node type - :param int node_id: node id - :return: nothing - """ - if node_type == core_pb2.NodeType.WIRELESS_LAN: - config = OrderedDict() - config["autostart"] = "" - config["file"] = "" - config["loop"] = "1" - config["map"] = "" - config["refresh_ms"] = "50" - config["script_pause"] = "" - config["script_start"] = "" - config["script_stop"] = "" - self.configurations[node_id] = config - - def set_custom_configuration( - self, - node_id, - file, - refresh_ms, - loop, - autostart, - node_mapping, - script_start, - script_pause, - script_stop, - ): - """ - set custom mobility configuration for a node - - :param int node_id: node id - :param str file: path to mobility script file - :param str refresh_ms: refresh time - :param str loop: loop option - :param str autostart: auto-start seconds value - :param str node_mapping: node mapping - :param str script_start: path to script to run upon start - :param str script_pause: path to script to run upon pause - :param str script_stop: path to script to run upon stop - :return: nothing - """ - if node_id in self.configurations: - self.configurations[node_id]["autostart"] = autostart - self.configurations[node_id]["file"] = file - self.configurations[node_id]["loop"] = loop - self.configurations[node_id]["map"] = node_mapping - self.configurations[node_id]["refresh_ms"] = refresh_ms - self.configurations[node_id]["script_pause"] = script_pause - self.configurations[node_id]["script_start"] = script_start - self.configurations[node_id]["script_stop"] = script_stop - else: - logging.error("mobilitynodeconfig.py invalid node_id") From 9445b63bd2eb7ea44c2b45d39a49a9f2c576aa54 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:29:33 -0800 Subject: [PATCH 135/305] removed saving default configurations for wlan and mobility by default, updated session.add_node to set default configurations for wlan and emane --- coretk/coretk/coreclient.py | 2 -- daemon/core/api/grpc/grpcutils.py | 2 ++ daemon/core/api/grpc/server.py | 4 ---- daemon/core/emulator/emudata.py | 1 + daemon/core/emulator/session.py | 9 ++++++++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 93ebba4c..beeb1c0a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -670,7 +670,6 @@ class CoreClient: if not config: response = self.client.get_wlan_config(self.session_id, node_id) config = response.config - self.wlan_configs[node_id] = config return config def get_mobility_config(self, node_id): @@ -678,5 +677,4 @@ class CoreClient: if not config: response = self.client.get_mobility_config(self.session_id, node_id) config = response.config - self.mobility_configs[node_id] = config return config diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c6b625fb..cf1250c8 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -30,6 +30,8 @@ def add_node_data(node_proto): options.opaque = node_proto.opaque options.image = node_proto.image options.services = node_proto.services + if node_proto.emane: + options.emane = node_proto.emane if node_proto.server: options.server = node_proto.server diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 8c987371..79053eee 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -713,10 +713,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session = self.get_session(request.session_id, context) _type, _id, options = grpcutils.add_node_data(request.node) node = session.add_node(_type=_type, _id=_id, options=options) - # configure emane if provided - emane_model = request.node.emane - if emane_model: - session.emane.set_model_config(id, emane_model) return core_pb2.AddNodeResponse(node_id=node.id) def GetNode(self, request, context): diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py index 5e59eaae..2a8f9bf6 100644 --- a/daemon/core/emulator/emudata.py +++ b/daemon/core/emulator/emudata.py @@ -89,6 +89,7 @@ class NodeOptions: self.emulation_id = None self.server = None self.image = image + self.emane = None def set_position(self, x, y): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cba99e8e..4852d026 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -30,7 +30,7 @@ from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError from core.location.corelocation import CoreLocation from core.location.event import EventLoop -from core.location.mobility import MobilityManager +from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase from core.nodes.docker import DockerNode from core.nodes.ipaddress import MacAddress @@ -703,6 +703,13 @@ class Session: logging.debug("set node type: %s", node.type) self.services.add_services(node, node.type, options.services) + # ensure default emane configuration + if _type == NodeTypes.EMANE: + self.emane.set_model_config(_id, node.emane) + # set default wlan config if needed + if _type == NodeTypes.WIRELESS_LAN: + self.mobility.set_model_config(_id, BasicRangeModel.name) + # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) if self.state == EventTypes.RUNTIME_STATE.value and is_boot_node: From e059f8952098c94ec37e3c93d2b5afa1711d0b32 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:33:43 -0800 Subject: [PATCH 136/305] change some icons --- coretk/coretk/icons/docker.png | Bin 0 -> 2084 bytes coretk/coretk/icons/edit-node.png | Bin 0 -> 3050 bytes coretk/coretk/icons/emane.png | Bin 0 -> 3334 bytes coretk/coretk/icons/hub.png | Bin 1108 -> 2242 bytes coretk/coretk/icons/lanswitch.png | Bin 669 -> 1138 bytes coretk/coretk/icons/link.png | Bin 0 -> 1692 bytes coretk/coretk/icons/lxc.png | Bin 0 -> 2167 bytes coretk/coretk/icons/marker.png | Bin 667 -> 1211 bytes coretk/coretk/icons/mdr.png | Bin 0 -> 2786 bytes coretk/coretk/icons/oval.png | Bin 1236 -> 2407 bytes coretk/coretk/icons/pc.png | Bin 517 -> 828 bytes coretk/coretk/icons/prouter.png | Bin 0 -> 2590 bytes coretk/coretk/icons/rectangle.png | Bin 157 -> 259 bytes coretk/coretk/icons/rj45.png | Bin 696 -> 1121 bytes coretk/coretk/icons/router.png | Bin 1462 -> 2082 bytes coretk/coretk/icons/run.png | Bin 0 -> 1805 bytes coretk/coretk/icons/select.png | Bin 348 -> 1038 bytes coretk/coretk/icons/start.png | Bin 588 -> 3010 bytes coretk/coretk/icons/stop.png | Bin 145 -> 2305 bytes coretk/coretk/icons/text.png | Bin 146 -> 314 bytes coretk/coretk/icons/twonode.png | Bin 0 -> 2494 bytes coretk/coretk/icons/wlan.png | Bin 0 -> 3457 bytes coretk/coretk/images.py | 4 ++-- coretk/coretk/{icons => oldicons}/docker.gif | Bin coretk/coretk/{icons => oldicons}/emane.gif | Bin coretk/coretk/{icons => oldicons}/link.gif | Bin coretk/coretk/{icons => oldicons}/lxc.gif | Bin coretk/coretk/{icons => oldicons}/marker.gif | Bin coretk/coretk/{icons => oldicons}/mdr.gif | Bin .../{icons => oldicons}/router_green.gif | Bin coretk/coretk/{icons => oldicons}/run.gif | Bin coretk/coretk/{icons => oldicons}/twonode.gif | Bin coretk/coretk/{icons => oldicons}/wlan.gif | Bin 33 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 coretk/coretk/icons/docker.png create mode 100644 coretk/coretk/icons/edit-node.png create mode 100644 coretk/coretk/icons/emane.png create mode 100644 coretk/coretk/icons/link.png create mode 100644 coretk/coretk/icons/lxc.png create mode 100644 coretk/coretk/icons/mdr.png create mode 100644 coretk/coretk/icons/prouter.png create mode 100644 coretk/coretk/icons/run.png create mode 100644 coretk/coretk/icons/twonode.png create mode 100644 coretk/coretk/icons/wlan.png rename coretk/coretk/{icons => oldicons}/docker.gif (100%) rename coretk/coretk/{icons => oldicons}/emane.gif (100%) rename coretk/coretk/{icons => oldicons}/link.gif (100%) rename coretk/coretk/{icons => oldicons}/lxc.gif (100%) rename coretk/coretk/{icons => oldicons}/marker.gif (100%) rename coretk/coretk/{icons => oldicons}/mdr.gif (100%) rename coretk/coretk/{icons => oldicons}/router_green.gif (100%) rename coretk/coretk/{icons => oldicons}/run.gif (100%) rename coretk/coretk/{icons => oldicons}/twonode.gif (100%) rename coretk/coretk/{icons => oldicons}/wlan.gif (100%) diff --git a/coretk/coretk/icons/docker.png b/coretk/coretk/icons/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..6727a58badf3525beabe79ac32f04f6020c05402 GIT binary patch literal 2084 zcmV+<2;29GP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2f0Z^K~#8N?Va6k z6-5-lXLlB<Q4$3dV#=$;Kxr#qEo5icInz_p_U^~*?wq|_oL_R&nZ5V+&Ym+LXU{o1 z%Pba)#bU8oESBnV@b8?Wx$%sT#Tob~)-)0A^-S`FX7R) z({=^O>EtByf+wV49$}HA*Tc)=M3GeV2n)Q6a@u+okG6%jDM0r2pP4J3W5VmiL+ybF zw^+!}$x-n#9;uYd6`-aWH zAMII+HF7FZKy&ly3$n*Qjn%mU7Z?0v>z>6L8RQC(z5bf)^&~y2YgwfJjt&!@1`o#k zV?AA1BZ187=pE>t-`w;|KqdUeT@m}>P^ex|CP8=&xn>DC7d&DdlCHD$`>ow0NA?b5 z)i|AtLbm_QnL_{g9f#e%L1)EwZ13y-VO#$QR*hj?0rKkgyfo`pVbwSfbcF1{*YQ1m z-&+p5J3)9UL9i|uRX}s|;E)`)Ph!S~l%9gXLb(0a zfeEaV09MZ4&qJ)~@#%XmyFN-}o+6!Z@9Q|e>G&P27NT?uo4wN1j0}4&rx9oI=t6B7 zfrr%}SnFY}5T#ltFS14`g%1`_QQ-sdUHBhlgf3SyC_&*7KJ|O?H{`j2`t6a zx2v^3jE#lFmXWw9&C_SFmW$#F*m#C`rHg0jvScNI@JGll|I+bKtSKbRLfu07cPukd zp56lFR)F&76Zv=MG=bMGWpfH^+TdveH1Ujc$I$C4!r!5_v_CRz$m z=2cZmQ}+W>R=Osso=~}X3QHDUxX_A?Xk9+5;JWEt&4Pd{B8_&rB3f|6z#mvU4{M3& zjQ|6}=bjm_BnUX|WdheT;N}WlEjM+h0#u5)0hTmeTGy~4ppA`2=`e{x%I-HTCXzh@ zAV1#_kj=1Qaeb8l!Jnjzi$b{3qgYBLTLEx$gh4*~BBD#ADgm^qOzp)9B70bJiltmYv23(7Y`uCy-Lu_l+>NI2QpH+rDoF+jE$x4I0Kv zW*qEl)8WNjvCp}``MXE02oOr}K~ z8KKaVX?Jf{+wD>SfmE?LlPtC+sEq=Edy6v>WQ%PHYNdexBehZhq!W_e?IIbw=BVGO z6SZj#EW*^eoB;d={8nAaY>72P^&l}rfg}awCCP04aEHSLW!hj0a3OZ^Nb90(GQ%!Q z0oi8`8($Fc*)Esuev`LN_&^vqZ1;A~4!%#+2H2k>gX~{+!&3S0aok&MYhsR*EzVB1 z>@-FIyNM+ueDZEJi)~&=7H5auz)~XF3JBf5u~ZsZY;RpB8tw)FR|vGx7@>O^ONnUx znu&!N^8HD3^bKOFtvV^|dcUHvm}Fr}_+U+?y-Z-Ft!@}#DHXjDP@`Q=K*)8xL8ApP z81|3$JcPAG^i(bQ5SA>u2!07msc0!c?Xl)W^>QQD87V8>ed+WkJ1(wZ$&v*Z|6nN- zeFX%borZJ?Jgw;m&{lvnsn)9v?kwp@8Ks3FXQMv?fRLZVk|l|pGRma@wY4McE@+8| z2jh9SzsxrRfY3dQCCh9=o;!~=w*u6@nqF`ftClI?!mGgMulYxSL|)<9+m&XabJZJ1 zAq6P7Ft%${9N-X}?dM5zLjdqz+;`xAky1hyrF^f=ErVC3%_~olhOD@)4&?D>FBp-% zzp$UIwDX(#_9eSvQwE(o_b}>P z5X9F7K1&_;gpFMLxAot}YGI6Yksr79|0@@wKe0+eo{Txl>^g;9bWQD|Yix}A$JVWa zO-->%0&L2fh0bS6!Gj@_yXP96B-M_xt~ifX<3Ok|Eoct!k;a|X*2O(WZ(29rT7O{O zYM39fY8)^r%s_{=_IBgG#Oe^C+0-(w7hXVR3CUS zF57mhO5p>Qj)3W?-34KTJK>SKGBTNbkQ+Qd>=isxDQ#206zX1N9z4z>_c2PR-V7!m zWq3tShkxY>pi|Ix1x!bcipht0b{SU&#HdM O0000h{ER5N1j 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 QVE_OC07*qoM6N<$g150mDgXcg literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/hub.png b/coretk/coretk/icons/hub.png index 26b7eba5034121f98fe6f3c6d2ee395a2eca396a..c9a2523be17ecd00310c38278d8f3a44ac4863eb 100644 GIT binary patch literal 2242 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 MZDEIdh4F~{7sF09RR910 delta 1105 zcmV-X1g`tS5!47FiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*_6h zL_t(&f$f;fPa9VhhrjVd5VtY034s_|lf*HEN=+lgQ*0U_RVvcdE`6zss+&r!)K#mf zk*fX)&7V+pSsceCMO_q>Oa%CB4!KW6Hcbc4_QyIio>omr;ElgUe?HA)DVIi5iq$nmA|&0zorGcJOhFI0RZ})uLCf#a)-deO+7;C zBOoni7zz4t+gt$n=CA7!2GB{sUMZ*TmLx#yzP_7s+HOe?@pgckDl12-Y8Z45;5p?{ zpYblbj_0@CPMI}1N)jN~f7g2)U5ftg_3d2g_zI`hiPP%jO2?P9*PqMtYj~lap_6Z7 zFcJ@>5h3B~!nrCBc9e@v7-ebK|zT`DJMW|3ZKk zyNh$jUgL!I1OQJ{PpPTSofan7C$$qWwKmO#=7F5GNAbthR@c(!f9R#p(MvQLV=J>o zkE2`hoD0nZwAHqd$%+6Z#00-S`i;8}XX$_G{7%_Su1}TRc1sel-PdyijAE;@amL=t zD@WU?t+vx*cNabD=%G4hHfFiIF~c9BKM|$>8o@}=r)pMG{6jMocssvt3gCG0INx@E zgUL|%J4uNtKEM6Ne_oCTV9YZro2zD{#6M5g5TBJsAw-J%>;+9{q2>I zR>19*J4JoQmnXCgsjYye=rZAWMA05egjwBO)iR`Z0?bA;!)`CJRFt`gbIeDAB*i32 zG0A)+$n3@p(PWHwPrpNzu}Z73+6;370&krhqV7-~p+t!9e}DghL?)TzV>X)kr0rvl zS?aia>Jq-7U-kvLS2Xzr`kHz{f30+!<{InHS~<5Cs!B+1I(^yNRPz4D!;N`sYHuwx>q6z;I;h>RN0JGV>PT(x|a+>68m@VpRx;;6m5SF5g2rrEtuefUPCe@OUCi~uKO z>I`5Ne?>b*i-f;O&e<-}8dEUFcZ=2tH-G_aMQaSwAX*?Zl`NO239NK71uNp&jJiU! z!1%emwY~+1n=-I1Tg&*yHvj;VHA!?fAIGYAo$q5)1Lh=R2^`;W49S|Luj5h!{$g7; zj;=q1t!p;>HYPP-9>y5je;Rgsy*QT?RX&2x&TKpF$#GiiTad6<;_in&IIiQ#F&9Et zz#{xuoT&l-p?Yzq1}uTf#aUYe7-MK}5PozLI$9d==ZxCo<94qcj_csYyW4pF<%Q?R z_I($Wh$V2S>7bUoP8pLkleqmpi?7q)lpNpJ1|(~eIIylm+jAFDfBC7r@@=SZ4QQ=z zL3=~H@|&Bp&&~<`eX09~mlGox92bhc#u(bx?y8vio^y_&kHbpl`-LcN zfNj}0u&!g#J+$bZtLPan!f_qk{m_T}R9@K_^C@cpEw)ReqyeKCq{7V{T(frb0tzTPvoca=zVp~_XVvIDVwM# zXC`s&&2?Y5bd^Zy8t~^K)?zneD);r3K>1dMbB@8WA@t`5l;74@BH#9kL0z2v`tiLK zUq=q&;%ramhHxb#U|D9puJyI)-16amlzMFKWLFf1Qt#aRETdxH3^PE<`99 z7XXxu3xH52<034ARAZa2;R8ckGVaLw3^uLaq+@ttp-RTp*|o?t9Ymd7>-W%7C>f^_ zCF3MeGHxD9#x0JLaetv?oJN$4(}&zR20n?_804d9fy}FmheT@(B78~9 zyu7;yfGb*O0M}x?SF}jD0gv|Idkx0BMe9P3@hkT`?!Of+liCHv3)Ni!p2*aO)bQu= z)u&_%r5cdUWDD`xsx*MhFnc<3!T6=m6ZPrb$*Z$6MF%MR-&pN=uxCT@SG05PpBFSD zWUsehzz+bU03KV6_sUE=gb+dqA%qY@2qDBwzX3%$;5Bo4^J4%2002ovPDHLkV1fWo B7GVGY delta 662 zcmV;H0%`s72%QBXiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*#iT zL_t(&f$f)1YZE~j#(y)hZ9Eu=O7$W&r{thCwEp=78tNsYm}pP_`3PPrcoaN|A3y~0 zpf;h>=29y511Ls{AT(Z9(SsFBFg=*IWY&X?pk}r^n`}0R`5p7_yYoD=^Jbp~i^XDz z65&V(*@$O zc%c5s4FIGPDQ+CU%E7${jC=Qlpr<3ZPv7#aCr6T8KX#3BZ4Rg5_`dHze>{OU!*Yn} zOp#NEPx$T$Y(NM>ZtP6Z_MQ>QKs**_I#cAN!A|*MOKtOHb(Ys(-}vq^o`K{@l4AOj z@xa}Ab+_a1?b)br)|su9`Tld==)Ohi4~!Z@;bZtX5)6ZFjX`uQ7T8 zPQ&5ByN7=bvB_Bv1LFf-->mcK{bQrIr58TKoR&bH%~q4At0k&y)xZn4i7d=1S9Lvr zZETnu6#5$Gq>xML>}h4 z#mm5kS7R^3+-?Z?p{scrSPnr;AQUX=YFYzQsu`djiXt=Cm6A7gMXiBKzEX<`cU%a{ wKqC}s-ZWe&wUo>*UVQ!~g7qyHi^T`O0Ns}A&6k_p#{d8T07*qoM6N<$f^>&Ew*UYD diff --git a/coretk/coretk/icons/link.png b/coretk/coretk/icons/link.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b6745b29e839599b85ea86df290f709977529a GIT binary patch literal 1692 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/marker.png b/coretk/coretk/icons/marker.png index 9a41ccbea61bed6d289187b291c6971dd80e0e23..8c60bacbe5bbf2bf76a5347f776d12d774749217 100644 GIT binary patch delta 1209 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 literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/oval.png b/coretk/coretk/icons/oval.png index 06f492d4893b061669037e76487be46de3e9e494..1babf1b7edd79ae20e1e9652a5b419fae7bbbb82 100644 GIT binary patch literal 2407 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 Z{tNmR$e-W{4OjpG002ovPDHLkV1nyAevtqG delta 1234 zcmV;@1TFjL64VJHiBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*}q1 zL_t(&f$f-EY|~X7$3MT*ZUcs}fd=FOFUX5Pvfv~H(v5~8iHVo3*eKaFCYqQSd{G~a zWR8KCX!Hd`G{nSkF{bV!8cmRqrW-sU!#HC!Cdfq-$uNv1 zz*$5F@bv^UDv$73?d4nFKrB8RF--`YfptJdf%{(t5{T~S(zPz0-FPYA0{J!2)iWP2 z_$o$TLaEMovq_X(MA?O3_7TxFL%A=@g*G*;NW2WPZQ6yWG6b{|soRB!e@>P>c>@P} z7I3|MA812{3nSU<*PEaDSSNGZ-GFss8s&B{DbA zkyr}!0t=nU1zxtNNbpe;`1B>q@!x_yIb3(e3^QA@lcY@?~JJ(mZoV4@pFP<@Wh zzSE$sNs$K#lzjp`<1R`41infoiy$5`6YtgsdG`oRVyargCVuXY)Z;~x zl{-d$bUc`7k>EkK>u2Y`9+`+a8=b_5TY~rLw}+U@{soF?`)z zZ0;_Cr2HtmvdHmVOg7vG{=kktTm(t^F;6hTs+__hXLERD`XEMGgT z8|myzH|E|Nx(_cwe~HXQn;N{}ZtQ{c7n;^( z`2+w&-gP`7%)-``ykfJww;c#uKGhxWG@SltvLn$0thb%gP9)S;)NvX>Rp{t#N7(8_ zeqdAmMmIfu0|$;QV$Ay~_1}^1BcNMo3=gMWH&wdhRd})!q}hp_L*yYMbtwmH=7TEM zy9%R+fH_X&e_w>Hc!Qr!+msrM1&{?VAZ!Qja-t*PH?S$(pKfXPleROy8a3afDGG1~ z(Y<)OhiLd@LBcx{OA&2Br3JV*O~!dJkFzP{{=DC0@=+m{xQ_r|qO8i0p`gEk^aIBc z`5Dn6RE8P#Zk(EP0@Zj_gEtQor~$3L@sed(c0NYHe}9_B@R@A#a+`JH@gNt>Yrvbp z?3~sV&VMLxbEp3y;qdrm$@vXyvG_tv;C0X!fjbISn1NB0-IQPdV6w}sWD308Jv0|n z9sxE3A)vgVIxq(G0bg*nqJ!quqxlw?`nbI3U?nqWtVLJ})F2br>%y#w>vcsKnYdm* wi|AjdC=6iq1ZA_2PW5`dM2Qk5O5EQ353ikAm0`n5NdN!<07*qoM6N<$f?%{s$N&HU diff --git a/coretk/coretk/icons/pc.png b/coretk/coretk/icons/pc.png index a0577da3f28c33cf1ac9e39f4135b47334787376..3f587e7034e25bd49495cca974ffd31cec6c1242 100644 GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lNlJ8K6<)1 zhE&XXdv|YkaG(U+hvzDcXVp3swgyRUTp;%H-IE7FJr>=I75*`&6_ylbso58ZJXd<8 z^g`k61BKiTEO{GMU6LKH1Sqeabm#Gd@Jn~9i}Ei$TX^!b^yKOyzW2rNYA1V7W>SIz zBR!uxjNG%FN?v=<1-2i zZQeE1F-?e_BK3te@7})M(ywo9e|Kk>;f0F1j450VK8zDI8H`vRmNA?VVVKFJ5X?Yj z!J7x}3(I|}-t^cn9cuo@I{l2LTc&;Ot=tseEy;Rp)|)#kh5eP~<-ZyE9yEAQKHRciXAo^gki{(`&m?W@DAr8hC|*gk9aSuw8D zt2Z2;cSB;%Z0p_-Bg;248@EMY<7}v2{WE*9;h#s$4O>^_n@Jcm=kng%kZ)T3l>f(% zm&VCiJ~Ea)lY_VXcilME=f~!i8=}u14!Zy6*et2|j^fAu&Su^E(=@^ve)`snI)V~M gLkAR`nDB?m&P-g{{?uERLtqzyorgAGWAGyQCzs3=m;xXaVUF(jk$?F?J* z!vP{~_b-d(m^renbk02DSW*xs&Mv<3!MerW^#^qHnkG-48zXacXU7f>D-T(jBVtP@ zELtk!?I?1wD>P@F^Xy4ql((*ZvaMjA?)QR2pYJ_sQDHc2(BrcrGBGh#m#tCKJXNM+ zuH3uxrdk!VwteZdVK`%^UH>DhG%x4<-}PRz-{h>bzBW6)(XNsCL>msY?5TO->inRa zU;c__{D{|>=hMo&NO^ne90kXRFFQ9r-u3lX>*<`ZK)Zc;j4Rg7_&c}#I^+8%H5wsX z^sb$42-3(>2x3&2eEP9M)uU+%&SBHV1?zcw7#gg8u&UW}oNUeg#2G|K)!jR6+J{uqmy|{O+4I=S(A;YDugq zPw@Gzopr0Jad`*#H0l diff --git a/coretk/coretk/icons/prouter.png b/coretk/coretk/icons/prouter.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ccf6641d7dccccc4502cbb2a8c5fcdf08eaa7e GIT binary patch literal 2590 zcmV+(3gPvMP)H!I{Uu%Ie?9gjg5_sjg5_sO}0UqHXs#M z%~kWFin?4;m132+n1Ese<}ok>Kt4wQ0>I}01_}6_fh!F30XU-oz44gqSXUtUpN#zo z4W^U>d|_WMV$Sufs815p8U~iYx{VB`lOR6Kih3w_#B(4R2;LH5uLv^kfcpA+<>=BA zkAZj#1DgTlNiK32Kr4uYIZ=0KFc3^gE-xh+bHM6`I`7Q{dM^ zZ+4Y+jby@rv^&5T_T?g>JOiemFmQjm%TERYpD@#Ia>aii_Jwbzx$N{&-cspfrac5K zN@uwSxCG)C+WoEXq^rQRIKUtBFNy=N0Vo%#yK4RlFcXemIVw^XU}_^*{JF3%d{M0M z-6*mP@P+*TIM4&Zm0y@whz$$YW8=bgD9jT)gb-J7+;P$u@(08UCn-vnaA{|$#}ReE z45l5TwF|s+ux-g!cpY8wBlBL{*i)G4O~;-Ep+~`|)u>5)UVsj#RW( z6pIuU=792W`Gf38ybD11Qv!_6cvjxGQdl~ZEKn1UgJn(C3q=VEaX?jbRRQA80|0~> zb|*%kY$-FNaflxzMeP(dCRlbrX=kY?rl{=z9u{EFU6|?HQ;iiOAmFPmS1erGS?aN> zW!V9asQWiE23)|}>_+CyXpF}kQTNMMMJzd>yruFxV0zZ75y=qo6w90$0RWhGY-+Bm zwjyELGJnXwh?xFhrSMdU>c=I|j6lS1SQ(U^W;4ctJpg8z&96n*h5hKdu>Y?A|Kb}z zPnqwRfB%&%lO|&}De9{L{AMznY5~5G-w(iNChv5FfXecgN>lY=*a5z14~@>PDAo; zIucLnNvO*Ktf;!gY;L0{>KZ-ibnOL=1rURmi zx*USyhYIrwvHg)c%*`nfAt#i4Pd@4vKP~boQ&AJjGR;KVDvHo300}?TGO`{F(ouJs z4p51OIsmxc)W0sKsary3*GM|*PIC|@ph%246Lg8NVc~kr&H1vB!P!*#3)YG1?~}rS zbq4^LpX{GnCsTS^F>e|2y|d8N*MdkQB1BUQufvO)B7e$m(CF_=Iy+-ZT6cG%XB{xp zkR>K4%qzsUC0nJ1k=#jI#~@}-48k`6$nTsndeNe(uLb?X{lc|-K0S`X$Pj9Z{ON23pJ^q~(*iU_L~u27 z4GkadlT!KP^bhyrjgx!v-&r4~9Q4zYt zY;MEE;vA4W;yFOTkTsLC2Lgt2qVB_b(&}wgtY!E}QgJ4J$G`=n`KCJrocnBO*6T*| z8FoO}7rv>8ttft=;L9Y07 z9Cv&hz-+S`sVH5v!Blukvb~}YFZbZxfo>6IC1cQ`s5{MOHXW7{_JuDJ(-sKYs-8&q zz|Y4^8=tY}yc%u2YUF9G00ScQCStT77d3uN z!XI`h8`|z`|5#DfjR2&)as0!}J-XhKt~Z@YE~-pxMZQ&CXmD;@Wm|7d zRUQS?KSk<}qxEFVobfPl+CltyXLU$uoRskB=c05tJgdqMvm0w>Nb41)aCN9XC+1qrzzbmdIGtr{ za0$TkH?GfKoX*MzGWa34(#}$kGwQAb(~Ar|lp!d&`38XN7`@MS1wbE| z&ak5Px>e;^s5&(D;^A#z_PXGV_07*qoM6N<$f*@Dt Ae*gdg literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/icons/rectangle.png index f8a9581ee1df0f3e5795b74658e731af2f23f45b..ca6c8c06a1b6ae83e78bb593209b5820fd314f01 100644 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt^TJY5_^ zD(1YsxRJ9VK!DY8tKsYoKC?F%^|JDoJ~W;j_i4vmHJ~yOa5LQ>_wDhY!s9=-ef@a- z^nL3c!U}2*J`FP%3>lM{j<9ra3ea2t;u3}f?s{*ZGZ*|h(Xa(1;_2$=vd$@?2>=~K BO+f$v literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}}HPZ!6K zjK;U;H*z*OFt8lFek|vZQSPBr_k0XQT6pK2)Y~n~%U-Q9ov({XW@ gD>xtnLwy-z)n8@{hR1x1fTlBey85}Sb4q9e09ofZqyPW_ diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/icons/rj45.png index baf4f9e80fb8632f29c5951d6ed6dbc32a1c5a98..c9d87cfdc52472db6adb4f7d234172e13e7a2b68 100644 GIT binary patch literal 1121 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lNlIT#5`Rb zLn`LHoxMLxG*F;D-TXSMp_*c=n7FA(fE z>z>@)EVw-7$J=zq+x^;W6}P(P**2)1;bFUcXV%Tu%(7nAxdH)!bxqO1N*l9vUbHl5 zzS)>wtRJ$E>6X%a)^CT;WS(N(!pgK^`{WCcuSZ(VRs9sC(LHkVyyocTD_-~X9?hZL8`E~cuZ zS8B@wV^2#bB}`B0;D5sVQ$;kOt)MyDxvAY^9q*r84JYK1c|2JKF1eQZ$|^VPuQGU` zTIkH^$r>W6x1rl0iCgHz>ZXJqK^5T*e{w2T@kJE8HdwG@dCKoq*;>=)y`L{;o;u0L zq3@~lBoVh<$3Ia_4K*bP_UmwXsx3HF5FMmm*|BjeW1vYG=SdcYOPiMHebwb)VBicp z;yY7CIDp+sUrXdpN>cXfD2p&gPnIv{D>bzLJ-24e*|uO6-{rr)N%0aYDuM+$<>hl- z)D0Mp-f3#t>ca5z-1Xg$j`2+5SmCuteeKo=Mg|6#ofT(fCUH0%TKy=g*_eSrAz_DF z{+&50OisPsm!ejS3NUpZKcm>$VDwb=38=DX(W`)cyP z{qjB0ofiH_2(u!ey7m?)W^T8dy)ZYX*u3x$umA56{pKd?USls z(*i!euqn%#l*s@9dS%&_wEmzge`>KLH(=SElC*|c)M80)phLO1%Q@P`pMc3xY|?ON z@eL4wcuX*twOEoH$jHbOAX@whFl1tr#IhDk`Ux2Mm}G(D6-#;p%;GL*M%H3Egd-&H zG9SVbiux-jf2axMMdyj6 zF`^h3(lVl_F}*|?Xb|s-B04!Rpdpc*GeDF9CcY+WkrN=K>oL7V8L*ZHk`^>TvSv}v z8A5Gf^L2IBAr+DyG5}82oa5rgn}c)Oe$)mGHP1Mphjy8ee|uMI9(0Z1lBI}y@o&UX|nuf{o)zlviYva zk^GJ9f3x%k)W5_&ZVpusj-)L81X=?}O|*fX?IG-J50ie5;rLEqQD_YuHLZb`mL=y8 Yt>^W4O{6Fm000002uVdwM6N<$g8j)p@&Et; diff --git a/coretk/coretk/icons/router.png b/coretk/coretk/icons/router.png index a42623a49499cc37ad0986163ddcb67b6f53dd9a..1de5014a578496e2d28f8850d20a3a19e2fbb367 100644 GIT binary patch literal 2082 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 Q*8l(j07*qoM6N<$g0Q%$(EtDd diff --git a/coretk/coretk/icons/run.png b/coretk/coretk/icons/run.png new file mode 100644 index 0000000000000000000000000000000000000000..a39a997fe2f2bccbfd829279511721daef6074ae GIT binary patch literal 1805 zcmV+o2lDudP)#}Y^SQ)msqj7`t-F4-%DMeHpf%T3L?PK9`^m++1GXI z*v%$3xDZ1q2&>oDoM=C?=Rj(nR)~Sz?Hg@BQ(}`30Cz_s`_R^wHs_8DaD0&Ku62*T zmMF2w2Y`6o@I2k~WO-_y;{tf}#Gb+4iGul_LQ+}nn?CpT!H21Njtg*SDD*-k9Cfo?~)F2LAG?EEarTt<#+6r~u4N6XR2xob0ohh&RMPIg%|R92Nu{r2sFQFFf6;c@Id{K2yhcUdg(`^UeU zo9nC(0Jk{VUooKp7c%5K!_gl05%82#{S_IpXrk=Od32y{MEnUg+Xr-OYJ6%5QdfF+r;3krPlKuVO1nO^LF*MEFAhf3A~ zd!f(kfC2j~fc5gkOs6M%xfVd9O??$RptsC+K7xy<+fM=17}TX;qo&+#jc!u00`8IBkPo5|DVJb0f_jV zDN86oUnt`rr05CMil>l4&I^z*$SiNl3kxtbPsSY(V2=-&ynWAqGnqKq z%WDAranS>Plj+OPXRQ@@ey!7CDr-g~jNM=SJCk1JCscSv0`S0rdYTh}OGUZ`F!Nco z%{m>TgmC#uFfbTSw}36nJ+g--h3-3vwDh{g`7J-dJQffxG9SQZ>f|>hYltiyuxa^e zO8NH>43DHu{<5WY1)F>T=)RL~sn!-C-T?tN3(&@_0oWm`6P!%{QBBKdhjgXqMq1gX z#s+zV%MslGu8+@JK!t~}vRb%U7;)Wu$*Nsj1=!se+XA3QRL3m9))WD*ngv*2I01Ub z6PQkUd$N8pK@%_5J*|yodHrXr07T;WPb9_|f0(`mr8;1<1#I51UgGF-(WX>ZMo0_rk9MDUCX9zna9nmeQ(#z>=`3beFtgC7$#%@{o!;AIMNxz zkmO&&jhMwN zi2Wf0Zv8?jV6_7>tRtYQ*mWmt5dn4Q@&i~$Ks8V1m$2ropVw$IK!j#dc`)S8zrWu{ zSXL=sf}(NH#a!ByF2Lzq(+vQYi|V-b4meP&i9f6+>TPYLd%3hJAr?f(f`|OCBvT2^ z+d>dG@zHY&^&MR!>+NTfye)u!Ay0WV~jvp)eR$y1M#gesOnLlcR7E(yo#oBeMssqh`Dq>!T{&C z|4$-S0D|}ma$%F*6{H!t=KwCt!(FvEb1NJ z?+Efrops@d2n(38{p-qvAE1bkVgWJMH^^wml@OoF=W!_lcs$0qjCNc|R3m(xPXkf} z*tfR)I*|TiRtcbk>CL_MKFePf4c_)b4@RL- vC=?2XLZMJ76bgkxp-?Ck3WY+UC~p1(pRFLwce)IN00000NkvXXu0mjf6QxXk literal 0 HcmV?d00001 diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/icons/select.png index 9063d308a02c5693c78f9cf680d8a153bbcf7d36..04e18891bc3a3a1c3cea14a66c03019ecae22d20 100644 GIT binary patch delta 1034 zcmV+l1oive0*(kFiBL{Q4GJ0x0000DNk~Le0001B0001B2nGNE0OFW;IFTU~e*?ow zL_t(|ob8-BXjD-YhCfY=iVNZr#GtsKg1DfC25lr33O06Pqc&P9+S$1jiiN0^VB-Q7 zHX@cuAwp0g1SExuh$Mn$bHPN78gnh)Oo)?N?kxA7-22W42c~#)-ha>k=DoasUIr9J zQ4~c{6h%=KMNt&R9d*Dtpa|vdO2z7pqK#$_ylYhQj)`gwFu(&~f2MEYu}}*G z4A2J5^{hP#YGr@{o&gK}$`7Af8eo7fV42qy1Wv6DFu+@2wa-O_j0*z{@B!H9aUH?p z$^b)>r0(#ylrV8=fI*YglDIa&ph;>`d>CNRB()&E3@~Vtniro2WJ{CO2}l@V&?I#n zDFX~}8%0uYD z)Fi3WgDzI7NK&PTT&ki^Ql$rMQ&A_W(!;f_zj*j0NQ6^HV4s!p23Y5E2WmiZMu7$# zYM$<@aU?t^tOLdh>2ten^#Yq6&0w4~XX8JC0g^!vWU4Py-6Vc}6Vl-d`c*_5S z?FITXulq@cJS-(p8rP?eQDBX?)l^B07vVbKgtUmMjE@WGFtEtif~q03PrxtWiN$tv zfa8v}1WTny^><(|u#)6FZ}Vs5EI&&Mi%Nz+1hh=(@zC;pM_n%_M9LlhXP`N!M~mfq zyU>TRCQ!WW*bMC*rEjKY1A~_Rw$^$P zAyaBtFM%e@UKcI;8Q@OsvFZiW8o^3hto!K_gV?N^7PABUx9hm2#I&80f3bF8XTqr}_oEcxFm?vK z2WALqjG6}EBX%Yn64DqwXR$M&Lr7orG?IKBX=B1}8Hw?87drzUNePUdZP*zw25b=0 z7(MOSnQ&7|VC)>k&VUhMsgTB~sV9lDZA>^Xq%nHVU}wM}`n|Ce76U)9Ghx4wMj*V$ zQfwIn?pbcTV9KSCQp9Qiz;(;*`kkssIjs~w{RHy_U8rm#d8Vk40jK4prH5wVHItA0 zDe+TgX?bX=4%i8-CAmO9#=}2fD2k#eilQirq9{|4fA_FisMx}b!vFvP07*qoM6N<$ Ef=v(3_W%F@ delta 339 zcmV-Z0j&Ox2;2f8iBL{Q4GJ0x0000DNk~Le0000O0000O2nGNE0N{5$_>mzLe*qCm zL_t(YiM7_fE(B2&0N`)^6beM45gl5+LdgSYRT_yxBk=}`EforHun)is=rtONj!H!0 zCy}sg?N!XkOk`$uC$slfH|I{ycTVPJ&QQMEXjPmyUIFKrhzSIAFdYLJ&;198HOOd#M3J9UBY8bC-3 z+WVhmssac@3h3ck^0J@O{R$TFI!XttvM!FA0A=@jRrVQq5NQ})$hz2#0c_`h8xejEDO!@b@P}-c lJvi9_=}T_efn=N(u3yq_kEx!A7sCJm002ovPDHLkV1n)GidFys diff --git a/coretk/coretk/icons/start.png b/coretk/coretk/icons/start.png index 191d66367faf2e6433a4dbec7df0deb8f571b007..719f4cd95f744e42b1efd56e43b7e876538a4c09 100644 GIT binary patch literal 3010 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 Ef+`}3D*ylh delta 581 zcmV-L0=oUe7t919iBL{Q4GJ0x0000DNk~Le0000m0000m2nGNE09OL}hmj!^e*ytX zL_t(&f$f(uXj4HL$A9-Osfnne)j_eYB7%y81Y9gQIJoHK)=4KfCkqxz(TW)wXa{Yn z2v(Qk;N}n&Crfn@5k#t+6dg3u7J@Iyd!K_i2)WqE-90V$TmO0g`+o4fdjtZ3Kp;z0 zx3Do(9uzHAfm6VCPc!{uU5To-e>Im&wWar_9~lK2L*>C#%WGi3$#@&59*I7@_37eD zr!X=-C0eRvKRm!dN-}8*%>qBy=HEb>XE6%kf9JbVG6mfP{tJRY)&zm9u`aM>bON=_ z-vwq}Aa~*<#ar`42M^hAQFda(S!W{;pA_$e%q&acYie?>=j_-oFFU{sRhIN5< z?GerC3esrUuI`9+f%wLl?aHo%e`Hw`1hQsR3#@sY;?Jy_0;7Nu_1MiUy`!LJ%8UYu zk`dsmlZkqx-cIz0DKrY~c=BR75`6?=fo#@kO~HdC(dE*!#Sa;r8y^S+0vq55fyRCj T^j?Jl00000NkvXXu0mjfTzv~| diff --git a/coretk/coretk/icons/stop.png b/coretk/coretk/icons/stop.png index 1db886c8eeb185cf1db33c246a3a9e999abd53ff..1e87c929740d1a896decf7f4eca06e55ad65c745 100644 GIT binary patch literal 2305 zcmV+c3I6tpP)^Hgx(Sl}$FqzCbBOD$t>7#Uddk*+i#Pr6@M< zo9ub~klnD^Y&QFH?!8Oy{e9ZKFVFeU`R}>+?mg!`2e2^}GFw18bZ*}5bQaw#XOLS; z%o9X+Fi=8_c>utK0Rs9Y^Z$gv0D(gz!wWjgUp+Kpvh*3rI|HtGc9j&7T*u7o3Ai7? zau8eqvK<~TXDCtbg5?9#Ik-3<7kwzjZ1Jm)C}?FUe#S2;Q}#6&OPCJvOI z_6_S%fG!5~cUy*uDPr#iM$0~nxQkM~I?`W1izm9g`HF(!J> z{h<3<+P`0_3aZS2kE*`?xv(f`0%(h>n0d-c$pt&hg1`NjieO3%=&owGj|9ICV6lpL zw*jGu;7v;c{;RnL61fKG_0+8<;xn*L)AY}W^BN<%7$`H&L=EsU1_`TN+u^|WI$)_6AKH%@*=Et!_9<{ zL-6W*1O6N7o{;uUm;rKx)y~X<6Gbv4RAR7 z_5xU@M3%K!O7r_5!F(wlC==DCF^0F!*t03II62P_8nZt~kG_{F!5G&$jwi z$#x{a2M9a^)&0dH=dK6i&_OFRo=#d+;)Y(&u6v?`rfk$w)4iqP?!=8#3}6x2iToL{ zHk;7MA-|Kjaf$(wvC|km7KErSanmFNLLP5LbV#_3K0Mgv@jej0VR8!~+HR;E3qlEX zoOlWPnt@s@2{AUtf1fbmif2~|fTYi=HVPp;^zYi*iN5p+1BwuN4E8H$BPbXcnYT9f z<<=#Lc)cldtO#L4?Bgv1m{yvh#flK~irDA47J&OqQDa5o>RJHf-HxzFI~Yl6w9dy3 za2DMxhyD7&NVI~Yn`J=M02vW3bM;sdCN~f@fCc88sl|#As04@_Ab=8cZ(Y;t4#5(M;_EyQX_qECU?B}hw9Ps_O{@7`SAP9P2^Ee~WzFZ}xd1lPBf+O*pC=5M zTR3nAz^EC5EJk?F5YEQFOc)T&D0|TqNfv^E3wOWKJP`Yy_%iO4!FsI7Tk&5e8Nfs@ z8>_{7I54@6CmB%M>iY|TzZ$B@TKsuYn{U$7=G6WGrlW?*u^0?ACvKZ!fPi=r_G<|} z7{e&Nl(=z<0gHo;9|34JM3R*t1WJGJ`#5oP(k@9R`Z=I*k}v>BWw}oEN4-`?L>x%A zBdGz=oAK)^6^#77Q)M_^8M&NuI?zcl@XB;)@uy#@Lh!SYq%7n4;Q9A({rQ7%F3<0D z(jy($l+Ka|07CL#|Mg zTEddx1rS!E%o>ami-vR-l1po7Vp*&IT`)b9yDn?Mz#ezokw2z$Y}zSl7@*#54FP7h zGpVHJxT~$HiqiZeoA%&b0$3yYT>znMmCbq>U0R{Tn{MR;8IF#=H{idaapW5Slq}yd zt6*H0(6%gUIsBhYPRn?B`l8^`4+YCg*yBm#=_7&7Wx>XbpBA&4AFw#se36*8!+tUX zfG~l-q#C*M;y48}zaaeCbhZ`)Z zvb5cIK9?X$G9zBA@h;OOy#}CEMZCO+(WN1>X;JX#hq;DQGDmDlz<<>tcnyH#D&plc z476)b#~Kxt59qg);R~33t1IRAQikz_YP^p?k`J>c?LBuexW2Tl`GV@mx=d4b2mI$g z`Mh%_G5-L-?K}Z+h>3nW8ouL8dQ?7Oz=F=9s=88;I0#CfygFYYqPB?8zOVOM^M)-g z9`blAAZRZbJB%zh3r@16ALsgbnJ$J`M9AZ<0MR}GHOQYFMG+8=Nz$Gx3pBoOWWlEW z68~A_{cMD3J2Sih9yLkEWdi%g#qz={CM5Gp(1UaAi17F_XAiCz>QR6n36Mw@xF@=0$-Bg({qM!HhywM zK9F|?B;$kH+QQ;+@lvLUi=-%q;5%7zF%cC505kWKpngdDFALcZ;?RI&_}Yq=7W-|j bO=|uR`17L!0e5Q$00000NkvXXu0mjf09{27 literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v_b|PZ!6K zjK;U;H}W#8 b_z*AuI)zzdTjAtdpot8gu6{1-oD!M<$_gx= diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/icons/text.png index 4933dabbc40d0694ad46369a884685bb463f1f04..14a85dc02261ccdfc217bad1a38ff926fffcab17 100644 GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoY)RhkE)4%caKYZ?lYt`dJzX3_ zD(1Ys>&VySAi&_L^6&rYs;6m6hs}Ol*YgHT1G9@vg$QUwG vUEqf7{zl94q^+)4-KyO%F9_N*uF6a@fbW^{an^LB{Ts5sLo;l delta 134 zcmdnRG>K8MGr-TCmrII^fq{Y7)59eQNK1e)2OE%lP;UEUqM~SwyQhm|h(~8~f&?oI zgRVcRC&BU=p(_`cdP)<$UHl=AeW1Y(kGap4mxkO}Nq&$lcb=dc(^#havo@ ibHM)v+P>}z3=GRQu_>PP{PYZHG=rzBpUXO@geCw70xO09 diff --git a/coretk/coretk/icons/twonode.png b/coretk/coretk/icons/twonode.png new file mode 100644 index 0000000000000000000000000000000000000000..6828db8e7f9d48f656fe9ce4ad6902aa50c5f51f GIT binary patch literal 2494 zcmV;v2|@OWP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2~$Z#K~#8N?VV@L zHANN2Uq$5=lp;k0lrBU>6qFF`5Q4}D5C~NQexRsClmJFPKs1U(utW(h6#Za8g#gka zfC3310wRW{prH#$S1Bs~e>=PToSi#+@7>+mTb|4>`DbUJ_qLt0JLQ}+^Dapw5{c=t zOnyu>0ask(kG-hNQp?iLpomiw`41Qm~NG} zl@cAMiRq{AGTUu5cN_t$vd{sJ88t5ZexO4lwRON-uug*&qZjyGEp32SwBm)ex zzw}dAEO}iOKUQ4@41p0oEzwsDkPJ{|Yw0&q*>u$1@xurh0fTGJxYf(uZ8J z``tf;UGif|GJxZ=QZN0XXH34QLNb73{ruQBd{4#D$p8*~J(Gap?JBP5NCt4cEkE|z z{ujJGKV~EYIR2g=>s$&kc_Ke%Bm+1e&yRhBa}UpINiu)~e@!oVPt?u%u_PJ5fs5cp zSIjEyKY1}W7v;y2WB>=A%JW?@<4yUo6W-m=cl_EF7f1$J;G5E`uGmXw{1a)QuSo`2 z;BV3?uIP{v{+kyY;~DTD(sy04N^(b6VWM=sv{iPN(`0U-iho{*72R0|{Ru9J;3Jiu zfGV`p$GYMI<2Fb6>ulx2;ZpSWY_UQX#9yRPkiDs1mI!Yq#^lY?wUVtwo(P%{`?qmK zYRZSDL<3He{E9q4dap!^3G)R1->v>9{Z86j8oO^zov`B|=`>d?`JqJnU9g-)T$c`d zU1?>BEIEdjG+UQRk4lM#5GJIlv3D~mh*2hEiQ%jx;R&^sf1kwNo#+`doPRIb%6~!H zR0^@V^d0F6$-ajU_b90nXqg1-N!v;LNnez9lX#rDuvv$`?^lwoNM^k|Ng@1759t{R zBlQZ|1II~sOQ9cu{v};5F&dcF^#{jD{_p;}REPiCI$?xfo&;uhBo;ME(?;aY*WFdq z;&T#1M*BN|E)5$I@)|}u2TMf&pNi((eh%VqQQ1VgU-G|bTye1s=~zuNZ8nqq@jPw# zsAltIsqW4`SF)eSYokG%9?X${dhHEs6?(mdzkQlRyH%wJCHtEmlUQoh1v+5+ne-Q9 zGt3#sWa9#9rR*$66`W}pHLuxKI>Qww4sHfIT?P&XjQtrnJd&NYhkCM*;5#;T2H*!@ zD6N*AIbI%B@jijqC#`yb6b|C~! z$S$e`7)h7)YUxfoIHzr9Zl2FOaE#5$uGEYzAT)P2mY@qOqGF2pNF%it_I1 z=_w!whp~B|kYx2OS1kF8gjaTEz+xfr6xvMTn9jJTC6b*c;f&)3qZ75+wIt7OArTvx zI&}sha6*<>xq4jSj$WFV$0qOu?J3b7Pm0y0J0!al6`y^P4>VOmO%RX|N$m8=?98#W zX>$~K{h87vD<8N9CQf$Q(U#H=TyeV8Jpl#VNS8;@(@ZA53w_FtW13K8IGXMkaU8~p*np_zx7B};6``LP!;O5_hU8kY48 zfq8sg_BZ0&pNC|!3^EA5EfM#$7h%Olrrc>b!0P&#X+pEgVm$ksPnV{`s6JpAM4ZSn zz$omsfD3~O5F-x!JhRLZ(h06u@_C5_t;7W2#v|p%J_RpH?~@V}f{~q{sP!j_g&2tm zK{(N0oH5a6s7oIJACp*qU(6G_e%>@VOd@g1!XBUg=BU^4ReYg&h5T3U zVtXyjVR$kKQrW+cBYaJj)%G#q@e6&K)jY$^)pVjcObo?Y<{Ay(0OZx%b2RUKN%F6$ z5R*6jVx*{$(}ar#w>I(SA4|0Vq4yDkhyn({%SIDKIrK!#JGdlB9`>(`K4Mgc0^v?7 zn>dGkFJ9PQ!kUZG0Q@&3_Ijwirdt|sEV0zpehx;hTVd0h#by9z!MMup^|akj8utvV zZ|rAb+^8%z12ELI*J8IYYr`ic`+0QA?~SlE^L#2(nt zqa+!?fhW|$Hasz4C8b?SUqs+ak^vk=zEA;KfiX50Aq~@VX?~2tVw(y3yJvOvVlv{P zxv`%~oh51@l4UCkuO^PQHV|cFJ+b{fR@TpB$aZ4T923d1m4;Wd>2@|$GJDI}&p~XP zkQf0oRex77yqY-H#(`$Xzw^8P)z^8s|a ztE!iDy3*C5f8YAPSMT0_x9YuD@7?zb2ofYn5WhkGHlV3kEEZw!e~aU7+!zZlTlg=qR*lbE6Ngt*t|K%@v$GUyjnUA8?|i z6jrOvm0Ahl7h;XVU-I(uTxQf7zdB&tICGp;#3uwEsg^DX0@Bm&!kE#cke)ga;o;8u zUPrE7yN&|~595okcHzLmZ@|pXvKE9Pbo!Pp?^SvE7n+UV9AL_x-c1yQ{{ZMCPa`6X z9c{v_+-ZpK+ND`~&X-@n`x`z+{*Ika96?7V!uTDV^U9m0$V>d{(BcjlHf2iN2q|LQznhaBMassBqT%=-3<+mcxCN+Z2o+^TJmA5kxT`7d9|7-cY?R$Yp?0mv)^k3 zjI);!5f(0(hxv17!eG$OB!j_#jPz7QwQY+--+U`i?kY5J!l}~ZA8BTQnoTXKY|Mn5 zhd_A7UPeS%{MbTFoix71>_|>ZL~Lvf4jeq}m_XFGM^c}&r%H+x-qDH{bAV}rxwAm{ z6hOGWeE!^7m_Bu4OPTQ-y%G}u07t$rk|&Mq**)ovva;g3maxO=xZBr=g7hMQ7<*Yp zdMak+PHQO>6wIAv#-P*za&fFA8kV<|4K30MkDWXtS(4}&>>M7|wk=-&=W0YpN410r zPN}M{!Gn)1LT#TTs+%!&pk0}`F)Az4rrk3_d+&* zpF3l!ro+vqA_xL8UUpBeYcO6_Jrfq^I2_lk#x0*&M2+UQHd)D7F~{z@fHN2lUrc zWwS^fc&A(xSy9iLu1{#&1WC%7&b}$hnyPFL{Ze`>WCm9L2VbIbAHWW^EsCPrXhS!| zckKdEbQ~NoAxTS}T6e+@wcW8pdtHrw&?)Gj?y|hIUMG?FA?DyudetjT{ z&F<3!0;|=E@4i2VZQFNXXW>3)k#~o$8X7Y9Y})jTtFl}=z+$log$Ivq1$dl1tzSxS zJo@l_^i1$|5OZKrdh!(h@#-5mR#dE#N?V5yN}23x(}>GHKs0t(3gU%w`Q!;>@$}Ng zXx}c*3zJ(H?c!pQl{pGEH8m(Xc}kv~c((j<-Ko+dr;*rB9dLK{y$K{p8z391&Y78m zg$w3E5LzstLiY%QfQ*d%_=*)^(tK-R<=Jpd|!-4hZU6OHw2Uxu-%o{EZa0OBt1>BQWV5Fq9|f; zda8qr!2Pqc%^l=<6%LR>g#M8A4H>e+g19GRkfP4W2K18)6%Jsg1i37}tA5vU^-8yR zeTfU=f{MyYg^XEJOw1{t073XsF8jGcSy&L)R8}fyq6MN03YlCW!m60o zJ1U>w2;!C_Ds{$8in>)eASyyUiW`g1zp;1!L0{T-OR#s}*K$#HY$OyL(i9H3d3M4M zd)e_5r8sl;yiW~m&73}S7AH!}$MO|x6mnu=nX^NpoG!==OYi$S0S-M$ZCxFnw!9E@f(mEPmE&p4^Qf=C zDo@%sEIs8dXVHQg{|XVO zx!P~+HY{cI1?k1E5D!uWy}j8L_Y|e{0^GxUrAGW4sRQ zH#=6T}y*&)pC;KYQX0xI4x|AZ=C<#Penb8K| zR@PV0FGQ#X^N-9_92=MTMNSsE{>1%Ry@bHpqYRZeAxyC>A6X zFWJRw{cVr81myPKMTMLsNN`x-CPLE65me$Yo4h3;p8#Sgj#QTbO;0t^ihy)LYa-|X zKNFA+kVx5gsmTI?fHL}zdpN-+VKD<{FgiLU5`F?>0|={DjUdH-QN0on4lr+EL$@wi z2F4OV8DT7=fq@m}7xCx3T+;nyneH91crQng6`!!|qYoaiE!u`@mQ1>)iD5oM-+cfx zkrivbsC)nzUt@soU(j^@1T5Lf_cGxFxjZf$$mk42-dy*uZ2%5j0xDJRbt2@h96@Cyoou6~$Z`?ismSY@RU4a(}=+8j@>XG!KtNcrL zvsq%yA;8#7!xC8bLC>YBKqoh)UaSXRJggA^06-Bht|3-1cGg8=#j9!5=qjVz6P9W! z@hkyL0Dyq3CA<0J$`Pa*>$qem4`s%O2@@ip#7cj%^hZ5X6fC%{x7s!4) zYQ!FB^#23k%vS|OD0G*{Z4-3vBA)Hm2{j;?h_QL)D97D1_N+G1z9)8ZE&+1@H0z*f zc^^>U2wbXn-wa)-1Fk9~bXvR*AVNWu6)W6j)a`xshe94MN!V>K>k|hoNO5j716X$y zI4;{EN{nGEN0BZ*;4DYC!_c!oVjqB$_OhZ%Ag}bsm|u=n0ydN?D<42f)JW(yo>jL` z001oB$+;kWD3`~D1L>WB_?rjIm)8OZE&^58-;C5N^88AL&39D9T(rG`K=M@3v0DGR;#a)2k j!c#7+9;rNuBoO!?7n||9IY2L<00000NkvXXu0mjfsuors literal 0 HcmV?d00001 diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 8f13c593..3763f2d2 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -53,9 +53,9 @@ class ImageEnum(Enum): HOST = "host" PC = "pc" MDR = "mdr" - PROUTER = "router_green" + PROUTER = "prouter" OVS = "OVS" - EDITNODE = "document-properties" + EDITNODE = "edit-node" PLOT = "plot" TWONODE = "twonode" STOP = "stop" diff --git a/coretk/coretk/icons/docker.gif b/coretk/coretk/oldicons/docker.gif similarity index 100% rename from coretk/coretk/icons/docker.gif rename to coretk/coretk/oldicons/docker.gif diff --git a/coretk/coretk/icons/emane.gif b/coretk/coretk/oldicons/emane.gif similarity index 100% rename from coretk/coretk/icons/emane.gif rename to coretk/coretk/oldicons/emane.gif diff --git a/coretk/coretk/icons/link.gif b/coretk/coretk/oldicons/link.gif similarity index 100% rename from coretk/coretk/icons/link.gif rename to coretk/coretk/oldicons/link.gif diff --git a/coretk/coretk/icons/lxc.gif b/coretk/coretk/oldicons/lxc.gif similarity index 100% rename from coretk/coretk/icons/lxc.gif rename to coretk/coretk/oldicons/lxc.gif diff --git a/coretk/coretk/icons/marker.gif b/coretk/coretk/oldicons/marker.gif similarity index 100% rename from coretk/coretk/icons/marker.gif rename to coretk/coretk/oldicons/marker.gif diff --git a/coretk/coretk/icons/mdr.gif b/coretk/coretk/oldicons/mdr.gif similarity index 100% rename from coretk/coretk/icons/mdr.gif rename to coretk/coretk/oldicons/mdr.gif diff --git a/coretk/coretk/icons/router_green.gif b/coretk/coretk/oldicons/router_green.gif similarity index 100% rename from coretk/coretk/icons/router_green.gif rename to coretk/coretk/oldicons/router_green.gif diff --git a/coretk/coretk/icons/run.gif b/coretk/coretk/oldicons/run.gif similarity index 100% rename from coretk/coretk/icons/run.gif rename to coretk/coretk/oldicons/run.gif diff --git a/coretk/coretk/icons/twonode.gif b/coretk/coretk/oldicons/twonode.gif similarity index 100% rename from coretk/coretk/icons/twonode.gif rename to coretk/coretk/oldicons/twonode.gif diff --git a/coretk/coretk/icons/wlan.gif b/coretk/coretk/oldicons/wlan.gif similarity index 100% rename from coretk/coretk/icons/wlan.gif rename to coretk/coretk/oldicons/wlan.gif From 059b0cc316b0d1b6a7f63d11814632c1fd82f759 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 12:44:50 -0800 Subject: [PATCH 137/305] changes to fix session adding default emane configuration --- daemon/core/emulator/session.py | 6 +++--- daemon/tests/test_grpc.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 4852d026..d2d841e4 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -704,10 +704,10 @@ class Session: self.services.add_services(node, node.type, options.services) # ensure default emane configuration - if _type == NodeTypes.EMANE: - self.emane.set_model_config(_id, node.emane) + if isinstance(node, EmaneNet): + self.emane.set_model_config(_id, options.emane) # set default wlan config if needed - if _type == NodeTypes.WIRELESS_LAN: + if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 5f934b64..28005dbb 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -692,7 +692,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "platform_id_start" config_value = "2" @@ -716,7 +718,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) config_key = "bandwidth" config_value = "900000" @@ -742,7 +746,9 @@ class TestGrpc: client = CoreGrpcClient() session = grpc_server.coreemu.create_session() session.set_location(47.57917, -122.13232, 2.00000, 1.0) - emane_network = session.add_node(_type=NodeTypes.EMANE) + options = NodeOptions() + options.emane = EmaneIeee80211abgModel.name + emane_network = session.add_node(_type=NodeTypes.EMANE, options=options) session.emane.set_model(emane_network, EmaneIeee80211abgModel) # then From b983a09ae7f49fbf82e5135ad5ba479c8277c9c0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 14:41:05 -0800 Subject: [PATCH 138/305] small cleanup to emane config dialog, fixed default service storage to just use names --- coretk/coretk/coreclient.py | 33 +- coretk/coretk/dialogs/emaneconfig.py | 478 +++++++++++---------------- coretk/coretk/emaneodelnodeconfig.py | 2 +- coretk/coretk/graph.py | 10 + 4 files changed, 209 insertions(+), 314 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index beeb1c0a..dbcbcf18 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -50,6 +50,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.emane_models = [] self.observer = None # loaded configuration data @@ -70,6 +71,7 @@ class CoreClient: self.interfaces_manager = InterfaceManager() self.wlan_configs = {} self.mobility_configs = {} + self.emane_model_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) @@ -145,6 +147,10 @@ class CoreClient: self.state = session.state self.client.events(self.session_id, self.handle_events) + # get emane models + response = self.client.get_emane_models(self.session_id) + self.emane_models = response.models + # get hooks response = self.client.get_hooks(self.session_id) logging.info("joined session hooks: %s", response) @@ -240,8 +246,8 @@ class CoreClient: # get service information response = self.client.get_services() for service in response.services: - group_services = self.services.setdefault(service.group, []) - group_services.append(service) + group_services = self.services.setdefault(service.group, set()) + group_services.add(service.name) # if there are no sessions, create a new session, else join a session response = self.client.get_sessions() @@ -443,6 +449,9 @@ class CoreClient: image = None if NodeUtils.is_image_node(node_type): image = "ubuntu:latest" + emane = None + if node_type == core_pb2.NodeType.EMANE: + emane = self.emane_models[0] node = core_pb2.Node( id=node_id, type=node_type, @@ -450,6 +459,7 @@ class CoreClient: model=model, position=position, image=image, + emane=emane, ) # set default emane configuration for emane node @@ -571,25 +581,6 @@ class CoreClient: if interface_two is not None: self.interface_to_edge[(node_two.id, interface_two.id)] = token - # emane setup - # TODO: determine if this is needed - if ( - node_one.type == core_pb2.NodeType.EMANE - and node_two.type == core_pb2.NodeType.DEFAULT - ): - if node_two.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_one.node_id, node_two.node_id, interface_two.id - ) - elif ( - node_two.type == core_pb2.NodeType.EMANE - and node_one.type == core_pb2.NodeType.DEFAULT - ): - if node_one.model == "mdr": - self.emaneconfig_management.set_default_for_mdr( - node_two.node_id, node_one.node_id, interface_one.id - ) - link = core_pb2.Link( type=core_pb2.LinkType.WIRED, node_one_id=node_one.id, diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 2c885511..046b0d62 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -1,7 +1,6 @@ """ emane configuration """ - import logging import tkinter as tk import webbrowser @@ -11,271 +10,122 @@ from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.widgets import ConfigFrame -PAD_X = 2 -PAD_Y = 2 +PAD = 5 -class EmaneConfiguration(Dialog): +class GlobalEmaneDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "EMANE Configuration", modal=True) + self.config_frame = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame( + self.top, self.app, self.app.core.emane_config, borderwidth=0 + ) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PAD) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.destroy() + + +class EmaneModelDialog(Dialog): + def __init__(self, master, app, node, model): + super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) + self.node = node + self.model = f"emane_{model}" + self.config_frame = None + session_id = self.app.core.session_id + response = self.app.core.client.get_emane_model_config( + session_id, self.node.id, self.model + ) + self.config = response.config + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame.draw_config() + self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_buttons() + + def draw_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PAD) + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_apply(self): + self.config_frame.parse_config() + self.app.core.emaneconfig_management.set_custom_emane_cloud_config( + self.node.id, self.model + ) + self.destroy() + + +class EmaneConfigDialog(Dialog): def __init__(self, master, app, canvas_node): - super().__init__(master, app, "emane configuration", modal=False) + super().__init__( + master, app, f"{canvas_node.core_node.name} EMANE Configuration", modal=True + ) self.app = app self.canvas_node = canvas_node self.node = canvas_node.core_node self.radiovar = tk.IntVar() self.radiovar.set(1) - self.columnconfigure(0, weight=1) + self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] + emane_model = None + if self.emane_models: + emane_model = self.emane_models[0] + self.emane_model = tk.StringVar(value=emane_model) + self.emane_model_button = None + self.draw() - # list(string) of emane models - self.emane_models = None - - self.emane_dialog = Dialog(self, app, "emane configuration", modal=False) - self.emane_model_dialog = None - self.emane_model_combobox = None - - # draw - self.node_name_and_image() - self.emane_configuration() - self.draw_ip_subnets() - self.emane_options() + def draw(self): + self.top.columnconfigure(0, weight=1) + self.draw_emane_configuration() + self.draw_emane_models() + self.draw_emane_buttons() self.draw_apply_and_cancel() - self.emane_config_frame = None - self.options = app.core.emane_config - self.model_options = None - self.model_config_frame = None - - def create_text_variable(self, val): - """ - create a string variable for convenience - - :param str val: entry text - :return: nothing - """ - var = tk.StringVar() - var.set(val) - return var - - def choose_core(self): - logging.info("not implemented") - - def node_name_and_image(self): - f = ttk.Frame(self.top) - - lbl = ttk.Label(f, text="Node name:") - lbl.grid(row=0, column=0, padx=2, pady=2) - e = ttk.Entry(f, textvariable=self.create_text_variable("")) - e.grid(row=0, column=1, padx=2, pady=2) - - cbb = ttk.Combobox(f, values=["(none)", "core1", "core2"], state="readonly") - cbb.current(0) - cbb.grid(row=0, column=2, padx=2, pady=2) - - b = ttk.Button(f, image=self.canvas_node.image) - b.grid(row=0, column=3, padx=2, pady=2) - - f.grid(row=0, column=0, sticky="nsew") - - def save_emane_option(self): - self.emane_config_frame.parse_config() - self.emane_dialog.destroy() - - def draw_emane_options(self): - if not self.emane_dialog.winfo_exists(): - self.emane_dialog = Dialog( - self, self.app, "emane configuration", modal=False - ) - - if self.options is None: - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_config(session_id) - logging.info("emane config: %s", response) - self.options = response.config - - self.emane_dialog.top.columnconfigure(0, weight=1) - self.emane_dialog.top.rowconfigure(0, weight=1) - self.emane_config_frame = ConfigFrame( - self.emane_dialog.top, self.app, config=self.options - ) - self.emane_config_frame.draw_config() - self.emane_config_frame.grid(sticky="nsew") - - frame = ttk.Frame(self.emane_dialog.top) - frame.grid(sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - b1 = ttk.Button(frame, text="Appy", command=self.save_emane_option) - b1.grid(row=0, column=0, sticky="ew") - b2 = ttk.Button(frame, text="Cancel", command=self.emane_dialog.destroy) - b2.grid(row=0, column=1, sticky="ew") - self.emane_dialog.show() - - def save_emane_model_options(self): - """ - configure the node's emane model on the fly - - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # parse configuration - config = self.model_config_frame.parse_config() - - # add string emane_ infront for grpc call - response = self.app.core.client.set_emane_model_config( - self.app.core.session_id, self.node.id, f"emane_{model_name}", config - ) - logging.info( - "emaneconfig.py config emane model (%s), result: %s", self.node.id, response - ) - - # store the change locally - self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.node.id, f"emane_{model_name}" - ) - - self.emane_model_dialog.destroy() - - def draw_model_options(self): - """ - draw emane model configuration - - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # create the dialog and the necessry widget - if not self.emane_model_dialog or not self.emane_model_dialog.winfo_exists(): - self.emane_model_dialog = Dialog( - self, self.app, f"{model_name} configuration", modal=False - ) - self.emane_model_dialog.top.columnconfigure(0, weight=1) - self.emane_model_dialog.top.rowconfigure(0, weight=1) - - # query for configurations - session_id = self.app.core.session_id - # add string emane_ before model name for grpc call - response = self.app.core.client.get_emane_model_config( - session_id, self.node.id, f"emane_{model_name}" - ) - logging.info("emane model config %s", response) - - self.model_options = response.config - self.model_config_frame = ConfigFrame( - self.emane_model_dialog.top, self.app, config=self.model_options - ) - self.model_config_frame.grid(sticky="nsew") - self.model_config_frame.draw_config() - - frame = ttk.Frame(self.emane_model_dialog.top) - frame.grid(sticky="ew") - for i in range(2): - frame.columnconfigure(i, weight=1) - b1 = ttk.Button(frame, text="Apply", command=self.save_emane_model_options) - b1.grid(row=0, column=0, sticky="ew") - b2 = ttk.Button(frame, text="Cancel", command=self.emane_model_dialog.destroy) - b2.grid(row=0, column=1, sticky="ew") - self.emane_model_dialog.show() - - def draw_option_buttons(self, parent): - f = ttk.Frame(parent) - f.grid(row=4, column=0, sticky="nsew") - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - - image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, - text=self.emane_models[0] + " options", - image=image, - compound=tk.RIGHT, - command=self.draw_model_options, - ) - b.image = image - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - - image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, - text="EMANE options", - image=image, - compound=tk.RIGHT, - command=self.draw_emane_options, - ) - b.image = image - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - - def combobox_select(self, event): - """ - update emane model options button - - :param event: - :return: nothing - """ - # get model name - model_name = self.emane_models[self.emane_model_combobox.current()] - - # get the button and configure button text - config_frame = self.grid_slaves(row=2, column=0)[0] - option_button_frame = config_frame.grid_slaves(row=4, column=0)[0] - b = option_button_frame.grid_slaves(row=0, column=0)[0] - b.config(text=model_name + " options") - - def draw_emane_models(self, parent): - """ - create a combobox that has all the known emane models - - :param parent: parent - :return: nothing - """ - # query for all the known model names - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_models(session_id) - self.emane_models = [x.split("_")[1] for x in response.models] - - # create combo box and its binding - f = ttk.Frame(parent) - self.emane_model_combobox = ttk.Combobox( - f, values=self.emane_models, state="readonly" - ) - self.emane_model_combobox.grid() - self.emane_model_combobox.current(0) - self.emane_model_combobox.bind("<>", self.combobox_select) - f.grid(row=3, column=0, sticky="ew") - - def draw_text_label_and_entry(self, parent, label_text, entry_text): - """ - draw a label and an entry on a single row - - :return: nothing - """ - var = tk.StringVar() - var.set(entry_text) - f = ttk.Frame(parent) - lbl = ttk.Label(f, text=label_text) - lbl.grid(row=0, column=0) - e = ttk.Entry(f, textvariable=var) - e.grid(row=0, column=1) - f.grid(stick=tk.W, padx=2, pady=2) - - def emane_configuration(self): + def draw_emane_configuration(self): """ draw the main frame for emane configuration :return: nothing """ - # draw label - lbl = ttk.Label(self.top, text="Emane") - lbl.grid(row=1, column=0) - - # main frame that has emane wiki, a short description, emane models and the configure buttons - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) + label = ttk.Label( + self.top, + text="The EMANE emulation system provides more complex wireless radio emulation " + "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + ) + label.grid(sticky="ew", pady=PAD) image = Images.get(ImageEnum.EDITNODE, 16) - b = ttk.Button( - f, + button = ttk.Button( + self.top, image=image, text="EMANE Wiki", compound=tk.RIGHT, @@ -283,55 +133,99 @@ class EmaneConfiguration(Dialog): "https://github.com/adjacentlink/emane/wiki" ), ) - b.image = image - b.grid(row=0, column=0, sticky="w") + button.image = image + button.grid(sticky="ew", pady=PAD) - lbl = ttk.Label( - f, - text="The EMANE emulation system provides more complex wireless radio emulation " - "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + def draw_emane_models(self): + """ + create a combobox that has all the known emane models + + :return: nothing + """ + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + + label = ttk.Label(frame, text="Model") + label.grid(row=0, column=0, sticky="w") + + # create combo box and its binding + combobox = ttk.Combobox( + frame, + textvariable=self.emane_model, + values=self.emane_models, + state="readonly", ) - lbl.grid(row=1, column=0, sticky="nsew") + combobox.grid(row=0, column=1, sticky="ew") + combobox.bind("<>", self.emane_model_change) - lbl = ttk.Label(f, text="EMANE Models") - lbl.grid(row=2, column=0, sticky="w") + def draw_emane_buttons(self): + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + for i in range(2): + frame.columnconfigure(i, weight=1) - self.draw_emane_models(f) - self.draw_option_buttons(f) + image = Images.get(ImageEnum.EDITNODE, 16) + self.emane_model_button = ttk.Button( + frame, + text=f"{self.emane_model.get()} options", + image=image, + compound=tk.RIGHT, + command=self.click_model_config, + ) + self.emane_model_button.image = image + self.emane_model_button.grid(row=0, column=0, padx=PAD, sticky="ew") - f.grid(row=2, column=0, sticky="nsew") - - def draw_ip_subnets(self): - self.draw_text_label_and_entry(self.top, "IPv4 subnet", "") - self.draw_text_label_and_entry(self.top, "IPv6 subnet", "") - - def emane_options(self): - """ - create wireless node options - - :return: - """ - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - b = ttk.Button(f, text="Link to all routers") - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = ttk.Button(f, text="Choose WLAN members") - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") - f.grid(row=5, column=0, sticky="nsew") - - def apply(self): - # save emane configuration - self.app.core.emane_config = self.options - self.destroy() + image = Images.get(ImageEnum.EDITNODE, 16) + button = ttk.Button( + frame, + text="EMANE options", + image=image, + compound=tk.RIGHT, + command=self.click_emane_config, + ) + button.image = image + button.grid(row=0, column=1, sticky="ew") def draw_apply_and_cancel(self): - f = ttk.Frame(self.top) - f.columnconfigure(0, weight=1) - f.columnconfigure(1, weight=1) - b = ttk.Button(f, text="Apply", command=self.apply) - b.grid(row=0, column=0, padx=10, pady=2, sticky="nsew") - b = ttk.Button(f, text="Cancel", command=self.destroy) - b.grid(row=0, column=1, padx=10, pady=2, sticky="nsew") + frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(2): + frame.columnconfigure(i, weight=1) - f.grid(sticky="nsew") + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, padx=PAD, sticky="ew") + + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="ew") + + def click_emane_config(self): + dialog = GlobalEmaneDialog(self, self.app) + dialog.show() + + def click_model_config(self): + """ + draw emane model configuration + + :return: nothing + """ + model_name = self.emane_model.get() + logging.info("configuring emane model: %s", model_name) + dialog = EmaneModelDialog( + self, self.app, self.canvas_node.core_node, model_name + ) + dialog.show() + + def emane_model_change(self, event): + """ + update emane model options button + + :param event: + :return: nothing + """ + model_name = self.emane_model.get() + self.emane_model_button.config(text=f"{model_name} options") + + def click_apply(self): + self.node.emane = f"emane_{self.emane_model.get()}" + self.destroy() diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py index e3a9cc33..e1aaadbb 100644 --- a/coretk/coretk/emaneodelnodeconfig.py +++ b/coretk/coretk/emaneodelnodeconfig.py @@ -28,7 +28,7 @@ class EmaneModelNodeConfig: """ session_id = self.app.core.session_id client = self.app.core.client - default_emane_model = client.get_emane_models(session_id).models[0] + default_emane_model = self.app.core.emane_models[0] response = client.get_emane_model_config( session_id, node_id, default_emane_model ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 86ce617e..e2722c77 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -7,6 +7,7 @@ from PIL import ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType from coretk.canvastooltip import CanvasTooltip +from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog @@ -77,6 +78,10 @@ class CanvasGraph(tk.Canvas): context.add_command( label="Mobility Config", command=canvas_node.show_mobility_config ) + if node.type == NodeType.EMANE: + context.add_command( + label="EMANE Config", command=canvas_node.show_emane_config + ) context.add_command(label="Select adjacent", state=tk.DISABLED) context.add_command(label="Create link to", state=tk.DISABLED) context.add_command(label="Assign to", state=tk.DISABLED) @@ -711,3 +716,8 @@ class CanvasNode: self.canvas.context = None dialog = MobilityConfigDialog(self.app, self.app, self) dialog.show() + + def show_emane_config(self): + self.canvas.context = None + dialog = EmaneConfigDialog(self.app, self.app, self) + dialog.show() From 4cd42c2a20eb5a66c61c3ad080eb9a14f1870861 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 15:00:17 -0800 Subject: [PATCH 139/305] work on service config --- coretk/coretk/coreclient.py | 20 ++++++++- coretk/coretk/dialogs/nodeservice.py | 12 ++++- coretk/coretk/dialogs/serviceconfiguration.py | 45 +++++++++++++++---- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index beeb1c0a..e3232ef8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -50,6 +50,7 @@ class CoreClient: self.master = app.master self.interface_helper = None self.services = {} + self.default_services = {} self.observer = None # loaded configuration data @@ -77,6 +78,9 @@ class CoreClient: self.created_nodes = set() self.created_links = set() + self.service_configs = {} + self.file_configs = {} + def set_observer(self, value): self.observer = value @@ -253,9 +257,15 @@ class CoreClient: dialog = SessionsDialog(self.app, self.app) dialog.show() + response = self.client.get_service_defaults(self.session_id) + logging.debug("get service defaults: %s", response) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + def get_session_state(self): response = self.client.get_session(self.session_id) - # logging.info("get session: %s", response) + logging.info("get session: %s", response) return response.session.state def edit_node(self, node_id, x, y): @@ -358,6 +368,9 @@ class CoreClient: self.session_id, node_id, service_name, startups, validations, shutdowns ) logging.debug("set node service %s", response) + response = self.client.get_node_service(self.session_id, node_id, service_name) + logging.debug("get node service : %s", response) + return response.service def get_node_service_file(self, node_id, service_name, file_name): response = self.client.get_node_service_file( @@ -381,7 +394,10 @@ class CoreClient: node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) + if self.get_session_state() != core_pb2.SessionState.DEFINITION: + self.client.set_session_state( + self.session_id, core_pb2.SessionState.DEFINITION + ) for node_proto in node_protos: if node_proto.id not in self.created_nodes: response = self.client.add_node(self.session_id, node_proto) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 35c55b75..8a30e051 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -12,6 +12,7 @@ from coretk.widgets import CheckboxList, ListboxScroll class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): super().__init__(master, app, "Node Services", modal=True) + self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id self.groups = None @@ -108,8 +109,15 @@ class NodeService(Dialog): ) def click_save(self): - print("not implemented") - print(self.current_services) + if ( + self.current_services + != self.app.core.default_services[self.canvas_node.core_node.model] + ): + self.canvas_node.core_node.services[:] = self.current_services + else: + if len(self.canvas_node.core_node.services) > 0: + self.canvas_node.core_node.services[:] = [] + self.destroy() def click_cancel(self): self.current_services = None diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index bb1fd858..20684311 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -14,6 +14,7 @@ class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, node_id): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app + self.core = app.core self.service_manager = app.core.serviceconfig_manager self.node_id = node_id self.service_name = service_name @@ -50,18 +51,32 @@ class ServiceConfiguration(Dialog): def load(self): # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() - # load data from local memory - if self.service_name in self.service_manager.configurations[self.node_id]: - service_config = self.service_manager.configurations[self.node_id][ + + service_configs = self.app.core.service_configs + if ( + self.node_id in service_configs + and self.service_name in service_configs[self.node_id] + ): + service_config = self.app.core.service_configs[self.node_id][ self.service_name ] else: - self.service_manager.node_custom_service_configuration( + service_config = self.app.core.get_node_service( self.node_id, self.service_name ) - service_config = self.service_manager.configurations[self.node_id][ - self.service_name - ] + + # # load data from local memory + # if self.service_name in self.service_manager.configurations[self.node_id]: + # service_config = self.service_manager.configurations[self.node_id][ + # self.service_name + # ] + # else: + # self.service_manager.node_custom_service_configuration( + # self.node_id, self.service_name + # ) + # service_config = self.service_manager.configurations[self.node_id][ + # self.service_name + # ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -359,16 +374,30 @@ class ServiceConfiguration(Dialog): entry.delete(0, tk.END) def click_apply(self): + service_configs = self.app.core.service_configs startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") - self.service_manager.node_service_custom_configuration( + config = self.core.set_node_service( self.node_id, self.service_name, startup_commands, validate_commands, shutdown_commands, ) + if self.node_id not in service_configs: + service_configs[self.node_id] = {} + if self.service_name not in service_configs[self.node_id]: + self.app.core.service_configs[self.node_id][self.service_name] = config + print(self.app.core.client.get_session(self.app.core.session_id)) + + # self.service_manager.node_service_custom_configuration( + # self.node_id, + # self.service_name, + # startup_commands, + # validate_commands, + # shutdown_commands, + # ) for file in self.modified_files: self.app.core.servicefileconfig_manager.set_custom_service_file_config( self.node_id, self.service_name, file, self.temp_service_files[file] From 97cb1444f3e60ffb0f410e310e3f04789b3281d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:41:38 -0800 Subject: [PATCH 140/305] updates to emane model config storage, fixes to reconnecting to a wlan session --- coretk/coretk/coreclient.py | 86 +++++++++++---------------- coretk/coretk/dialogs/emaneconfig.py | 13 ++--- coretk/coretk/emaneodelnodeconfig.py | 78 ------------------------- coretk/coretk/graph.py | 87 +++++++++++++++------------- coretk/coretk/graph_helper.py | 30 +++++----- coretk/coretk/linkinfo.py | 1 - coretk/coretk/nodeutils.py | 7 +++ coretk/coretk/wirelessconnection.py | 10 ++-- daemon/core/nodes/network.py | 2 +- 9 files changed, 111 insertions(+), 203 deletions(-) delete mode 100644 coretk/coretk/emaneodelnodeconfig.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index dbcbcf18..229f59a5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,7 +6,6 @@ import os from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog -from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils from coretk.servicefileconfig import ServiceFileConfig @@ -72,7 +71,6 @@ class CoreClient: self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} - self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None self.serviceconfig_manager = ServiceNodeConfig(app) self.servicefileconfig_manager = ServiceFileConfig() @@ -104,7 +102,7 @@ class CoreClient: def handle_events(self, event): logging.info("event: %s", event) if event.HasField("link_event"): - self.app.canvas.wireless_draw.hangle_link_event(event.link_event) + self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event @@ -153,7 +151,6 @@ class CoreClient: # get hooks response = self.client.get_hooks(self.session_id) - logging.info("joined session hooks: %s", response) for hook in response.hooks: self.hooks[hook.file] = hook @@ -161,19 +158,16 @@ class CoreClient: for node in session.nodes: if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) - logging.debug("wlan config(%s): %s", node.id, response) self.wlan_configs[node.id] = response.config # get mobility configs response = self.client.get_mobility_configs(self.session_id) - logging.debug("mobility configs: %s", response) for node_id in response.configs: node_config = response.configs[node_id].config self.mobility_configs[node_id] = node_config # get emane config response = self.client.get_emane_config(self.session_id) - logging.debug("emane config: %s", response) self.emane_config = response.config # get emane model config @@ -462,10 +456,6 @@ class CoreClient: emane=emane, ) - # set default emane configuration for emane node - if node_type == core_pb2.NodeType.EMANE: - self.emaneconfig_management.set_default_config(node_id) - # set default service configurations # TODO: need to deal with this and custom node cases if node_type == core_pb2.NodeType.DEFAULT: @@ -492,50 +482,28 @@ class CoreClient: :return: nothing """ # delete the nodes - for node_id in node_ids: + for i in node_ids: try: - del self.canvas_nodes[node_id] - self.reusable.append(node_id) + del self.canvas_nodes[i] + self.reusable.append(i) + if i in self.mobility_configs: + del self.mobility_configs[i] + if i in self.wlan_configs: + del self.wlan_configs[i] + for key in list(self.emane_model_configs): + node_id, _, _ = key + if node_id == i: + del self.emane_model_configs[key] except KeyError: - logging.error("invalid canvas id: %s", node_id) + logging.error("invalid canvas id: %s", i) self.reusable.sort() # delete the edges and interfaces - node_interface_pairs = [] for i in edge_tokens: try: - link = self.links.pop(i) - if link.interface_one is not None: - node_interface_pairs.append( - (link.node_one_id, link.interface_one.id) - ) - if link.interface_two is not None: - node_interface_pairs.append( - (link.node_two_id, link.interface_two.id) - ) + self.links.pop(i) except KeyError: - logging.error("coreclient.py invalid edge token ") - - # delete global emane config if there no longer exist any emane cloud - # TODO: should not need to worry about this - node_types = [x.core_node.type for x in self.canvas_nodes.values()] - if core_pb2.NodeType.EMANE not in node_types: - self.emane_config = None - - # delete any mobility configuration, wlan configuration - for i in node_ids: - if i in self.mobility_configs: - del self.mobility_configs[i] - if i in self.wlan_configs: - del self.wlan_configs[i] - - # delete emane configurations - for i in node_interface_pairs: - if i in self.emaneconfig_management.configurations: - self.emaneconfig_management.configurations.pop(i) - for i in node_ids: - if tuple([i, None]) in self.emaneconfig_management.configurations: - self.emaneconfig_management.configurations.pop(tuple([i, None])) + logging.error("invalid edge token: %s", i) def create_interface(self, canvas_node): interface = None @@ -609,13 +577,11 @@ class CoreClient: def get_emane_model_configs_proto(self): configs = [] - emane_configs = self.emaneconfig_management.configurations - for key, value in emane_configs.items(): - node_id, interface_id = key - model, options = value - config = {x: options[x].value for x in options} + for key, config in self.emane_model_configs.items(): + node_id, model, interface = key + config = {x: config[x].value for x in config} config_proto = core_pb2.EmaneModelConfig( - node_id=node_id, interface_id=interface_id, model=model, config=config + node_id=node_id, interface_id=interface, model=model, config=config ) configs.append(config_proto) return configs @@ -669,3 +635,17 @@ class CoreClient: response = self.client.get_mobility_config(self.session_id, node_id) config = response.config return config + + def get_emane_model_config(self, node_id, model, interface=None): + config = self.emane_model_configs.get((node_id, model, interface)) + if not config: + if interface is None: + interface = -1 + response = self.client.get_emane_model_config( + self.session_id, node_id, model, interface + ) + config = response.config + return config + + def set_emane_model_config(self, node_id, model, config, interface=None): + self.emane_model_configs[(node_id, model, interface)] = config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 046b0d62..bb6d8f1b 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -46,16 +46,15 @@ class GlobalEmaneDialog(Dialog): class EmaneModelDialog(Dialog): - def __init__(self, master, app, node, model): + def __init__(self, master, app, node, model, interface=None): super().__init__(master, app, f"{node.name} {model} Configuration", modal=True) self.node = node self.model = f"emane_{model}" + self.interface = interface self.config_frame = None - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_model_config( - session_id, self.node.id, self.model + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface ) - self.config = response.config self.draw() def draw(self): @@ -79,8 +78,8 @@ class EmaneModelDialog(Dialog): def click_apply(self): self.config_frame.parse_config() - self.app.core.emaneconfig_management.set_custom_emane_cloud_config( - self.node.id, self.model + self.app.core.set_emane_model_config( + self.node.id, self.model, self.config, self.interface ) self.destroy() diff --git a/coretk/coretk/emaneodelnodeconfig.py b/coretk/coretk/emaneodelnodeconfig.py deleted file mode 100644 index e1aaadbb..00000000 --- a/coretk/coretk/emaneodelnodeconfig.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -emane model configurations -""" -import logging - - -class EmaneModelNodeConfig: - def __init__(self, app): - """ - create an instance for EmaneModelNodeConfig - - :param app: application - """ - # dict(tuple(node_id, interface_id, model) : config) - self.configurations = {} - - # dict(int, list(int)) stores emane node maps to mdr nodes that are linked to that emane node - self.links = {} - - self.app = app - - def set_default_config(self, node_id): - """ - set a default emane configuration for a newly created emane - - :param int node_id: node id - :return: nothing - """ - session_id = self.app.core.session_id - client = self.app.core.client - default_emane_model = self.app.core.emane_models[0] - response = client.get_emane_model_config( - session_id, node_id, default_emane_model - ) - logging.info( - "emanemodelnodeconfig.py get emane model config (%s), result: %s", - node_id, - response, - ) - self.configurations[tuple([node_id, None])] = tuple( - [default_emane_model, response.config] - ) - self.links[node_id] = [] - - def set_default_for_mdr(self, emane_node_id, mdr_node_id, interface_id): - """ - set emane configuration of an mdr node on the correct interface - - :param int emane_node_id: emane node id - :param int mdr_node_id: mdr node id - :param int interface_id: interface id - :return: nothing - """ - self.configurations[tuple([mdr_node_id, interface_id])] = self.configurations[ - tuple([emane_node_id, None]) - ] - self.links[emane_node_id].append(tuple([mdr_node_id, interface_id])) - - def set_custom_emane_cloud_config(self, emane_node_id, model_name): - """ - set custom configuration for an emane node, if model is changed, update the nodes connected to that emane node - - :param int emane_node_id: emane node id - :param str model_name: model name - :return: nothing - """ - prev_model_name = self.configurations[tuple([emane_node_id, None])][0] - session_id = self.app.core.session_id - response = self.app.core.client.get_emane_model_config( - session_id, emane_node_id, model_name - ) - self.configurations[tuple([emane_node_id, None])] = tuple( - [model_name, response.config] - ) - - if prev_model_name != model_name: - for k in self.links[emane_node_id]: - self.configurations[k] = tuple([model_name, response.config]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index e2722c77..43b8a830 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -176,48 +176,53 @@ class CanvasGraph(tk.Canvas): node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node - is_wired = link.type == core_pb2.LinkType.WIRED - edge = CanvasEdge( - node_one.position.x, - node_one.position.y, - node_two.position.x, - node_two.position.y, - canvas_node_one.id, - self, - is_wired=is_wired, - ) - edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) - edge.dst = canvas_node_two.id - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = link - self.helper.redraw_antenna(link, canvas_node_one, canvas_node_two) + if link.type == core_pb2.LinkType.WIRELESS: + self.wireless_draw.add_connection(link.node_one_id, link.node_two_id) + else: + is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) + is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) + has_no_wireless = not (is_node_one_wireless or is_node_two_wireless) + edge = CanvasEdge( + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, + canvas_node_one.id, + self, + is_wired=has_no_wireless, + ) + edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + edge.dst = canvas_node_two.id + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = link + self.helper.redraw_antenna(canvas_node_one, canvas_node_two) - # TODO add back the link info to grpc manager also redraw - # TODO will include throughput and ipv6 in the future - interface_one = link.interface_one - interface_two = link.interface_two - ip4_src = None - ip4_dst = None - ip6_src = None - ip6_dst = None - if interface_one is not None: - ip4_src = interface_one.ip4 - ip6_src = interface_one.ip6 - if interface_two is not None: - ip4_dst = interface_two.ip4 - ip6_dst = interface_two.ip6 - edge.link_info = LinkInfo( - canvas=self, - edge=edge, - ip4_src=ip4_src, - ip6_src=ip6_src, - ip4_dst=ip4_dst, - ip6_dst=ip6_dst, - ) - canvas_node_one.interfaces.append(interface_one) - canvas_node_two.interfaces.append(interface_two) + # TODO add back the link info to grpc manager also redraw + # TODO will include throughput and ipv6 in the future + interface_one = link.interface_one + interface_two = link.interface_two + ip4_src = None + ip4_dst = None + ip6_src = None + ip6_dst = None + if interface_one is not None: + ip4_src = interface_one.ip4 + ip6_src = interface_one.ip6 + if interface_two is not None: + ip4_dst = interface_two.ip4 + ip6_dst = interface_two.ip6 + edge.link_info = LinkInfo( + canvas=self, + edge=edge, + ip4_src=ip4_src, + ip6_src=ip6_src, + ip4_dst=ip4_dst, + ip6_dst=ip6_dst, + ) + canvas_node_one.interfaces.append(interface_one) + canvas_node_two.interfaces.append(interface_two) # raise the nodes so they on top of the links self.tag_raise("node") diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 476c0182..e691667b 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -4,8 +4,8 @@ Some graph helper functions import logging import tkinter as tk -from core.api.grpc import core_pb2 from coretk.images import ImageEnum, Images +from coretk.nodeutils import NodeUtils CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] @@ -31,34 +31,30 @@ class GraphHelper: def draw_wireless_case(self, src_id, dst_id, edge): src_node_type = self.canvas.nodes[src_id].core_node.type dst_node_type = self.canvas.nodes[dst_id].core_node.type - is_src_wlan = src_node_type == core_pb2.NodeType.WIRELESS_LAN - is_dst_wlan = dst_node_type == core_pb2.NodeType.WIRELESS_LAN - if is_src_wlan or is_dst_wlan: + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: self.canvas.itemconfig(edge.id, state=tk.HIDDEN) edge.wired = False if edge.token not in self.canvas.edges: - if is_src_wlan and is_dst_wlan: + if is_src_wireless and is_dst_wireless: self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif is_src_wlan: + elif is_src_wireless: self.canvas.nodes[dst_id].antenna_draw.add_antenna() else: self.canvas.nodes[src_id].antenna_draw.add_antenna() edge.wired = True - def redraw_antenna(self, link, node_one, node_two): - is_node_one_wlan = node_one.core_node.type == core_pb2.NodeType.WIRELESS_LAN - is_node_two_wlan = node_two.core_node.type == core_pb2.NodeType.WIRELESS_LAN - if link.type == core_pb2.LinkType.WIRELESS: - if is_node_one_wlan and is_node_two_wlan: - node_one.antenna_draw.add_antenna() - elif is_node_one_wlan and not is_node_two_wlan: + def redraw_antenna(self, node_one, node_two): + is_node_one_wireless = NodeUtils.is_wireless_node(node_one.core_node.type) + is_node_two_wireless = NodeUtils.is_wireless_node(node_two.core_node.type) + if is_node_one_wireless or is_node_two_wireless: + if is_node_one_wireless and not is_node_two_wireless: node_two.antenna_draw.add_antenna() - elif not is_node_one_wlan and is_node_two_wlan: + elif not is_node_one_wireless and is_node_two_wireless: node_one.antenna_draw.add_antenna() else: - logging.error( - "graph_helper.py WIRELESS link but both nodes are non-wireless node" - ) + logging.error("bad link between two wireless nodes") def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): for eid in edge_ids: diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 1e0363cf..00df0913 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -18,7 +18,6 @@ class LinkInfo: """ self.canvas = canvas self.edge = edge - # self.edge_id = edge.id self.radius = 37 self.core = self.canvas.core diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 380c9a0b..fcc29953 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -46,6 +46,7 @@ class NodeUtils: NODE_ICONS = {} CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} + WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} @classmethod @@ -60,8 +61,14 @@ class NodeUtils: def is_image_node(cls, node_type): return node_type in cls.IMAGE_NODES + @classmethod + def is_wireless_node(cls, node_type): + return node_type in cls.WIRELESS_NODES + @classmethod def node_icon(cls, node_type, model): + if model == "": + model = None return cls.NODE_ICONS[(node_type, model)] @classmethod diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index e8d03126..2e0e0af9 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -11,7 +11,7 @@ class WirelessConnection: # map a (node_one_id, node_two_id) to a wlan canvas id self.map = {} - def add_wlan_connection(self, node_one_id, node_two_id): + def add_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) @@ -25,7 +25,7 @@ class WirelessConnection: canvas_node_one.wlans.append(wlan_canvas_id) canvas_node_two.wlans.append(wlan_canvas_id) - def delete_wlan_connection(self, node_one_id, node_two_id): + def delete_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) @@ -35,13 +35,13 @@ class WirelessConnection: self.canvas.delete(wlan_canvas_id) self.map.pop(key, None) - def hangle_link_event(self, link_event): + def handle_link_event(self, link_event): if link_event.message_type == core_pb2.MessageType.ADD: - self.add_wlan_connection( + self.add_connection( link_event.link.node_one_id, link_event.link.node_two_id ) if link_event.message_type == core_pb2.MessageType.DELETE: - self.delete_wlan_connection( + self.delete_connection( link_event.link.node_one_id, link_event.link.node_two_id ) diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 80a730e2..6fe291dd 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -1017,7 +1017,7 @@ class WlanNode(CoreNetwork): """ apitype = NodeTypes.WIRELESS_LAN.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value policy = "DROP" type = "wlan" From c21f06079736f53bce0139cd1ccb77af50deae46 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 21 Nov 2019 16:59:55 -0800 Subject: [PATCH 141/305] service file config --- coretk/coretk/coreclient.py | 44 ++++++++++++------- coretk/coretk/dialogs/nodeservice.py | 8 ++++ coretk/coretk/dialogs/serviceconfiguration.py | 26 ++++++++--- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 62c33ee7..6b527c8c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -312,7 +312,6 @@ class CoreClient: emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() - print(service_configs) # service_file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() @@ -397,9 +396,9 @@ class CoreClient: :return: nothing """ - node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) + print(node_protos) if self.get_session_state() != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -426,6 +425,7 @@ class CoreClient: self.created_links.add( tuple([link_proto.node_one_id, link_proto.node_two_id]) ) + print(self.app.core.client.get_session(self.app.core.session_id)) def close(self): """ @@ -638,20 +638,32 @@ class CoreClient: def get_service_config_proto(self): configs = [] - for ( - node_id, - service_configs, - ) in self.serviceconfig_manager.configurations.items(): - for service, config in service_configs.items(): - if service in self.serviceconfig_manager.current_services[node_id]: - config = core_pb2.ServiceConfig( - node_id=node_id, - service=service, - startup=config.startup, - validate=config.validate, - shutdown=config.shutdown, - ) - configs.append(config) + for node_id, services in self.service_configs.items(): + for name, config in services.items(): + config_proto = core_pb2.ServiceConfig( + node_id=node_id, + service=name, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + configs.append(config_proto) + + # configs = [] + # for ( + # node_id, + # service_configs, + # ) in self.serviceconfig_manager.configurations.items(): + # for service, config in service_configs.items(): + # if service in self.serviceconfig_manager.current_services[node_id]: + # config = core_pb2.ServiceConfig( + # node_id=node_id, + # service=service, + # startup=config.startup, + # validate=config.validate, + # shutdown=config.shutdown, + # ) + # configs.append(config) return configs def get_service_file_config_proto(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 6293f9d9..9b3d8978 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -22,6 +22,14 @@ class NodeService(Dialog): services = set( app.core.serviceconfig_manager.current_services[self.node_id] ) + if services is None: + services = canvas_node.core_node.services + model = canvas_node.core_node.model + if len(services) == 0: + services = set(self.app.core.default_services[model]) + else: + services = set(services) + self.current_services = services self.service_manager = self.app.core.serviceconfig_manager self.service_file_manager = self.app.core.servicefileconfig_manager diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 20684311..7505bb40 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -93,9 +93,13 @@ class ServiceConfiguration(Dialog): self.temp_service_files = { x: self.original_service_files[x] for x in self.original_service_files } - configs = self.app.core.servicefileconfig_manager.configurations - if self.node_id in configs and self.service_name in configs[self.node_id]: - for file, data in configs[self.node_id][self.service_name].items(): + # configs = self.app.core.servicefileconfig_manager.configurations + file_configs = self.app.core.file_configs + if ( + self.node_id in file_configs + and self.service_name in file_configs[self.node_id] + ): + for file, data in file_configs[self.node_id][self.service_name].items(): self.temp_service_files[file] = data def draw(self): @@ -389,7 +393,6 @@ class ServiceConfiguration(Dialog): service_configs[self.node_id] = {} if self.service_name not in service_configs[self.node_id]: self.app.core.service_configs[self.node_id][self.service_name] = config - print(self.app.core.client.get_session(self.app.core.session_id)) # self.service_manager.node_service_custom_configuration( # self.node_id, @@ -399,9 +402,18 @@ class ServiceConfiguration(Dialog): # shutdown_commands, # ) for file in self.modified_files: - self.app.core.servicefileconfig_manager.set_custom_service_file_config( - self.node_id, self.service_name, file, self.temp_service_files[file] - ) + # self.app.core.servicefileconfig_manager.set_custom_service_file_config( + # self.node_id, self.service_name, file, self.temp_service_files[file] + # ) + file_configs = self.app.core.file_configs + if self.node_id not in file_configs: + file_configs[self.node_id] = {} + if self.service_name not in file_configs[self.node_id]: + file_configs[self.node_id][self.service_name] = {} + file_configs[self.node_id][self.service_name][ + file + ] = self.temp_service_files[file] + self.app.core.set_node_service_file( self.node_id, self.service_name, file, self.temp_service_files[file] ) From df9c7308dbb6457bd434cb21da86048026cfac71 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 17:03:18 -0800 Subject: [PATCH 142/305] update to avoid issue when old gui creates emane nodes without emane models --- daemon/core/emulator/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index d2d841e4..fee2bde4 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -704,7 +704,7 @@ class Session: self.services.add_services(node, node.type, options.services) # ensure default emane configuration - if isinstance(node, EmaneNet): + if isinstance(node, EmaneNet) and options.emane: self.emane.set_model_config(_id, options.emane) # set default wlan config if needed if isinstance(node, WlanNode): From bb7bad89d32086dae383626cda4891cae2e932b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 17:14:23 -0800 Subject: [PATCH 143/305] added a default location for now to get emane working, until session location is supported --- coretk/coretk/coreclient.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 229f59a5..49489798 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -60,6 +60,9 @@ class CoreClient: # data for managing the current session self.canvas_nodes = {} + self.location = core_pb2.SessionLocation( + x=0, y=0, z=0, lat=47.5791667, lon=-122.132322, alt=2.0, scale=150.0 + ) self.interface_to_edge = {} self.state = None self.links = {} @@ -308,12 +311,13 @@ class CoreClient: self.session_id, nodes, links, - hooks=hooks, - wlan_configs=wlan_configs, - emane_config=emane_config, - emane_model_configs=emane_model_configs, - mobility_configs=mobility_configs, - service_configs=service_configs, + self.location, + hooks, + emane_config, + emane_model_configs, + wlan_configs, + mobility_configs, + service_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) From fbbf31f4fa09fbee28895919171ffc34554f87d6 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 21:56:28 -0800 Subject: [PATCH 144/305] added saving session location to config, and query location when joining a session --- coretk/coretk/appconfig.py | 9 ++ coretk/coretk/coreclient.py | 23 ++++-- coretk/coretk/dialogs/canvassizeandscale.py | 92 ++++++++++++--------- coretk/coretk/graph.py | 1 - 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index e8f5db6e..2af1047c 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -75,6 +75,15 @@ def check_directory(): "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", }, + "location": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "lat": 47.5791667, + "lon": -122.132322, + "alt": 2.0, + "scale": 150.0, + }, "servers": [{"name": "example", "address": "127.0.0.1", "port": 50051}], "nodes": [], "observers": [{"name": "hello", "cmd": "echo hello"}], diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 49489798..d801875e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -60,9 +60,7 @@ class CoreClient: # data for managing the current session self.canvas_nodes = {} - self.location = core_pb2.SessionLocation( - x=0, y=0, z=0, lat=47.5791667, lon=-122.132322, alt=2.0, scale=150.0 - ) + self.location = None self.interface_to_edge = {} self.state = None self.links = {} @@ -123,7 +121,7 @@ class CoreClient: throughputs_belong_to_session ) - def join_session(self, session_id): + def join_session(self, session_id, query_location=True): self.master.config(cursor="watch") self.master.update() @@ -148,6 +146,11 @@ class CoreClient: self.state = session.state self.client.events(self.session_id, self.handle_events) + # get location + if query_location: + response = self.client.get_session_location(self.session_id) + self.location = response.location + # get emane models response = self.client.get_emane_models(self.session_id) self.emane_models = response.models @@ -207,7 +210,17 @@ class CoreClient: """ response = self.client.create_session() logging.info("created session: %s", response) - self.join_session(response.session_id) + location_config = self.app.config["location"] + self.location = core_pb2.SessionLocation( + x=location_config["x"], + y=location_config["y"], + z=location_config["z"], + lat=location_config["lat"], + lon=location_config["lon"], + alt=location_config["alt"], + scale=location_config["scale"], + ) + self.join_session(response.session_id, query_location=False) def delete_session(self, custom_sid=None): if custom_sid is None: diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 3a72389d..0d88b11f 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -6,8 +6,8 @@ from tkinter import font, ttk from coretk.dialogs.dialog import Dialog -FRAME_PAD = 5 -PADX = 5 +PAD = 5 +PIXEL_SCALE = 100 class SizeAndScaleDialog(Dialog): @@ -19,9 +19,7 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(master, app, "Canvas Size and Scale", modal=True) self.canvas = self.app.canvas - self.meter_per_pixel = self.canvas.meters_per_pixel self.section_font = font.Font(weight="bold") - # get current canvas dimensions plot = self.canvas.find_withtag("rectangle") x0, y0, x1, y1 = self.canvas.bbox(plot[0]) @@ -29,14 +27,15 @@ class SizeAndScaleDialog(Dialog): height = abs(y0 - y1) - 2 self.pixel_width = tk.IntVar(value=width) self.pixel_height = tk.IntVar(value=height) - self.meters_width = tk.IntVar(value=width * self.meter_per_pixel) - self.meters_height = tk.IntVar(value=height * self.meter_per_pixel) - self.scale = tk.IntVar(value=self.meter_per_pixel * 100) - self.x = tk.IntVar(value=0) - self.y = tk.IntVar(value=0) - self.lat = tk.DoubleVar(value=47.5791667) - self.lon = tk.DoubleVar(value=-122.132322) - self.alt = tk.DoubleVar(value=2.0) + location = self.app.core.location + self.x = tk.DoubleVar(value=location.x) + self.y = tk.DoubleVar(value=location.y) + self.lat = tk.DoubleVar(value=location.lat) + self.lon = tk.DoubleVar(value=location.lon) + self.alt = tk.DoubleVar(value=location.alt) + self.scale = tk.DoubleVar(value=location.scale) + self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) + self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) self.draw() @@ -49,7 +48,7 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -59,13 +58,13 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.pixel_width) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.pixel_height) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -75,35 +74,33 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.meters_width) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.meters_height) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) - label = ttk.Label(frame, text="100 Pixels =") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.scale) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe( - self.top, text="Reference Point", padding=FRAME_PAD - ) + label_frame = ttk.Labelframe(self.top, text="Reference Point", padding=PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -118,14 +115,14 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.x) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.y) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") label.grid() @@ -137,17 +134,17 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w", padx=PADX) + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.lat) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) + entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w", padx=PADX) + label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.lon) - entry.grid(row=0, column=3, sticky="ew", padx=PADX) + entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w", padx=PADX) + label.grid(row=0, column=4, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.alt) entry.grid(row=0, column=5, sticky="ew") @@ -164,16 +161,31 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") def click_apply(self): - meter_per_pixel = float(self.scale.get()) / 100 width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.meters_per_pixel = meter_per_pixel self.canvas.redraw_grid(width, height) if self.canvas.wallpaper: self.canvas.redraw() + location = self.app.core.location + location.x = self.x.get() + location.y = self.y.get() + location.lat = self.lat.get() + location.lon = self.lon.get() + location.alt = self.alt.get() + location.scale = self.scale.get() + if self.save_default.get(): + location_config = self.app.config["location"] + location_config["x"] = location.x + location_config["y"] = location.y + location_config["z"] = location.z + location_config["lat"] = location.lat + location_config["lon"] = location.lon + location_config["alt"] = location.alt + location_config["scale"] = location.scale + self.app.save_config() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 43b8a830..2c874f34 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -49,7 +49,6 @@ class CanvasGraph(tk.Canvas): self.edges = {} self.drawing_edge = None self.grid = None - self.meters_per_pixel = 1.5 self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() self.draw_grid() From a6cdd63570ec65d3d9e3c618c12c7c00c68484f2 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:03:07 -0800 Subject: [PATCH 145/305] fixed name issue with app config, renamed to guiconfig --- coretk/coretk/app.py | 6 +++--- coretk/coretk/coreclient.py | 8 ++++---- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/dialogs/customnodes.py | 6 +++--- coretk/coretk/dialogs/observers.py | 2 +- coretk/coretk/dialogs/preferences.py | 4 ++-- coretk/coretk/dialogs/servers.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index fbce02e1..68060927 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -26,7 +26,7 @@ class Application(tk.Frame): self.statusbar = None # setup - self.config = appconfig.read() + self.guiconfig = appconfig.read() self.style = ttk.Style() self.setup_theme() self.core = CoreClient(self) @@ -36,7 +36,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) - self.style.theme_use(self.config["preferences"]["theme"]) + self.style.theme_use(self.guiconfig["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) @@ -88,7 +88,7 @@ class Application(tk.Frame): menu_action.on_quit() def save_config(self): - appconfig.save(self.config) + appconfig.save(self.guiconfig) if __name__ == "__main__": diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d801875e..b64aab98 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -83,12 +83,12 @@ class CoreClient: def read_config(self): # read distributed server - for config in self.app.config.get("servers", []): + for config in self.app.guiconfig.get("servers", []): server = CoreServer(config["name"], config["address"], config["port"]) self.servers[server.name] = server # read custom nodes - for config in self.app.config.get("nodes", []): + for config in self.app.guiconfig.get("nodes", []): name = config["name"] image_file = config["image"] services = set(config["services"]) @@ -96,7 +96,7 @@ class CoreClient: self.custom_nodes[name] = node_draw # read observers - for config in self.app.config.get("observers", []): + for config in self.app.guiconfig.get("observers", []): observer = Observer(config["name"], config["cmd"]) self.custom_observers[observer.name] = observer @@ -210,7 +210,7 @@ class CoreClient: """ response = self.client.create_session() logging.info("created session: %s", response) - location_config = self.app.config["location"] + location_config = self.app.guiconfig["location"] self.location = core_pb2.SessionLocation( x=location_config["x"], y=location_config["y"], diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 0d88b11f..f25dfdd6 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -179,7 +179,7 @@ class SizeAndScaleDialog(Dialog): location.alt = self.alt.get() location.scale = self.scale.get() if self.save_default.get(): - location_config = self.app.config["location"] + location_config = self.app.guiconfig["location"] location_config["x"] = location.x location_config["y"] = location.y location_config["z"] = location.z diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index da7a1fb0..55199541 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -180,17 +180,17 @@ class CustomNodesDialog(Dialog): self.services.update(dialog.current_services) def click_save(self): - self.app.config["nodes"].clear() + self.app.guiconfig["nodes"].clear() for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] - self.app.config["nodes"].append( + self.app.guiconfig["nodes"].append( { "name": name, "image": node_draw.image_file, "services": list(node_draw.services), } ) - logging.info("saving custom nodes: %s", self.app.config["nodes"]) + logging.info("saving custom nodes: %s", self.app.guiconfig["nodes"]) self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 78c5811f..58499bd7 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -95,7 +95,7 @@ class ObserverDialog(Dialog): for name in sorted(self.app.core.custom_observers): observer = self.app.core.custom_observers[name] observers.append({"name": observer.name, "cmd": observer.cmd}) - self.app.config["observers"] = observers + self.app.guiconfig["observers"] = observers self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 148cb6bb..7298f727 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -9,7 +9,7 @@ from coretk.dialogs.dialog import Dialog class PreferencesDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Preferences", modal=True) - preferences = self.app.config["preferences"] + preferences = self.app.guiconfig["preferences"] self.editor = tk.StringVar(value=preferences["editor"]) self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) @@ -76,7 +76,7 @@ class PreferencesDialog(Dialog): self.app.style.theme_use(theme) def click_save(self): - preferences = self.app.config["preferences"] + preferences = self.app.guiconfig["preferences"] preferences["terminal"] = self.terminal.get() preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index d3db22e7..10a6e79e 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -115,7 +115,7 @@ class ServersDialog(Dialog): servers.append( {"name": server.name, "address": server.address, "port": server.port} ) - self.app.config["servers"] = servers + self.app.guiconfig["servers"] = servers self.app.save_config() self.destroy() From 72e9ae75eb1f0a4579f6324bf72da5347faa2fb3 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:39:39 -0800 Subject: [PATCH 146/305] fixed issue with changing themes and abg colors not being present --- coretk/coretk/menubar.py | 6 ++---- coretk/coretk/themes.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 621510e2..d9508681 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -444,9 +444,7 @@ class Menubar(tk.Menu): menu.add_command(label="Comments...", state=tk.DISABLED) menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) menu.add_command(label="Reset node positions", state=tk.DISABLED) - menu.add_command( - label="Emulation servers...", command=self.menuaction.session_servers - ) + menu.add_command(label="Servers...", command=self.menuaction.session_servers) menu.add_command(label="Options...", command=self.menuaction.session_options) self.add_cascade(label="Session", menu=menu) @@ -458,7 +456,7 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Core Github (www)", command=self.menuaction.help_core_github + label="Core GitHub (www)", command=self.menuaction.help_core_github ) menu.add_command( label="Core Documentation (www)", diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 267c3a3c..ad91276a 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -137,6 +137,8 @@ def update_menu(style, event): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") + if not abg: + abg = bg event.widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) From 8ff63219a3f5f13b93865b2180be93a7f7e074bc Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 21 Nov 2019 22:55:37 -0800 Subject: [PATCH 147/305] increased node icon size, added improve way to offset text regardless of icon size --- coretk/coretk/graph.py | 6 +++++- coretk/coretk/images.py | 2 -- coretk/coretk/nodeutils.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 2c874f34..545cbb41 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -18,6 +18,8 @@ from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils from coretk.wirelessconnection import WirelessConnection +NODE_TEXT_OFFSET = 5 + class GraphMode(enum.Enum): SELECT = 0 @@ -615,8 +617,10 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags="node" ) + image_box = self.canvas.bbox(self.id) + y = image_box[3] + NODE_TEXT_OFFSET self.text_id = self.canvas.create_text( - x, y + 20, text=self.core_node.name, tags="nodename" + x, y, text=self.core_node.name, tags="nodename" ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index 3763f2d2..287a7359 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -4,8 +4,6 @@ from PIL import Image, ImageTk from coretk.appconfig import LOCAL_ICONS_PATH -NODE_WIDTH = 32 - class Images: images = {} diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index fcc29953..18b468b5 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -1,7 +1,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.images import ImageEnum, Images -ICON_SIZE = 32 +ICON_SIZE = 48 class NodeDraw: From 6c0d4d3a9372382f87fd86788607b0c08dcbc19d Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 09:09:00 -0800 Subject: [PATCH 148/305] updated node labels --- coretk/coretk/graph.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 545cbb41..b8749e1d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -1,6 +1,7 @@ import enum import logging import tkinter as tk +from tkinter import font from PIL import ImageTk @@ -619,8 +620,14 @@ class CanvasNode: ) image_box = self.canvas.bbox(self.id) y = image_box[3] + NODE_TEXT_OFFSET + text_font = font.Font(family="TkIconFont", size=12) self.text_id = self.canvas.create_text( - x, y, text=self.core_node.name, tags="nodename" + x, + y, + text=self.core_node.name, + tags="nodename", + font=text_font, + fill="#0000CD", ) self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) @@ -631,7 +638,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() self.interfaces = [] self.wlans = [] From 3a73b10902a5c9cf5b49a1980efc2da035712d52 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 10:01:36 -0800 Subject: [PATCH 149/305] service file configs --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 6b527c8c..92e2df54 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -312,7 +312,7 @@ class CoreClient: emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() - # service_file_configs = self.get_service_file_config_proto() + file_configs = self.get_service_file_config_proto() self.created_links.clear() self.created_nodes.clear() if self.emane_config: @@ -329,6 +329,7 @@ class CoreClient: emane_model_configs=emane_model_configs, mobility_configs=mobility_configs, service_configs=service_configs, + service_file_configs=file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) @@ -483,11 +484,10 @@ class CoreClient: self.emaneconfig_management.set_default_config(node_id) # set default service configurations - # TODO: need to deal with this and custom node cases - if node_type == core_pb2.NodeType.DEFAULT: - self.serviceconfig_manager.node_default_services_configuration( - node_id=node_id, node_model=model - ) + # if node_type == core_pb2.NodeType.DEFAULT: + # self.serviceconfig_manager.node_default_services_configuration( + # node_id=node_id, node_model=model + # ) logging.debug( "adding node to core session: %s, coords: (%s, %s), name: %s", @@ -668,16 +668,23 @@ class CoreClient: def get_service_file_config_proto(self): configs = [] - for ( - node_id, - service_file_configs, - ) in self.servicefileconfig_manager.configurations.items(): - for service, file_configs in service_file_configs.items(): - for file, data in file_configs.items(): - config = core_pb2.ServiceFileConfig( + for (node_id, file_configs) in self.file_configs.items(): + for service, file_config in file_configs.items(): + for file, data in file_config.items(): + config_proto = core_pb2.ServiceFileConfig( node_id=node_id, service=service, file=file, data=data ) - configs.append(config) + configs.append(config_proto) + # for ( + # node_id, + # service_file_configs, + # ) in self.servicefileconfig_manager.configurations.items(): + # for service, file_configs in service_file_configs.items(): + # for file, data in file_configs.items(): + # config = core_pb2.ServiceFileConfig( + # node_id=node_id, service=service, file=file, data=data + # ) + # configs.append(config) return configs def run(self, node_id): From e39db4bd639fa563f82282b12e2e598fa8d3053e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 10:32:25 -0800 Subject: [PATCH 150/305] get rid of 2 unnecessary classes for service config --- coretk/coretk/coreclient.py | 39 ++----------------- coretk/coretk/dialogs/nodeservice.py | 15 +------ coretk/coretk/dialogs/serviceconfiguration.py | 26 ------------- .../{ => todelete}/servicefileconfig.py | 0 .../{ => todelete}/servicenodeconfig.py | 0 5 files changed, 4 insertions(+), 76 deletions(-) rename coretk/coretk/{ => todelete}/servicefileconfig.py (100%) rename coretk/coretk/{ => todelete}/servicenodeconfig.py (100%) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 92e2df54..089a352e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -9,8 +9,6 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.emaneodelnodeconfig import EmaneModelNodeConfig from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.servicefileconfig import ServiceFileConfig -from coretk.servicenodeconfig import ServiceNodeConfig OBSERVERS = { "processes": "ps", @@ -75,8 +73,6 @@ class CoreClient: self.emane_model_configs = {} self.emaneconfig_management = EmaneModelNodeConfig(app) self.emane_config = None - self.serviceconfig_manager = ServiceNodeConfig(app) - self.servicefileconfig_manager = ServiceFileConfig() self.created_nodes = set() self.created_links = set() @@ -143,6 +139,8 @@ class CoreClient: self.wlan_configs.clear() self.mobility_configs.clear() self.emane_config = None + self.service_configs.clear() + self.file_configs.clear() # get session data response = self.client.get_session(self.session_id) @@ -332,6 +330,7 @@ class CoreClient: service_file_configs=file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + print(self.client.get_session(self.session_id)) def stop_session(self): response = self.client.stop_session(session_id=self.session_id) @@ -483,12 +482,6 @@ class CoreClient: if node_type == core_pb2.NodeType.EMANE: self.emaneconfig_management.set_default_config(node_id) - # set default service configurations - # if node_type == core_pb2.NodeType.DEFAULT: - # self.serviceconfig_manager.node_default_services_configuration( - # node_id=node_id, node_model=model - # ) - logging.debug( "adding node to core session: %s, coords: (%s, %s), name: %s", self.session_id, @@ -648,22 +641,6 @@ class CoreClient: shutdown=config.shutdown, ) configs.append(config_proto) - - # configs = [] - # for ( - # node_id, - # service_configs, - # ) in self.serviceconfig_manager.configurations.items(): - # for service, config in service_configs.items(): - # if service in self.serviceconfig_manager.current_services[node_id]: - # config = core_pb2.ServiceConfig( - # node_id=node_id, - # service=service, - # startup=config.startup, - # validate=config.validate, - # shutdown=config.shutdown, - # ) - # configs.append(config) return configs def get_service_file_config_proto(self): @@ -675,16 +652,6 @@ class CoreClient: node_id=node_id, service=service, file=file, data=data ) configs.append(config_proto) - # for ( - # node_id, - # service_file_configs, - # ) in self.servicefileconfig_manager.configurations.items(): - # for service, file_configs in service_file_configs.items(): - # for file, data in file_configs.items(): - # config = core_pb2.ServiceFileConfig( - # node_id=node_id, service=service, file=file, data=data - # ) - # configs.append(config) return configs def run(self, node_id): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 9b3d8978..35142497 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -18,10 +18,6 @@ class NodeService(Dialog): self.groups = None self.services = None self.current = None - if services is None: - services = set( - app.core.serviceconfig_manager.current_services[self.node_id] - ) if services is None: services = canvas_node.core_node.services model = canvas_node.core_node.model @@ -31,8 +27,6 @@ class NodeService(Dialog): services = set(services) self.current_services = services - self.service_manager = self.app.core.serviceconfig_manager - self.service_file_manager = self.app.core.servicefileconfig_manager self.draw() def draw(self): @@ -87,16 +81,9 @@ class NodeService(Dialog): def service_clicked(self, name, var): if var.get() and name not in self.current_services: - if self.service_manager.node_new_service_configuration(self.node_id, name): - self.current_services.add(name) - else: - for checkbutton in self.services.frame.winfo_children(): - if name == checkbutton.cget("text"): - checkbutton.config(variable=tk.BooleanVar(value=False)) - + self.current_services.add(name) elif not var.get() and name in self.current_services: self.current_services.remove(name) - self.service_manager.current_services[self.node_id].remove(name) self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 7505bb40..1d8059d8 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -15,7 +15,6 @@ class ServiceConfiguration(Dialog): super().__init__(master, app, f"{service_name} service", modal=True) self.app = app self.core = app.core - self.service_manager = app.core.serviceconfig_manager self.node_id = node_id self.service_name = service_name self.radiovar = tk.IntVar() @@ -64,19 +63,6 @@ class ServiceConfiguration(Dialog): service_config = self.app.core.get_node_service( self.node_id, self.service_name ) - - # # load data from local memory - # if self.service_name in self.service_manager.configurations[self.node_id]: - # service_config = self.service_manager.configurations[self.node_id][ - # self.service_name - # ] - # else: - # self.service_manager.node_custom_service_configuration( - # self.node_id, self.service_name - # ) - # service_config = self.service_manager.configurations[self.node_id][ - # self.service_name - # ] self.dependencies = [x for x in service_config.dependencies] self.executables = [x for x in service_config.executables] self.metadata = service_config.meta @@ -93,7 +79,6 @@ class ServiceConfiguration(Dialog): self.temp_service_files = { x: self.original_service_files[x] for x in self.original_service_files } - # configs = self.app.core.servicefileconfig_manager.configurations file_configs = self.app.core.file_configs if ( self.node_id in file_configs @@ -393,18 +378,7 @@ class ServiceConfiguration(Dialog): service_configs[self.node_id] = {} if self.service_name not in service_configs[self.node_id]: self.app.core.service_configs[self.node_id][self.service_name] = config - - # self.service_manager.node_service_custom_configuration( - # self.node_id, - # self.service_name, - # startup_commands, - # validate_commands, - # shutdown_commands, - # ) for file in self.modified_files: - # self.app.core.servicefileconfig_manager.set_custom_service_file_config( - # self.node_id, self.service_name, file, self.temp_service_files[file] - # ) file_configs = self.app.core.file_configs if self.node_id not in file_configs: file_configs[self.node_id] = {} diff --git a/coretk/coretk/servicefileconfig.py b/coretk/coretk/todelete/servicefileconfig.py similarity index 100% rename from coretk/coretk/servicefileconfig.py rename to coretk/coretk/todelete/servicefileconfig.py diff --git a/coretk/coretk/servicenodeconfig.py b/coretk/coretk/todelete/servicenodeconfig.py similarity index 100% rename from coretk/coretk/servicenodeconfig.py rename to coretk/coretk/todelete/servicenodeconfig.py From f2b37d32c8c011c78f0de85f01a3346530279661 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 12:51:58 -0800 Subject: [PATCH 151/305] added netaddr library, added logic to support dealing with creating new subnet when needed --- coretk/Pipfile | 5 +- coretk/Pipfile.lock | 109 +++++++++++++++++++----------------- coretk/coretk/coreclient.py | 78 +++++++++++++------------- coretk/coretk/graph.py | 12 ++-- coretk/coretk/interface.py | 103 ++++++++++++++++++++++------------ coretk/setup.py | 2 +- 6 files changed, 175 insertions(+), 134 deletions(-) diff --git a/coretk/Pipfile b/coretk/Pipfile index dfeb664c..b30b3d54 100644 --- a/coretk/Pipfile +++ b/coretk/Pipfile @@ -13,6 +13,7 @@ black = "==19.3b0" pre-commit = "*" [packages] -coretk = {editable = true,path = "."} -core = {editable = true,path = "./../daemon"} +coretk = {path = ".",editable = true} +core = {path = "./../daemon",editable = true} pyyaml = "*" +netaddr = "*" diff --git a/coretk/Pipfile.lock b/coretk/Pipfile.lock index 07906a87..7ff33122 100644 --- a/coretk/Pipfile.lock +++ b/coretk/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb59f8233d6608de2d67743d8d8afe51c929f837cf6cf4d991ffc79ab5134910" + "sha256": "9024ff4821ee3ccffee21a83f5436953371ad7d64a81a22b6c3723002c92b2cd" }, "pipfile-spec": 6, "requires": {}, @@ -50,6 +50,7 @@ "sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", + "sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", @@ -117,56 +118,56 @@ }, "grpcio": { "hashes": [ - "sha256:01cb705eafba1108e2a947ba0457da4f6a1e8142c729fc61702b5fdd11009eb1", - "sha256:0b5a79e29f167d3cd06faad6b15babbc2661066daaacf79373c3a8e67ca1fca1", - "sha256:1097a61a0e97b3580642e6e1460a3a1f1ba1815e2a70d6057173bcc495417076", - "sha256:13970e665a4ec4cec7d067d7d3504a0398c657d91d26c581144ad9044e429c9a", - "sha256:1557817cea6e0b87fad2a3e20da385170efb03a313db164e8078955add2dfa1b", - "sha256:1b0fb036a2f9dd93d9a35c57c26420eeb4b571fcb14b51cddf5b1e73ea5d882b", - "sha256:24d9e58d08e8cd545d8a3247a18654aff0e5e60414701696a8098fbb0d792b75", - "sha256:2c38b586163d2b91567fe5e6d9e7798f792012365adc838a64b66b22dce3f4d4", - "sha256:2df3ab4348507de60e1cbf75196403df1b9b4c4d4dc5bd11ac4eb63c46f691c7", - "sha256:32f70f7c90454ea568b868af2e96616743718d9233d23f62407e98caed81dfbf", - "sha256:3af2a49d576820045c9c880ff29a5a96d020fe31b35d248519bfc6ccb8be4eac", - "sha256:4ff7d63800a63db031ebac6a6f581ae84877c959401c24c28f2cc51fd36c47ad", - "sha256:502aaa8be56f0ae69cda66bc27e1fb5531ceaa27ca515ec3c34f6178b1297180", - "sha256:55358ce3ec283222e435f7dbc6603521438458f3c65f7c1cb33b8dabf56d70d8", - "sha256:5583b01c67f85fa64a2c3fb085e5517c88b9c1500a2cce12d473cd99d0ed2e49", - "sha256:58d9a5557d3eb7b734a3cea8b16c891099a522b3953a45a30bd4c034f75fc913", - "sha256:5911f042c4ab177757eec5bcb4e2e9a2e823d888835d24577321bf55f02938fa", - "sha256:5e16ea922f4e5017c04fd94e2639b1006e03097e9dd0cbb7a1c852af3ea8bf2e", - "sha256:656e19d3f1b9050ee01b457f92838a9679d7cf84c995f708780f44484048705e", - "sha256:6a1435449a82008c451c7e1a82a834387b9108f9a8d27910f86e7c482f5568e9", - "sha256:6ff02ca6cbed0ddb76e93ba0f8beb6a8c77d83a84eb7cafe2ae3399a8b9d69ea", - "sha256:76de68f60102f333bf4817f38e81ecbee68b850f5a5da9f355235e948ac40981", - "sha256:7c6d7ddd50fc6548ea1dfe09c62509c4f95b8b40082287747be05aa8feb15ee2", - "sha256:836b9d29507de729129e363276fe7c7d6a34c7961e0f155787025552b15d22c0", - "sha256:869242b2baf8a888a4fe0548f86abc47cb4b48bdfd76ae62d6456e939c202e65", - "sha256:8954b24bd08641d906ee50b2d638efc76df893fbd0913149b80484fd0eac40c9", - "sha256:8cdea65d1abb2e698420db8daf20c8d272fbd9d96a51b26a713c1c76f237d181", - "sha256:90161840b4fe9636f91ed0d3ea1e7e615e488cbea4e77594c889e5f3d7a776db", - "sha256:90fb6316b4d7d36700c40db4335902b78dcae13b5466673c21fd3b08a3c1b0c6", - "sha256:91b34f58db2611c9a93ecf751028f97fba1f06e65f49b38f272f6aa5d2977331", - "sha256:9474944a96a33eb8734fa8dc5805403d57973a3526204a5e1c1780d02e0572b6", - "sha256:9a36275db2a4774ac16c6822e7af816ee048071d5030b4c035fd53942b361935", - "sha256:9cbe26e2976b994c5f7c2d35a63354674d6ca0ce62f5b513f078bf63c1745229", - "sha256:9eaeabb3c0eecd6ddd0c16767fd12d130e2cebb8c2618f959a278b1ff336ddc3", - "sha256:a2bc7e10ebcf4be503ae427f9887e75c0cc24e88ce467a8e6eaca6bd2862406e", - "sha256:a5b42e6292ba51b8e67e09fc256963ba4ca9c04026de004d2fe59cc17e3c3776", - "sha256:bd6ec1233c86c0b9bb5d03ec30dbe3ffbfa53335790320d99a7ae9018c5450f2", - "sha256:bef57530816af54d66b1f4c70a8f851f320cb6f84d4b5a0b422b0e9811ea4e59", - "sha256:c146a63eaadc6589b732780061f3c94cd0574388d372baccbb3c1597a9ebdb7a", - "sha256:c2efd3b130dc639d615b6f58980e1bfd1b177ad821f30827afa5001aa30ddd48", - "sha256:c888b18f7392e6cc79a33a803e7ebd7890ac3318f571fca6b356526f35b53b12", - "sha256:ca30721fda297ae22f16bc37aa7ed244970ddfdcb98247570cdd26daaad4665e", - "sha256:cf5f5340dd682ab034baa52f423a0f91326489c262ac9617fa06309ec05880e9", - "sha256:d0726aa0d9b57c56985db5952e90fb1033a317074f2877db5307cdd6eede1564", - "sha256:df442945b2dd6f8ae0e20b403e0fd4548cd5c2aad69200047cc3251257b78f65", - "sha256:e08e758c31919d167c0867539bd3b2441629ef00aa595e3ea2b635273659f40a", - "sha256:e4864339deeeaefaad34dd3a432ee618a039fca28efb292949c855e00878203c", - "sha256:f4cd049cb94d9f517b1cab5668a3b345968beba093bc79a637e671000b3540ec" + "sha256:0419ae5a45f49c7c40d9ae77ae4de9442431b7822851dfbbe56ee0eacb5e5654", + "sha256:1e8631eeee0fb0b4230aeb135e4890035f6ef9159c2a3555fa184468e325691a", + "sha256:24db2fa5438f3815a4edb7a189035051760ca6aa2b0b70a6a948b28bfc63c76b", + "sha256:2adb1cdb7d33e91069517b41249622710a94a1faece1fed31cd36904e4201cde", + "sha256:2cd51f35692b551aeb1fdeb7a256c7c558f6d78fcddff00640942d42f7aeba5f", + "sha256:3247834d24964589f8c2b121b40cd61319b3c2e8d744a6a82008643ef8a378b1", + "sha256:3433cb848b4209717722b62392e575a77a52a34d67c6730138102abc0a441685", + "sha256:39671b7ff77a962bd745746d9d2292c8ed227c5748f16598d16d8631d17dd7e5", + "sha256:40a0b8b2e6f6dd630f8b267eede2f40a848963d0f3c40b1b1f453a4a870f679e", + "sha256:40f9a74c7aa210b3e76eb1c9d56aa8d08722b73426a77626967019df9bbac287", + "sha256:423f76aa504c84cb94594fb88b8a24027c887f1c488cf58f2173f22f4fbd046c", + "sha256:43bd04cec72281a96eb361e1b0232f0f542b46da50bcfe72ef7e5a1b41d00cb3", + "sha256:43e38762635c09e24885d15e3a8e374b72d105d4178ee2cc9491855a8da9c380", + "sha256:4413b11c2385180d7de03add6c8845dd66692b148d36e27ec8c9ef537b2553a1", + "sha256:4450352a87094fd58daf468b04c65a9fa19ad11a0ac8ac7b7ff17d46f873cbc1", + "sha256:49ffda04a6e44de028b3b786278ac9a70043e7905c3eea29eed88b6524d53a29", + "sha256:4a38c4dde4c9120deef43aaabaa44f19186c98659ce554c29788c4071ab2f0a4", + "sha256:50b1febdfd21e2144b56a9aa226829e93a79c354ef22a4e5b013d9965e1ec0ed", + "sha256:559b1a3a8be7395ded2943ea6c2135d096f8cc7039d6d12127110b6496f251fe", + "sha256:5de86c182667ec68cf84019aa0d8ceccf01d352cdca19bf9e373725204bdbf50", + "sha256:5fc069bb481fe3fad0ba24d3baaf69e22dfa6cc1b63290e6dfeaf4ac1e996fb7", + "sha256:6a19d654da49516296515d6f65de4bbcbd734bc57913b21a610cfc45e6df3ff1", + "sha256:7535b3e52f498270e7877dde1c8944d6b7720e93e2e66b89c82a11447b5818f5", + "sha256:7c4e495bcabc308198b8962e60ca12f53b27eb8f03a21ac1d2d711d6dd9ecfca", + "sha256:8a8fc4a0220367cb8370cedac02272d574079ccc32bffbb34d53aaf9e38b5060", + "sha256:8b008515e067232838daca020d1af628bf6520c8cc338bf383284efe6d8bd083", + "sha256:8d1684258e1385e459418f3429e107eec5fb3d75e1f5a8c52e5946b3f329d6ea", + "sha256:8eb5d54b87fb561dc2e00a5c5226c33ffe8dbc13f2e4033a412bafb7b37b194d", + "sha256:94cdef0c61bd014bb7af495e21a1c3a369dd0399c3cd1965b1502043f5c88d94", + "sha256:9d9f3be69c7a5e84c3549a8c4403fa9ac7672da456863d21e390b2bbf45ccad1", + "sha256:9fb6fb5975a448169756da2d124a1beb38c0924ff6c0306d883b6848a9980f38", + "sha256:a5eaae8700b87144d7dfb475aa4675e500ff707292caba3deff41609ddc5b845", + "sha256:aaeac2d552772b76d24eaff67a5d2325bc5205c74c0d4f9fbe71685d4a971db2", + "sha256:bb611e447559b3b5665e12a7da5160c0de6876097f62bf1d23ba66911564868e", + "sha256:bc0d41f4eb07da8b8d3ea85e50b62f6491ab313834db86ae2345be07536a4e5a", + "sha256:bf51051c129b847d1bb63a9b0826346b5f52fb821b15fe5e0d5ef86f268510f5", + "sha256:c948c034d8997526011960db54f512756fb0b4be1b81140a15b4ef094c6594a4", + "sha256:d435a01334157c3b126b4ee5141401d44bdc8440993b18b05e2f267a6647f92d", + "sha256:d46c1f95672b73288e08cdca181e14e84c6229b5879561b7b8cfd48374e09287", + "sha256:d5d58309b42064228b16b0311ff715d6c6e20230e81b35e8d0c8cfa1bbdecad8", + "sha256:dc6e2e91365a1dd6314d615d80291159c7981928b88a4c65654e3fefac83a836", + "sha256:e0dfb5f7a39029a6cbec23affa923b22a2c02207960fd66f109e01d6f632c1eb", + "sha256:eb4bf58d381b1373bd21d50837a53953d625d1693f1b58fed12743c75d3dd321", + "sha256:ebb211a85248dbc396b29320273c1ffde484b898852432613e8df0164c091006", + "sha256:ec759ece4786ae993a5b7dc3b3dead6e9375d89a6c65dfd6860076d2eb2abe7b", + "sha256:f55108397a8fa164268238c3e69cc134e945d1f693572a2f05a028b8d0d2b837", + "sha256:f6c706866d424ff285b85a02de7bbe5ed0ace227766b2c42cbe12f3d9ea5a8aa", + "sha256:f8370ad332b36fbad117440faf0dd4b910e80b9c49db5648afd337abdde9a1b6" ], - "version": "==1.24.3" + "version": "==1.25.0" }, "invoke": { "hashes": [ @@ -207,6 +208,14 @@ ], "version": "==4.4.1" }, + "netaddr": { + "hashes": [ + "sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd", + "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" + ], + "index": "pypi", + "version": "==0.7.19" + }, "paramiko": { "hashes": [ "sha256:99f0179bdc176281d21961a003ffdb2ec369daac1a1007241f53374e376576cf", diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b64aab98..8b8f38be 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -68,7 +68,7 @@ class CoreClient: self.id = 1 self.reusable = [] self.preexisting = set() - self.interfaces_manager = InterfaceManager() + self.interfaces_manager = InterfaceManager(self.app) self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} @@ -523,57 +523,59 @@ class CoreClient: logging.error("invalid edge token: %s", i) def create_interface(self, canvas_node): - interface = None - core_node = canvas_node.core_node - if NodeUtils.is_container_node(core_node.type): - ifid = len(canvas_node.interfaces) - name = f"eth{ifid}" - interface = core_pb2.Interface( - id=ifid, - name=name, - ip4=str(self.interfaces_manager.get_address()), - ip4mask=24, - ) - canvas_node.interfaces.append(interface) - logging.debug( - "create node(%s) interface IPv4: %s, name: %s", - core_node.name, - interface.ip4, - interface.name, - ) + node = canvas_node.core_node + ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id) + interface_id = len(canvas_node.interfaces) + name = f"eth{interface_id}" + interface = core_pb2.Interface( + id=interface_id, name=name, ip4=ip4, ip4mask=prefix, ip6=ip6, ip6mask=prefix + ) + canvas_node.interfaces.append(interface) + logging.debug( + "create node(%s) interface IPv4: %s, name: %s", + node.name, + interface.ip4, + interface.name, + ) return interface - def create_link(self, token, canvas_node_one, canvas_node_two): + def create_link(self, edge, canvas_src_node, canvas_dst_node): """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. - :param tuple(int, int) token: edge's identification in the canvas - :param canvas_node_one: canvas node one - :param canvas_node_two: canvas node two + :param edge: edge for link + :param canvas_src_node: canvas node one + :param canvas_dst_node: canvas node two :return: nothing """ - node_one = canvas_node_one.core_node - node_two = canvas_node_two.core_node + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node - # create interfaces - self.interfaces_manager.new_subnet() - interface_one = self.create_interface(canvas_node_one) - if interface_one is not None: - self.interface_to_edge[(node_one.id, interface_one.id)] = token - interface_two = self.create_interface(canvas_node_two) - if interface_two is not None: - self.interface_to_edge[(node_two.id, interface_two.id)] = token + # determine subnet + self.interfaces_manager.determine_subnet(canvas_src_node, canvas_dst_node) + + src_interface = None + if NodeUtils.is_container_node(src_node.type): + src_interface = self.create_interface(canvas_src_node) + edge.src_interface = src_interface + self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token + + dst_interface = None + if NodeUtils.is_container_node(dst_node.type): + dst_interface = self.create_interface(canvas_dst_node) + edge.dst_interface = dst_interface + self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token link = core_pb2.Link( type=core_pb2.LinkType.WIRED, - node_one_id=node_one.id, - node_two_id=node_two.id, - interface_one=interface_one, - interface_two=interface_two, + node_one_id=src_node.id, + node_two_id=dst_node.id, + interface_one=src_interface, + interface_two=dst_interface, ) - self.links[token] = link + self.links[edge.token] = link return link def get_wlan_configs_proto(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index b8749e1d..fc14246d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -297,9 +297,8 @@ class CanvasGraph(tk.Canvas): # edge dst must be a node logging.debug(f"current selected: {self.selected}") - logging.debug(f"current nodes: {self.find_withtag('node')}") - is_node = self.selected in self.find_withtag("node") - if not is_node: + dst_node = self.nodes.get(self.selected) + if not dst_node: edge.delete() return @@ -320,7 +319,7 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - link = self.core.create_link(edge.token, node_src, node_dst) + link = self.core.create_link(edge, node_src, node_dst) # draw link info on the edge ip4_and_prefix_1 = None @@ -570,8 +569,9 @@ class CanvasEdge: """ self.src = src self.dst = None + self.src_interface = None + self.dst_interface = None self.canvas = canvas - if is_wired is None or is_wired is True: self.id = self.canvas.create_line( x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" @@ -588,8 +588,6 @@ class CanvasEdge: state=tk.HIDDEN, ) self.token = None - - # link info object self.link_info = None self.throughput = None self.wired = is_wired diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index b0fcd346..9b58e7e6 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -1,45 +1,76 @@ -import ipaddress +import logging +import random + +from netaddr import IPNetwork + +from coretk.nodeutils import NodeUtils -class SubnetAddresses: - def __init__(self, network, addresses): - self.network = network - self.address = addresses - self.address_index = 0 - - def get_new_ip_address(self): - ipaddr = self.address[self.address_index] - self.address_index = self.address_index + 1 - return ipaddr +def random_mac(): + return ("{:02x}" * 6).format(*[random.randrange(256) for _ in range(6)]) class InterfaceManager: - def __init__(self): - self.core_subnets = list( - ipaddress.ip_network("10.0.0.0/12").subnets(prefixlen_diff=12) - ) - self.subnet_index = 0 - self.address_index = 0 - self.network = None - self.addresses = None + def __init__(self, app, cidr="10.0.0.0/24"): + self.app = app + self.cidr = IPNetwork(cidr) + self.deleted = [] + self.current = None - def start_interface_manager(self): - self.subnet_index = 0 - self.network = self.core_subnets[self.subnet_index] - self.subnet_index = self.subnet_index + 1 - self.addresses = list(self.network.hosts()) - self.address_index = 0 + def get_ips(self, node_id): + ip4 = self.current[node_id] + ip6 = ip4.ipv6() + return str(ip4), str(ip6), self.current.prefixlen - def get_address(self): - """ - Retrieve a new ipv4 address + def next_subnet(self): + if self.current: + self.cidr = self.cidr.next() + return self.cidr - :return: - """ - ipaddr = self.addresses[self.address_index] - self.address_index = self.address_index + 1 - return ipaddr + def determine_subnet(self, canvas_src_node, canvas_dst_node): + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node + is_src_container = NodeUtils.is_container_node(src_node.type) + is_dst_container = NodeUtils.is_container_node(dst_node.type) + if is_src_container and is_dst_container: + self.current = self.next_subnet() + elif is_src_container and not is_dst_container: + cidr = self.find_subnet(canvas_dst_node, visited={src_node.id}) + if cidr: + self.current = cidr + else: + self.current = self.next_subnet() + # else: + # self.current = self.cidr + elif not is_src_container and is_dst_container: + cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) + if cidr: + self.current = self.cidr + else: + self.current = self.next_subnet() + else: + logging.info("ignoring subnet change for link between network nodes") - def new_subnet(self): - self.network = self.core_subnets[self.subnet_index] - self.addresses = list(self.network.hosts()) + def find_subnet(self, canvas_node, visited): + logging.info("finding subnet for node: %s", canvas_node.core_node.name) + canvas = self.app.canvas + cidr = None + visited.add(canvas_node.core_node.id) + for edge in canvas_node.edges: + src_node = canvas.nodes[edge.src] + dst_node = canvas.nodes[edge.dst] + interface = edge.src_interface + check_node = src_node + if src_node == canvas_node: + interface = edge.dst_interface + check_node = dst_node + if check_node.core_node.id in visited: + continue + visited.add(check_node.core_node.id) + if interface: + logging.info("found interface: %s", interface) + cidr = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr + break + else: + cidr = self.find_subnet(check_node, visited) + return cidr diff --git a/coretk/setup.py b/coretk/setup.py index 8b8ce2d3..846ab074 100644 --- a/coretk/setup.py +++ b/coretk/setup.py @@ -4,7 +4,7 @@ setup( name="coretk", version="0.1.0", packages=find_packages(), - install_requires=["pillow"], + install_requires=["netaddr", "pillow"], description="CORE GUI", url="https://github.com/coreemu/core", author="Boeing Research & Technology", From 15e05ac580b37fd83378179447406291f07e1320 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 12:59:22 -0800 Subject: [PATCH 152/305] work on status bar --- coretk/coretk/app.py | 3 ++- coretk/coretk/status.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/status.py diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 68060927..f0445257 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -10,6 +10,7 @@ from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils +from coretk.status import StatusBar from coretk.toolbar import Toolbar @@ -80,7 +81,7 @@ class Application(tk.Frame): self.canvas.configure(yscrollcommand=scroll_y.set) def draw_status(self): - self.statusbar = ttk.Frame(self) + self.statusbar = StatusBar(master=self, app=self) self.statusbar.pack(side=tk.BOTTOM, fill=tk.X) def on_closing(self): diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py new file mode 100644 index 00000000..dde16cd8 --- /dev/null +++ b/coretk/coretk/status.py @@ -0,0 +1,29 @@ +"status bar" +from tkinter import ttk + + +class StatusBar(ttk.Frame): + def __init__(self, master, app, **kwargs): + super().__init__(master, **kwargs) + self.app = app + + self.status = None + self.zoom = None + self.cpu_usage = None + self.memory = None + self.emulation_light = None + self.draw() + + def draw(self): + self.columnconfigure(0, weight=8) + self.columnconfigure(1, weight=1) + self.columnconfigure(2, weight=1) + self.columnconfigure(3, weight=1) + self.status = ttk.Label(self, text="status") + self.status.grid(row=0, column=0) + self.zoom = ttk.Label(self, text="zoom") + self.zoom.grid(row=0, column=1) + self.cpu_usage = ttk.Label(self, text="cpu usage") + self.cpu_usage.grid(row=0, column=2) + self.emulation_light = ttk.Label(self, text="emulation light") + self.emulation_light.grid(row=0, column=3) From 52c6f2f31c7a7766fbeed987e0419af18da559e9 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:52:46 -0800 Subject: [PATCH 153/305] fixed issue with services identifying ip4/ip6 addresses --- daemon/core/services/bird.py | 7 ++++--- daemon/core/services/frr.py | 13 ++++++++----- daemon/core/services/nrl.py | 7 ++++--- daemon/core/services/quagga.py | 18 ++++++++++-------- daemon/core/services/sdn.py | 6 ++++-- daemon/core/services/utility.py | 13 +++++++++---- daemon/core/services/xorp.py | 18 ++++++++++-------- 7 files changed, 49 insertions(+), 33 deletions(-) diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py index 80774201..a0f4c640 100644 --- a/daemon/core/services/bird.py +++ b/daemon/core/services/bird.py @@ -1,7 +1,7 @@ """ bird.py: defines routing services provided by the BIRD Internet Routing Daemon. """ - +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -38,8 +38,9 @@ class Bird(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index b4332009..95ffb0b5 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -112,9 +112,10 @@ class FRRZebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -328,8 +329,9 @@ class FrrService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -411,7 +413,8 @@ class FRROspfv2(FrrService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: + addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): continue net = ipaddress.Ipv4Prefix(a) cfg += " network %s area 0\n" % net diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py index a610f1cc..0a6b1f92 100644 --- a/daemon/core/services/nrl.py +++ b/daemon/core/services/nrl.py @@ -4,6 +4,7 @@ nrl.py: defines services provided by NRL protolib tools hosted here: """ from core import utils +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix from core.services.coreservices import CoreService @@ -36,9 +37,9 @@ class NrlService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - addr = a.split("/")[0] - pre = Ipv4Prefix("%s/%s" % (addr, prefixlen)) + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + pre = Ipv4Prefix("%s/%s" % (a, prefixlen)) return str(pre) # raise ValueError, "no IPv4 address found" return "0.0.0.0/%s" % prefixlen diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 5b5cf0ba..267bbcdd 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -109,9 +109,10 @@ class Zebra(CoreService): """ helper for mapping IP addresses to zebra config statements """ - if x.find(".") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv4_address(addr): return "ip address %s" % x - elif x.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): return "ipv6 address %s" % x else: raise ValueError("invalid address: %s", x) @@ -255,8 +256,9 @@ class QuaggaService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -338,10 +340,10 @@ class Ospfv2(QuaggaService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") < 0: - continue - net = ipaddress.Ipv4Prefix(a) - cfg += " network %s area 0\n" % net + addr = a.split("/")[0] + if ipaddress.is_ipv4_address(addr): + net = ipaddress.Ipv4Prefix(a) + cfg += " network %s area 0\n" % net cfg += "!\n" return cfg diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py index c837de53..d924abd7 100644 --- a/daemon/core/services/sdn.py +++ b/daemon/core/services/sdn.py @@ -4,6 +4,7 @@ sdn.py defines services to start Open vSwitch and the Ryu SDN Controller. import re +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -56,11 +57,12 @@ class OvsService(SdnService): # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces # or assign them manually to rtr interfaces if zebra is not running for ifcaddr in ifc.addrlist: - if ifcaddr.find(".") >= 0: + addr = ifcaddr.split("/")[0] + if ipaddress.is_ipv4_address(addr): cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum) - elif ifcaddr.find(":") >= 0: + elif ipaddress.is_ipv6_address(addr): cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name) if has_zebra == 0: cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum) diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index e408b182..16dc6906 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -6,6 +6,7 @@ import os from core import constants, utils from core.errors import CoreCommandError +from core.nodes import ipaddress from core.nodes.ipaddress import Ipv4Prefix, Ipv6Prefix from core.services.coreservices import CoreService @@ -87,7 +88,8 @@ class DefaultRouteService(UtilService): @staticmethod def addrstr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) else: net = Ipv4Prefix(x) @@ -147,7 +149,8 @@ class StaticRouteService(UtilService): @staticmethod def routestr(x): - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) dst = "3ffe:4::/64" else: @@ -280,7 +283,8 @@ ddns-update-style none; Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): return "" else: addr = x.split("/")[0] @@ -702,7 +706,8 @@ interface %s Generate a subnet declaration block given an IPv6 prefix string for inclusion in the RADVD config file. """ - if x.find(":") >= 0: + addr = x.split("/")[0] + if ipaddress.is_ipv6_address(addr): net = Ipv6Prefix(x) return str(net) else: diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py index 1cd62620..3c1de852 100644 --- a/daemon/core/services/xorp.py +++ b/daemon/core/services/xorp.py @@ -4,6 +4,7 @@ xorp.py: defines routing services provided by the XORP routing suite. import logging +from core.nodes import ipaddress from core.services.coreservices import CoreService @@ -150,8 +151,9 @@ class XorpService(CoreService): if hasattr(ifc, "control") and ifc.control is True: continue for a in ifc.addrlist: - if a.find(".") >= 0: - return a.split("/")[0] + a = a.split("/")[0] + if ipaddress.is_ipv4_address(a): + return a # raise ValueError, "no IPv4 address found for router ID" return "0.0.0.0" @@ -187,9 +189,9 @@ class XorpOspfv2(XorpService): cfg += "\t interface %s {\n" % ifc.name cfg += "\t\tvif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\t address %s {\n" % addr cfg += "\t\t }\n" cfg += "\t\t}\n" @@ -280,9 +282,9 @@ class XorpRip(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t disable: false\n" cfg += "\t\t}\n" @@ -462,9 +464,9 @@ class XorpOlsr(XorpService): cfg += "\tinterface %s {\n" % ifc.name cfg += "\t vif %s {\n" % ifc.name for a in ifc.addrlist: - if a.find(".") < 0: - continue addr = a.split("/")[0] + if not ipaddress.is_ipv4_address(addr): + continue cfg += "\t\taddress %s {\n" % addr cfg += "\t\t}\n" cfg += "\t }\n" From 8ffac10a1e76c241df2cea87b2d5b816773a1ea7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:55:10 -0800 Subject: [PATCH 154/305] status bar --- coretk/coretk/coreclient.py | 3 ++- coretk/coretk/status.py | 15 +++++++++++++++ coretk/coretk/toolbar.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 46d1cdad..b75b66ed 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -327,6 +327,7 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None + print(links) response = self.client.start_session( self.session_id, nodes, @@ -341,7 +342,7 @@ class CoreClient: file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) - print(self.client.get_session(self.session_id)) + # print(self.client.get_session(self.session_id)) def stop_session(self): response = self.client.stop_session(session_id=self.session_id) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index dde16cd8..2a160457 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,4 +1,5 @@ "status bar" +import time from tkinter import ttk @@ -12,6 +13,7 @@ class StatusBar(ttk.Frame): self.cpu_usage = None self.memory = None self.emulation_light = None + self.running = False self.draw() def draw(self): @@ -27,3 +29,16 @@ class StatusBar(ttk.Frame): self.cpu_usage.grid(row=0, column=2) self.emulation_light = ttk.Label(self, text="emulation light") self.emulation_light.grid(row=0, column=3) + + def processing(self): + self.running = True + texts = ["Processing.", "Processing..", "Processing...", "Processing...."] + i = 0 + while self.running is True: + self.status.config(text=texts[i % 4]) + self.app.master.update() + i = i + 1 + time.sleep(0.3) + print("running") + print("thread finish") + # self.status.config(text="status") diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 834f7b39..648a2d8c 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,4 +1,5 @@ import logging +import threading import tkinter as tk from functools import partial from tkinter import ttk @@ -197,12 +198,21 @@ class Toolbar(ttk.Frame): """ logging.debug("clicked start button") self.master.config(cursor="watch") + status_thread = threading.Thread(target=self.app.statusbar.processing) + status_thread.start() self.master.update() self.app.canvas.mode = GraphMode.SELECT self.app.core.start_session() self.runtime_frame.tkraise() self.master.config(cursor="") + self.app.statusbar.running = False + if status_thread.is_alive(): + print("still running") + status_thread.join() + print("thread terminate") + self.app.statusbar.status.config(text="status") + def click_link(self): logging.debug("Click LINK button") self.design_select(self.link_button) From 981be3b7ffae6a4cbfd448d9ef00ed5a85b77286 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:58:41 -0800 Subject: [PATCH 155/305] small cleanup with stopping sessions --- coretk/coretk/coreclient.py | 93 ++++++++++--------------------- coretk/coretk/dialogs/sessions.py | 2 +- coretk/coretk/interface.py | 6 ++ 3 files changed, 36 insertions(+), 65 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 46d1cdad..08003dcf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -74,10 +74,25 @@ class CoreClient: self.emane_config = None self.created_nodes = set() self.created_links = set() - self.service_configs = {} self.file_configs = {} + def reset(self): + self.id = 1 + self.reusable.clear() + self.preexisting.clear() + self.canvas_nodes.clear() + self.links.clear() + self.hooks.clear() + self.wlan_configs.clear() + self.mobility_configs.clear() + self.emane_config = None + self.location = None + self.service_configs.clear() + self.file_configs.clear() + self.interfaces_manager.reset() + self.interface_to_edge.clear() + def set_observer(self, value): self.observer = value @@ -130,16 +145,7 @@ class CoreClient: self.master.title(f"CORE Session({self.session_id})") # clear session data - self.reusable.clear() - self.preexisting.clear() - self.canvas_nodes.clear() - self.links.clear() - self.hooks.clear() - self.wlan_configs.clear() - self.mobility_configs.clear() - self.emane_config = None - self.service_configs.clear() - self.file_configs.clear() + self.reset() # get session data response = self.client.get_session(self.session_id) @@ -224,29 +230,12 @@ class CoreClient: ) self.join_session(response.session_id, query_location=False) - def delete_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - response = self.client.delete_session(sid) + def delete_session(self, session_id=None): + if session_id is None: + session_id = self.session_id + response = self.client.delete_session(session_id) logging.info("Deleted session result: %s", response) - def shutdown_session(self, custom_sid=None): - if custom_sid is None: - sid = self.session_id - else: - sid = custom_sid - s = self.client.get_session(sid).session - # delete links and nodes from running session - if s.state == core_pb2.SessionState.RUNTIME: - self.client.set_session_state( - self.session_id, core_pb2.SessionState.DATACOLLECT - ) - self.delete_links(sid) - self.delete_nodes(sid) - self.delete_session(sid) - def set_up(self): """ Query sessions, if there exist any, prompt whether to join one @@ -287,31 +276,6 @@ class CoreClient: response = self.client.edit_node(self.session_id, node_id, position) logging.info("updated node id %s: %s", node_id, response) - def delete_nodes(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - for node in self.client.get_session(sid).session.nodes: - response = self.client.delete_node(self.session_id, node.id) - logging.info("delete nodes %s", response) - - def delete_links(self, delete_session=None): - if delete_session is None: - sid = self.session_id - else: - sid = delete_session - - for link in self.client.get_session(sid).session.links: - response = self.client.delete_link( - self.session_id, - link.node_one_id, - link.node_two_id, - link.interface_one.id, - link.interface_two.id, - ) - logging.info("delete links %s", response) - def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] links = list(self.links.values()) @@ -340,17 +304,18 @@ class CoreClient: service_configs, file_configs, ) - logging.debug("Start session %s, result: %s", self.session_id, response.result) - print(self.client.get_session(self.session_id)) + logging.debug("start session(%s), result: %s", self.session_id, response.result) - def stop_session(self): - response = self.client.stop_session(session_id=self.session_id) - logging.debug("coregrpc.py Stop session, result: %s", response.result) + def stop_session(self, session_id=None): + if not session_id: + session_id = self.session_id + response = self.client.stop_session(session_id) + logging.debug("stopped session(%s), result: %s", session_id, response.result) def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) - os.system("xterm -e %s &" % response.terminal) + os.system(f"xterm -e {response.terminal} &") def save_xml(self, file_path): """ @@ -360,7 +325,7 @@ class CoreClient: :return: nothing """ response = self.client.save_xml(self.session_id, file_path) - logging.info("coregrpc.py save xml %s", response) + logging.info("saved xml(%s): %s", file_path, response) self.client.events(self.session_id, self.handle_events) def open_xml(self, file_path): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 6ea66973..6f299dfc 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -155,6 +155,6 @@ class SessionsDialog(Dialog): self.join_session(sid) def shutdown_session(self, sid): - self.app.core.shutdown_session(sid) + self.app.core.stop_session(sid) self.click_new() self.destroy() diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 9b58e7e6..e1203389 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -13,10 +13,16 @@ def random_mac(): class InterfaceManager: def __init__(self, app, cidr="10.0.0.0/24"): self.app = app + self.default = cidr self.cidr = IPNetwork(cidr) self.deleted = [] self.current = None + def reset(self): + self.cidr = IPNetwork(self.default) + self.deleted.clear() + self.current = None + def get_ips(self, node_id): ip4 = self.current[node_id] ip6 = ip4.ipv6() From 46627aad11e7befabf8f28ebcc93ae324970a855 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 15:43:43 -0800 Subject: [PATCH 156/305] renamed coreclient delete graph nodes func --- coretk/coretk/coreclient.py | 33 ++++++++++++++++++++------------- coretk/coretk/graph.py | 2 +- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 08003dcf..309fcba2 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -57,41 +57,48 @@ class CoreClient: self.custom_observers = {} self.read_config() - # data for managing the current session - self.canvas_nodes = {} - self.location = None + # helpers self.interface_to_edge = {} - self.state = None - self.links = {} - self.hooks = {} - self.id = 1 self.reusable = [] self.preexisting = set() self.interfaces_manager = InterfaceManager(self.app) + self.created_nodes = set() + self.created_links = set() + + # session data + self.id = 1 + self.state = None + self.canvas_nodes = {} + self.location = None + self.links = {} + self.hooks = {} self.wlan_configs = {} self.mobility_configs = {} self.emane_model_configs = {} self.emane_config = None - self.created_nodes = set() - self.created_links = set() self.service_configs = {} self.file_configs = {} def reset(self): self.id = 1 + # helpers + self.created_nodes.clear() + self.created_links.clear() self.reusable.clear() self.preexisting.clear() + self.interfaces_manager.reset() + self.interface_to_edge.clear() + # session data self.canvas_nodes.clear() + self.location = None self.links.clear() self.hooks.clear() self.wlan_configs.clear() self.mobility_configs.clear() + self.emane_model_configs.clear() self.emane_config = None - self.location = None self.service_configs.clear() self.file_configs.clear() - self.interfaces_manager.reset() - self.interface_to_edge.clear() def set_observer(self, value): self.observer = value @@ -462,7 +469,7 @@ class CoreClient: ) return node - def delete_wanted_graph_nodes(self, node_ids, edge_tokens): + def delete_graph_nodes(self, node_ids, edge_tokens): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index fc14246d..b87bccbf 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -411,7 +411,7 @@ class CanvasGraph(tk.Canvas): self.nodes[nid].edges.remove(edge) # delete the related data from core - self.core.delete_wanted_graph_nodes(node_ids, to_delete_edge_tokens) + self.core.delete_graph_nodes(node_ids, to_delete_edge_tokens) def add_node(self, x, y): plot_id = self.find_all()[0] From e8f8fa3bd5a376bc58646407b8c12d4893feba40 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 16:30:25 -0800 Subject: [PATCH 157/305] cleaned up some of the logic for deleting nodes/edges --- coretk/coretk/coreclient.py | 10 ++++----- coretk/coretk/graph.py | 27 ++++------------------- coretk/coretk/interface.py | 3 +++ coretk/coretk/nodedelete.py | 43 +++++++++++++++++++++---------------- 4 files changed, 37 insertions(+), 46 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 309fcba2..cd10a073 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -469,13 +469,13 @@ class CoreClient: ) return node - def delete_graph_nodes(self, node_ids, edge_tokens): + def delete_graph_nodes(self, node_ids, edges): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces :param list[int] node_ids: list of nodes to delete - :param list edge_tokens: list of edges to delete + :param list edges: list of edges to delete :return: nothing """ # delete the nodes @@ -496,11 +496,11 @@ class CoreClient: self.reusable.sort() # delete the edges and interfaces - for i in edge_tokens: + for edge in edges: try: - self.links.pop(i) + self.links.pop(edge.token) except KeyError: - logging.error("invalid edge token: %s", i) + logging.error("invalid edge token: %s", edge.token) def create_interface(self, canvas_node): node = canvas_node.core_node diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index b87bccbf..3f140261 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -388,30 +388,11 @@ class CanvasGraph(tk.Canvas): :param event: :return: """ - # hide nodes, links, link information that shows on the GUI - to_delete_nodes, to_delete_edge_tokens = ( - self.canvas_management.delete_selected_nodes() - ) + # delete canvas data + node_ids, edges = self.canvas_management.delete_selected_nodes() - # delete nodes and link info stored in CanvasGraph object - node_ids = [] - for nid in to_delete_nodes: - canvas_node = self.nodes.pop(nid) - node_ids.append(canvas_node.core_node.id) - for token in to_delete_edge_tokens: - self.edges.pop(token) - - # delete the edge data inside of canvas node - canvas_node_link_to_delete = [] - for canvas_id, node in self.nodes.items(): - for e in node.edges: - if e.token in to_delete_edge_tokens: - canvas_node_link_to_delete.append(tuple([canvas_id, e])) - for nid, edge in canvas_node_link_to_delete: - self.nodes[nid].edges.remove(edge) - - # delete the related data from core - self.core.delete_graph_nodes(node_ids, to_delete_edge_tokens) + # delete core data + self.core.delete_graph_nodes(node_ids, edges) def add_node(self, x, y): plot_id = self.find_all()[0] diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index e1203389..7d0c374c 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -33,6 +33,9 @@ class InterfaceManager: self.cidr = self.cidr.next() return self.cidr + def deleted_interface(self, interface): + logging.info("deleted interface: %s", interface) + def determine_subnet(self, canvas_src_node, canvas_dst_node): src_node = canvas_src_node.core_node dst_node = canvas_dst_node.core_node diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 6ce3a6f9..496bbc6d 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -39,23 +39,30 @@ class CanvasComponentManagement: self.selected.clear() def delete_selected_nodes(self): - selected_nodes = list(self.selected.keys()) edges = set() - for n in selected_nodes: - edges = edges.union(self.canvas.nodes[n].edges) - edge_canvas_ids = [x.id for x in edges] - edge_tokens = [x.token for x in edges] - link_infos = [x.link_info.id1 for x in edges] + [x.link_info.id2 for x in edges] - - for i in edge_canvas_ids: - self.canvas.itemconfig(i, state="hidden") - - for i in link_infos: - self.canvas.itemconfig(i, state="hidden") - - for cnid, bbid in self.selected.items(): - self.canvas.itemconfig(cnid, state="hidden") - self.canvas.itemconfig(bbid, state="hidden") - self.canvas.itemconfig(self.canvas.nodes[cnid].text_id, state="hidden") + node_ids = [] + for node_id in list(self.selected): + bbox_id = self.selected[node_id] + canvas_node = self.canvas.nodes.pop(node_id) + node_ids.append(canvas_node.core_node.id) + self.canvas.delete(node_id) + self.canvas.delete(bbox_id) + self.canvas.delete(canvas_node.text_id) + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.canvas.edges.pop(edge.token) + self.canvas.delete(edge.id) + self.canvas.delete(edge.link_info.id1) + self.canvas.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == node_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.canvas.nodes[other_id] + other_node.edges.remove(edge) + other_node.interfaces.remove(other_interface) self.selected.clear() - return selected_nodes, edge_tokens + return node_ids, edges From 4e32c9c13cf934d93b90cccf9feaf50103a1c6d5 Mon Sep 17 00:00:00 2001 From: bharnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 22 Nov 2019 23:48:10 -0800 Subject: [PATCH 158/305] updates to add reuse of deleted subnets --- coretk/coretk/coreclient.py | 76 +++++++++++++++++++++++++------------ coretk/coretk/graph.py | 4 +- coretk/coretk/interface.py | 35 +++++++++++------ coretk/coretk/nodedelete.py | 11 ++++-- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index cd10a073..d5a2f854 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -469,38 +469,64 @@ class CoreClient: ) return node - def delete_graph_nodes(self, node_ids, edges): + def delete_graph_nodes(self, canvas_nodes): """ remove the nodes selected by the user and anything related to that node such as link, configurations, interfaces - :param list[int] node_ids: list of nodes to delete - :param list edges: list of edges to delete + :param list canvas_nodes: list of nodes to delete :return: nothing """ - # delete the nodes - for i in node_ids: - try: - del self.canvas_nodes[i] - self.reusable.append(i) - if i in self.mobility_configs: - del self.mobility_configs[i] - if i in self.wlan_configs: - del self.wlan_configs[i] - for key in list(self.emane_model_configs): - node_id, _, _ = key - if node_id == i: - del self.emane_model_configs[key] - except KeyError: - logging.error("invalid canvas id: %s", i) - self.reusable.sort() + edges = set() + for canvas_node in canvas_nodes: + node_id = canvas_node.core_node.id + if node_id not in self.canvas_nodes: + logging.error("unknown node: %s", node_id) + continue + del self.canvas_nodes[node_id] + self.reusable.append(node_id) + if node_id in self.mobility_configs: + del self.mobility_configs[node_id] + if node_id in self.wlan_configs: + del self.wlan_configs[node_id] + for key in list(self.emane_model_configs): + node_id, _, _ = key + if node_id == node_id: + del self.emane_model_configs[key] - # delete the edges and interfaces - for edge in edges: - try: - self.links.pop(edge.token) - except KeyError: - logging.error("invalid edge token: %s", edge.token) + deleted_cidrs = set() + keep_cidrs = set() + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + if edge.token not in self.links: + logging.error("unknown edge: %s", edge.token) + del self.links[edge.token] + other_id = edge.src + other_interface = edge.src_interface + interface = edge.dst_interface + if canvas_node.id == edge.src: + other_id = edge.dst + other_interface = edge.dst_interface + interface = edge.src_interface + other_node = self.app.canvas.nodes.get(other_id) + if not other_node: + continue + if other_interface: + cidr = self.interfaces_manager.get_cidr(other_interface) + deleted_cidrs.add(cidr) + else: + cidr = self.interfaces_manager.find_subnet(other_node) + if cidr: + keep_cidrs.add(cidr) + else: + cidr = self.interfaces_manager.get_cidr(interface) + deleted_cidrs.add(cidr) + deleted_cidrs = deleted_cidrs - keep_cidrs + for cidr in deleted_cidrs: + self.interfaces_manager.deleted_cidr(cidr) + self.reusable.sort() def create_interface(self, canvas_node): node = canvas_node.core_node diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 3f140261..14e978be 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -389,10 +389,10 @@ class CanvasGraph(tk.Canvas): :return: """ # delete canvas data - node_ids, edges = self.canvas_management.delete_selected_nodes() + nodes = self.canvas_management.delete_selected_nodes() # delete core data - self.core.delete_graph_nodes(node_ids, edges) + self.core.delete_graph_nodes(nodes) def add_node(self, x, y): plot_id = self.find_all()[0] diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 7d0c374c..4b56f3a8 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -29,12 +29,22 @@ class InterfaceManager: return str(ip4), str(ip6), self.current.prefixlen def next_subnet(self): - if self.current: - self.cidr = self.cidr.next() - return self.cidr + if self.deleted: + return self.deleted.pop(0) + else: + if self.current: + self.cidr = self.cidr.next() + return self.cidr - def deleted_interface(self, interface): - logging.info("deleted interface: %s", interface) + def deleted_cidr(self, cidr): + logging.info("deleted cidr: %s", cidr) + if cidr not in self.deleted: + self.deleted.append(cidr) + self.deleted.sort() + + @classmethod + def get_cidr(cls, interface): + return IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr def determine_subnet(self, canvas_src_node, canvas_dst_node): src_node = canvas_src_node.core_node @@ -49,21 +59,21 @@ class InterfaceManager: self.current = cidr else: self.current = self.next_subnet() - # else: - # self.current = self.cidr elif not is_src_container and is_dst_container: cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) if cidr: - self.current = self.cidr + self.current = cidr else: self.current = self.next_subnet() else: logging.info("ignoring subnet change for link between network nodes") - def find_subnet(self, canvas_node, visited): + def find_subnet(self, canvas_node, visited=None): logging.info("finding subnet for node: %s", canvas_node.core_node.name) canvas = self.app.canvas cidr = None + if not visited: + visited = set() visited.add(canvas_node.core_node.id) for edge in canvas_node.edges: src_node = canvas.nodes[edge.src] @@ -77,9 +87,10 @@ class InterfaceManager: continue visited.add(check_node.core_node.id) if interface: - logging.info("found interface: %s", interface) - cidr = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr - break + cidr = self.get_cidr(interface) else: cidr = self.find_subnet(check_node, visited) + if cidr: + logging.info("found subnet: %s", cidr) + break return cidr diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 496bbc6d..165fde53 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -40,11 +40,11 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() - node_ids = [] + nodes = [] for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) - node_ids.append(canvas_node.core_node.id) + nodes.append(canvas_node) self.canvas.delete(node_id) self.canvas.delete(bbox_id) self.canvas.delete(canvas_node.text_id) @@ -63,6 +63,9 @@ class CanvasComponentManagement: other_interface = edge.dst_interface other_node = self.canvas.nodes[other_id] other_node.edges.remove(edge) - other_node.interfaces.remove(other_interface) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass self.selected.clear() - return node_ids, edges + return nodes From 27a37c77fc3e0980e0652336ded514128e849f22 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 09:52:50 -0800 Subject: [PATCH 159/305] made preexisting nodes a local variable, renamed reuseable to delete_nodes for more clarity --- coretk/coretk/coreclient.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index d5a2f854..df83d9c7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -59,8 +59,7 @@ class CoreClient: # helpers self.interface_to_edge = {} - self.reusable = [] - self.preexisting = set() + self.deleted_nodes = [] self.interfaces_manager = InterfaceManager(self.app) self.created_nodes = set() self.created_links = set() @@ -84,8 +83,7 @@ class CoreClient: # helpers self.created_nodes.clear() self.created_links.clear() - self.reusable.clear() - self.preexisting.clear() + self.deleted_nodes.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -195,14 +193,15 @@ class CoreClient: # determine next node id and reusable nodes max_id = 1 + existing_nodes = set() for node in session.nodes: if node.id > max_id: max_id = node.id - self.preexisting.add(node.id) + existing_nodes.add(node.id) self.id = max_id for i in range(1, self.id): - if i not in self.preexisting: - self.reusable.append(i) + if i not in existing_nodes: + self.deleted_nodes.append(i) # draw session self.app.canvas.reset_and_redraw(session) @@ -426,12 +425,12 @@ class CoreClient: :rtype: int :return: the next id to be used """ - if len(self.reusable) == 0: + if self.deleted_nodes: + return self.deleted_nodes.pop(0) + else: new_id = self.id self.id = self.id + 1 return new_id - else: - return self.reusable.pop(0) def create_node(self, x, y, node_type, model): """ @@ -484,7 +483,7 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.reusable.append(node_id) + self.deleted_nodes.append(node_id) if node_id in self.mobility_configs: del self.mobility_configs[node_id] if node_id in self.wlan_configs: @@ -526,7 +525,7 @@ class CoreClient: deleted_cidrs = deleted_cidrs - keep_cidrs for cidr in deleted_cidrs: self.interfaces_manager.deleted_cidr(cidr) - self.reusable.sort() + self.deleted_nodes.sort() def create_interface(self, canvas_node): node = canvas_node.core_node From 89dfeae07c9ce0ab4a5b2723fe55fc47b7e17322 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:23:04 -0800 Subject: [PATCH 160/305] basic status bar, node deletion also remove antenna --- coretk/coretk/coreclient.py | 2 +- coretk/coretk/graph.py | 20 ++++++++++++++ coretk/coretk/graph_helper.py | 51 +++++++++++++++++++++-------------- coretk/coretk/status.py | 16 +++++------ coretk/coretk/toolbar.py | 31 +++++++++++++-------- 5 files changed, 80 insertions(+), 40 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b75b66ed..f847d827 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -327,7 +327,6 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None - print(links) response = self.client.start_session( self.session_id, nodes, @@ -342,6 +341,7 @@ class CoreClient: file_configs, ) logging.debug("Start session %s, result: %s", self.session_id, response.result) + print(self.servers) # print(self.client.get_session(self.session_id)) def stop_session(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index fc14246d..b084712d 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -393,6 +393,26 @@ class CanvasGraph(tk.Canvas): self.canvas_management.delete_selected_nodes() ) + # delete antennas + for cnid in to_delete_nodes: + canvas_node = self.nodes[cnid] + if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + canvas_node.antenna_draw.delete_antennas() + else: + for e in canvas_node.edges: + link_proto = self.core.links[e.token] + node_one_id, node_two_id = ( + link_proto.node_one_id, + link_proto.node_two_id, + ) + if node_one_id == canvas_node.core_node.id: + neighbor_id = node_two_id + else: + neighbor_id = node_one_id + neighbor = self.core.canvas_nodes[neighbor_id] + if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: + neighbor.antenna_draw.delete_antenna() + # delete nodes and link info stored in CanvasGraph object node_ids = [] for nid in to_delete_nodes: diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index e691667b..1ad0c8f6 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -88,18 +88,41 @@ class WlanAntennaManager: """ if self.quantity < 5: x, y = self.canvas.coords(self.node_id) - self.antennas.append( - self.canvas.create_image( - x - 16 + self.offset, - y - 16, - anchor=tk.CENTER, - image=self.image, - tags="antenna", - ) + aid = self.canvas.create_image( + x - 16 + self.offset, + y - 23, + anchor=tk.CENTER, + image=self.image, + tags="antenna", ) + # self.canvas.tag_raise("antenna") + self.antennas.append(aid) self.quantity = self.quantity + 1 self.offset = self.offset + 8 + def delete_antenna(self): + """ + delete one antenna + + :return: nothing + """ + if len(self.antennas) > 0: + self.canvas.delete(self.antennas.pop()) + self.quantity -= 1 + self.offset -= 8 + + def delete_antennas(self): + """ + delete all antennas + + :return: nothing + """ + for aid in self.antennas: + self.canvas.delete(aid) + self.antennas.clear() + self.quantity = 0 + self.offset = 0 + def update_antennas_position(self, offset_x, offset_y): """ redraw antennas of a node according to the new node position @@ -108,15 +131,3 @@ class WlanAntennaManager: """ for i in self.antennas: self.canvas.move(i, offset_x, offset_y) - - def delete_antenna(self, canvas_id): - return - - def delete_antennas(self): - """ - Delete all the antennas of a node - - :return: nothing - """ - for i in self.antennas: - self.canvas.delete(i) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index 2a160457..69869f51 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,5 +1,6 @@ "status bar" import time +import tkinter as tk from tkinter import ttk @@ -9,6 +10,7 @@ class StatusBar(ttk.Frame): self.app = app self.status = None + self.statusvar = tk.StringVar() self.zoom = None self.cpu_usage = None self.memory = None @@ -21,7 +23,8 @@ class StatusBar(ttk.Frame): self.columnconfigure(1, weight=1) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) - self.status = ttk.Label(self, text="status") + self.status = ttk.Label(self, textvariable=self.statusvar) + self.statusvar.set("status") self.status.grid(row=0, column=0) self.zoom = ttk.Label(self, text="zoom") self.zoom.grid(row=0, column=1) @@ -31,14 +34,11 @@ class StatusBar(ttk.Frame): self.emulation_light.grid(row=0, column=3) def processing(self): - self.running = True texts = ["Processing.", "Processing..", "Processing...", "Processing...."] i = 0 - while self.running is True: - self.status.config(text=texts[i % 4]) - self.app.master.update() + while self.running: + self.statusvar.set(texts[i % 4]) + self.master.update() i = i + 1 - time.sleep(0.3) - print("running") + time.sleep(0.002) print("thread finish") - # self.status.config(text="status") diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 648a2d8c..1aae22c8 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,5 +1,5 @@ import logging -import threading +import time import tkinter as tk from functools import partial from tkinter import ttk @@ -196,22 +196,25 @@ class Toolbar(ttk.Frame): :return: nothing """ - logging.debug("clicked start button") + self.app.statusbar.running = True + # thread = threading.Thread(target=self.app.statusbar.processing) + # thread.start() self.master.config(cursor="watch") - status_thread = threading.Thread(target=self.app.statusbar.processing) - status_thread.start() self.master.update() self.app.canvas.mode = GraphMode.SELECT + start = time.time() self.app.core.start_session() + dur = time.time() - start self.runtime_frame.tkraise() self.master.config(cursor="") - - self.app.statusbar.running = False - if status_thread.is_alive(): - print("still running") - status_thread.join() - print("thread terminate") - self.app.statusbar.status.config(text="status") + nodes_num = len(self.app.core.canvas_nodes) + links_num = len(self.app.core.links) + self.app.statusbar.statusvar.set( + "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" + % ("%.3f" % dur, nodes_num, links_num) + ) + # self.app.statusbar.running = False + # print("done") def click_link(self): logging.debug("Click LINK button") @@ -365,7 +368,13 @@ class Toolbar(ttk.Frame): :return: nothing """ logging.debug("Click on STOP button ") + # self.status_thread.join() + start = time.time() self.app.core.stop_session() + dur = time.time() - start + self.app.statusbar.statusvar.set( + "Cleanup completed in %s seconds" % "%.3f" % dur + ) self.app.canvas.delete("wireless") self.design_frame.tkraise() From 1290dae4f882fd99e021c5306510551985267ffc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 11:31:40 -0800 Subject: [PATCH 161/305] simplified logic for picking next subnet, avoiding trying to optimize too early --- coretk/coretk/coreclient.py | 27 +------------ coretk/coretk/interface.py | 75 +++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 63 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index df83d9c7..8c370a91 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -45,7 +45,6 @@ class CoreClient: self.node_ids = [] self.app = app self.master = app.master - self.interface_helper = None self.services = {} self.default_services = {} self.emane_models = [] @@ -493,8 +492,6 @@ class CoreClient: if node_id == node_id: del self.emane_model_configs[key] - deleted_cidrs = set() - keep_cidrs = set() for edge in canvas_node.edges: if edge in edges: continue @@ -502,29 +499,7 @@ class CoreClient: if edge.token not in self.links: logging.error("unknown edge: %s", edge.token) del self.links[edge.token] - other_id = edge.src - other_interface = edge.src_interface - interface = edge.dst_interface - if canvas_node.id == edge.src: - other_id = edge.dst - other_interface = edge.dst_interface - interface = edge.src_interface - other_node = self.app.canvas.nodes.get(other_id) - if not other_node: - continue - if other_interface: - cidr = self.interfaces_manager.get_cidr(other_interface) - deleted_cidrs.add(cidr) - else: - cidr = self.interfaces_manager.find_subnet(other_node) - if cidr: - keep_cidrs.add(cidr) - else: - cidr = self.interfaces_manager.get_cidr(interface) - deleted_cidrs.add(cidr) - deleted_cidrs = deleted_cidrs - keep_cidrs - for cidr in deleted_cidrs: - self.interfaces_manager.deleted_cidr(cidr) + self.deleted_nodes.sort() def create_interface(self, canvas_node): diff --git a/coretk/coretk/interface.py b/coretk/coretk/interface.py index 4b56f3a8..f4a380ae 100644 --- a/coretk/coretk/interface.py +++ b/coretk/coretk/interface.py @@ -11,39 +11,40 @@ def random_mac(): class InterfaceManager: - def __init__(self, app, cidr="10.0.0.0/24"): + def __init__(self, app, address="10.0.0.0", mask=24): self.app = app - self.default = cidr - self.cidr = IPNetwork(cidr) - self.deleted = [] - self.current = None - - def reset(self): - self.cidr = IPNetwork(self.default) - self.deleted.clear() - self.current = None - - def get_ips(self, node_id): - ip4 = self.current[node_id] - ip6 = ip4.ipv6() - return str(ip4), str(ip6), self.current.prefixlen + self.mask = mask + self.base_prefix = max(self.mask - 8, 0) + self.subnets = IPNetwork(f"{address}/{self.base_prefix}") + self.current_subnet = None def next_subnet(self): - if self.deleted: - return self.deleted.pop(0) - else: - if self.current: - self.cidr = self.cidr.next() - return self.cidr + # define currently used subnets + used_subnets = set() + for link in self.app.core.links.values(): + if link.HasField("interface_one"): + subnet = self.get_subnet(link.interface_one) + used_subnets.add(subnet) + if link.HasField("interface_two"): + subnet = self.get_subnet(link.interface_two) + used_subnets.add(subnet) - def deleted_cidr(self, cidr): - logging.info("deleted cidr: %s", cidr) - if cidr not in self.deleted: - self.deleted.append(cidr) - self.deleted.sort() + # find next available subnet + for i in self.subnets.subnet(self.mask): + if i not in used_subnets: + return i + + def reset(self): + self.current_subnet = None + + def get_ips(self, node_id): + ip4 = self.current_subnet[node_id] + ip6 = ip4.ipv6() + prefix = self.current_subnet.prefixlen + return str(ip4), str(ip6), prefix @classmethod - def get_cidr(cls, interface): + def get_subnet(cls, interface): return IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr def determine_subnet(self, canvas_src_node, canvas_dst_node): @@ -52,19 +53,19 @@ class InterfaceManager: is_src_container = NodeUtils.is_container_node(src_node.type) is_dst_container = NodeUtils.is_container_node(dst_node.type) if is_src_container and is_dst_container: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() elif is_src_container and not is_dst_container: - cidr = self.find_subnet(canvas_dst_node, visited={src_node.id}) - if cidr: - self.current = cidr + subnet = self.find_subnet(canvas_dst_node, visited={src_node.id}) + if subnet: + self.current_subnet = subnet else: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() elif not is_src_container and is_dst_container: - cidr = self.find_subnet(canvas_src_node, visited={dst_node.id}) - if cidr: - self.current = cidr + subnet = self.find_subnet(canvas_src_node, visited={dst_node.id}) + if subnet: + self.current_subnet = subnet else: - self.current = self.next_subnet() + self.current_subnet = self.next_subnet() else: logging.info("ignoring subnet change for link between network nodes") @@ -87,7 +88,7 @@ class InterfaceManager: continue visited.add(check_node.core_node.id) if interface: - cidr = self.get_cidr(interface) + cidr = self.get_subnet(interface) else: cidr = self.find_subnet(check_node, visited) if cidr: From 3d2e372663a0dd799259c28820e8c299fd62eca4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 12:59:32 -0800 Subject: [PATCH 162/305] simplified next node id logic to avoid trying to over optimize --- coretk/coretk/coreclient.py | 39 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8c370a91..c4e58795 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -58,13 +58,11 @@ class CoreClient: # helpers self.interface_to_edge = {} - self.deleted_nodes = [] self.interfaces_manager = InterfaceManager(self.app) self.created_nodes = set() self.created_links = set() # session data - self.id = 1 self.state = None self.canvas_nodes = {} self.location = None @@ -78,11 +76,9 @@ class CoreClient: self.file_configs = {} def reset(self): - self.id = 1 # helpers self.created_nodes.clear() self.created_links.clear() - self.deleted_nodes.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -190,18 +186,6 @@ class CoreClient: # get emane model config - # determine next node id and reusable nodes - max_id = 1 - existing_nodes = set() - for node in session.nodes: - if node.id > max_id: - max_id = node.id - existing_nodes.add(node.id) - self.id = max_id - for i in range(1, self.id): - if i not in existing_nodes: - self.deleted_nodes.append(i) - # draw session self.app.canvas.reset_and_redraw(session) @@ -417,19 +401,19 @@ class CoreClient: logging.debug("Close grpc") self.client.close() - def get_id(self): + def next_node_id(self): """ - Get the next node id as well as update id status and reusable ids + Get the next usable node id. - :rtype: int :return: the next id to be used + :rtype: int """ - if self.deleted_nodes: - return self.deleted_nodes.pop(0) - else: - new_id = self.id - self.id = self.id + 1 - return new_id + i = 1 + while True: + if i not in self.canvas_nodes: + break + i += 1 + return i def create_node(self, x, y, node_type, model): """ @@ -441,7 +425,7 @@ class CoreClient: :param str model: node model :return: nothing """ - node_id = self.get_id() + node_id = self.next_node_id() position = core_pb2.Position(x=x, y=y) image = None if NodeUtils.is_image_node(node_type): @@ -482,7 +466,6 @@ class CoreClient: logging.error("unknown node: %s", node_id) continue del self.canvas_nodes[node_id] - self.deleted_nodes.append(node_id) if node_id in self.mobility_configs: del self.mobility_configs[node_id] if node_id in self.wlan_configs: @@ -500,8 +483,6 @@ class CoreClient: logging.error("unknown edge: %s", edge.token) del self.links[edge.token] - self.deleted_nodes.sort() - def create_interface(self, canvas_node): node = canvas_node.core_node ip4, ip6, prefix = self.interfaces_manager.get_ips(node.id) From e3fc318a1b6983574cc30f3686cd4a55df38ce5e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 14:52:00 -0800 Subject: [PATCH 163/305] linkinfo cleanup, changed text distance to be proprotionate of line distance --- coretk/coretk/graph.py | 50 ++-------------- coretk/coretk/linkinfo.py | 119 ++++++++++++++------------------------ 2 files changed, 48 insertions(+), 121 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 14e978be..ca4b0001 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -200,31 +200,11 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.core.links[edge.token] = link self.helper.redraw_antenna(canvas_node_one, canvas_node_two) - - # TODO add back the link info to grpc manager also redraw - # TODO will include throughput and ipv6 in the future - interface_one = link.interface_one - interface_two = link.interface_two - ip4_src = None - ip4_dst = None - ip6_src = None - ip6_dst = None - if interface_one is not None: - ip4_src = interface_one.ip4 - ip6_src = interface_one.ip6 - if interface_two is not None: - ip4_dst = interface_two.ip4 - ip6_dst = interface_two.ip6 - edge.link_info = LinkInfo( - canvas=self, - edge=edge, - ip4_src=ip4_src, - ip6_src=ip6_src, - ip4_dst=ip4_dst, - ip6_dst=ip6_dst, - ) - canvas_node_one.interfaces.append(interface_one) - canvas_node_two.interfaces.append(interface_two) + edge.link_info = LinkInfo(self, edge, link) + if link.HasField("interface_one"): + canvas_node_one.interfaces.append(link.interface_one) + if link.HasField("interface_two"): + canvas_node_two.interfaces.append(link.interface_two) # raise the nodes so they on top of the links self.tag_raise("node") @@ -320,25 +300,7 @@ class CanvasGraph(tk.Canvas): node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) link = self.core.create_link(edge, node_src, node_dst) - - # draw link info on the edge - ip4_and_prefix_1 = None - ip4_and_prefix_2 = None - if link.HasField("interface_one"): - if1 = link.interface_one - ip4_and_prefix_1 = f"{if1.ip4}/{if1.ip4mask}" - if link.HasField("interface_two"): - if2 = link.interface_two - ip4_and_prefix_2 = f"{if2.ip4}/{if2.ip4mask}" - edge.link_info = LinkInfo( - self, - edge, - ip4_src=ip4_and_prefix_1, - ip6_src=None, - ip4_dst=ip4_and_prefix_2, - ip6_dst=None, - ) - + edge.link_info = LinkInfo(self, edge, link) logging.debug(f"edges: {self.find_withtag('edge')}") def click_press(self, event): diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 00df0913..2b7ecc7e 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -3,76 +3,60 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ import logging import math +import tkinter as tk + +TEXT_DISTANCE = 0.25 class LinkInfo: - def __init__(self, canvas, edge, ip4_src, ip6_src, ip4_dst, ip6_dst): + def __init__(self, canvas, edge, link): """ create an instance of LinkInfo object :param coretk.graph.Graph canvas: canvas object :param coretk.graph.CanvasEdge edge: canvas edge onject - :param ip4_src: - :param ip6_src: - :param ip4_dst: - :param ip6_dst: + :param link: core link to draw info for """ self.canvas = canvas self.edge = edge - self.radius = 37 - self.core = self.canvas.core + self.link = link + self.id1 = None + self.id2 = None + self.draw_labels() - self.ip4_address_1 = ip4_src - self.ip6_address_1 = ip6_src - self.ip4_address_2 = ip4_dst - self.ip6_address_2 = ip6_dst - self.id1 = self.create_edge_src_info() - self.id2 = self.create_edge_dst_info() - - def slope_src_dst(self): - """ - calculate slope of the line connecting source node to destination node - :rtype: float - :return: slope of line - """ + def get_coordinates(self): x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - if x2 - x1 == 0: - return 9999.0 - else: - return (y2 - y1) / (x2 - x1) + v1 = x2 - x1 + v2 = y2 - y1 + d = math.sqrt(v1 ** 2 + v2 ** 2) + ux = TEXT_DISTANCE * v1 + uy = TEXT_DISTANCE * v2 + x1 = x1 + ux + y1 = y1 + uy + x2 = x2 - ux + y2 = y2 - uy + logging.info("line distance: %s", d) + return x1, y1, x2, y2 - def create_edge_src_info(self): - """ - draw the ip address for source node - - :return: nothing - """ - x1, y1, x2, _ = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id1 = self.canvas.create_text(x1, y1, text=self.ip4_address_1) - id1 = self.canvas.create_text( - x1 + distance, y1 + distance * m, text=self.ip4_address_1, tags="linkinfo" + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + label_one = None + if self.link.HasField("interface_one"): + label_one = ( + f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n" + f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n" + ) + label_two = None + if self.link.HasField("interface_two"): + label_two = ( + f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" + f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" + ) + self.id1 = self.canvas.create_text( + x1, y1, text=label_one, justify=tk.CENTER, tags="linkinfo" ) - return id1 - - def create_edge_dst_info(self): - """ - draw the ip address for destination node - - :return: nothing - """ - x1, _, x2, y2 = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - # id2 = self.canvas.create_text(x2, y2, text=self.ip4_address_2) - id2 = self.canvas.create_text( - x2 - distance, y2 - distance * m, text=self.ip4_address_2, tags="linkinfo" + self.id2 = self.canvas.create_text( + x2, y2, text=label_two, justify=tk.CENTER, tags="linkinfo" ) - return id2 def recalculate_info(self): """ @@ -80,32 +64,13 @@ class LinkInfo: :return: nothing """ - x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - m = self.slope_src_dst() - distance = math.cos(math.atan(m)) * self.radius - if x1 > x2: - distance = -distance - new_x1 = x1 + distance - new_y1 = y1 + distance * m - new_x2 = x2 - distance - new_y2 = y2 - distance * m - self.canvas.coords(self.id1, new_x1, new_y1) - self.canvas.coords(self.id2, new_x2, new_y2) - - # def link_througput(self): - # x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - # x = (x1 + x2) / 2 - # y = (y1 + y2) / 2 - # tid = self.canvas.create_text(x, y, text="place text here") - # return tid + x1, y1, x2, y2 = self.get_coordinates() + self.canvas.coords(self.id1, x1, y1) + self.canvas.coords(self.id2, x2, y2) class Throughput: def __init__(self, canvas, core): - """ - create an instance of Throughput object - :param coretk.app.Application app: application - """ self.canvas = canvas self.core = core # edge canvas id mapped to throughput value From 731b586df041d4e6723ea0d5d9f5950d52e44959 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:05:07 -0800 Subject: [PATCH 164/305] fixed issue with clearing out session location for created sessions --- coretk/coretk/coreclient.py | 1 - coretk/coretk/linkinfo.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c4e58795..794401b6 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -83,7 +83,6 @@ class CoreClient: self.interface_to_edge.clear() # session data self.canvas_nodes.clear() - self.location = None self.links.clear() self.hooks.clear() self.wlan_configs.clear() diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 2b7ecc7e..9004afe8 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -5,7 +5,7 @@ import logging import math import tkinter as tk -TEXT_DISTANCE = 0.25 +TEXT_DISTANCE = 0.33 class LinkInfo: From cdc48a765a1e8e9fb171b9090e88e7fcf6ee46c3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:40:09 -0800 Subject: [PATCH 165/305] ignoring control networks on join, fixed emane links on join being wireless, fixed setting emane model being set when configuring emane --- coretk/coretk/coreclient.py | 13 ++++++++++++- coretk/coretk/dialogs/emaneconfig.py | 5 +---- coretk/coretk/graph.py | 2 +- coretk/coretk/nodeutils.py | 5 +++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 794401b6..c88647c3 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -184,6 +184,15 @@ class CoreClient: self.emane_config = response.config # get emane model config + response = self.client.get_emane_model_configs(self.session_id) + for _id in response.configs: + config = response.configs[_id] + interface = None + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + self.set_emane_model_config(node_id, config.model, config.config, interface) # draw session self.app.canvas.reset_and_redraw(session) @@ -249,7 +258,6 @@ class CoreClient: dialog.show() response = self.client.get_service_defaults(self.session_id) - logging.debug("get service defaults: %s", response) self.default_services = { x.node_type: set(x.services) for x in response.defaults } @@ -609,6 +617,7 @@ class CoreClient: return config def get_emane_model_config(self, node_id, model, interface=None): + logging.info("getting emane model config: %s %s %s", node_id, model, interface) config = self.emane_model_configs.get((node_id, model, interface)) if not config: if interface is None: @@ -620,4 +629,6 @@ class CoreClient: return config def set_emane_model_config(self, node_id, model, config, interface=None): + logging.info("setting emane model config: %s %s %s", node_id, model, interface) + logging.info("model config: %s", config) self.emane_model_configs[(node_id, model, interface)] = config diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index bb6d8f1b..71ffe078 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -95,10 +95,7 @@ class EmaneConfigDialog(Dialog): self.radiovar = tk.IntVar() self.radiovar.set(1) self.emane_models = [x.split("_")[1] for x in self.app.core.emane_models] - emane_model = None - if self.emane_models: - emane_model = self.emane_models[0] - self.emane_model = tk.StringVar(value=emane_model) + self.emane_model = tk.StringVar(value=self.node.emane.split("_")[1]) self.emane_model_button = None self.draw() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index df660c7f..7d46ada8 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -163,7 +163,7 @@ class CanvasGraph(tk.Canvas): # draw existing nodes for core_node in session.nodes: # peer to peer node is not drawn on the GUI - if core_node.type == core_pb2.NodeType.PEER_TO_PEER: + if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 18b468b5..6d880126 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -47,8 +47,13 @@ class NodeUtils: CONTAINER_NODES = {NodeType.DEFAULT, NodeType.DOCKER, NodeType.LXC} IMAGE_NODES = {NodeType.DOCKER, NodeType.LXC} WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} + IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + @classmethod + def is_ignore_node(cls, node_type): + return node_type in cls.IGNORE_NODES + @classmethod def is_container_node(cls, node_type): return node_type in cls.CONTAINER_NODES From ad4ee58ddd0385023af37ae3ef7d782d73d27aca Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:46:32 -0800 Subject: [PATCH 166/305] properly adding change for emane node links being wireless, fixed issue when sending emane model configs when there is no interface set --- coretk/coretk/coreclient.py | 3 ++- daemon/core/emane/nodes.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c88647c3..71d6da1b 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -567,6 +567,8 @@ class CoreClient: for key, config in self.emane_model_configs.items(): node_id, model, interface = key config = {x: config[x].value for x in config} + if interface is None: + interface = -1 config_proto = core_pb2.EmaneModelConfig( node_id=node_id, interface_id=interface, model=model, config=config ) @@ -630,5 +632,4 @@ class CoreClient: def set_emane_model_config(self, node_id, model, config, interface=None): logging.info("setting emane model config: %s %s %s", node_id, model, interface) - logging.info("model config: %s", config) self.emane_model_configs[(node_id, model, interface)] = config diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index c7817b41..bd76ed81 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -25,7 +25,7 @@ class EmaneNet(CoreNetworkBase): """ apitype = NodeTypes.EMANE.value - linktype = LinkTypes.WIRELESS.value + linktype = LinkTypes.WIRED.value type = "wlan" is_emane = True From c1a8fada7a45c0734de3fc302ce3ec2c129bd5a6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 25 Nov 2019 15:48:41 -0800 Subject: [PATCH 167/305] avoid querying state for node movement, check currently known state instead --- coretk/coretk/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7d46ada8..ef36e2f9 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -636,7 +636,7 @@ class CanvasNode: new_x, new_y = self.canvas.coords(self.id) - if self.canvas.core.get_session_state() == core_pb2.SessionState.RUNTIME: + if self.canvas.core.is_runtime(): self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) for edge in self.edges: From 4238c14362a89a5d407a151ad3c4b45115b2c0e2 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 25 Nov 2019 16:50:44 -0800 Subject: [PATCH 168/305] progress bar for start session --- coretk/coretk/coreclient.py | 5 ++++ coretk/coretk/dialogs/nodeconfig.py | 5 ++-- coretk/coretk/status.py | 37 ++++++++++++++++------------- coretk/coretk/toolbar.py | 23 ++++-------------- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c4e58795..7b547fd8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -3,6 +3,7 @@ Incorporate grpc into python tkinter GUI """ import logging import os +import time from core.api.grpc import client, core_pb2 from coretk.dialogs.sessions import SessionsDialog @@ -280,6 +281,8 @@ class CoreClient: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: emane_config = None + + start = time.time() response = self.client.start_session( self.session_id, nodes, @@ -293,7 +296,9 @@ class CoreClient: service_configs, file_configs, ) + process_time = time.time() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) + self.app.statusbar.start_session_callback(process_time) def stop_session(self, session_id=None): if not session_id: diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8b9fcfab..f296e4cf 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -197,8 +197,9 @@ class NodeConfigDialog(Dialog): self.node.name = self.name.get() if NodeUtils.is_image_node(self.node.type): self.node.image = self.container_image.get() - if NodeUtils.is_container_node(self.node.type): - self.node.server = self.server.get() + server = self.server.get() + if NodeUtils.is_container_node(self.node.type) and server != "localhost": + self.node.server = server # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index 69869f51..eb285760 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -1,5 +1,4 @@ "status bar" -import time import tkinter as tk from tkinter import ttk @@ -11,6 +10,7 @@ class StatusBar(ttk.Frame): self.status = None self.statusvar = tk.StringVar() + self.progress_bar = None self.zoom = None self.cpu_usage = None self.memory = None @@ -19,26 +19,31 @@ class StatusBar(ttk.Frame): self.draw() def draw(self): - self.columnconfigure(0, weight=8) - self.columnconfigure(1, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=7) self.columnconfigure(2, weight=1) self.columnconfigure(3, weight=1) + self.columnconfigure(4, weight=1) + + self.progress_bar = ttk.Progressbar( + self, orient="horizontal", mode="indeterminate" + ) + self.progress_bar.grid(row=0, column=0, sticky="nsew") self.status = ttk.Label(self, textvariable=self.statusvar) self.statusvar.set("status") - self.status.grid(row=0, column=0) + self.status.grid(row=0, column=1, sticky="nsew") self.zoom = ttk.Label(self, text="zoom") - self.zoom.grid(row=0, column=1) + self.zoom.grid(row=0, column=2) self.cpu_usage = ttk.Label(self, text="cpu usage") - self.cpu_usage.grid(row=0, column=2) + self.cpu_usage.grid(row=0, column=3) self.emulation_light = ttk.Label(self, text="emulation light") - self.emulation_light.grid(row=0, column=3) + self.emulation_light.grid(row=0, column=4) - def processing(self): - texts = ["Processing.", "Processing..", "Processing...", "Processing...."] - i = 0 - while self.running: - self.statusvar.set(texts[i % 4]) - self.master.update() - i = i + 1 - time.sleep(0.002) - print("thread finish") + def start_session_callback(self, process_time): + num_nodes = len(self.app.core.canvas_nodes) + num_links = len(self.app.core.links) + self.progress_bar.stop() + self.statusvar.set( + "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" + % ("%.3f" % process_time, num_nodes, num_links) + ) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 1aae22c8..9bda6eda 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,4 +1,5 @@ import logging +import threading import time import tkinter as tk from functools import partial @@ -196,25 +197,11 @@ class Toolbar(ttk.Frame): :return: nothing """ - self.app.statusbar.running = True - # thread = threading.Thread(target=self.app.statusbar.processing) - # thread.start() - self.master.config(cursor="watch") - self.master.update() + self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT - start = time.time() - self.app.core.start_session() - dur = time.time() - start + thread = threading.Thread(target=self.app.core.start_session) + thread.start() self.runtime_frame.tkraise() - self.master.config(cursor="") - nodes_num = len(self.app.core.canvas_nodes) - links_num = len(self.app.core.links) - self.app.statusbar.statusvar.set( - "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" - % ("%.3f" % dur, nodes_num, links_num) - ) - # self.app.statusbar.running = False - # print("done") def click_link(self): logging.debug("Click LINK button") @@ -367,8 +354,6 @@ class Toolbar(ttk.Frame): :return: nothing """ - logging.debug("Click on STOP button ") - # self.status_thread.join() start = time.time() self.app.core.stop_session() dur = time.time() - start From fa7e5e321ba04c6ae401434233137df3a91ae460 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 26 Nov 2019 11:30:25 -0800 Subject: [PATCH 169/305] save service config, file config when join session, update progress bar to start, stop, join session, delete antennas, wirelesslink as well as other stuff that we had before when join session --- coretk/coretk/coreclient.py | 35 +++++++++++++++++++++++++++---- coretk/coretk/dialogs/sessions.py | 7 ++++++- coretk/coretk/graph_helper.py | 11 +++++++++- coretk/coretk/menuaction.py | 35 ++++++++++++++++++++++++------- coretk/coretk/nodedelete.py | 5 ++++- coretk/coretk/status.py | 4 ++++ coretk/coretk/toolbar.py | 10 +++------ 7 files changed, 86 insertions(+), 21 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e6531817..3feb334a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -137,8 +137,8 @@ class CoreClient: ) def join_session(self, session_id, query_location=True): - self.master.config(cursor="watch") - self.master.update() + # self.master.config(cursor="watch") + # self.master.update() # update session and title self.session_id = session_id @@ -198,12 +198,36 @@ class CoreClient: # draw session self.app.canvas.reset_and_redraw(session) - # draw tool bar appropritate with session state + # get node service config and file config + for node in session.nodes: + self.created_nodes.add(node.id) + for link in session.links: + self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + for node in session.nodes: + if node.type == core_pb2.NodeType.DEFAULT: + for service in node.services: + response = self.client.get_node_service( + self.session_id, node.id, service + ) + if node.id not in self.service_configs: + self.service_configs[node.id] = {} + self.service_configs[node.id][service] = response.service + for file in response.service.configs: + response = self.client.get_node_service_file( + self.session_id, node.id, service, file + ) + if node.id not in self.file_configs: + self.file_configs[node.id] = {} + if service not in self.file_configs[node.id]: + self.file_configs[node.id][service] = {} + self.file_configs[node.id][service][file] = response.data + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() - self.master.config(cursor="") + # self.master.config(cursor="") + self.app.statusbar.progress_bar.stop() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME @@ -310,7 +334,10 @@ class CoreClient: def stop_session(self, session_id=None): if not session_id: session_id = self.session_id + start = time.time() response = self.client.stop_session(session_id) + process_time = time.time() - start + self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) def launch_terminal(self, node_id): diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 6f299dfc..7a07c059 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -1,4 +1,5 @@ import logging +import threading import tkinter as tk from tkinter import ttk @@ -146,7 +147,11 @@ class SessionsDialog(Dialog): logging.error("querysessiondrawing.py invalid state") def join_session(self, session_id): - self.app.core.join_session(session_id) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.app.core.join_session, args=([session_id]) + ) + thread.start() self.destroy() def on_selected(self, event): diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index 1ad0c8f6..eea4ff55 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -7,7 +7,16 @@ import tkinter as tk from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils -CANVAS_COMPONENT_TAGS = ["edge", "node", "nodename", "wallpaper", "linkinfo"] +CANVAS_COMPONENT_TAGS = [ + "edge", + "node", + "nodename", + "wallpaper", + "linkinfo", + "antenna", + "wireless", + "selectednodes", +] class GraphHelper: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index b47c6ef2..066e37e2 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -3,6 +3,8 @@ The actions taken when each menubar option is clicked """ import logging +import threading +import time import webbrowser from tkinter import filedialog, messagebox @@ -27,7 +29,16 @@ class MenuAction: self.master = master self.app = app - def prompt_save_running_session(self): + def cleanup_old_session(self, quitapp=False): + start = time.time() + self.app.core.stop_session() + self.app.core.delete_session() + process_time = time.time() - start + self.app.statusbar.stop_session_callback(process_time) + if quitapp: + self.app.quit() + + def prompt_save_running_session(self, quitapp=False): """ Prompt use to stop running session before application is closed @@ -43,13 +54,20 @@ class MenuAction: or state == core_pb2.SessionState.DEFINITION ): self.app.core.delete_session() + if quitapp: + self.app.quit() else: msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") - if msgbox or msgbox is False: if msgbox: - self.app.core.stop_session() - self.app.core.delete_session() + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread( + target=self.cleanup_old_session, args=([quitapp]) + ) + thread.start() + + # self.app.core.stop_session() + # self.app.core.delete_session() def on_quit(self, event=None): """ @@ -57,8 +75,8 @@ class MenuAction: :return: nothing """ - self.prompt_save_running_session() - self.app.quit() + self.prompt_save_running_session(quitapp=True) + # self.app.quit() def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") @@ -81,7 +99,10 @@ class MenuAction: if file_path: logging.info("opening xml: %s", file_path) self.prompt_save_running_session() - self.app.core.open_xml(file_path) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) + thread.start() + # self.app.core.open_xml(file_path) def gui_preferences(self): dialog = PreferencesDialog(self.app, self.app) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 27e330b5..4ec986aa 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -27,7 +27,10 @@ class CanvasComponentManagement: if canvas_node.id not in self.selected: x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) bbox_id = self.canvas.create_rectangle( - (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-" + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), + activedash=True, + dash="-", + tags="selectednodes", ) self.selected[canvas_node.id] = bbox_id diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index eb285760..ea31d6fc 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -47,3 +47,7 @@ class StatusBar(ttk.Frame): "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" % ("%.3f" % process_time, num_nodes, num_links) ) + + def stop_session_callback(self, cleanup_time): + self.progress_bar.stop() + self.statusvar.set("Cleanup completed in %s seconds" % "%.3f" % cleanup_time) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 9bda6eda..40fc36c0 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -1,6 +1,5 @@ import logging import threading -import time import tkinter as tk from functools import partial from tkinter import ttk @@ -354,12 +353,9 @@ class Toolbar(ttk.Frame): :return: nothing """ - start = time.time() - self.app.core.stop_session() - dur = time.time() - start - self.app.statusbar.statusvar.set( - "Cleanup completed in %s seconds" % "%.3f" % dur - ) + self.app.statusbar.progress_bar.start(5) + thread = threading.Thread(target=self.app.core.stop_session) + thread.start() self.app.canvas.delete("wireless") self.design_frame.tkraise() From bc026bb69369c2dbb7ae733f44744fffeb99b251 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 11:32:48 -0800 Subject: [PATCH 170/305] initial work for mobility player dialog, added default sample1.xml to created gui directory, fixed issue with non modal dialogs --- coretk/coretk/appconfig.py | 4 + coretk/coretk/coreclient.py | 30 ++- coretk/coretk/dialogs/dialog.py | 2 +- coretk/coretk/dialogs/mobilityplayer.py | 61 +++++ coretk/coretk/linkinfo.py | 11 +- coretk/coretk/menuaction.py | 2 +- coretk/coretk/xmls/sample1.xml | 296 ++++++++++++++++++++++++ 7 files changed, 397 insertions(+), 9 deletions(-) create mode 100644 coretk/coretk/dialogs/mobilityplayer.py create mode 100644 coretk/coretk/xmls/sample1.xml diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 2af1047c..74d52553 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -20,6 +20,7 @@ CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH = Path(__file__).parent.joinpath("xmls").absolute() # configuration data TERMINALS = [ @@ -59,6 +60,9 @@ def check_directory(): for background in LOCAL_BACKGROUND_PATH.glob("*"): new_background = BACKGROUNDS_PATH.joinpath(background.name) shutil.copy(background, new_background) + for xml_file in LOCAL_XMLS_PATH.glob("*"): + new_xml = XML_PATH.joinpath(xml_file.name) + shutil.copy(xml_file, new_xml) if "TERM" in os.environ: terminal = TERMINALS[0] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 71d6da1b..865baedc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,6 +5,7 @@ import logging import os from core.api.grpc import client, core_pb2 +from coretk.dialogs.mobilityplayer import MobilityPlayerDialog from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -74,6 +75,7 @@ class CoreClient: self.emane_config = None self.service_configs = {} self.file_configs = {} + self.mobility_players = {} def reset(self): # helpers @@ -91,6 +93,7 @@ class CoreClient: self.emane_config = None self.service_configs.clear() self.file_configs.clear() + self.mobility_players.clear() def set_observer(self, value): self.observer = value @@ -119,8 +122,33 @@ class CoreClient: if event.HasField("link_event"): self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): - if event.session_event.event <= core_pb2.SessionState.SHUTDOWN: + session_event = event.session_event + if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event + # mobility start + elif session_event.event == 7: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog + # mobility stop + elif session_event.event == 8: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog + # mobility pause + elif session_event.event == 9: + node_id = session_event.node_id + if node_id not in self.mobility_players: + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) + dialog.show() + self.mobility_players[node_id] = dialog def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 769fd176..29362960 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -29,4 +29,4 @@ class Dialog(tk.Toplevel): if self.modal: self.wait_visibility() self.grab_set() - self.wait_window() + self.wait_window() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py new file mode 100644 index 00000000..cac4caa7 --- /dev/null +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -0,0 +1,61 @@ +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + +PAD = 5 + + +class MobilityPlayerDialog(Dialog): + def __init__(self, master, app, canvas_node): + super().__init__( + master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False + ) + self.play_button = None + self.pause_button = None + self.stop_button = None + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + + label = ttk.Label(self.top, text="File Name") + label.grid(sticky="ew", pady=PAD) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(0, weight=1) + progressbar = ttk.Progressbar(frame, mode="indeterminate") + progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) + progressbar.start() + label = ttk.Label(frame, text="time") + label.grid(row=0, column=1) + + frame = ttk.Frame(self.top) + frame.grid(sticky="ew", pady=PAD) + self.play_button = ttk.Button(frame, text="Play", command=self.click_play) + self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) + self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) + self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) + checkbutton = ttk.Checkbutton(frame, text="Loop?") + checkbutton.grid(row=0, column=3, padx=PAD) + label = ttk.Label(frame, text="rate 50 ms") + label.grid(row=0, column=4) + + def clear_buttons(self): + self.play_button.state(["!pressed"]) + self.pause_button.state(["!pressed"]) + self.stop_button.state(["!pressed"]) + + def click_play(self): + self.clear_buttons() + self.play_button.state(["pressed"]) + + def click_pause(self): + self.clear_buttons() + self.pause_button.state(["pressed"]) + + def click_stop(self): + self.clear_buttons() + self.stop_button.state(["pressed"]) diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/linkinfo.py index 9004afe8..c78fe7e4 100644 --- a/coretk/coretk/linkinfo.py +++ b/coretk/coretk/linkinfo.py @@ -2,10 +2,10 @@ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ import logging -import math import tkinter as tk +from tkinter import font -TEXT_DISTANCE = 0.33 +TEXT_DISTANCE = 0.30 class LinkInfo: @@ -21,20 +21,19 @@ class LinkInfo: self.link = link self.id1 = None self.id2 = None + self.font = font.Font(size=8) self.draw_labels() def get_coordinates(self): x1, y1, x2, y2 = self.canvas.coords(self.edge.id) v1 = x2 - x1 v2 = y2 - y1 - d = math.sqrt(v1 ** 2 + v2 ** 2) ux = TEXT_DISTANCE * v1 uy = TEXT_DISTANCE * v2 x1 = x1 + ux y1 = y1 + uy x2 = x2 - ux y2 = y2 - uy - logging.info("line distance: %s", d) return x1, y1, x2, y2 def draw_labels(self): @@ -52,10 +51,10 @@ class LinkInfo: f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" ) self.id1 = self.canvas.create_text( - x1, y1, text=label_one, justify=tk.CENTER, tags="linkinfo" + x1, y1, text=label_one, justify=tk.CENTER, font=self.font, tags="linkinfo" ) self.id2 = self.canvas.create_text( - x2, y2, text=label_two, justify=tk.CENTER, tags="linkinfo" + x2, y2, text=label_two, justify=tk.CENTER, font=self.font, tags="linkinfo" ) def recalculate_info(self): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index b47c6ef2..89386162 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -76,7 +76,7 @@ class MenuAction: file_path = filedialog.askopenfilename( initialdir=str(XML_PATH), title="Open", - filetypes=(("EmulationScript XML File", "*.xml"), ("All Files", "*")), + filetypes=(("XML Files", "*.xml"), ("All Files", "*")), ) if file_path: logging.info("opening xml: %s", file_path) diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/xmls/sample1.xml new file mode 100644 index 00000000..cfa5a99a --- /dev/null +++ b/coretk/coretk/xmls/sample1.xml @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.5/32 + ipv6 address a::3/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +interface eth1 + ip address 10.0.6.2/24 + !ip ospf hello-interval 2 + !ip ospf dead-interval 6 + !ip ospf retransmit-interval 5 + !ip ospf network point-to-point + ipv6 address a:6::2/64 +! +router ospf + router-id 10.0.0.5 + network 10.0.0.5/32 area 0 + network 10.0.6.0/24 area 0 + redistribute connected metric-type 1 + redistribute ospf6 metric-type 1 +! +router ospf6 + router-id 10.0.0.5 + interface eth0 area 0.0.0.0 + redistribute connected + redistribute ospf +! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 00e0da2990cf4855cb7104d347f4c7930e9f060d Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 26 Nov 2019 15:12:04 -0800 Subject: [PATCH 171/305] improve wireless link logic when deleting nodes, stop session and join session --- coretk/coretk/dialogs/customnodes.py | 6 +++--- coretk/coretk/graph.py | 1 + coretk/coretk/nodedelete.py | 24 ++++++++++++++++++++++++ coretk/coretk/toolbar.py | 5 ++++- coretk/coretk/wirelessconnection.py | 4 ++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 55199541..dbf431d3 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -62,9 +62,9 @@ class ServicesSelectDialog(Dialog): index = selection[0] group = self.groups.listbox.get(index) self.services.clear() - for service in sorted(self.app.core.services[group], key=lambda x: x.name): - checked = service.name in self.current_services - self.services.add(service.name, checked) + for name in sorted(self.app.core.services[group]): + checked = name in self.current_services + self.services.add(name, checked) def service_clicked(self, name, var): if var.get() and name not in self.current_services: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef36e2f9..b64ce950 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -114,6 +114,7 @@ class CanvasGraph(tk.Canvas): self.nodes.clear() self.edges.clear() self.drawing_edge = None + self.wireless_draw.map.clear() self.draw_session(session) def setup_bindings(self): diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 4ec986aa..92844aac 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -45,6 +45,18 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() nodes = [] + + node_to_wlink = {} + for link_tuple in self.canvas.wireless_draw.map: + nid_one, nid_two = link_tuple + if nid_one not in node_to_wlink: + node_to_wlink[nid_one] = [] + if nid_two not in node_to_wlink: + node_to_wlink[nid_two] = [] + node_to_wlink[nid_one].append(link_tuple) + node_to_wlink[nid_two].append(link_tuple) + + # delete antennas and wireless links for cnid in self.selected: canvas_node = self.canvas.nodes[cnid] if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: @@ -63,6 +75,18 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() + for link_tuple in node_to_wlink[canvas_node.core_node.id]: + nid_one, nid_two = link_tuple + if link_tuple in self.canvas.wireless_draw.map: + self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) + link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) + canvas_node_one = self.app.canvas_nodes[nid_one] + canvas_node_two = self.app.canvas_nodes[nid_two] + if link_cid in canvas_node_one.wlans: + canvas_node_one.wlans.remove(link_cid) + if link_cid in canvas_node_two.wlans: + canvas_node_two.wlans.remove(link_cid) + for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 40fc36c0..d1830084 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -356,7 +356,10 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - self.app.canvas.delete("wireless") + for cid in self.app.canvas.find_withtag("wireless"): + self.app.canvas.itemconfig(cid, state="hidden") + # self.app.canvas.delete("wireless") + self.design_frame.tkraise() def update_annotation(self, image): diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 2e0e0af9..7b868bea 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -16,6 +16,7 @@ class WirelessConnection: canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) if key not in self.map: + print("not in map") x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( @@ -24,6 +25,9 @@ class WirelessConnection: self.map[key] = wlan_canvas_id canvas_node_one.wlans.append(wlan_canvas_id) canvas_node_two.wlans.append(wlan_canvas_id) + else: + print("in map") + self.canvas.itemconfig(self.map[key], state="normal") def delete_connection(self, node_one_id, node_two_id): canvas_node_one = self.core.canvas_nodes[node_one_id] From 2a08c770d37392e0e506c2a5f305ce72184e4253 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 16:50:23 -0800 Subject: [PATCH 172/305] updates to handle node events for moving nodes and displaying mobility player dialog on session event with configured data --- coretk/coretk/coreclient.py | 26 ++++++++++++------------- coretk/coretk/dialogs/mobilityplayer.py | 18 ++++++++++++++--- coretk/coretk/graph.py | 20 ++++++++++++++++++- coretk/coretk/xmls/sample1.xml | 2 +- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 146ed44d..04d9df06 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -119,10 +119,11 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - logging.info("event: %s", event) if event.HasField("link_event"): + logging.info("link event: %s", event) self.app.canvas.wireless_draw.handle_link_event(event.link_event) elif event.HasField("session_event"): + logging.info("session event: %s", event) session_event = event.session_event if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event @@ -136,20 +137,19 @@ class CoreClient: self.mobility_players[node_id] = dialog # mobility stop elif session_event.event == 8: - node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog + pass # mobility pause elif session_event.event == 9: - node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog + pass + elif event.HasField("node_event"): + node_event = event.node_event + node_id = node_event.node.id + x = node_event.node.position.x + y = node_event.node.position.y + canvas_node = self.canvas_nodes[node_id] + canvas_node.move(x, y) + else: + logging.info("unhandled event: %s", event) def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index cac4caa7..0eee7166 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,3 +1,4 @@ +import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog @@ -10,6 +11,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.config = self.app.core.mobility_configs[canvas_node.core_node.id] self.play_button = None self.pause_button = None self.stop_button = None @@ -18,7 +20,8 @@ class MobilityPlayerDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - label = ttk.Label(self.top, text="File Name") + file_name = self.config["file"].value + label = ttk.Label(self.top, text=file_name) label.grid(sticky="ew", pady=PAD) frame = ttk.Frame(self.top) @@ -32,15 +35,24 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) + self.play_button = ttk.Button(frame, text="Play", command=self.click_play) self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) - checkbutton = ttk.Checkbutton(frame, text="Loop?") + + loop = tk.IntVar(value=int(self.config["loop"].value == "1")) + checkbutton = ttk.Checkbutton( + frame, text="Loop?", variable=loop, state=tk.DISABLED + ) checkbutton.grid(row=0, column=3, padx=PAD) - label = ttk.Label(frame, text="rate 50 ms") + + rate = self.config["refresh_ms"].value + label = ttk.Label(frame, text=f"rate {rate} ms") label.grid(row=0, column=4) def clear_buttons(self): diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index ef36e2f9..7c8f154e 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -585,6 +585,25 @@ class CanvasNode: self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) + def move(self, x, y): + old_x = self.core_node.position.x + old_y = self.core_node.position.y + x_offset = x - old_x + y_offset = y - old_y + self.core_node.position.x = x + self.core_node.position.y = y + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x_offset, y_offset, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) + edge.link_info.recalculate_info() + self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) + def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") @@ -612,7 +631,6 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - self.canvas.canvas_management.node_select(self) def click_release(self, event): diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/xmls/sample1.xml index cfa5a99a..8bda5b8c 100644 --- a/coretk/coretk/xmls/sample1.xml +++ b/coretk/coretk/xmls/sample1.xml @@ -188,7 +188,7 @@ - + From 13ca85cf3f12a374996ccbba1d6c6782b576cce2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 26 Nov 2019 17:00:55 -0800 Subject: [PATCH 173/305] changes to support mobility actions for mobility player dialog --- coretk/coretk/dialogs/mobilityplayer.py | 25 ++++++++++++++++++++++--- coretk/coretk/wirelessconnection.py | 1 - 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 0eee7166..3a4020e6 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,6 +1,7 @@ import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog PAD = 5 @@ -11,10 +12,13 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.canvas_node = canvas_node + self.node = canvas_node.core_node self.config = self.app.core.mobility_configs[canvas_node.core_node.id] self.play_button = None self.pause_button = None self.stop_button = None + self.progressbar = None self.draw() def draw(self): @@ -27,9 +31,9 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(0, weight=1) - progressbar = ttk.Progressbar(frame, mode="indeterminate") - progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) - progressbar.start() + self.progressbar = ttk.Progressbar(frame, mode="indeterminate") + self.progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) + self.progressbar.start() label = ttk.Label(frame, text="time") label.grid(row=0, column=1) @@ -63,11 +67,26 @@ class MobilityPlayerDialog(Dialog): def click_play(self): self.clear_buttons() self.play_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.START + ) + self.progressbar.start() def click_pause(self): self.clear_buttons() self.pause_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.PAUSE + ) + self.progressbar.stop() def click_stop(self): self.clear_buttons() self.stop_button.state(["pressed"]) + session_id = self.app.core.session_id + self.app.core.client.mobility_action( + session_id, self.node.id, core_pb2.MobilityAction.STOP + ) + self.progressbar.stop() diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 7b868bea..729fd84e 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -16,7 +16,6 @@ class WirelessConnection: canvas_node_two = self.core.canvas_nodes[node_two_id] key = tuple(sorted((node_one_id, node_two_id))) if key not in self.map: - print("not in map") x1, y1 = self.canvas.coords(canvas_node_one.id) x2, y2 = self.canvas.coords(canvas_node_two.id) wlan_canvas_id = self.canvas.create_line( From 3cd48ec1aba9cc2436b2156b9c6a1e3138d203a2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 08:48:21 -0800 Subject: [PATCH 174/305] added pause icon and icons to mobility player buttons --- coretk/coretk/dialogs/mobilityplayer.py | 16 +++++++++++++--- coretk/coretk/icons/pause.png | Bin 0 -> 2368 bytes coretk/coretk/images.py | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 coretk/coretk/icons/pause.png diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 3a4020e6..30053498 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -3,8 +3,10 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images PAD = 5 +ICON_SIZE = 16 class MobilityPlayerDialog(Dialog): @@ -39,14 +41,22 @@ class MobilityPlayerDialog(Dialog): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) + for i in range(3): + frame.columnconfigure(i, weight=1) - self.play_button = ttk.Button(frame, text="Play", command=self.click_play) + image = Images.get(ImageEnum.START, width=ICON_SIZE) + self.play_button = ttk.Button(frame, image=image, command=self.click_play) + self.play_button.image = image self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) - self.pause_button = ttk.Button(frame, text="Pause", command=self.click_pause) + image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) + self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) + self.pause_button.image = image self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) - self.stop_button = ttk.Button(frame, text="Stop", command=self.click_stop) + image = Images.get(ImageEnum.STOP, width=ICON_SIZE) + self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) + self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) diff --git a/coretk/coretk/icons/pause.png b/coretk/coretk/icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac4e6eab0939e105f042d7b97722579b98b5a82 GIT binary patch literal 2368 zcmV-G3BUG`WoBV_kfjAR0kx<|j5ox#R>T;& zjMPv}Q@=D#Y)zY3iKR+vOKj4l?T3D`QRqb(HY%-{8Y$ke6k{)xYKu#8h22?p7K*YL zV4d?m{jfXh-uKMqoHOj1-={g3_j&($|K~RE`<~}OnLv@C1-PN{rTLSqPj@WH*wl+s zDR7|?trn#cK!q+#r9FJydo`M4qZ$-TGk=0(>_hm7IOGK{pC| z7r9?nyuwlB1+yznlM5$Zq$eLl^ig1nSM_+z8C0I=>7KG>-q)JW zdQpKV2DEICT`wYA6+Y)h`~YZF^uBl^y|37Ti)w%>(z>JO0pKSJ6~&BQ0%TBmy!PM6 zH_M}#Vg)Ul0jb~B&LrY&RajB1m?g^YNJiJ!+?#GIMzEq7kZP#COpWt0aB(r>od-0j zac+yRJMn&@fviFUq#9;lqsDoI@H9R^y;%0X=49-;LIV|KKy$KYxf-(vh!!g3JYFolPx3L%8j2B5k#K;;Hmm?dx6I_U}{I_!=RTG zM^~trUr}}DyW{ON<|U|iFvU8E2mSf>)LJgQax-?N`+K@N-E=lS$=T)?N8Elvd?l3& z?nmsYu46h~NBQ``CYoPfTCStQ_tU;YIXCL&9WO*`}a^aPmnG#=_bGhj=uA(iy z7t{06p!L?I>0Gq*mxz_ym9k>`6)13O-~Bmjalr){ho3RV!^^0bpw#x+H+eaXXjj$e zRZgr)*i+-f<4vv0x$(0^^3>vzl`Y$2*N?VwR0C9Wqc?iOhlo5p+O`o5XlaNp2K|zE zlEQ_;9jWB(t4G>4q5*2`M=0&7Bj+K`_eNSf%z&fM%&hI3G*zaEC~|i?8J#tB{V)UU zNMt>wGh=NcVfP^S4qZ9S08w4BE=?z+{bT9<2An$=xvAXc z>^%w<8Mq<4+;4zjZVpCL_)xKyWqn~+uDNOVMofd z9sFcoUz_(DP<^^%0p;!oKd~!McYPN649HkAH&{L4Lu{>wJ_AH`TA*q|h-0XRtO3%y zP{uz*^iu;2TAt3Ed=rh1VN-Bp=7|0&< zTB8G{RT`H z=M8~R0#Ot$daQG@H@nbpKrGSTsqjuPn!<$cpS7W_E4ws!Y9j3MUr^YvULCYFaKT_2 z{1p>E3_7Td2N@7gwEqM6r>}}ah5UU^qHWOA<_O~f@*BV8gbh{w_0V<03~-#xZz*?6 zctK{8m}iEr9A?19UpbLdV~O_TL)VWoB(f*3B}l3M%pl<0lhb4 zdD&Y{q2k5S(&CT)raWe|cX~pG=!i!@jA1}Lk=`e;%Ns@DbT;KxPE8L^ck}Rg-7Toe zuO}swrxus&tXrG@^Jp8#{#J`^eG|vr0=Uor;*yUKY(jxZF6nAA-ErNDW@rRg}^5&x@@d#e->9$6Xzt3zYX$8;nKo^ zQ66&B_>eL&XNS zjSo4_j*hQtIcYFo1d8(cjtNG)V6T{yX#QU=g>s&rJ}1$BSS)KLl;@KUphK{3uS=wI zep<}y{eapvt?z)Y272--Kf&lhSyh{8{cApj^L|02E|Gpk;dUU*6MMS^b64FxZM*Xg zP;g?t-jDd0U4-w04ky*HGQO_uFNFjtN@m37XJ=j_mi<&DZ_y@Cu7$M*3%Ow?Y$l{ zmfSaIP22urNA~13)p#Pkul3aNMFQUj&gT~Z&!F;Hch$5jy=Z*!AwBW&WX(JWeh%cG zt3FtEifw(%>u=5bmLIyMA-Y(M^{~PkKkLnd-D>!Lp^ukt@x38>jgXCs+)e4^C~^dz zGe#eeuS*~Bv*N&x#OCMfK4WDvs}(i?qkiDPW8On$TZK4VN18O{2PJVSIMxum!d7dI zsxB9})~ocH0-q?dAJ}8ihM^N>gPS-jmRk*I@OV; mQ`U_CE=;^z?yW5|Hvb2nH000xvBxR^0000 Date: Wed, 27 Nov 2019 08:49:58 -0800 Subject: [PATCH 175/305] improve small logic in node deletion and wallpaper change --- coretk/coretk/dialogs/canvasbackground.py | 6 ++++-- coretk/coretk/dialogs/preferences.py | 12 ++++++++++++ coretk/coretk/nodedelete.py | 24 ++++++++++++----------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 8fb92c8b..84b0bdb6 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -12,6 +12,7 @@ from coretk.dialogs.dialog import Dialog from coretk.images import Images PADX = 5 +ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class CanvasBackgroundDialog(Dialog): @@ -182,17 +183,18 @@ class CanvasBackgroundDialog(Dialog): self.canvas.wallpaper_file = None self.destroy() return - try: img = Image.open(filename) self.canvas.wallpaper = img self.canvas.wallpaper_file = filename self.canvas.redraw() + for component in ABOVE_WALLPAPER: + self.canvas.tag_raise(component) + except FileNotFoundError: logging.error("invalid background: %s", filename) if self.canvas.wallpaper_id: self.canvas.delete(self.canvas.wallpaper_id) self.canvas.wallpaper_id = None self.canvas.wallpaper_file = None - self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 7298f727..d65b87c3 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,6 +14,8 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) + self.width = tk.StringVar(value="1000") + self.height = tk.StringVar(value="800") self.draw() def draw(self): @@ -58,6 +60,16 @@ class PreferencesDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") + label = ttk.Label(frame, text="Canvas width (in pixel)") + label.grid(row=4, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.width) + entry.grid(row=4, column=1, sticky="ew") + + label = ttk.Label(frame, text="Canvas height (in pixel)") + label.grid(row=5, column=0, pady=2, padx=2, sticky="w") + entry = ttk.Entry(frame, textvariable=self.height) + entry.grid(row=5, column=1, sticky="ew") + def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 92844aac..58e90170 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -75,17 +75,19 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() - for link_tuple in node_to_wlink[canvas_node.core_node.id]: - nid_one, nid_two = link_tuple - if link_tuple in self.canvas.wireless_draw.map: - self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) - link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) - canvas_node_one = self.app.canvas_nodes[nid_one] - canvas_node_two = self.app.canvas_nodes[nid_two] - if link_cid in canvas_node_one.wlans: - canvas_node_one.wlans.remove(link_cid) - if link_cid in canvas_node_two.wlans: - canvas_node_two.wlans.remove(link_cid) + + if canvas_node.core_node.id in node_to_wlink: + for link_tuple in node_to_wlink[canvas_node.core_node.id]: + nid_one, nid_two = link_tuple + if link_tuple in self.canvas.wireless_draw.map: + self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) + link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) + canvas_node_one = self.app.canvas_nodes[nid_one] + canvas_node_two = self.app.canvas_nodes[nid_two] + if link_cid in canvas_node_one.wlans: + canvas_node_one.wlans.remove(link_cid) + if link_cid in canvas_node_two.wlans: + canvas_node_two.wlans.remove(link_cid) for node_id in list(self.selected): bbox_id = self.selected[node_id] From af9915191df37d9c89351b789fb813b828430af0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 08:52:20 -0800 Subject: [PATCH 176/305] fixed wireless link events displaying over nodes --- coretk/coretk/wirelessconnection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py index 729fd84e..dd51723b 100644 --- a/coretk/coretk/wirelessconnection.py +++ b/coretk/coretk/wirelessconnection.py @@ -43,6 +43,7 @@ class WirelessConnection: self.add_connection( link_event.link.node_one_id, link_event.link.node_two_id ) + self.canvas.tag_raise("node") if link_event.message_type == core_pb2.MessageType.DELETE: self.delete_connection( From 8b7d651d061600d9e6469c5c624177c0816cc0f7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:05:37 -0800 Subject: [PATCH 177/305] fixed issue with nod emobility location calculations being forced to use ints --- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/dataconversion.py | 4 ++-- daemon/core/location/mobility.py | 6 ------ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 79053eee..54606818 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -488,7 +488,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) + position = core_pb2.Position(x=int(event.x_position), y=int(event.y_position)) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8e1c270d..8228b536 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -26,8 +26,8 @@ def convert_node(node_data): (NodeTlvs.EMULATION_ID, node_data.emulation_id), (NodeTlvs.EMULATION_SERVER, node_data.server), (NodeTlvs.SESSION, node_data.session), - (NodeTlvs.X_POSITION, node_data.x_position), - (NodeTlvs.Y_POSITION, node_data.y_position), + (NodeTlvs.X_POSITION, int(node_data.x_position)), + (NodeTlvs.Y_POSITION, int(node_data.y_position)), (NodeTlvs.CANVAS, node_data.canvas), (NodeTlvs.NETWORK_ID, node_data.network_id), (NodeTlvs.SERVICES, node_data.services), diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 085962ff..6a796526 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -811,12 +811,6 @@ class WayPointMobility(WirelessModel): :param z: z position :return: nothing """ - if x is not None: - x = int(x) - if y is not None: - y = int(y) - if z is not None: - z = int(z) node.position.set(x, y, z) node_data = node.data(message_type=0) self.session.broadcast_node(node_data) From da203d578e7e9ef7d795ce66929f446ce3a88554 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:15:19 -0800 Subject: [PATCH 178/305] fixed issue with node event tests when position is None --- daemon/core/api/grpc/server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 54606818..c0fe1d94 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -488,7 +488,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - position = core_pb2.Position(x=int(event.x_position), y=int(event.y_position)) + x = None + if event.x_position is not None: + x = int(event.x_position) + y = None + if event.y_position is not None: + y = int(event.y_position) + position = core_pb2.Position(x=x, y=y) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( From 1ca9aec247c738522619644ec3abebfcf601752d Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:54:43 -0800 Subject: [PATCH 179/305] canvas size added to preferences and updated to dialog --- coretk/coretk/app.py | 9 ++++++++- coretk/coretk/appconfig.py | 2 ++ coretk/coretk/dialogs/preferences.py | 6 ++++-- coretk/coretk/graph.py | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index f0445257..c8232ebd 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -67,8 +67,15 @@ class Application(tk.Frame): self.draw_status() def draw_canvas(self): + width = self.guiconfig["preferences"]["width"] + height = self.guiconfig["preferences"]["height"] self.canvas = CanvasGraph( - self, self.core, background="#cccccc", scrollregion=(0, 0, 1200, 1000) + self, + self.core, + width, + height, + background="#cccccc", + scrollregion=(0, 0, 1200, 1000), ) self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = ttk.Scrollbar( diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 74d52553..fc1bc1a5 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -78,6 +78,8 @@ def check_directory(): "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", + "width": 1000, + "height": 750, }, "location": { "x": 0.0, diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index d65b87c3..11903d2d 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,8 +14,8 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) - self.width = tk.StringVar(value="1000") - self.height = tk.StringVar(value="800") + self.width = tk.StringVar(value=preferences["width"]) + self.height = tk.StringVar(value=preferences["height"]) self.draw() def draw(self): @@ -93,5 +93,7 @@ class PreferencesDialog(Dialog): preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() + preferences["width"] = self.width.get() + preferences["height"] = self.height.get() self.app.save_config() self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c03d7b48..518245d3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -39,7 +39,7 @@ class ScaleOption(enum.Enum): class CanvasGraph(tk.Canvas): - def __init__(self, master, core, cnf=None, **kwargs): + def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 @@ -54,7 +54,7 @@ class CanvasGraph(tk.Canvas): self.grid = None self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() - self.draw_grid() + self.draw_grid(width, height) self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) From d51dc0e9097727053a3b9318b537dc50170404c4 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 11:21:03 -0800 Subject: [PATCH 180/305] canvas size --- coretk/coretk/dialogs/canvassizeandscale.py | 3 +++ coretk/coretk/dialogs/preferences.py | 14 -------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index f25dfdd6..fe4c3b62 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -187,5 +187,8 @@ class SizeAndScaleDialog(Dialog): location_config["lon"] = location.lon location_config["alt"] = location.alt location_config["scale"] = location.scale + preferences = self.app.guiconfig["preferences"] + preferences["width"] = width + preferences["height"] = height self.app.save_config() self.destroy() diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 11903d2d..7298f727 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -14,8 +14,6 @@ class PreferencesDialog(Dialog): self.theme = tk.StringVar(value=preferences["theme"]) self.terminal = tk.StringVar(value=preferences["terminal"]) self.gui3d = tk.StringVar(value=preferences["gui3d"]) - self.width = tk.StringVar(value=preferences["width"]) - self.height = tk.StringVar(value=preferences["height"]) self.draw() def draw(self): @@ -60,16 +58,6 @@ class PreferencesDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") - label = ttk.Label(frame, text="Canvas width (in pixel)") - label.grid(row=4, column=0, pady=2, padx=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=4, column=1, sticky="ew") - - label = ttk.Label(frame, text="Canvas height (in pixel)") - label.grid(row=5, column=0, pady=2, padx=2, sticky="w") - entry = ttk.Entry(frame, textvariable=self.height) - entry.grid(row=5, column=1, sticky="ew") - def draw_buttons(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -93,7 +81,5 @@ class PreferencesDialog(Dialog): preferences["editor"] = self.editor.get() preferences["gui3d"] = self.gui3d.get() preferences["theme"] = self.theme.get() - preferences["width"] = self.width.get() - preferences["height"] = self.height.get() self.app.save_config() self.destroy() From 9a55ff4ca5964bc4cef915da66e9cf85831ec0c2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 11:41:54 -0800 Subject: [PATCH 181/305] display mobility player on session start, change buttons and progress bar on session events --- coretk/coretk/coreclient.py | 30 +++++++++++-------- coretk/coretk/dialogs/mobilityplayer.py | 40 ++++++++++++++----------- coretk/coretk/menubar.py | 2 +- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 04d9df06..e6dfb77e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -128,19 +128,18 @@ class CoreClient: if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event # mobility start - elif session_event.event == 7: + elif session_event.event in {7, 8, 9}: node_id = session_event.node_id - if node_id not in self.mobility_players: - canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node) - dialog.show() - self.mobility_players[node_id] = dialog - # mobility stop - elif session_event.event == 8: - pass - # mobility pause - elif session_event.event == 9: - pass + dialog = self.mobility_players.get(node_id) + if dialog: + if session_event.event == 7: + dialog.set_play() + elif session_event.event == 8: + dialog.set_stop() + else: + dialog.set_pause() + else: + logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): node_event = event.node_event node_id = node_event.node.id @@ -359,6 +358,13 @@ class CoreClient: logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + dialog = MobilityPlayerDialog(self.app, self.app, canvas_node, config) + dialog.show() + self.mobility_players[node_id] = dialog + def stop_session(self, session_id=None): if not session_id: session_id = self.session_id diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 30053498..65b16058 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -10,13 +10,13 @@ ICON_SIZE = 16 class MobilityPlayerDialog(Dialog): - def __init__(self, master, app, canvas_node): + def __init__(self, master, app, canvas_node, config): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) self.canvas_node = canvas_node self.node = canvas_node.core_node - self.config = self.app.core.mobility_configs[canvas_node.core_node.id] + self.config = config self.play_button = None self.pause_button = None self.stop_button = None @@ -30,14 +30,8 @@ class MobilityPlayerDialog(Dialog): label = ttk.Label(self.top, text=file_name) label.grid(sticky="ew", pady=PAD) - frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) - frame.columnconfigure(0, weight=1) - self.progressbar = ttk.Progressbar(frame, mode="indeterminate") - self.progressbar.grid(row=0, column=0, sticky="ew", padx=PAD) - self.progressbar.start() - label = ttk.Label(frame, text="time") - label.grid(row=0, column=1) + self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") + self.progressbar.grid(sticky="ew", pady=PAD) frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PAD) @@ -58,6 +52,7 @@ class MobilityPlayerDialog(Dialog): self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) + self.stop_button.state(["pressed"]) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( @@ -74,29 +69,38 @@ class MobilityPlayerDialog(Dialog): self.pause_button.state(["!pressed"]) self.stop_button.state(["!pressed"]) - def click_play(self): + def set_play(self): self.clear_buttons() self.play_button.state(["pressed"]) + self.progressbar.start() + + def set_pause(self): + self.clear_buttons() + self.pause_button.state(["pressed"]) + self.progressbar.stop() + + def set_stop(self): + self.clear_buttons() + self.stop_button.state(["pressed"]) + self.progressbar.stop() + + def click_play(self): + self.set_play() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.START ) - self.progressbar.start() def click_pause(self): - self.clear_buttons() - self.pause_button.state(["pressed"]) + self.set_pause() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.PAUSE ) - self.progressbar.stop() def click_stop(self): - self.clear_buttons() - self.stop_button.state(["pressed"]) + self.set_stop() session_id = self.app.core.session_id self.app.core.client.mobility_action( session_id, self.node.id, core_pb2.MobilityAction.STOP ) - self.progressbar.stop() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index d9508681..e56c55ec 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -51,10 +51,10 @@ class Menubar(tk.Menu): label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) self.app.bind_all("", self.menuaction.file_open_xml) - menu.add_command(label="Reload", underline=0, state=tk.DISABLED) menu.add_command( label="Save", accelerator="Ctrl+S", command=self.menuaction.file_save_as_xml ) + menu.add_command(label="Reload", underline=0, state=tk.DISABLED) self.app.bind_all("", self.menuaction.file_save_as_xml) menu.add_separator() menu.add_command(label="Export Python script...", state=tk.DISABLED) From 554028ad5ce56ab368b6a94b5663430f20dc3993 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:11:11 -0800 Subject: [PATCH 182/305] display mobility player context for mobility configured nodes during runtime --- coretk/coretk/graph.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c03d7b48..d45cce9b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -77,9 +77,16 @@ class CanvasGraph(tk.Canvas): context.add_command( label="WLAN Config", command=canvas_node.show_wlan_config ) - context.add_command( - label="Mobility Config", command=canvas_node.show_mobility_config - ) + if self.master.core.is_runtime(): + if canvas_node.core_node.id in self.master.core.mobility_players: + context.add_command( + label="Mobility Player", + command=canvas_node.show_mobility_player, + ) + else: + context.add_command( + label="Mobility Config", command=canvas_node.show_mobility_config + ) if node.type == NodeType.EMANE: context.add_command( label="EMANE Config", command=canvas_node.show_emane_config @@ -688,6 +695,9 @@ class CanvasNode: dialog = MobilityConfigDialog(self.app, self.app, self) dialog.show() + def show_mobility_player(self): + self.canvas.context = None + def show_emane_config(self): self.canvas.context = None dialog = EmaneConfigDialog(self.app, self.app, self) From 5708dbd083378409abe5a00578c0f3a207d57bd4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:16:56 -0800 Subject: [PATCH 183/305] fixed issue when deleting node without a link tuple --- coretk/coretk/appconfig.py | 7 ++++--- coretk/coretk/{ => data}/backgrounds/sample1-bg.gif | Bin coretk/coretk/{ => data}/backgrounds/sample4-bg.jpg | Bin coretk/coretk/{ => data}/icons/OVS.gif | Bin coretk/coretk/{ => data}/icons/antenna.gif | Bin coretk/coretk/{ => data}/icons/core-icon.png | Bin coretk/coretk/{ => data}/icons/docker.png | Bin coretk/coretk/{ => data}/icons/document-new.gif | Bin .../coretk/{ => data}/icons/document-properties.gif | Bin coretk/coretk/{ => data}/icons/document-save.gif | Bin coretk/coretk/{ => data}/icons/edit-delete.gif | Bin coretk/coretk/{ => data}/icons/edit-node.png | Bin coretk/coretk/{ => data}/icons/emane.png | Bin coretk/coretk/{ => data}/icons/fileopen.gif | Bin coretk/coretk/{ => data}/icons/host.png | Bin coretk/coretk/{ => data}/icons/hub.png | Bin coretk/coretk/{ => data}/icons/lanswitch.png | Bin coretk/coretk/{ => data}/icons/link.png | Bin coretk/coretk/{ => data}/icons/lxc.png | Bin coretk/coretk/{ => data}/icons/marker.png | Bin coretk/coretk/{ => data}/icons/mdr.png | Bin coretk/coretk/{ => data}/icons/observe.gif | Bin coretk/coretk/{ => data}/icons/oval.png | Bin coretk/coretk/{ => data}/icons/pause.png | Bin coretk/coretk/{ => data}/icons/pc.png | Bin coretk/coretk/{ => data}/icons/plot.gif | Bin coretk/coretk/{ => data}/icons/prouter.png | Bin coretk/coretk/{ => data}/icons/rectangle.png | Bin coretk/coretk/{ => data}/icons/rj45.png | Bin coretk/coretk/{ => data}/icons/router.png | Bin coretk/coretk/{ => data}/icons/run.png | Bin coretk/coretk/{ => data}/icons/select.png | Bin coretk/coretk/{ => data}/icons/start.png | Bin coretk/coretk/{ => data}/icons/stop.png | Bin coretk/coretk/{ => data}/icons/text.png | Bin coretk/coretk/{ => data}/icons/tunnel.gif | Bin coretk/coretk/{ => data}/icons/twonode.png | Bin coretk/coretk/{ => data}/icons/wlan.png | Bin coretk/coretk/{ => data}/oldicons/docker.gif | Bin coretk/coretk/{ => data}/oldicons/emane.gif | Bin coretk/coretk/{ => data}/oldicons/host.gif | Bin coretk/coretk/{ => data}/oldicons/hub.gif | Bin coretk/coretk/{ => data}/oldicons/lanswitch.gif | Bin coretk/coretk/{ => data}/oldicons/link.gif | Bin coretk/coretk/{ => data}/oldicons/lxc.gif | Bin coretk/coretk/{ => data}/oldicons/marker.gif | Bin coretk/coretk/{ => data}/oldicons/mdr.gif | Bin coretk/coretk/{ => data}/oldicons/oval.gif | Bin coretk/coretk/{ => data}/oldicons/pc.gif | Bin coretk/coretk/{ => data}/oldicons/rectangle.gif | Bin coretk/coretk/{ => data}/oldicons/rj45.gif | Bin coretk/coretk/{ => data}/oldicons/router.gif | Bin coretk/coretk/{ => data}/oldicons/router_green.gif | Bin coretk/coretk/{ => data}/oldicons/run.gif | Bin coretk/coretk/{ => data}/oldicons/select.gif | Bin coretk/coretk/{ => data}/oldicons/start.gif | Bin coretk/coretk/{ => data}/oldicons/stop.gif | Bin coretk/coretk/{ => data}/oldicons/text.gif | Bin coretk/coretk/{ => data}/oldicons/twonode.gif | Bin coretk/coretk/{ => data}/oldicons/wlan.gif | Bin .../coretk/{ => data}/todelete/servicefileconfig.py | 0 .../coretk/{ => data}/todelete/servicenodeconfig.py | 0 coretk/coretk/{ => data}/xmls/sample1.xml | 0 coretk/coretk/nodedelete.py | 3 ++- 64 files changed, 6 insertions(+), 4 deletions(-) rename coretk/coretk/{ => data}/backgrounds/sample1-bg.gif (100%) rename coretk/coretk/{ => data}/backgrounds/sample4-bg.jpg (100%) rename coretk/coretk/{ => data}/icons/OVS.gif (100%) rename coretk/coretk/{ => data}/icons/antenna.gif (100%) rename coretk/coretk/{ => data}/icons/core-icon.png (100%) rename coretk/coretk/{ => data}/icons/docker.png (100%) rename coretk/coretk/{ => data}/icons/document-new.gif (100%) rename coretk/coretk/{ => data}/icons/document-properties.gif (100%) rename coretk/coretk/{ => data}/icons/document-save.gif (100%) rename coretk/coretk/{ => data}/icons/edit-delete.gif (100%) rename coretk/coretk/{ => data}/icons/edit-node.png (100%) rename coretk/coretk/{ => data}/icons/emane.png (100%) rename coretk/coretk/{ => data}/icons/fileopen.gif (100%) rename coretk/coretk/{ => data}/icons/host.png (100%) rename coretk/coretk/{ => data}/icons/hub.png (100%) rename coretk/coretk/{ => data}/icons/lanswitch.png (100%) rename coretk/coretk/{ => data}/icons/link.png (100%) rename coretk/coretk/{ => data}/icons/lxc.png (100%) rename coretk/coretk/{ => data}/icons/marker.png (100%) rename coretk/coretk/{ => data}/icons/mdr.png (100%) rename coretk/coretk/{ => data}/icons/observe.gif (100%) rename coretk/coretk/{ => data}/icons/oval.png (100%) rename coretk/coretk/{ => data}/icons/pause.png (100%) rename coretk/coretk/{ => data}/icons/pc.png (100%) rename coretk/coretk/{ => data}/icons/plot.gif (100%) rename coretk/coretk/{ => data}/icons/prouter.png (100%) rename coretk/coretk/{ => data}/icons/rectangle.png (100%) rename coretk/coretk/{ => data}/icons/rj45.png (100%) rename coretk/coretk/{ => data}/icons/router.png (100%) rename coretk/coretk/{ => data}/icons/run.png (100%) rename coretk/coretk/{ => data}/icons/select.png (100%) rename coretk/coretk/{ => data}/icons/start.png (100%) rename coretk/coretk/{ => data}/icons/stop.png (100%) rename coretk/coretk/{ => data}/icons/text.png (100%) rename coretk/coretk/{ => data}/icons/tunnel.gif (100%) rename coretk/coretk/{ => data}/icons/twonode.png (100%) rename coretk/coretk/{ => data}/icons/wlan.png (100%) rename coretk/coretk/{ => data}/oldicons/docker.gif (100%) rename coretk/coretk/{ => data}/oldicons/emane.gif (100%) rename coretk/coretk/{ => data}/oldicons/host.gif (100%) rename coretk/coretk/{ => data}/oldicons/hub.gif (100%) rename coretk/coretk/{ => data}/oldicons/lanswitch.gif (100%) rename coretk/coretk/{ => data}/oldicons/link.gif (100%) rename coretk/coretk/{ => data}/oldicons/lxc.gif (100%) rename coretk/coretk/{ => data}/oldicons/marker.gif (100%) rename coretk/coretk/{ => data}/oldicons/mdr.gif (100%) rename coretk/coretk/{ => data}/oldicons/oval.gif (100%) rename coretk/coretk/{ => data}/oldicons/pc.gif (100%) rename coretk/coretk/{ => data}/oldicons/rectangle.gif (100%) rename coretk/coretk/{ => data}/oldicons/rj45.gif (100%) rename coretk/coretk/{ => data}/oldicons/router.gif (100%) rename coretk/coretk/{ => data}/oldicons/router_green.gif (100%) rename coretk/coretk/{ => data}/oldicons/run.gif (100%) rename coretk/coretk/{ => data}/oldicons/select.gif (100%) rename coretk/coretk/{ => data}/oldicons/start.gif (100%) rename coretk/coretk/{ => data}/oldicons/stop.gif (100%) rename coretk/coretk/{ => data}/oldicons/text.gif (100%) rename coretk/coretk/{ => data}/oldicons/twonode.gif (100%) rename coretk/coretk/{ => data}/oldicons/wlan.gif (100%) rename coretk/coretk/{ => data}/todelete/servicefileconfig.py (100%) rename coretk/coretk/{ => data}/todelete/servicenodeconfig.py (100%) rename coretk/coretk/{ => data}/xmls/sample1.xml (100%) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 74d52553..a85e5991 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -18,9 +18,10 @@ XML_PATH = HOME_PATH.joinpath("xml") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths -LOCAL_ICONS_PATH = Path(__file__).parent.joinpath("icons").absolute() -LOCAL_BACKGROUND_PATH = Path(__file__).parent.joinpath("backgrounds").absolute() -LOCAL_XMLS_PATH = Path(__file__).parent.joinpath("xmls").absolute() +DATA_PATH = Path(__file__).parent.joinpath("data") +LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() +LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() +LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() # configuration data TERMINALS = [ diff --git a/coretk/coretk/backgrounds/sample1-bg.gif b/coretk/coretk/data/backgrounds/sample1-bg.gif similarity index 100% rename from coretk/coretk/backgrounds/sample1-bg.gif rename to coretk/coretk/data/backgrounds/sample1-bg.gif diff --git a/coretk/coretk/backgrounds/sample4-bg.jpg b/coretk/coretk/data/backgrounds/sample4-bg.jpg similarity index 100% rename from coretk/coretk/backgrounds/sample4-bg.jpg rename to coretk/coretk/data/backgrounds/sample4-bg.jpg diff --git a/coretk/coretk/icons/OVS.gif b/coretk/coretk/data/icons/OVS.gif similarity index 100% rename from coretk/coretk/icons/OVS.gif rename to coretk/coretk/data/icons/OVS.gif diff --git a/coretk/coretk/icons/antenna.gif b/coretk/coretk/data/icons/antenna.gif similarity index 100% rename from coretk/coretk/icons/antenna.gif rename to coretk/coretk/data/icons/antenna.gif diff --git a/coretk/coretk/icons/core-icon.png b/coretk/coretk/data/icons/core-icon.png similarity index 100% rename from coretk/coretk/icons/core-icon.png rename to coretk/coretk/data/icons/core-icon.png diff --git a/coretk/coretk/icons/docker.png b/coretk/coretk/data/icons/docker.png similarity index 100% rename from coretk/coretk/icons/docker.png rename to coretk/coretk/data/icons/docker.png diff --git a/coretk/coretk/icons/document-new.gif b/coretk/coretk/data/icons/document-new.gif similarity index 100% rename from coretk/coretk/icons/document-new.gif rename to coretk/coretk/data/icons/document-new.gif diff --git a/coretk/coretk/icons/document-properties.gif b/coretk/coretk/data/icons/document-properties.gif similarity index 100% rename from coretk/coretk/icons/document-properties.gif rename to coretk/coretk/data/icons/document-properties.gif diff --git a/coretk/coretk/icons/document-save.gif b/coretk/coretk/data/icons/document-save.gif similarity index 100% rename from coretk/coretk/icons/document-save.gif rename to coretk/coretk/data/icons/document-save.gif diff --git a/coretk/coretk/icons/edit-delete.gif b/coretk/coretk/data/icons/edit-delete.gif similarity index 100% rename from coretk/coretk/icons/edit-delete.gif rename to coretk/coretk/data/icons/edit-delete.gif diff --git a/coretk/coretk/icons/edit-node.png b/coretk/coretk/data/icons/edit-node.png similarity index 100% rename from coretk/coretk/icons/edit-node.png rename to coretk/coretk/data/icons/edit-node.png diff --git a/coretk/coretk/icons/emane.png b/coretk/coretk/data/icons/emane.png similarity index 100% rename from coretk/coretk/icons/emane.png rename to coretk/coretk/data/icons/emane.png diff --git a/coretk/coretk/icons/fileopen.gif b/coretk/coretk/data/icons/fileopen.gif similarity index 100% rename from coretk/coretk/icons/fileopen.gif rename to coretk/coretk/data/icons/fileopen.gif diff --git a/coretk/coretk/icons/host.png b/coretk/coretk/data/icons/host.png similarity index 100% rename from coretk/coretk/icons/host.png rename to coretk/coretk/data/icons/host.png diff --git a/coretk/coretk/icons/hub.png b/coretk/coretk/data/icons/hub.png similarity index 100% rename from coretk/coretk/icons/hub.png rename to coretk/coretk/data/icons/hub.png diff --git a/coretk/coretk/icons/lanswitch.png b/coretk/coretk/data/icons/lanswitch.png similarity index 100% rename from coretk/coretk/icons/lanswitch.png rename to coretk/coretk/data/icons/lanswitch.png diff --git a/coretk/coretk/icons/link.png b/coretk/coretk/data/icons/link.png similarity index 100% rename from coretk/coretk/icons/link.png rename to coretk/coretk/data/icons/link.png diff --git a/coretk/coretk/icons/lxc.png b/coretk/coretk/data/icons/lxc.png similarity index 100% rename from coretk/coretk/icons/lxc.png rename to coretk/coretk/data/icons/lxc.png diff --git a/coretk/coretk/icons/marker.png b/coretk/coretk/data/icons/marker.png similarity index 100% rename from coretk/coretk/icons/marker.png rename to coretk/coretk/data/icons/marker.png diff --git a/coretk/coretk/icons/mdr.png b/coretk/coretk/data/icons/mdr.png similarity index 100% rename from coretk/coretk/icons/mdr.png rename to coretk/coretk/data/icons/mdr.png diff --git a/coretk/coretk/icons/observe.gif b/coretk/coretk/data/icons/observe.gif similarity index 100% rename from coretk/coretk/icons/observe.gif rename to coretk/coretk/data/icons/observe.gif diff --git a/coretk/coretk/icons/oval.png b/coretk/coretk/data/icons/oval.png similarity index 100% rename from coretk/coretk/icons/oval.png rename to coretk/coretk/data/icons/oval.png diff --git a/coretk/coretk/icons/pause.png b/coretk/coretk/data/icons/pause.png similarity index 100% rename from coretk/coretk/icons/pause.png rename to coretk/coretk/data/icons/pause.png diff --git a/coretk/coretk/icons/pc.png b/coretk/coretk/data/icons/pc.png similarity index 100% rename from coretk/coretk/icons/pc.png rename to coretk/coretk/data/icons/pc.png diff --git a/coretk/coretk/icons/plot.gif b/coretk/coretk/data/icons/plot.gif similarity index 100% rename from coretk/coretk/icons/plot.gif rename to coretk/coretk/data/icons/plot.gif diff --git a/coretk/coretk/icons/prouter.png b/coretk/coretk/data/icons/prouter.png similarity index 100% rename from coretk/coretk/icons/prouter.png rename to coretk/coretk/data/icons/prouter.png diff --git a/coretk/coretk/icons/rectangle.png b/coretk/coretk/data/icons/rectangle.png similarity index 100% rename from coretk/coretk/icons/rectangle.png rename to coretk/coretk/data/icons/rectangle.png diff --git a/coretk/coretk/icons/rj45.png b/coretk/coretk/data/icons/rj45.png similarity index 100% rename from coretk/coretk/icons/rj45.png rename to coretk/coretk/data/icons/rj45.png diff --git a/coretk/coretk/icons/router.png b/coretk/coretk/data/icons/router.png similarity index 100% rename from coretk/coretk/icons/router.png rename to coretk/coretk/data/icons/router.png diff --git a/coretk/coretk/icons/run.png b/coretk/coretk/data/icons/run.png similarity index 100% rename from coretk/coretk/icons/run.png rename to coretk/coretk/data/icons/run.png diff --git a/coretk/coretk/icons/select.png b/coretk/coretk/data/icons/select.png similarity index 100% rename from coretk/coretk/icons/select.png rename to coretk/coretk/data/icons/select.png diff --git a/coretk/coretk/icons/start.png b/coretk/coretk/data/icons/start.png similarity index 100% rename from coretk/coretk/icons/start.png rename to coretk/coretk/data/icons/start.png diff --git a/coretk/coretk/icons/stop.png b/coretk/coretk/data/icons/stop.png similarity index 100% rename from coretk/coretk/icons/stop.png rename to coretk/coretk/data/icons/stop.png diff --git a/coretk/coretk/icons/text.png b/coretk/coretk/data/icons/text.png similarity index 100% rename from coretk/coretk/icons/text.png rename to coretk/coretk/data/icons/text.png diff --git a/coretk/coretk/icons/tunnel.gif b/coretk/coretk/data/icons/tunnel.gif similarity index 100% rename from coretk/coretk/icons/tunnel.gif rename to coretk/coretk/data/icons/tunnel.gif diff --git a/coretk/coretk/icons/twonode.png b/coretk/coretk/data/icons/twonode.png similarity index 100% rename from coretk/coretk/icons/twonode.png rename to coretk/coretk/data/icons/twonode.png diff --git a/coretk/coretk/icons/wlan.png b/coretk/coretk/data/icons/wlan.png similarity index 100% rename from coretk/coretk/icons/wlan.png rename to coretk/coretk/data/icons/wlan.png diff --git a/coretk/coretk/oldicons/docker.gif b/coretk/coretk/data/oldicons/docker.gif similarity index 100% rename from coretk/coretk/oldicons/docker.gif rename to coretk/coretk/data/oldicons/docker.gif diff --git a/coretk/coretk/oldicons/emane.gif b/coretk/coretk/data/oldicons/emane.gif similarity index 100% rename from coretk/coretk/oldicons/emane.gif rename to coretk/coretk/data/oldicons/emane.gif diff --git a/coretk/coretk/oldicons/host.gif b/coretk/coretk/data/oldicons/host.gif similarity index 100% rename from coretk/coretk/oldicons/host.gif rename to coretk/coretk/data/oldicons/host.gif diff --git a/coretk/coretk/oldicons/hub.gif b/coretk/coretk/data/oldicons/hub.gif similarity index 100% rename from coretk/coretk/oldicons/hub.gif rename to coretk/coretk/data/oldicons/hub.gif diff --git a/coretk/coretk/oldicons/lanswitch.gif b/coretk/coretk/data/oldicons/lanswitch.gif similarity index 100% rename from coretk/coretk/oldicons/lanswitch.gif rename to coretk/coretk/data/oldicons/lanswitch.gif diff --git a/coretk/coretk/oldicons/link.gif b/coretk/coretk/data/oldicons/link.gif similarity index 100% rename from coretk/coretk/oldicons/link.gif rename to coretk/coretk/data/oldicons/link.gif diff --git a/coretk/coretk/oldicons/lxc.gif b/coretk/coretk/data/oldicons/lxc.gif similarity index 100% rename from coretk/coretk/oldicons/lxc.gif rename to coretk/coretk/data/oldicons/lxc.gif diff --git a/coretk/coretk/oldicons/marker.gif b/coretk/coretk/data/oldicons/marker.gif similarity index 100% rename from coretk/coretk/oldicons/marker.gif rename to coretk/coretk/data/oldicons/marker.gif diff --git a/coretk/coretk/oldicons/mdr.gif b/coretk/coretk/data/oldicons/mdr.gif similarity index 100% rename from coretk/coretk/oldicons/mdr.gif rename to coretk/coretk/data/oldicons/mdr.gif diff --git a/coretk/coretk/oldicons/oval.gif b/coretk/coretk/data/oldicons/oval.gif similarity index 100% rename from coretk/coretk/oldicons/oval.gif rename to coretk/coretk/data/oldicons/oval.gif diff --git a/coretk/coretk/oldicons/pc.gif b/coretk/coretk/data/oldicons/pc.gif similarity index 100% rename from coretk/coretk/oldicons/pc.gif rename to coretk/coretk/data/oldicons/pc.gif diff --git a/coretk/coretk/oldicons/rectangle.gif b/coretk/coretk/data/oldicons/rectangle.gif similarity index 100% rename from coretk/coretk/oldicons/rectangle.gif rename to coretk/coretk/data/oldicons/rectangle.gif diff --git a/coretk/coretk/oldicons/rj45.gif b/coretk/coretk/data/oldicons/rj45.gif similarity index 100% rename from coretk/coretk/oldicons/rj45.gif rename to coretk/coretk/data/oldicons/rj45.gif diff --git a/coretk/coretk/oldicons/router.gif b/coretk/coretk/data/oldicons/router.gif similarity index 100% rename from coretk/coretk/oldicons/router.gif rename to coretk/coretk/data/oldicons/router.gif diff --git a/coretk/coretk/oldicons/router_green.gif b/coretk/coretk/data/oldicons/router_green.gif similarity index 100% rename from coretk/coretk/oldicons/router_green.gif rename to coretk/coretk/data/oldicons/router_green.gif diff --git a/coretk/coretk/oldicons/run.gif b/coretk/coretk/data/oldicons/run.gif similarity index 100% rename from coretk/coretk/oldicons/run.gif rename to coretk/coretk/data/oldicons/run.gif diff --git a/coretk/coretk/oldicons/select.gif b/coretk/coretk/data/oldicons/select.gif similarity index 100% rename from coretk/coretk/oldicons/select.gif rename to coretk/coretk/data/oldicons/select.gif diff --git a/coretk/coretk/oldicons/start.gif b/coretk/coretk/data/oldicons/start.gif similarity index 100% rename from coretk/coretk/oldicons/start.gif rename to coretk/coretk/data/oldicons/start.gif diff --git a/coretk/coretk/oldicons/stop.gif b/coretk/coretk/data/oldicons/stop.gif similarity index 100% rename from coretk/coretk/oldicons/stop.gif rename to coretk/coretk/data/oldicons/stop.gif diff --git a/coretk/coretk/oldicons/text.gif b/coretk/coretk/data/oldicons/text.gif similarity index 100% rename from coretk/coretk/oldicons/text.gif rename to coretk/coretk/data/oldicons/text.gif diff --git a/coretk/coretk/oldicons/twonode.gif b/coretk/coretk/data/oldicons/twonode.gif similarity index 100% rename from coretk/coretk/oldicons/twonode.gif rename to coretk/coretk/data/oldicons/twonode.gif diff --git a/coretk/coretk/oldicons/wlan.gif b/coretk/coretk/data/oldicons/wlan.gif similarity index 100% rename from coretk/coretk/oldicons/wlan.gif rename to coretk/coretk/data/oldicons/wlan.gif diff --git a/coretk/coretk/todelete/servicefileconfig.py b/coretk/coretk/data/todelete/servicefileconfig.py similarity index 100% rename from coretk/coretk/todelete/servicefileconfig.py rename to coretk/coretk/data/todelete/servicefileconfig.py diff --git a/coretk/coretk/todelete/servicenodeconfig.py b/coretk/coretk/data/todelete/servicenodeconfig.py similarity index 100% rename from coretk/coretk/todelete/servicenodeconfig.py rename to coretk/coretk/data/todelete/servicenodeconfig.py diff --git a/coretk/coretk/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml similarity index 100% rename from coretk/coretk/xmls/sample1.xml rename to coretk/coretk/data/xmls/sample1.xml diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 92844aac..f52731e4 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -75,7 +75,8 @@ class CanvasComponentManagement: neighbor = self.app.canvas_nodes[neighbor_id] if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: neighbor.antenna_draw.delete_antenna() - for link_tuple in node_to_wlink[canvas_node.core_node.id]: + + for link_tuple in node_to_wlink.get(canvas_node.core_node.id, []): nid_one, nid_two = link_tuple if link_tuple in self.canvas.wireless_draw.map: self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) From 354d227cb3a6badc24c741327ac8a084c6620e30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:20:00 -0800 Subject: [PATCH 184/305] removed old service config code --- .../coretk/data/todelete/servicefileconfig.py | 37 -------- .../coretk/data/todelete/servicenodeconfig.py | 85 ------------------- 2 files changed, 122 deletions(-) delete mode 100644 coretk/coretk/data/todelete/servicefileconfig.py delete mode 100644 coretk/coretk/data/todelete/servicenodeconfig.py diff --git a/coretk/coretk/data/todelete/servicefileconfig.py b/coretk/coretk/data/todelete/servicefileconfig.py deleted file mode 100644 index 06a134cd..00000000 --- a/coretk/coretk/data/todelete/servicefileconfig.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -service file configuration -""" - - -class ServiceFileConfig: - def __init__(self): - # dict(node_id:dict(service:dict(filename, data))) - self.configurations = {} - - # def set_service_configs(self, node_id, service_name, file_configs): - # """ - # store file configs - # - # :param int node_id: node id - # :param str service_name: service name - # :param dict(str, str) file_configs: map of service file to its data - # :return: nothing - # """ - # for key, value in file_configs.items(): - # self.configurations[node_id][service_name][key] = value - - def set_custom_service_file_config(self, node_id, service_name, file_name, data): - """ - store file config - - :param int node_id: node id - :param str service_name: service name - :param str file_name: file name - :param str data: data - :return: nothing - """ - if node_id not in self.configurations: - self.configurations[node_id] = {} - if service_name not in self.configurations[node_id]: - self.configurations[node_id][service_name] = {} - self.configurations[node_id][service_name][file_name] = data diff --git a/coretk/coretk/data/todelete/servicenodeconfig.py b/coretk/coretk/data/todelete/servicenodeconfig.py deleted file mode 100644 index 3473e1d8..00000000 --- a/coretk/coretk/data/todelete/servicenodeconfig.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -service node configuration -""" -import logging -from tkinter import messagebox - -import grpc - - -class ServiceNodeConfig: - def __init__(self, app): - self.app = app - # dict(node_id:dict(service:node_service_config_proto)) - # maps node to all of its service configuration - self.configurations = {} - # dict(node_id:set(str)) - # maps node to current configurations - self.current_services = {} - self.default_services = {} - - # todo rewrite, no need self.default services - def node_default_services_configuration(self, node_id, node_model): - """ - set the default configurations for the default services of a node - - :param coretk.graph.CanvasNode canvas_node: canvas node object - :return: nothing - """ - session_id = self.app.core.session_id - client = self.app.core.client - - if len(self.default_services) == 0: - response = client.get_service_defaults(session_id) - logging.info("session default services: %s", response) - for default in response.defaults: - self.default_services[default.node_type] = default.services - - self.configurations[node_id] = {} - - self.current_services[node_id] = set() - for default in self.default_services[node_model]: - response = client.get_node_service(session_id, node_id, default) - logging.info( - "servicenodeconfig.py get node service (%s), result: %s", - node_id, - response, - ) - self.configurations[node_id][default] = response.service - self.current_services[node_id].add(default) - - def node_new_service_configuration(self, node_id, service_name): - """ - store node's configuration if a new service is added from the GUI - - :param int node_id: node id - :param str service_name: service name - :return: nothing - """ - try: - config = self.app.core.get_node_service(node_id, service_name) - except grpc.RpcError: - messagebox.showerror("Service problem", "Service not found") - return False - if node_id not in self.configurations: - self.configurations[node_id] = {} - if node_id not in self.current_services: - self.current_services[node_id] = set() - if service_name not in self.configurations[node_id]: - self.configurations[node_id][service_name] = config - self.current_services[node_id].add(service_name) - return True - - def node_custom_service_configuration(self, node_id, service_name): - self.configurations[node_id][service_name] = self.app.core.get_node_service( - node_id, service_name - ) - - def node_service_custom_configuration( - self, node_id, service_name, startups, validates, shutdowns - ): - self.app.core.set_node_service( - node_id, service_name, startups, validates, shutdowns - ) - config = self.app.core.get_node_service(node_id, service_name) - self.configurations[node_id][service_name] = config From d1db5e4b4ec00f96c9a03d2038a4b27834a0b0f1 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:55:39 -0800 Subject: [PATCH 185/305] added some data for node events to have a source field to help distinguish what originates from the gui or not --- coretk/coretk/coreclient.py | 23 +++++++++++------------ coretk/coretk/graph.py | 6 +++--- daemon/core/api/grpc/client.py | 9 +++++++-- daemon/core/api/grpc/server.py | 7 +++++-- daemon/core/emulator/data.py | 1 + daemon/core/nodes/base.py | 4 +++- daemon/proto/core/api/grpc/core.proto | 2 ++ 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index e6dfb77e..1671a0fe 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -141,15 +141,19 @@ class CoreClient: else: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): - node_event = event.node_event - node_id = node_event.node.id - x = node_event.node.position.x - y = node_event.node.position.y - canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y) + self.handle_node_event(event.node_event) else: logging.info("unhandled event: %s", event) + def handle_node_event(self, event): + if event.source == "gui": + return + node_id = event.node.id + x = event.node.position.x + y = event.node.position.y + canvas_node = self.canvas_nodes[node_id] + canvas_node.move(x, y) + def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs for i in interface_throughputs: @@ -164,9 +168,6 @@ class CoreClient: ) def join_session(self, session_id, query_location=True): - # self.master.config(cursor="watch") - # self.master.update() - # update session and title self.session_id = session_id self.master.title(f"CORE Session({self.session_id})") @@ -253,7 +254,6 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() - # self.master.config(cursor="") self.app.statusbar.progress_bar.stop() def is_runtime(self): @@ -321,8 +321,7 @@ class CoreClient: def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - response = self.client.edit_node(self.session_id, node_id, position) - logging.info("updated node id %s: %s", node_id, response) + self.client.edit_node(self.session_id, node_id, position, source="gui") def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d45cce9b..7bfe7ed3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -600,6 +600,9 @@ class CanvasNode: y_offset = y - old_y self.core_node.position.x = x self.core_node.position.y = y + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -608,9 +611,6 @@ class CanvasNode: self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) edge.link_info.recalculate_info() self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) - self.canvas.move(self.id, x_offset, y_offset) - self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ceec0448..05380b79 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -428,7 +428,7 @@ class CoreGrpcClient: request = core_pb2.GetNodeRequest(session_id=session_id, node_id=node_id) return self.stub.GetNode(request) - def edit_node(self, session_id, node_id, position, icon=None): + def edit_node(self, session_id, node_id, position, icon=None, source=None): """ Edit a node, currently only changes position. @@ -436,12 +436,17 @@ class CoreGrpcClient: :param int node_id: node id :param core_pb2.Position position: position to set node to :param str icon: path to icon for gui to use for node + :param str source: application source editing node :return: response with result of success or failure :rtype: core_pb2.EditNodeResponse :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.EditNodeRequest( - session_id=session_id, node_id=node_id, position=position, icon=icon + session_id=session_id, + node_id=node_id, + position=position, + icon=icon, + source=source, ) return self.stub.EditNode(request) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index c0fe1d94..a914d617 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -504,7 +504,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): position=position, services=services, ) - return core_pb2.NodeEvent(node=node_proto) + return core_pb2.NodeEvent(node=node_proto, source=event.source) def _handle_link_event(self, event): """ @@ -800,7 +800,10 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): result = True try: session.edit_node(node.id, options) - node_data = node.data(0) + source = None + if request.source: + source = request.source + node_data = node.data(0, source=source) session.broadcast_node(node_data) except CoreError: result = False diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index ba0dd457..0ed1fa67 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -76,6 +76,7 @@ NodeData = collections.namedtuple( "altitude", "icon", "opaque", + "source", ], ) NodeData.__new__.__defaults__ = (None,) * len(NodeData._fields) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 72fc0fe1..a663741e 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -176,7 +176,7 @@ class NodeBase: self.ifindex += 1 return ifindex - def data(self, message_type, lat=None, lon=None, alt=None): + def data(self, message_type, lat=None, lon=None, alt=None, source=None): """ Build a data object for this node. @@ -184,6 +184,7 @@ class NodeBase: :param str lat: latitude :param str lon: longitude :param str alt: altitude + :param str source: source of node data :return: node data object :rtype: core.emulator.data.NodeData """ @@ -217,6 +218,7 @@ class NodeBase: model=model, server=server, services=services, + source=source, ) return node_data diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ac7cc2ed..57bbf3f4 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -298,6 +298,7 @@ message Event { message NodeEvent { Node node = 1; + string source = 2; } message LinkEvent { @@ -378,6 +379,7 @@ message EditNodeRequest { int32 node_id = 2; Position position = 3; string icon = 4; + string source = 5; } message EditNodeResponse { From f3ca5682ac9e4700d50f65db57296aec2c326974 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:15:04 -0800 Subject: [PATCH 186/305] start on shape drawing --- coretk/coretk/graph.py | 13 ++++++++++++- coretk/coretk/shape.py | 15 +++++++++++++++ coretk/coretk/toolbar.py | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/shape.py diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 518245d3..71774826 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,6 +17,7 @@ from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils +from coretk.shape import Shape from coretk.wirelessconnection import WirelessConnection NODE_TEXT_OFFSET = 5 @@ -27,7 +28,8 @@ class GraphMode(enum.Enum): EDGE = 1 PICKNODE = 2 NODE = 3 - OTHER = 4 + ANNOTATION = 4 + OTHER = 5 class ScaleOption(enum.Enum): @@ -38,12 +40,18 @@ class ScaleOption(enum.Enum): TILED = 4 +class ShapeType(enum.Enum): + OVAL = 0 + RECTANGLE = 1 + + class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: cnf = {} kwargs["highlightthickness"] = 0 super().__init__(master, cnf, **kwargs) + self.app = master self.mode = GraphMode.SELECT self.selected = None self.node_draw = None @@ -317,6 +325,9 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + if self.mode == GraphMode.ANNOTATION: + shape = Shape(self.app, self, event.x, event.y) + print(shape) def click_motion(self, event): """ diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py new file mode 100644 index 00000000..29c4a4c4 --- /dev/null +++ b/coretk/coretk/shape.py @@ -0,0 +1,15 @@ +""" +class for shapes +""" +# from coretk.images import ImageEnum, Images + + +class Shape: + def __init__(self, app, canvas, topleft_x, topleft_y): + self.app = app + self.canvas = canvas + self.x0 = topleft_x + self.y0 = topleft_y + self.id = self.canvas.create_oval( + topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 + ) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index d1830084..c68c8f24 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -367,6 +367,7 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image + self.app.canvas.mode = GraphMode.ANNOTATION def click_run_button(self): logging.debug("Click on RUN button") From 804b95d486de3ac21e218a965da68c34ff826703 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:23:45 -0800 Subject: [PATCH 187/305] consolidated node move logic to one function, used by both node events and node drag --- coretk/coretk/graph.py | 36 ++++++------------------------------ coretk/coretk/nodedelete.py | 4 +++- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7bfe7ed3..274073df 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -598,17 +598,18 @@ class CanvasNode: old_y = self.core_node.position.y x_offset = x - old_x y_offset = y - old_y - self.core_node.position.x = x - self.core_node.position.y = y + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.canvas.canvas_management.node_drag(self, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: - self.canvas.coords(edge.id, x_offset, y_offset, x2, y2) + self.canvas.coords(edge.id, x, y, x2, y2) else: - self.canvas.coords(edge.id, x1, y1, x_offset, y_offset) + self.canvas.coords(edge.id, x1, y1, x, y) edge.link_info.recalculate_info() self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) @@ -650,32 +651,7 @@ class CanvasNode: if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: 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) - self.antenna_draw.update_antennas_position(offset_x, offset_y) - self.canvas.canvas_management.node_drag(self, offset_x, offset_y) - - new_x, new_y = self.canvas.coords(self.id) - - if self.canvas.core.is_runtime(): - self.canvas.core.edit_node(self.core_node.id, int(new_x), int(new_y)) - - 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) - edge.link_info.recalculate_info() - - self.canvas.helper.update_wlan_connection( - old_x, old_y, new_x, new_y, self.wlans - ) + self.move(x, y) def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index f52731e4..341f95ef 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -35,7 +35,9 @@ class CanvasComponentManagement: self.selected[canvas_node.id] = bbox_id def node_drag(self, canvas_node, offset_x, offset_y): - self.canvas.move(self.selected[canvas_node.id], offset_x, offset_y) + select_id = self.selected.get(canvas_node.id) + if select_id is not None: + self.canvas.move(select_id, offset_x, offset_y) def delete_current_bbox(self): for bbid in self.selected.values(): From 2fc8782360cbc50b97e3548a169e5fd63991d8e8 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 13:58:34 -0800 Subject: [PATCH 188/305] shape --- coretk/coretk/graph.py | 8 ++++---- coretk/coretk/shape.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 71774826..eb18869b 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -366,9 +366,9 @@ class CanvasGraph(tk.Canvas): self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - plot_id = self.find_all()[0] - logging.info("add node event: %s - %s", plot_id, self.selected) - if self.selected == plot_id: + canvas_id = self.find_all()[0] + logging.info("add node event: %s - %s", canvas_id, self.selected) + if self.selected == canvas_id: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -651,7 +651,7 @@ class CanvasNode: self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: + if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) moving_x, moving_y = self.moving diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 29c4a4c4..7b9df97b 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -10,6 +10,7 @@ class Shape: self.canvas = canvas self.x0 = topleft_x self.y0 = topleft_y + # imageenum = self.app.toolbar self.id = self.canvas.create_oval( topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 ) From 3c7bf57b5cf736c1c45e7ea921a9cd33a0ca0c53 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 14:25:29 -0800 Subject: [PATCH 189/305] simplified select logic to check against known nodes and modified get_selected to avoid returning the canvas id --- coretk/coretk/graph.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 274073df..d9b9e1e3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -238,19 +238,15 @@ class CanvasGraph(tk.Canvas): :return: the item that the mouse point to """ 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: + if _id in self.nodes: selected = _id break - if selected is None: - selected = _id - return selected def click_release(self, event): @@ -362,9 +358,7 @@ class CanvasGraph(tk.Canvas): self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - plot_id = self.find_all()[0] - logging.info("add node event: %s - %s", plot_id, self.selected) - if self.selected == plot_id: + if self.selected is None: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -648,7 +642,7 @@ class CanvasNode: self.moving = None def motion(self, event): - if self.canvas.mode == GraphMode.EDGE or self.canvas.mode == GraphMode.NODE: + if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) self.move(x, y) From 693d7beeb77bef8c569c350dc497c69daa2434ae Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 15:40:54 -0800 Subject: [PATCH 190/305] updated remove antenna logic to simplify, updated wireless edges to be objects --- coretk/coretk/coreclient.py | 16 +++++++-- coretk/coretk/graph.py | 51 +++++++++++++++++++++++---- coretk/coretk/nodedelete.py | 53 ++++++----------------------- coretk/coretk/wirelessconnection.py | 51 --------------------------- 4 files changed, 69 insertions(+), 102 deletions(-) delete mode 100644 coretk/coretk/wirelessconnection.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1671a0fe..fec00a73 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -121,13 +121,12 @@ class CoreClient: def handle_events(self, event): if event.HasField("link_event"): logging.info("link event: %s", event) - self.app.canvas.wireless_draw.handle_link_event(event.link_event) + self.handle_link_event(event.link_event) elif event.HasField("session_event"): logging.info("session event: %s", event) session_event = event.session_event if session_event.event <= core_pb2.SessionState.SHUTDOWN: self.state = event.session_event.event - # mobility start elif session_event.event in {7, 8, 9}: node_id = session_event.node_id dialog = self.mobility_players.get(node_id) @@ -145,6 +144,19 @@ class CoreClient: else: logging.info("unhandled event: %s", event) + def handle_link_event(self, event): + node_one_id = event.link.node_one_id + node_two_id = event.link.node_two_id + canvas_node_one = self.canvas_nodes[node_one_id] + canvas_node_two = self.canvas_nodes[node_two_id] + + if event.message_type == core_pb2.MessageType.ADD: + self.app.canvas.add_wireless_edge(canvas_node_one, canvas_node_two) + elif event.message_type == core_pb2.MessageType.DELETE: + self.app.canvas.delete_wireless_edge(canvas_node_one, canvas_node_two) + else: + logging.warning("unknown link event: %s", event.message_type) + def handle_node_event(self, event): if event.source == "gui": return diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index d9b9e1e3..6c38b83c 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -17,7 +17,6 @@ from coretk.images import Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils -from coretk.wirelessconnection import WirelessConnection NODE_TEXT_OFFSET = 5 @@ -50,6 +49,7 @@ class CanvasGraph(tk.Canvas): self.context = None self.nodes = {} self.edges = {} + self.wireless_edges = {} self.drawing_edge = None self.grid = None self.canvas_management = CanvasComponentManagement(self, core) @@ -58,7 +58,6 @@ class CanvasGraph(tk.Canvas): self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) - self.wireless_draw = WirelessConnection(self, core) # background related self.wallpaper_id = None @@ -120,8 +119,8 @@ class CanvasGraph(tk.Canvas): self.selected = None self.nodes.clear() self.edges.clear() + self.wireless_edges.clear() self.drawing_edge = None - self.wireless_draw.map.clear() self.draw_session(session) def setup_bindings(self): @@ -162,6 +161,25 @@ class CanvasGraph(tk.Canvas): self.tag_lower("gridline") self.tag_lower(self.grid) + def add_wireless_edge(self, src, dst): + token = tuple(sorted((src.id, dst.id))) + x1, y1 = self.coords(src.id) + x2, y2 = self.coords(dst.id) + position = (x1, y1, x2, y2) + edge = CanvasWirelessEdge(token, position, src.id, dst.id, self) + self.wireless_edges[token] = edge + src.wireless_edges.add(edge) + dst.wireless_edges.add(edge) + self.tag_raise(src.id) + self.tag_raise(dst.id) + + def delete_wireless_edge(self, src, dst): + token = tuple(sorted((src.id, dst.id))) + edge = self.wireless_edges.pop(token) + edge.delete() + src.wireless_edges.remove(edge) + dst.wireless_edges.remove(edge) + def draw_session(self, session): """ Draw existing session. @@ -187,7 +205,7 @@ class CanvasGraph(tk.Canvas): canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node if link.type == core_pb2.LinkType.WIRELESS: - self.wireless_draw.add_connection(link.node_one_id, link.node_two_id) + self.add_wireless_edge(canvas_node_one, canvas_node_two) else: is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) @@ -492,6 +510,20 @@ class CanvasGraph(tk.Canvas): self.itemconfig("gridline", state=tk.HIDDEN) +class CanvasWirelessEdge: + def __init__(self, token, position, src, dst, canvas): + self.token = token + self.src = src + self.dst = dst + self.canvas = canvas + self.id = self.canvas.create_line( + *position, tags="wireless", width=1.5, fill="#009933" + ) + + def delete(self): + self.canvas.delete(self.id) + + class CanvasEdge: """ Canvas edge class @@ -580,7 +612,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.on_leave) self.edges = set() self.interfaces = [] - self.wlans = [] + self.wireless_edges = set() self.moving = None def redraw(self): @@ -605,7 +637,14 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) edge.link_info.recalculate_info() - self.canvas.helper.update_wlan_connection(old_x, old_y, x, y, self.wlans) + for edge in self.wireless_edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + if self.app.core.is_runtime(): + self.app.core.edit_node(self.core_node.id, int(x), int(y)) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index 341f95ef..a4de5f0d 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -1,7 +1,7 @@ """ manage deletion """ -from core.api.grpc import core_pb2 +from coretk.nodeutils import NodeUtils class CanvasComponentManagement: @@ -48,48 +48,6 @@ class CanvasComponentManagement: edges = set() nodes = [] - node_to_wlink = {} - for link_tuple in self.canvas.wireless_draw.map: - nid_one, nid_two = link_tuple - if nid_one not in node_to_wlink: - node_to_wlink[nid_one] = [] - if nid_two not in node_to_wlink: - node_to_wlink[nid_two] = [] - node_to_wlink[nid_one].append(link_tuple) - node_to_wlink[nid_two].append(link_tuple) - - # delete antennas and wireless links - for cnid in self.selected: - canvas_node = self.canvas.nodes[cnid] - if canvas_node.core_node.type != core_pb2.NodeType.WIRELESS_LAN: - canvas_node.antenna_draw.delete_antennas() - else: - for e in canvas_node.edges: - link_proto = self.app.links[e.token] - node_one_id, node_two_id = ( - link_proto.node_one_id, - link_proto.node_two_id, - ) - if node_one_id == canvas_node.core_node.id: - neighbor_id = node_two_id - else: - neighbor_id = node_one_id - neighbor = self.app.canvas_nodes[neighbor_id] - if neighbor.core_node.type != core_pb2.NodeType.WIRELESS_LAN: - neighbor.antenna_draw.delete_antenna() - - for link_tuple in node_to_wlink.get(canvas_node.core_node.id, []): - nid_one, nid_two = link_tuple - if link_tuple in self.canvas.wireless_draw.map: - self.canvas.delete(self.canvas.wireless_draw.map[link_tuple]) - link_cid = self.canvas.wireless_draw.map.pop(link_tuple, None) - canvas_node_one = self.app.canvas_nodes[nid_one] - canvas_node_two = self.app.canvas_nodes[nid_two] - if link_cid in canvas_node_one.wlans: - canvas_node_one.wlans.remove(link_cid) - if link_cid in canvas_node_two.wlans: - canvas_node_two.wlans.remove(link_cid) - for node_id in list(self.selected): bbox_id = self.selected[node_id] canvas_node = self.canvas.nodes.pop(node_id) @@ -97,6 +55,13 @@ class CanvasComponentManagement: self.canvas.delete(node_id) self.canvas.delete(bbox_id) self.canvas.delete(canvas_node.text_id) + + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) + if is_wireless: + canvas_node.antenna_draw.delete_antennas() + + # delete related edges for edge in canvas_node.edges: if edge in edges: continue @@ -116,5 +81,7 @@ class CanvasComponentManagement: other_node.interfaces.remove(other_interface) except ValueError: pass + if is_wireless: + other_node.antenna_draw.delete_antenna() self.selected.clear() return nodes diff --git a/coretk/coretk/wirelessconnection.py b/coretk/coretk/wirelessconnection.py deleted file mode 100644 index dd51723b..00000000 --- a/coretk/coretk/wirelessconnection.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Wireless connection handler -""" -from core.api.grpc import core_pb2 - - -class WirelessConnection: - def __init__(self, canvas, core): - self.canvas = canvas - self.core = core - # map a (node_one_id, node_two_id) to a wlan canvas id - self.map = {} - - def add_connection(self, node_one_id, node_two_id): - canvas_node_one = self.core.canvas_nodes[node_one_id] - canvas_node_two = self.core.canvas_nodes[node_two_id] - key = tuple(sorted((node_one_id, node_two_id))) - if key not in self.map: - x1, y1 = self.canvas.coords(canvas_node_one.id) - x2, y2 = self.canvas.coords(canvas_node_two.id) - wlan_canvas_id = self.canvas.create_line( - x1, y1, x2, y2, fill="#009933", tags="wireless", width=1.5 - ) - self.map[key] = wlan_canvas_id - canvas_node_one.wlans.append(wlan_canvas_id) - canvas_node_two.wlans.append(wlan_canvas_id) - else: - print("in map") - self.canvas.itemconfig(self.map[key], state="normal") - - def delete_connection(self, node_one_id, node_two_id): - canvas_node_one = self.core.canvas_nodes[node_one_id] - canvas_node_two = self.core.canvas_nodes[node_two_id] - key = tuple(sorted((node_one_id, node_two_id))) - wlan_canvas_id = self.map[key] - canvas_node_one.wlans.remove(wlan_canvas_id) - canvas_node_two.wlans.remove(wlan_canvas_id) - self.canvas.delete(wlan_canvas_id) - self.map.pop(key, None) - - def handle_link_event(self, link_event): - if link_event.message_type == core_pb2.MessageType.ADD: - self.add_connection( - link_event.link.node_one_id, link_event.link.node_two_id - ) - self.canvas.tag_raise("node") - - if link_event.message_type == core_pb2.MessageType.DELETE: - self.delete_connection( - link_event.link.node_one_id, link_event.link.node_two_id - ) From b30b8ab83de6d4bd494fbcd21e10a0b93592be6a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:14:14 -0800 Subject: [PATCH 191/305] updated status bar text to be centered --- coretk/coretk/status.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/status.py b/coretk/coretk/status.py index ea31d6fc..c4263e85 100644 --- a/coretk/coretk/status.py +++ b/coretk/coretk/status.py @@ -28,16 +28,20 @@ class StatusBar(ttk.Frame): self.progress_bar = ttk.Progressbar( self, orient="horizontal", mode="indeterminate" ) - self.progress_bar.grid(row=0, column=0, sticky="nsew") - self.status = ttk.Label(self, textvariable=self.statusvar) + self.progress_bar.grid(row=0, column=0, sticky="ew") + + self.status = ttk.Label(self, textvariable=self.statusvar, anchor=tk.CENTER) self.statusvar.set("status") - self.status.grid(row=0, column=1, sticky="nsew") - self.zoom = ttk.Label(self, text="zoom") - self.zoom.grid(row=0, column=2) - self.cpu_usage = ttk.Label(self, text="cpu usage") - self.cpu_usage.grid(row=0, column=3) - self.emulation_light = ttk.Label(self, text="emulation light") - self.emulation_light.grid(row=0, column=4) + self.status.grid(row=0, column=1, sticky="ew") + + self.zoom = ttk.Label(self, text="zoom", anchor=tk.CENTER) + self.zoom.grid(row=0, column=2, sticky="ew") + + self.cpu_usage = ttk.Label(self, text="cpu usage", anchor=tk.CENTER) + self.cpu_usage.grid(row=0, column=3, sticky="ew") + + self.emulation_light = ttk.Label(self, text="emulation light", anchor=tk.CENTER) + self.emulation_light.grid(row=0, column=4, sticky="ew") def start_session_callback(self, process_time): num_nodes = len(self.app.core.canvas_nodes) From 6a8e0c8360a523faa2904eca8ba26deffa5cffef Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:27:53 -0800 Subject: [PATCH 192/305] fixed app quit when grpc fails, fixed quitting when not stopping the running session --- coretk/coretk/menuaction.py | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 5b1cec9e..d8dc89cb 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -8,6 +8,8 @@ import time import webbrowser from tkinter import filedialog, messagebox +import grpc + from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH from coretk.dialogs.canvasbackground import CanvasBackgroundDialog @@ -30,6 +32,7 @@ class MenuAction: self.app = app def cleanup_old_session(self, quitapp=False): + logging.info("cleaning up old session") start = time.time() self.app.core.stop_session() self.app.core.delete_session() @@ -47,27 +50,31 @@ class MenuAction: logging.info( "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) - state = self.app.core.get_session_state() + try: + state = self.app.core.get_session_state() - if ( - state == core_pb2.SessionState.SHUTDOWN - or state == core_pb2.SessionState.DEFINITION - ): - self.app.core.delete_session() - if quitapp: - self.app.quit() - else: - msgbox = messagebox.askyesnocancel("stop", "Stop the running session?") - if msgbox or msgbox is False: - if msgbox: + if ( + state == core_pb2.SessionState.SHUTDOWN + or state == core_pb2.SessionState.DEFINITION + ): + self.app.core.delete_session() + if quitapp: + self.app.quit() + else: + result = messagebox.askyesnocancel("stop", "Stop the running session?") + if result: self.app.statusbar.progress_bar.start(5) thread = threading.Thread( target=self.cleanup_old_session, args=([quitapp]) ) + thread.daemon = True thread.start() - - # self.app.core.stop_session() - # self.app.core.delete_session() + elif quitapp: + self.app.quit() + except grpc.RpcError: + logging.error("error getting session state") + if quitapp: + self.app.quit() def on_quit(self, event=None): """ @@ -76,7 +83,6 @@ class MenuAction: :return: nothing """ self.prompt_save_running_session(quitapp=True) - # self.app.quit() def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") From 5c04225cc4255b127aa849f7c19547bfbadbbd42 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:39:48 -0800 Subject: [PATCH 193/305] working on shape --- coretk/coretk/graph.py | 42 ++++++++++++++++++++---------- coretk/coretk/shape.py | 55 +++++++++++++++++++++++++++++++++++----- coretk/coretk/toolbar.py | 5 ++-- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 1faaff76..c80834da 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -13,7 +13,7 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager -from coretk.images import Images +from coretk.images import ImageEnum, Images from coretk.linkinfo import LinkInfo, Throughput from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils @@ -53,11 +53,13 @@ class CanvasGraph(tk.Canvas): super().__init__(master, cnf, **kwargs) self.app = master self.mode = GraphMode.SELECT + self.annotation_type = None self.selected = None self.node_draw = None self.context = None self.nodes = {} self.edges = {} + self.shapes = {} self.drawing_edge = None self.grid = None self.canvas_management = CanvasComponentManagement(self, core) @@ -272,16 +274,23 @@ class CanvasGraph(tk.Canvas): self.context.unpost() self.context = None else: - self.focus_set() - self.selected = self.get_selected(event) - logging.debug(f"click release selected({self.selected}) mode({self.mode})") - 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) - elif self.mode == GraphMode.PICKNODE: - self.mode = GraphMode.NODE + if self.mode == GraphMode.ANNOTATION: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + self.shapes[self.selected].shape_complete(x, y) + else: + self.focus_set() + self.selected = self.get_selected(event) + logging.debug( + f"click release selected({self.selected}) mode({self.mode})" + ) + 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) + elif self.mode == GraphMode.PICKNODE: + self.mode = GraphMode.NODE def handle_edge_release(self, event): edge = self.drawing_edge @@ -333,8 +342,11 @@ class CanvasGraph(tk.Canvas): x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION: - shape = Shape(self.app, self, event.x, event.y) - print(shape) + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape def click_motion(self, event): """ @@ -347,6 +359,10 @@ class CanvasGraph(tk.Canvas): x2, y2 = self.canvas_xy(event) x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) + if self.mode == GraphMode.ANNOTATION: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + self.shapes[self.selected].shape_motion(x, y) def click_context(self, event): logging.info("context event: %s", self.context) diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 7b9df97b..76e76972 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -1,16 +1,57 @@ """ class for shapes """ -# from coretk.images import ImageEnum, Images +import logging + +from coretk.images import ImageEnum + +ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] class Shape: - def __init__(self, app, canvas, topleft_x, topleft_y): + def __init__(self, app, canvas, top_x, top_y): self.app = app self.canvas = canvas - self.x0 = topleft_x - self.y0 = topleft_y - # imageenum = self.app.toolbar - self.id = self.canvas.create_oval( - topleft_x, topleft_y, topleft_x + 30, topleft_y + 30 + self.x0 = top_x + self.y0 = top_y + self.cursor_x = None + self.cursor_y = None + annotation_type = self.canvas.annotation_type + if annotation_type == ImageEnum.OVAL: + self.id = canvas.create_oval( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + elif annotation_type == ImageEnum.RECTANGLE: + self.id = canvas.create_rectangle( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + self.canvas.tag_bind(self.id, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + + def shape_motion(self, x1, y1): + self.canvas.coords(self.id, self.x0, self.y0, x1, y1) + + def shape_complete(self, x, y): + self.canvas.itemconfig(self.id, width=0, fill="#ccccff") + for component in ABOVE_COMPONENT: + self.canvas.tag_raise(component) + + def click_press(self, event): + logging.debug("Click on shape %s", self.id) + self.cursor_x = event.x + self.cursor_y = event.y + + def click_release(self, event): + logging.debug("Click release on shape %s", self.id) + + def motion(self, event): + logging.debug("motion on shape %s", self.id) + delta_x = event.x - self.cursor_x + delta_y = event.y - self.cursor_y + x0, y0, x1, y1 = self.canvas.bbox(self.id) + self.canvas.coords( + self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) + self.cursor_x = event.x + self.cursor_y = event.y diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c68c8f24..fb0560dc 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -300,7 +300,7 @@ class Toolbar(ttk.Frame): image = icon(image_enum) self.create_picker_button( image, - partial(self.update_annotation, image), + partial(self.update_annotation, image, image_enum), self.annotation_picker, tooltip, ) @@ -362,12 +362,13 @@ class Toolbar(ttk.Frame): self.design_frame.tkraise() - def update_annotation(self, image): + def update_annotation(self, image, image_enum): logging.info("clicked annotation: ") self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION + self.app.canvas.annotation_type = image_enum def click_run_button(self): logging.debug("Click on RUN button") From ff473b97489c00615a9dc4c3b4c35decd24899fd Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 2 Dec 2019 16:05:10 -0800 Subject: [PATCH 194/305] shape dialog --- coretk/coretk/dialogs/shapemod.py | 126 ++++++++++++++++++++++++++++++ coretk/coretk/graph.py | 49 ++++++++++-- coretk/coretk/nodedelete.py | 76 ++++++++++-------- coretk/coretk/shape.py | 13 ++- 4 files changed, 215 insertions(+), 49 deletions(-) create mode 100644 coretk/coretk/dialogs/shapemod.py diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py new file mode 100644 index 00000000..b76fc0d1 --- /dev/null +++ b/coretk/coretk/dialogs/shapemod.py @@ -0,0 +1,126 @@ +""" +shape input dialog +""" +import tkinter as tk +from tkinter import colorchooser, font, ttk + +from coretk.dialogs.dialog import Dialog + +FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] +BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +class ShapeDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Add a new shape", modal=True) + self.shape_text = tk.StringVar(value="") + self.font = tk.StringVar(value="Arial") + self.font_size = tk.IntVar(value=12) + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "black" + self.border_width = tk.IntVar(value=0) + + self.fill = None + self.border = None + + self.top.columnconfigure(0, weight=1) + self.draw() + + def draw(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Text for top of shape: ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.shape_text) + entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + combobox = ttk.Combobox( + frame, + textvariable=self.font, + values=sorted(font.families()), + state="readonly", + ) + combobox.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" + ) + combobox.grid(row=0, column=1, padx=3, sticky="nsew") + button = ttk.Button(frame, text="Text color", command=self.choose_text_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + button = ttk.Checkbutton(frame, text="Bold") + button.grid(row=0, column=0) + button = ttk.Checkbutton(frame, text="Italic") + button.grid(row=0, column=1, padx=3) + button = ttk.Checkbutton(frame, text="Underline") + button.grid(row=0, column=2) + frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Fill color") + label.grid(row=0, column=0, sticky="nsew") + self.fill = ttk.Label(frame, text=self.fill_color, background="#CFCFFF") + self.fill.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Border color:") + label.grid(row=0, column=0, sticky="nsew") + self.border = ttk.Label( + frame, text=self.border_color, background=self.fill_color + ) + self.border.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Border width:") + label.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="nsew") + frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="Add shape") + button.grid(row=0, column=0, sticky="e", padx=3) + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="w", pady=3) + frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) + + def choose_text_color(self): + color = colorchooser.askcolor(color="black") + self.text_color = color[1] + + def choose_fill_color(self): + color = colorchooser.askcolor(color=self.fill_color) + self.fill_color = color[1] + self.fill.config(background=color[1], text=color[1]) + + def choose_border_color(self): + color = colorchooser.askcolor(color="black") + self.border_color = color[1] + self.border.config(background=color[1], text=color[1]) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 942dfce8..c8c5ceba 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -68,6 +68,7 @@ class CanvasGraph(tk.Canvas): self.core = core self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) + self.shape_drawing = False # background related self.wallpaper_id = None @@ -144,6 +145,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_motion) self.bind("", self.click_context) self.bind("", self.press_delete) + self.bind("", self.ctrl_click) def draw_grid(self, width=1000, height=800): """ @@ -275,6 +277,9 @@ class CanvasGraph(tk.Canvas): selected = _id break + if _id in self.shapes: + selected = _id + return selected def click_release(self, event): @@ -290,8 +295,10 @@ class CanvasGraph(tk.Canvas): else: if self.mode == GraphMode.ANNOTATION: if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + self.focus_set() x, y = self.canvas_xy(event) self.shapes[self.selected].shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -355,12 +362,28 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape + if ( + self.mode == GraphMode.ANNOTATION + and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] + and selected is None + ): + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape + self.shape_drawing = True + if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + x, y = self.canvas_xy(event) + self.shapes[selected].cursor_x = x + self.shapes[selected].cursor_y = y + self.canvas_management.node_select(self.shapes[selected]) + self.selected = selected + + def ctrl_click(self, event): + logging.debug("Control left click %s", event) + selected = self.get_selected(event) + if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + self.canvas_management.node_select(self.shapes[selected], True) def click_motion(self, event): """ @@ -374,9 +397,14 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + if ( + self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] + and self.shape_drawing + ): x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) + if self.mode == GraphMode.SELECT and "shape" in self.gettags(self.selected): + self.shapes[self.selected].motion(event) def click_context(self, event): logging.info("context event: %s", self.context) @@ -399,11 +427,12 @@ class CanvasGraph(tk.Canvas): :param event: :return: """ + logging.debug("press delete key") nodes = self.canvas_management.delete_selected_nodes() self.core.delete_graph_nodes(nodes) def add_node(self, x, y): - if self.selected is None: + if self.selected is None or "shape" in self.gettags(self.selected): core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) @@ -536,6 +565,9 @@ class CanvasGraph(tk.Canvas): else: self.itemconfig("gridline", state=tk.HIDDEN) + def is_selection_mode(self): + return self.mode == GraphMode.SELECT + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -701,6 +733,7 @@ class CanvasNode: logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id def click_release(self, event): logging.debug(f"node click release {self.core_node.name}: {event}") diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index a4de5f0d..8ea1500e 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -47,41 +47,49 @@ class CanvasComponentManagement: def delete_selected_nodes(self): edges = set() nodes = [] + for node_id in self.selected: + if "node" in self.canvas.gettags(node_id): + bbox_id = self.selected[node_id] + canvas_node = self.canvas.nodes.pop(node_id) + nodes.append(canvas_node) + self.canvas.delete(node_id) + self.canvas.delete(bbox_id) + self.canvas.delete(canvas_node.text_id) - for node_id in list(self.selected): - bbox_id = self.selected[node_id] - canvas_node = self.canvas.nodes.pop(node_id) - nodes.append(canvas_node) - self.canvas.delete(node_id) - self.canvas.delete(bbox_id) - self.canvas.delete(canvas_node.text_id) - - # delete antennas - is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() - - # delete related edges - for edge in canvas_node.edges: - if edge in edges: - continue - edges.add(edge) - self.canvas.edges.pop(edge.token) - self.canvas.delete(edge.id) - self.canvas.delete(edge.link_info.id1) - self.canvas.delete(edge.link_info.id2) - other_id = edge.src - other_interface = edge.src_interface - if edge.src == node_id: - other_id = edge.dst - other_interface = edge.dst_interface - other_node = self.canvas.nodes[other_id] - other_node.edges.remove(edge) - try: - other_node.interfaces.remove(other_interface) - except ValueError: - pass + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) if is_wireless: - other_node.antenna_draw.delete_antenna() + canvas_node.antenna_draw.delete_antennas() + + # delete related edges + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.canvas.edges.pop(edge.token) + self.canvas.delete(edge.id) + self.canvas.delete(edge.link_info.id1) + self.canvas.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == node_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.canvas.nodes[other_id] + other_node.edges.remove(edge) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass + if is_wireless: + other_node.antenna_draw.delete_antenna() + + for shape_id in self.selected: + if "shape" in self.canvas.gettags(shape_id): + bbox_id = self.selected[node_id] + self.canvas.delete(shape_id) + self.canvas.delete(bbox_id) + self.canvas.shapes.pop(shape_id) + self.selected.clear() return nodes diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 76e76972..696f1e5e 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -3,6 +3,7 @@ class for shapes """ import logging +from coretk.dialogs.shapemod import ShapeDialog from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] @@ -16,6 +17,7 @@ class Shape: self.y0 = top_y self.cursor_x = None self.cursor_y = None + canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: self.id = canvas.create_oval( @@ -25,9 +27,8 @@ class Shape: self.id = canvas.create_rectangle( top_x, top_y, top_x, top_y, tags="shape", dash="-" ) - self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) + # self.canvas.tag_bind(self.id, "", self.motion) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -36,11 +37,8 @@ class Shape: self.canvas.itemconfig(self.id, width=0, fill="#ccccff") for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - - def click_press(self, event): - logging.debug("Click on shape %s", self.id) - self.cursor_x = event.x - self.cursor_y = event.y + s = ShapeDialog(self.app, self.app) + s.show() def click_release(self, event): logging.debug("Click release on shape %s", self.id) @@ -53,5 +51,6 @@ class Shape: self.canvas.coords( self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) + self.canvas.canvas_management.node_drag(self, delta_x, delta_y) self.cursor_x = event.x self.cursor_y = event.y From 0a26a8f8e38a890cca9e720287ad667e4dea52c0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 3 Dec 2019 16:18:00 -0800 Subject: [PATCH 195/305] shape dialog, fix move shape --- coretk/coretk/dialogs/shapemod.py | 51 +++++++++++++++++++++++++++---- coretk/coretk/graph.py | 24 ++++++++++++--- coretk/coretk/shape.py | 5 +-- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index b76fc0d1..577d3821 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -11,8 +11,10 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, master, app): + def __init__(self, master, app, shape_id): super().__init__(master, app, "Add a new shape", modal=True) + self.canvas = app.canvas + self.id = shape_id self.shape_text = tk.StringVar(value="") self.font = tk.StringVar(value="Arial") self.font_size = tk.IntVar(value=12) @@ -20,6 +22,9 @@ class ShapeDialog(Dialog): self.fill_color = "#CFCFFF" self.border_color = "black" self.border_width = tk.IntVar(value=0) + self.bold = tk.IntVar(value=0) + self.italic = tk.IntVar(value=0) + self.underline = tk.IntVar(value=0) self.fill = None self.border = None @@ -57,11 +62,11 @@ class ShapeDialog(Dialog): frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) frame = ttk.Frame(self.top) - button = ttk.Checkbutton(frame, text="Bold") + button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") button.grid(row=0, column=0) - button = ttk.Checkbutton(frame, text="Italic") + button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") button.grid(row=0, column=1, padx=3) - button = ttk.Checkbutton(frame, text="Underline") + button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) @@ -105,9 +110,9 @@ class ShapeDialog(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Add shape") + button = ttk.Button(frame, text="Add shape", command=self.add_shape) button.grid(row=0, column=0, sticky="e", padx=3) - button = ttk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.cancel) button.grid(row=0, column=1, sticky="w", pady=3) frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) @@ -124,3 +129,37 @@ class ShapeDialog(Dialog): color = colorchooser.askcolor(color="black") self.border_color = color[1] self.border.config(background=color[1], text=color[1]) + + def cancel(self): + if not self.canvas.shapes[self.id].created: + self.canvas.delete(self.id) + self.canvas.shapes.pop(self.id) + self.destroy() + + def add_shape(self): + self.canvas.itemconfig( + self.id, + fill=self.fill_color, + dash="", + outline=self.border_color, + width=int(self.border_width.get()), + ) + shape = self.canvas.shapes[self.id] + shape_text = self.shape_text.get() + if shape.text is None: + size = int(self.font_size.get()) + x0, y0, x1, y1 = self.canvas.bbox(self.id) + text_y = y0 + 2 * size + text_x = (x0 + x1) / 2 + f = [self.font.get(), size] + if self.bold.get() == 1: + f.append("bold") + if self.italic.get() == 1: + f.append("italic") + if self.underline.get() == 1: + f.append("underline") + shape.text = self.canvas.create_text( + text_x, text_y, text=shape_text, fill=self.text_color, font=f + ) + self.canvas.shapes[self.id].created = True + self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c8c5ceba..c9c349ec 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -146,6 +146,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) + self.bind("", self.double_click) def draw_grid(self, width=1000, height=800): """ @@ -297,8 +298,9 @@ class CanvasGraph(tk.Canvas): if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: self.focus_set() x, y = self.canvas_xy(event) - self.shapes[self.selected].shape_complete(x, y) - self.shape_drawing = False + if self.shape_drawing: + self.shapes[self.selected].shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -312,6 +314,7 @@ class CanvasGraph(tk.Canvas): self.add_node(x, y) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE + self.selected = None def handle_edge_release(self, event): edge = self.drawing_edge @@ -372,7 +375,11 @@ class CanvasGraph(tk.Canvas): self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + if ( + self.mode == GraphMode.SELECT + and selected is not None + and "shape" in self.gettags(selected) + ): x, y = self.canvas_xy(event) self.shapes[selected].cursor_x = x self.shapes[selected].cursor_y = y @@ -403,7 +410,11 @@ class CanvasGraph(tk.Canvas): ): x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) - if self.mode == GraphMode.SELECT and "shape" in self.gettags(self.selected): + if ( + self.mode == GraphMode.SELECT + and self.selected is not None + and "shape" in self.gettags(self.selected) + ): self.shapes[self.selected].motion(event) def click_context(self, event): @@ -431,6 +442,11 @@ class CanvasGraph(tk.Canvas): nodes = self.canvas_management.delete_selected_nodes() self.core.delete_graph_nodes(nodes) + def double_click(self, event): + selected = self.get_selected(event) + if selected is not None and "shape" in self.gettags(selected): + print("bring up shape dialog ") + def add_node(self, x, y): if self.selected is None or "shape" in self.gettags(self.selected): core_node = self.core.create_node( diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 696f1e5e..9c85837e 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -17,6 +17,8 @@ class Shape: self.y0 = top_y self.cursor_x = None self.cursor_y = None + self.created = False + self.text = None canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -34,10 +36,9 @@ class Shape: self.canvas.coords(self.id, self.x0, self.y0, x1, y1) def shape_complete(self, x, y): - self.canvas.itemconfig(self.id, width=0, fill="#ccccff") for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - s = ShapeDialog(self.app, self.app) + s = ShapeDialog(self.app, self.app, self.id) s.show() def click_release(self, event): From 41a9b88189a2330163e41fcdc076c8fc7a9e404c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 3 Dec 2019 17:17:45 -0800 Subject: [PATCH 196/305] working on shape config --- coretk/coretk/dialogs/shapemod.py | 69 ++++++++++++++++++++----------- coretk/coretk/graph.py | 4 +- coretk/coretk/shape.py | 20 ++++++++- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 577d3821..c00a9649 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -11,20 +11,35 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): - def __init__(self, master, app, shape_id): + def __init__(self, master, app, shape): super().__init__(master, app, "Add a new shape", modal=True) self.canvas = app.canvas - self.id = shape_id - self.shape_text = tk.StringVar(value="") - self.font = tk.StringVar(value="Arial") - self.font_size = tk.IntVar(value=12) - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "black" - self.border_width = tk.IntVar(value=0) - self.bold = tk.IntVar(value=0) - self.italic = tk.IntVar(value=0) - self.underline = tk.IntVar(value=0) + self.id = shape.id + data = shape.shape_data + self.shape_text = tk.StringVar(value=data.text) + self.font = tk.StringVar(value=data.font) + self.font_size = tk.IntVar(value=data.font_size) + self.text_color = data.text_color + self.fill_color = data.fill_color + self.border_color = data.border_color + self.border_width = tk.IntVar(value=data.border_width) + self.bold = tk.IntVar(value=data.bold) + self.italic = tk.IntVar(value=data.italic) + self.underline = tk.IntVar(value=data.underline) + + # else: + # self.fill_color = self.canvas.itemcget(self.id, "fill") + # self.shape_text = tk.StringVar(value="") + # self.font = tk.StringVar(value="Arial") + # self.font_size = tk.IntVar(value=12) + # self.text_color = "#000000" + # # self.fill_color = "#CFCFFF" + # self.border_color = "black" + # self.border_width = tk.IntVar(value=0) + # self.bold = tk.IntVar(value=0) + # self.italic = tk.IntVar(value=0) + # self.underline = tk.IntVar(value=0) + # print(self.fill_color) self.fill = None self.border = None @@ -146,20 +161,24 @@ class ShapeDialog(Dialog): ) shape = self.canvas.shapes[self.id] shape_text = self.shape_text.get() - if shape.text is None: - size = int(self.font_size.get()) - x0, y0, x1, y1 = self.canvas.bbox(self.id) - text_y = y0 + 2 * size - text_x = (x0 + x1) / 2 - f = [self.font.get(), size] - if self.bold.get() == 1: - f.append("bold") - if self.italic.get() == 1: - f.append("italic") - if self.underline.get() == 1: - f.append("underline") + size = int(self.font_size.get()) + x0, y0, x1, y1 = self.canvas.bbox(self.id) + text_y = y0 + 2 * size + text_x = (x0 + x1) / 2 + f = [self.font.get(), size] + if self.bold.get() == 1: + f.append("bold") + if self.italic.get() == 1: + f.append("italic") + if self.underline.get() == 1: + f.append("underline") + if shape.text_id is None: shape.text = self.canvas.create_text( text_x, text_y, text=shape_text, fill=self.text_color, font=f ) - self.canvas.shapes[self.id].created = True + self.canvas.shapes[self.id].created = True + else: + self.canvas.itemconfig( + shape.text_id, text=shape_text, fill=self.text_color, font=f + ) self.destroy() diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index c9c349ec..7389be52 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -11,6 +11,7 @@ from coretk.canvastooltip import CanvasTooltip from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.shapemod import ShapeDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph_helper import GraphHelper, WlanAntennaManager from coretk.images import ImageEnum, Images @@ -445,7 +446,8 @@ class CanvasGraph(tk.Canvas): def double_click(self, event): selected = self.get_selected(event) if selected is not None and "shape" in self.gettags(selected): - print("bring up shape dialog ") + s = ShapeDialog(self.app, self.app, self.shapes[selected]) + print(s) def add_node(self, x, y): if self.selected is None or "shape" in self.gettags(self.selected): diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index 9c85837e..2bb7b516 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -9,6 +9,20 @@ from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] +class ShapeData: + def __init__(self): + self.text = "" + self.font = "Arial" + self.font_size = 12 + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "#000000" + self.border_width = 0 + self.bold = 0 + self.italic = 0 + self.underline = 0 + + class Shape: def __init__(self, app, canvas, top_x, top_y): self.app = app @@ -18,7 +32,9 @@ class Shape: self.cursor_x = None self.cursor_y = None self.created = False - self.text = None + self.text_id = None + + self.shape_data = ShapeData() canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -38,7 +54,7 @@ class Shape: def shape_complete(self, x, y): for component in ABOVE_COMPONENT: self.canvas.tag_raise(component) - s = ShapeDialog(self.app, self.app, self.id) + s = ShapeDialog(self.app, self.app, self) s.show() def click_release(self, event): From bbb8be6655cd37646be8b964d5941b6ec2f817e5 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:28:16 -0800 Subject: [PATCH 197/305] shape configuration, replace tunntel tool image --- coretk/coretk/data/icons/tunnel.png | Bin 0 -> 2256 bytes .../data/{icons => oldicons}/tunnel.gif | Bin coretk/coretk/dialogs/shapemod.py | 31 ++++++++---------- coretk/coretk/graph.py | 8 ++++- coretk/coretk/nodedelete.py | 2 +- coretk/coretk/shape.py | 7 +++- 6 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 coretk/coretk/data/icons/tunnel.png rename coretk/coretk/data/{icons => oldicons}/tunnel.gif (100%) diff --git a/coretk/coretk/data/icons/tunnel.png b/coretk/coretk/data/icons/tunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..2871b74f01e8f4080a3a88e30cabaed82a182b3a GIT binary patch literal 2256 zcmV;>2ru`EP)J3k%D_a<^qW!7*UKY22>d7`&}r zz{%E%)VMd*Ra-YoA9_(=s=g!-ReDJ+KQxL`rHYgkD!Hyz>Nu^esx3|$2u>ty?BY^` zi!m3oP;=YeGkw4qa53zeGv}Peo!=9z_WOUc^FNn4-^>|6AP@)y0)apv5C{YUfk5#8 z0x`M)>k*AcO#{<&)dZ$0BGauPWD6)QgG3#m!~?)__p5h6=xxa44hXqMAos~cx`L#9 zu{hiIL0el}(#YSj9%*MledKT#G-{Wp;!gwgD1dzcB#lxf0rDph`E~o;RQvh!=jSxalQM=H5DrJ3v$pv!1LQjZ z3iQlPK}RJbos1_eKk0g>Ev9Ggl^9|`P0cZjDfi~{gp%(8@EXyE6-)r+g~XWu<*u$@ z#Pv?Dy8(4g&3h%_Bn2MPJNE|k5#acRv%h^u&!oB;5DtekX07g*K=3u5c^}`ofbV|k zKQKwai?2HI*B+}HIZ6pm_{hSyJ32b%d9`spbTOdeKuZaw^i2TuytU8#d;&Qh82~UJ zCph-QZ1fG7RH~A03G$K7v#0-|zJ=?f)o9}$4Nc8YQc5pEgU$zlq`dsM%|h|Qb5pRG z)vD5J8RY%C#zXtmDs6Ku*5pSoMF9_sJw z=Aw8Xn%o2G8e3Wjq3zJn;FS;p>~EMyD3Ci+fs`fIM^I?5SV#7Uph1UE`sB zAkyy%VB@WAFbj~$p?A-ndYh*xR|D$zAFPCAdLJ5I^9?3ul1Rt>&et7JCN8D?fs}nEZYfUSXKy$*h)Ri?accb$faiFKaXAC(nwt0CeZox}N(++rIv)U@iOiyL51)ry zLUDXgr1cX#v{V}q4u{PIIDs@NZ7*^4y7_-bLtj5~hnGB|vV5YZ=9uaswrT@rGF;yR zP^DIfH}M4U{g+*sk1rn=obCev017-}@{Jrfs!k3J^e(V!sJaQy*%rP5z^6t-YdSAl(ek`zQx3ja zTl}j4USbtsJ}JZDaK^07eGNc>RpTMyrsT4gb+)z5vIH~V17>XQ&q8!Q0OTj97Ft<@ znGGPov#hZXA5UK|z0CMPgFcn!Th@8MF>BeC)|XisPQx{~0v4evU0Ep^8f~iVdI?Ta?D=&5m6SOr5{P z-u=zk{usj|4x+qol#hZ4wknR%5tGS`ro-wVQ6f)$9wxK0VkeQ6_YKQ1w1go>H@5Gp zhc_orwT>spk8M@E6~$13Ql)uV3~)2V=!VixTUTpmt1`BBGZ-n+($*I0H5BwJjW}`6u&(EHm2{Sc2v7_{204&9q|SJ z+ed5~0|;3>5_5wg5J}mIyLr=q;6uqh0%WjYWjt%Tj7 zd@d-I?}y*Fe3_yY=hwa*_NtIT@d{2(Sq zhCyX{<1eR3+8H285^{ne_yR$weev@Cqkr(`D2B%-nDxvSE8MNT>N3 zz*0QhEbu7OX=Z?$;u#B)MDT^?Mr36!?;G;&hnH~SqsyA*OvY5W+7Ggiur0`R(* zm*&ed#Rr$TZEUz9YKnBK4d7WkV`a?=ml=|70*~$Yr>_AL&=#HU!c&i+ya3>AP@)y0)apv e5C{Z<^yj~|p)*WPe", self.click_release) - # self.canvas.tag_bind(self.id, "", self.motion) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -69,5 +68,11 @@ class Shape: self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y ) self.canvas.canvas_management.node_drag(self, delta_x, delta_y) + if self.text_id is not None: + self.canvas.move(self.text_id, delta_x, delta_y) self.cursor_x = event.x self.cursor_y = event.y + + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) From 5a387537bb9f57d99b211379509694bdf8da4e90 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 13:40:35 -0800 Subject: [PATCH 198/305] updates clean up setting wallpaper for graph canvas, updates to read metadata for canvas to display wallpaper from loaded xml file, update to allow reopening mobility player from node context --- coretk/coretk/coreclient.py | 37 ++++++++++++++----- coretk/coretk/data/xmls/sample1.xml | 2 +- coretk/coretk/dialogs/canvasbackground.py | 23 +++--------- coretk/coretk/dialogs/mobilityplayer.py | 44 ++++++++++++++++++++--- coretk/coretk/graph.py | 24 +++++++++++-- 5 files changed, 94 insertions(+), 36 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index fec00a73..ef617ac0 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -1,12 +1,14 @@ """ Incorporate grpc into python tkinter GUI """ +import json import logging import os import time from core.api.grpc import client, core_pb2 -from coretk.dialogs.mobilityplayer import MobilityPlayerDialog +from coretk import appconfig +from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -141,6 +143,8 @@ class CoreClient: logging.warning("unknown session event: %s", session_event) elif event.HasField("node_event"): self.handle_node_event(event.node_event) + elif event.HasField("config_event"): + logging.info("config event: %s", event) else: logging.info("unhandled event: %s", event) @@ -164,7 +168,7 @@ class CoreClient: x = event.node.position.x y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y) + canvas_node.move(x, y, update=False) def handle_throughputs(self, event): interface_throughputs = event.interface_throughputs @@ -189,7 +193,6 @@ class CoreClient: # get session data response = self.client.get_session(self.session_id) - logging.info("joining session(%s): %s", self.session_id, response) session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) @@ -235,9 +238,6 @@ class CoreClient: node_id = int(_id / 1000) self.set_emane_model_config(node_id, config.model, config.config, interface) - # draw session - self.app.canvas.reset_and_redraw(session) - # get node service config and file config for node in session.nodes: self.created_nodes.add(node.id) @@ -262,6 +262,13 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data + # draw session + self.app.canvas.reset_and_redraw(session) + + # get metadata + response = self.client.get_session_metadata(self.session_id) + self.parse_metadata(response.config) + if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: @@ -271,6 +278,18 @@ class CoreClient: def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME + def parse_metadata(self, config): + # canvas settings + canvas_config = config.get("canvas") + if canvas_config: + logging.info("canvas metadata: %s", canvas_config) + canvas_config = json.loads(canvas_config) + wallpaper_style = canvas_config["wallpaper-style"] + self.app.canvas.scale_option.set(wallpaper_style) + wallpaper = canvas_config["wallpaper"] + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) + def create_new_session(self): """ Create a new session @@ -372,9 +391,9 @@ class CoreClient: # display mobility players for node_id, config in self.mobility_configs.items(): canvas_node = self.canvas_nodes[node_id] - dialog = MobilityPlayerDialog(self.app, self.app, canvas_node, config) - dialog.show() - self.mobility_players[node_id] = dialog + mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) + mobility_player.show() + self.mobility_players[node_id] = mobility_player def stop_session(self, session_id=None): if not session_id: diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 8bda5b8c..7c8ccf72 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -269,7 +269,7 @@ router ospf6 - + diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvasbackground.py index 84b0bdb6..844b5595 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvasbackground.py @@ -5,14 +5,11 @@ import logging import tkinter as tk from tkinter import filedialog, ttk -from PIL import Image - from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images PADX = 5 -ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class CanvasBackgroundDialog(Dialog): @@ -178,23 +175,11 @@ class CanvasBackgroundDialog(Dialog): filename = self.filename.get() if not filename: - self.canvas.delete(self.canvas.wallpaper_id) - self.canvas.wallpaper = None - self.canvas.wallpaper_file = None - self.destroy() - return - try: - img = Image.open(filename) - self.canvas.wallpaper = img - self.canvas.wallpaper_file = filename - self.canvas.redraw() - for component in ABOVE_WALLPAPER: - self.canvas.tag_raise(component) + filename = None + try: + self.canvas.set_wallpaper(filename) except FileNotFoundError: logging.error("invalid background: %s", filename) - if self.canvas.wallpaper_id: - self.canvas.delete(self.canvas.wallpaper_id) - self.canvas.wallpaper_id = None - self.canvas.wallpaper_file = None + self.destroy() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 65b16058..136e6179 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import ttk -from core.api.grpc import core_pb2 +from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images @@ -9,6 +9,42 @@ PAD = 5 ICON_SIZE = 16 +class MobilityPlayer: + def __init__(self, master, app, canvas_node, config): + self.master = master + self.app = app + self.canvas_node = canvas_node + self.config = config + self.dialog = None + self.state = None + + def show(self): + if self.dialog: + self.dialog.destroy() + self.dialog = MobilityPlayerDialog( + self.master, self.app, self.canvas_node, self.config + ) + if self.state == MobilityAction.START: + self.set_play() + elif self.state == MobilityAction.PAUSE: + self.set_pause() + else: + self.set_stop() + self.dialog.show() + + def set_play(self): + self.dialog.set_play() + self.state = MobilityAction.START + + def set_pause(self): + self.dialog.set_pause() + self.state = MobilityAction.PAUSE + + def set_stop(self): + self.dialog.set_stop() + self.state = MobilityAction.STOP + + class MobilityPlayerDialog(Dialog): def __init__(self, master, app, canvas_node, config): super().__init__( @@ -88,19 +124,19 @@ class MobilityPlayerDialog(Dialog): self.set_play() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.START + session_id, self.node.id, MobilityAction.START ) def click_pause(self): self.set_pause() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.PAUSE + session_id, self.node.id, MobilityAction.PAUSE ) def click_stop(self): self.set_stop() session_id = self.app.core.session_id self.app.core.client.mobility_action( - session_id, self.node.id, core_pb2.MobilityAction.STOP + session_id, self.node.id, MobilityAction.STOP ) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 7389be52..7ef341a3 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -3,7 +3,7 @@ import logging import tkinter as tk from tkinter import font -from PIL import ImageTk +from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType @@ -21,6 +21,7 @@ from coretk.nodeutils import NodeUtils from coretk.shape import Shape NODE_TEXT_OFFSET = 5 +ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] class GraphMode(enum.Enum): @@ -583,6 +584,21 @@ class CanvasGraph(tk.Canvas): else: self.itemconfig("gridline", state=tk.HIDDEN) + def set_wallpaper(self, filename): + logging.info("setting wallpaper: %s", filename) + if filename is not None: + img = Image.open(filename) + self.wallpaper = img + self.wallpaper_file = filename + self.redraw() + for component in ABOVE_WALLPAPER: + self.tag_raise(component) + else: + if self.wallpaper_id is not None: + self.delete(self.wallpaper_id) + self.wallpaper = None + self.wallpaper_file = None + def is_selection_mode(self): return self.mode == GraphMode.SELECT @@ -696,7 +712,7 @@ class CanvasNode: self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) - def move(self, x, y): + def move(self, x, y, update=True): old_x = self.core_node.position.x old_y = self.core_node.position.y x_offset = x - old_x @@ -720,7 +736,7 @@ class CanvasNode: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) - if self.app.core.is_runtime(): + if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node.id, int(x), int(y)) def on_enter(self, event): @@ -784,6 +800,8 @@ class CanvasNode: def show_mobility_player(self): self.canvas.context = None + mobility_player = self.app.core.mobility_players[self.core_node.id] + mobility_player.show() def show_emane_config(self): self.canvas.context = None From 5aa01d9bb55f735dae3357df86252714b67fe190 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:44:43 -0800 Subject: [PATCH 199/305] simplify some join session logic to loop over nodes once --- coretk/coretk/coreclient.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ef617ac0..0f7eb996 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -211,12 +211,6 @@ class CoreClient: for hook in response.hooks: self.hooks[hook.file] = hook - # get wlan configs - for node in session.nodes: - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config - # get mobility configs response = self.client.get_mobility_configs(self.session_id) for node_id in response.configs: @@ -238,13 +232,17 @@ class CoreClient: node_id = int(_id / 1000) self.set_emane_model_config(node_id, config.model, config.config, interface) - # get node service config and file config + # save and retrieve data, needed for session nodes for node in session.nodes: + # get node service config and file config self.created_nodes.add(node.id) - for link in session.links: - self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) - for node in session.nodes: - if node.type == core_pb2.NodeType.DEFAULT: + + # get wlan configs for wlan nodes + if node.type == core_pb2.NodeType.WIRELESS_LAN: + response = self.client.get_wlan_config(self.session_id, node.id) + self.wlan_configs[node.id] = response.config + # retrieve service configurations data for default nodes + elif node.type == core_pb2.NodeType.DEFAULT: for service in node.services: response = self.client.get_node_service( self.session_id, node.id, service @@ -262,6 +260,10 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data + # store links as created links + for link in session.links: + self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + # draw session self.app.canvas.reset_and_redraw(session) From 96d273815d8583367ba9d9809977ce82d367d9ed Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:08:05 -0800 Subject: [PATCH 200/305] support moving multiple nodes and shape --- coretk/coretk/graph.py | 60 +++++++++++++++++++++++++------------ coretk/coretk/nodedelete.py | 3 ++ coretk/coretk/shape.py | 16 +++++----- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 6f75e78c..e5a79612 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -361,9 +361,6 @@ class CanvasGraph(tk.Canvas): :return: nothing """ logging.debug(f"click press: {event}") - self.delete(self.find_withtag("selectednodes")) - self.canvas_management.selected.clear() - selected = self.get_selected(event) is_node = selected in self.find_withtag("node") if self.mode == GraphMode.EDGE and is_node: @@ -379,16 +376,19 @@ class CanvasGraph(tk.Canvas): self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - if ( - self.mode == GraphMode.SELECT - and selected is not None - and "shape" in self.gettags(selected) - ): - x, y = self.canvas_xy(event) - self.shapes[selected].cursor_x = x - self.shapes[selected].cursor_y = y - self.canvas_management.node_select(self.shapes[selected]) - self.selected = selected + if self.mode == GraphMode.SELECT: + if selected is not None: + if "shape" in self.gettags(selected): + x, y = self.canvas_xy(event) + self.shapes[selected].cursor_x = x + self.shapes[selected].cursor_y = y + if selected not in self.canvas_management.selected: + self.canvas_management.node_select(self.shapes[selected]) + self.selected = selected + else: + for i in self.find_withtag("selectednodes"): + self.delete(i) + self.canvas_management.selected.clear() def ctrl_click(self, event): logging.debug("Control left click %s", event) @@ -419,7 +419,19 @@ class CanvasGraph(tk.Canvas): and self.selected is not None and "shape" in self.gettags(self.selected) ): - self.shapes[self.selected].motion(event) + x, y = self.canvas_xy(event) + shape = self.shapes[self.selected] + delta_x = x - shape.cursor_x + delta_y = y - shape.cursor_y + shape.motion(event) + # move other selected components + for nid in self.canvas_management.selected: + if nid != self.selected and nid in self.shapes: + self.shapes[nid].motion(None, delta_x, delta_y) + if nid != self.selected and nid in self.nodes: + node_x = self.nodes[nid].core_node.position.x + node_y = self.nodes[nid].core_node.position.y + self.nodes[nid].move(node_x + delta_x, node_y + delta_y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -753,8 +765,9 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - self.canvas.canvas_management.node_select(self) - self.canvas.selected = self.id + if self.id not in self.canvas.canvas_management.selected: + self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id def click_release(self, event): logging.debug(f"node click release {self.core_node.name}: {event}") @@ -765,10 +778,19 @@ class CanvasNode: if self.canvas.mode == GraphMode.EDGE: return x, y = self.canvas.canvas_xy(event) + my_x = self.core_node.position.x + my_y = self.core_node.position.y self.move(x, y) - # for nid, bboxid in self.canvas.canvas_management.selected.items(): - # if nid in self.canvas.nodes: - # self.canvas.nodes[nid].motion(event) + # move other selected components + for nid, bboxid in self.canvas.canvas_management.selected.items(): + if nid != self.id and nid in self.canvas.nodes: + other_old_x = self.canvas.nodes[nid].core_node.position.x + other_old_y = self.canvas.nodes[nid].core_node.position.y + other_new_x = x + other_old_x - my_x + other_new_y = y + other_old_y - my_y + self.canvas.nodes[nid].move(other_new_x, other_new_y) + if nid != self.id and nid in self.canvas.shapes: + self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) def select_multiple(self, event): self.canvas.canvas_management.node_select(self, True) diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/nodedelete.py index e30415c6..1365dc0f 100644 --- a/coretk/coretk/nodedelete.py +++ b/coretk/coretk/nodedelete.py @@ -33,6 +33,9 @@ class CanvasComponentManagement: tags="selectednodes", ) self.selected[canvas_node.id] = bbox_id + else: + bbox_id = self.selected.pop(canvas_node.id) + self.canvas.delete(bbox_id) def node_drag(self, canvas_node, offset_x, offset_y): select_id = self.selected.get(canvas_node.id) diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index d60beaef..c39c549b 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -59,19 +59,17 @@ class Shape: def click_release(self, event): logging.debug("Click release on shape %s", self.id) - def motion(self, event): + def motion(self, event, delta_x=None, delta_y=None): logging.debug("motion on shape %s", self.id) - delta_x = event.x - self.cursor_x - delta_y = event.y - self.cursor_y - x0, y0, x1, y1 = self.canvas.bbox(self.id) - self.canvas.coords( - self.id, x0 + delta_x, y0 + delta_y, x1 + delta_x, y1 + delta_y - ) + if event is not None: + delta_x = event.x - self.cursor_x + delta_y = event.y - self.cursor_y + self.cursor_x = event.x + self.cursor_y = event.y + self.canvas.move(self.id, delta_x, delta_y) self.canvas.canvas_management.node_drag(self, delta_x, delta_y) if self.text_id is not None: self.canvas.move(self.text_id, delta_x, delta_y) - self.cursor_x = event.x - self.cursor_y = event.y def delete(self): self.canvas.delete(self.id) From d024bdf0b73512deca2d066693769c130600038c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:12:31 -0800 Subject: [PATCH 201/305] fix small logic --- coretk/coretk/graph.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index bb2efcad..60f4e498 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -394,7 +394,11 @@ class CanvasGraph(tk.Canvas): def ctrl_click(self, event): logging.debug("Control left click %s", event) selected = self.get_selected(event) - if self.mode == GraphMode.SELECT and "shape" in self.gettags(selected): + if ( + self.mode == GraphMode.SELECT + and selected is not None + and "shape" in self.gettags(selected) + ): self.canvas_management.node_select(self.shapes[selected], True) def click_motion(self, event): From c7b9c7bfb414fbde8d70abb89bf0b249e621a1b3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 4 Dec 2019 16:40:33 -0800 Subject: [PATCH 202/305] updated sample1 xml to store all metadata in json format --- coretk/coretk/data/xmls/sample1.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 7c8ccf72..c780d21b 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -266,11 +266,11 @@ router ospf6 - - - + + + - + From 59614fa8d3cacfe4d498381e1d90ec01d3912564 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 10:12:31 -0800 Subject: [PATCH 203/305] try to load canvas config --- coretk/coretk/coreclient.py | 32 ++++++++ coretk/coretk/dialogs/shapemod.py | 7 +- coretk/coretk/graph.py | 4 +- coretk/coretk/graph_helper.py | 2 + coretk/coretk/parsedata.py | 28 +++++++ coretk/coretk/shape.py | 123 +++++++++++++++++++++++------- 6 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 coretk/coretk/parsedata.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 0f7eb996..79bc5359 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -13,6 +13,8 @@ from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils +# from coretk.shape import Shape, ShapeData + OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -281,6 +283,36 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # if canvas_config["type"] == "rectangle": + # data = ShapeData(False, canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple([float(x) for x in canvas_config["iconcoords"].split()]) + # print(coords) + # shape = Shape(self.app, self.app.canvas, None, None, coords, data, canvas_config["type"]) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "oval": + # print("not implemented") + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 005ad000..18ba0eca 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -158,7 +158,12 @@ class ShapeDialog(Dialog): f.append("underline") if shape.text_id is None: shape.text_id = self.canvas.create_text( - text_x, text_y, text=shape_text, fill=self.text_color, font=f + text_x, + text_y, + text=shape_text, + fill=self.text_color, + font=f, + tags="shapetext", ) self.canvas.shapes[self.id].created = True else: diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph.py index 60f4e498..24f07727 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph.py @@ -132,6 +132,7 @@ class CanvasGraph(tk.Canvas): self.selected = None self.nodes.clear() self.edges.clear() + self.shapes.clear() self.wireless_edges.clear() self.drawing_edge = None self.draw_session(session) @@ -451,8 +452,6 @@ class CanvasGraph(tk.Canvas): self.context.unpost() self.context = None - # TODO rather than delete, might move the data to somewhere else in order to reuse - # TODO when the user undo def press_delete(self, event): """ delete selected nodes and any data that relates to it @@ -585,6 +584,7 @@ class CanvasGraph(tk.Canvas): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: + print(self.scale_option.get()) option = ScaleOption(self.scale_option.get()) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph_helper.py index eea4ff55..af6d4823 100644 --- a/coretk/coretk/graph_helper.py +++ b/coretk/coretk/graph_helper.py @@ -16,6 +16,8 @@ CANVAS_COMPONENT_TAGS = [ "antenna", "wireless", "selectednodes", + "shape", + "shapetext", ] diff --git a/coretk/coretk/parsedata.py b/coretk/coretk/parsedata.py new file mode 100644 index 00000000..9afff3e2 --- /dev/null +++ b/coretk/coretk/parsedata.py @@ -0,0 +1,28 @@ +""" +parse meta data +""" +# from coretk.graph import ScaleOption + + +def parse(meta_string): + parsed = {} + if meta_string[0] == "{" and meta_string[len(meta_string) - 1] == "}": + meta_string = meta_string[1:-1] + for key_value in meta_string.split("} {"): + if key_value[len(key_value) - 1] == "}": + key, value = key_value[:-1].split(" {") + if key == "wallpaper-style": + if value == "upperleft": + parsed[key] = 1 + elif value == "centered": + parsed[key] = 2 + elif value == "scaled": + parsed[key] = 3 + elif value == "tiled": + parsed[key] = 4 + else: + parsed[key] = value + else: + key, value = tuple(key_value.split()) + parsed[key] = value + return parsed diff --git a/coretk/coretk/shape.py b/coretk/coretk/shape.py index c39c549b..63025b34 100644 --- a/coretk/coretk/shape.py +++ b/coretk/coretk/shape.py @@ -10,41 +10,108 @@ ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename" class ShapeData: - def __init__(self): - self.text = "" - self.font = "Arial" - self.font_size = 12 - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "#000000" - self.border_width = 0 - self.bold = 0 - self.italic = 0 - self.underline = 0 + def __init__( + self, + is_default=True, + text=None, + font=None, + font_size=None, + text_color=None, + fill_color=None, + border_color=None, + border_width=None, + bold=0, + italic=0, + underline=0, + ): + if is_default: + self.text = "" + self.font = "Arial" + self.font_size = 12 + self.text_color = "#000000" + self.fill_color = "#CFCFFF" + self.border_color = "#000000" + self.border_width = 0 + self.bold = 0 + self.italic = 0 + self.underline = 0 + else: + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.fill_color = fill_color + self.border_color = border_color + self.border_width = border_width + self.bold = bold + self.italic = italic + self.underline = underline class Shape: - def __init__(self, app, canvas, top_x, top_y): + def __init__( + self, + app, + canvas, + top_x=None, + top_y=None, + coords=None, + data=None, + shape_type=None, + ): self.app = app self.canvas = canvas - self.x0 = top_x - self.y0 = top_y + if data is None: + self.x0 = top_x + self.y0 = top_y + self.created = False + self.text_id = None + self.shape_data = ShapeData() + canvas.delete(canvas.find_withtag("selectednodes")) + annotation_type = self.canvas.annotation_type + if annotation_type == ImageEnum.OVAL: + self.id = canvas.create_oval( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + elif annotation_type == ImageEnum.RECTANGLE: + self.id = canvas.create_rectangle( + top_x, top_y, top_x, top_y, tags="shape", dash="-" + ) + else: + x0, y0, x1, y1 = coords + self.x0 = x0 + self.y0 = y0 + self.created = True + if shape_type == "oval": + self.id = self.canvas.create_oval( + x0, + y0, + x1, + y1, + tags="shape", + fill=data.fill_color, + outline=data.border_color, + width=data.border_width, + ) + elif shape_type == "rectangle": + self.id = self.canvas.create_rectangle( + x0, + y0, + x1, + y1, + tags="shape", + fill=data.fill_color, + outline=data.border_color, + width=data.border_width, + ) + _x = (x0 + x1) / 2 + _y = (y0 + y1) / 2 + self.text_id = self.canvas.create_text( + _x, _y, text=data.text, fill=data.text_color + ) + self.shape_data = data self.cursor_x = None self.cursor_y = None - self.created = False - self.text_id = None - - self.shape_data = ShapeData() - canvas.delete(canvas.find_withtag("selectednodes")) - annotation_type = self.canvas.annotation_type - if annotation_type == ImageEnum.OVAL: - self.id = canvas.create_oval( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) - elif annotation_type == ImageEnum.RECTANGLE: - self.id = canvas.create_rectangle( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) self.canvas.tag_bind(self.id, "", self.click_release) def shape_motion(self, x1, y1): From 8c30ad6af57bfde50e8a4c90fdf0bb958ecbd57c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 10:39:53 -0800 Subject: [PATCH 204/305] commit before splitting files --- coretk/coretk/coreclient.py | 75 +++++++++++++++++++++---------------- coretk/coretk/parsedata.py | 1 - 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 79bc5359..1a7d06e3 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,13 +7,12 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig +from coretk import appconfig, parsedata from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils - -# from coretk.shape import Shape, ShapeData +from coretk.shape import Shape, ShapeData OBSERVERS = { "processes": "ps", @@ -283,36 +282,46 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - # for key, value in config.items(): - # if "global_options" != key: - # canvas_config = parsedata.parse(value) - # print(canvas_config) - # if canvas_config.get("type"): - # if canvas_config["type"] == "rectangle": - # data = ShapeData(False, canvas_config["label"], - # canvas_config["fontfamily"], - # canvas_config["fontsize"], - # canvas_config["labelcolor"], - # canvas_config["color"], - # canvas_config["border"], - # canvas_config["width"], - # ) - # coords = tuple([float(x) for x in canvas_config["iconcoords"].split()]) - # print(coords) - # shape = Shape(self.app, self.app.canvas, None, None, coords, data, canvas_config["type"]) - # self.app.canvas.shapes[shape.id] = shape - # elif canvas_config["type"] == "oval": - # print("not implemented") - # elif canvas_config["type"] == "text": - # print("not implemented") - # else: - # if "wallpaper" in canvas_config: - # logging.info("canvas metadata: %s", canvas_config) - # wallpaper_style = canvas_config["wallpaper-style"] - # self.app.canvas.scale_option.set(wallpaper_style) - # wallpaper = canvas_config["wallpaper"] - # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - # self.app.canvas.set_wallpaper(wallpaper) + for key, value in config.items(): + if "global_options" != key: + canvas_config = parsedata.parse(value) + print(canvas_config) + if canvas_config.get("type"): + config_type = canvas_config["type"] + if config_type == "rectangle" or config_type == "oval": + data = ShapeData( + False, + canvas_config["label"], + canvas_config["fontfamily"], + canvas_config["fontsize"], + canvas_config["labelcolor"], + canvas_config["color"], + canvas_config["border"], + canvas_config["width"], + ) + coords = tuple( + [float(x) for x in canvas_config["iconcoords"].split()] + ) + shape = Shape( + self.app, + self.app.canvas, + None, + None, + coords, + data, + config_type, + ) + self.app.canvas.shapes[shape.id] = shape + elif canvas_config["type"] == "text": + print("not implemented") + else: + if "wallpaper" in canvas_config: + logging.info("canvas metadata: %s", canvas_config) + wallpaper_style = canvas_config["wallpaper-style"] + self.app.canvas.scale_option.set(wallpaper_style) + wallpaper = canvas_config["wallpaper"] + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/parsedata.py b/coretk/coretk/parsedata.py index 9afff3e2..e4312846 100644 --- a/coretk/coretk/parsedata.py +++ b/coretk/coretk/parsedata.py @@ -1,7 +1,6 @@ """ parse meta data """ -# from coretk.graph import ScaleOption def parse(meta_string): From d970d5ee85d071ecad9e61df248cf782f8479a5c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 11:12:25 -0800 Subject: [PATCH 205/305] separated out graph code into more file and its own sub package --- coretk/coretk/app.py | 4 +- coretk/coretk/coreclient.py | 83 ++++--- coretk/coretk/graph/__init__.py | 0 coretk/coretk/{ => graph}/canvastooltip.py | 0 coretk/coretk/graph/edges.py | 70 ++++++ coretk/coretk/graph/enums.py | 23 ++ coretk/coretk/{ => graph}/graph.py | 260 +-------------------- coretk/coretk/{ => graph}/graph_helper.py | 0 coretk/coretk/{ => graph}/linkinfo.py | 0 coretk/coretk/graph/node.py | 163 +++++++++++++ coretk/coretk/{ => graph}/nodedelete.py | 0 coretk/coretk/{ => graph}/shape.py | 0 coretk/coretk/{status.py => statusbar.py} | 0 coretk/coretk/toolbar.py | 2 +- 14 files changed, 307 insertions(+), 298 deletions(-) create mode 100644 coretk/coretk/graph/__init__.py rename coretk/coretk/{ => graph}/canvastooltip.py (100%) create mode 100644 coretk/coretk/graph/edges.py create mode 100644 coretk/coretk/graph/enums.py rename coretk/coretk/{ => graph}/graph.py (71%) rename coretk/coretk/{ => graph}/graph_helper.py (100%) rename coretk/coretk/{ => graph}/linkinfo.py (100%) create mode 100644 coretk/coretk/graph/node.py rename coretk/coretk/{ => graph}/nodedelete.py (100%) rename coretk/coretk/{ => graph}/shape.py (100%) rename coretk/coretk/{status.py => statusbar.py} (100%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index c8232ebd..2dd2c5ca 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -5,12 +5,12 @@ from tkinter import ttk from coretk import appconfig, themes from coretk.coreclient import CoreClient -from coretk.graph import CanvasGraph +from coretk.graph.graph import CanvasGraph from coretk.images import ImageEnum, Images from coretk.menuaction import MenuAction from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils -from coretk.status import StatusBar +from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1a7d06e3..d0519bb5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,12 +7,11 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig, parsedata +from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.shape import Shape, ShapeData OBSERVERS = { "processes": "ps", @@ -282,46 +281,46 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - for key, value in config.items(): - if "global_options" != key: - canvas_config = parsedata.parse(value) - print(canvas_config) - if canvas_config.get("type"): - config_type = canvas_config["type"] - if config_type == "rectangle" or config_type == "oval": - data = ShapeData( - False, - canvas_config["label"], - canvas_config["fontfamily"], - canvas_config["fontsize"], - canvas_config["labelcolor"], - canvas_config["color"], - canvas_config["border"], - canvas_config["width"], - ) - coords = tuple( - [float(x) for x in canvas_config["iconcoords"].split()] - ) - shape = Shape( - self.app, - self.app.canvas, - None, - None, - coords, - data, - config_type, - ) - self.app.canvas.shapes[shape.id] = shape - elif canvas_config["type"] == "text": - print("not implemented") - else: - if "wallpaper" in canvas_config: - logging.info("canvas metadata: %s", canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] - self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # config_type = canvas_config["type"] + # if config_type == "rectangle" or config_type == "oval": + # data = ShapeData( + # False, + # canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple( + # [float(x) for x in canvas_config["iconcoords"].split()] + # ) + # shape = Shape( + # self.app, + # self.app.canvas, + # None, + # None, + # coords, + # data, + # config_type, + # ) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings canvas_config = config.get("canvas") if canvas_config: diff --git a/coretk/coretk/graph/__init__.py b/coretk/coretk/graph/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/coretk/coretk/canvastooltip.py b/coretk/coretk/graph/canvastooltip.py similarity index 100% rename from coretk/coretk/canvastooltip.py rename to coretk/coretk/graph/canvastooltip.py diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py new file mode 100644 index 00000000..92f99400 --- /dev/null +++ b/coretk/coretk/graph/edges.py @@ -0,0 +1,70 @@ +import tkinter as tk + + +class CanvasWirelessEdge: + def __init__(self, token, position, src, dst, canvas): + self.token = token + self.src = src + self.dst = dst + self.canvas = canvas + self.id = self.canvas.create_line( + *position, tags="wireless", width=1.5, fill="#009933" + ) + + def delete(self): + self.canvas.delete(self.id) + + +class CanvasEdge: + """ + Canvas edge class + """ + + width = 1.4 + + def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): + """ + Create an instance of canvas edge object + :param int x1: source x-coord + :param int y1: source y-coord + :param int x2: destination x-coord + :param int y2: destination y-coord + :param int src: source id + :param tkinter.Canvas canvas: canvas object + """ + self.src = src + self.dst = None + self.src_interface = None + self.dst_interface = None + self.canvas = canvas + if is_wired is None or is_wired is True: + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) + else: + self.id = self.canvas.create_line( + x1, + y1, + x2, + y2, + tags="edge", + width=self.width, + fill="#ff0000", + state=tk.HIDDEN, + ) + self.token = None + self.link_info = None + self.throughput = None + self.wired = is_wired + + 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) + self.canvas.helper.draw_wireless_case(self.src, self.dst, self) + self.canvas.tag_raise(self.src) + self.canvas.tag_raise(self.dst) + + def delete(self): + self.canvas.delete(self.id) diff --git a/coretk/coretk/graph/enums.py b/coretk/coretk/graph/enums.py new file mode 100644 index 00000000..65fb10eb --- /dev/null +++ b/coretk/coretk/graph/enums.py @@ -0,0 +1,23 @@ +import enum + + +class GraphMode(enum.Enum): + SELECT = 0 + EDGE = 1 + PICKNODE = 2 + NODE = 3 + ANNOTATION = 4 + OTHER = 5 + + +class ScaleOption(enum.Enum): + NONE = 0 + UPPER_LEFT = 1 + CENTERED = 2 + SCALED = 3 + TILED = 4 + + +class ShapeType(enum.Enum): + OVAL = 0 + RECTANGLE = 1 diff --git a/coretk/coretk/graph.py b/coretk/coretk/graph/graph.py similarity index 71% rename from coretk/coretk/graph.py rename to coretk/coretk/graph/graph.py index 24f07727..d39ca012 100644 --- a/coretk/coretk/graph.py +++ b/coretk/coretk/graph/graph.py @@ -1,51 +1,24 @@ -import enum import logging import tkinter as tk -from tkinter import font from PIL import Image, ImageTk from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType -from coretk.canvastooltip import CanvasTooltip -from coretk.dialogs.emaneconfig import EmaneConfigDialog -from coretk.dialogs.mobilityconfig import MobilityConfigDialog -from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.shapemod import ShapeDialog -from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.graph_helper import GraphHelper, WlanAntennaManager +from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge +from coretk.graph.enums import GraphMode, ScaleOption +from coretk.graph.graph_helper import GraphHelper +from coretk.graph.linkinfo import LinkInfo, Throughput +from coretk.graph.node import CanvasNode +from coretk.graph.nodedelete import CanvasComponentManagement +from coretk.graph.shape import Shape from coretk.images import ImageEnum, Images -from coretk.linkinfo import LinkInfo, Throughput -from coretk.nodedelete import CanvasComponentManagement from coretk.nodeutils import NodeUtils -from coretk.shape import Shape -NODE_TEXT_OFFSET = 5 ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] -class GraphMode(enum.Enum): - SELECT = 0 - EDGE = 1 - PICKNODE = 2 - NODE = 3 - ANNOTATION = 4 - OTHER = 5 - - -class ScaleOption(enum.Enum): - NONE = 0 - UPPER_LEFT = 1 - CENTERED = 2 - SCALED = 3 - TILED = 4 - - -class ShapeType(enum.Enum): - OVAL = 0 - RECTANGLE = 1 - - class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): if cnf is None: @@ -620,222 +593,3 @@ class CanvasGraph(tk.Canvas): def is_selection_mode(self): return self.mode == GraphMode.SELECT - - -class CanvasWirelessEdge: - def __init__(self, token, position, src, dst, canvas): - self.token = token - self.src = src - self.dst = dst - self.canvas = canvas - self.id = self.canvas.create_line( - *position, tags="wireless", width=1.5, fill="#009933" - ) - - def delete(self): - self.canvas.delete(self.id) - - -class CanvasEdge: - """ - Canvas edge class - """ - - width = 1.4 - - def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): - """ - Create an instance of canvas edge object - :param int x1: source x-coord - :param int y1: source y-coord - :param int x2: destination x-coord - :param int y2: destination y-coord - :param int src: source id - :param tkinter.Canvas canvas: canvas object - """ - self.src = src - self.dst = None - self.src_interface = None - self.dst_interface = None - self.canvas = canvas - if is_wired is None or is_wired is True: - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) - else: - self.id = self.canvas.create_line( - x1, - y1, - x2, - y2, - tags="edge", - width=self.width, - fill="#ff0000", - state=tk.HIDDEN, - ) - self.token = None - self.link_info = None - self.throughput = None - self.wired = is_wired - - 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) - self.canvas.helper.draw_wireless_case(self.src, self.dst, self) - self.canvas.tag_raise(self.src) - self.canvas.tag_raise(self.dst) - - def delete(self): - self.canvas.delete(self.id) - - -class CanvasNode: - def __init__(self, app, core_node, image): - self.app = app - self.canvas = app.canvas - self.image = image - self.core_node = core_node - x = self.core_node.position.x - y = self.core_node.position.y - self.id = self.canvas.create_image( - x, y, anchor=tk.CENTER, image=self.image, tags="node" - ) - image_box = self.canvas.bbox(self.id) - y = image_box[3] + NODE_TEXT_OFFSET - text_font = font.Font(family="TkIconFont", size=12) - self.text_id = self.canvas.create_text( - x, - y, - text=self.core_node.name, - tags="nodename", - font=text_font, - fill="#0000CD", - ) - self.antenna_draw = WlanAntennaManager(self.canvas, self.id) - self.tooltip = CanvasTooltip(self.canvas) - self.canvas.tag_bind(self.id, "", self.click_press) - self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) - self.canvas.tag_bind(self.id, "", self.double_click) - self.canvas.tag_bind(self.id, "", self.select_multiple) - self.canvas.tag_bind(self.id, "", self.on_enter) - self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() - self.interfaces = [] - self.wireless_edges = set() - self.moving = None - - def redraw(self): - self.canvas.itemconfig(self.id, image=self.image) - self.canvas.itemconfig(self.text_id, text=self.core_node.name) - - def move(self, x, y, update=True): - old_x = self.core_node.position.x - old_y = self.core_node.position.y - x_offset = x - old_x - y_offset = y - old_y - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - self.canvas.move(self.id, x_offset, y_offset) - self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) - self.canvas.canvas_management.node_drag(self, x_offset, y_offset) - for edge in self.edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) - edge.link_info.recalculate_info() - for edge in self.wireless_edges: - x1, y1, x2, y2 = self.canvas.coords(edge.id) - if edge.src == self.id: - self.canvas.coords(edge.id, x, y, x2, y2) - else: - self.canvas.coords(edge.id, x1, y1, x, y) - if self.app.core.is_runtime() and update: - self.app.core.edit_node(self.core_node.id, int(x), int(y)) - - def on_enter(self, event): - if self.app.core.is_runtime() and self.app.core.observer: - self.tooltip.text.set("waiting...") - self.tooltip.on_enter(event) - output = self.app.core.run(self.core_node.id) - self.tooltip.text.set(output) - - def on_leave(self, event): - self.tooltip.on_leave(event) - - def click(self, event): - print("click") - - def double_click(self, event): - if self.app.core.is_runtime(): - self.canvas.core.launch_terminal(self.core_node.id) - else: - self.show_config() - - def update_coords(self): - x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - - def click_press(self, event): - logging.debug(f"node click press {self.core_node.name}: {event}") - self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.canvas_management.selected: - self.canvas.canvas_management.node_select(self) - self.canvas.selected = self.id - - def click_release(self, event): - logging.debug(f"node click release {self.core_node.name}: {event}") - self.update_coords() - self.moving = None - - def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: - return - x, y = self.canvas.canvas_xy(event) - my_x = self.core_node.position.x - my_y = self.core_node.position.y - self.move(x, y) - # move other selected components - for nid, bboxid in self.canvas.canvas_management.selected.items(): - if nid != self.id and nid in self.canvas.nodes: - other_old_x = self.canvas.nodes[nid].core_node.position.x - other_old_y = self.canvas.nodes[nid].core_node.position.y - other_new_x = x + other_old_x - my_x - other_new_y = y + other_old_y - my_y - self.canvas.nodes[nid].move(other_new_x, other_new_y) - if nid != self.id and nid in self.canvas.shapes: - self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) - - def select_multiple(self, event): - self.canvas.canvas_management.node_select(self, True) - - def show_config(self): - self.canvas.context = None - dialog = NodeConfigDialog(self.app, self.app, self) - dialog.show() - - def show_wlan_config(self): - self.canvas.context = None - dialog = WlanConfigDialog(self.app, self.app, self) - dialog.show() - - def show_mobility_config(self): - self.canvas.context = None - dialog = MobilityConfigDialog(self.app, self.app, self) - dialog.show() - - def show_mobility_player(self): - self.canvas.context = None - mobility_player = self.app.core.mobility_players[self.core_node.id] - mobility_player.show() - - def show_emane_config(self): - self.canvas.context = None - dialog = EmaneConfigDialog(self.app, self.app, self) - dialog.show() diff --git a/coretk/coretk/graph_helper.py b/coretk/coretk/graph/graph_helper.py similarity index 100% rename from coretk/coretk/graph_helper.py rename to coretk/coretk/graph/graph_helper.py diff --git a/coretk/coretk/linkinfo.py b/coretk/coretk/graph/linkinfo.py similarity index 100% rename from coretk/coretk/linkinfo.py rename to coretk/coretk/graph/linkinfo.py diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py new file mode 100644 index 00000000..4f80f77d --- /dev/null +++ b/coretk/coretk/graph/node.py @@ -0,0 +1,163 @@ +import logging +import tkinter as tk +from tkinter import font + +from coretk.dialogs.emaneconfig import EmaneConfigDialog +from coretk.dialogs.mobilityconfig import MobilityConfigDialog +from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.graph.canvastooltip import CanvasTooltip +from coretk.graph.enums import GraphMode +from coretk.graph.graph_helper import WlanAntennaManager + +NODE_TEXT_OFFSET = 5 + + +class CanvasNode: + def __init__(self, app, core_node, image): + self.app = app + self.canvas = app.canvas + self.image = image + self.core_node = core_node + x = self.core_node.position.x + y = self.core_node.position.y + self.id = self.canvas.create_image( + x, y, anchor=tk.CENTER, image=self.image, tags="node" + ) + image_box = self.canvas.bbox(self.id) + y = image_box[3] + NODE_TEXT_OFFSET + text_font = font.Font(family="TkIconFont", size=12) + self.text_id = self.canvas.create_text( + x, + y, + text=self.core_node.name, + tags="nodename", + font=text_font, + fill="#0000CD", + ) + self.antenna_draw = WlanAntennaManager(self.canvas, self.id) + self.tooltip = CanvasTooltip(self.canvas) + self.canvas.tag_bind(self.id, "", self.click_press) + self.canvas.tag_bind(self.id, "", self.click_release) + self.canvas.tag_bind(self.id, "", self.motion) + self.canvas.tag_bind(self.id, "", self.double_click) + self.canvas.tag_bind(self.id, "", self.select_multiple) + self.canvas.tag_bind(self.id, "", self.on_enter) + self.canvas.tag_bind(self.id, "", self.on_leave) + self.edges = set() + self.interfaces = [] + self.wireless_edges = set() + self.moving = None + + def redraw(self): + self.canvas.itemconfig(self.id, image=self.image) + self.canvas.itemconfig(self.text_id, text=self.core_node.name) + + def move(self, x, y, update=True): + old_x = self.core_node.position.x + old_y = self.core_node.position.y + x_offset = x - old_x + y_offset = y - old_y + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move(self.text_id, x_offset, y_offset) + self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.canvas.canvas_management.node_drag(self, x_offset, y_offset) + for edge in self.edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + edge.link_info.recalculate_info() + for edge in self.wireless_edges: + x1, y1, x2, y2 = self.canvas.coords(edge.id) + if edge.src == self.id: + self.canvas.coords(edge.id, x, y, x2, y2) + else: + self.canvas.coords(edge.id, x1, y1, x, y) + if self.app.core.is_runtime() and update: + self.app.core.edit_node(self.core_node.id, int(x), int(y)) + + def on_enter(self, event): + if self.app.core.is_runtime() and self.app.core.observer: + self.tooltip.text.set("waiting...") + self.tooltip.on_enter(event) + output = self.app.core.run(self.core_node.id) + self.tooltip.text.set(output) + + def on_leave(self, event): + self.tooltip.on_leave(event) + + def click(self, event): + print("click") + + def double_click(self, event): + if self.app.core.is_runtime(): + self.canvas.core.launch_terminal(self.core_node.id) + else: + self.show_config() + + def update_coords(self): + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + + def click_press(self, event): + logging.debug(f"node click press {self.core_node.name}: {event}") + self.moving = self.canvas.canvas_xy(event) + if self.id not in self.canvas.canvas_management.selected: + self.canvas.canvas_management.node_select(self) + self.canvas.selected = self.id + + def click_release(self, event): + logging.debug(f"node click release {self.core_node.name}: {event}") + self.update_coords() + self.moving = None + + def motion(self, event): + if self.canvas.mode == GraphMode.EDGE: + return + x, y = self.canvas.canvas_xy(event) + my_x = self.core_node.position.x + my_y = self.core_node.position.y + self.move(x, y) + # move other selected components + for nid, bboxid in self.canvas.canvas_management.selected.items(): + if nid != self.id and nid in self.canvas.nodes: + other_old_x = self.canvas.nodes[nid].core_node.position.x + other_old_y = self.canvas.nodes[nid].core_node.position.y + other_new_x = x + other_old_x - my_x + other_new_y = y + other_old_y - my_y + self.canvas.nodes[nid].move(other_new_x, other_new_y) + if nid != self.id and nid in self.canvas.shapes: + self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) + + def select_multiple(self, event): + self.canvas.canvas_management.node_select(self, True) + + def show_config(self): + self.canvas.context = None + dialog = NodeConfigDialog(self.app, self.app, self) + dialog.show() + + def show_wlan_config(self): + self.canvas.context = None + dialog = WlanConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_config(self): + self.canvas.context = None + dialog = MobilityConfigDialog(self.app, self.app, self) + dialog.show() + + def show_mobility_player(self): + self.canvas.context = None + mobility_player = self.app.core.mobility_players[self.core_node.id] + mobility_player.show() + + def show_emane_config(self): + self.canvas.context = None + dialog = EmaneConfigDialog(self.app, self.app, self) + dialog.show() diff --git a/coretk/coretk/nodedelete.py b/coretk/coretk/graph/nodedelete.py similarity index 100% rename from coretk/coretk/nodedelete.py rename to coretk/coretk/graph/nodedelete.py diff --git a/coretk/coretk/shape.py b/coretk/coretk/graph/shape.py similarity index 100% rename from coretk/coretk/shape.py rename to coretk/coretk/graph/shape.py diff --git a/coretk/coretk/status.py b/coretk/coretk/statusbar.py similarity index 100% rename from coretk/coretk/status.py rename to coretk/coretk/statusbar.py diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index fb0560dc..5f528628 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,7 +5,7 @@ from functools import partial from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog -from coretk.graph import GraphMode +from coretk.graph.enums import GraphMode from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip From c82453b9814a08bfc6134571a7631614e7a95d40 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 11:15:51 -0800 Subject: [PATCH 206/305] load shapes --- coretk/coretk/coreclient.py | 103 +++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1a7d06e3..273e3037 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -7,12 +7,12 @@ import os import time from core.api.grpc import client, core_pb2 -from coretk import appconfig, parsedata +from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -from coretk.shape import Shape, ShapeData +from coretk.shape import ShapeData OBSERVERS = { "processes": "ps", @@ -282,47 +282,48 @@ class CoreClient: return self.state == core_pb2.SessionState.RUNTIME def parse_metadata(self, config): - for key, value in config.items(): - if "global_options" != key: - canvas_config = parsedata.parse(value) - print(canvas_config) - if canvas_config.get("type"): - config_type = canvas_config["type"] - if config_type == "rectangle" or config_type == "oval": - data = ShapeData( - False, - canvas_config["label"], - canvas_config["fontfamily"], - canvas_config["fontsize"], - canvas_config["labelcolor"], - canvas_config["color"], - canvas_config["border"], - canvas_config["width"], - ) - coords = tuple( - [float(x) for x in canvas_config["iconcoords"].split()] - ) - shape = Shape( - self.app, - self.app.canvas, - None, - None, - coords, - data, - config_type, - ) - self.app.canvas.shapes[shape.id] = shape - elif canvas_config["type"] == "text": - print("not implemented") - else: - if "wallpaper" in canvas_config: - logging.info("canvas metadata: %s", canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] - self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) + # for key, value in config.items(): + # if "global_options" != key: + # canvas_config = parsedata.parse(value) + # print(canvas_config) + # if canvas_config.get("type"): + # config_type = canvas_config["type"] + # if config_type == "rectangle" or config_type == "oval": + # data = ShapeData( + # False, + # canvas_config["label"], + # canvas_config["fontfamily"], + # canvas_config["fontsize"], + # canvas_config["labelcolor"], + # canvas_config["color"], + # canvas_config["border"], + # canvas_config["width"], + # ) + # coords = tuple( + # [float(x) for x in canvas_config["iconcoords"].split()] + # ) + # shape = Shape( + # self.app, + # self.app.canvas, + # None, + # None, + # coords, + # data, + # config_type, + # ) + # self.app.canvas.shapes[shape.id] = shape + # elif canvas_config["type"] == "text": + # print("not implemented") + # else: + # if "wallpaper" in canvas_config: + # logging.info("canvas metadata: %s", canvas_config) + # wallpaper_style = canvas_config["wallpaper-style"] + # self.app.canvas.scale_option.set(wallpaper_style) + # wallpaper = canvas_config["wallpaper"] + # wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + # self.app.canvas.set_wallpaper(wallpaper) # canvas settings + print(config) canvas_config = config.get("canvas") if canvas_config: logging.info("canvas metadata: %s", canvas_config) @@ -332,6 +333,24 @@ class CoreClient: wallpaper = canvas_config["wallpaper"] wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) self.app.canvas.set_wallpaper(wallpaper) + for key, annotation_config in config.items(): + if "annotation" in key: + annotation_config = json.loads(annotation_config) + print(annotation_config) + config_type = annotation_config["type"] + if config_type in ["rectangle", "oval"]: + coords = tuple(annotation_config["iconcoords"]) + data = ShapeData( + False, + annotation_config["label"], + annotation_config["fontfamily"], + annotation_config["fontsize"], + annotation_config["labelcolor"], + annotation_config["color"], + annotation_config["border"], + annotation_config["width"], + ) + print(data, coords) def create_new_session(self): """ From 7c8f9dac0f8e3c44611fd396857d897b4d09f2c8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:13:35 -0800 Subject: [PATCH 207/305] removed nodedelete module and added logic to the graph canvas itself --- coretk/coretk/graph/graph.py | 140 ++++++++++++++---- coretk/coretk/graph/node.py | 27 ++-- coretk/coretk/graph/nodedelete.py | 98 ------------ coretk/coretk/graph/shape.py | 3 +- .../graph/{canvastooltip.py => tooltip.py} | 0 5 files changed, 129 insertions(+), 139 deletions(-) delete mode 100644 coretk/coretk/graph/nodedelete.py rename coretk/coretk/graph/{canvastooltip.py => tooltip.py} (100%) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index d39ca012..8e18c157 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -11,7 +11,6 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.graph_helper import GraphHelper from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode -from coretk.graph.nodedelete import CanvasComponentManagement from coretk.graph.shape import Shape from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils @@ -28,6 +27,7 @@ class CanvasGraph(tk.Canvas): self.app = master self.mode = GraphMode.SELECT self.annotation_type = None + self.selection = {} self.selected = None self.node_draw = None self.context = None @@ -37,7 +37,6 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.canvas_management = CanvasComponentManagement(self, core) self.setup_bindings() self.draw_grid(width, height) self.core = core @@ -328,6 +327,90 @@ class CanvasGraph(tk.Canvas): edge.link_info = LinkInfo(self, edge, link) logging.debug(f"edges: {self.find_withtag('edge')}") + def select_object(self, object_id, choose_multiple=False): + """ + create a bounding box when a node is selected + """ + if not choose_multiple: + self.clear_selection() + + # draw a bounding box if node hasn't been selected yet + if object_id not in self.selection: + x0, y0, x1, y1 = self.bbox(object_id) + selection_id = self.create_rectangle( + (x0 - 6, y0 - 6, x1 + 6, y1 + 6), + activedash=True, + dash="-", + tags="selectednodes", + ) + self.selection[object_id] = selection_id + else: + selection_id = self.selection.pop(object_id) + self.delete(selection_id) + + def clear_selection(self): + """ + Clear current selection boxes. + + :return: nothing + """ + for _id in self.selection.values(): + self.delete(_id) + self.selection.clear() + + def object_drag(self, object_id, offset_x, offset_y): + select_id = self.selection.get(object_id) + if select_id is not None: + self.move(select_id, offset_x, offset_y) + + def delete_selection_objects(self): + edges = set() + nodes = [] + for object_id in self.selection: + if object_id in self.nodes: + selection_id = self.selection[object_id] + canvas_node = self.nodes.pop(object_id) + nodes.append(canvas_node) + self.delete(object_id) + self.delete(selection_id) + self.delete(canvas_node.text_id) + + # delete antennas + is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) + if is_wireless: + canvas_node.antenna_draw.delete_antennas() + + # delete related edges + for edge in canvas_node.edges: + if edge in edges: + continue + edges.add(edge) + self.edges.pop(edge.token) + self.delete(edge.id) + self.delete(edge.link_info.id1) + self.delete(edge.link_info.id2) + other_id = edge.src + other_interface = edge.src_interface + if edge.src == object_id: + other_id = edge.dst + other_interface = edge.dst_interface + other_node = self.nodes[other_id] + other_node.edges.remove(edge) + try: + other_node.interfaces.remove(other_interface) + except ValueError: + pass + if is_wireless: + other_node.antenna_draw.delete_antenna() + if object_id in self.shapes: + selection_id = self.selection[object_id] + self.shapes[object_id].delete() + self.delete(selection_id) + self.shapes.pop(object_id) + + self.selection.clear() + return nodes + def click_press(self, event): """ Start drawing an edge if mouse click is on a node @@ -337,7 +420,7 @@ class CanvasGraph(tk.Canvas): """ logging.debug(f"click press: {event}") selected = self.get_selected(event) - is_node = selected in self.find_withtag("node") + is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) @@ -353,27 +436,26 @@ class CanvasGraph(tk.Canvas): self.shape_drawing = True if self.mode == GraphMode.SELECT: if selected is not None: - if "shape" in self.gettags(selected): + if selected in self.shapes: x, y = self.canvas_xy(event) - self.shapes[selected].cursor_x = x - self.shapes[selected].cursor_y = y - if selected not in self.canvas_management.selected: - self.canvas_management.node_select(self.shapes[selected]) + shape = self.shapes[selected] + shape.cursor_x = x + shape.cursor_y = y + if selected not in self.selection: + self.select_object(shape.id) self.selected = selected else: - for i in self.find_withtag("selectednodes"): - self.delete(i) - self.canvas_management.selected.clear() + self.clear_selection() def ctrl_click(self, event): - logging.debug("Control left click %s", event) + logging.debug("control left click: %s", event) selected = self.get_selected(event) if ( self.mode == GraphMode.SELECT and selected is not None - and "shape" in self.gettags(selected) + and selected in self.shapes ): - self.canvas_management.node_select(self.shapes[selected], True) + self.select_object(selected, choose_multiple=True) def click_motion(self, event): """ @@ -396,21 +478,24 @@ class CanvasGraph(tk.Canvas): if ( self.mode == GraphMode.SELECT and self.selected is not None - and "shape" in self.gettags(self.selected) + and self.selected in self.shapes ): x, y = self.canvas_xy(event) shape = self.shapes[self.selected] delta_x = x - shape.cursor_x delta_y = y - shape.cursor_y shape.motion(event) + # move other selected components - for nid in self.canvas_management.selected: - if nid != self.selected and nid in self.shapes: - self.shapes[nid].motion(None, delta_x, delta_y) - if nid != self.selected and nid in self.nodes: - node_x = self.nodes[nid].core_node.position.x - node_y = self.nodes[nid].core_node.position.y - self.nodes[nid].move(node_x + delta_x, node_y + delta_y) + for _id in self.selection: + if _id != self.selected and _id in self.shapes: + shape = self.shapes[_id] + shape.motion(None, delta_x, delta_y) + if _id != self.selected and _id in self.nodes: + node = self.nodes[_id] + node_x = node.core_node.position.x + node_y = node.core_node.position.y + node.move(node_x + delta_x, node_y + delta_y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -432,17 +517,18 @@ class CanvasGraph(tk.Canvas): :return: """ logging.debug("press delete key") - nodes = self.canvas_management.delete_selected_nodes() + nodes = self.delete_selection_objects() self.core.delete_graph_nodes(nodes) def double_click(self, event): selected = self.get_selected(event) - if selected is not None and "shape" in self.gettags(selected): - s = ShapeDialog(self.app, self.app, self.shapes[selected]) - s.show() + if selected is not None and selected in self.shapes: + shape = self.shapes[selected] + dialog = ShapeDialog(self.app, self.app, shape) + dialog.show() def add_node(self, x, y): - if self.selected is None or "shape" in self.gettags(self.selected): + if self.selected is None or self.selected in self.shapes: core_node = self.core.create_node( int(x), int(y), self.node_draw.node_type, self.node_draw.model ) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 4f80f77d..a4e4a539 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -6,9 +6,9 @@ from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog -from coretk.graph.canvastooltip import CanvasTooltip from coretk.graph.enums import GraphMode from coretk.graph.graph_helper import WlanAntennaManager +from coretk.graph.tooltip import CanvasTooltip NODE_TEXT_OFFSET = 5 @@ -63,7 +63,7 @@ class CanvasNode: self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) self.antenna_draw.update_antennas_position(x_offset, y_offset) - self.canvas.canvas_management.node_drag(self, x_offset, y_offset) + self.canvas.object_drag(self.id, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -107,8 +107,8 @@ class CanvasNode: def click_press(self, event): logging.debug(f"node click press {self.core_node.name}: {event}") self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.canvas_management.selected: - self.canvas.canvas_management.node_select(self) + if self.id not in self.canvas.selection: + self.canvas.select_object(self.id) self.canvas.selected = self.id def click_release(self, event): @@ -123,19 +123,22 @@ class CanvasNode: my_x = self.core_node.position.x my_y = self.core_node.position.y self.move(x, y) + # move other selected components - for nid, bboxid in self.canvas.canvas_management.selected.items(): - if nid != self.id and nid in self.canvas.nodes: - other_old_x = self.canvas.nodes[nid].core_node.position.x - other_old_y = self.canvas.nodes[nid].core_node.position.y + for object_id, selection_id in self.canvas.selection.items(): + if object_id != self.id and object_id in self.canvas.nodes: + canvas_node = self.canvas.nodes[object_id] + other_old_x = canvas_node.core_node.position.x + other_old_y = canvas_node.core_node.position.y other_new_x = x + other_old_x - my_x other_new_y = y + other_old_y - my_y - self.canvas.nodes[nid].move(other_new_x, other_new_y) - if nid != self.id and nid in self.canvas.shapes: - self.canvas.shapes[nid].motion(None, x - my_x, y - my_y) + self.canvas.nodes[object_id].move(other_new_x, other_new_y) + elif object_id in self.canvas.shapes: + shape = self.canvas.shapes[object_id] + shape.motion(None, x - my_x, y - my_y) def select_multiple(self, event): - self.canvas.canvas_management.node_select(self, True) + self.canvas.select_object(self.id, choose_multiple=True) def show_config(self): self.canvas.context = None diff --git a/coretk/coretk/graph/nodedelete.py b/coretk/coretk/graph/nodedelete.py deleted file mode 100644 index 1365dc0f..00000000 --- a/coretk/coretk/graph/nodedelete.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -manage deletion -""" -from coretk.nodeutils import NodeUtils - - -class CanvasComponentManagement: - def __init__(self, canvas, core): - self.app = core - self.canvas = canvas - - # dictionary that maps node to box - self.selected = {} - - def node_select(self, canvas_node, choose_multiple=False): - """ - create a bounding box when a node is selected - - :param coretk.graph.CanvasNode canvas_node: canvas node object - :return: nothing - """ - - if not choose_multiple: - self.delete_current_bbox() - - # draw a bounding box if node hasn't been selected yet - if canvas_node.id not in self.selected: - x0, y0, x1, y1 = self.canvas.bbox(canvas_node.id) - bbox_id = self.canvas.create_rectangle( - (x0 - 6, y0 - 6, x1 + 6, y1 + 6), - activedash=True, - dash="-", - tags="selectednodes", - ) - self.selected[canvas_node.id] = bbox_id - else: - bbox_id = self.selected.pop(canvas_node.id) - self.canvas.delete(bbox_id) - - def node_drag(self, canvas_node, offset_x, offset_y): - select_id = self.selected.get(canvas_node.id) - if select_id is not None: - self.canvas.move(select_id, offset_x, offset_y) - - def delete_current_bbox(self): - for bbid in self.selected.values(): - self.canvas.delete(bbid) - self.selected.clear() - - def delete_selected_nodes(self): - edges = set() - nodes = [] - for node_id in self.selected: - if "node" in self.canvas.gettags(node_id): - bbox_id = self.selected[node_id] - canvas_node = self.canvas.nodes.pop(node_id) - nodes.append(canvas_node) - self.canvas.delete(node_id) - self.canvas.delete(bbox_id) - self.canvas.delete(canvas_node.text_id) - - # delete antennas - is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() - - # delete related edges - for edge in canvas_node.edges: - if edge in edges: - continue - edges.add(edge) - self.canvas.edges.pop(edge.token) - self.canvas.delete(edge.id) - self.canvas.delete(edge.link_info.id1) - self.canvas.delete(edge.link_info.id2) - other_id = edge.src - other_interface = edge.src_interface - if edge.src == node_id: - other_id = edge.dst - other_interface = edge.dst_interface - other_node = self.canvas.nodes[other_id] - other_node.edges.remove(edge) - try: - other_node.interfaces.remove(other_interface) - except ValueError: - pass - if is_wireless: - other_node.antenna_draw.delete_antenna() - - for shape_id in self.selected: - if "shape" in self.canvas.gettags(shape_id): - bbox_id = self.selected[node_id] - self.canvas.shapes[shape_id].delete() - self.canvas.delete(bbox_id) - self.canvas.shapes.pop(shape_id) - - self.selected.clear() - return nodes diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 63025b34..4940374e 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -127,14 +127,13 @@ class Shape: logging.debug("Click release on shape %s", self.id) def motion(self, event, delta_x=None, delta_y=None): - logging.debug("motion on shape %s", self.id) if event is not None: delta_x = event.x - self.cursor_x delta_y = event.y - self.cursor_y self.cursor_x = event.x self.cursor_y = event.y self.canvas.move(self.id, delta_x, delta_y) - self.canvas.canvas_management.node_drag(self, delta_x, delta_y) + self.canvas.object_drag(self.id, delta_x, delta_y) if self.text_id is not None: self.canvas.move(self.text_id, delta_x, delta_y) diff --git a/coretk/coretk/graph/canvastooltip.py b/coretk/coretk/graph/tooltip.py similarity index 100% rename from coretk/coretk/graph/canvastooltip.py rename to coretk/coretk/graph/tooltip.py From 6425b08878f28a62fa43eace2b259e077d4b1932 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:17:12 -0800 Subject: [PATCH 208/305] updated graph canvas width and height to use the id it already knows of --- coretk/coretk/graph/graph.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 8e18c157..95ad19dc 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -543,8 +543,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - grid = self.find_withtag("rectangle")[0] - x0, y0, x1, y1 = self.coords(grid) + x0, y0, x1, y1 = self.coords(self.grid) canvas_w = abs(x0 - x1) canvas_h = abs(y0 - y1) return canvas_w, canvas_h From 7e2ebb4a2c845e6d61d0f9bc1c599391659535b6 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:43:12 -0800 Subject: [PATCH 209/305] delete some extra print statement --- coretk/coretk/coreclient.py | 3 --- coretk/coretk/graph/shape.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 7a760c1f..05a36c82 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -309,7 +309,6 @@ class CoreClient: for key, annotation_config in config.items(): if "annotation" in key: annotation_config = json.loads(annotation_config) - print(annotation_config) config_type = annotation_config["type"] if config_type in ["rectangle", "oval"]: coords = tuple(annotation_config["iconcoords"]) @@ -508,7 +507,6 @@ class CoreClient: """ node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - print(node_protos) if self.get_session_state() != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -535,7 +533,6 @@ class CoreClient: self.created_links.add( tuple([link_proto.node_one_id, link_proto.node_two_id]) ) - print(self.app.core.client.get_session(self.app.core.session_id)) def close(self): """ diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index dd83ee3e..91020ecb 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -106,8 +106,6 @@ class Shape: ) _x = (x0 + x1) / 2 _y = y0 + 1.5 * data.font_size - print("create text with text: ", data.text) - print(data.text_color) self.text_id = self.canvas.create_text( _x, _y, tags="shapetext", text=data.text, fill=data.text_color ) From 81eeac9ec62a6c755be066a24dabf2427416943f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:37:48 -0800 Subject: [PATCH 210/305] merged drawing antenna code to graph node class --- coretk/coretk/graph/edges.py | 57 +++++++---- coretk/coretk/graph/graph.py | 61 ++++++------ coretk/coretk/graph/graph_helper.py | 144 ---------------------------- coretk/coretk/graph/node.py | 49 +++++++++- coretk/coretk/nodeutils.py | 3 + 5 files changed, 120 insertions(+), 194 deletions(-) delete mode 100644 coretk/coretk/graph/graph_helper.py diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 92f99400..d62a309b 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,7 @@ import tkinter as tk +from coretk.nodeutils import NodeUtils + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -22,7 +24,7 @@ class CanvasEdge: width = 1.4 - def __init__(self, x1, y1, x2, y2, src, canvas, is_wired=None): + def __init__(self, x1, y1, x2, y2, src, canvas): """ Create an instance of canvas edge object :param int x1: source x-coord @@ -30,41 +32,56 @@ class CanvasEdge: :param int x2: destination x-coord :param int y2: destination y-coord :param int src: source id - :param tkinter.Canvas canvas: canvas object + :param coretk.graph.graph.GraphCanvas canvas: canvas object """ self.src = src self.dst = None self.src_interface = None self.dst_interface = None self.canvas = canvas - if is_wired is None or is_wired is True: - self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" - ) - else: - self.id = self.canvas.create_line( - x1, - y1, - x2, - y2, - tags="edge", - width=self.width, - fill="#ff0000", - state=tk.HIDDEN, - ) + self.id = self.canvas.create_line( + x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + ) self.token = None self.link_info = None self.throughput = None - self.wired = is_wired - def complete(self, dst, x, y): + def complete(self, dst): self.dst = dst self.token = tuple(sorted((self.src, self.dst))) + x, y = self.canvas.coords(self.dst) x1, y1, _, _ = self.canvas.coords(self.id) self.canvas.coords(self.id, x1, y1, x, y) - self.canvas.helper.draw_wireless_case(self.src, self.dst, self) + self.check_wireless() self.canvas.tag_raise(self.src) self.canvas.tag_raise(self.dst) + def check_wireless(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + self.canvas.itemconfig(self.id, state=tk.HIDDEN) + self._check_antenna() + + def _check_antenna(self): + src_node = self.canvas.nodes[self.src] + dst_node = self.canvas.nodes[self.dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + if is_src_wireless or is_dst_wireless: + if is_src_wireless and not is_dst_wireless: + dst_node.add_antenna() + elif not is_src_wireless and is_dst_wireless: + src_node.add_antenna() + # TODO: remove this? dont allow linking wireless nodes? + else: + src_node.add_antenna() + def delete(self): self.canvas.delete(self.id) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 95ad19dc..ad6788c5 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -8,7 +8,6 @@ from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption -from coretk.graph.graph_helper import GraphHelper from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape @@ -16,6 +15,18 @@ from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] +CANVAS_COMPONENT_TAGS = [ + "edge", + "node", + "nodename", + "wallpaper", + "linkinfo", + "antenna", + "wireless", + "selectednodes", + "shape", + "shapetext", +] class CanvasGraph(tk.Canvas): @@ -40,7 +51,6 @@ class CanvasGraph(tk.Canvas): self.setup_bindings() self.draw_grid(width, height) self.core = core - self.helper = GraphHelper(self, core) self.throughput_draw = Throughput(self, core) self.shape_drawing = False @@ -96,7 +106,8 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # delete any existing drawn items - self.helper.delete_canvas_components() + for tag in CANVAS_COMPONENT_TAGS: + self.delete(tag) # set the private variables to default value self.mode = GraphMode.SELECT @@ -195,9 +206,6 @@ class CanvasGraph(tk.Canvas): if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: - is_node_one_wireless = NodeUtils.is_wireless_node(node_one.type) - is_node_two_wireless = NodeUtils.is_wireless_node(node_two.type) - has_no_wireless = not (is_node_one_wireless or is_node_two_wireless) edge = CanvasEdge( node_one.position.x, node_one.position.y, @@ -205,15 +213,14 @@ class CanvasGraph(tk.Canvas): node_two.position.y, canvas_node_one.id, self, - is_wired=has_no_wireless, ) edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) edge.dst = canvas_node_two.id + edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge self.core.links[edge.token] = link - self.helper.redraw_antenna(canvas_node_one, canvas_node_two) edge.link_info = LinkInfo(self, edge, link) if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) @@ -311,21 +318,23 @@ class CanvasGraph(tk.Canvas): edge.delete() return - # set dst node and snap edge to center - x, y = self.coords(self.selected) - edge.complete(self.selected, x, y) - logging.debug(f"drawing edge token: {edge.token}") - if edge.token in self.edges: + # ignore repeated edges + token = tuple(sorted((edge.src, self.selected))) + if 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) - link = self.core.create_link(edge, node_src, node_dst) - edge.link_info = LinkInfo(self, edge, link) - logging.debug(f"edges: {self.find_withtag('edge')}") + return + + # set dst node and snap edge to center + edge.complete(self.selected) + logging.debug("drawing edge token: %s", edge.token) + + 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) + link = self.core.create_link(edge, node_src, node_dst) + edge.link_info = LinkInfo(self, edge, link) def select_object(self, object_id, choose_multiple=False): """ @@ -374,11 +383,8 @@ class CanvasGraph(tk.Canvas): self.delete(object_id) self.delete(selection_id) self.delete(canvas_node.text_id) - - # delete antennas + canvas_node.delete_antennae() is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) - if is_wireless: - canvas_node.antenna_draw.delete_antennas() # delete related edges for edge in canvas_node.edges: @@ -401,7 +407,7 @@ class CanvasGraph(tk.Canvas): except ValueError: pass if is_wireless: - other_node.antenna_draw.delete_antenna() + other_node.delete_antenna() if object_id in self.shapes: selection_id = self.selection[object_id] self.shapes[object_id].delete() @@ -424,6 +430,7 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) + self.tag_raise(selected) if ( self.mode == GraphMode.ANNOTATION and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] diff --git a/coretk/coretk/graph/graph_helper.py b/coretk/coretk/graph/graph_helper.py deleted file mode 100644 index af6d4823..00000000 --- a/coretk/coretk/graph/graph_helper.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Some graph helper functions -""" -import logging -import tkinter as tk - -from coretk.images import ImageEnum, Images -from coretk.nodeutils import NodeUtils - -CANVAS_COMPONENT_TAGS = [ - "edge", - "node", - "nodename", - "wallpaper", - "linkinfo", - "antenna", - "wireless", - "selectednodes", - "shape", - "shapetext", -] - - -class GraphHelper: - def __init__(self, canvas, core): - """ - create an instance of GraphHelper object - """ - self.canvas = canvas - self.core = core - - def delete_canvas_components(self): - """ - delete the components of the graph leaving only the blank canvas - - :return: nothing - """ - for tag in CANVAS_COMPONENT_TAGS: - for i in self.canvas.find_withtag(tag): - self.canvas.delete(i) - - def draw_wireless_case(self, src_id, dst_id, edge): - src_node_type = self.canvas.nodes[src_id].core_node.type - dst_node_type = self.canvas.nodes[dst_id].core_node.type - is_src_wireless = NodeUtils.is_wireless_node(src_node_type) - is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - if is_src_wireless or is_dst_wireless: - self.canvas.itemconfig(edge.id, state=tk.HIDDEN) - edge.wired = False - if edge.token not in self.canvas.edges: - if is_src_wireless and is_dst_wireless: - self.canvas.nodes[src_id].antenna_draw.add_antenna() - elif is_src_wireless: - self.canvas.nodes[dst_id].antenna_draw.add_antenna() - else: - self.canvas.nodes[src_id].antenna_draw.add_antenna() - edge.wired = True - - def redraw_antenna(self, node_one, node_two): - is_node_one_wireless = NodeUtils.is_wireless_node(node_one.core_node.type) - is_node_two_wireless = NodeUtils.is_wireless_node(node_two.core_node.type) - if is_node_one_wireless or is_node_two_wireless: - if is_node_one_wireless and not is_node_two_wireless: - node_two.antenna_draw.add_antenna() - elif not is_node_one_wireless and is_node_two_wireless: - node_one.antenna_draw.add_antenna() - else: - logging.error("bad link between two wireless nodes") - - def update_wlan_connection(self, old_x, old_y, new_x, new_y, edge_ids): - for eid in edge_ids: - x1, y1, x2, y2 = self.canvas.coords(eid) - if x1 == old_x and y1 == old_y: - self.canvas.coords(eid, new_x, new_y, x2, y2) - else: - self.canvas.coords(eid, x1, y1, new_x, new_y) - - -class WlanAntennaManager: - def __init__(self, canvas, node_id): - """ - crate an instance for AntennaManager - """ - self.canvas = canvas - self.node_id = node_id - self.quantity = 0 - self._max = 5 - self.antennas = [] - self.image = Images.get(ImageEnum.ANTENNA, 32) - - # distance between each antenna - self.offset = 0 - - def add_antenna(self): - """ - add an antenna to a node - - :return: nothing - """ - if self.quantity < 5: - x, y = self.canvas.coords(self.node_id) - aid = self.canvas.create_image( - x - 16 + self.offset, - y - 23, - anchor=tk.CENTER, - image=self.image, - tags="antenna", - ) - # self.canvas.tag_raise("antenna") - self.antennas.append(aid) - self.quantity = self.quantity + 1 - self.offset = self.offset + 8 - - def delete_antenna(self): - """ - delete one antenna - - :return: nothing - """ - if len(self.antennas) > 0: - self.canvas.delete(self.antennas.pop()) - self.quantity -= 1 - self.offset -= 8 - - def delete_antennas(self): - """ - delete all antennas - - :return: nothing - """ - for aid in self.antennas: - self.canvas.delete(aid) - self.antennas.clear() - self.quantity = 0 - self.offset = 0 - - def update_antennas_position(self, offset_x, offset_y): - """ - redraw antennas of a node according to the new node position - - :return: nothing - """ - for i in self.antennas: - self.canvas.move(i, offset_x, offset_y) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index a4e4a539..2d34780e 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -7,8 +7,8 @@ from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.graph.enums import GraphMode -from coretk.graph.graph_helper import WlanAntennaManager from coretk.graph.tooltip import CanvasTooltip +from coretk.nodeutils import NodeUtils NODE_TEXT_OFFSET = 5 @@ -35,7 +35,6 @@ class CanvasNode: font=text_font, fill="#0000CD", ) - self.antenna_draw = WlanAntennaManager(self.canvas, self.id) self.tooltip = CanvasTooltip(self.canvas) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) @@ -48,6 +47,50 @@ class CanvasNode: self.interfaces = [] self.wireless_edges = set() self.moving = None + self.antennae = [] + + def add_antenna(self): + x, y = self.canvas.coords(self.id) + offset = len(self.antennae) * 8 + + antenna_id = self.canvas.create_image( + x - 16 + offset, + y - 23, + anchor=tk.CENTER, + image=NodeUtils.ANTENNA_ICON, + tags="antenna", + ) + self.antennae.append(antenna_id) + + def delete_antenna(self): + """ + delete one antenna + + :return: nothing + """ + if self.antennae: + antenna_id = self.antennae.pop() + self.canvas.delete(antenna_id) + + def delete_antennae(self): + """ + delete all antennas + + :return: nothing + """ + logging.info("deleting antennae: %s", self.antennae) + for antenna_id in self.antennae: + self.canvas.delete(antenna_id) + self.antennae.clear() + + def move_antennae(self, x_offset, y_offset): + """ + redraw antennas of a node according to the new node position + + :return: nothing + """ + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) def redraw(self): self.canvas.itemconfig(self.id, image=self.image) @@ -62,7 +105,7 @@ class CanvasNode: self.core_node.position.y = int(y) self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) - self.antenna_draw.update_antennas_position(x_offset, y_offset) + self.move_antennae(x_offset, y_offset) self.canvas.object_drag(self.id, x_offset, y_offset) for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 6d880126..10d926f7 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -2,6 +2,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.images import ImageEnum, Images ICON_SIZE = 48 +ANTENNA_SIZE = 32 class NodeDraw: @@ -49,6 +50,7 @@ class NodeUtils: WIRELESS_NODES = {NodeType.WIRELESS_LAN, NodeType.EMANE} IGNORE_NODES = {NodeType.CONTROL_NET, NodeType.PEER_TO_PEER} NODE_MODELS = {"router", "host", "PC", "mdr", "prouter"} + ANTENNA_ICON = None @classmethod def is_ignore_node(cls, node_type): @@ -104,3 +106,4 @@ class NodeUtils: node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) cls.NETWORK_NODES.append(node_draw) cls.NODE_ICONS[(node_type, None)] = node_draw.image + cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) From 6077e81bf4a1def0205b88edb0ba59917d2a0eb5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 5 Dec 2019 17:01:48 -0800 Subject: [PATCH 211/305] moved node/edge delete logic into their own classes for helping make them managing their own data a bit easier --- coretk/coretk/graph/edges.py | 3 +++ coretk/coretk/graph/graph.py | 27 ++++++++++++++------------- coretk/coretk/graph/node.py | 6 +++++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index d62a309b..27d1e3ba 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -85,3 +85,6 @@ class CanvasEdge: def delete(self): self.canvas.delete(self.id) + if self.link_info: + self.canvas.delete(self.link_info.id1) + self.canvas.delete(self.link_info.id2) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index ad6788c5..abf3f952 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -376,14 +376,15 @@ class CanvasGraph(tk.Canvas): edges = set() nodes = [] for object_id in self.selection: + # delete selection box + selection_id = self.selection[object_id] + self.delete(selection_id) + + # delete node and related edges if object_id in self.nodes: - selection_id = self.selection[object_id] canvas_node = self.nodes.pop(object_id) + canvas_node.delete() nodes.append(canvas_node) - self.delete(object_id) - self.delete(selection_id) - self.delete(canvas_node.text_id) - canvas_node.delete_antennae() is_wireless = NodeUtils.is_wireless_node(canvas_node.core_node.type) # delete related edges @@ -391,10 +392,10 @@ class CanvasGraph(tk.Canvas): if edge in edges: continue edges.add(edge) - self.edges.pop(edge.token) - self.delete(edge.id) - self.delete(edge.link_info.id1) - self.delete(edge.link_info.id2) + del self.edges[edge.token] + edge.delete() + + # update node connected to edge being deleted other_id = edge.src other_interface = edge.src_interface if edge.src == object_id: @@ -408,11 +409,11 @@ class CanvasGraph(tk.Canvas): pass if is_wireless: other_node.delete_antenna() + + # delete shape if object_id in self.shapes: - selection_id = self.selection[object_id] - self.shapes[object_id].delete() - self.delete(selection_id) - self.shapes.pop(object_id) + shape = self.shapes.pop(object_id) + shape.delete() self.selection.clear() return nodes diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 2d34780e..0a46fd77 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -49,6 +49,11 @@ class CanvasNode: self.moving = None self.antennae = [] + def delete(self): + self.canvas.delete(self.id) + self.canvas.delete(self.text_id) + self.delete_antennae() + def add_antenna(self): x, y = self.canvas.coords(self.id) offset = len(self.antennae) * 8 @@ -78,7 +83,6 @@ class CanvasNode: :return: nothing """ - logging.info("deleting antennae: %s", self.antennae) for antenna_id in self.antennae: self.canvas.delete(antenna_id) self.antennae.clear() From 4a34aaa30d72249d6ba65182e67c876597d077f9 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:03:21 -0800 Subject: [PATCH 212/305] working on shapes and texts --- coretk/coretk/coreclient.py | 10 +- coretk/coretk/dialogs/shapemod.py | 225 ++++++++++++++++++++---------- coretk/coretk/dialogs/textmod.py | 26 ++++ coretk/coretk/graph/graph.py | 26 ++-- coretk/coretk/graph/shape.py | 58 +++----- 5 files changed, 222 insertions(+), 123 deletions(-) create mode 100644 coretk/coretk/dialogs/textmod.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 05a36c82..efbc6a39 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -10,7 +10,7 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog -from coretk.graph.shape import Shape, ShapeData +from coretk.graph.shape import AnnotationData, Shape from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -136,6 +136,7 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): + print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -160,6 +161,8 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) + elif event.HasField("throughput_event"): + print("throughput") else: logging.info("unhandled event: %s", event) @@ -189,7 +192,7 @@ class CoreClient: interface_throughputs = event.interface_throughputs for i in interface_throughputs: print("") - return + # return throughputs_belong_to_session = [] for if_tp in interface_throughputs: if if_tp.node_id in self.node_ids: @@ -312,8 +315,7 @@ class CoreClient: config_type = annotation_config["type"] if config_type in ["rectangle", "oval"]: coords = tuple(annotation_config["iconcoords"]) - data = ShapeData( - False, + data = AnnotationData( annotation_config["label"], annotation_config["fontfamily"], annotation_config["fontsize"], diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index ebeb7139..4b507dfa 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -12,9 +13,17 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): def __init__(self, master, app, shape): - super().__init__(master, app, "Add a new shape", modal=True) + self.annotation_type = app.canvas.annotation_type self.canvas = app.canvas - self.id = shape.id + if self.is_shape(): + super().__init__(master, app, "Add a new shape", modal=True) + self.id = shape.id + self.fill = None + self.border = None + else: + super().__init__(master, app, "Add a new text", modal=True) + + self.shape = shape data = shape.shape_data self.shape_text = tk.StringVar(value=data.text) self.font = tk.StringVar(value=data.font) @@ -26,11 +35,18 @@ class ShapeDialog(Dialog): self.bold = tk.IntVar(value=data.bold) self.italic = tk.IntVar(value=data.italic) self.underline = tk.IntVar(value=data.underline) - self.fill = None - self.border = None self.top.columnconfigure(0, weight=1) self.draw() + def is_shape(self): + return ( + self.annotation_type == ImageEnum.OVAL + or self.annotation_type == ImageEnum.RECTANGLE + ) + + def is_text(self): + return self.annotation_type == ImageEnum.TEXT + def draw(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) @@ -69,47 +85,53 @@ class ShapeDialog(Dialog): button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Fill color") - label.grid(row=0, column=0, sticky="nsew") - self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) - self.fill.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_fill_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + if self.is_shape(): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Fill color") + label.grid(row=0, column=0, sticky="nsew") + self.fill = ttk.Label( + frame, text=self.fill_color, background=self.fill_color + ) + self.fill.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + label = ttk.Label(frame, text="Border color:") + label.grid(row=0, column=0, sticky="nsew") + self.border = ttk.Label( + frame, text=self.border_color, background=self.fill_color + ) + self.border.grid(row=0, column=1, sticky="nsew", padx=3) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=0, column=2, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=2) + label = ttk.Label(frame, text="Border width:") + label.grid(row=0, column=0, sticky="nsew") + combobox = ttk.Combobox( + frame, + textvariable=self.border_width, + values=BORDER_WIDTH, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="nsew") + frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Border color:") - label.grid(row=0, column=0, sticky="nsew") - self.border = ttk.Label( - frame, text=self.border_color, background=self.fill_color - ) - self.border.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_border_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) - - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Border width:") - label.grid(row=0, column=0, sticky="nsew") - combobox = ttk.Combobox( - frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" - ) - combobox.grid(row=0, column=1, sticky="nsew") - frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) - - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Add shape", command=self.add_shape) + button = ttk.Button(frame, text="Add shape", command=self.click_add) button.grid(row=0, column=0, sticky="e", padx=3) button = ttk.Button(frame, text="Cancel", command=self.cancel) button.grid(row=0, column=1, sticky="w", pady=3) @@ -130,11 +152,86 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if not self.canvas.shapes[self.id].created: + if self.is_shape() and not self.canvas.shapes[self.id].created: self.canvas.delete(self.id) self.canvas.shapes.pop(self.id) self.destroy() + def click_add(self): + if self.is_shape(): + self.add_shape() + elif self.is_text(): + self.add_text() + self.destroy() + + def make_font(self): + """ + create font for text or shape label + :return: list(font specifications) + """ + size = int(self.font_size.get()) + text_font = [self.font.get(), size] + if self.bold.get() == 1: + text_font.append("bold") + if self.italic.get() == 1: + text_font.append("italic") + if self.underline.get() == 1: + text_font.append("underline") + return text_font + + def save_text(self): + """ + save info related to text or shape label + + :return: nothing + """ + data = self.shape.shape_data + data.text = self.shape_text.get() + data.font = self.font.get() + data.font_size = int(self.font_size.get()) + data.text_color = self.text_color + data.bold = self.bold.get() + data.italic = self.italic.get() + data.underline = self.underline.get() + + def save_shape(self): + """ + save info related to shape + + :return: nothing + """ + data = self.shape.shape_data + data.fill_color = self.fill_color + data.border_color = self.border_color + data.border_width = int(self.border_width.get()) + + def add_text(self): + """ + add text to canvas + + :return: nothing + """ + text = self.shape_text.get() + x = self.shape.x0 + y = self.shape.y0 + text_font = self.make_font() + if self.shape.text_id is None: + tid = self.canvas.create_text( + x, y, text=text, fill=self.text_color, font=text_font, tags="text" + ) + self.shape.text_id = tid + self.id = tid + self.shape.id = tid + self.canvas.texts[tid] = self.shape + self.shape.created = True + self.save_text() + print(self.canvas.texts) + # self.canvas.shapes[self.id].created = True + # else: + # self.canvas.itemconfig( + # self.shape.text_id, text=text, fill=self.text_color, font=f + # ) + def add_shape(self): self.canvas.itemconfig( self.id, @@ -143,42 +240,28 @@ class ShapeDialog(Dialog): outline=self.border_color, width=int(self.border_width.get()), ) - shape = self.canvas.shapes[self.id] shape_text = self.shape_text.get() size = int(self.font_size.get()) x0, y0, x1, y1 = self.canvas.bbox(self.id) - text_y = y0 + 1.5 * size - text_x = (x0 + x1) / 2 - f = [self.font.get(), size] - if self.bold.get() == 1: - f.append("bold") - if self.italic.get() == 1: - f.append("italic") - if self.underline.get() == 1: - f.append("underline") - if shape.text_id is None: - shape.text_id = self.canvas.create_text( - text_x, - text_y, + _y = y0 + 1.5 * size + _x = (x0 + x1) / 2 + text_font = self.make_font() + if self.shape.text_id is None: + self.shape.text_id = self.canvas.create_text( + _x, + _y, text=shape_text, fill=self.text_color, - font=f, + font=text_font, tags="shapetext", ) - self.canvas.shapes[self.id].created = True + self.shape.created = True else: self.canvas.itemconfig( - shape.text_id, text=shape_text, fill=self.text_color, font=f + self.shape.text_id, + text=shape_text, + fill=self.text_color, + font=text_font, ) - data = self.canvas.shapes[self.id].shape_data - data.text = shape_text - data.font = self.font.get() - data.font_size = int(self.font_size.get()) - data.text_color = self.text_color - data.fill_color = self.fill_color - data.border_color = self.border_color - data.border_width = int(self.border_width.get()) - data.bold = self.bold.get() - data.italic = self.italic.get() - data.underline = self.underline.get() - self.destroy() + self.save_text() + self.save_shape() diff --git a/coretk/coretk/dialogs/textmod.py b/coretk/coretk/dialogs/textmod.py new file mode 100644 index 00000000..03af38fb --- /dev/null +++ b/coretk/coretk/dialogs/textmod.py @@ -0,0 +1,26 @@ +""" +text dialog +""" +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class TextDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "Add a new text", modal=True) + self.canvas = app.canvas + self.text = tk.StringVar(value="") + + self.draw() + + def draw(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + label = ttk.Label(frame, text="Text for top of text: ") + label.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.text) + entry.grid(row=0, column=1) + frame.grid(row=0, column=0, sticky="nsew") diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 95ad19dc..688e89da 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -34,6 +34,7 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.shapes = {} + self.texts = {} self.wireless_edges = {} self.drawing_edge = None self.grid = None @@ -276,6 +277,8 @@ class CanvasGraph(tk.Canvas): if self.shape_drawing: self.shapes[self.selected].shape_complete(x, y) self.shape_drawing = False + elif self.annotation_type == ImageEnum.TEXT: + self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) else: self.focus_set() self.selected = self.get_selected(event) @@ -424,16 +427,19 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if ( - self.mode == GraphMode.ANNOTATION - and self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] - and selected is None - ): - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape - self.shape_drawing = True + + if self.mode == GraphMode.ANNOTATION and selected is None: + if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + x, y = self.canvas_xy(event) + shape = Shape(self.app, self, x, y) + self.selected = shape.id + self.shapes[shape.id] = shape + self.shape_drawing = True + elif self.annotation_type == ImageEnum.TEXT: + x, y = self.canvas_xy(event) + self.text = Shape(self.app, self, x, y) + # self.shapes[shape.id] = shape + if self.mode == GraphMode.SELECT: if selected is not None: if selected in self.shapes: diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 91020ecb..660ef291 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,51 +1,37 @@ """ class for shapes """ -import logging - from coretk.dialogs.shapemod import ShapeDialog from coretk.images import ImageEnum ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] -class ShapeData: +class AnnotationData: def __init__( self, - is_default=True, - text=None, - font=None, - font_size=None, - text_color=None, - fill_color=None, - border_color=None, - border_width=None, + text="", + font="Arial", + font_size=12, + text_color="#000000", + fill_color="#CFCFFF", + border_color="#000000", + border_width=0, bold=0, italic=0, underline=0, ): - if is_default: - self.text = "" - self.font = "Arial" - self.font_size = 12 - self.text_color = "#000000" - self.fill_color = "#CFCFFF" - self.border_color = "#000000" - self.border_width = 0 - self.bold = 0 - self.italic = 0 - self.underline = 0 - else: - self.text = text - self.font = font - self.font_size = font_size - self.text_color = text_color - self.fill_color = fill_color - self.border_color = border_color - self.border_width = border_width - self.bold = bold - self.italic = italic - self.underline = underline + + self.text = text + self.font = font + self.font_size = font_size + self.text_color = text_color + self.fill_color = fill_color + self.border_color = border_color + self.border_width = border_width + self.bold = bold + self.italic = italic + self.underline = underline class Shape: @@ -66,7 +52,7 @@ class Shape: self.y0 = top_y self.created = False self.text_id = None - self.shape_data = ShapeData() + self.shape_data = AnnotationData() canvas.delete(canvas.find_withtag("selectednodes")) annotation_type = self.canvas.annotation_type if annotation_type == ImageEnum.OVAL: @@ -112,7 +98,6 @@ class Shape: self.shape_data = data self.cursor_x = None self.cursor_y = None - self.canvas.tag_bind(self.id, "", self.click_release) def shape_motion(self, x1, y1): self.canvas.coords(self.id, self.x0, self.y0, x1, y1) @@ -123,9 +108,6 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() - def click_release(self, event): - logging.debug("Click release on shape %s", self.id) - def motion(self, event, delta_x=None, delta_y=None): if event is not None: delta_x = event.x - self.cursor_x From 49acac026c0cc71332bbfe0052c050b87bdec423 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:10:50 -0800 Subject: [PATCH 213/305] updates to move node context logic to node class and added check to display options the same as old core --- coretk/coretk/graph/graph.py | 37 +--------------------- coretk/coretk/graph/node.py | 60 +++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index abf3f952..4abb6e18 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -4,7 +4,6 @@ import tkinter as tk from PIL import Image, ImageTk from core.api.grpc import core_pb2 -from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption @@ -63,40 +62,6 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) - def create_node_context(self, canvas_node): - node = canvas_node.core_node - context = tk.Menu(self.master) - context.add_command(label="Configure", command=canvas_node.show_config) - if node.type == NodeType.WIRELESS_LAN: - context.add_command( - label="WLAN Config", command=canvas_node.show_wlan_config - ) - if self.master.core.is_runtime(): - if canvas_node.core_node.id in self.master.core.mobility_players: - context.add_command( - label="Mobility Player", - command=canvas_node.show_mobility_player, - ) - else: - context.add_command( - label="Mobility Config", command=canvas_node.show_mobility_config - ) - if node.type == NodeType.EMANE: - context.add_command( - label="EMANE Config", command=canvas_node.show_emane_config - ) - context.add_command(label="Select adjacent", state=tk.DISABLED) - context.add_command(label="Create link to", state=tk.DISABLED) - context.add_command(label="Assign to", state=tk.DISABLED) - context.add_command(label="Move to", state=tk.DISABLED) - context.add_command(label="Cut", state=tk.DISABLED) - context.add_command(label="Copy", state=tk.DISABLED) - context.add_command(label="Paste", state=tk.DISABLED) - context.add_command(label="Delete", state=tk.DISABLED) - context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) - return context - def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc @@ -512,7 +477,7 @@ class CanvasGraph(tk.Canvas): canvas_node = self.nodes.get(selected) if canvas_node: logging.debug(f"node context: {selected}") - self.context = self.create_node_context(canvas_node) + self.context = canvas_node.create_context() self.context.post(event.x_root, event.y_root) else: self.context.unpost() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0a46fd77..20e95867 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -2,6 +2,7 @@ import logging import tkinter as tk from tkinter import font +from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog @@ -36,6 +37,15 @@ class CanvasNode: fill="#0000CD", ) self.tooltip = CanvasTooltip(self.canvas) + self.edges = set() + self.interfaces = [] + self.wireless_edges = set() + self.moving = None + self.antennae = [] + self.setup_bindings() + + def setup_bindings(self): + # self.canvas.bind("", self.click_context) self.canvas.tag_bind(self.id, "", self.click_press) self.canvas.tag_bind(self.id, "", self.click_release) self.canvas.tag_bind(self.id, "", self.motion) @@ -43,11 +53,6 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) - self.edges = set() - self.interfaces = [] - self.wireless_edges = set() - self.moving = None - self.antennae = [] def delete(self): self.canvas.delete(self.id) @@ -184,6 +189,51 @@ class CanvasNode: shape = self.canvas.shapes[object_id] shape.motion(None, x - my_x, y - my_y) + def create_context(self): + is_wlan = self.core_node.type == NodeType.WIRELESS_LAN + is_emane = self.core_node.type == NodeType.EMANE + context = tk.Menu(self.canvas) + if self.app.core.is_runtime(): + context.add_command(label="Configure", command=self.show_config) + if is_wlan and self.core_node.id in self.app.core.mobility_players: + context.add_command( + label="Mobility Player", command=self.show_mobility_player + ) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Shell Window", state=tk.DISABLED) + context.add_command(label="Tcpdump", state=tk.DISABLED) + context.add_command(label="Tshark", state=tk.DISABLED) + context.add_command(label="Wireshark", state=tk.DISABLED) + context.add_command(label="View Log", state=tk.DISABLED) + else: + context.add_command(label="Configure", command=self.show_config) + if is_emane: + context.add_command( + label="EMANE Config", command=self.show_emane_config + ) + if is_wlan: + context.add_command(label="WLAN Config", command=self.show_wlan_config) + context.add_command( + label="Mobility Config", command=self.show_mobility_config + ) + if is_wlan or is_emane: + context.add_command(label="Link To All MDRs", state=tk.DISABLED) + context.add_command(label="Select Members", state=tk.DISABLED) + context.add_command(label="Select Adjacent", state=tk.DISABLED) + context.add_command(label="Create Link To", state=tk.DISABLED) + context.add_command(label="Assign To", state=tk.DISABLED) + context.add_command(label="Move To", state=tk.DISABLED) + context.add_command(label="Cut", state=tk.DISABLED) + context.add_command(label="Copy", state=tk.DISABLED) + context.add_command(label="Paste", state=tk.DISABLED) + context.add_command(label="Delete", state=tk.DISABLED) + context.add_command(label="Hide", state=tk.DISABLED) + context.add_command(label="Services", state=tk.DISABLED) + return context + def select_multiple(self, event): self.canvas.select_object(self.id, choose_multiple=True) From 27ead56a15cc056db39a343cc419fa7428c304f0 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:11:21 -0800 Subject: [PATCH 214/305] remove redundant file --- coretk/coretk/dialogs/textmod.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 coretk/coretk/dialogs/textmod.py diff --git a/coretk/coretk/dialogs/textmod.py b/coretk/coretk/dialogs/textmod.py deleted file mode 100644 index 03af38fb..00000000 --- a/coretk/coretk/dialogs/textmod.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -text dialog -""" -import tkinter as tk -from tkinter import ttk - -from coretk.dialogs.dialog import Dialog - - -class TextDialog(Dialog): - def __init__(self, master, app): - super().__init__(master, app, "Add a new text", modal=True) - self.canvas = app.canvas - self.text = tk.StringVar(value="") - - self.draw() - - def draw(self): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - label = ttk.Label(frame, text="Text for top of text: ") - label.grid(row=0, column=0) - entry = ttk.Entry(frame, textvariable=self.text) - entry.grid(row=0, column=1) - frame.grid(row=0, column=0, sticky="nsew") From b9bbf397c96f9eaade45e97a093602b6e0d3ce27 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:13:58 -0800 Subject: [PATCH 215/305] updated node context menu option for wireless to use nodeutils --- coretk/coretk/graph/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 20e95867..f726739c 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -219,7 +219,7 @@ class CanvasNode: context.add_command( label="Mobility Config", command=self.show_mobility_config ) - if is_wlan or is_emane: + if NodeUtils.is_wireless_node(self.core_node.type): context.add_command(label="Link To All MDRs", state=tk.DISABLED) context.add_command(label="Select Members", state=tk.DISABLED) context.add_command(label="Select Adjacent", state=tk.DISABLED) From 45a23a6c1417d7dc08af9e824d0f65749251c6be Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:42:41 -0800 Subject: [PATCH 216/305] updated usage of time.time to time.monotonic or time.perf_counter due to time.time possibly rolling backwards --- coretk/coretk/coreclient.py | 8 ++++---- coretk/coretk/menuaction.py | 4 ++-- daemon/core/api/grpc/server.py | 2 +- daemon/core/api/tlv/corehandlers.py | 2 +- daemon/core/emulator/session.py | 8 ++++---- daemon/core/location/event.py | 8 ++++---- daemon/core/location/mobility.py | 10 +++++----- daemon/core/nodes/network.py | 8 ++++---- daemon/core/services/coreservices.py | 4 ++-- daemon/tests/test_grpc.py | 2 +- daemon/tests/test_gui.py | 2 +- 11 files changed, 29 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 05a36c82..e2aef5a2 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -411,7 +411,7 @@ class CoreClient: else: emane_config = None - start = time.time() + start = time.perf_counter() response = self.client.start_session( self.session_id, nodes, @@ -425,7 +425,7 @@ class CoreClient: service_configs, file_configs, ) - process_time = time.time() - start + process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) @@ -439,9 +439,9 @@ class CoreClient: def stop_session(self, session_id=None): if not session_id: session_id = self.session_id - start = time.time() + start = time.perf_counter() response = self.client.stop_session(session_id) - process_time = time.time() - start + process_time = time.perf_counter() - start self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index d8dc89cb..1a64800c 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -33,10 +33,10 @@ class MenuAction: def cleanup_old_session(self, quitapp=False): logging.info("cleaning up old session") - start = time.time() + start = time.perf_counter() self.app.core.stop_session() self.app.core.delete_session() - process_time = time.time() - start + process_time = time.perf_counter() - start self.app.statusbar.stop_session_callback(process_time) if quitapp: self.app.quit() diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index a914d617..6a6747f1 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -660,7 +660,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): last_check = None last_stats = None while self._is_running(context): - now = time.time() + now = time.monotonic() stats = get_net_stats() # calculate average diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 321306a2..009cb067 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1710,7 +1710,7 @@ class CoreHandler(socketserver.BaseRequestHandler): event_type=event_type, name=name, data=fail_data + ";" + unknown_data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index fee2bde4..1a01bdaa 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -103,7 +103,7 @@ class Session: # TODO: should the default state be definition? self.state = EventTypes.NONE.value - self._state_time = time.time() + self._state_time = time.monotonic() self._state_file = os.path.join(self.session_dir, "state") # hooks handlers @@ -1030,7 +1030,7 @@ class Session: return self.state = state_value - self._state_time = time.time() + self._state_time = time.monotonic() logging.info("changing session(%s) to state %s", self.id, state_name) self.write_state(state_value) @@ -1038,7 +1038,7 @@ class Session: self.run_state_hooks(state_value) if send_event: - event_data = EventData(event_type=state_value, time=str(time.time())) + event_data = EventData(event_type=state_value, time=str(time.monotonic())) self.broadcast_event(event_data) def write_state(self, state): @@ -1821,7 +1821,7 @@ class Session: if not in runtime. """ if self.state == EventTypes.RUNTIME_STATE.value: - return time.time() - self._state_time + return time.monotonic() - self._state_time else: return 0.0 diff --git a/daemon/core/location/event.py b/daemon/core/location/event.py index 11e535d3..f930d9b7 100644 --- a/daemon/core/location/event.py +++ b/daemon/core/location/event.py @@ -145,7 +145,7 @@ class EventLoop: with self.lock: if not self.running or not self.queue: break - now = time.time() + now = time.monotonic() if self.queue[0].time > now: schedule = True break @@ -170,7 +170,7 @@ class EventLoop: raise ValueError("scheduling event while not running") if not self.queue: return - delay = self.queue[0].time - time.time() + delay = self.queue[0].time - time.monotonic() if self.timer: raise ValueError("timer was already set") self.timer = Timer(delay, self.__run_events) @@ -187,7 +187,7 @@ class EventLoop: if self.running: return self.running = True - self.start = time.time() + self.start = time.monotonic() for event in self.queue: event.time += self.start self.__schedule_event() @@ -225,7 +225,7 @@ class EventLoop: self.eventnum += 1 evtime = float(delaysec) if self.running: - evtime += time.time() + evtime += time.monotonic() event = Event(eventnum, evtime, func, *args, **kwds) if self.queue: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 6a796526..b3eb4884 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -174,7 +174,7 @@ class MobilityManager(ModelManager): event_type=event_type, name=f"mobility:{model.name}", data=data, - time=str(time.time()), + time=str(time.monotonic()), ) self.session.broadcast_event(event_data) @@ -612,7 +612,7 @@ class WayPointMobility(WirelessModel): if self.state != self.STATE_RUNNING: return t = self.lasttime - self.lasttime = time.time() + self.lasttime = time.monotonic() now = self.lasttime - self.timezero dt = self.lasttime - t @@ -664,7 +664,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ logging.info("running mobility scenario") - self.timezero = time.time() + self.timezero = time.monotonic() self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.movenodesinitial() self.runround() @@ -844,7 +844,7 @@ class WayPointMobility(WirelessModel): self.lasttime = 0 self.run() elif laststate == self.STATE_PAUSED: - now = time.time() + now = time.monotonic() self.timezero += now - self.lasttime self.lasttime = now - (0.001 * self.refresh_ms) self.runround() @@ -871,7 +871,7 @@ class WayPointMobility(WirelessModel): :return: nothing """ self.state = self.STATE_PAUSED - self.lasttime = time.time() + self.lasttime = time.monotonic() class Ns2ScriptedMobility(WayPointMobility): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 6fe291dd..f0639649 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -57,7 +57,7 @@ class EbtablesQueue: :return: nothing """ with self.updatelock: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() if self.doupdateloop: return @@ -108,9 +108,9 @@ class EbtablesQueue: :rtype: float """ try: - elapsed = time.time() - self.last_update_time[wlan] + elapsed = time.monotonic() - self.last_update_time[wlan] except KeyError: - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() elapsed = 0.0 return elapsed @@ -122,7 +122,7 @@ class EbtablesQueue: :param wlan: wlan entity :return: nothing """ - self.last_update_time[wlan] = time.time() + self.last_update_time[wlan] = time.monotonic() self.updates.remove(wlan) def updateloop(self): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 80168425..361061be 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -538,13 +538,13 @@ class CoreServices: time.sleep(service.validation_timer) # non-blocking, attempt to validate periodically, up to validation_timer time elif service.validation_mode == ServiceMode.NON_BLOCKING: - start = time.time() + start = time.monotonic() while True: status = self.validate_service(node, service) if not status: break - if time.time() - start > service.validation_timer: + if time.monotonic() - start > service.validation_timer: break time.sleep(service.validation_period) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 28005dbb..56523e81 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1042,7 +1042,7 @@ class TestGrpc: client.events(session.id, handle_event) time.sleep(0.1) event = EventData( - event_type=EventTypes.RUNTIME_STATE.value, time=str(time.time()) + event_type=EventTypes.RUNTIME_STATE.value, time=str(time.monotonic()) ) session.broadcast_event(event) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index c2a8c9fc..91324d08 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -523,7 +523,7 @@ class TestGui: MessageFlags.ADD.value, [ (EventTlvs.TYPE, EventTypes.SCHEDULED.value), - (EventTlvs.TIME, str(time.time() + 100)), + (EventTlvs.TIME, str(time.monotonic() + 100)), (EventTlvs.NODE, node.id), (EventTlvs.NAME, "event"), (EventTlvs.DATA, "data"), From 9ef5cdd70be6fa180dc97a8628e6f19761283061 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 11:13:43 -0800 Subject: [PATCH 217/305] added basic about dialog and created codetext widget for displaying text in terminal like colors using scrolledtext widget --- coretk/coretk/dialogs/about.py | 44 +++++++++++++++++++ coretk/coretk/dialogs/hooks.py | 3 +- coretk/coretk/dialogs/serviceconfiguration.py | 5 +-- coretk/coretk/menuaction.py | 5 +++ coretk/coretk/menubar.py | 2 +- coretk/coretk/widgets.py | 26 ++++++++++- 6 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/dialogs/about.py diff --git a/coretk/coretk/dialogs/about.py b/coretk/coretk/dialogs/about.py new file mode 100644 index 00000000..28ddea33 --- /dev/null +++ b/coretk/coretk/dialogs/about.py @@ -0,0 +1,44 @@ +import tkinter as tk + +from coretk.dialogs.dialog import Dialog +from coretk.widgets import CodeText + +LICENSE = """\ +Copyright (c) 2005-2020, the Boeing Company. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE.\ +""" + + +class AboutDialog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "About CORE", modal=True) + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + + text = CodeText(self.top) + text.insert("1.0", LICENSE) + text.config(state=tk.DISABLED) + text.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index dc677e12..d40f1c36 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -3,6 +3,7 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.widgets import CodeText class HookDialog(Dialog): @@ -43,7 +44,7 @@ class HookDialog(Dialog): frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) frame.grid(row=1, sticky="nsew", pady=2) - self.data = tk.Text(frame) + self.data = CodeText(frame) self.data.insert( 1.0, ( diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 1d8059d8..446f9c12 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -2,12 +2,11 @@ import logging import tkinter as tk from tkinter import ttk -from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images -from coretk.widgets import ListboxScroll +from coretk.widgets import CodeText, ListboxScroll class ServiceConfiguration(Dialog): @@ -182,7 +181,7 @@ class ServiceConfiguration(Dialog): button.grid(row=0, column=2) frame.grid(row=3, column=0, sticky="nsew") - self.service_file_data = ScrolledText(tab1) + self.service_file_data = CodeText(tab1) self.service_file_data.grid(row=4, column=0, sticky="nsew") if len(self.filenames) > 0: self.filename_combobox.current(0) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 1a64800c..c4cc27bc 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -12,6 +12,7 @@ import grpc from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH +from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.hooks import HooksDialog @@ -151,3 +152,7 @@ class MenuAction: def edit_observer_widgets(self): dialog = ObserverDialog(self.app, self.app) dialog.show() + + def show_about(self): + dialog = AboutDialog(self.app, self.app) + dialog.show() diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index e56c55ec..288a517f 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -462,5 +462,5 @@ class Menubar(tk.Menu): label="Core Documentation (www)", command=self.menuaction.help_core_documentation, ) - menu.add_command(label="About", state=tk.DISABLED) + menu.add_command(label="About", command=self.menuaction.show_about) self.add_cascade(label="Help", menu=menu) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index bd6904b2..760846a3 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,7 +1,8 @@ import logging import tkinter as tk from functools import partial -from tkinter import ttk +from tkinter import font, ttk +from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 @@ -155,3 +156,26 @@ class CheckboxList(FrameScroll): func = partial(self.clicked, name, var) checkbox = ttk.Checkbutton(self.frame, text=name, variable=var, command=func) checkbox.grid(sticky="w") + + +class CodeFont(font.Font): + def __init__(self): + super().__init__(font="TkFixedFont", color="green") + + +class CodeText(ScrolledText): + def __init__(self, master, **kwargs): + super().__init__( + master, + bd=0, + bg="black", + cursor="xterm lime lime", + fg="lime", + font=CodeFont(), + highlightbackground="black", + insertbackground="lime", + selectbackground="lime", + selectforeground="black", + relief=tk.FLAT, + **kwargs + ) From c238b5dfc8ac2b5d5bd708c9d7b81a749c147b55 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 11:17:05 -0800 Subject: [PATCH 218/305] moved node context services option higher for better convenience, added check that context services only shown for container nodes --- coretk/coretk/graph/node.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index f726739c..70424176 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -195,13 +195,14 @@ class CanvasNode: context = tk.Menu(self.canvas) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Services", state=tk.DISABLED) if is_wlan and self.core_node.id in self.app.core.mobility_players: context.add_command( label="Mobility Player", command=self.show_mobility_player ) context.add_command(label="Select Adjacent", state=tk.DISABLED) context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Shell Window", state=tk.DISABLED) context.add_command(label="Tcpdump", state=tk.DISABLED) @@ -210,6 +211,8 @@ class CanvasNode: context.add_command(label="View Log", state=tk.DISABLED) else: context.add_command(label="Configure", command=self.show_config) + if NodeUtils.is_container_node(self.core_node.type): + context.add_command(label="Services", state=tk.DISABLED) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -231,7 +234,6 @@ class CanvasNode: context.add_command(label="Paste", state=tk.DISABLED) context.add_command(label="Delete", state=tk.DISABLED) context.add_command(label="Hide", state=tk.DISABLED) - context.add_command(label="Services", state=tk.DISABLED) return context def select_multiple(self, event): From a7d9d588ae97f8ffd7a830cab84207f8bc73677e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 12:46:00 -0800 Subject: [PATCH 219/305] add display throughput back to the gui --- coretk/coretk/coreclient.py | 29 +++--- coretk/coretk/graph/graph.py | 1 + coretk/coretk/graph/linkinfo.py | 160 +++++++++++++++----------------- coretk/coretk/graph/node.py | 2 + 4 files changed, 94 insertions(+), 98 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index efbc6a39..d4cfa2a6 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -136,7 +136,6 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -161,8 +160,6 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) - elif event.HasField("throughput_event"): - print("throughput") else: logging.info("unhandled event: %s", event) @@ -189,16 +186,21 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - interface_throughputs = event.interface_throughputs - for i in interface_throughputs: - print("") - # return - throughputs_belong_to_session = [] - for if_tp in interface_throughputs: - if if_tp.node_id in self.node_ids: - throughputs_belong_to_session.append(if_tp) - self.throughput_draw.process_grpc_throughput_event( - throughputs_belong_to_session + # interface_throughputs = event.interface_throughputs + # # print(interface_throughputs) + # # return + # # for i in interface_throughputs: + # # print("") + # # # return + # print(event) + # throughputs_belong_to_session = [] + # print(self.node_ids) + # for throughput in interface_throughputs: + # if throughput.node_id in self.node_ids: + # throughputs_belong_to_session.append(throughput) + # print(throughputs_belong_to_session) + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs ) def join_session(self, session_id, query_location=True): @@ -214,6 +216,7 @@ class CoreClient: session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) + self.client.throughputs(self.handle_throughputs) # get location if query_location: diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 4d7b9c65..9587cbdb 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -395,6 +395,7 @@ class CanvasGraph(tk.Canvas): if edge in edges: continue edges.add(edge) + self.throughput_draw.delete(edge) del self.edges[edge.token] edge.delete() diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index c78fe7e4..15c08e45 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -1,10 +1,11 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ -import logging import tkinter as tk from tkinter import font +from core.api.grpc import core_pb2 + TEXT_DISTANCE = 0.30 @@ -76,6 +77,7 @@ class Throughput: self.tracker = {} # map an edge canvas id to a throughput canvas id self.map = {} + # map edge canvas id to token self.edge_id_to_token = {} def load_throughput_info(self, interface_throughputs): @@ -86,21 +88,23 @@ class Throughput: throughputs :return: nothing """ - for t in interface_throughputs: - nid = t.node_id - iid = t.interface_id - tp = t.throughput - token = self.core.interface_to_edge[(nid, iid)] - print(token) - edge_id = self.canvas.edges[token].id - - self.edge_id_to_token[edge_id] = token - - if edge_id not in self.tracker: - self.tracker[edge_id] = tp - else: - temp = self.tracker[edge_id] - self.tracker[edge_id] = (temp + tp) / 2 + for throughput in interface_throughputs: + nid = throughput.node_id + iid = throughput.interface_id + tp = throughput.throughput + token = self.core.interface_to_edge.get((nid, iid)) + if token: + edge = self.canvas.edges.get(token) + if edge: + edge_id = edge.id + self.edge_id_to_token[edge_id] = token + if edge_id not in self.tracker: + self.tracker[edge_id] = tp + else: + temp = self.tracker[edge_id] + self.tracker[edge_id] = (temp + tp) / 2 + else: + self.core.interface_to_edge.pop((nid, iid), None) def edge_is_wired(self, token): """ @@ -112,41 +116,35 @@ class Throughput: canvas_edge = self.canvas.edges[token] canvas_src_id = canvas_edge.src canvas_dst_id = canvas_edge.dst - src_node = self.canvas.nodes[canvas_src_id] - dst_node = self.canvas.nodes[canvas_dst_id] - - if src_node.node_type == "wlan": - if dst_node.node_type == "mdr": - return False - else: - logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") - return True - if dst_node.node_type == "wlan": - if src_node.node_type == "mdr": - return False - else: - logging.debug("linkinfo.py is_wired WARNING wlan only connected to mdr") - return True - return True + src = self.canvas.nodes[canvas_src_id].core_node + dst = self.canvas.nodes[canvas_dst_id].core_node + return not ( + src.type == core_pb2.NodeType.WIRELESS_LAN + and dst.model == "mdr" + or src.model == "mdr" + and dst.type == core_pb2.NodeType.WIRELESS_LAN + ) def draw_wired_throughput(self, edge_id): - x1, y1, x2, y2 = self.canvas.coords(edge_id) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - + x0, y0, x1, y1 = self.canvas.coords(edge_id) + x = (x0 + x1) / 2 + y = (y0 + y1) / 2 if edge_id not in self.map: - tp_id = self.canvas.create_text( - x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - ) - self.map[edge_id] = tp_id - - # redraw throughput - else: - self.canvas.itemconfig( - self.map[edge_id], + tpid = self.canvas.create_text( + x, + y, + tags="throughput", + font=("Arial", 8), text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), ) + self.map[edge_id] = tpid + else: + tpid = self.map[edge_id] + self.canvas.coords(tpid, x, y) + self.canvas.itemconfig( + tpid, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) + ) def draw_wireless_throughput(self, edge_id): token = self.edge_id_to_token[edge_id] @@ -156,17 +154,19 @@ class Throughput: src_node = self.canvas.nodes[canvas_src_id] dst_node = self.canvas.nodes[canvas_dst_id] - # non_wlan_node = None - if src_node.node_type == "wlan": - non_wlan_node = dst_node - else: - non_wlan_node = src_node + not_wlan = ( + dst_node + if src_node.core_node.type == core_pb2.NodeType.WIRELESS_LAN + else src_node + ) - x, y = self.canvas.coords(non_wlan_node.id) + x, y = self.canvas.coords(not_wlan.id) if edge_id not in self.map: tp_id = self.canvas.create_text( x + 50, y + 25, + font=("Arial", 8), + tags="throughput", text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), ) self.map[edge_id] = tp_id @@ -184,43 +184,33 @@ class Throughput: self.draw_wired_throughput(edge_id) else: self.draw_wireless_throughput(edge_id) - # draw wireless throughput - - # x1, y1, x2, y2 = self.canvas.coords(edge_id) - # x = (x1 + x2) / 2 - # y = (y1 + y2) / 2 - # - # print(self.is_wired(self.edge_id_to_token[edge_id])) - # # new throughput - # if edge_id not in self.map: - # tp_id = self.canvas.create_text( - # x, y, text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]) - # ) - # self.map[edge_id] = tp_id - # - # # redraw throughput - # else: - # self.canvas.itemconfig( - # self.map[edge_id], - # text="{0:.3f} kbps".format(0.001 * self.tracker[edge_id]), - # ) def process_grpc_throughput_event(self, interface_throughputs): self.load_throughput_info(interface_throughputs) self.draw_throughputs() - def update_throughtput_location(self, edge): - tp_id = self.map[edge.id] - if self.edge_is_wired(self.edge_id_to_token[edge.id]): - x1, y1 = self.canvas.coords(edge.src) - x2, y2 = self.canvas.coords(edge.dst) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.canvas.coords(tp_id, x, y) - else: - if self.canvas.nodes[edge.src].node_type == "wlan": - x, y = self.canvas.coords(edge.dst) - self.canvas.coords(tp_id, x + 50, y + 20) + def move(self, edge): + tpid = self.map.get(edge.id) + if tpid: + if self.edge_is_wired(edge.token): + x0, y0, x1, y1 = self.canvas.coords(edge.id) + self.canvas.coords(tpid, (x0 + x1) / 2, (y0 + y1) / 2) else: - x, y = self.canvas.coords(edge.src) - self.canvas.coords(tp_id, x + 50, y + 25) + if ( + self.canvas.nodes[edge.src].core_node.type + == core_pb2.NodeType.WIRELESS_LAN + ): + x, y = self.canvas.coords(edge.dst) + self.canvas.coords(tpid, x + 50, y + 20) + else: + x, y = self.canvas.coords(edge.src) + self.canvas.coords(tpid, x + 50, y + 25) + + def delete(self, edge): + tpid = self.map.get(edge.id) + if tpid: + eid = edge.id + self.canvas.delete(tpid) + self.tracker.pop(eid) + self.map.pop(eid) + self.edge_id_to_token.pop(eid) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0a46fd77..e80f62d2 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -117,6 +117,8 @@ class CanvasNode: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) + self.canvas.throughput_draw.move(edge) + edge.link_info.recalculate_info() for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) From 742eb2bed6f0cec0b4d9a1f156e165f8c1b01f8d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:01:03 -0800 Subject: [PATCH 220/305] cleanup for shapes and creating shapes and storing and restoring shapes from metadata --- coretk/coretk/coreclient.py | 69 +++++++++----- coretk/coretk/dialogs/shapemod.py | 49 +++++----- coretk/coretk/graph/enums.py | 5 -- coretk/coretk/graph/graph.py | 23 +++-- coretk/coretk/graph/shape.py | 143 +++++++++++++++++------------- coretk/coretk/graph/shapeutils.py | 19 ++++ coretk/coretk/toolbar.py | 19 ++-- 7 files changed, 190 insertions(+), 137 deletions(-) create mode 100644 coretk/coretk/graph/shapeutils.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 4295fc21..f2f400d5 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -5,12 +5,14 @@ import json import logging import os import time +from pathlib import Path from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog from coretk.graph.shape import AnnotationData, Shape +from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils @@ -136,7 +138,6 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): - print(event) if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -162,7 +163,7 @@ class CoreClient: elif event.HasField("config_event"): logging.info("config event: %s", event) elif event.HasField("throughput_event"): - print("throughput") + logging.info("throughput event: %s", event) else: logging.info("unhandled event: %s", event) @@ -301,35 +302,42 @@ class CoreClient: def parse_metadata(self, config): # canvas setting canvas_config = config.get("canvas") + logging.info("canvas metadata: %s", canvas_config) if canvas_config: - logging.info("canvas metadata: %s", canvas_config) canvas_config = json.loads(canvas_config) wallpaper_style = canvas_config["wallpaper-style"] self.app.canvas.scale_option.set(wallpaper_style) wallpaper = canvas_config["wallpaper"] - wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) - for key, annotation_config in config.items(): - if "annotation" in key: - annotation_config = json.loads(annotation_config) - config_type = annotation_config["type"] - if config_type in ["rectangle", "oval"]: - coords = tuple(annotation_config["iconcoords"]) + if wallpaper: + wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) + self.app.canvas.set_wallpaper(wallpaper) + + # load saved shapes + shapes_config = config.get("shapes") + if shapes_config: + shapes_config = json.loads(shapes_config) + for shape_config in shapes_config: + logging.info("loading shape: %s", shapes_config) + shape_type = shape_config["type"] + try: + shape_type = ShapeType(shape_type) + x1, y1, x2, y2 = shape_config["iconcoords"] data = AnnotationData( - annotation_config["label"], - annotation_config["fontfamily"], - annotation_config["fontsize"], - annotation_config["labelcolor"], - annotation_config["color"], - annotation_config["border"], - annotation_config["width"], + shape_config["label"], + shape_config["fontfamily"], + shape_config["fontsize"], + shape_config["labelcolor"], + shape_config["color"], + shape_config["border"], + shape_config["width"], ) shape = Shape( - self.app, self.app.canvas, None, None, coords, data, config_type + self.app, self.app.canvas, shape_type, x1, y1, x2, y2, data ) self.app.canvas.shapes[shape.id] = shape - else: - logging.debug("not implemented") + except ValueError: + logging.debug("unknown shape: %s", shape_type) + for tag in LIFT_ORDER: self.app.canvas.tag_raise(tag) @@ -427,6 +435,7 @@ class CoreClient: service_configs, file_configs, ) + self.set_metadata() process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) self.app.statusbar.start_session_callback(process_time) @@ -447,6 +456,24 @@ class CoreClient: self.app.statusbar.stop_session_callback(process_time) logging.debug("stopped session(%s), result: %s", session_id, response.result) + def set_metadata(self): + # create canvas data + canvas_config = { + "wallpaper": Path(self.app.canvas.wallpaper_file).name, + "wallpaper-style": self.app.canvas.scale_option.get(), + } + canvas_config = json.dumps(canvas_config) + + # create shapes data + shapes = [] + for shape in self.app.canvas.shapes.values(): + shapes.append(shape.metadata()) + shapes = json.dumps(shapes) + + metadata = {"canvas": canvas_config, "shapes": shapes} + response = self.client.set_session_metadata(self.session_id, metadata) + logging.info("set session metadata: %s", response) + def launch_terminal(self, node_id): response = self.client.get_node_terminal(self.session_id, node_id) logging.info("get terminal %s", response.terminal) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 4b507dfa..faff5ef8 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,7 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog -from coretk.images import ImageEnum +from coretk.graph.shapeutils import is_draw_shape, is_shape_text FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -13,40 +13,34 @@ BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] class ShapeDialog(Dialog): def __init__(self, master, app, shape): - self.annotation_type = app.canvas.annotation_type - self.canvas = app.canvas - if self.is_shape(): - super().__init__(master, app, "Add a new shape", modal=True) + if is_draw_shape(shape.shape_type): + title = "Add Shape" self.id = shape.id - self.fill = None - self.border = None else: - super().__init__(master, app, "Add a new text", modal=True) - + title = "Add Text" + self.id = None + self.canvas = app.canvas + super().__init__(master, app, title, modal=True) + self.fill = None + self.border = None self.shape = shape data = shape.shape_data self.shape_text = tk.StringVar(value=data.text) self.font = tk.StringVar(value=data.font) self.font_size = tk.IntVar(value=data.font_size) self.text_color = data.text_color - self.fill_color = data.fill_color + fill_color = data.fill_color + if not fill_color: + fill_color = "#CFCFFF" + self.fill_color = fill_color self.border_color = data.border_color - self.border_width = tk.IntVar(value=data.border_width) + self.border_width = tk.IntVar(value=0) self.bold = tk.IntVar(value=data.bold) self.italic = tk.IntVar(value=data.italic) self.underline = tk.IntVar(value=data.underline) self.top.columnconfigure(0, weight=1) self.draw() - def is_shape(self): - return ( - self.annotation_type == ImageEnum.OVAL - or self.annotation_type == ImageEnum.RECTANGLE - ) - - def is_text(self): - return self.annotation_type == ImageEnum.TEXT - def draw(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) @@ -85,7 +79,7 @@ class ShapeDialog(Dialog): button.grid(row=0, column=2) frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) - if self.is_shape(): + if is_draw_shape(self.shape.shape_type): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) @@ -152,15 +146,18 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if self.is_shape() and not self.canvas.shapes[self.id].created: + if ( + is_draw_shape(self.shape.shape_type) + and not self.canvas.shapes[self.id].created + ): self.canvas.delete(self.id) self.canvas.shapes.pop(self.id) self.destroy() def click_add(self): - if self.is_shape(): + if is_draw_shape(self.shape.shape_type): self.add_shape() - elif self.is_text(): + elif is_shape_text(self.shape.shape_type): self.add_text() self.destroy() @@ -212,8 +209,8 @@ class ShapeDialog(Dialog): :return: nothing """ text = self.shape_text.get() - x = self.shape.x0 - y = self.shape.y0 + x = self.shape.x1 + y = self.shape.y1 text_font = self.make_font() if self.shape.text_id is None: tid = self.canvas.create_text( diff --git a/coretk/coretk/graph/enums.py b/coretk/coretk/graph/enums.py index 65fb10eb..b292938f 100644 --- a/coretk/coretk/graph/enums.py +++ b/coretk/coretk/graph/enums.py @@ -16,8 +16,3 @@ class ScaleOption(enum.Enum): CENTERED = 2 SCALED = 3 TILED = 4 - - -class ShapeType(enum.Enum): - OVAL = 0 - RECTANGLE = 1 diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 58ae990a..fdade021 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -10,7 +10,8 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.images import ImageEnum, Images +from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.images import Images from coretk.nodeutils import NodeUtils ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] @@ -243,13 +244,13 @@ class CanvasGraph(tk.Canvas): self.context = None else: if self.mode == GraphMode.ANNOTATION: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: + if is_draw_shape(self.annotation_type): self.focus_set() x, y = self.canvas_xy(event) if self.shape_drawing: self.shapes[self.selected].shape_complete(x, y) self.shape_drawing = False - elif self.annotation_type == ImageEnum.TEXT: + elif is_shape_text(self.annotation_type): self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) else: self.focus_set() @@ -401,15 +402,14 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - if self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE]: - x, y = self.canvas_xy(event) - shape = Shape(self.app, self, x, y) + x, y = self.canvas_xy(event) + if is_draw_shape(self.annotation_type): + shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shapes[shape.id] = shape self.shape_drawing = True - elif self.annotation_type == ImageEnum.TEXT: - x, y = self.canvas_xy(event) - self.text = Shape(self.app, self, x, y) + elif is_shape_text(self.annotation_type): + self.text = Shape(self.app, self, self.annotation_type, x, y) if self.mode == GraphMode.SELECT: if selected is not None: @@ -446,10 +446,7 @@ class CanvasGraph(tk.Canvas): x1, y1, _, _ = self.coords(self.drawing_edge.id) self.coords(self.drawing_edge.id, x1, y1, x2, y2) if self.mode == GraphMode.ANNOTATION: - if ( - self.annotation_type in [ImageEnum.OVAL, ImageEnum.RECTANGLE] - and self.shape_drawing - ): + if is_draw_shape(self.annotation_type) and self.shape_drawing: x, y = self.canvas_xy(event) self.shapes[self.selected].shape_motion(x, y) if ( diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 660ef291..7ef9530f 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,8 +1,7 @@ -""" -class for shapes -""" +from tkinter.font import Font + from coretk.dialogs.shapemod import ShapeDialog -from coretk.images import ImageEnum +from coretk.graph.shapeutils import ShapeType ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] @@ -14,14 +13,13 @@ class AnnotationData: font="Arial", font_size=12, text_color="#000000", - fill_color="#CFCFFF", + fill_color="", border_color="#000000", - border_width=0, + border_width=1, bold=0, italic=0, underline=0, ): - self.text = text self.font = font self.font_size = font_size @@ -35,72 +33,77 @@ class AnnotationData: class Shape: - def __init__( - self, - app, - canvas, - top_x=None, - top_y=None, - coords=None, - data=None, - shape_type=None, - ): + def __init__(self, app, canvas, shape_type, x1, y1, x2=None, y2=None, data=None): self.app = app self.canvas = canvas + self.shape_type = shape_type + self.id = None + self.text_id = None + self.x1 = x1 + self.y1 = y1 + if x2 is None: + x2 = x1 + self.x2 = x2 + if y2 is None: + y2 = y1 + self.y2 = y2 if data is None: - self.x0 = top_x - self.y0 = top_y self.created = False - self.text_id = None self.shape_data = AnnotationData() - canvas.delete(canvas.find_withtag("selectednodes")) - annotation_type = self.canvas.annotation_type - if annotation_type == ImageEnum.OVAL: - self.id = canvas.create_oval( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) - elif annotation_type == ImageEnum.RECTANGLE: - self.id = canvas.create_rectangle( - top_x, top_y, top_x, top_y, tags="shape", dash="-" - ) + self.cursor_x = x1 + self.cursor_y = y1 else: - x0, y0, x1, y1 = coords - self.x0 = x0 - self.y0 = y0 self.created = True - if shape_type == "oval": - self.id = self.canvas.create_oval( - x0, - y0, - x1, - y1, - tags="shape", - fill=data.fill_color, - outline=data.border_color, - width=data.border_width, - ) - elif shape_type == "rectangle": - self.id = self.canvas.create_rectangle( - x0, - y0, - x1, - y1, - tags="shape", - fill=data.fill_color, - outline=data.border_color, - width=data.border_width, - ) - _x = (x0 + x1) / 2 - _y = y0 + 1.5 * data.font_size - self.text_id = self.canvas.create_text( - _x, _y, tags="shapetext", text=data.text, fill=data.text_color - ) self.shape_data = data - self.cursor_x = None - self.cursor_y = None + self.cursor_x = None + self.cursor_y = None + self.draw() + + def draw(self): + if self.created: + dash = None + else: + dash = "-" + if self.shape_type == ShapeType.OVAL: + self.id = self.canvas.create_oval( + self.x1, + self.y1, + self.x2, + self.y2, + tags="shape", + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + elif self.shape_type == ShapeType.RECTANGLE: + self.id = self.canvas.create_rectangle( + self.x1, + self.y1, + self.x2, + self.y2, + tags="shape", + dash=dash, + fill=self.shape_data.fill_color, + outline=self.shape_data.border_color, + width=self.shape_data.border_width, + ) + + if self.shape_data.text: + x = (self.x1 + self.x2) / 2 + y = self.y1 + 1.5 * self.shape_data.font_size + font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + self.text_id = self.canvas.create_text( + x, + y, + tags="shapetext", + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) def shape_motion(self, x1, y1): - self.canvas.coords(self.id, self.x0, self.y0, x1, y1) + self.canvas.coords(self.id, self.x1, self.y1, x1, y1) def shape_complete(self, x, y): for component in ABOVE_COMPONENT: @@ -122,3 +125,17 @@ class Shape: def delete(self): self.canvas.delete(self.id) self.canvas.delete(self.text_id) + + def metadata(self): + coords = self.canvas.coords(self.id) + return { + "type": self.shape_type.value, + "iconcoords": coords, + "label": self.shape_data.text, + "fontfamily": self.shape_data.font, + "fontsize": self.shape_data.font_size, + "labelcolor": self.shape_data.text_color, + "color": self.shape_data.fill_color, + "border": self.shape_data.border_color, + "width": self.shape_data.border_width, + } diff --git a/coretk/coretk/graph/shapeutils.py b/coretk/coretk/graph/shapeutils.py new file mode 100644 index 00000000..a566a713 --- /dev/null +++ b/coretk/coretk/graph/shapeutils.py @@ -0,0 +1,19 @@ +import enum + + +class ShapeType(enum.Enum): + MARKER = "marker" + OVAL = "oval" + RECTANGLE = "rectangle" + TEXT = "text" + + +SHAPES = {ShapeType.OVAL, ShapeType.RECTANGLE} + + +def is_draw_shape(shape_type): + return shape_type in SHAPES + + +def is_shape_text(shape_type): + return shape_type == ShapeType.TEXT diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 5f528628..1f1e734f 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -6,6 +6,7 @@ from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph.enums import GraphMode +from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.tooltip import Tooltip @@ -291,18 +292,18 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.annotation_picker = ttk.Frame(self.master) nodes = [ - (ImageEnum.MARKER, "marker"), - (ImageEnum.OVAL, "oval"), - (ImageEnum.RECTANGLE, "rectangle"), - (ImageEnum.TEXT, "text"), + (ImageEnum.MARKER, ShapeType.MARKER), + (ImageEnum.OVAL, ShapeType.OVAL), + (ImageEnum.RECTANGLE, ShapeType.RECTANGLE), + (ImageEnum.TEXT, ShapeType.TEXT), ] - for image_enum, tooltip in nodes: + for image_enum, shape_type in nodes: image = icon(image_enum) self.create_picker_button( image, - partial(self.update_annotation, image, image_enum), + partial(self.update_annotation, image, shape_type), self.annotation_picker, - tooltip, + shape_type.value, ) self.design_select(self.annotation_button) self.annotation_button.after( @@ -362,13 +363,13 @@ class Toolbar(ttk.Frame): self.design_frame.tkraise() - def update_annotation(self, image, image_enum): + def update_annotation(self, image, shape_type): logging.info("clicked annotation: ") self.hide_pickers() self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION - self.app.canvas.annotation_type = image_enum + self.app.canvas.annotation_type = shape_type def click_run_button(self): logging.debug("Click on RUN button") From 2824ae09b03515235fdd3d1be3e9537f578d1350 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:18:13 -0800 Subject: [PATCH 221/305] updated sample1.xml to draw shapes using new metadata logic --- coretk/coretk/data/xmls/sample1.xml | 1711 +++++++++++++++++++++++++-- 1 file changed, 1642 insertions(+), 69 deletions(-) diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 0c1526fb..0d5dd02d 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,16 +1,16 @@ - + - - + + - - + + - - + + @@ -18,8 +18,8 @@ - - + + @@ -27,8 +27,8 @@ - - + + @@ -36,8 +36,16 @@ - - + + + + + + + + + + @@ -45,65 +53,57 @@ - - + + - - + + - - + + - - - - - - - - - - + + - - + + - - + + - - + + - - + + @@ -113,71 +113,71 @@ - - + + - - - - - - + + + + + + - - - - - + - + + + + + - + - + - - - - - - - - - - + + + + + + + + + + @@ -199,6 +199,686 @@ + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.3.2/24 + ipv6 address a:3::2/64 +! +interface eth1 + ip address 10.0.5.1/24 + ipv6 address a:5::1/64 +! +router ospf + router-id 10.0.3.2 + network 10.0.3.0/24 area 0 + network 10.0.5.0/24 area 0 +! +router ospf6 + router-id 10.0.3.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.2.2/24 + ipv6 address a:2::2/64 +! +interface eth1 + ip address 10.0.3.1/24 + ipv6 address a:3::1/64 +! +interface eth2 + ip address 10.0.4.1/24 + ipv6 address a:4::1/64 +! +router ospf + router-id 10.0.2.2 + network 10.0.2.0/24 area 0 + network 10.0.3.0/24 area 0 + network 10.0.4.0/24 area 0 +! +router ospf6 + router-id 10.0.2.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.1.1/24 + ipv6 address a:1::1/64 +! +interface eth1 + ip address 10.0.2.1/24 + ipv6 address a:2::1/64 +! +router ospf + router-id 10.0.1.1 + network 10.0.1.0/24 area 0 + network 10.0.2.0/24 area 0 +! +router ospf6 + router-id 10.0.1.1 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.6/32 + ipv6 address a::6/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.6 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + /usr/local/etc/quagga @@ -248,11 +928,907 @@ router ospf6 redistribute ospf ! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.7/32 + ipv6 address a::7/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.7 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.8/32 + ipv6 address a::8/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.8 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.9/32 + ipv6 address a::9/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.9 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + sh defaultroute.sh + + + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 + + + + + + /etc/ssh + /var/run/sshd + + + sh startsshd.sh + + + killall sshd + + + #!/bin/sh +# auto-generated by SSH service (utility.py) +ssh-keygen -q -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key +chmod 655 /var/run/sshd +# wait until RSA host key has been generated to launch sshd +/usr/sbin/sshd -f /etc/ssh/sshd_config + + # auto-generated by SSH service (utility.py) +Port 22 +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +UsePrivilegeSeparation yes +PidFile /var/run/sshd/sshd.pid + +KeyRegenerationInterval 3600 +ServerKeyBits 768 + +SyslogFacility AUTH +LogLevel INFO + +LoginGraceTime 120 +PermitRootLogin yes +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes + +IgnoreRhosts yes +RhostsRSAAuthentication no +HostbasedAuthentication no + +PermitEmptyPasswords no +ChallengeResponseAuthentication no + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes + +AcceptEnv LANG LC_* +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +UseDNS no + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.4.2/24 + ipv6 address a:4::2/64 +! +interface eth1 + ip address 10.0.5.2/24 + ipv6 address a:5::2/64 +! +interface eth2 + ip address 10.0.6.1/24 + ipv6 address a:6::1/64 +! +router ospf + router-id 10.0.4.2 + network 10.0.4.0/24 area 0 + network 10.0.5.0/24 area 0 + network 10.0.6.0/24 area 0 +! +router ospf6 + router-id 10.0.4.2 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 + interface eth2 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth2.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth2.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth2.rp_filter=0 - + @@ -266,11 +1842,8 @@ router ospf6 - - - - - + + From 2add2157043ac5142fc0f4a3880b7e3b48c55d5a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 6 Dec 2019 15:06:35 -0800 Subject: [PATCH 222/305] fix configuring newly added services --- coretk/coretk/coreclient.py | 14 +++++++++++++- coretk/coretk/dialogs/nodeservice.py | 1 + coretk/coretk/graph/node.py | 3 --- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 91d88487..b7b0a0df 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -432,6 +432,14 @@ class CoreClient: ) process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) + # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) + # # print(self.client.set_service_defaults(self.session_id, {"router": ["DefaultRouter"]})) + # print(self.client.set_node_service(self.session_id, 1, "DefaultRoute", ["echo hello"], [], [])) + # + # # print(self.client.get_service_defaults(self.session_id)) + # + # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) + # # print(self.client.get_node_service_file(self.session_id, 1, "DefaultRoute", "defaultroute.sh")) self.app.statusbar.start_session_callback(process_time) # display mobility players @@ -516,8 +524,11 @@ class CoreClient: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION ) + + # temp + self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - if node_proto.id not in self.created_nodes: + if node_proto.id not in self.created_nodes or True: response = self.client.add_node(self.session_id, node_proto) logging.debug("create node: %s", response) self.created_nodes.add(node_proto.id) @@ -525,6 +536,7 @@ class CoreClient: if ( tuple([link_proto.node_one_id, link_proto.node_two_id]) not in self.created_links + or True ): response = self.client.add_link( self.session_id, diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 35142497..d950371b 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -87,6 +87,7 @@ class NodeService(Dialog): self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) + self.canvas_node.core_node.services[:] = self.current_services def click_configure(self): current_selection = self.current.listbox.curselection() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 83b29da2..27f6a5dc 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -144,9 +144,6 @@ class CanvasNode: def on_leave(self, event): self.tooltip.on_leave(event) - def click(self, event): - print("click") - def double_click(self, event): if self.app.core.is_runtime(): self.canvas.core.launch_terminal(self.core_node.id) From 71df2a3b7f415363821301174bb25e8e14cee3b7 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:10:27 -0800 Subject: [PATCH 223/305] updated annotation text to be selectable/moveable, save annotation text with other shapes and reload from xml --- coretk/coretk/coreclient.py | 8 +++---- coretk/coretk/dialogs/shapemod.py | 36 +++++++------------------------ coretk/coretk/graph/graph.py | 31 +++++++++++--------------- coretk/coretk/graph/shape.py | 17 +++++++++++++++ daemon/core/xml/corexml.py | 6 +++--- 5 files changed, 45 insertions(+), 53 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5c9edfde..5b6f0fd9 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -322,11 +322,11 @@ class CoreClient: if shapes_config: shapes_config = json.loads(shapes_config) for shape_config in shapes_config: - logging.info("loading shape: %s", shapes_config) + logging.info("loading shape: %s", shape_config) shape_type = shape_config["type"] try: shape_type = ShapeType(shape_type) - x1, y1, x2, y2 = shape_config["iconcoords"] + coords = shape_config["iconcoords"] data = AnnotationData( shape_config["label"], shape_config["fontfamily"], @@ -337,11 +337,11 @@ class CoreClient: shape_config["width"], ) shape = Shape( - self.app, self.app.canvas, shape_type, x1, y1, x2, y2, data + self.app, self.app.canvas, shape_type, *coords, data=data ) self.app.canvas.shapes[shape.id] = shape except ValueError: - logging.debug("unknown shape: %s", shape_type) + logging.exception("unknown shape: %s", shape_type) for tag in LIFT_ORDER: self.app.canvas.tag_raise(tag) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index faff5ef8..04a617c8 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -15,12 +15,10 @@ class ShapeDialog(Dialog): def __init__(self, master, app, shape): if is_draw_shape(shape.shape_type): title = "Add Shape" - self.id = shape.id else: title = "Add Text" - self.id = None - self.canvas = app.canvas super().__init__(master, app, title, modal=True) + self.canvas = app.canvas self.fill = None self.border = None self.shape = shape @@ -146,12 +144,8 @@ class ShapeDialog(Dialog): self.border.config(background=color[1], text=color[1]) def cancel(self): - if ( - is_draw_shape(self.shape.shape_type) - and not self.canvas.shapes[self.id].created - ): - self.canvas.delete(self.id) - self.canvas.shapes.pop(self.id) + self.shape.delete() + self.canvas.shapes.pop(self.shape.id) self.destroy() def click_add(self): @@ -209,29 +203,15 @@ class ShapeDialog(Dialog): :return: nothing """ text = self.shape_text.get() - x = self.shape.x1 - y = self.shape.y1 text_font = self.make_font() - if self.shape.text_id is None: - tid = self.canvas.create_text( - x, y, text=text, fill=self.text_color, font=text_font, tags="text" - ) - self.shape.text_id = tid - self.id = tid - self.shape.id = tid - self.canvas.texts[tid] = self.shape - self.shape.created = True + self.canvas.itemconfig( + self.shape.id, text=text, fill=self.text_color, font=text_font + ) self.save_text() - print(self.canvas.texts) - # self.canvas.shapes[self.id].created = True - # else: - # self.canvas.itemconfig( - # self.shape.text_id, text=text, fill=self.text_color, font=f - # ) def add_shape(self): self.canvas.itemconfig( - self.id, + self.shape.id, fill=self.fill_color, dash="", outline=self.border_color, @@ -239,7 +219,7 @@ class ShapeDialog(Dialog): ) shape_text = self.shape_text.get() size = int(self.font_size.get()) - x0, y0, x1, y1 = self.canvas.bbox(self.id) + x0, y0, x1, y1 = self.canvas.bbox(self.shape.id) _y = y0 + 1.5 * size _x = (x0 + x1) / 2 text_font = self.make_font() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 57a2f6e4..bdcfe98b 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -10,7 +10,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils @@ -45,7 +45,6 @@ class CanvasGraph(tk.Canvas): self.nodes = {} self.edges = {} self.shapes = {} - self.texts = {} self.wireless_edges = {} self.drawing_edge = None self.grid = None @@ -244,14 +243,12 @@ class CanvasGraph(tk.Canvas): self.context = None else: if self.mode == GraphMode.ANNOTATION: - if is_draw_shape(self.annotation_type): - self.focus_set() - x, y = self.canvas_xy(event) - if self.shape_drawing: - self.shapes[self.selected].shape_complete(x, y) - self.shape_drawing = False - elif is_shape_text(self.annotation_type): - self.text.shape_complete(self.text.cursor_x, self.text.cursor_y) + self.focus_set() + x, y = self.canvas_xy(event) + if self.shape_drawing: + shape = self.shapes[self.selected] + shape.shape_complete(x, y) + self.shape_drawing = False else: self.focus_set() self.selected = self.get_selected(event) @@ -404,13 +401,10 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.ANNOTATION and selected is None: x, y = self.canvas_xy(event) - if is_draw_shape(self.annotation_type): - shape = Shape(self.app, self, self.annotation_type, x, y) - self.selected = shape.id - self.shapes[shape.id] = shape - self.shape_drawing = True - elif is_shape_text(self.annotation_type): - self.text = Shape(self.app, self, self.annotation_type, x, y) + shape = Shape(self.app, self, self.annotation_type, x, y) + self.selected = shape.id + self.shape_drawing = True + self.shapes[shape.id] = shape if self.mode == GraphMode.SELECT: if selected is not None: @@ -449,7 +443,8 @@ class CanvasGraph(tk.Canvas): if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: x, y = self.canvas_xy(event) - self.shapes[self.selected].shape_motion(x, y) + shape = self.shapes[self.selected] + shape.shape_motion(x, y) if ( self.mode == GraphMode.SELECT and self.selected is not None diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 7ef9530f..8188b8c2 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,3 +1,4 @@ +import logging from tkinter.font import Font from coretk.dialogs.shapemod import ShapeDialog @@ -76,6 +77,7 @@ class Shape: outline=self.shape_data.border_color, width=self.shape_data.border_width, ) + self.draw_shape_text() elif self.shape_type == ShapeType.RECTANGLE: self.id = self.canvas.create_rectangle( self.x1, @@ -88,7 +90,22 @@ class Shape: outline=self.shape_data.border_color, width=self.shape_data.border_width, ) + self.draw_shape_text() + elif self.shape_type == ShapeType.TEXT: + font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + self.id = self.canvas.create_text( + self.x1, + self.y1, + tags="shapetext", + text=self.shape_data.text, + fill=self.shape_data.text_color, + font=font, + ) + else: + logging.error("unknown shape type: %s", self.shape_type) + self.created = True + def draw_shape_text(self): if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index dcca6b80..96a5a6a5 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -123,9 +123,9 @@ class NodeElement: if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) position = etree.SubElement(self.element, "position") - add_attribute(position, "x", x) - add_attribute(position, "y", y) - add_attribute(position, "z", z) + add_attribute(position, "x", int(x)) + add_attribute(position, "y", int(y)) + add_attribute(position, "z", int(z)) add_attribute(position, "lat", lat) add_attribute(position, "lon", lon) add_attribute(position, "alt", alt) From 0308a4c8d76aaea803faa55a0b2b82d01e95a87c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:33:21 -0800 Subject: [PATCH 224/305] fixed temp issue for dealing with xml and node positions as floats, updated shape metadata to save bold/italic/underline options and read them back from xml --- coretk/coretk/coreclient.py | 3 + coretk/coretk/data/xmls/sample1.xml | 384 ++++++++++++++-------------- coretk/coretk/dialogs/shapemod.py | 12 +- coretk/coretk/graph/shape.py | 24 +- daemon/core/xml/corexml.py | 12 +- 5 files changed, 228 insertions(+), 207 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5b6f0fd9..2e009425 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -335,6 +335,9 @@ class CoreClient: shape_config["color"], shape_config["border"], shape_config["width"], + shape_config["bold"], + shape_config["italic"], + shape_config["underline"], ) shape = Shape( self.app, self.app.canvas, shape_type, *coords, data=data diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 0d5dd02d..8c61b7de 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,5 +1,5 @@ - + @@ -37,7 +37,7 @@ - + @@ -53,24 +53,30 @@ - - + + - - + + + + + + + + - + @@ -83,12 +89,6 @@ - - - - - - @@ -113,26 +113,30 @@ + + + + - - - - - - - - - - + + + + + + + + + + @@ -145,10 +149,6 @@ - - - - @@ -1063,6 +1063,164 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.8/32 + ipv6 address a::8/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.8 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1224,161 +1382,15 @@ bootquagga - - - /usr/local/etc/quagga - /var/run/quagga - + - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.8/32 - ipv6 address a::8/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.8 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh + sh defaultroute.sh - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + #!/bin/sh +# auto-generated by DefaultRoute service (utility.py) +ip route add default via 10.0.1.1 +ip route add default via a:1::1 @@ -1549,18 +1561,6 @@ bootquagga # auto-generated by DefaultRoute service (utility.py) ip route add default via 10.0.1.1 ip route add default via a:1::1 - - - - - - sh defaultroute.sh - - - #!/bin/sh -# auto-generated by DefaultRoute service (utility.py) -ip route add default via 10.0.1.1 -ip route add default via a:1::1 @@ -1842,8 +1842,8 @@ bootquagga - + diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 04a617c8..0410a8ae 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -33,9 +33,9 @@ class ShapeDialog(Dialog): self.fill_color = fill_color self.border_color = data.border_color self.border_width = tk.IntVar(value=0) - self.bold = tk.IntVar(value=data.bold) - self.italic = tk.IntVar(value=data.italic) - self.underline = tk.IntVar(value=data.underline) + self.bold = tk.BooleanVar(value=data.bold) + self.italic = tk.BooleanVar(value=data.italic) + self.underline = tk.BooleanVar(value=data.underline) self.top.columnconfigure(0, weight=1) self.draw() @@ -162,11 +162,11 @@ class ShapeDialog(Dialog): """ size = int(self.font_size.get()) text_font = [self.font.get(), size] - if self.bold.get() == 1: + if self.bold.get(): text_font.append("bold") - if self.italic.get() == 1: + if self.italic.get(): text_font.append("italic") - if self.underline.get() == 1: + if self.underline.get(): text_font.append("underline") return text_font diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 8188b8c2..22c676db 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,5 +1,4 @@ import logging -from tkinter.font import Font from coretk.dialogs.shapemod import ShapeDialog from coretk.graph.shapeutils import ShapeType @@ -17,9 +16,9 @@ class AnnotationData: fill_color="", border_color="#000000", border_width=1, - bold=0, - italic=0, - underline=0, + bold=False, + italic=False, + underline=False, ): self.text = text self.font = font @@ -92,7 +91,7 @@ class Shape: ) self.draw_shape_text() elif self.shape_type == ShapeType.TEXT: - font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + font = self.get_font() self.id = self.canvas.create_text( self.x1, self.y1, @@ -105,11 +104,21 @@ class Shape: logging.error("unknown shape type: %s", self.shape_type) self.created = True + def get_font(self): + font = [self.shape_data.font, self.shape_data.font_size] + if self.shape_data.bold: + font.append("bold") + if self.shape_data.italic: + font.append("italic") + if self.shape_data.underline: + font.append("underline") + return font + def draw_shape_text(self): if self.shape_data.text: x = (self.x1 + self.x2) / 2 y = self.y1 + 1.5 * self.shape_data.font_size - font = Font(family=self.shape_data.font, size=self.shape_data.font_size) + font = self.get_font() self.text_id = self.canvas.create_text( x, y, @@ -155,4 +164,7 @@ class Shape: "color": self.shape_data.fill_color, "border": self.shape_data.border_color, "width": self.shape_data.border_width, + "bold": self.shape_data.bold, + "italic": self.shape_data.italic, + "underline": self.shape_data.underline, } diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 96a5a6a5..9ba63395 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -117,15 +117,21 @@ class NodeElement: def add_position(self): x = self.node.position.x + if x is not None: + x = int(x) y = self.node.position.y + if y is not None: + y = int(y) z = self.node.position.z + if z is not None: + z = int(z) lat, lon, alt = None, None, None if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) position = etree.SubElement(self.element, "position") - add_attribute(position, "x", int(x)) - add_attribute(position, "y", int(y)) - add_attribute(position, "z", int(z)) + add_attribute(position, "x", x) + add_attribute(position, "y", y) + add_attribute(position, "z", z) add_attribute(position, "lat", lat) add_attribute(position, "lon", lon) add_attribute(position, "alt", alt) From 456e33187041ad2590209f16ebfc63001bf34a97 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 08:53:54 -0800 Subject: [PATCH 225/305] loading xml set canvas state to select mode and display that on toolbar --- coretk/coretk/coreclient.py | 31 +++++++------------------------ coretk/coretk/graph/graph.py | 1 + coretk/coretk/menuaction.py | 7 +++++++ coretk/coretk/menubar.py | 2 +- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b7b0a0df..64772b98 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -94,6 +94,7 @@ class CoreClient: self.service_configs = {} self.file_configs = {} self.mobility_players = {} + self.throughput = False def reset(self): # helpers @@ -186,22 +187,11 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - # interface_throughputs = event.interface_throughputs - # # print(interface_throughputs) - # # return - # # for i in interface_throughputs: - # # print("") - # # # return - # print(event) - # throughputs_belong_to_session = [] - # print(self.node_ids) - # for throughput in interface_throughputs: - # if throughput.node_id in self.node_ids: - # throughputs_belong_to_session.append(throughput) - # print(throughputs_belong_to_session) - self.app.canvas.throughput_draw.process_grpc_throughput_event( - event.interface_throughputs - ) + # print(event.interface_throughputs) + if self.throughput: + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs + ) def join_session(self, session_id, query_location=True): # update session and title @@ -296,6 +286,7 @@ class CoreClient: self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() + self.app.toolbar.select_button.invoke() self.app.statusbar.progress_bar.stop() def is_runtime(self): @@ -432,14 +423,6 @@ class CoreClient: ) process_time = time.perf_counter() - start logging.debug("start session(%s), result: %s", self.session_id, response.result) - # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) - # # print(self.client.set_service_defaults(self.session_id, {"router": ["DefaultRouter"]})) - # print(self.client.set_node_service(self.session_id, 1, "DefaultRoute", ["echo hello"], [], [])) - # - # # print(self.client.get_service_defaults(self.session_id)) - # - # # print(self.client.get_node_service(self.session_id, 1, "DefaultRoute")) - # # print(self.client.get_node_service_file(self.session_id, 1, "DefaultRoute", "defaultroute.sh")) self.app.statusbar.start_session_callback(process_time) # display mobility players diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index f4303713..0b9a20d2 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -77,6 +77,7 @@ class CanvasGraph(tk.Canvas): # set the private variables to default value self.mode = GraphMode.SELECT + self.annotation_type = None self.node_draw = None self.selected = None self.nodes.clear() diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index c4cc27bc..08298727 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -156,3 +156,10 @@ class MenuAction: def show_about(self): dialog = AboutDialog(self.app, self.app) dialog.show() + + def throughput(self): + throughput = self.app.core.throughput + if throughput: + self.app.core.throughput = False + else: + self.app.core.throughput = True diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 288a517f..ed5cb2ca 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -422,7 +422,7 @@ class Menubar(tk.Menu): menu = tk.Menu(self) self.create_observer_widgets_menu(menu) self.create_adjacency_menu(menu) - menu.add_command(label="Throughput", state=tk.DISABLED) + menu.add_checkbutton(label="Throughput", command=self.menuaction.throughput) menu.add_separator() menu.add_command(label="Configure Adjacency...", state=tk.DISABLED) menu.add_command(label="Configure Throughput...", state=tk.DISABLED) From 27131ef36785c9d792467d320297c5cda28a5981 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 10:07:21 -0800 Subject: [PATCH 226/305] start on input validation --- coretk/coretk/validation.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 coretk/coretk/validation.py diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py new file mode 100644 index 00000000..00563d68 --- /dev/null +++ b/coretk/coretk/validation.py @@ -0,0 +1,3 @@ +""" +input validation +""" From 5003e2356c605f9d0890972a5de5bf73517df731 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 12:07:42 -0800 Subject: [PATCH 227/305] small cleanup to canvas resize/redraw logic and updates to support saving/drawing gridlines and canvas dimensions --- coretk/coretk/coreclient.py | 21 +++++- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/graph/graph.py | 72 +++++++++++++-------- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 2e009425..43aa88ed 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -310,12 +310,26 @@ class CoreClient: logging.info("canvas metadata: %s", canvas_config) if canvas_config: canvas_config = json.loads(canvas_config) - wallpaper_style = canvas_config["wallpaper-style"] + + gridlines = canvas_config.get("gridlines", True) + self.app.canvas.show_grid.set(gridlines) + + fit_image = canvas_config.get("fit_image", False) + self.app.canvas.adjust_to_dim.set(fit_image) + + wallpaper_style = canvas_config.get("wallpaper-style", 1) self.app.canvas.scale_option.set(wallpaper_style) - wallpaper = canvas_config["wallpaper"] + + width = self.app.guiconfig["preferences"]["width"] + height = self.app.guiconfig["preferences"]["height"] + width, height = canvas_config.get("dimensions", [width, height]) + self.app.canvas.redraw_canvas(width, height) + + wallpaper = canvas_config.get("wallpaper") if wallpaper: wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) self.app.canvas.set_wallpaper(wallpaper) + self.app.canvas.update_grid() # load saved shapes shapes_config = config.get("shapes") @@ -469,6 +483,9 @@ class CoreClient: canvas_config = { "wallpaper": Path(self.app.canvas.wallpaper_file).name, "wallpaper-style": self.app.canvas.scale_option.get(), + "gridlines": self.app.canvas.show_grid.get(), + "fit_image": self.app.canvas.adjust_to_dim.get(), + "dimensions": self.app.canvas.width_and_height(), } canvas_config = json.dumps(canvas_config) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index fe4c3b62..8d6db86f 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -168,7 +168,7 @@ class SizeAndScaleDialog(Dialog): def click_apply(self): width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.redraw_grid(width, height) + self.canvas.redraw_canvas(width, height) if self.canvas.wallpaper: self.canvas.redraw() location = self.app.core.location diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index bdcfe98b..720ce560 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -14,7 +14,17 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils -ABOVE_WALLPAPER = ["edge", "linkinfo", "wireless", "antenna", "nodename", "node"] +ABOVE_WALLPAPER = [ + "gridline", + "shape", + "shapetext", + "edge", + "linkinfo", + "wireless", + "antenna", + "nodename", + "node", +] CANVAS_COMPONENT_TAGS = [ "edge", "node", @@ -49,10 +59,11 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = None self.grid = None self.setup_bindings() - self.draw_grid(width, height) self.core = core self.throughput_draw = Throughput(self, core) self.shape_drawing = False + self.default_width = width + self.default_height = height # background related self.wallpaper_id = None @@ -63,6 +74,22 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # draw base canvas + self.draw_canvas() + self.draw_grid() + + def draw_canvas(self): + self.grid = self.create_rectangle( + 0, + 0, + self.default_width, + self.default_height, + outline="#000000", + fill="#ffffff", + width=1, + tags="rectangle", + ) + def reset_and_redraw(self, session): """ Reset the private variables CanvasGraph object, redraw nodes given the new grpc @@ -100,7 +127,7 @@ class CanvasGraph(tk.Canvas): self.bind("", self.ctrl_click) self.bind("", self.double_click) - def draw_grid(self, width=1000, height=800): + def draw_grid(self): """ Create grid @@ -109,16 +136,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - self.grid = self.create_rectangle( - 0, - 0, - width, - height, - outline="#000000", - fill="#ffffff", - width=1, - tags="rectangle", - ) + width, height = self.width_and_height() + width = int(width) + height = int(height) for i in range(0, width, 27): self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") for i in range(0, height, 27): @@ -584,36 +604,31 @@ class CanvasGraph(tk.Canvas): img_w = image_tk.width() img_h = image_tk.height() self.delete(self.wallpaper_id) - self.delete("rectangle") - self.delete("gridline") - self.draw_grid(img_w, img_h) + self.redraw_canvas(img_w, img_h) self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) self.wallpaper_drawn = image_tk - def redraw_grid(self, width, height): + def redraw_canvas(self, width, height): """ redraw grid with new dimension :return: nothing """ + # resize canvas and scrollregion self.config(scrollregion=(0, 0, width + 200, height + 200)) + self.coords(self.grid, 0, 0, width, height) - # delete previous grid - self.delete("rectangle") + # redraw gridlines to new canvas size self.delete("gridline") - - # redraw - self.draw_grid(width=width, height=height) - - # hide/show grid + self.draw_grid() self.update_grid() def redraw(self): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: - print(self.scale_option.get()) option = ScaleOption(self.scale_option.get()) + logging.info("canvas scale option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -623,11 +638,14 @@ class CanvasGraph(tk.Canvas): elif option == ScaleOption.TILED: logging.warning("tiled background not implemented yet") + # raise items above wallpaper + for component in ABOVE_WALLPAPER: + self.tag_raise(component) + def update_grid(self): logging.info("updating grid show: %s", self.show_grid.get()) if self.show_grid.get(): self.itemconfig("gridline", state=tk.NORMAL) - self.tag_raise("gridline") else: self.itemconfig("gridline", state=tk.HIDDEN) @@ -638,8 +656,6 @@ class CanvasGraph(tk.Canvas): self.wallpaper = img self.wallpaper_file = filename self.redraw() - for component in ABOVE_WALLPAPER: - self.tag_raise(component) else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) From 4ca9ab910e40c46a724f654944f3c549e8e8b253 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:05:07 -0800 Subject: [PATCH 228/305] added spinbox missing from 3.6, added spinbox theme, updated config gen to display a file picker for labels with file in it --- coretk/coretk/dialogs/dialog.py | 1 + coretk/coretk/themes.py | 8 ++++++++ coretk/coretk/widgets.py | 30 +++++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 29362960..d814a47c 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -9,6 +9,7 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): super().__init__(master) + self.geometry("800x600") self.withdraw() self.app = app self.modal = modal diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index ad91276a..b80caf87 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -88,6 +88,14 @@ def load(style): }, "map": {"fieldbackground": [("disabled", Colors.frame)]}, }, + "TSpinbox": { + "configure": { + "fieldbackground": Colors.white, + "foreground": Colors.black, + "padding": (2, 0), + }, + "map": {"fieldbackground": [("disabled", Colors.frame)]}, + }, "TCombobox": { "configure": { "fieldbackground": Colors.white, diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 760846a3..486370e0 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -1,7 +1,7 @@ import logging import tkinter as tk from functools import partial -from tkinter import font, ttk +from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 @@ -19,6 +19,12 @@ INT_TYPES = { PAD = 5 +def file_button_click(value): + file_path = filedialog.askopenfilename(title="Select File") + if file_path: + value.set(file_path) + + class FrameScroll(ttk.LabelFrame): def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) @@ -100,8 +106,18 @@ class ConfigFrame(FrameScroll): combobox.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + if "file" in option.label: + file_frame = ttk.Frame(frame) + file_frame.grid(row=index, column=1, sticky="ew", pady=pady) + file_frame.columnconfigure(0, weight=1) + entry = ttk.Entry(file_frame, textvariable=value) + entry.grid(row=0, column=0, sticky="ew", padx=padx) + func = partial(file_button_click, value) + button = ttk.Button(file_frame, text="...", command=func) + button.grid(row=0, column=1) + else: + entry = ttk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry(frame, textvariable=value) @@ -179,3 +195,11 @@ class CodeText(ScrolledText): relief=tk.FLAT, **kwargs ) + + +class Spinbox(ttk.Entry): + def __init__(self, master=None, **kwargs): + super().__init__(master, "ttk::spinbox", **kwargs) + + def set(self, value): + self.tk.call(self._w, "set", value) From 2a29cd1fe59b814db5f3e9c9aa861d9ef8df27b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:09:01 -0800 Subject: [PATCH 229/305] after joining a session default to select mode --- coretk/coretk/coreclient.py | 2 ++ coretk/coretk/menuaction.py | 1 - coretk/coretk/toolbar.py | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 43aa88ed..7e86c87c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -295,11 +295,13 @@ class CoreClient: response = self.client.get_session_metadata(self.session_id) self.parse_metadata(response.config) + # update ui to represent current state if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() else: self.app.toolbar.design_frame.tkraise() self.app.statusbar.progress_bar.stop() + self.app.toolbar.click_selection() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index c4cc27bc..f7f9c579 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -109,7 +109,6 @@ class MenuAction: self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.open_xml, args=([file_path])) thread.start() - # self.app.core.open_xml(file_path) def gui_preferences(self): dialog = PreferencesDialog(self.app, self.app) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 1f1e734f..c9efa74c 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -40,8 +40,6 @@ class Toolbar(ttk.Frame): self.network_button = None self.annotation_button = None - # runtime buttons - # frames self.design_frame = None self.runtime_frame = None From afdacf0c94688612bea5a4018903a8b3145bb814 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:19:45 -0800 Subject: [PATCH 230/305] set to selection mode when starting/stopping a session as well as joining for runtime/design states --- coretk/coretk/coreclient.py | 3 ++- coretk/coretk/toolbar.py | 24 ++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 7e86c87c..6edc22cf 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -298,10 +298,11 @@ class CoreClient: # update ui to represent current state if self.is_runtime(): self.app.toolbar.runtime_frame.tkraise() + self.app.toolbar.click_runtime_selection() else: self.app.toolbar.design_frame.tkraise() + self.app.toolbar.click_selection() self.app.statusbar.progress_bar.stop() - self.app.toolbar.click_selection() def is_runtime(self): return self.state == core_pb2.SessionState.RUNTIME diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index c9efa74c..55b2356e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -40,6 +40,9 @@ class Toolbar(ttk.Frame): self.network_button = None self.annotation_button = None + # runtime buttons + self.runtime_select_button = None + # frames self.design_frame = None self.runtime_frame = None @@ -89,6 +92,11 @@ class Toolbar(ttk.Frame): self.annotation_button.state(["!pressed"]) button.state(["pressed"]) + def runtime_select(self, button): + logging.info("selecting runtime button: %s", button) + self.runtime_select_button.state(["!pressed"]) + button.state(["pressed"]) + def draw_runtime_frame(self): self.runtime_frame = ttk.Frame(self) self.runtime_frame.grid(row=0, column=0, sticky="nsew") @@ -100,10 +108,10 @@ class Toolbar(ttk.Frame): self.click_stop, "stop the session", ) - self.create_button( + self.runtime_select_button = self.create_button( self.runtime_frame, icon(ImageEnum.SELECT), - self.click_selection, + self.click_runtime_selection, "selection tool", ) # self.create_observe_button() @@ -188,6 +196,11 @@ class Toolbar(ttk.Frame): self.design_select(self.select_button) self.app.canvas.mode = GraphMode.SELECT + def click_runtime_selection(self): + logging.debug("clicked selection tool") + self.runtime_select(self.runtime_select_button) + self.app.canvas.mode = GraphMode.SELECT + def click_start(self): """ Start session handler redraw buttons, send node and link messages to grpc @@ -200,6 +213,7 @@ class Toolbar(ttk.Frame): thread = threading.Thread(target=self.app.core.start_session) thread.start() self.runtime_frame.tkraise() + self.click_runtime_selection() def click_link(self): logging.debug("Click LINK button") @@ -355,11 +369,9 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - for cid in self.app.canvas.find_withtag("wireless"): - self.app.canvas.itemconfig(cid, state="hidden") - # self.app.canvas.delete("wireless") - + self.app.canvas.delete("wireless") self.design_frame.tkraise() + self.click_selection() def update_annotation(self, image, shape_type): logging.info("clicked annotation: ") From b04e61cceeb159598e5dbe00414c1d2da5df4802 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 14:13:21 -0800 Subject: [PATCH 231/305] added common tags file for canvas created items, to reduce duplicate strings --- coretk/coretk/coreclient.py | 16 ++-------- coretk/coretk/dialogs/shapemod.py | 3 +- coretk/coretk/graph/edges.py | 5 +-- coretk/coretk/graph/graph.py | 51 +++++++++---------------------- coretk/coretk/graph/linkinfo.py | 15 +++++++-- coretk/coretk/graph/node.py | 8 ++--- coretk/coretk/graph/shape.py | 13 ++++---- coretk/coretk/graph/tags.py | 35 +++++++++++++++++++++ coretk/coretk/toolbar.py | 3 +- 9 files changed, 81 insertions(+), 68 deletions(-) create mode 100644 coretk/coretk/graph/tags.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 6edc22cf..44e4ee99 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -11,24 +11,12 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog +from coretk.graph import tags from coretk.graph.shape import AnnotationData, Shape from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils -LIFT_ORDER = [ - "wallpaper", - "shape", - "gridline", - "shapetext", - "text", - "edge", - "antenna", - "nodename", - "linkinfo", - "node", -] - OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -363,7 +351,7 @@ class CoreClient: except ValueError: logging.exception("unknown shape: %s", shape_type) - for tag in LIFT_ORDER: + for tag in tags.ABOVE_WALLPAPER_TAGS: self.app.canvas.tag_raise(tag) def create_new_session(self): diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 0410a8ae..e0085a8c 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog +from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] @@ -230,7 +231,7 @@ class ShapeDialog(Dialog): text=shape_text, fill=self.text_color, font=text_font, - tags="shapetext", + tags=tags.SHAPE_TEXT, ) self.shape.created = True else: diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 27d1e3ba..94d847bc 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,6 @@ import tkinter as tk +from coretk.graph import tags from coretk.nodeutils import NodeUtils @@ -10,7 +11,7 @@ class CanvasWirelessEdge: self.dst = dst self.canvas = canvas self.id = self.canvas.create_line( - *position, tags="wireless", width=1.5, fill="#009933" + *position, tags=tags.WIRELESS_EDGE, width=1.5, fill="#009933" ) def delete(self): @@ -40,7 +41,7 @@ class CanvasEdge: self.dst_interface = None self.canvas = canvas self.id = self.canvas.create_line( - x1, y1, x2, y2, tags="edge", width=self.width, fill="#ff0000" + x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" ) self.token = None self.link_info = None diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 720ce560..c2c0935d 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -5,6 +5,7 @@ from PIL import Image, ImageTk from core.api.grpc import core_pb2 from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput @@ -14,30 +15,6 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils -ABOVE_WALLPAPER = [ - "gridline", - "shape", - "shapetext", - "edge", - "linkinfo", - "wireless", - "antenna", - "nodename", - "node", -] -CANVAS_COMPONENT_TAGS = [ - "edge", - "node", - "nodename", - "wallpaper", - "linkinfo", - "antenna", - "wireless", - "selectednodes", - "shape", - "shapetext", -] - class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height, cnf=None, **kwargs): @@ -99,7 +76,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # delete any existing drawn items - for tag in CANVAS_COMPONENT_TAGS: + for tag in tags.COMPONENT_TAGS: self.delete(tag) # set the private variables to default value @@ -140,10 +117,10 @@ class CanvasGraph(tk.Canvas): width = int(width) height = int(height) for i in range(0, width, 27): - self.create_line(i, 0, i, height, dash=(2, 4), tags="gridline") + self.create_line(i, 0, i, height, dash=(2, 4), tags=tags.GRIDLINE) for i in range(0, height, 27): - self.create_line(0, i, width, i, dash=(2, 4), tags="gridline") - self.tag_lower("gridline") + self.create_line(0, i, width, i, dash=(2, 4), tags=tags.GRIDLINE) + self.tag_lower(tags.GRIDLINE) self.tag_lower(self.grid) def add_wireless_edge(self, src, dst): @@ -214,7 +191,7 @@ class CanvasGraph(tk.Canvas): canvas_node_two.interfaces.append(link.interface_two) # raise the nodes so they on top of the links - self.tag_raise("node") + self.tag_raise(tags.NODE) def canvas_xy(self, event): """ @@ -336,7 +313,7 @@ class CanvasGraph(tk.Canvas): (x0 - 6, y0 - 6, x1 + 6, y1 + 6), activedash=True, dash="-", - tags="selectednodes", + tags=tags.SELECTION, ) self.selection[object_id] = selection_id else: @@ -553,7 +530,7 @@ class CanvasGraph(tk.Canvas): self.delete(self.wallpaper_id) # place left corner of image to the left corner of the canvas self.wallpaper_id = self.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags="wallpaper" + (cropx / 2, cropy / 2), image=cropped_tk, tags=tags.WALLPAPER ) self.wallpaper_drawn = cropped_tk @@ -581,7 +558,7 @@ class CanvasGraph(tk.Canvas): # place the center of the image at the center of the canvas self.delete(self.wallpaper_id) self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags="wallpaper" + (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags=tags.WALLPAPER ) self.wallpaper_drawn = cropped_tk @@ -595,7 +572,7 @@ class CanvasGraph(tk.Canvas): image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) self.delete(self.wallpaper_id) self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=image, tags="wallpaper" + (canvas_w / 2, canvas_h / 2), image=image, tags=tags.WALLPAPER ) self.wallpaper_drawn = image @@ -619,7 +596,7 @@ class CanvasGraph(tk.Canvas): self.coords(self.grid, 0, 0, width, height) # redraw gridlines to new canvas size - self.delete("gridline") + self.delete(tags.GRIDLINE) self.draw_grid() self.update_grid() @@ -639,15 +616,15 @@ class CanvasGraph(tk.Canvas): logging.warning("tiled background not implemented yet") # raise items above wallpaper - for component in ABOVE_WALLPAPER: + for component in tags.ABOVE_WALLPAPER_TAGS: self.tag_raise(component) def update_grid(self): logging.info("updating grid show: %s", self.show_grid.get()) if self.show_grid.get(): - self.itemconfig("gridline", state=tk.NORMAL) + self.itemconfig(tags.GRIDLINE, state=tk.NORMAL) else: - self.itemconfig("gridline", state=tk.HIDDEN) + self.itemconfig(tags.GRIDLINE, state=tk.HIDDEN) def set_wallpaper(self, filename): logging.info("setting wallpaper: %s", filename) diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index 15c08e45..decca3bb 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import font from core.api.grpc import core_pb2 +from coretk.graph import tags TEXT_DISTANCE = 0.30 @@ -52,10 +53,20 @@ class LinkInfo: f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" ) self.id1 = self.canvas.create_text( - x1, y1, text=label_one, justify=tk.CENTER, font=self.font, tags="linkinfo" + x1, + y1, + text=label_one, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, ) self.id2 = self.canvas.create_text( - x2, y2, text=label_two, justify=tk.CENTER, font=self.font, tags="linkinfo" + x2, + y2, + text=label_two, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, ) def recalculate_info(self): diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 83b29da2..1dcb8959 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -7,6 +7,7 @@ from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip from coretk.nodeutils import NodeUtils @@ -23,7 +24,7 @@ class CanvasNode: x = self.core_node.position.x y = self.core_node.position.y self.id = self.canvas.create_image( - x, y, anchor=tk.CENTER, image=self.image, tags="node" + x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) image_box = self.canvas.bbox(self.id) y = image_box[3] + NODE_TEXT_OFFSET @@ -32,7 +33,7 @@ class CanvasNode: x, y, text=self.core_node.name, - tags="nodename", + tags=tags.NODE_NAME, font=text_font, fill="#0000CD", ) @@ -62,13 +63,12 @@ class CanvasNode: def add_antenna(self): x, y = self.canvas.coords(self.id) offset = len(self.antennae) * 8 - antenna_id = self.canvas.create_image( x - 16 + offset, y - 23, anchor=tk.CENTER, image=NodeUtils.ANTENNA_ICON, - tags="antenna", + tags=tags.ANTENNA, ) self.antennae.append(antenna_id) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 22c676db..2928a695 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -1,10 +1,9 @@ import logging from coretk.dialogs.shapemod import ShapeDialog +from coretk.graph import tags from coretk.graph.shapeutils import ShapeType -ABOVE_COMPONENT = ["gridline", "edge", "linkinfo", "antenna", "node", "nodename"] - class AnnotationData: def __init__( @@ -70,7 +69,7 @@ class Shape: self.y1, self.x2, self.y2, - tags="shape", + tags=tags.SHAPE, dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, @@ -83,7 +82,7 @@ class Shape: self.y1, self.x2, self.y2, - tags="shape", + tags=tags.SHAPE, dash=dash, fill=self.shape_data.fill_color, outline=self.shape_data.border_color, @@ -95,7 +94,7 @@ class Shape: self.id = self.canvas.create_text( self.x1, self.y1, - tags="shapetext", + tags=tags.SHAPE_TEXT, text=self.shape_data.text, fill=self.shape_data.text_color, font=font, @@ -122,7 +121,7 @@ class Shape: self.text_id = self.canvas.create_text( x, y, - tags="shapetext", + tags=tags.SHAPE_TEXT, text=self.shape_data.text, fill=self.shape_data.text_color, font=font, @@ -132,7 +131,7 @@ class Shape: self.canvas.coords(self.id, self.x1, self.y1, x1, y1) def shape_complete(self, x, y): - for component in ABOVE_COMPONENT: + for component in tags.ABOVE_SHAPE: self.canvas.tag_raise(component) s = ShapeDialog(self.app, self.app, self) s.show() diff --git a/coretk/coretk/graph/tags.py b/coretk/coretk/graph/tags.py new file mode 100644 index 00000000..42f4ff5f --- /dev/null +++ b/coretk/coretk/graph/tags.py @@ -0,0 +1,35 @@ +GRIDLINE = "gridline" +SHAPE = "shape" +SHAPE_TEXT = "shapetext" +EDGE = "edge" +LINK_INFO = "linkinfo" +WIRELESS_EDGE = "wireless" +ANTENNA = "antenna" +NODE_NAME = "nodename" +NODE = "node" +WALLPAPER = "wallpaper" +SELECTION = "selectednodes" +ABOVE_WALLPAPER_TAGS = [ + GRIDLINE, + SHAPE, + SHAPE_TEXT, + EDGE, + LINK_INFO, + WIRELESS_EDGE, + ANTENNA, + NODE, + NODE_NAME, +] +ABOVE_SHAPE = [GRIDLINE, EDGE, LINK_INFO, WIRELESS_EDGE, ANTENNA, NODE, NODE_NAME] +COMPONENT_TAGS = [ + EDGE, + NODE, + NODE_NAME, + WALLPAPER, + LINK_INFO, + ANTENNA, + WIRELESS_EDGE, + SELECTION, + SHAPE, + SHAPE_TEXT, +] diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 55b2356e..a47cac5e 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -5,6 +5,7 @@ from functools import partial from tkinter import ttk from coretk.dialogs.customnodes import CustomNodesDialog +from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images @@ -369,7 +370,7 @@ class Toolbar(ttk.Frame): self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() - self.app.canvas.delete("wireless") + self.app.canvas.delete(tags.WIRELESS_EDGE) self.design_frame.tkraise() self.click_selection() From c36a72bc168df00246d8b514d63848e9a0701a30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 15:27:51 -0800 Subject: [PATCH 232/305] small tweak to statusbar to define the different sections --- coretk/coretk/statusbar.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index c4263e85..8910c742 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -25,33 +25,42 @@ class StatusBar(ttk.Frame): self.columnconfigure(3, weight=1) self.columnconfigure(4, weight=1) + frame = ttk.Frame(self, borderwidth=1, relief=tk.RIDGE) + frame.grid(row=0, column=0, sticky="ew") + frame.columnconfigure(0, weight=1) self.progress_bar = ttk.Progressbar( - self, orient="horizontal", mode="indeterminate" + frame, orient="horizontal", mode="indeterminate" ) - self.progress_bar.grid(row=0, column=0, sticky="ew") + self.progress_bar.grid(sticky="ew") - self.status = ttk.Label(self, textvariable=self.statusvar, anchor=tk.CENTER) - self.statusvar.set("status") + self.status = ttk.Label( + self, + textvariable=self.statusvar, + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, + ) self.status.grid(row=0, column=1, sticky="ew") - self.zoom = ttk.Label(self, text="zoom", anchor=tk.CENTER) + self.zoom = ttk.Label( + self, text="ZOOM TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.zoom.grid(row=0, column=2, sticky="ew") - self.cpu_usage = ttk.Label(self, text="cpu usage", anchor=tk.CENTER) + self.cpu_usage = ttk.Label( + self, text="CPU TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - self.emulation_light = ttk.Label(self, text="emulation light", anchor=tk.CENTER) + self.emulation_light = ttk.Label( + self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + ) self.emulation_light.grid(row=0, column=4, sticky="ew") def start_session_callback(self, process_time): - num_nodes = len(self.app.core.canvas_nodes) - num_links = len(self.app.core.links) self.progress_bar.stop() - self.statusvar.set( - "Network topology instantiated in %s seconds (%s node(s) and %s link(s))" - % ("%.3f" % process_time, num_nodes, num_links) - ) + self.statusvar.set(f"Session started in {process_time:.3f} seconds") def stop_session_callback(self, cleanup_time): self.progress_bar.stop() - self.statusvar.set("Cleanup completed in %s seconds" % "%.3f" % cleanup_time) + self.statusvar.set(f"Stopped session in {cleanup_time:.3f} seconds") From 088a69d9d912caccc1b238b1602e2066f0a50b55 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:23:09 -0800 Subject: [PATCH 233/305] catching grpc error on setup, displaying error dialog and exiting app --- coretk/coretk/app.py | 3 +++ coretk/coretk/coreclient.py | 48 ++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 2dd2c5ca..4d51cd1e 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -98,6 +98,9 @@ class Application(tk.Frame): def save_config(self): appconfig.save(self.guiconfig) + def close(self): + self.master.destroy() + if __name__ == "__main__": log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 44e4ee99..5c728477 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,6 +6,9 @@ import logging import os import time from pathlib import Path +from tkinter import messagebox + +import grpc from core.api.grpc import client, core_pb2 from coretk import appconfig @@ -386,28 +389,35 @@ class CoreClient: :return: existing sessions """ - self.client.connect() + try: + self.client.connect() - # get service information - response = self.client.get_services() - for service in response.services: - group_services = self.services.setdefault(service.group, set()) - group_services.add(service.name) + # get service information + response = self.client.get_services() + for service in response.services: + group_services = self.services.setdefault(service.group, set()) + group_services.add(service.name) - # if there are no sessions, create a new session, else join a session - response = self.client.get_sessions() - logging.info("current sessions: %s", response) - sessions = response.sessions - if len(sessions) == 0: - self.create_new_session() - else: - dialog = SessionsDialog(self.app, self.app) - dialog.show() + # if there are no sessions, create a new session, else join a session + response = self.client.get_sessions() + logging.info("current sessions: %s", response) + sessions = response.sessions + if len(sessions) == 0: + self.create_new_session() + else: + dialog = SessionsDialog(self.app, self.app) + dialog.show() - response = self.client.get_service_defaults(self.session_id) - self.default_services = { - x.node_type: set(x.services) for x in response.defaults - } + response = self.client.get_service_defaults(self.session_id) + self.default_services = { + x.node_type: set(x.services) for x in response.defaults + } + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.UNAVAILABLE: + messagebox.showerror("Server Error", e.details()) + else: + messagebox.showerror("GRPC Error", e.details()) + self.app.close() def get_session_state(self): response = self.client.get_session(self.session_id) From 33e3a4614690c0ab552e921e89eef8d7ce08a274 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:25:29 -0800 Subject: [PATCH 234/305] better gui startup connection failure message --- coretk/coretk/coreclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 5c728477..376c739c 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -414,7 +414,8 @@ class CoreClient: } except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE: - messagebox.showerror("Server Error", e.details()) + + messagebox.showerror("Server Error", "CORE Daemon Unavailable") else: messagebox.showerror("GRPC Error", e.details()) self.app.close() From 0b3f3a5166aefc7da80bdc073b9f0f7a8fa27178 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:33:32 -0800 Subject: [PATCH 235/305] start working on input validation --- coretk/coretk/dialogs/canvassizeandscale.py | 34 ++++- coretk/coretk/dialogs/nodeconfig.py | 7 +- coretk/coretk/validation.py | 148 ++++++++++++++++++++ coretk/coretk/widgets.py | 18 ++- 4 files changed, 200 insertions(+), 7 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index fe4c3b62..770d40df 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -4,6 +4,7 @@ size and scale import tkinter as tk from tkinter import font, ttk +from coretk import validation from coretk.dialogs.dialog import Dialog PAD = 5 @@ -37,6 +38,12 @@ class SizeAndScaleDialog(Dialog): self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) + self.vcmd_canvas_int = validation.validate_command( + app.master, validation.check_canvas_int + ) + self.vcmd_canvas_float = validation.validate_command( + app.master, validation.check_canvas_float + ) self.draw() def draw(self): @@ -47,6 +54,11 @@ class SizeAndScaleDialog(Dialog): self.draw_save_as_default() self.draw_buttons() + def focus_out(self, event): + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, 0) + def draw_size(self): label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") @@ -59,11 +71,22 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.pixel_width) + entry = ttk.Entry( + frame, + textvariable=self.pixel_width, + validate="key", + validatecommand=(self.vcmd_canvas_int, "%P"), + ) entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.bind("", self.focus_out) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.pixel_height) + entry = ttk.Entry( + frame, + textvariable=self.pixel_height, + validate="key", + validatecommand=(self.vcmd_canvas_int, "%P"), + ) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -94,7 +117,12 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.scale) + entry = ttk.Entry( + frame, + textvariable=self.scale, + validate="key", + validatecommand=(self.vcmd_canvas_float, "%P"), + ) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index f296e4cf..185a8f62 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,6 +3,7 @@ import tkinter as tk from functools import partial from tkinter import ttk +import coretk.validation as validation from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService @@ -69,7 +70,10 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - entry = ttk.Entry(frame, textvariable=self.name) + vcmd = self.app.master.register(validation.check_node_name) + entry = ttk.Entry( + frame, textvariable=self.name, validate="key", validatecommand=(vcmd, "%P") + ) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -206,5 +210,4 @@ class NodeConfigDialog(Dialog): # redraw self.canvas_node.redraw() - self.destroy() diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 00563d68..400cac18 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -1,3 +1,151 @@ """ input validation """ +import logging +import tkinter as tk + + +def validate_command(master, func): + return master.register(func) + + +def check_positive_int(s): + logging.debug("int validation...") + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + +def check_positive_float(s): + logging.debug("float validation...") + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + +def check_node_name(name): + logging.debug("node name validation...") + if len(name) <= 0: + return False + for char in name: + if not char.isalnum() and char != "_": + return False + return True + + +def check_canvas_int(s): + logging.debug("int validation...") + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False + + +def check_canvas_float(s): + logging.debug("canvas float validation") + if not s: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False + + +def check_interface(name): + logging.debug("interface name validation...") + if len(name) <= 0: + return False, "Interface name cannot be an empty string" + for char in name: + if not char.isalnum() and char != "_": + return ( + False, + "Interface name can only contain alphanumeric letter (a-z) and (0-9) or underscores (_)", + ) + return True, "" + + +def combine_message(key, current_validation, current_message, res, msg): + if not res: + current_validation = res + current_message = current_message + key + ": " + msg + "\n\n" + return current_validation, current_message + + +def check_wlan_config(config): + result = True + message = "" + checks = ["bandwidth", "delay", "error", "jitter", "range"] + for check in checks: + if check in ["bandwidth", "delay", "jitter"]: + res, msg = check_positive_int(config[check].value) + result, message = combine_message(check, result, message, res, msg) + elif check in ["range", "error"]: + res, msg = check_positive_float(config[check].value) + result, message = combine_message(check, result, message, res, msg) + return result, message + + +def check_size_and_scale(dialog): + result = True + message = "" + try: + pixel_width = dialog.pixel_width.get() + if pixel_width < 0: + result, message = combine_message( + "pixel width", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "pixel width", + result, + message, + False, + "invalid value, input non-negative float", + ) + try: + pixel_height = dialog.pixel_height.get() + if pixel_height < 0: + result, message = combine_message( + "pixel height", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "pixel height", + result, + message, + False, + "invalid value, input non-negative float", + ) + try: + scale = dialog.scale.get() + if scale <= 0: + result, message = combine_message( + "scale", result, message, False, "cannot be negative" + ) + except tk.TclError: + result, message = combine_message( + "scale", result, message, False, "invalid value, input non-negative float" + ) + # pixel_height = dialog.pixel_height.get() + # print(pixel_width, pixel_height) + # res, msg = check_positive_int(pixel_width) + # result, message = combine_message("pixel width", result, message, res, msg) + # res, msg = check_positive_int(pixel_height) + # result, message = combine_message("pixel height", result, message, res, msg) + return result, message diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 760846a3..2fddbf9f 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk import validation INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -60,6 +61,7 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master, app, config, **kw): super().__init__(master, app, ttk.Notebook, **kw) + self.app = app self.config = config self.values = {} @@ -67,6 +69,8 @@ class ConfigFrame(FrameScroll): padx = 2 pady = 2 group_mapping = {} + vcmd_int = self.app.master.register(validation.check_positive_int) + vcmd_float = self.app.master.register(validation.check_positive_float) for key in self.config: option = self.config[key] group = group_mapping.setdefault(option.group, []) @@ -104,11 +108,21 @@ class ConfigFrame(FrameScroll): entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type in INT_TYPES: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(vcmd_int, "%P"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(vcmd_float, "%P"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From 7039a3682e86cd7f223099aec7e14ae01a3da244 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:37:52 -0800 Subject: [PATCH 236/305] fix merge conflict --- coretk/coretk/coreclient.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 72ab7cdc..26b9c9c8 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -282,11 +282,7 @@ class CoreClient: self.app.toolbar.click_runtime_selection() else: self.app.toolbar.design_frame.tkraise() - # <<<<<<< HEAD - # self.app.toolbar.select_button.invoke() - # ======= self.app.toolbar.click_selection() - # >>>>>>> coretk self.app.statusbar.progress_bar.stop() def is_runtime(self): From d78ef86cef6a6e9b5f6a35fe20bf4e0943b54628 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 22:50:26 -0800 Subject: [PATCH 237/305] added grpc error display and updated grpc calls to catch and display grpc exceptions --- coretk/coretk/coreclient.py | 291 ++++++++++-------- coretk/coretk/dialogs/emaneconfig.py | 13 +- coretk/coretk/dialogs/mobilityconfig.py | 9 +- coretk/coretk/dialogs/mobilityplayer.py | 30 +- coretk/coretk/dialogs/serviceconfiguration.py | 132 ++++---- coretk/coretk/dialogs/sessionoptions.py | 28 +- coretk/coretk/dialogs/sessions.py | 17 +- coretk/coretk/dialogs/wlanconfig.py | 9 +- coretk/coretk/errors.py | 8 + coretk/coretk/graph/node.py | 10 +- coretk/coretk/menuaction.py | 10 +- 11 files changed, 329 insertions(+), 228 deletions(-) create mode 100644 coretk/coretk/errors.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 376c739c..1b1a30da 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -6,7 +6,6 @@ import logging import os import time from pathlib import Path -from tkinter import messagebox import grpc @@ -14,6 +13,7 @@ from core.api.grpc import client, core_pb2 from coretk import appconfig from coretk.dialogs.mobilityplayer import MobilityPlayer from coretk.dialogs.sessions import SessionsDialog +from coretk.errors import show_grpc_error from coretk.graph import tags from coretk.graph.shape import AnnotationData, Shape from coretk.graph.shapeutils import ShapeType @@ -206,85 +206,92 @@ class CoreClient: self.reset() # get session data - response = self.client.get_session(self.session_id) - session = response.session - self.state = session.state - self.client.events(self.session_id, self.handle_events) - self.client.throughputs(self.handle_throughputs) + try: + response = self.client.get_session(self.session_id) + session = response.session + self.state = session.state + self.client.events(self.session_id, self.handle_events) + self.client.throughputs(self.handle_throughputs) - # get location - if query_location: - response = self.client.get_session_location(self.session_id) - self.location = response.location + # get location + if query_location: + response = self.client.get_session_location(self.session_id) + self.location = response.location - # get emane models - response = self.client.get_emane_models(self.session_id) - self.emane_models = response.models + # get emane models + response = self.client.get_emane_models(self.session_id) + self.emane_models = response.models - # get hooks - response = self.client.get_hooks(self.session_id) - for hook in response.hooks: - self.hooks[hook.file] = hook + # get hooks + response = self.client.get_hooks(self.session_id) + for hook in response.hooks: + self.hooks[hook.file] = hook - # get mobility configs - response = self.client.get_mobility_configs(self.session_id) - for node_id in response.configs: - node_config = response.configs[node_id].config - self.mobility_configs[node_id] = node_config + # get mobility configs + response = self.client.get_mobility_configs(self.session_id) + for node_id in response.configs: + node_config = response.configs[node_id].config + self.mobility_configs[node_id] = node_config - # get emane config - response = self.client.get_emane_config(self.session_id) - self.emane_config = response.config + # get emane config + response = self.client.get_emane_config(self.session_id) + self.emane_config = response.config - # get emane model config - response = self.client.get_emane_model_configs(self.session_id) - for _id in response.configs: - config = response.configs[_id] - interface = None - node_id = _id - if _id >= 1000: - interface = _id % 1000 - node_id = int(_id / 1000) - self.set_emane_model_config(node_id, config.model, config.config, interface) + # get emane model config + response = self.client.get_emane_model_configs(self.session_id) + for _id in response.configs: + config = response.configs[_id] + interface = None + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + self.set_emane_model_config( + node_id, config.model, config.config, interface + ) - # save and retrieve data, needed for session nodes - for node in session.nodes: - # get node service config and file config - self.created_nodes.add(node.id) + # save and retrieve data, needed for session nodes + for node in session.nodes: + # get node service config and file config + self.created_nodes.add(node.id) - # get wlan configs for wlan nodes - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config - # retrieve service configurations data for default nodes - elif node.type == core_pb2.NodeType.DEFAULT: - for service in node.services: - response = self.client.get_node_service( - self.session_id, node.id, service - ) - if node.id not in self.service_configs: - self.service_configs[node.id] = {} - self.service_configs[node.id][service] = response.service - for file in response.service.configs: - response = self.client.get_node_service_file( - self.session_id, node.id, service, file + # get wlan configs for wlan nodes + if node.type == core_pb2.NodeType.WIRELESS_LAN: + response = self.client.get_wlan_config(self.session_id, node.id) + self.wlan_configs[node.id] = response.config + # retrieve service configurations data for default nodes + elif node.type == core_pb2.NodeType.DEFAULT: + for service in node.services: + response = self.client.get_node_service( + self.session_id, node.id, service ) - if node.id not in self.file_configs: - self.file_configs[node.id] = {} - if service not in self.file_configs[node.id]: - self.file_configs[node.id][service] = {} - self.file_configs[node.id][service][file] = response.data + if node.id not in self.service_configs: + self.service_configs[node.id] = {} + self.service_configs[node.id][service] = response.service + for file in response.service.configs: + response = self.client.get_node_service_file( + self.session_id, node.id, service, file + ) + if node.id not in self.file_configs: + self.file_configs[node.id] = {} + if service not in self.file_configs[node.id]: + self.file_configs[node.id][service] = {} + self.file_configs[node.id][service][file] = response.data - # store links as created links - for link in session.links: - self.created_links.add(tuple(sorted([link.node_one_id, link.node_two_id]))) + # store links as created links + for link in session.links: + self.created_links.add( + tuple(sorted([link.node_one_id, link.node_two_id])) + ) - # draw session - self.app.canvas.reset_and_redraw(session) + # draw session + self.app.canvas.reset_and_redraw(session) - # get metadata - response = self.client.get_session_metadata(self.session_id) - self.parse_metadata(response.config) + # get metadata + response = self.client.get_session_metadata(self.session_id) + self.parse_metadata(response.config) + except grpc.RpcError as e: + show_grpc_error(e) # update ui to represent current state if self.is_runtime(): @@ -363,25 +370,31 @@ class CoreClient: :return: nothing """ - response = self.client.create_session() - logging.info("created session: %s", response) - location_config = self.app.guiconfig["location"] - self.location = core_pb2.SessionLocation( - x=location_config["x"], - y=location_config["y"], - z=location_config["z"], - lat=location_config["lat"], - lon=location_config["lon"], - alt=location_config["alt"], - scale=location_config["scale"], - ) - self.join_session(response.session_id, query_location=False) + try: + response = self.client.create_session() + logging.info("created session: %s", response) + location_config = self.app.guiconfig["location"] + self.location = core_pb2.SessionLocation( + x=location_config["x"], + y=location_config["y"], + z=location_config["z"], + lat=location_config["lat"], + lon=location_config["lon"], + alt=location_config["alt"], + scale=location_config["scale"], + ) + self.join_session(response.session_id, query_location=False) + except grpc.RpcError as e: + show_grpc_error(e) def delete_session(self, session_id=None): if session_id is None: session_id = self.session_id - response = self.client.delete_session(session_id) - logging.info("Deleted session result: %s", response) + try: + response = self.client.delete_session(session_id) + logging.info("deleted session result: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) def set_up(self): """ @@ -413,21 +426,15 @@ class CoreClient: x.node_type: set(x.services) for x in response.defaults } except grpc.RpcError as e: - if e.code() == grpc.StatusCode.UNAVAILABLE: - - messagebox.showerror("Server Error", "CORE Daemon Unavailable") - else: - messagebox.showerror("GRPC Error", e.details()) + show_grpc_error(e) self.app.close() - def get_session_state(self): - response = self.client.get_session(self.session_id) - logging.info("get session: %s", response) - return response.session.state - def edit_node(self, node_id, x, y): position = core_pb2.Position(x=x, y=y) - self.client.edit_node(self.session_id, node_id, position, source="gui") + try: + self.client.edit_node(self.session_id, node_id, position, source="gui") + except grpc.RpcError as e: + show_grpc_error(e) def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] @@ -446,39 +453,51 @@ class CoreClient: emane_config = None start = time.perf_counter() - response = self.client.start_session( - self.session_id, - nodes, - links, - self.location, - hooks, - emane_config, - emane_model_configs, - wlan_configs, - mobility_configs, - service_configs, - file_configs, - ) - self.set_metadata() - process_time = time.perf_counter() - start - logging.debug("start session(%s), result: %s", self.session_id, response.result) - self.app.statusbar.start_session_callback(process_time) + try: + response = self.client.start_session( + self.session_id, + nodes, + links, + self.location, + hooks, + emane_config, + emane_model_configs, + wlan_configs, + mobility_configs, + service_configs, + file_configs, + ) + self.set_metadata() + process_time = time.perf_counter() - start + logging.debug( + "start session(%s), result: %s", self.session_id, response.result + ) + self.app.statusbar.start_session_callback(process_time) - # display mobility players - for node_id, config in self.mobility_configs.items(): - canvas_node = self.canvas_nodes[node_id] - mobility_player = MobilityPlayer(self.app, self.app, canvas_node, config) - mobility_player.show() - self.mobility_players[node_id] = mobility_player + # display mobility players + for node_id, config in self.mobility_configs.items(): + canvas_node = self.canvas_nodes[node_id] + mobility_player = MobilityPlayer( + self.app, self.app, canvas_node, config + ) + mobility_player.show() + self.mobility_players[node_id] = mobility_player + except grpc.RpcError as e: + show_grpc_error(e) def stop_session(self, session_id=None): if not session_id: session_id = self.session_id start = time.perf_counter() - response = self.client.stop_session(session_id) - process_time = time.perf_counter() - start - self.app.statusbar.stop_session_callback(process_time) - logging.debug("stopped session(%s), result: %s", session_id, response.result) + try: + response = self.client.stop_session(session_id) + logging.debug( + "stopped session(%s), result: %s", session_id, response.result + ) + process_time = time.perf_counter() - start + self.app.statusbar.stop_session_callback(process_time) + except grpc.RpcError as e: + show_grpc_error(e) def set_metadata(self): # create canvas data @@ -502,9 +521,12 @@ class CoreClient: logging.info("set session metadata: %s", response) def launch_terminal(self, node_id): - response = self.client.get_node_terminal(self.session_id, node_id) - logging.info("get terminal %s", response.terminal) - os.system(f"xterm -e {response.terminal} &") + try: + response = self.client.get_node_terminal(self.session_id, node_id) + logging.info("get terminal %s", response.terminal) + os.system(f"xterm -e {response.terminal} &") + except grpc.RpcError as e: + show_grpc_error(e) def save_xml(self, file_path): """ @@ -513,9 +535,11 @@ class CoreClient: :param str file_path: file path that user pick :return: nothing """ - response = self.client.save_xml(self.session_id, file_path) - logging.info("saved xml(%s): %s", file_path, response) - self.client.events(self.session_id, self.handle_events) + try: + response = self.client.save_xml(self.session_id, file_path) + logging.info("saved xml(%s): %s", file_path, response) + except grpc.RpcError as e: + show_grpc_error(e) def open_xml(self, file_path): """ @@ -524,9 +548,12 @@ class CoreClient: :param str file_path: file to open :return: session id """ - response = self.client.open_xml(file_path) - logging.debug("open xml: %s", response) - self.join_session(response.session_id) + try: + response = self.client.open_xml(file_path) + logging.debug("open xml: %s", response) + self.join_session(response.session_id) + except grpc.RpcError as e: + show_grpc_error(e) def get_node_service(self, node_id, service_name): response = self.client.get_node_service(self.session_id, node_id, service_name) @@ -563,7 +590,7 @@ class CoreClient: """ node_protos = [x.core_node for x in self.canvas_nodes.values()] link_protos = list(self.links.values()) - if self.get_session_state() != core_pb2.SessionState.DEFINITION: + if self.state != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION ) @@ -596,7 +623,7 @@ class CoreClient: :return: nothing """ - logging.debug("Close grpc") + logging.debug("close grpc") self.client.close() def next_node_id(self): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index 71ffe078..a9042ba3 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -6,7 +6,10 @@ import tkinter as tk import webbrowser from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images from coretk.widgets import ConfigFrame @@ -52,9 +55,13 @@ class EmaneModelDialog(Dialog): self.model = f"emane_{model}" self.interface = interface self.config_frame = None - self.config = self.app.core.get_emane_model_config( - self.node.id, self.model, self.interface - ) + try: + self.config = self.app.core.get_emane_model_config( + self.node.id, self.model, self.interface + ) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 21782ee6..4e9d9590 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -3,7 +3,10 @@ mobility configuration """ from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD = 5 @@ -20,7 +23,11 @@ class MobilityConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None - self.config = self.app.core.get_mobility_config(self.node.id) + try: + self.config = self.app.core.get_mobility_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 136e6179..3ccf0b5d 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -1,8 +1,11 @@ import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images PAD = 5 @@ -123,20 +126,29 @@ class MobilityPlayerDialog(Dialog): def click_play(self): self.set_play() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.START - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.START + ) + except grpc.RpcError as e: + show_grpc_error(e) def click_pause(self): self.set_pause() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.PAUSE - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.PAUSE + ) + except grpc.RpcError as e: + show_grpc_error(e) def click_stop(self): self.set_stop() session_id = self.app.core.session_id - self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.STOP - ) + try: + self.app.core.client.mobility_action( + session_id, self.node.id, MobilityAction.STOP + ) + except grpc.RpcError as e: + show_grpc_error(e) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 446f9c12..354937e5 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -3,8 +3,11 @@ import logging import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images from coretk.widgets import CodeText, ListboxScroll @@ -47,44 +50,48 @@ class ServiceConfiguration(Dialog): self.draw() def load(self): - # create nodes and links in definition state for getting and setting service file - self.app.core.create_nodes_and_links() - - service_configs = self.app.core.service_configs - if ( - self.node_id in service_configs - and self.service_name in service_configs[self.node_id] - ): - service_config = self.app.core.service_configs[self.node_id][ - self.service_name - ] - else: - service_config = self.app.core.get_node_service( - self.node_id, self.service_name - ) - self.dependencies = [x for x in service_config.dependencies] - self.executables = [x for x in service_config.executables] - self.metadata = service_config.meta - self.filenames = [x for x in service_config.configs] - self.startup_commands = [x for x in service_config.startup] - self.validation_commands = [x for x in service_config.validate] - self.shutdown_commands = [x for x in service_config.shutdown] - self.validation_mode = service_config.validation_mode - self.validation_time = service_config.validation_timer - self.original_service_files = { - x: self.app.core.get_node_service_file(self.node_id, self.service_name, x) - for x in self.filenames - } - self.temp_service_files = { - x: self.original_service_files[x] for x in self.original_service_files - } - file_configs = self.app.core.file_configs - if ( - self.node_id in file_configs - and self.service_name in file_configs[self.node_id] - ): - for file, data in file_configs[self.node_id][self.service_name].items(): - self.temp_service_files[file] = data + try: + # create nodes and links in definition state for getting and setting service file + self.app.core.create_nodes_and_links() + service_configs = self.app.core.service_configs + if ( + self.node_id in service_configs + and self.service_name in service_configs[self.node_id] + ): + service_config = self.app.core.service_configs[self.node_id][ + self.service_name + ] + else: + service_config = self.app.core.get_node_service( + self.node_id, self.service_name + ) + self.dependencies = [x for x in service_config.dependencies] + self.executables = [x for x in service_config.executables] + self.metadata = service_config.meta + self.filenames = [x for x in service_config.configs] + self.startup_commands = [x for x in service_config.startup] + self.validation_commands = [x for x in service_config.validate] + self.shutdown_commands = [x for x in service_config.shutdown] + self.validation_mode = service_config.validation_mode + self.validation_time = service_config.validation_timer + self.original_service_files = { + x: self.app.core.get_node_service_file( + self.node_id, self.service_name, x + ) + for x in self.filenames + } + self.temp_service_files = { + x: self.original_service_files[x] for x in self.original_service_files + } + file_configs = self.app.core.file_configs + if ( + self.node_id in file_configs + and self.service_name in file_configs[self.node_id] + ): + for file, data in file_configs[self.node_id][self.service_name].items(): + self.temp_service_files[file] = data + except grpc.RpcError as e: + show_grpc_error(e) def draw(self): # self.columnconfigure(1, weight=1) @@ -366,30 +373,33 @@ class ServiceConfiguration(Dialog): startup_commands = self.startup_commands_listbox.get(0, "end") shutdown_commands = self.shutdown_commands_listbox.get(0, "end") validate_commands = self.validate_commands_listbox.get(0, "end") - config = self.core.set_node_service( - self.node_id, - self.service_name, - startup_commands, - validate_commands, - shutdown_commands, - ) - if self.node_id not in service_configs: - service_configs[self.node_id] = {} - if self.service_name not in service_configs[self.node_id]: - self.app.core.service_configs[self.node_id][self.service_name] = config - for file in self.modified_files: - file_configs = self.app.core.file_configs - if self.node_id not in file_configs: - file_configs[self.node_id] = {} - if self.service_name not in file_configs[self.node_id]: - file_configs[self.node_id][self.service_name] = {} - file_configs[self.node_id][self.service_name][ - file - ] = self.temp_service_files[file] - - self.app.core.set_node_service_file( - self.node_id, self.service_name, file, self.temp_service_files[file] + try: + config = self.core.set_node_service( + self.node_id, + self.service_name, + startup_commands, + validate_commands, + shutdown_commands, ) + if self.node_id not in service_configs: + service_configs[self.node_id] = {} + if self.service_name not in service_configs[self.node_id]: + self.app.core.service_configs[self.node_id][self.service_name] = config + for file in self.modified_files: + file_configs = self.app.core.file_configs + if self.node_id not in file_configs: + file_configs[self.node_id] = {} + if self.service_name not in file_configs[self.node_id]: + file_configs[self.node_id][self.service_name] = {} + file_configs[self.node_id][self.service_name][ + file + ] = self.temp_service_files[file] + + self.app.core.set_node_service_file( + self.node_id, self.service_name, file, self.temp_service_files[file] + ) + except grpc.RpcError as e: + show_grpc_error(e) self.destroy() def display_service_file_data(self, event): diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index b2666015..7c77f9df 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -1,7 +1,10 @@ import logging from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD_X = 2 @@ -12,17 +15,23 @@ class SessionOptionsDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Session Options", modal=True) self.config_frame = None + self.config = self.get_config() self.draw() + def get_config(self): + try: + session_id = self.app.core.session_id + response = self.app.core.client.get_session_options(session_id) + return response.config + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - session_id = self.app.core.session_id - response = self.app.core.client.get_session_options(session_id) - logging.info("session options: %s", response) - - self.config_frame = ConfigFrame(self.top, self.app, config=response.config) + self.config_frame = ConfigFrame(self.top, self.app, config=self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew") @@ -37,7 +46,10 @@ class SessionOptionsDialog(Dialog): def save(self): config = self.config_frame.parse_config() - session_id = self.app.core.session_id - response = self.app.core.client.set_session_options(session_id, config) - logging.info("saved session config: %s", response) + try: + session_id = self.app.core.session_id + response = self.app.core.client.set_session_options(session_id, config) + logging.info("saved session config: %s", response) + except grpc.RpcError as e: + show_grpc_error(e) self.destroy() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index 7a07c059..e3d9e696 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -3,8 +3,11 @@ import threading import tkinter as tk from tkinter import ttk +import grpc + from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images @@ -14,8 +17,18 @@ class SessionsDialog(Dialog): self.selected = False self.selected_id = None self.tree = None + self.sessions = self.get_sessions() self.draw() + def get_sessions(self): + try: + response = self.app.core.client.get_sessions() + logging.info("sessions: %s", response) + return response.sessions + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() + def draw(self): self.top.columnconfigure(0, weight=1) self.draw_description() @@ -48,9 +61,7 @@ class SessionsDialog(Dialog): self.tree.column("nodes", stretch=tk.YES) self.tree.heading("nodes", text="Node Count") - response = self.app.core.client.get_sessions() - logging.info("sessions: %s", response) - for index, session in enumerate(response.sessions): + for index, session in enumerate(self.sessions): state_name = core_pb2.SessionState.Enum.Name(session.state) self.tree.insert( "", diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 3c118834..42adc49a 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -4,7 +4,10 @@ wlan configuration from tkinter import ttk +import grpc + from coretk.dialogs.dialog import Dialog +from coretk.errors import show_grpc_error from coretk.widgets import ConfigFrame PAD = 5 @@ -18,7 +21,11 @@ class WlanConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.config_frame = None - self.config = self.app.core.get_wlan_config(self.node.id) + try: + self.config = self.app.core.get_wlan_config(self.node.id) + except grpc.RpcError as e: + show_grpc_error(e) + self.destroy() self.draw() def draw(self): diff --git a/coretk/coretk/errors.py b/coretk/coretk/errors.py new file mode 100644 index 00000000..936968ad --- /dev/null +++ b/coretk/coretk/errors.py @@ -0,0 +1,8 @@ +from tkinter import messagebox + + +def show_grpc_error(e): + title = [x.capitalize() for x in e.code().name.lower().split("_")] + title = " ".join(title) + title = f"GRPC {title}" + messagebox.showerror(title, e.details()) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 1dcb8959..79401c04 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -2,11 +2,14 @@ import logging import tkinter as tk from tkinter import font +import grpc + from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog +from coretk.errors import show_grpc_error from coretk.graph import tags from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip @@ -138,8 +141,11 @@ class CanvasNode: if self.app.core.is_runtime() and self.app.core.observer: self.tooltip.text.set("waiting...") self.tooltip.on_enter(event) - output = self.app.core.run(self.core_node.id) - self.tooltip.text.set(output) + try: + output = self.app.core.run(self.core_node.id) + self.tooltip.text.set(output) + except grpc.RpcError as e: + show_grpc_error(e) def on_leave(self, event): self.tooltip.on_leave(event) diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index f7f9c579..246020ba 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -10,7 +10,6 @@ from tkinter import filedialog, messagebox import grpc -from core.api.grpc import core_pb2 from coretk.appconfig import XML_PATH from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvasbackground import CanvasBackgroundDialog @@ -52,12 +51,7 @@ class MenuAction: "menuaction.py: clean_nodes_links_and_set_configuration() Exiting the program" ) try: - state = self.app.core.get_session_state() - - if ( - state == core_pb2.SessionState.SHUTDOWN - or state == core_pb2.SessionState.DEFINITION - ): + if not self.app.core.is_runtime(): self.app.core.delete_session() if quitapp: self.app.quit() @@ -73,7 +67,7 @@ class MenuAction: elif quitapp: self.app.quit() except grpc.RpcError: - logging.error("error getting session state") + logging.exception("error deleting session") if quitapp: self.app.quit() From 21f0857e654170c49f15ec7972492acb2496ad30 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:09:39 -0800 Subject: [PATCH 238/305] reset mobility player to default dialog size --- coretk/coretk/dialogs/mobilityplayer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 3ccf0b5d..6c9799b9 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -53,6 +53,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.geometry("") self.canvas_node = canvas_node self.node = canvas_node.core_node self.config = config From ada21997e99d68c62d21f94cfe10cbd057211070 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 09:57:12 -0800 Subject: [PATCH 239/305] add validation to canvassizescale, wlanconfig, nodename --- coretk/coretk/app.py | 3 + coretk/coretk/dialogs/canvassizeandscale.py | 79 +++++--- coretk/coretk/dialogs/nodeconfig.py | 8 +- coretk/coretk/validation.py | 206 +++++++------------- coretk/coretk/widgets.py | 9 +- 5 files changed, 140 insertions(+), 165 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4d51cd1e..47626d0d 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -12,6 +12,7 @@ from coretk.menubar import Menubar from coretk.nodeutils import NodeUtils from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar +from coretk.validation import InputValidation class Application(tk.Frame): @@ -25,6 +26,7 @@ class Application(tk.Frame): self.toolbar = None self.canvas = None self.statusbar = None + self.validation = None # setup self.guiconfig = appconfig.read() @@ -48,6 +50,7 @@ class Application(tk.Frame): image = Images.get(ImageEnum.CORE, 16) self.master.tk.call("wm", "iconphoto", self.master._w, image) self.pack(fill=tk.BOTH, expand=True) + self.validation = InputValidation(self) def center(self): width = 1000 diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 669c0732..624e4246 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -4,7 +4,6 @@ size and scale import tkinter as tk from tkinter import font, ttk -from coretk import validation from coretk.dialogs.dialog import Dialog PAD = 5 @@ -20,6 +19,7 @@ class SizeAndScaleDialog(Dialog): """ super().__init__(master, app, "Canvas Size and Scale", modal=True) self.canvas = self.app.canvas + self.validation = app.validation self.section_font = font.Font(weight="bold") # get current canvas dimensions plot = self.canvas.find_withtag("rectangle") @@ -38,12 +38,6 @@ class SizeAndScaleDialog(Dialog): self.meters_width = tk.IntVar(value=width / PIXEL_SCALE * location.scale) self.meters_height = tk.IntVar(value=height / PIXEL_SCALE * location.scale) self.save_default = tk.BooleanVar(value=False) - self.vcmd_canvas_int = validation.validate_command( - app.master, validation.check_canvas_int - ) - self.vcmd_canvas_float = validation.validate_command( - app.master, validation.check_canvas_float - ) self.draw() def draw(self): @@ -54,11 +48,6 @@ class SizeAndScaleDialog(Dialog): self.draw_save_as_default() self.draw_buttons() - def focus_out(self, event): - value = event.widget.get() - if value == "": - event.widget.insert(tk.END, 0) - def draw_size(self): label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) label_frame.grid(sticky="ew") @@ -75,18 +64,19 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.pixel_width, validate="key", - validatecommand=(self.vcmd_canvas_int, "%P"), + validatecommand=(self.validation.positive_int, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) - entry.bind("", self.focus_out) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) entry = ttk.Entry( frame, textvariable=self.pixel_height, validate="key", - validatecommand=(self.vcmd_canvas_int, "%P"), + validatecommand=(self.validation.positive_int, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -98,11 +88,23 @@ class SizeAndScaleDialog(Dialog): frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.meters_width) + entry = ttk.Entry( + frame, + textvariable=self.meters_width, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.meters_height) + entry = ttk.Entry( + frame, + textvariable=self.meters_height, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -121,8 +123,9 @@ class SizeAndScaleDialog(Dialog): frame, textvariable=self.scale, validate="key", - validatecommand=(self.vcmd_canvas_float, "%P"), + validatecommand=(self.validation.positive_float, "%P"), ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -144,12 +147,24 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="X") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.x) + entry = ttk.Entry( + frame, + textvariable=self.x, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.y) + entry = ttk.Entry( + frame, + textvariable=self.y, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") @@ -163,17 +178,35 @@ class SizeAndScaleDialog(Dialog): label = ttk.Label(frame, text="Lat") label.grid(row=0, column=0, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.lat) + entry = ttk.Entry( + frame, + textvariable=self.lat, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") label.grid(row=0, column=2, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.lon) + entry = ttk.Entry( + frame, + textvariable=self.lon, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") label.grid(row=0, column=4, sticky="w", padx=PAD) - entry = ttk.Entry(frame, textvariable=self.alt) + entry = ttk.Entry( + frame, + textvariable=self.alt, + validate="key", + validatecommand=(self.validation.positive_float, "%P"), + ) + entry.bind("", self.validation.focus_out) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 185a8f62..9f7c4d6c 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,7 +3,6 @@ import tkinter as tk from functools import partial from tkinter import ttk -import coretk.validation as validation from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService @@ -70,10 +69,13 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - vcmd = self.app.master.register(validation.check_node_name) entry = ttk.Entry( - frame, textvariable=self.name, validate="key", validatecommand=(vcmd, "%P") + frame, + textvariable=self.name, + validate="key", + validatecommand=(self.app.validation.name, "%P"), ) + entry.bind("", self.app.validation.name_focus_out) entry.grid(row=row, column=1, sticky="ew") row += 1 diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 400cac18..a580c712 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -5,147 +5,85 @@ import logging import tkinter as tk -def validate_command(master, func): - return master.register(func) +class InputValidation: + def __init__(self, app): + self.master = app.master + self.positive_int = None + self.positive_float = None + self.name = None + self.register() + def register(self): + self.positive_int = self.master.register(self.check_positive_int) + self.positive_float = self.master.register(self.check_positive_float) + self.name = self.master.register(self.check_node_name) -def check_positive_int(s): - logging.debug("int validation...") - try: - int_value = int(s) - if int_value >= 0: + def focus_out(self, event): + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, 0) + + def name_focus_out(self, event): + logging.debug("name focus out") + value = event.widget.get() + if value == "": + event.widget.insert(tk.END, "empty") + + def check_positive_int(self, s): + logging.debug("int validation...") + if len(s) == 0: return True - return False - except ValueError: - return False - - -def check_positive_float(s): - logging.debug("float validation...") - try: - float_value = float(s) - if float_value >= 0.0: - return True - return False - except ValueError: - return False - - -def check_node_name(name): - logging.debug("node name validation...") - if len(name) <= 0: - return False - for char in name: - if not char.isalnum() and char != "_": + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: return False - return True - -def check_canvas_int(s): - logging.debug("int validation...") - if len(s) == 0: - return True - try: - int_value = int(s) - if int_value >= 0: + def check_positive_float(self, s): + logging.debug("float validation...") + if len(s) == 0: return True - return False - except ValueError: - return False + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False - -def check_canvas_float(s): - logging.debug("canvas float validation") - if not s: - return True - try: - float_value = float(s) - if float_value >= 0.0: + def check_node_name(self, s): + logging.debug("node name validation...") + if len(s) < 0: + return False + if len(s) == 0: return True - return False - except ValueError: - return False + for char in s: + if not char.isalnum() and char != "_": + return False + return True + def check_canvas_int(sefl, s): + logging.debug("int validation...") + if len(s) == 0: + return True + try: + int_value = int(s) + if int_value >= 0: + return True + return False + except ValueError: + return False -def check_interface(name): - logging.debug("interface name validation...") - if len(name) <= 0: - return False, "Interface name cannot be an empty string" - for char in name: - if not char.isalnum() and char != "_": - return ( - False, - "Interface name can only contain alphanumeric letter (a-z) and (0-9) or underscores (_)", - ) - return True, "" - - -def combine_message(key, current_validation, current_message, res, msg): - if not res: - current_validation = res - current_message = current_message + key + ": " + msg + "\n\n" - return current_validation, current_message - - -def check_wlan_config(config): - result = True - message = "" - checks = ["bandwidth", "delay", "error", "jitter", "range"] - for check in checks: - if check in ["bandwidth", "delay", "jitter"]: - res, msg = check_positive_int(config[check].value) - result, message = combine_message(check, result, message, res, msg) - elif check in ["range", "error"]: - res, msg = check_positive_float(config[check].value) - result, message = combine_message(check, result, message, res, msg) - return result, message - - -def check_size_and_scale(dialog): - result = True - message = "" - try: - pixel_width = dialog.pixel_width.get() - if pixel_width < 0: - result, message = combine_message( - "pixel width", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "pixel width", - result, - message, - False, - "invalid value, input non-negative float", - ) - try: - pixel_height = dialog.pixel_height.get() - if pixel_height < 0: - result, message = combine_message( - "pixel height", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "pixel height", - result, - message, - False, - "invalid value, input non-negative float", - ) - try: - scale = dialog.scale.get() - if scale <= 0: - result, message = combine_message( - "scale", result, message, False, "cannot be negative" - ) - except tk.TclError: - result, message = combine_message( - "scale", result, message, False, "invalid value, input non-negative float" - ) - # pixel_height = dialog.pixel_height.get() - # print(pixel_width, pixel_height) - # res, msg = check_positive_int(pixel_width) - # result, message = combine_message("pixel width", result, message, res, msg) - # res, msg = check_positive_int(pixel_height) - # result, message = combine_message("pixel height", result, message, res, msg) - return result, message + def check_canvas_float(self, s): + logging.debug("canvas float validation") + if not s: + return True + try: + float_value = float(s) + if float_value >= 0.0: + return True + return False + except ValueError: + return False diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 521edc98..7d908476 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,7 +5,6 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 -from coretk import validation INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -75,8 +74,6 @@ class ConfigFrame(FrameScroll): padx = 2 pady = 2 group_mapping = {} - vcmd_int = self.app.master.register(validation.check_positive_int) - vcmd_float = self.app.master.register(validation.check_positive_float) for key in self.config: option = self.config[key] group = group_mapping.setdefault(option.group, []) @@ -128,8 +125,9 @@ class ConfigFrame(FrameScroll): frame, textvariable=value, validate="key", - validatecommand=(vcmd_int, "%P"), + validatecommand=(self.app.validation.positive_int, "%P"), ) + entry.bind("", self.app.validation.focus_out) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) @@ -137,8 +135,9 @@ class ConfigFrame(FrameScroll): frame, textvariable=value, validate="key", - validatecommand=(vcmd_float, "%P"), + validatecommand=(self.app.validation.positive_float, "%P"), ) + entry.bind("", self.app.validation.focus_out) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From c4a117a2366b42845ba5f2c8462e80f5668cb2f1 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 13:23:03 -0800 Subject: [PATCH 240/305] insert default value to some entry when entry is empty --- coretk/coretk/coreclient.py | 42 +++++-------------- coretk/coretk/dialogs/canvassizeandscale.py | 20 ++++----- coretk/coretk/dialogs/nodeconfig.py | 6 ++- coretk/coretk/dialogs/servers.py | 10 ++++- coretk/coretk/dialogs/serviceconfiguration.py | 1 - coretk/coretk/graph/node.py | 8 +++- coretk/coretk/validation.py | 17 +++++--- coretk/coretk/widgets.py | 10 ++++- 8 files changed, 61 insertions(+), 53 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8738c4f7..ad85b689 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -70,8 +70,6 @@ class CoreClient: # helpers self.interface_to_edge = {} self.interfaces_manager = InterfaceManager(self.app) - self.created_nodes = set() - self.created_links = set() # session data self.state = None @@ -90,8 +88,6 @@ class CoreClient: def reset(self): # helpers - self.created_nodes.clear() - self.created_links.clear() self.interfaces_manager.reset() self.interface_to_edge.clear() # session data @@ -181,7 +177,6 @@ class CoreClient: canvas_node.move(x, y, update=False) def handle_throughputs(self, event): - # print(event.interface_throughputs) if self.throughput: self.app.canvas.throughput_draw.process_grpc_throughput_event( event.interface_throughputs @@ -243,8 +238,6 @@ class CoreClient: # save and retrieve data, needed for session nodes for node in session.nodes: # get node service config and file config - self.created_nodes.add(node.id) - # get wlan configs for wlan nodes if node.type == core_pb2.NodeType.WIRELESS_LAN: response = self.client.get_wlan_config(self.session_id, node.id) @@ -435,8 +428,6 @@ class CoreClient: hooks = list(self.hooks.values()) service_configs = self.get_service_config_proto() file_configs = self.get_service_file_config_proto() - self.created_links.clear() - self.created_nodes.clear() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -585,31 +576,20 @@ class CoreClient: self.session_id, core_pb2.SessionState.DEFINITION ) - # temp self.client.set_session_state(self.session_id, core_pb2.SessionState.DEFINITION) for node_proto in node_protos: - if node_proto.id not in self.created_nodes or True: - response = self.client.add_node(self.session_id, node_proto) - logging.debug("create node: %s", response) - self.created_nodes.add(node_proto.id) + response = self.client.add_node(self.session_id, node_proto) + logging.debug("create node: %s", response) for link_proto in link_protos: - if ( - tuple([link_proto.node_one_id, link_proto.node_two_id]) - not in self.created_links - or True - ): - response = self.client.add_link( - self.session_id, - link_proto.node_one_id, - link_proto.node_two_id, - link_proto.interface_one, - link_proto.interface_two, - link_proto.options, - ) - logging.debug("create link: %s", response) - self.created_links.add( - tuple([link_proto.node_one_id, link_proto.node_two_id]) - ) + response = self.client.add_link( + self.session_id, + link_proto.node_one_id, + link_proto.node_two_id, + link_proto.interface_one, + link_proto.interface_two, + link_proto.options, + ) + logging.debug("create link: %s", response) def close(self): """ diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 624e4246..f2bf4fc3 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -66,7 +66,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_int, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) @@ -76,7 +76,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_int, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") @@ -94,7 +94,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="x Height") label.grid(row=0, column=2, sticky="w", padx=PAD) @@ -104,7 +104,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") @@ -125,7 +125,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") @@ -153,7 +153,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Y") @@ -164,7 +164,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(label_frame, text="Translates To") @@ -184,7 +184,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=1, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Lon") @@ -195,7 +195,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=3, sticky="ew", padx=PAD) label = ttk.Label(frame, text="Alt") @@ -206,7 +206,7 @@ class SizeAndScaleDialog(Dialog): validate="key", validatecommand=(self.validation.positive_float, "%P"), ) - entry.bind("", self.validation.focus_out) + entry.bind("", lambda event: self.validation.focus_out(event, "0")) entry.grid(row=0, column=5, sticky="ew") def draw_save_as_default(self): diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 9f7c4d6c..8853e908 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -75,7 +75,9 @@ class NodeConfigDialog(Dialog): validate="key", validatecommand=(self.app.validation.name, "%P"), ) - entry.bind("", self.app.validation.name_focus_out) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "noname") + ) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -165,12 +167,14 @@ class NodeConfigDialog(Dialog): label.grid(row=1, column=0, padx=PAD, pady=PAD) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") entry = ttk.Entry(frame, textvariable=ip4) + entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=1, column=1, columnspan=2, sticky="ew") label = ttk.Label(frame, text="IPv6") label.grid(row=2, column=0, padx=PAD, pady=PAD) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") entry = ttk.Entry(frame, textvariable=ip6) + entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=2, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 10a6e79e..9df7bf55 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -72,7 +72,15 @@ class ServersDialog(Dialog): label = ttk.Label(frame, text="Port") label.grid(row=0, column=4, sticky="w") - entry = ttk.Entry(frame, textvariable=self.port) + entry = ttk.Entry( + frame, + textvariable=self.port, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.bind( + "", lambda event: self.app.validation.focus_out(event, "50051") + ) entry.grid(row=0, column=5, sticky="ew") def draw_servers_buttons(self): diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 354937e5..01610eb1 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -51,7 +51,6 @@ class ServiceConfiguration(Dialog): def load(self): try: - # create nodes and links in definition state for getting and setting service file self.app.core.create_nodes_and_links() service_configs = self.app.core.service_configs if ( diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index ede26440..bfc11cbf 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -8,6 +8,7 @@ from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog +from coretk.dialogs.nodeservice import NodeService from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.errors import show_grpc_error from coretk.graph import tags @@ -217,7 +218,7 @@ class CanvasNode: else: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): - context.add_command(label="Services", state=tk.DISABLED) + context.add_command(label="Services", command=self.show_services) if is_emane: context.add_command( label="EMANE Config", command=self.show_emane_config @@ -268,3 +269,8 @@ class CanvasNode: self.canvas.context = None dialog = EmaneConfigDialog(self.app, self.app, self) dialog.show() + + def show_services(self): + self.canvas.context = None + dialog = NodeService(self.app.master, self.app, self) + dialog.show() diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index a580c712..403e073d 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -4,6 +4,9 @@ input validation import logging import tkinter as tk +import netaddr +from netaddr import IPNetwork + class InputValidation: def __init__(self, app): @@ -18,16 +21,18 @@ class InputValidation: self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) - def focus_out(self, event): + def ip_focus_out(self, event): value = event.widget.get() - if value == "": - event.widget.insert(tk.END, 0) + try: + IPNetwork(value) + except netaddr.core.AddrFormatError: + event.widget.delete(0, tk.END) + event.widget.insert(tk.END, "invalid") - def name_focus_out(self, event): - logging.debug("name focus out") + def focus_out(self, event, default): value = event.widget.get() if value == "": - event.widget.insert(tk.END, "empty") + event.widget.insert(tk.END, default) def check_positive_int(self, s): logging.debug("int validation...") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 7d908476..68983210 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -127,7 +127,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.bind("", self.app.validation.focus_out) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) @@ -137,7 +140,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.positive_float, "%P"), ) - entry.bind("", self.app.validation.focus_out) + entry.bind( + "", + lambda event: self.app.validation.focus_out(event, "0"), + ) entry.grid(row=index, column=1, sticky="ew", pady=pady) else: logging.error("unhandled config option type: %s", option.type) From 51163a30d3d9bb31f4fdb34589ffd221424c89b6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:33:52 -0800 Subject: [PATCH 241/305] updates to handling movement for nodes/shapes on canvas, added initial canvas zoom/panning, fixed some issues with mobility player event handling when dialog is closed --- coretk/coretk/app.py | 9 +- coretk/coretk/coreclient.py | 9 +- coretk/coretk/dialogs/mobilityplayer.py | 15 ++- coretk/coretk/graph/graph.py | 133 +++++++++++++++--------- coretk/coretk/graph/node.py | 93 +++++------------ coretk/coretk/graph/shape.py | 17 +-- 6 files changed, 128 insertions(+), 148 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 4d51cd1e..2c4826a9 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -69,14 +69,7 @@ class Application(tk.Frame): def draw_canvas(self): width = self.guiconfig["preferences"]["width"] height = self.guiconfig["preferences"]["height"] - self.canvas = CanvasGraph( - self, - self.core, - width, - height, - background="#cccccc", - scrollregion=(0, 0, 1200, 1000), - ) + self.canvas = CanvasGraph(self, self.core, width, height) self.canvas.pack(fill=tk.BOTH, expand=True) scroll_x = ttk.Scrollbar( self.canvas, orient=tk.HORIZONTAL, command=self.canvas.xview diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 1b1a30da..5ba2384a 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -177,7 +177,7 @@ class CoreClient: x = event.node.position.x y = event.node.position.y canvas_node = self.canvas_nodes[node_id] - canvas_node.move(x, y, update=False) + canvas_node.move(x, y) def handle_throughputs(self, event): # interface_throughputs = event.interface_throughputs @@ -429,10 +429,11 @@ class CoreClient: show_grpc_error(e) self.app.close() - def edit_node(self, node_id, x, y): - position = core_pb2.Position(x=x, y=y) + def edit_node(self, core_node): try: - self.client.edit_node(self.session_id, node_id, position, source="gui") + self.client.edit_node( + self.session_id, core_node.id, core_node.position, source="gui" + ) except grpc.RpcError as e: show_grpc_error(e) diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 6c9799b9..63698644 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -27,6 +27,7 @@ class MobilityPlayer: self.dialog = MobilityPlayerDialog( self.master, self.app, self.canvas_node, self.config ) + self.dialog.protocol("WM_DELETE_WINDOW", self.handle_close) if self.state == MobilityAction.START: self.set_play() elif self.state == MobilityAction.PAUSE: @@ -35,17 +36,24 @@ class MobilityPlayer: self.set_stop() self.dialog.show() + def handle_close(self): + self.dialog.destroy() + self.dialog = None + def set_play(self): - self.dialog.set_play() self.state = MobilityAction.START + if self.dialog: + self.dialog.set_play() def set_pause(self): - self.dialog.set_pause() self.state = MobilityAction.PAUSE + if self.dialog: + self.dialog.set_pause() def set_stop(self): - self.dialog.set_stop() self.state = MobilityAction.STOP + if self.dialog: + self.dialog.set_stop() class MobilityPlayerDialog(Dialog): @@ -92,7 +100,6 @@ class MobilityPlayerDialog(Dialog): self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) - self.stop_button.state(["pressed"]) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index c2c0935d..612088a0 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -15,14 +15,21 @@ from coretk.graph.shapeutils import is_draw_shape from coretk.images import Images from coretk.nodeutils import NodeUtils +SCROLL_BUFFER = 25 +ZOOM_IN = 1.1 +ZOOM_OUT = 0.9 + class CanvasGraph(tk.Canvas): - def __init__(self, master, core, width, height, cnf=None, **kwargs): - if cnf is None: - cnf = {} - kwargs["highlightthickness"] = 0 - super().__init__(master, cnf, **kwargs) + def __init__(self, master, core, width, height): + super().__init__( + master, + highlightthickness=0, + background="#cccccc", + scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER), + ) self.app = master + self.core = core self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} @@ -35,12 +42,13 @@ class CanvasGraph(tk.Canvas): self.wireless_edges = {} self.drawing_edge = None self.grid = None - self.setup_bindings() - self.core = core self.throughput_draw = Throughput(self, core) self.shape_drawing = False self.default_width = width self.default_height = height + self.ratio = 1.0 + self.offset = (0, 0) + self.cursor = (0, 0) # background related self.wallpaper_id = None @@ -51,6 +59,9 @@ class CanvasGraph(tk.Canvas): self.show_grid = tk.BooleanVar(value=True) self.adjust_to_dim = tk.BooleanVar(value=False) + # bindings + self.setup_bindings() + # draw base canvas self.draw_canvas() self.draw_grid() @@ -99,17 +110,19 @@ class CanvasGraph(tk.Canvas): self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.click_context) + self.bind("", self.click_context) self.bind("", self.press_delete) self.bind("", self.ctrl_click) self.bind("", self.double_click) + self.bind("", self.zoom) + self.bind("", lambda e: self.zoom(e, ZOOM_IN)) + self.bind("", lambda e: self.zoom(e, ZOOM_OUT)) + self.bind("", lambda e: self.scan_mark(e.x, e.y)) + self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) def draw_grid(self): """ - Create grid - - :param int width: the width - :param int height: the height + Create grid. :return: nothing """ @@ -213,7 +226,8 @@ class CanvasGraph(tk.Canvas): :rtype: int :return: the item that the mouse point to """ - overlapping = self.find_overlapping(event.x, event.y, event.x, event.y) + x, y = self.canvas_xy(event) + overlapping = self.find_overlapping(x, y, x, y) selected = None for _id in overlapping: if self.drawing_edge and self.drawing_edge.id == _id: @@ -330,10 +344,10 @@ class CanvasGraph(tk.Canvas): self.delete(_id) self.selection.clear() - def object_drag(self, object_id, offset_x, offset_y): + def move_selection(self, object_id, x_offset, y_offset): select_id = self.selection.get(object_id) if select_id is not None: - self.move(select_id, offset_x, offset_y) + self.move(select_id, x_offset, y_offset) def delete_selection_objects(self): edges = set() @@ -382,6 +396,20 @@ class CanvasGraph(tk.Canvas): self.selection.clear() return nodes + def zoom(self, event, factor=None): + if not factor: + factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT + event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) + self.scale("all", event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox("all")) + self.ratio *= float(factor) + self.offset = ( + self.offset[0] * factor + event.x * (1 - factor), + self.offset[1] * factor + event.y * (1 - factor), + ) + logging.info("ratio: %s", self.ratio) + logging.info("offset: %s", self.offset) + def click_press(self, event): """ Start drawing an edge if mouse click is on a node @@ -389,40 +417,46 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ - logging.debug(f"click press: {event}") + x, y = self.canvas_xy(event) + self.cursor = x, y selected = self.get_selected(event) + logging.debug(f"click press: %s", selected) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - x, y = self.canvas_xy(event) shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True self.shapes[shape.id] = shape - if self.mode == GraphMode.SELECT: - if selected is not None: + if selected is not None: + if selected not in self.selection: if selected in self.shapes: - x, y = self.canvas_xy(event) shape = self.shapes[selected] - shape.cursor_x = x - shape.cursor_y = y - if selected not in self.selection: - self.select_object(shape.id) + self.select_object(shape.id) self.selected = selected - else: - self.clear_selection() + elif selected in self.nodes: + node = self.nodes[selected] + self.select_object(node.id) + self.selected = selected + else: + self.clear_selection() def ctrl_click(self, event): + # update cursor location + x, y = self.canvas_xy(event) + self.cursor = x, y + + # handle multiple selections logging.debug("control left click: %s", event) selected = self.get_selected(event) if ( - self.mode == GraphMode.SELECT - and selected is not None + selected not in self.selection and selected in self.shapes + or selected in self.nodes ): self.select_object(selected, choose_multiple=True) @@ -433,36 +467,31 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + x, y = self.canvas_xy(event) + x_offset = x - self.cursor[0] + y_offset = y - self.cursor[1] + self.cursor = x, y + 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) + self.coords(self.drawing_edge.id, x1, y1, x, y) if self.mode == GraphMode.ANNOTATION: if is_draw_shape(self.annotation_type) and self.shape_drawing: - x, y = self.canvas_xy(event) shape = self.shapes[self.selected] shape.shape_motion(x, y) - if ( - self.mode == GraphMode.SELECT - and self.selected is not None - and self.selected in self.shapes - ): - x, y = self.canvas_xy(event) - shape = self.shapes[self.selected] - delta_x = x - shape.cursor_x - delta_y = y - shape.cursor_y - shape.motion(event) - # move other selected components - for _id in self.selection: - if _id != self.selected and _id in self.shapes: - shape = self.shapes[_id] - shape.motion(None, delta_x, delta_y) - if _id != self.selected and _id in self.nodes: - node = self.nodes[_id] - node_x = node.core_node.position.x - node_y = node.core_node.position.y - node.move(node_x + delta_x, node_y + delta_y) + if self.mode == GraphMode.EDGE: + return + + # move selected objects + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) + + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) def click_context(self, event): logging.info("context event: %s", self.context) @@ -592,7 +621,7 @@ class CanvasGraph(tk.Canvas): :return: nothing """ # resize canvas and scrollregion - self.config(scrollregion=(0, 0, width + 200, height + 200)) + self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER)) self.coords(self.grid, 0, 0, width, height) # redraw gridlines to new canvas size diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 79401c04..acd03de3 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import font @@ -11,7 +10,6 @@ from coretk.dialogs.nodeconfig import NodeConfigDialog from coretk.dialogs.wlanconfig import WlanConfigDialog from coretk.errors import show_grpc_error from coretk.graph import tags -from coretk.graph.enums import GraphMode from coretk.graph.tooltip import CanvasTooltip from coretk.nodeutils import NodeUtils @@ -29,12 +27,11 @@ class CanvasNode: self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) - image_box = self.canvas.bbox(self.id) - y = image_box[3] + NODE_TEXT_OFFSET text_font = font.Font(family="TkIconFont", size=12) + label_y = self._get_label_y() self.text_id = self.canvas.create_text( x, - y, + label_y, text=self.core_node.name, tags=tags.NODE_NAME, font=text_font, @@ -44,17 +41,11 @@ class CanvasNode: self.edges = set() self.interfaces = [] self.wireless_edges = set() - self.moving = None self.antennae = [] self.setup_bindings() def setup_bindings(self): - # self.canvas.bind("", self.click_context) - self.canvas.tag_bind(self.id, "", self.click_press) - self.canvas.tag_bind(self.id, "", self.click_release) - self.canvas.tag_bind(self.id, "", self.motion) self.canvas.tag_bind(self.id, "", self.double_click) - self.canvas.tag_bind(self.id, "", self.select_multiple) self.canvas.tag_bind(self.id, "", self.on_enter) self.canvas.tag_bind(self.id, "", self.on_leave) @@ -95,30 +86,32 @@ class CanvasNode: self.canvas.delete(antenna_id) self.antennae.clear() - def move_antennae(self, x_offset, y_offset): - """ - redraw antennas of a node according to the new node position - - :return: nothing - """ - for antenna_id in self.antennae: - self.canvas.move(antenna_id, x_offset, y_offset) - def redraw(self): self.canvas.itemconfig(self.id, image=self.image) self.canvas.itemconfig(self.text_id, text=self.core_node.name) - def move(self, x, y, update=True): - old_x = self.core_node.position.x - old_y = self.core_node.position.y - x_offset = x - old_x - y_offset = y - old_y - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) + def _get_label_y(self): + image_box = self.canvas.bbox(self.id) + return image_box[3] + NODE_TEXT_OFFSET + + def move(self, x, y): + x_offset = x - self.core_node.position.x + y_offset = y - self.core_node.position.y + self.motion(x_offset, y_offset, update=False) + + def motion(self, x_offset, y_offset, update=True): self.canvas.move(self.id, x_offset, y_offset) self.canvas.move(self.text_id, x_offset, y_offset) - self.move_antennae(x_offset, y_offset) - self.canvas.object_drag(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + self.core_node.position.x = int(x) + self.core_node.position.y = int(y) + + # move antennae + for antenna_id in self.antennae: + self.canvas.move(antenna_id, x_offset, y_offset) + + # move edges for edge in self.edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: @@ -126,16 +119,18 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.throughput_draw.move(edge) - edge.link_info.recalculate_info() + for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) if edge.src == self.id: self.canvas.coords(edge.id, x, y, x2, y2) else: self.canvas.coords(edge.id, x1, y1, x, y) + + # update core with new location if self.app.core.is_runtime() and update: - self.app.core.edit_node(self.core_node.id, int(x), int(y)) + self.app.core.edit_node(self.core_node) def on_enter(self, event): if self.app.core.is_runtime() and self.app.core.observer: @@ -164,39 +159,6 @@ class CanvasNode: self.core_node.position.x = int(x) self.core_node.position.y = int(y) - def click_press(self, event): - logging.debug(f"node click press {self.core_node.name}: {event}") - self.moving = self.canvas.canvas_xy(event) - if self.id not in self.canvas.selection: - self.canvas.select_object(self.id) - self.canvas.selected = self.id - - def click_release(self, event): - logging.debug(f"node click release {self.core_node.name}: {event}") - self.update_coords() - self.moving = None - - def motion(self, event): - if self.canvas.mode == GraphMode.EDGE: - return - x, y = self.canvas.canvas_xy(event) - my_x = self.core_node.position.x - my_y = self.core_node.position.y - self.move(x, y) - - # move other selected components - for object_id, selection_id in self.canvas.selection.items(): - if object_id != self.id and object_id in self.canvas.nodes: - canvas_node = self.canvas.nodes[object_id] - other_old_x = canvas_node.core_node.position.x - other_old_y = canvas_node.core_node.position.y - other_new_x = x + other_old_x - my_x - other_new_y = y + other_old_y - my_y - self.canvas.nodes[object_id].move(other_new_x, other_new_y) - elif object_id in self.canvas.shapes: - shape = self.canvas.shapes[object_id] - shape.motion(None, x - my_x, y - my_y) - def create_context(self): is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE @@ -244,9 +206,6 @@ class CanvasNode: context.add_command(label="Hide", state=tk.DISABLED) return context - def select_multiple(self, event): - self.canvas.select_object(self.id, choose_multiple=True) - def show_config(self): self.canvas.context = None dialog = NodeConfigDialog(self.app, self.app, self) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 2928a695..56e679d2 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -49,13 +49,9 @@ class Shape: if data is None: self.created = False self.shape_data = AnnotationData() - self.cursor_x = x1 - self.cursor_y = y1 else: self.created = True self.shape_data = data - self.cursor_x = None - self.cursor_y = None self.draw() def draw(self): @@ -136,16 +132,11 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() - def motion(self, event, delta_x=None, delta_y=None): - if event is not None: - delta_x = event.x - self.cursor_x - delta_y = event.y - self.cursor_y - self.cursor_x = event.x - self.cursor_y = event.y - self.canvas.move(self.id, delta_x, delta_y) - self.canvas.object_drag(self.id, delta_x, delta_y) + def motion(self, x_offset, y_offset): + self.canvas.move(self.id, x_offset, y_offset) + self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: - self.canvas.move(self.text_id, delta_x, delta_y) + self.canvas.move(self.text_id, x_offset, y_offset) def delete(self): self.canvas.delete(self.id) From 0c61c6bffe8025cbb25ca3b2ab95fba7d52ae618 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:31:01 -0800 Subject: [PATCH 242/305] updates to wallpaper drawing and redrawing on zoom --- coretk/coretk/dialogs/canvassizeandscale.py | 2 +- coretk/coretk/graph/graph.py | 100 ++++++++++---------- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 624e4246..b18de7e8 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -231,7 +231,7 @@ class SizeAndScaleDialog(Dialog): width, height = self.pixel_width.get(), self.pixel_height.get() self.canvas.redraw_canvas(width, height) if self.canvas.wallpaper: - self.canvas.redraw() + self.canvas.redraw_wallpaper() location = self.app.core.location location.x = self.x.get() location.y = self.y.get() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 1091905c..9f9c4d99 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -410,6 +410,7 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) + self.redraw_wallpaper() def click_press(self, event): """ @@ -545,24 +546,30 @@ class CanvasGraph(tk.Canvas): canvas_h = abs(y0 - y1) return canvas_w, canvas_h - def wallpaper_upper_left(self): - tk_img = ImageTk.PhotoImage(self.wallpaper) - # crop image if it is bigger than canvas - canvas_w, canvas_h = self.width_and_height() - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - cropped = self.wallpaper.crop((0, 0, cropx, cropy)) - cropped_tk = ImageTk.PhotoImage(cropped) - self.delete(self.wallpaper_id) - # place left corner of image to the left corner of the canvas + def draw_wallpaper(self, image): + x1, y1, x2, y2 = self.bbox(self.grid) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 self.wallpaper_id = self.create_image( - (cropx / 2, cropy / 2), image=cropped_tk, tags=tags.WALLPAPER + (x + 1, y + 1), image=image, tags=tags.WALLPAPER ) - self.wallpaper_drawn = cropped_tk + self.wallpaper_drawn = image + + def wallpaper_upper_left(self): + self.delete(self.wallpaper_id) + + # place left corner of image to the left corner of the canvas + tk_img = ImageTk.PhotoImage(self.wallpaper) + width, height = self.width_and_height() + cropx = image_width = tk_img.width() + cropy = image_height = tk_img.height() + if image_width > width: + cropx = width + if image_height > height: + cropy = height + cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + image = ImageTk.PhotoImage(cropped) + self.draw_wallpaper(image) def wallpaper_center(self): """ @@ -570,27 +577,26 @@ class CanvasGraph(tk.Canvas): :return: nothing """ - tk_img = ImageTk.PhotoImage(self.wallpaper) - canvas_w, canvas_h = self.width_and_height() - cropx = img_w = tk_img.width() - cropy = img_h = tk_img.height() - # dimension of the cropped image - if img_w > canvas_w: - cropx -= img_w - canvas_w - if img_h > canvas_h: - cropy -= img_h - canvas_h - x0 = (img_w - cropx) / 2 - y0 = (img_h - cropy) / 2 - x1 = x0 + cropx - y1 = y0 + cropy - cropped = self.wallpaper.crop((x0, y0, x1, y1)) - cropped_tk = ImageTk.PhotoImage(cropped) - # place the center of the image at the center of the canvas self.delete(self.wallpaper_id) - self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=cropped_tk, tags=tags.WALLPAPER - ) - self.wallpaper_drawn = cropped_tk + + # dimension of the cropped image + tk_img = ImageTk.PhotoImage(self.wallpaper) + width, height = self.width_and_height() + image_width = tk_img.width() + image_height = tk_img.height() + cropx = 0 + if image_width > width: + cropx = (image_width - width) / 2 + cropy = 0 + if image_height > height: + cropy = (image_height - height) / 2 + x1 = 0 + cropx + y1 = 0 + cropy + x2 = image_width - cropx + y2 = image_height - cropy + cropped = self.wallpaper.crop((x1, y1, x2, y2)) + image = ImageTk.PhotoImage(cropped) + self.draw_wallpaper(image) def wallpaper_scaled(self): """ @@ -598,22 +604,18 @@ class CanvasGraph(tk.Canvas): :return: nothing """ + self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) - self.delete(self.wallpaper_id) - self.wallpaper_id = self.create_image( - (canvas_w / 2, canvas_h / 2), image=image, tags=tags.WALLPAPER - ) - self.wallpaper_drawn = image + self.draw_wallpaper(image) def resize_to_wallpaper(self): - image_tk = ImageTk.PhotoImage(self.wallpaper) - img_w = image_tk.width() - img_h = image_tk.height() self.delete(self.wallpaper_id) - self.redraw_canvas(img_w, img_h) - self.wallpaper_id = self.create_image((img_w / 2, img_h / 2), image=image_tk) - self.wallpaper_drawn = image_tk + image = ImageTk.PhotoImage(self.wallpaper) + image_width = image.width() + image_height = image.height() + self.redraw_canvas(image_width, image_height) + self.draw_wallpaper(image) def redraw_canvas(self, width, height): """ @@ -630,7 +632,7 @@ class CanvasGraph(tk.Canvas): self.draw_grid() self.update_grid() - def redraw(self): + def redraw_wallpaper(self): if self.adjust_to_dim.get(): self.resize_to_wallpaper() else: @@ -662,7 +664,7 @@ class CanvasGraph(tk.Canvas): img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename - self.redraw() + self.redraw_wallpaper() else: if self.wallpaper_id is not None: self.delete(self.wallpaper_id) From 6a42748191f07e8bd3611d564641d3b06fa17125 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 10 Dec 2019 16:50:28 -0800 Subject: [PATCH 243/305] controlnet validation --- coretk/coretk/validation.py | 24 ++++++++++++++++++------ coretk/coretk/widgets.py | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 403e073d..68fc79c3 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -1,7 +1,7 @@ """ input validation """ -import logging +import re import tkinter as tk import netaddr @@ -14,12 +14,14 @@ class InputValidation: self.positive_int = None self.positive_float = None self.name = None + self.ip4 = None self.register() def register(self): self.positive_int = self.master.register(self.check_positive_int) self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) + self.ip4 = self.master.register(self.check_ip4) def ip_focus_out(self, event): value = event.widget.get() @@ -35,7 +37,6 @@ class InputValidation: event.widget.insert(tk.END, default) def check_positive_int(self, s): - logging.debug("int validation...") if len(s) == 0: return True try: @@ -47,7 +48,6 @@ class InputValidation: return False def check_positive_float(self, s): - logging.debug("float validation...") if len(s) == 0: return True try: @@ -59,7 +59,6 @@ class InputValidation: return False def check_node_name(self, s): - logging.debug("node name validation...") if len(s) < 0: return False if len(s) == 0: @@ -70,7 +69,6 @@ class InputValidation: return True def check_canvas_int(sefl, s): - logging.debug("int validation...") if len(s) == 0: return True try: @@ -82,7 +80,6 @@ class InputValidation: return False def check_canvas_float(self, s): - logging.debug("canvas float validation") if not s: return True try: @@ -92,3 +89,18 @@ class InputValidation: return False except ValueError: return False + + def check_ip4(self, s): + if not s: + return True + pat = re.compile("^([0-9]+[.])*[0-9]*$") + if pat.match(s) is not None: + _32bits = s.split(".") + if len(_32bits) > 4: + return False + for _8bits in _32bits: + if (_8bits and int(_8bits) > 255) or len(_8bits) > 3: + return False + return True + else: + return False diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 68983210..fb5582a1 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -117,8 +117,18 @@ class ConfigFrame(FrameScroll): button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) else: - entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + if "controlnet" in option.name and "script" not in option.name: + entry = ttk.Entry( + frame, + textvariable=value, + validate="key", + validatecommand=(self.app.validation.ip4, "%P"), + ) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + else: + entry = ttk.Entry(frame, textvariable=value) + entry.grid(row=index, column=1, sticky="ew", pady=pady) + elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry( From 737c14cc0f269cf29ecf4b126a266a3ee2977a1f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 10 Dec 2019 19:25:00 -0800 Subject: [PATCH 244/305] updated canvas wallpaper to scale with zoom level --- coretk/coretk/graph/graph.py | 76 ++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 9f9c4d99..fc5d6fd7 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -12,7 +12,6 @@ from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import is_draw_shape -from coretk.images import Images from coretk.nodeutils import NodeUtils SCROLL_BUFFER = 25 @@ -410,7 +409,8 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) - self.redraw_wallpaper() + if self.wallpaper: + self.redraw_wallpaper() def click_press(self, event): """ @@ -422,7 +422,7 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) self.cursor = x, y selected = self.get_selected(event) - logging.debug(f"click press: %s", selected) + logging.debug("click press: %s", selected) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) @@ -546,30 +546,41 @@ class CanvasGraph(tk.Canvas): canvas_h = abs(y0 - y1) return canvas_w, canvas_h - def draw_wallpaper(self, image): - x1, y1, x2, y2 = self.bbox(self.grid) - x = (x1 + x2) / 2 - y = (y1 + y2) / 2 - self.wallpaper_id = self.create_image( - (x + 1, y + 1), image=image, tags=tags.WALLPAPER - ) + def get_wallpaper_image(self): + width = int(self.wallpaper.width * self.ratio) + height = int(self.wallpaper.height * self.ratio) + image = self.wallpaper.resize((width, height), Image.ANTIALIAS) + return image + + def draw_wallpaper(self, image, x=None, y=None): + if x is None and y is None: + x1, y1, x2, y2 = self.bbox(self.grid) + x = (x1 + x2) / 2 + y = (y1 + y2) / 2 + + self.wallpaper_id = self.create_image((x, y), image=image, tags=tags.WALLPAPER) self.wallpaper_drawn = image def wallpaper_upper_left(self): self.delete(self.wallpaper_id) - # place left corner of image to the left corner of the canvas - tk_img = ImageTk.PhotoImage(self.wallpaper) + # create new scaled image, cropped if needed width, height = self.width_and_height() - cropx = image_width = tk_img.width() - cropy = image_height = tk_img.height() - if image_width > width: - cropx = width - if image_height > height: - cropy = height - cropped = self.wallpaper.crop((0, 0, cropx, cropy)) + image = self.get_wallpaper_image() + cropx = image.width + cropy = image.height + if image.width > width: + cropx = image.width + if image.height > height: + cropy = image.height + cropped = image.crop((0, 0, cropx, cropy)) image = ImageTk.PhotoImage(cropped) - self.draw_wallpaper(image) + + # draw on canvas + x1, y1, _, _ = self.bbox(self.grid) + x = (cropx / 2) + x1 + y = (cropy / 2) + y1 + self.draw_wallpaper(image, x, y) def wallpaper_center(self): """ @@ -580,21 +591,19 @@ class CanvasGraph(tk.Canvas): self.delete(self.wallpaper_id) # dimension of the cropped image - tk_img = ImageTk.PhotoImage(self.wallpaper) width, height = self.width_and_height() - image_width = tk_img.width() - image_height = tk_img.height() + image = self.get_wallpaper_image() cropx = 0 - if image_width > width: - cropx = (image_width - width) / 2 + if image.width > width: + cropx = (image.width - width) / 2 cropy = 0 - if image_height > height: - cropy = (image_height - height) / 2 + if image.height > height: + cropy = (image.height - height) / 2 x1 = 0 + cropx y1 = 0 + cropy - x2 = image_width - cropx - y2 = image_height - cropy - cropped = self.wallpaper.crop((x1, y1, x2, y2)) + x2 = image.width - cropx + y2 = image.height - cropy + cropped = image.crop((x1, y1, x2, y2)) image = ImageTk.PhotoImage(cropped) self.draw_wallpaper(image) @@ -606,15 +615,14 @@ class CanvasGraph(tk.Canvas): """ self.delete(self.wallpaper_id) canvas_w, canvas_h = self.width_and_height() - image = Images.create(self.wallpaper_file, int(canvas_w), int(canvas_h)) + image = self.wallpaper.resize((int(canvas_w), int(canvas_h)), Image.ANTIALIAS) + image = ImageTk.PhotoImage(image) self.draw_wallpaper(image) def resize_to_wallpaper(self): self.delete(self.wallpaper_id) image = ImageTk.PhotoImage(self.wallpaper) - image_width = image.width() - image_height = image.height() - self.redraw_canvas(image_width, image_height) + self.redraw_canvas(image.width(), image.height()) self.draw_wallpaper(image) def redraw_canvas(self, width, height): From 4cc83cf3135127dc394cab9b771f83c7830f7455 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 09:17:39 -0800 Subject: [PATCH 245/305] cel --- coretk/coretk/coreclient.py | 6 ---- coretk/coretk/dialogs/cel.py | 29 +++++++++++++++++++ coretk/coretk/dialogs/serviceconfiguration.py | 3 +- coretk/coretk/statusbar.py | 7 +++++ coretk/coretk/validation.py | 6 +++- 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 coretk/coretk/dialogs/cel.py diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index c02a3295..827f4d01 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -261,12 +261,6 @@ class CoreClient: self.file_configs[node.id][service] = {} self.file_configs[node.id][service][file] = response.data - # store links as created links - for link in session.links: - self.created_links.add( - tuple(sorted([link.node_one_id, link.node_two_id])) - ) - # draw session self.app.canvas.reset_and_redraw(session) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py new file mode 100644 index 00000000..847d245b --- /dev/null +++ b/coretk/coretk/dialogs/cel.py @@ -0,0 +1,29 @@ +""" +check engine light +""" +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class CheckLight(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "CEL", modal=True) + self.app = app + + self.columnconfigure(0, weight=1) + self.draw() + + def draw(self): + row = 0 + frame = ttk.Frame(self) + button = ttk.Button(frame, text="Reset CEL") + button.grid(row=0, column=0) + button = ttk.Button(frame, text="View core-daemon log") + button.grid(row=0, column=1) + button = ttk.Button(frame, text="View node log") + button.grid(row=0, column=2) + button = ttk.Button(frame, text="Close", command=self.destroy) + button.grid(row=0, column=3) + frame.grid(row=row, column=0, sticky="nsew") + ++row diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 01610eb1..812d1a6a 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -43,6 +43,7 @@ class ServiceConfiguration(Dialog): self.validation_time_entry = None self.validation_mode_entry = None self.service_file_data = None + self.validation_period_entry = None self.original_service_files = {} self.temp_service_files = {} self.modified_files = set() @@ -100,8 +101,6 @@ class ServiceConfiguration(Dialog): label.grid(row=0, column=0, sticky="ew") frame1.grid(row=0, column=0) frame2 = ttk.Frame(frame) - # frame2.columnconfigure(0, weight=1) - # frame2.columnconfigure(1, weight=4) label = ttk.Label(frame2, text="Meta-data") label.grid(row=0, column=0) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 8910c742..f45dce9f 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -2,6 +2,8 @@ import tkinter as tk from tkinter import ttk +from coretk.dialogs.cel import CheckLight + class StatusBar(ttk.Frame): def __init__(self, master, app, **kwargs): @@ -55,8 +57,13 @@ class StatusBar(ttk.Frame): self.emulation_light = ttk.Label( self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE ) + self.emulation_light.bind("", self.cel_callback) self.emulation_light.grid(row=0, column=4, sticky="ew") + def cel_callback(self, event): + dialog = CheckLight(self.app, self.app) + dialog.show() + def start_session_callback(self, process_time): self.progress_bar.stop() self.statusvar.set(f"Session started in {process_time:.3f} seconds") diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index 68fc79c3..c7923229 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -99,7 +99,11 @@ class InputValidation: if len(_32bits) > 4: return False for _8bits in _32bits: - if (_8bits and int(_8bits) > 255) or len(_8bits) > 3: + if ( + (_8bits and int(_8bits) > 225) + or len(_8bits) > 3 + or (_8bits.startswith("0") and len(_8bits) > 1) + ): return False return True else: From 8585911900ddef76de0e9fa6df0940c200e9eac4 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 11:42:05 -0800 Subject: [PATCH 246/305] updated theme and size for picker buttons, also added text for clarity --- coretk/coretk/app.py | 2 ++ coretk/coretk/dialogs/dialog.py | 1 - coretk/coretk/nodeutils.py | 39 ++++++++++++++++----------------- coretk/coretk/themes.py | 5 +++++ coretk/coretk/toolbar.py | 31 ++++++++++++++++---------- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index c2eeadc3..0a9275f1 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -42,6 +42,8 @@ class Application(tk.Frame): self.style.theme_use(self.guiconfig["preferences"]["theme"]) func = partial(themes.update_menu, self.style) self.master.bind_class("Menu", "<>", func) + func = partial(themes.theme_change, self.style) + self.master.bind("<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index d814a47c..29362960 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -9,7 +9,6 @@ DIALOG_PAD = 5 class Dialog(tk.Toplevel): def __init__(self, master, app, title, modal=False): super().__init__(master) - self.geometry("800x600") self.withdraw() self.app = app self.modal = modal diff --git a/coretk/coretk/nodeutils.py b/coretk/coretk/nodeutils.py index 10d926f7..cb0bc7a3 100644 --- a/coretk/coretk/nodeutils.py +++ b/coretk/coretk/nodeutils.py @@ -13,18 +13,16 @@ class NodeDraw: self.image_file = None self.node_type = None self.model = None - self.tooltip = None self.services = set() @classmethod - def from_setup(cls, image_enum, node_type, model=None, tooltip=None): + def from_setup(cls, image_enum, node_type, label, model=None, tooltip=None): node_draw = NodeDraw() node_draw.image_enum = image_enum node_draw.image = Images.get(image_enum, ICON_SIZE) node_draw.node_type = node_type + node_draw.label = label node_draw.model = model - if tooltip is None: - tooltip = model node_draw.tooltip = tooltip return node_draw @@ -36,6 +34,7 @@ class NodeDraw: node_draw.image = Images.get_custom(image_file, ICON_SIZE) node_draw.node_type = NodeType.DEFAULT node_draw.services = services + node_draw.label = name node_draw.model = name node_draw.tooltip = name return node_draw @@ -81,29 +80,29 @@ class NodeUtils: @classmethod def setup(cls): nodes = [ - (ImageEnum.ROUTER, NodeType.DEFAULT, "router"), - (ImageEnum.HOST, NodeType.DEFAULT, "host"), - (ImageEnum.PC, NodeType.DEFAULT, "PC"), - (ImageEnum.MDR, NodeType.DEFAULT, "mdr"), - (ImageEnum.PROUTER, NodeType.DEFAULT, "prouter"), - (ImageEnum.DOCKER, NodeType.DOCKER, "Docker"), - (ImageEnum.LXC, NodeType.LXC, "LXC"), + (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), + (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), + (ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"), + (ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"), + (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), + (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), + (ImageEnum.LXC, NodeType.LXC, "LXC", None), ] - for image_enum, node_type, model in nodes: - node_draw = NodeDraw.from_setup(image_enum, node_type, model) + for image_enum, node_type, label, model in nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label, model) cls.NODES.append(node_draw) cls.NODE_ICONS[(node_type, model)] = node_draw.image network_nodes = [ - (ImageEnum.HUB, NodeType.HUB, "ethernet hub"), - (ImageEnum.SWITCH, NodeType.SWITCH, "ethernet switch"), - (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "wireless LAN"), + (ImageEnum.HUB, NodeType.HUB, "Hub"), + (ImageEnum.SWITCH, NodeType.SWITCH, "Switch"), + (ImageEnum.WLAN, NodeType.WIRELESS_LAN, "WLAN"), (ImageEnum.EMANE, NodeType.EMANE, "EMANE"), - (ImageEnum.RJ45, NodeType.RJ45, "rj45 physical interface tool"), - (ImageEnum.TUNNEL, NodeType.TUNNEL, "tunnel tool"), + (ImageEnum.RJ45, NodeType.RJ45, "RJ45"), + (ImageEnum.TUNNEL, NodeType.TUNNEL, "Tunnel"), ] - for image_enum, node_type, tooltip in network_nodes: - node_draw = NodeDraw.from_setup(image_enum, node_type, tooltip=tooltip) + for image_enum, node_type, label in network_nodes: + node_draw = NodeDraw.from_setup(image_enum, node_type, label) cls.NETWORK_NODES.append(node_draw) cls.NODE_ICONS[(node_type, None)] = node_draw.image cls.ANTENNA_ICON = Images.get(ImageEnum.ANTENNA, ANTENNA_SIZE) diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index b80caf87..a1982b3e 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -8,6 +8,7 @@ class Styles: tooltip = "Tooltip.TLabel" tooltip_frame = "Tooltip.TFrame" service_checkbutton = "Service.TCheckbutton" + picker_button = "Picker.TButton" class Colors: @@ -150,3 +151,7 @@ def update_menu(style, event): event.widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) + + +def theme_change(style, event): + style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index a47cac5e..72288b71 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -3,6 +3,7 @@ import threading import tkinter as tk from functools import partial from tkinter import ttk +from tkinter.font import Font from coretk.dialogs.customnodes import CustomNodesDialog from coretk.graph import tags @@ -10,13 +11,15 @@ from coretk.graph.enums import GraphMode from coretk.graph.shapeutils import ShapeType from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils +from coretk.themes import Styles from coretk.tooltip import Tooltip WIDTH = 32 +PICKER_SIZE = 24 -def icon(image_enum): - return Images.get(image_enum, WIDTH) +def icon(image_enum, width=WIDTH): + return Images.get(image_enum, width) class Toolbar(ttk.Frame): @@ -34,6 +37,9 @@ class Toolbar(ttk.Frame): self.app = app self.master = app.master + # picker data + self.picker_font = Font(size=8) + # design buttons self.select_button = None self.link_button = None @@ -140,9 +146,9 @@ class Toolbar(ttk.Frame): self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: - image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) func = partial(self.update_button, self.node_button, image, node_draw) - self.create_picker_button(image, func, self.node_picker, node_draw.tooltip) + self.create_picker_button(image, func, self.node_picker, node_draw.label) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] @@ -152,7 +158,7 @@ class Toolbar(ttk.Frame): # draw edit node image = icon(ImageEnum.EDITNODE) self.create_picker_button( - image, self.click_edit_node, self.node_picker, "custom nodes" + image, self.click_edit_node, self.node_picker, "Custom" ) self.design_select(self.node_button) self.node_button.after( @@ -169,21 +175,22 @@ class Toolbar(ttk.Frame): self.wait_window(picker) self.app.unbind_all("") - def create_picker_button(self, image, func, frame, tooltip): + def create_picker_button(self, image, func, frame, label): """ Create button and put it on the frame :param PIL.Image image: button image :param func: the command that is executed when button is clicked :param tkinter.Frame frame: frame that contains the button - :param str tooltip: tooltip text + :param str label: button label :return: nothing """ - button = ttk.Button(frame, image=image) + button = ttk.Button( + frame, image=image, text=label, compound=tk.TOP, style=Styles.picker_button + ) button.image = image button.bind("", lambda e: func()) button.grid(pady=1) - Tooltip(button, tooltip) def create_button(self, frame, image, func, tooltip): button = ttk.Button(frame, image=image, command=func) @@ -269,12 +276,12 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: - image = icon(node_draw.image_enum) + image = icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_button, self.network_button, image, node_draw), self.network_picker, - node_draw.tooltip, + node_draw.label, ) self.design_select(self.network_button) self.network_button.after( @@ -311,7 +318,7 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: - image = icon(image_enum) + image = icon(image_enum, PICKER_SIZE) self.create_picker_button( image, partial(self.update_annotation, image, shape_type), From 69296d6ea9a3f1efcbccc1c63b4d75661b17301a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:09:50 -0800 Subject: [PATCH 247/305] pass on updating dialogs to have buttons float to bottom and start using common pad configuration --- coretk/coretk/app.py | 13 +- coretk/coretk/dialogs/canvassizeandscale.py | 1 + ...canvasbackground.py => canvaswallpaper.py} | 23 ++-- coretk/coretk/dialogs/customnodes.py | 26 ++-- coretk/coretk/dialogs/dialog.py | 6 + coretk/coretk/dialogs/emaneconfig.py | 12 +- coretk/coretk/dialogs/hooks.py | 34 ++--- coretk/coretk/dialogs/icondialog.py | 17 ++- coretk/coretk/dialogs/mobilityconfig.py | 3 +- coretk/coretk/dialogs/mobilityplayer.py | 1 + coretk/coretk/dialogs/nodeconfig.py | 27 ++-- coretk/coretk/dialogs/nodeservice.py | 12 +- coretk/coretk/dialogs/observers.py | 12 +- coretk/coretk/dialogs/preferences.py | 15 ++- coretk/coretk/dialogs/servers.py | 26 ++-- coretk/coretk/dialogs/sessions.py | 26 ++-- coretk/coretk/dialogs/shapemod.py | 126 +++++++++--------- coretk/coretk/dialogs/wlanconfig.py | 3 +- coretk/coretk/graph/node.py | 2 + coretk/coretk/menuaction.py | 2 +- coretk/coretk/menubar.py | 16 +-- coretk/coretk/themes.py | 8 +- coretk/coretk/widgets.py | 2 +- 23 files changed, 223 insertions(+), 190 deletions(-) rename coretk/coretk/dialogs/{canvasbackground.py => canvaswallpaper.py} (90%) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 0a9275f1..7c2562a9 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -14,6 +14,9 @@ from coretk.statusbar import StatusBar from coretk.toolbar import Toolbar from coretk.validation import InputValidation +WIDTH = 1000 +HEIGHT = 800 + class Application(tk.Frame): def __init__(self, master=None): @@ -40,7 +43,7 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) self.style.theme_use(self.guiconfig["preferences"]["theme"]) - func = partial(themes.update_menu, self.style) + func = partial(themes.theme_change_menu, self.style) self.master.bind_class("Menu", "<>", func) func = partial(themes.theme_change, self.style) self.master.bind("<>", func) @@ -55,13 +58,11 @@ class Application(tk.Frame): self.validation = InputValidation(self) def center(self): - width = 1000 - height = 800 screen_width = self.master.winfo_screenwidth() screen_height = self.master.winfo_screenheight() - x = int((screen_width / 2) - (width / 2)) - y = int((screen_height / 2) - (height / 2)) - self.master.geometry(f"{width}x{height}+{x}+{y}") + x = int((screen_width / 2) - (WIDTH / 2)) + y = int((screen_height / 2) - (HEIGHT / 2)) + self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}") def draw(self): self.master.option_add("*tearOff", tk.FALSE) diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 2798f6f2..1fcfff59 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -46,6 +46,7 @@ class SizeAndScaleDialog(Dialog): self.draw_scale() self.draw_reference_point() self.draw_save_as_default() + self.draw_spacer() self.draw_buttons() def draw_size(self): diff --git a/coretk/coretk/dialogs/canvasbackground.py b/coretk/coretk/dialogs/canvaswallpaper.py similarity index 90% rename from coretk/coretk/dialogs/canvasbackground.py rename to coretk/coretk/dialogs/canvaswallpaper.py index 844b5595..bf6bf922 100644 --- a/coretk/coretk/dialogs/canvasbackground.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -9,7 +9,7 @@ from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images -PADX = 5 +PAD = 5 class CanvasBackgroundDialog(Dialog): @@ -36,17 +36,18 @@ class CanvasBackgroundDialog(Dialog): self.draw_image_selection() self.draw_options() self.draw_additional_options() + self.draw_spacer() self.draw_buttons() def draw_image(self): self.image_label = ttk.Label( self.top, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(row=0, column=0, pady=5) + self.image_label.grid(pady=PAD) def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") - label.grid(row=1, column=0, sticky="ew") + label.grid(sticky="ew") if self.filename.get(): self.draw_preview() @@ -55,14 +56,14 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() - entry.grid(row=0, column=0, sticky="ew", padx=PADX) + entry.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew", padx=PADX) + button.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -73,7 +74,7 @@ class CanvasBackgroundDialog(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - frame.grid(row=3, column=0, sticky="ew") + frame.grid(sticky="ew") button = ttk.Radiobutton( frame, text="upper-left", value=1, variable=self.scale_option @@ -103,7 +104,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self.top, text="Show grid", variable=self.show_grid ) - checkbutton.grid(row=4, column=0, sticky="ew", padx=PADX) + checkbutton.grid(sticky="ew", padx=PAD) checkbutton = ttk.Checkbutton( self.top, @@ -111,16 +112,16 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) - checkbutton.grid(row=5, column=0, sticky="ew", padx=PADX) + checkbutton.grid(sticky="ew", padx=PAD) def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(row=6, column=0, pady=5, sticky="ew") + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PADX) + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index dbf431d3..75052e6b 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -8,6 +8,8 @@ from coretk.dialogs.icondialog import IconDialog from coretk.nodeutils import NodeDraw from coretk.widgets import CheckboxList, ListboxScroll +PAD = 5 + class ServicesSelectDialog(Dialog): def __init__(self, master, app, current_services): @@ -23,11 +25,11 @@ class ServicesSelectDialog(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew") + frame.grid(stick="nsew", pady=PAD) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups") + self.groups = ListboxScroll(frame, text="Groups", padding=PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -35,11 +37,11 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected") + self.current = ListboxScroll(frame, text="Selected", padding=PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -49,7 +51,7 @@ class ServicesSelectDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.destroy) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -104,12 +106,12 @@ class CustomNodesDialog(Dialog): def draw_node_config(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PAD) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes") - self.nodes_list.grid(row=0, column=0, sticky="nsew") + self.nodes_list = ListboxScroll(frame, text="Nodes", padding=PAD) + self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PAD) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) @@ -128,17 +130,17 @@ class CustomNodesDialog(Dialog): def draw_node_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(sticky="ew", pady=PAD) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -152,7 +154,7 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 29362960..49662cc4 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -30,3 +30,9 @@ class Dialog(tk.Toplevel): self.wait_visibility() self.grab_set() self.wait_window() + + def draw_spacer(self, row=None): + frame = ttk.Frame(self.top) + frame.grid(row=row, sticky="nsew") + frame.rowconfigure(0, weight=1) + self.top.rowconfigure(frame.grid_info()["row"], weight=1) diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index a9042ba3..b16f7cd4 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -25,11 +25,10 @@ class GlobalEmaneDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame( - self.top, self.app, self.app.core.emane_config, borderwidth=0 - ) + self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_spacer() self.draw_buttons() def draw_buttons(self): @@ -67,9 +66,10 @@ class EmaneModelDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) + self.draw_spacer() self.draw_buttons() def draw_buttons(self): @@ -111,6 +111,7 @@ class EmaneConfigDialog(Dialog): self.draw_emane_configuration() self.draw_emane_models() self.draw_emane_buttons() + self.draw_spacer() self.draw_apply_and_cancel() def draw_emane_configuration(self): @@ -123,8 +124,9 @@ class EmaneConfigDialog(Dialog): self.top, text="The EMANE emulation system provides more complex wireless radio emulation " "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", + justify=tk.CENTER, ) - label.grid(sticky="ew", pady=PAD) + label.grid(pady=PAD) image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index d40f1c36..2850d70e 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -5,6 +5,8 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.widgets import CodeText +PAD = 5 + class HookDialog(Dialog): def __init__(self, master, app): @@ -21,14 +23,14 @@ class HookDialog(Dialog): # name and states frame = ttk.Frame(self.top) - frame.grid(row=0, sticky="ew", pady=2) + frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="ew") + label.grid(row=0, column=0, sticky="ew", padx=PAD) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PAD) values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) @@ -40,11 +42,7 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) - frame.grid(row=1, sticky="nsew", pady=2) - self.data = CodeText(frame) + self.data = CodeText(self.top) self.data.insert( 1.0, ( @@ -53,19 +51,15 @@ class HookDialog(Dialog): "# specified state\n" ), ) - self.data.grid(row=0, column=0, sticky="nsew") - scrollbar = ttk.Scrollbar(frame) - scrollbar.grid(row=0, column=1, sticky="ns") - self.data.config(yscrollcommand=scrollbar.set) - scrollbar.config(command=self.data.yview) + self.data.grid(sticky="nsew") # button row frame = ttk.Frame(self.top) - frame.grid(row=2, sticky="ew", pady=2) + frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=lambda: self.save()) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") @@ -104,25 +98,25 @@ class HooksDialog(Dialog): self.top.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self.top) - self.listbox.grid(row=0, sticky="nsew") + self.listbox.grid(sticky="nsew", pady=PAD) self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) frame = ttk.Frame(self.top) - frame.grid(row=1, sticky="ew") + frame.grid(sticky="ew") for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew") + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew") + self.delete_button.grid(row=0, column=2, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 98a4ed55..9273a177 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -6,6 +6,8 @@ from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images +PAD = 5 + class IconDialog(Dialog): def __init__(self, master, app, name, image): @@ -20,27 +22,30 @@ class IconDialog(Dialog): # row one frame = ttk.Frame(self.top) - frame.grid(row=0, column=0, pady=2, sticky="ew") + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew") + label.grid(row=0, column=0, sticky="ew", padx=PAD) entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew") + entry.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(row=1, column=0, pady=2, sticky="ew") + self.image_label.grid(pady=PAD, sticky="ew") + + # spacer + self.draw_spacer() # row three frame = ttk.Frame(self.top) - frame.grid(row=2, column=0, sticky="ew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 4e9d9590..007209dd 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -32,7 +32,8 @@ class MobilityConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 63698644..1e7b4dc1 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -61,6 +61,7 @@ class MobilityPlayerDialog(Dialog): super().__init__( master, app, f"{canvas_node.core_node.name} Mobility Player", modal=False ) + self.resizable(False, False) self.geometry("") self.canvas_node = canvas_node self.node = canvas_node.core_node diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8853e908..619c5ee7 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -66,6 +66,19 @@ class NodeConfigDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) + # icon field + label = ttk.Label(frame, text="Icon") + label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + self.image_button = ttk.Button( + frame, + text="Icon", + image=self.image, + compound=tk.NONE, + command=self.click_icon, + ) + self.image_button.grid(row=row, column=1, sticky="ew") + row += 1 + # name field label = ttk.Label(frame, text="Name") label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) @@ -81,19 +94,6 @@ class NodeConfigDialog(Dialog): entry.grid(row=row, column=1, sticky="ew") row += 1 - # icon field - label = ttk.Label(frame, text="Icon") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) - self.image_button = ttk.Button( - frame, - text="Icon", - image=self.image, - compound=tk.NONE, - command=self.click_icon, - ) - self.image_button.grid(row=row, column=1, sticky="ew") - row += 1 - # node type field if NodeUtils.is_model_node(self.node.type): label = ttk.Label(frame, text="Type") @@ -137,6 +137,7 @@ class NodeConfigDialog(Dialog): if self.canvas_node.interfaces: self.draw_interfaces() + self.draw_spacer() self.draw_buttons() def draw_interfaces(self): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index d950371b..dfcef922 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -8,6 +8,8 @@ from coretk.dialogs.dialog import Dialog from coretk.dialogs.serviceconfiguration import ServiceConfiguration from coretk.widgets import CheckboxList, ListboxScroll +PAD = 5 + class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): @@ -38,7 +40,7 @@ class NodeService(Dialog): frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups") + self.groups = ListboxScroll(frame, text="Groups", padding=PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -46,11 +48,11 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked + frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected") + self.current = ListboxScroll(frame, text="Selected", padding=PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -60,9 +62,9 @@ class NodeService(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew") + button.grid(row=0, column=1, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 58499bd7..525939e0 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -4,6 +4,8 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog +PAD = 5 + class ObserverDialog(Dialog): def __init__(self, master, app): @@ -27,7 +29,7 @@ class ObserverDialog(Dialog): def draw_listbox(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PAD) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -46,22 +48,22 @@ class ObserverDialog(Dialog): def draw_form_fields(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew") + frame.grid(sticky="ew", pady=PAD) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Command") - label.grid(row=1, column=0, sticky="w") + label.grid(row=1, column=0, sticky="w", padx=PAD) entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(sticky="ew", pady=PAD) for i in range(3): frame.columnconfigure(i, weight=1) diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index 7298f727..b094eec6 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -5,6 +5,8 @@ from tkinter import ttk from coretk import appconfig from coretk.dialogs.dialog import Dialog +PAD = 5 + class PreferencesDialog(Dialog): def __init__(self, master, app): @@ -18,16 +20,17 @@ class PreferencesDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) self.draw_preferences() self.draw_buttons() def draw_preferences(self): - frame = ttk.LabelFrame(self.top, text="Preferences") - frame.grid(sticky="ew", pady=2) + frame = ttk.LabelFrame(self.top, text="Preferences", padding=PAD) + frame.grid(sticky="nsew", pady=2) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Theme") - label.grid(row=0, column=0, pady=2, padx=2, sticky="w") + label.grid(row=0, column=0, pady=PAD, padx=PAD, sticky="w") themes = self.app.style.theme_names() combobox = ttk.Combobox( frame, textvariable=self.theme, values=themes, state="readonly" @@ -37,14 +40,14 @@ class PreferencesDialog(Dialog): combobox.bind("<>", self.theme_change) label = ttk.Label(frame, text="Editor") - label.grid(row=1, column=0, pady=2, padx=2, sticky="w") + label.grid(row=1, column=0, pady=PAD, padx=PAD, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=2, column=0, pady=2, padx=2, sticky="w") + label.grid(row=2, column=0, pady=PAD, padx=PAD, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, @@ -54,7 +57,7 @@ class PreferencesDialog(Dialog): combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=3, column=0, pady=2, padx=2, sticky="w") + label.grid(row=3, column=0, pady=PAD, padx=PAD, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 9df7bf55..18c5d6d1 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog +PAD = 5 DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_PORT = 50051 @@ -26,13 +27,13 @@ class ServersDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.draw_servers() - self.draw_server_configuration() self.draw_servers_buttons() + self.draw_server_configuration() self.draw_apply_buttons() def draw_servers(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="nsew") + frame.grid(pady=PAD, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -51,27 +52,24 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - label = ttk.Label(self.top, text="Server Configuration") - label.grid(pady=2, sticky="ew") - - frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=PAD) + frame.grid(pady=PAD, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w") + label.grid(row=0, column=2, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") label = ttk.Label(frame, text="Port") - label.grid(row=0, column=4, sticky="w") + label.grid(row=0, column=4, sticky="w", padx=PAD, pady=PAD) entry = ttk.Entry( frame, textvariable=self.port, @@ -85,17 +83,17 @@ class ServersDialog(Dialog): def draw_servers_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=2, sticky="ew") + frame.grid(pady=PAD, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.save_button.grid(row=0, column=1, sticky="ew", padx=PAD) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -111,7 +109,7 @@ class ServersDialog(Dialog): button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PAD) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index e3d9e696..f8d26667 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -10,6 +10,8 @@ from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +PAD = 5 + class SessionsDialog(Dialog): def __init__(self, master, app): @@ -31,6 +33,7 @@ class SessionsDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) self.draw_description() self.draw_tree() self.draw_buttons() @@ -46,14 +49,19 @@ class SessionsDialog(Dialog): "connect to an existing session. Usually, only sessions in \n" "the RUNTIME state persist in the daemon, except for the \n" "one you might be concurrently editting.", + justify=tk.CENTER, ) - label.grid(row=0, sticky="ew", pady=5) + label.grid(pady=PAD) def draw_tree(self): + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew") self.tree = ttk.Treeview( - self.top, columns=("id", "state", "nodes"), show="headings" + frame, columns=("id", "state", "nodes"), show="headings" ) - self.tree.grid(row=1, sticky="nsew") + self.tree.grid(sticky="nsew") self.tree.column("id", stretch=tk.YES) self.tree.heading("id", text="ID") self.tree.column("state", stretch=tk.YES) @@ -72,21 +80,19 @@ class SessionsDialog(Dialog): self.tree.bind("", self.on_selected) self.tree.bind("<>", self.click_select) - yscrollbar = ttk.Scrollbar(self.top, orient="vertical", command=self.tree.yview) - yscrollbar.grid(row=1, column=1, sticky="ns") + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") self.tree.configure(yscrollcommand=yscrollbar.set) - xscrollbar = ttk.Scrollbar( - self.top, orient="horizontal", command=self.tree.xview - ) - xscrollbar.grid(row=2, sticky="ew", pady=5) + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew", pady=5) self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): frame = ttk.Frame(self.top) for i in range(4): frame.columnconfigure(i, weight=1) - frame.grid(row=3, sticky="ew") + frame.grid(sticky="ew") image = Images.get(ImageEnum.DOCUMENTNEW, 16) b = ttk.Button( diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index e0085a8c..63037c9d 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -8,6 +8,8 @@ from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text +PADX = (0, 5) +PAD = 5 FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -37,20 +39,27 @@ class ShapeDialog(Dialog): self.bold = tk.BooleanVar(value=data.bold) self.italic = tk.BooleanVar(value=data.italic) self.underline = tk.BooleanVar(value=data.underline) - self.top.columnconfigure(0, weight=1) self.draw() def draw(self): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Text for top of shape: ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.shape_text) - entry.grid(row=0, column=1, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew", padx=3, pady=3) + self.top.columnconfigure(0, weight=1) + self.draw_label_options() + if is_draw_shape(self.shape.shape_type): + self.draw_shape_options() + self.draw_spacer() + self.draw_buttons() - frame = ttk.Frame(self.top) + def draw_label_options(self): + label_frame = ttk.LabelFrame(self.top, text="Label", padding=PAD) + label_frame.grid(sticky="ew") + label_frame.columnconfigure(0, weight=1) + + entry = ttk.Entry(label_frame, textvariable=self.shape_text) + entry.grid(sticky="ew", pady=PAD) + + # font options + frame = ttk.Frame(label_frame) + frame.grid(sticky="nsew", padx=3, pady=3) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -65,70 +74,65 @@ class ShapeDialog(Dialog): frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" ) combobox.grid(row=0, column=1, padx=3, sticky="nsew") - button = ttk.Button(frame, text="Text color", command=self.choose_text_color) + button = ttk.Button(frame, text="Color", command=self.choose_text_color) button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew", padx=3, pady=3) - frame = ttk.Frame(self.top) + # style options + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(3): + frame.columnconfigure(i, weight=1) button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="ew") button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") - button.grid(row=0, column=1, padx=3) + button.grid(row=0, column=1, padx=3, sticky="ew") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") - button.grid(row=0, column=2) - frame.grid(row=2, column=0, sticky="nsew", padx=3, pady=3) + button.grid(row=0, column=2, sticky="ew") - if is_draw_shape(self.shape.shape_type): - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Fill color") - label.grid(row=0, column=0, sticky="nsew") - self.fill = ttk.Label( - frame, text=self.fill_color, background=self.fill_color - ) - self.fill.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_fill_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=3, column=0, sticky="nsew", padx=3, pady=3) + def draw_shape_options(self): + label_frame = ttk.LabelFrame(self.top, text="Shape", padding=PAD) + label_frame.grid(sticky="ew", pady=PAD) + label_frame.columnconfigure(0, weight=1) - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) - label = ttk.Label(frame, text="Border color:") - label.grid(row=0, column=0, sticky="nsew") - self.border = ttk.Label( - frame, text=self.border_color, background=self.fill_color - ) - self.border.grid(row=0, column=1, sticky="nsew", padx=3) - button = ttk.Button(frame, text="Color", command=self.choose_border_color) - button.grid(row=0, column=2, sticky="nsew") - frame.grid(row=4, column=0, sticky="nsew", padx=3, pady=3) + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew") + for i in range(1, 3): + frame.columnconfigure(i, weight=1) + label = ttk.Label(frame, text="Fill Color") + label.grid(row=0, column=0, padx=PADX, sticky="w") + self.fill = ttk.Label(frame, text=self.fill_color, background=self.fill_color) + self.fill.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_fill_color) + button.grid(row=0, column=2, sticky="ew") - frame = ttk.Frame(self.top) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=2) - label = ttk.Label(frame, text="Border width:") - label.grid(row=0, column=0, sticky="nsew") - combobox = ttk.Combobox( - frame, - textvariable=self.border_width, - values=BORDER_WIDTH, - state="readonly", - ) - combobox.grid(row=0, column=1, sticky="nsew") - frame.grid(row=5, column=0, sticky="nsew", padx=3, pady=3) + label = ttk.Label(frame, text="Border Color") + label.grid(row=1, column=0, sticky="w", padx=PADX) + self.border = ttk.Label( + frame, text=self.border_color, background=self.border_color + ) + self.border.grid(row=1, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Color", command=self.choose_border_color) + button.grid(row=1, column=2, sticky="ew") + frame = ttk.Frame(label_frame) + frame.grid(sticky="ew", pady=PAD) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Border Width") + label.grid(row=0, column=0, sticky="w", padx=PADX) + combobox = ttk.Combobox( + frame, textvariable=self.border_width, values=BORDER_WIDTH, state="readonly" + ) + combobox.grid(row=0, column=1, sticky="nsew") + + def draw_buttons(self): frame = ttk.Frame(self.top) + frame.grid(sticky="nsew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Add shape", command=self.click_add) - button.grid(row=0, column=0, sticky="e", padx=3) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.cancel) - button.grid(row=0, column=1, sticky="w", pady=3) - frame.grid(row=6, column=0, sticky="nsew", padx=3, pady=3) + button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): color = colorchooser.askcolor(color="black") @@ -140,7 +144,7 @@ class ShapeDialog(Dialog): self.fill.config(background=color[1], text=color[1]) def choose_border_color(self): - color = colorchooser.askcolor(color="black") + color = colorchooser.askcolor(color=self.border_color) self.border_color = color[1] self.border.config(background=color[1], text=color[1]) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 42adc49a..711f94f0 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -30,7 +30,8 @@ class WlanConfigDialog(Dialog): def draw(self): self.top.columnconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, borderwidth=0) + self.top.rowconfigure(0, weight=1) + self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky="nsew", pady=PAD) self.draw_apply_buttons() diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 5757f081..ef4686be 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -4,6 +4,7 @@ from tkinter import font import grpc from core.api.grpc.core_pb2 import NodeType +from coretk import themes from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog from coretk.dialogs.nodeconfig import NodeConfigDialog @@ -161,6 +162,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) + themes.update_menu(self.app.style, context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index a9f9ca34..3115cd58 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -12,8 +12,8 @@ import grpc from coretk.appconfig import XML_PATH from coretk.dialogs.about import AboutDialog -from coretk.dialogs.canvasbackground import CanvasBackgroundDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog +from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog from coretk.dialogs.hooks import HooksDialog from coretk.dialogs.observers import ObserverDialog from coretk.dialogs.preferences import PreferencesDialog diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index ed5cb2ca..2521bd97 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -80,6 +80,7 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) + menu.add_command(label="Preferences", command=self.menuaction.gui_preferences) menu.add_command(label="Undo", accelerator="Ctrl+Z", state=tk.DISABLED) menu.add_command(label="Redo", accelerator="Ctrl+Y", state=tk.DISABLED) menu.add_separator() @@ -94,9 +95,6 @@ class Menubar(tk.Menu): menu.add_separator() menu.add_command(label="Find...", accelerator="Ctrl+F", state=tk.DISABLED) menu.add_command(label="Clear marker", state=tk.DISABLED) - menu.add_command( - label="Preferences...", command=self.menuaction.gui_preferences - ) self.add_cascade(label="Edit", menu=menu) def draw_canvas_menu(self): @@ -436,16 +434,14 @@ class Menubar(tk.Menu): """ menu = tk.Menu(self) menu.add_command( - label="Change sessions...", - command=self.menuaction.session_change_sessions, - underline=0, + label="Sessions...", command=self.menuaction.session_change_sessions ) menu.add_separator() - menu.add_command(label="Comments...", state=tk.DISABLED) - menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) - menu.add_command(label="Reset node positions", state=tk.DISABLED) - menu.add_command(label="Servers...", command=self.menuaction.session_servers) menu.add_command(label="Options...", command=self.menuaction.session_options) + menu.add_command(label="Servers...", command=self.menuaction.session_servers) + menu.add_command(label="Hooks...", command=self.menuaction.session_hooks) + menu.add_command(label="Reset Nodes", state=tk.DISABLED) + menu.add_command(label="Comments...", state=tk.DISABLED) self.add_cascade(label="Session", menu=menu) def draw_help_menu(self): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index a1982b3e..a870b278 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -140,15 +140,19 @@ def update_bg(style, event): event.widget.config(background=bg) -def update_menu(style, event): +def theme_change_menu(style, event): if not isinstance(event.widget, tk.Menu): return + update_menu(style, event.widget) + + +def update_menu(style, widget): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") if not abg: abg = bg - event.widget.config( + widget.config( background=bg, foreground=fg, activebackground=abg, activeforeground=fg ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index fb5582a1..e3fd5c35 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -65,7 +65,7 @@ class FrameScroll(ttk.LabelFrame): class ConfigFrame(FrameScroll): def __init__(self, master, app, config, **kw): - super().__init__(master, app, ttk.Notebook, **kw) + super().__init__(master, app, ttk.Notebook, borderwidth=0, **kw) self.app = app self.config = config self.values = {} From 899eb51c55bc2b71f575e183709a1537fafa237b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:36:27 -0800 Subject: [PATCH 248/305] added common padding for x, y, and frame paddings, to easily modify and provide consistent look and feel --- coretk/coretk/appconfig.py | 2 +- coretk/coretk/dialogs/canvassizeandscale.py | 60 +++++++++++---------- coretk/coretk/dialogs/canvaswallpaper.py | 17 +++--- coretk/coretk/dialogs/customnodes.py | 31 ++++++----- coretk/coretk/dialogs/dialog.py | 3 +- coretk/coretk/dialogs/emaneconfig.py | 23 ++++---- coretk/coretk/dialogs/hooks.py | 19 ++++--- coretk/coretk/dialogs/icondialog.py | 13 +++-- coretk/coretk/dialogs/mobilityconfig.py | 7 ++- coretk/coretk/dialogs/mobilityplayer.py | 16 +++--- coretk/coretk/dialogs/nodeconfig.py | 29 +++++----- coretk/coretk/dialogs/nodeservice.py | 19 ++++--- coretk/coretk/dialogs/observers.py | 19 ++++--- coretk/coretk/dialogs/preferences.py | 17 +++--- coretk/coretk/dialogs/servers.py | 22 ++++---- coretk/coretk/dialogs/sessionoptions.py | 10 ++-- coretk/coretk/dialogs/sessions.py | 17 +++--- coretk/coretk/dialogs/shapemod.py | 19 ++++--- coretk/coretk/dialogs/wlanconfig.py | 7 ++- coretk/coretk/themes.py | 8 ++- coretk/coretk/widgets.py | 24 ++++----- 21 files changed, 189 insertions(+), 193 deletions(-) diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index 0617562a..b2606994 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -75,7 +75,7 @@ def check_directory(): editor = EDITORS[1] config = { "preferences": { - "theme": themes.DARK, + "theme": themes.THEME_DARK, "editor": editor, "terminal": terminal, "gui3d": "/usr/local/bin/std3d.sh", diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 1fcfff59..5a472104 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -5,8 +5,8 @@ import tkinter as tk from tkinter import font, ttk from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY -PAD = 5 PIXEL_SCALE = 100 @@ -50,17 +50,17 @@ class SizeAndScaleDialog(Dialog): self.draw_buttons() def draw_size(self): - label_frame = ttk.Labelframe(self.top, text="Size", padding=PAD) + label_frame = ttk.Labelframe(self.top, text="Size", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) # draw size row 1 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.pixel_width, @@ -68,9 +68,9 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_int, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.pixel_height, @@ -78,17 +78,17 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_int, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Pixels") label.grid(row=0, column=4, sticky="w") # draw size row 2 frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="Width") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.meters_width, @@ -96,9 +96,9 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="x Height") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.meters_height, @@ -106,12 +106,12 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=4, sticky="w") def draw_scale(self): - label_frame = ttk.Labelframe(self.top, text="Scale", padding=PAD) + label_frame = ttk.Labelframe(self.top, text="Scale", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -119,7 +119,7 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text=f"{PIXEL_SCALE} Pixels =") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.scale, @@ -127,12 +127,14 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Meters") label.grid(row=0, column=2, sticky="w") def draw_reference_point(self): - label_frame = ttk.Labelframe(self.top, text="Reference Point", padding=PAD) + label_frame = ttk.Labelframe( + self.top, text="Reference Point", padding=FRAME_PAD + ) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) @@ -142,12 +144,12 @@ class SizeAndScaleDialog(Dialog): label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) label = ttk.Label(frame, text="X") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.x, @@ -155,10 +157,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Y") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.y, @@ -166,19 +168,19 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(label_frame, text="Translates To") label.grid() frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=3) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Lat") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.lat, @@ -186,10 +188,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Lon") - label.grid(row=0, column=2, sticky="w", padx=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.lon, @@ -197,10 +199,10 @@ class SizeAndScaleDialog(Dialog): validatecommand=(self.validation.positive_float, "%P"), ) entry.bind("", lambda event: self.validation.focus_out(event, "0")) - entry.grid(row=0, column=3, sticky="ew", padx=PAD) + entry.grid(row=0, column=3, sticky="ew", padx=PADX) label = ttk.Label(frame, text="Alt") - label.grid(row=0, column=4, sticky="w", padx=PAD) + label.grid(row=0, column=4, sticky="w", padx=PADX) entry = ttk.Entry( frame, textvariable=self.alt, @@ -214,7 +216,7 @@ class SizeAndScaleDialog(Dialog): button = ttk.Checkbutton( self.top, text="Save as default?", variable=self.save_default ) - button.grid(sticky="w", pady=3) + button.grid(sticky="w", pady=PADY) def draw_buttons(self): frame = ttk.Frame(self.top) @@ -223,7 +225,7 @@ class SizeAndScaleDialog(Dialog): frame.grid(sticky="ew") button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/coretk/coretk/dialogs/canvaswallpaper.py index bf6bf922..923e4d5f 100644 --- a/coretk/coretk/dialogs/canvaswallpaper.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -8,8 +8,7 @@ from tkinter import filedialog, ttk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images - -PAD = 5 +from coretk.themes import PADX, PADY class CanvasBackgroundDialog(Dialog): @@ -43,7 +42,7 @@ class CanvasBackgroundDialog(Dialog): self.image_label = ttk.Label( self.top, text="(image preview)", width=32, anchor=tk.CENTER ) - self.image_label.grid(pady=PAD) + self.image_label.grid(pady=PADY) def draw_image_label(self): label = ttk.Label(self.top, text="Image filename: ") @@ -60,10 +59,10 @@ class CanvasBackgroundDialog(Dialog): entry = ttk.Entry(frame, textvariable=self.filename) entry.focus() - entry.grid(row=0, column=0, sticky="ew", padx=PAD) + entry.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_open_image) - button.grid(row=0, column=1, sticky="ew", padx=PAD) + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Clear", command=self.click_clear) button.grid(row=0, column=2, sticky="ew") @@ -104,7 +103,7 @@ class CanvasBackgroundDialog(Dialog): checkbutton = ttk.Checkbutton( self.top, text="Show grid", variable=self.show_grid ) - checkbutton.grid(sticky="ew", padx=PAD) + checkbutton.grid(sticky="ew", padx=PADX) checkbutton = ttk.Checkbutton( self.top, @@ -112,16 +111,16 @@ class CanvasBackgroundDialog(Dialog): variable=self.adjust_to_dim, command=self.click_adjust_canvas, ) - checkbutton.grid(sticky="ew", padx=PAD) + checkbutton.grid(sticky="ew", padx=PADX) def draw_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 75052e6b..d17d8d19 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -6,10 +6,9 @@ from tkinter import ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.nodeutils import NodeDraw +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CheckboxList, ListboxScroll -PAD = 5 - class ServicesSelectDialog(Dialog): def __init__(self, master, app, current_services): @@ -25,11 +24,11 @@ class ServicesSelectDialog(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew", pady=PAD) + frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=PAD) + self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -37,11 +36,15 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD + frame, + self.app, + text="Services", + clicked=self.service_clicked, + padding=FRAME_PAD, ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=PAD) + self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -51,7 +54,7 @@ class ServicesSelectDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") @@ -106,12 +109,12 @@ class CustomNodesDialog(Dialog): def draw_node_config(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes", padding=PAD) - self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PAD) + self.nodes_list = ListboxScroll(frame, text="Nodes", padding=FRAME_PAD) + self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): self.nodes_list.listbox.insert(tk.END, name) @@ -130,17 +133,17 @@ class CustomNodesDialog(Dialog): def draw_node_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -154,7 +157,7 @@ class CustomNodesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/dialog.py b/coretk/coretk/dialogs/dialog.py index 49662cc4..92d9a7db 100644 --- a/coretk/coretk/dialogs/dialog.py +++ b/coretk/coretk/dialogs/dialog.py @@ -2,8 +2,7 @@ import tkinter as tk from tkinter import ttk from coretk.images import ImageEnum, Images - -DIALOG_PAD = 5 +from coretk.themes import DIALOG_PAD class Dialog(tk.Toplevel): diff --git a/coretk/coretk/dialogs/emaneconfig.py b/coretk/coretk/dialogs/emaneconfig.py index b16f7cd4..a1c30a6f 100644 --- a/coretk/coretk/dialogs/emaneconfig.py +++ b/coretk/coretk/dialogs/emaneconfig.py @@ -11,10 +11,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class GlobalEmaneDialog(Dialog): def __init__(self, master, app): @@ -27,7 +26,7 @@ class GlobalEmaneDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.app.core.emane_config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() self.draw_buttons() @@ -37,7 +36,7 @@ class GlobalEmaneDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -68,7 +67,7 @@ class EmaneModelDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_spacer() self.draw_buttons() @@ -78,7 +77,7 @@ class EmaneModelDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") @@ -126,7 +125,7 @@ class EmaneConfigDialog(Dialog): "\nusing pluggable MAC and PHY modules. Refer to the wiki for configuration option details", justify=tk.CENTER, ) - label.grid(pady=PAD) + label.grid(pady=PADY) image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( @@ -139,7 +138,7 @@ class EmaneConfigDialog(Dialog): ), ) button.image = image - button.grid(sticky="ew", pady=PAD) + button.grid(sticky="ew", pady=PADY) def draw_emane_models(self): """ @@ -148,7 +147,7 @@ class EmaneConfigDialog(Dialog): :return: nothing """ frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Model") @@ -166,7 +165,7 @@ class EmaneConfigDialog(Dialog): def draw_emane_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(2): frame.columnconfigure(i, weight=1) @@ -179,7 +178,7 @@ class EmaneConfigDialog(Dialog): command=self.click_model_config, ) self.emane_model_button.image = image - self.emane_model_button.grid(row=0, column=0, padx=PAD, sticky="ew") + self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky="ew") image = Images.get(ImageEnum.EDITNODE, 16) button = ttk.Button( @@ -199,7 +198,7 @@ class EmaneConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 2850d70e..40101823 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -3,10 +3,9 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY from coretk.widgets import CodeText -PAD = 5 - class HookDialog(Dialog): def __init__(self, master, app): @@ -23,14 +22,14 @@ class HookDialog(Dialog): # name and states frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(0, weight=2) frame.columnconfigure(1, weight=7) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="ew", padx=PAD) + label.grid(row=0, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) values = tuple(x for x in core_pb2.SessionState.Enum.keys() if x != "NONE") initial_state = core_pb2.SessionState.Enum.Name(core_pb2.SessionState.RUNTIME) self.state.set(initial_state) @@ -59,7 +58,7 @@ class HookDialog(Dialog): for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=lambda: self.save()) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=1, sticky="ew") @@ -98,7 +97,7 @@ class HooksDialog(Dialog): self.top.rowconfigure(0, weight=1) self.listbox = tk.Listbox(self.top) - self.listbox.grid(sticky="nsew", pady=PAD) + self.listbox.grid(sticky="nsew", pady=PADY) self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) @@ -108,15 +107,15 @@ class HooksDialog(Dialog): for i in range(4): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.edit_button = ttk.Button( frame, text="Edit", state=tk.DISABLED, command=self.click_edit ) - self.edit_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.edit_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete ) - self.delete_button.grid(row=0, column=2, sticky="ew", padx=PAD) + self.delete_button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=lambda: self.destroy()) button.grid(row=0, column=3, sticky="ew") diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py index 9273a177..ecbb5d54 100644 --- a/coretk/coretk/dialogs/icondialog.py +++ b/coretk/coretk/dialogs/icondialog.py @@ -5,8 +5,7 @@ from coretk import nodeutils from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images - -PAD = 5 +from coretk.themes import PADX, PADY class IconDialog(Dialog): @@ -22,19 +21,19 @@ class IconDialog(Dialog): # row one frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=3) label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew", padx=PAD) + label.grid(row=0, column=0, sticky="ew", padx=PADX) entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew", padx=PAD) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="...", command=self.click_file) button.grid(row=0, column=2) # row two self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(pady=PAD, sticky="ew") + self.image_label.grid(pady=PADY, sticky="ew") # spacer self.draw_spacer() @@ -45,7 +44,7 @@ class IconDialog(Dialog): frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityconfig.py b/coretk/coretk/dialogs/mobilityconfig.py index 007209dd..19dc46f4 100644 --- a/coretk/coretk/dialogs/mobilityconfig.py +++ b/coretk/coretk/dialogs/mobilityconfig.py @@ -7,10 +7,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class MobilityConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -35,7 +34,7 @@ class MobilityConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() def draw_apply_buttons(self): @@ -45,7 +44,7 @@ class MobilityConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/mobilityplayer.py b/coretk/coretk/dialogs/mobilityplayer.py index 1e7b4dc1..f0b46499 100644 --- a/coretk/coretk/dialogs/mobilityplayer.py +++ b/coretk/coretk/dialogs/mobilityplayer.py @@ -7,8 +7,8 @@ from core.api.grpc.core_pb2 import MobilityAction from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import PADX, PADY -PAD = 5 ICON_SIZE = 16 @@ -77,36 +77,36 @@ class MobilityPlayerDialog(Dialog): file_name = self.config["file"].value label = ttk.Label(self.top, text=file_name) - label.grid(sticky="ew", pady=PAD) + label.grid(sticky="ew", pady=PADY) self.progressbar = ttk.Progressbar(self.top, mode="indeterminate") - self.progressbar.grid(sticky="ew", pady=PAD) + self.progressbar.grid(sticky="ew", pady=PADY) frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) image = Images.get(ImageEnum.START, width=ICON_SIZE) self.play_button = ttk.Button(frame, image=image, command=self.click_play) self.play_button.image = image - self.play_button.grid(row=0, column=0, sticky="ew", padx=PAD) + self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX) image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE) self.pause_button = ttk.Button(frame, image=image, command=self.click_pause) self.pause_button.image = image - self.pause_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX) image = Images.get(ImageEnum.STOP, width=ICON_SIZE) self.stop_button = ttk.Button(frame, image=image, command=self.click_stop) self.stop_button.image = image - self.stop_button.grid(row=0, column=2, sticky="ew", padx=PAD) + self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX) loop = tk.IntVar(value=int(self.config["loop"].value == "1")) checkbutton = ttk.Checkbutton( frame, text="Loop?", variable=loop, state=tk.DISABLED ) - checkbutton.grid(row=0, column=3, padx=PAD) + checkbutton.grid(row=0, column=3, padx=PADX) rate = self.config["refresh_ms"].value label = ttk.Label(frame, text=f"rate {rate} ms") diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 619c5ee7..56c2a08d 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -7,10 +7,9 @@ from coretk.dialogs.dialog import Dialog from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService from coretk.nodeutils import NodeUtils +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import FrameScroll -PAD = 5 - def mac_auto(is_auto, entry): logging.info("mac auto clicked") @@ -68,7 +67,7 @@ class NodeConfigDialog(Dialog): # icon field label = ttk.Label(frame, text="Icon") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) self.image_button = ttk.Button( frame, text="Icon", @@ -81,7 +80,7 @@ class NodeConfigDialog(Dialog): # name field label = ttk.Label(frame, text="Name") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) entry = ttk.Entry( frame, textvariable=self.name, @@ -97,7 +96,7 @@ class NodeConfigDialog(Dialog): # node type field if NodeUtils.is_model_node(self.node.type): label = ttk.Label(frame, text="Type") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) combobox = ttk.Combobox( frame, textvariable=self.type, @@ -110,7 +109,7 @@ class NodeConfigDialog(Dialog): # container image field if NodeUtils.is_image_node(self.node.type): label = ttk.Label(frame, text="Image") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.container_image) entry.grid(row=row, column=1, sticky="ew") row += 1 @@ -120,7 +119,7 @@ class NodeConfigDialog(Dialog): frame.grid(sticky="ew") frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") - label.grid(row=row, column=0, sticky="ew", padx=PAD, pady=PAD) + label.grid(row=row, column=0, sticky="ew", padx=PADX, pady=PADY) servers = ["localhost"] servers.extend(list(sorted(self.app.core.servers.keys()))) combobox = ttk.Combobox( @@ -131,7 +130,7 @@ class NodeConfigDialog(Dialog): # services button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PAD) + button.grid(sticky="ew", pady=PADY) # interfaces if self.canvas_node.interfaces: @@ -147,17 +146,17 @@ class NodeConfigDialog(Dialog): scroll.frame.rowconfigure(0, weight=1) for interface in self.canvas_node.interfaces: logging.info("interface: %s", interface) - frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=PAD) - frame.grid(sticky="ew", pady=PAD) + frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=FRAME_PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) label = ttk.Label(frame, text="MAC") - label.grid(row=0, column=0, padx=PAD, pady=PAD) + label.grid(row=0, column=0, padx=PADX, pady=PADY) is_auto = tk.BooleanVar(value=True) checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) checkbutton.var = is_auto - checkbutton.grid(row=0, column=1, padx=PAD) + checkbutton.grid(row=0, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) entry.grid(row=0, column=2, sticky="ew") @@ -165,14 +164,14 @@ class NodeConfigDialog(Dialog): checkbutton.config(command=func) label = ttk.Label(frame, text="IPv4") - label.grid(row=1, column=0, padx=PAD, pady=PAD) + label.grid(row=1, column=0, padx=PADX, pady=PADY) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") entry = ttk.Entry(frame, textvariable=ip4) entry.bind("", self.app.validation.ip_focus_out) entry.grid(row=1, column=1, columnspan=2, sticky="ew") label = ttk.Label(frame, text="IPv6") - label.grid(row=2, column=0, padx=PAD, pady=PAD) + label.grid(row=2, column=0, padx=PADX, pady=PADY) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") entry = ttk.Entry(frame, textvariable=ip6) entry.bind("", self.app.validation.ip_focus_out) @@ -187,7 +186,7 @@ class NodeConfigDialog(Dialog): frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="Apply", command=self.config_apply) - button.grid(row=0, column=0, padx=2, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index dfcef922..c49aea50 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -6,10 +6,9 @@ from tkinter import messagebox, ttk from coretk.dialogs.dialog import Dialog from coretk.dialogs.serviceconfiguration import ServiceConfiguration +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CheckboxList, ListboxScroll -PAD = 5 - class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): @@ -36,11 +35,11 @@ class NodeService(Dialog): self.top.rowconfigure(0, weight=1) frame = ttk.Frame(self.top) - frame.grid(stick="nsew") + frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=PAD) + self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) self.groups.grid(row=0, column=0, sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) @@ -48,11 +47,15 @@ class NodeService(Dialog): self.groups.listbox.selection_set(0) self.services = CheckboxList( - frame, self.app, text="Services", clicked=self.service_clicked, padding=PAD + frame, + self.app, + text="Services", + clicked=self.service_clicked, + padding=FRAME_PAD, ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=PAD) + self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) self.current.grid(row=0, column=2, sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -62,9 +65,9 @@ class NodeService(Dialog): for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Configure", command=self.click_configure) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=1, sticky="ew", padx=PAD) + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=2, sticky="ew") diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index 525939e0..f496f7ef 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -3,8 +3,7 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog - -PAD = 5 +from coretk.themes import PADX, PADY class ObserverDialog(Dialog): @@ -29,7 +28,7 @@ class ObserverDialog(Dialog): def draw_listbox(self): frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -48,32 +47,32 @@ class ObserverDialog(Dialog): def draw_form_fields(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Command") - label.grid(row=1, column=0, sticky="w", padx=PAD) + label.grid(row=1, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, textvariable=self.cmd) entry.grid(row=1, column=1, sticky="ew") def draw_config_buttons(self): frame = ttk.Frame(self.top) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew") + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -87,7 +86,7 @@ class ObserverDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save_config) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/preferences.py b/coretk/coretk/dialogs/preferences.py index b094eec6..8c369027 100644 --- a/coretk/coretk/dialogs/preferences.py +++ b/coretk/coretk/dialogs/preferences.py @@ -4,8 +4,7 @@ from tkinter import ttk from coretk import appconfig from coretk.dialogs.dialog import Dialog - -PAD = 5 +from coretk.themes import FRAME_PAD, PADX, PADY class PreferencesDialog(Dialog): @@ -25,12 +24,12 @@ class PreferencesDialog(Dialog): self.draw_buttons() def draw_preferences(self): - frame = ttk.LabelFrame(self.top, text="Preferences", padding=PAD) - frame.grid(sticky="nsew", pady=2) + frame = ttk.LabelFrame(self.top, text="Preferences", padding=FRAME_PAD) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Theme") - label.grid(row=0, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=0, column=0, pady=PADY, padx=PADX, sticky="w") themes = self.app.style.theme_names() combobox = ttk.Combobox( frame, textvariable=self.theme, values=themes, state="readonly" @@ -40,14 +39,14 @@ class PreferencesDialog(Dialog): combobox.bind("<>", self.theme_change) label = ttk.Label(frame, text="Editor") - label.grid(row=1, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=1, column=0, pady=PADY, padx=PADX, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.editor, values=appconfig.EDITORS, state="readonly" ) combobox.grid(row=1, column=1, sticky="ew") label = ttk.Label(frame, text="Terminal") - label.grid(row=2, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=2, column=0, pady=PADY, padx=PADX, sticky="w") combobox = ttk.Combobox( frame, textvariable=self.terminal, @@ -57,7 +56,7 @@ class PreferencesDialog(Dialog): combobox.grid(row=2, column=1, sticky="ew") label = ttk.Label(frame, text="3D GUI") - label.grid(row=3, column=0, pady=PAD, padx=PAD, sticky="w") + label.grid(row=3, column=0, pady=PADY, padx=PADX, sticky="w") entry = ttk.Entry(frame, textvariable=self.gui3d) entry.grid(row=3, column=1, sticky="ew") @@ -68,7 +67,7 @@ class PreferencesDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) - button.grid(row=0, column=0, sticky="ew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index 18c5d6d1..ef406ecb 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -3,8 +3,8 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog +from coretk.themes import FRAME_PAD, PADX, PADY -PAD = 5 DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" DEFAULT_PORT = 50051 @@ -33,7 +33,7 @@ class ServersDialog(Dialog): def draw_servers(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="nsew") + frame.grid(pady=PADY, sticky="nsew") frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) @@ -52,24 +52,24 @@ class ServersDialog(Dialog): scrollbar.config(command=self.servers.yview) def draw_server_configuration(self): - frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=PAD) - frame.grid(pady=PAD, sticky="ew") + frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) + frame.grid(pady=PADY, sticky="ew") frame.columnconfigure(1, weight=1) frame.columnconfigure(3, weight=1) frame.columnconfigure(5, weight=1) label = ttk.Label(frame, text="Name") - label.grid(row=0, column=0, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=0, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.name) entry.grid(row=0, column=1, sticky="ew") label = ttk.Label(frame, text="Address") - label.grid(row=0, column=2, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=2, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry(frame, textvariable=self.address) entry.grid(row=0, column=3, sticky="ew") label = ttk.Label(frame, text="Port") - label.grid(row=0, column=4, sticky="w", padx=PAD, pady=PAD) + label.grid(row=0, column=4, sticky="w", padx=PADX, pady=PADY) entry = ttk.Entry( frame, textvariable=self.port, @@ -83,17 +83,17 @@ class ServersDialog(Dialog): def draw_servers_buttons(self): frame = ttk.Frame(self.top) - frame.grid(pady=PAD, sticky="ew") + frame.grid(pady=PADY, sticky="ew") for i in range(3): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Create", command=self.click_create) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) self.save_button = ttk.Button( frame, text="Save", state=tk.DISABLED, command=self.click_save ) - self.save_button.grid(row=0, column=1, sticky="ew", padx=PAD) + self.save_button.grid(row=0, column=1, sticky="ew", padx=PADX) self.delete_button = ttk.Button( frame, text="Delete", state=tk.DISABLED, command=self.click_delete @@ -109,7 +109,7 @@ class ServersDialog(Dialog): button = ttk.Button( frame, text="Save Configuration", command=self.click_save_configuration ) - button.grid(row=0, column=0, sticky="ew", padx=PAD) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/dialogs/sessionoptions.py b/coretk/coretk/dialogs/sessionoptions.py index 7c77f9df..24ff7381 100644 --- a/coretk/coretk/dialogs/sessionoptions.py +++ b/coretk/coretk/dialogs/sessionoptions.py @@ -5,11 +5,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD_X = 2 -PAD_Y = 2 - class SessionOptionsDialog(Dialog): def __init__(self, master, app): @@ -33,16 +31,16 @@ class SessionOptionsDialog(Dialog): self.config_frame = ConfigFrame(self.top, self.app, config=self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew") + self.config_frame.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) frame.grid(sticky="ew") for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.save) - button.grid(row=0, column=0, pady=PAD_Y, padx=PAD_X, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, pady=PAD_Y, padx=PAD_X, sticky="ew") + button.grid(row=0, column=1, padx=PADX, sticky="ew") def save(self): config = self.config_frame.parse_config() diff --git a/coretk/coretk/dialogs/sessions.py b/coretk/coretk/dialogs/sessions.py index f8d26667..b1fb970f 100644 --- a/coretk/coretk/dialogs/sessions.py +++ b/coretk/coretk/dialogs/sessions.py @@ -9,8 +9,7 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images - -PAD = 5 +from coretk.themes import PADX, PADY class SessionsDialog(Dialog): @@ -51,13 +50,13 @@ class SessionsDialog(Dialog): "one you might be concurrently editting.", justify=tk.CENTER, ) - label.grid(pady=PAD) + label.grid(pady=PADY) def draw_tree(self): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - frame.grid(sticky="nsew") + frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("id", "state", "nodes"), show="headings" ) @@ -85,7 +84,7 @@ class SessionsDialog(Dialog): self.tree.configure(yscrollcommand=yscrollbar.set) xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) - xscrollbar.grid(row=1, sticky="ew", pady=5) + xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) def draw_buttons(self): @@ -99,7 +98,7 @@ class SessionsDialog(Dialog): frame, image=image, text="New", compound=tk.LEFT, command=self.click_new ) b.image = image - b.grid(row=0, padx=2, sticky="ew") + b.grid(row=0, padx=PADX, sticky="ew") image = Images.get(ImageEnum.FILEOPEN, 16) b = ttk.Button( @@ -110,7 +109,7 @@ class SessionsDialog(Dialog): command=self.click_connect, ) b.image = image - b.grid(row=0, column=1, padx=2, sticky="ew") + b.grid(row=0, column=1, padx=PADX, sticky="ew") image = Images.get(ImageEnum.EDITDELETE, 16) b = ttk.Button( @@ -121,10 +120,10 @@ class SessionsDialog(Dialog): command=self.click_shutdown, ) b.image = image - b.grid(row=0, column=2, padx=2, sticky="ew") + b.grid(row=0, column=2, padx=PADX, sticky="ew") b = ttk.Button(frame, text="Cancel", command=self.click_new) - b.grid(row=0, column=3, padx=2, sticky="ew") + b.grid(row=0, column=3, sticky="ew") def click_new(self): self.app.core.create_new_session() diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 63037c9d..a6da0e3b 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -7,9 +7,8 @@ from tkinter import colorchooser, font, ttk from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text +from coretk.themes import FRAME_PAD, PADX, PADY -PADX = (0, 5) -PAD = 5 FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72] BORDER_WIDTH = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -50,16 +49,16 @@ class ShapeDialog(Dialog): self.draw_buttons() def draw_label_options(self): - label_frame = ttk.LabelFrame(self.top, text="Label", padding=PAD) + label_frame = ttk.LabelFrame(self.top, text="Label", padding=FRAME_PAD) label_frame.grid(sticky="ew") label_frame.columnconfigure(0, weight=1) entry = ttk.Entry(label_frame, textvariable=self.shape_text) - entry.grid(sticky="ew", pady=PAD) + entry.grid(sticky="ew", pady=PADY) # font options frame = ttk.Frame(label_frame) - frame.grid(sticky="nsew", padx=3, pady=3) + frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) @@ -73,7 +72,7 @@ class ShapeDialog(Dialog): combobox = ttk.Combobox( frame, textvariable=self.font_size, values=FONT_SIZES, state="readonly" ) - combobox.grid(row=0, column=1, padx=3, sticky="nsew") + combobox.grid(row=0, column=1, padx=PADX, sticky="nsew") button = ttk.Button(frame, text="Color", command=self.choose_text_color) button.grid(row=0, column=2, sticky="nsew") @@ -85,13 +84,13 @@ class ShapeDialog(Dialog): button = ttk.Checkbutton(frame, variable=self.bold, text="Bold") button.grid(row=0, column=0, sticky="ew") button = ttk.Checkbutton(frame, variable=self.italic, text="Italic") - button.grid(row=0, column=1, padx=3, sticky="ew") + button.grid(row=0, column=1, padx=PADX, sticky="ew") button = ttk.Checkbutton(frame, variable=self.underline, text="Underline") button.grid(row=0, column=2, sticky="ew") def draw_shape_options(self): - label_frame = ttk.LabelFrame(self.top, text="Shape", padding=PAD) - label_frame.grid(sticky="ew", pady=PAD) + label_frame = ttk.LabelFrame(self.top, text="Shape", padding=FRAME_PAD) + label_frame.grid(sticky="ew", pady=PADY) label_frame.columnconfigure(0, weight=1) frame = ttk.Frame(label_frame) @@ -115,7 +114,7 @@ class ShapeDialog(Dialog): button.grid(row=1, column=2, sticky="ew") frame = ttk.Frame(label_frame) - frame.grid(sticky="ew", pady=PAD) + frame.grid(sticky="ew", pady=PADY) frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Border Width") label.grid(row=0, column=0, sticky="w", padx=PADX) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index 711f94f0..aed411b8 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -8,10 +8,9 @@ import grpc from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error +from coretk.themes import PADX, PADY from coretk.widgets import ConfigFrame -PAD = 5 - class WlanConfigDialog(Dialog): def __init__(self, master, app, canvas_node): @@ -33,7 +32,7 @@ class WlanConfigDialog(Dialog): self.top.rowconfigure(0, weight=1) self.config_frame = ConfigFrame(self.top, self.app, self.config) self.config_frame.draw_config() - self.config_frame.grid(sticky="nsew", pady=PAD) + self.config_frame.grid(sticky="nsew", pady=PADY) self.draw_apply_buttons() def draw_apply_buttons(self): @@ -48,7 +47,7 @@ class WlanConfigDialog(Dialog): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, padx=PAD, sticky="ew") + button.grid(row=0, column=0, padx=PADX, sticky="ew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index a870b278..cb8a8b06 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,7 +1,11 @@ import logging import tkinter as tk -DARK = "black" +THEME_DARK = "black" +PADX = (0, 5) +PADY = (0, 5) +FRAME_PAD = 5 +DIALOG_PAD = 5 class Styles: @@ -28,7 +32,7 @@ class Colors: def load(style): style.theme_create( - DARK, + THEME_DARK, "clam", { ".": { diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index e3fd5c35..cdbfea28 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { core_pb2.ConfigOptionType.UINT8, @@ -16,7 +17,6 @@ INT_TYPES = { core_pb2.ConfigOptionType.INT32, core_pb2.ConfigOptionType.INT64, } -PAD = 5 def file_button_click(value): @@ -71,8 +71,6 @@ class ConfigFrame(FrameScroll): self.values = {} def draw_config(self): - padx = 2 - pady = 2 group_mapping = {} for key in self.config: option = self.config[key] @@ -81,19 +79,19 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame, padding=PAD) + frame = ttk.Frame(self.frame, padding=FRAME_PAD) frame.columnconfigure(1, weight=1) self.frame.add(frame, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): label = ttk.Label(frame, text=option.label) - label.grid(row=index, pady=pady, padx=padx, sticky="w") + label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) combobox = ttk.Combobox( frame, textvariable=value, values=select, state="readonly" ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) + combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": value.set("On") else: @@ -104,15 +102,15 @@ class ConfigFrame(FrameScroll): combobox = ttk.Combobox( frame, textvariable=value, values=select, state="readonly" ) - combobox.grid(row=index, column=1, sticky="ew", pady=pady) + combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) if "file" in option.label: file_frame = ttk.Frame(frame) - file_frame.grid(row=index, column=1, sticky="ew", pady=pady) + file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value) - entry.grid(row=0, column=0, sticky="ew", padx=padx) + entry.grid(row=0, column=0, sticky="ew", padx=PADX) func = partial(file_button_click, value) button = ttk.Button(file_frame, text="...", command=func) button.grid(row=0, column=1) @@ -124,10 +122,10 @@ class ConfigFrame(FrameScroll): validate="key", validatecommand=(self.app.validation.ip4, "%P"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") else: entry = ttk.Entry(frame, textvariable=value) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") elif option.type in INT_TYPES: value.set(option.value) @@ -141,7 +139,7 @@ class ConfigFrame(FrameScroll): "", lambda event: self.app.validation.focus_out(event, "0"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = ttk.Entry( @@ -154,7 +152,7 @@ class ConfigFrame(FrameScroll): "", lambda event: self.app.validation.focus_out(event, "0"), ) - entry.grid(row=index, column=1, sticky="ew", pady=pady) + entry.grid(row=index, column=1, sticky="ew") else: logging.error("unhandled config option type: %s", option.type) self.values[option.name] = value From d6ae39089ed3da8c1d591eaafc2a817f8e467d4a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 14:42:00 -0800 Subject: [PATCH 249/305] select box in select mode, link to selected for wlan/emane nodes, send session data to damon before save xml --- coretk/coretk/coreclient.py | 59 +++++++++++++++++++++++++++++--- coretk/coretk/graph/graph.py | 66 +++++++++++++++++++++++++++++++----- coretk/coretk/graph/node.py | 15 +++++++- coretk/coretk/graph/shape.py | 3 ++ 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 827f4d01..36562cce 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -421,8 +421,8 @@ class CoreClient: mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() hooks = list(self.hooks.values()) - service_configs = self.get_service_config_proto() - file_configs = self.get_service_file_config_proto() + service_configs = self.get_service_configs_proto() + file_configs = self.get_service_file_configs_proto() if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -512,6 +512,11 @@ class CoreClient: :return: nothing """ try: + if self.state != core_pb2.SessionState.RUNTIME: + logging.debug( + "session state not runtime, send session data to the daemon..." + ) + self.send_data() response = self.client.save_xml(self.session_id, file_path) logging.info("saved xml(%s): %s", file_path, response) except grpc.RpcError as e: @@ -586,6 +591,52 @@ class CoreClient: ) logging.debug("create link: %s", response) + def send_data(self): + """ + send to daemon all session info, but don't start the session + + :return: nothing + """ + self.create_nodes_and_links() + for config_proto in self.get_wlan_configs_proto(): + self.client.set_wlan_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_mobility_configs_proto(): + self.client.set_mobility_config( + self.session_id, config_proto.node_id, config_proto.config + ) + for config_proto in self.get_service_configs_proto(): + self.client.set_node_service( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.startup, + config_proto.validate, + config_proto.shutdown, + ) + for config_proto in self.get_service_file_configs_proto(): + self.client.set_node_service_file( + self.session_id, + config_proto.node_id, + config_proto.service, + config_proto.file, + config_proto.data, + ) + for hook in self.hooks.values(): + self.client.add_hook(self.session_id, hook.state, hook.file, hook.data) + for config_proto in self.get_emane_model_configs_proto(): + self.client.set_emane_model_config( + self.session_id, + config_proto.node_id, + config_proto.model, + config_proto.config, + config_proto.interface_id, + ) + if self.emane_config: + config = {x: self.emane_config[x].value for x in self.emane_config} + self.client.set_emane_config(self.session_id, config) + def close(self): """ Clean ups when done using grpc @@ -762,7 +813,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_config_proto(self): + def get_service_configs_proto(self): configs = [] for node_id, services in self.service_configs.items(): for name, config in services.items(): @@ -776,7 +827,7 @@ class CoreClient: configs.append(config_proto) return configs - def get_service_file_config_proto(self): + def get_service_file_configs_proto(self): configs = [] for (node_id, file_configs) in self.file_configs.items(): for service, file_config in file_configs.items(): diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index fc5d6fd7..e79e0f9c 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -11,7 +11,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import is_draw_shape +from coretk.graph.shapeutils import ShapeType, is_draw_shape from coretk.nodeutils import NodeUtils SCROLL_BUFFER = 25 @@ -32,6 +32,7 @@ class CanvasGraph(tk.Canvas): self.mode = GraphMode.SELECT self.annotation_type = None self.selection = {} + self.select_box = None self.selected = None self.node_draw = None self.context = None @@ -137,6 +138,13 @@ class CanvasGraph(tk.Canvas): self.tag_lower(self.grid) def add_wireless_edge(self, src, dst): + """ + add a wireless edge between 2 canvas nodes + + :param CanvasNode src: source node + :param CanvasNode dst: destination node + :return: nothing + """ token = tuple(sorted((src.id, dst.id))) x1, y1 = self.coords(src.id) x2, y2 = self.coords(dst.id) @@ -249,6 +257,7 @@ class CanvasGraph(tk.Canvas): :param event: mouse event :return: nothing """ + logging.debug("click release") if self.context: self.context.unpost() self.context = None @@ -260,6 +269,19 @@ class CanvasGraph(tk.Canvas): shape = self.shapes[self.selected] shape.shape_complete(x, y) self.shape_drawing = False + elif self.mode == GraphMode.SELECT: + self.focus_set() + if self.select_box: + x0, y0, x1, y1 = self.coords(self.select_box.id) + inside = [ + x + for x in self.find_enclosed(x0, y0, x1, y1) + if "node" in self.gettags(x) or "shape" in self.gettags(x) + ] + for i in inside: + self.select_object(i, True) + self.select_box.disappear() + self.select_box = None else: self.focus_set() self.selected = self.get_selected(event) @@ -445,6 +467,10 @@ class CanvasGraph(tk.Canvas): self.select_object(node.id) self.selected = selected else: + logging.debug("create selection box") + if self.mode == GraphMode.SELECT: + shape = Shape(self.app, self, ShapeType.RECTANGLE, x, y) + self.select_box = shape self.clear_selection() def ctrl_click(self, event): @@ -486,14 +512,18 @@ class CanvasGraph(tk.Canvas): return # move selected objects - for selected_id in self.selection: - if selected_id in self.shapes: - shape = self.shapes[selected_id] - shape.motion(x_offset, y_offset) + if len(self.selection) > 0: + for selected_id in self.selection: + if selected_id in self.shapes: + shape = self.shapes[selected_id] + shape.motion(x_offset, y_offset) - if selected_id in self.nodes: - node = self.nodes[selected_id] - node.motion(x_offset, y_offset, update=self.core.is_runtime()) + if selected_id in self.nodes: + node = self.nodes[selected_id] + node.motion(x_offset, y_offset, update=self.core.is_runtime()) + else: + if self.select_box and self.mode == GraphMode.SELECT: + self.select_box.shape_motion(x, y) def click_context(self, event): logging.info("context event: %s", self.context) @@ -681,3 +711,23 @@ class CanvasGraph(tk.Canvas): def is_selection_mode(self): return self.mode == GraphMode.SELECT + + def create_edge(self, source, dest): + """ + create an edge between source node and destination node + + :param CanvasNode source: source node + :param CanvasNode dest: destination node + :return: nothing + """ + if tuple([source.id, dest.id]) not in self.edges: + pos0 = source.core_node.position + x0 = pos0.x + y0 = pos0.y + edge = CanvasEdge(x0, y0, x0, y0, source.id, self) + edge.complete(dest.id) + self.edges[edge.token] = edge + self.nodes[source.id].edges.add(edge) + self.nodes[dest.id].edges.add(edge) + link = self.core.create_link(edge, source, dest) + edge.link_info = LinkInfo(self, edge, link) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 5757f081..bdad50e6 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -3,6 +3,7 @@ from tkinter import font import grpc +from core.api.grpc import core_pb2 from core.api.grpc.core_pb2 import NodeType from coretk.dialogs.emaneconfig import EmaneConfigDialog from coretk.dialogs.mobilityconfig import MobilityConfigDialog @@ -191,7 +192,9 @@ class CanvasNode: label="Mobility Config", command=self.show_mobility_config ) if NodeUtils.is_wireless_node(self.core_node.type): - context.add_command(label="Link To All MDRs", state=tk.DISABLED) + context.add_command( + label="Link To Selected", command=self.wireless_link_selected + ) context.add_command(label="Select Members", state=tk.DISABLED) context.add_command(label="Select Adjacent", state=tk.DISABLED) context.add_command(label="Create Link To", state=tk.DISABLED) @@ -233,3 +236,13 @@ class CanvasNode: self.canvas.context = None dialog = NodeService(self.app.master, self.app, self) dialog.show() + + def wireless_link_selected(self): + self.canvas.context = None + for canvas_nid in [ + x for x in self.canvas.selection if "node" in self.canvas.gettags(x) + ]: + core_node = self.canvas.nodes[canvas_nid].core_node + if core_node.type == core_pb2.NodeType.DEFAULT and core_node.model == "mdr": + self.canvas.create_edge(self, self.canvas.nodes[canvas_nid]) + self.canvas.clear_selection() diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 56e679d2..80621107 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -132,6 +132,9 @@ class Shape: s = ShapeDialog(self.app, self.app, self) s.show() + def disappear(self): + self.canvas.delete(self.id) + def motion(self, x_offset, y_offset): self.canvas.move(self.id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) From 80fb0e26b61f1a5d846b920878be014ccbbc8a00 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:16:59 -0800 Subject: [PATCH 250/305] attempt to work on check engine light dialog --- coretk/coretk/dialogs/cel.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 847d245b..a5fbea63 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -1,6 +1,7 @@ """ check engine light """ +import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog @@ -17,9 +18,14 @@ class CheckLight(Dialog): def draw(self): row = 0 frame = ttk.Frame(self) + label = ttk.Label(frame, text="Check Emulation Light") + label.grid(row=0, column=0) + frame.grid(row=row, column=0) + row = row + 1 + frame = ttk.Frame(self) button = ttk.Button(frame, text="Reset CEL") button.grid(row=0, column=0) - button = ttk.Button(frame, text="View core-daemon log") + button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) button.grid(row=0, column=1) button = ttk.Button(frame, text="View node log") button.grid(row=0, column=2) @@ -27,3 +33,23 @@ class CheckLight(Dialog): button.grid(row=0, column=3) frame.grid(row=row, column=0, sticky="nsew") ++row + + def daemon_log(self): + dialog = DaemonLog(self, self.app) + dialog.show() + + +class DaemonLog(Dialog): + def __init__(self, master, app): + super().__init__(master, app, "core-daemon log", modal=True) + self.columnconfigure(0, weight=1) + self.path = tk.StringVar(value="/var/log/core-daemon.log") + self.draw() + + def draw(self): + frame = ttk.Frame(self) + label = ttk.Label(frame, text="File: ") + label.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.path, state="readonly") + entry.grid(row=0, column=1) + frame.grid(row=0, column=0) From 9c9ccdf04fb8626ef7040efbdd3e1d090b8875c5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 11 Dec 2019 16:21:37 -0800 Subject: [PATCH 251/305] some cleanup to layout for service configuration, updated titles for service config and node config dialogs --- coretk/coretk/dialogs/nodeservice.py | 3 +- coretk/coretk/dialogs/serviceconfiguration.py | 264 ++++++++++-------- coretk/coretk/widgets.py | 1 - 3 files changed, 143 insertions(+), 125 deletions(-) diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index c49aea50..2aa9c6ad 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -12,7 +12,8 @@ from coretk.widgets import CheckboxList, ListboxScroll class NodeService(Dialog): def __init__(self, master, app, canvas_node, services=None): - super().__init__(master, app, "Node Services", modal=True) + title = f"{canvas_node.core_node.name} Services" + super().__init__(master, app, title, modal=True) self.app = app self.canvas_node = canvas_node self.node_id = canvas_node.core_node.id diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 812d1a6a..03c74ab2 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -9,12 +9,14 @@ from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.errors import show_grpc_error from coretk.images import ImageEnum, Images +from coretk.themes import FRAME_PAD, PADX, PADY from coretk.widgets import CodeText, ListboxScroll class ServiceConfiguration(Dialog): def __init__(self, master, app, service_name, node_id): - super().__init__(master, app, f"{service_name} service", modal=True) + title = f"{service_name} Service" + super().__init__(master, app, title, modal=True) self.app = app self.core = app.core self.node_id = node_id @@ -34,7 +36,7 @@ class ServiceConfiguration(Dialog): self.documentnew_img = Images.get(ImageEnum.DOCUMENTNEW, 16) self.editdelete_img = Images.get(ImageEnum.EDITDELETE, 16) - self.tab_parent = None + self.notebook = None self.metadata_entry = None self.filename_combobox = None self.startup_commands_listbox = None @@ -94,88 +96,88 @@ class ServiceConfiguration(Dialog): show_grpc_error(e) def draw(self): - # self.columnconfigure(1, weight=1) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + + # draw metadata frame = ttk.Frame(self.top) - frame1 = ttk.Frame(frame) - label = ttk.Label(frame1, text=self.service_name) - label.grid(row=0, column=0, sticky="ew") - frame1.grid(row=0, column=0) - frame2 = ttk.Frame(frame) - label = ttk.Label(frame2, text="Meta-data") - label.grid(row=0, column=0) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="Meta-data") + label.grid(row=0, column=0, sticky="w", padx=PADX) + self.metadata_entry = ttk.Entry(frame, textvariable=self.metadata) + self.metadata_entry.grid(row=0, column=1, sticky="ew") - self.metadata_entry = ttk.Entry(frame2, textvariable=self.metadata) - self.metadata_entry.grid(row=0, column=1) - frame2.grid(row=1, column=0) - frame.grid(row=0, column=0) + # draw notebook + self.notebook = ttk.Notebook(self.top) + self.notebook.grid(sticky="nsew") + self.draw_tab_files() + self.draw_tab_directories() + self.draw_tab_startstop() + self.draw_tab_configuration() - frame = ttk.Frame(self.top) - self.tab_parent = ttk.Notebook(frame) - tab1 = ttk.Frame(self.tab_parent) - tab2 = ttk.Frame(self.tab_parent) - tab3 = ttk.Frame(self.tab_parent) - tab4 = ttk.Frame(self.tab_parent) - tab1.columnconfigure(0, weight=1) - tab2.columnconfigure(0, weight=1) - tab3.columnconfigure(0, weight=1) - tab4.columnconfigure(0, weight=1) + button = ttk.Button(self.top, text="Only Save Changes") + button.grid(sticky="ew", pady=PADY) + self.draw_buttons() - self.tab_parent.add(tab1, text="Files", sticky="nsew") - self.tab_parent.add(tab2, text="Directories", sticky="nsew") - self.tab_parent.add(tab3, text="Startup/shutdown", sticky="nsew") - self.tab_parent.add(tab4, text="Configuration", sticky="nsew") - self.tab_parent.grid(row=0, column=0, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew") + def draw_tab_files(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Files") - # tab 1 label = ttk.Label( - tab1, text="Config files and scripts that are generated for this service." + tab, text="Config files and scripts that are generated for this service." ) - label.grid(row=0, column=0, sticky="nsew") + label.grid() - frame = ttk.Frame(tab1) - label = ttk.Label(frame, text="File name: ") - label.grid(row=0, column=0) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) + label = ttk.Label(frame, text="File Name") + label.grid(row=0, column=0, padx=PADX, sticky="w") self.filename_combobox = ttk.Combobox( frame, values=self.filenames, state="readonly" ) - self.filename_combobox.grid(row=0, column=1) self.filename_combobox.bind( "<>", self.display_service_file_data ) + self.filename_combobox.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, image=self.documentnew_img, state="disabled") button.bind("", self.add_filename) - button.grid(row=0, column=2) + button.grid(row=0, column=2, padx=PADX) button = ttk.Button(frame, image=self.editdelete_img, state="disabled") button.bind("", self.delete_filename) button.grid(row=0, column=3) - frame.grid(row=1, column=0, sticky="nsew") - frame = ttk.Frame(tab1) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) button = ttk.Radiobutton( frame, variable=self.radiovar, - text="Copy this source file:", + text="Copy Source File", value=1, - state="disabled", + state=tk.DISABLED, ) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="w", padx=PADX) entry = ttk.Entry(frame, state=tk.DISABLED) - entry.grid(row=0, column=1) + entry.grid(row=0, column=1, sticky="ew", padx=PADX) image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image button.grid(row=0, column=2) - frame.grid(row=2, column=0, sticky="nsew") - frame = ttk.Frame(tab1) + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(0, weight=1) button = ttk.Radiobutton( frame, variable=self.radiovar, - text="Use text below for file contents:", + text="Use text below for file contents", value=2, ) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="ew") image = Images.get(ImageEnum.FILEOPEN, 16) button = ttk.Button(frame, image=image) button.image = image @@ -184,10 +186,10 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, image=image) button.image = image button.grid(row=0, column=2) - frame.grid(row=3, column=0, sticky="nsew") - self.service_file_data = CodeText(tab1) - self.service_file_data.grid(row=4, column=0, sticky="nsew") + self.service_file_data = CodeText(tab) + self.service_file_data.grid(sticky="nsew") + tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) if len(self.filenames) > 0: self.filename_combobox.current(0) self.service_file_data.delete(1.0, "end") @@ -196,38 +198,60 @@ class ServiceConfiguration(Dialog): ) self.service_file_data.bind("", self.update_temp_service_file_data) - # tab 2 + def draw_tab_directories(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Directories") + label = ttk.Label( - tab2, + tab, text="Directories required by this service that are unique for each node.", ) - label.grid(row=0, column=0, sticky="nsew") + label.grid() + + def draw_tab_startstop(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + for i in range(3): + tab.rowconfigure(i, weight=1) + self.notebook.add(tab, text="Startup/shutdown") # tab 3 for i in range(3): label_frame = None if i == 0: - label_frame = ttk.LabelFrame(tab3, text="Startup commands") + label_frame = ttk.LabelFrame( + tab, text="Startup commands", padding=FRAME_PAD + ) commands = self.startup_commands elif i == 1: - label_frame = ttk.LabelFrame(tab3, text="Shutdown commands") + label_frame = ttk.LabelFrame( + tab, text="Shutdown commands", padding=FRAME_PAD + ) commands = self.shutdown_commands elif i == 2: - label_frame = ttk.LabelFrame(tab3, text="Validation commands") + label_frame = ttk.LabelFrame( + tab, text="Validation commands", padding=FRAME_PAD + ) commands = self.validation_commands label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(1, weight=1) + label_frame.grid(row=i, column=0, sticky="nsew") + frame = ttk.Frame(label_frame) + frame.grid(row=0, column=0, sticky="nsew") frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=tk.StringVar()) - entry.grid(row=0, column=0, stick="nsew") + entry.grid(row=0, column=0, stick="ew", padx=PADX) button = ttk.Button(frame, image=self.documentnew_img) button.bind("", self.add_command) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button(frame, image=self.editdelete_img) - button.grid(row=0, column=2, sticky="nsew") + button.grid(row=0, column=2, sticky="ew") button.bind("", self.delete_command) - frame.grid(row=0, column=0, sticky="nsew") - listbox_scroll = ListboxScroll(label_frame) + listbox_scroll = ListboxScroll(label_frame, borderwidth=0) listbox_scroll.listbox.bind("<>", self.update_entry) for command in commands: listbox_scroll.listbox.insert("end", command) @@ -239,81 +263,75 @@ class ServiceConfiguration(Dialog): self.shutdown_commands_listbox = listbox_scroll.listbox elif i == 2: self.validate_commands_listbox = listbox_scroll.listbox - label_frame.grid(row=i, column=0, sticky="nsew") - # tab 4 - for i in range(2): - label_frame = None - if i == 0: - label_frame = ttk.LabelFrame(tab4, text="Executables") - elif i == 1: - label_frame = ttk.LabelFrame(tab4, text="Dependencies") - label_frame.columnconfigure(0, weight=1) - listbox_scroll = ListboxScroll(label_frame) - listbox_scroll.listbox.config(height=4) - listbox_scroll.grid(row=0, column=0, sticky="nsew") - label_frame.grid(row=i, column=0, sticky="nsew") - if i == 0: - for executable in self.executables: - print(executable) - listbox_scroll.listbox.insert("end", executable) - if i == 1: - for dependency in self.dependencies: - listbox_scroll.listbox.insert("end", dependency) + def draw_tab_configuration(self): + tab = ttk.Frame(self.notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew") + tab.columnconfigure(0, weight=1) + self.notebook.add(tab, text="Configuration", sticky="nsew") - for i in range(3): - frame = ttk.Frame(tab4) - frame.columnconfigure(0, weight=1) - if i == 0: - label = ttk.Label(frame, text="Validation time:") - self.validation_time_entry = ttk.Entry(frame) - self.validation_time_entry.insert("end", self.validation_time) - self.validation_time_entry.config(state="disabled") - self.validation_time_entry.grid(row=i, column=1) - elif i == 1: - label = ttk.Label(frame, text="Validation mode:") - if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: - mode = "BLOCKING" - elif ( - self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING - ): - mode = "NON_BLOCKING" - elif self.validation_mode == core_pb2.ServiceValidationMode.TIMER: - mode = "TIMER" - self.validation_mode_entry = ttk.Entry( - frame, textvariable=tk.StringVar(value=mode) - ) - self.validation_mode_entry.insert("end", mode) - self.validation_mode_entry.config(state="disabled") - self.validation_mode_entry.grid(row=i, column=1) - elif i == 2: - label = ttk.Label(frame, text="Validation period:") - self.validation_period_entry = ttk.Entry( - frame, state="disabled", textvariable=tk.StringVar() - ) - self.validation_period_entry.grid(row=i, column=1) - label.grid(row=i, column=0) - frame.grid(row=2 + i, column=0, sticky="nsew") + frame = ttk.Frame(tab) + frame.grid(sticky="ew", pady=PADY) + frame.columnconfigure(1, weight=1) - button = ttk.Button( - self.top, text="onle store values that have changed from their defaults" + label = ttk.Label(frame, text="Validation Time") + label.grid(row=0, column=0, sticky="w") + self.validation_time_entry = ttk.Entry(frame) + self.validation_time_entry.insert("end", self.validation_time) + self.validation_time_entry.config(state=tk.DISABLED) + self.validation_time_entry.grid(row=0, column=1, sticky="ew") + + label = ttk.Label(frame, text="Validation Mode") + label.grid(row=1, column=0, sticky="w") + if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: + mode = "BLOCKING" + elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: + mode = "NON_BLOCKING" + else: + mode = "TIMER" + self.validation_mode_entry = ttk.Entry( + frame, textvariable=tk.StringVar(value=mode) ) - button.grid(row=2, column=0) + self.validation_mode_entry.insert("end", mode) + self.validation_mode_entry.config(state=tk.DISABLED) + self.validation_mode_entry.grid(row=1, column=1, sticky="ew") + label = ttk.Label(frame, text="Validation Period") + label.grid(row=2, column=0, sticky="w") + self.validation_period_entry = ttk.Entry( + frame, state=tk.DISABLED, textvariable=tk.StringVar() + ) + self.validation_period_entry.grid(row=2, column=1, sticky="ew") + + listbox_scroll = ListboxScroll(tab, text="Executables", padding=FRAME_PAD) + listbox_scroll.grid(sticky="nsew", pady=PADY) + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for executable in self.executables: + listbox_scroll.listbox.insert("end", executable) + + listbox_scroll = ListboxScroll(tab, text="Dependencies", padding=FRAME_PAD) + listbox_scroll.grid(sticky="nsew") + tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) + for dependency in self.dependencies: + listbox_scroll.listbox.insert("end", dependency) + + def draw_buttons(self): frame = ttk.Frame(self.top) + frame.grid(sticky="ew") + for i in range(4): + frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Apply", command=self.click_apply) - button.grid(row=0, column=0, sticky="nsew") + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button( frame, text="Dafults", command=self.click_defaults, state="disabled" ) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button( frame, text="Copy...", command=self.click_copy, state="disabled" ) - button.grid(row=0, column=2, sticky="nsew") + button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=3, sticky="nsew") - frame.grid(row=3, column=0) + button.grid(row=0, column=3, sticky="ew") def add_filename(self, event): # not worry about it for now diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index cdbfea28..0ba16587 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -183,7 +183,6 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) - logging.info("listbox background: %s", self.listbox.cget("background")) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 71e6df76ce6a5c47d7f44cd3c158472a8484c4f3 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 10:49:52 -0800 Subject: [PATCH 252/305] create layout for check emulation light dialog --- coretk/coretk/data/icons/alert.png | Bin 0 -> 2019 bytes coretk/coretk/dialogs/cel.py | 78 +++++++++++++++++++++++------ coretk/coretk/images.py | 1 + coretk/coretk/statusbar.py | 7 ++- 4 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 coretk/coretk/data/icons/alert.png diff --git a/coretk/coretk/data/icons/alert.png b/coretk/coretk/data/icons/alert.png new file mode 100644 index 0000000000000000000000000000000000000000..718fa9f1f2794dc3165f2cf71db560247721c52b GIT binary patch literal 2019 zcmV<92ORi`P)!Qp@lUfU%Db%W|ox?Bz|gZ z*$7A@n3z;FF-GY6FJY@OxU>;W{P&Sqq-~ZzOZ;Hshq}xxsKHjFsiN6x)MAUWrGc_2 zyVK)`ohj?ieQ)l&?@V`gdw_kR z|GUf&^2iDv0b1gIgwMZqfsyke2s+3p%KeKj?a)EK7AjAIj3~1WcsxPkm{j;us6m~K z!}(`#A>1kzp3EvUa{zP`S5o9K(3>K4)deBN`3+7XY?KOr%_29`6r=-m)5U($oDZvR zI`VKnz{MyB^ja>+B0JkV;6lEM@*%Ib3qcsoB)D>Er8Jh^_aTw^% zqF^OYM;V>pU>+EdD!=n->@@-ugc{!^tMeB+ugK{r(D?ve!1vVi;r5$?O56q96kyB( zD=Y{(oZp~CnNRpMY#Al}C~J6~qbQf>FsPxe#8wtkFZ}1n2^vfjkNe$9Rz;W)t+WF9N zKT+G{e1NN{aa&r0^Z*E-F*WD^c~=`{XG2>oEc3A=?Pg*CbdamTbJ3L*yi z;WEAeOteY*Cf&sPq?^ejK-o^VK|Tb|>$=5mdY3cmrq z(+1J&xsms|Zny&=e8k)Y;YHrYH_YW)Q)Cx#++1#fo86aPsJn44ls)n@UN@I(j7)MG zWskXFWyqt*0#L-jMP<;uJP&}+GXKDMH41~vj$)NdUF4kJu3!$hm)e@i{L z(8C-24d}}wX9*{%@(zJBi!U~hfD`m%>wJKHxt(8T8@>DyWj}B`&=2&~|N4R3Q4Y|{ zA9Rq9<GC%aCsMQ)4+mz zBLjjfECkL-m7Ty={HDigV2e~4 zS(<&ysQAv{zDX3DeVde${5Nokq3RiCdAA&5HjfY;pqsU9LD-BkfYeix%CY)S%?W0C zI(W`+=sAZe$*!fXWSp9oses{T81&J@r^#9JQFrWk{jYuehaK2DACyVgh)k-AIB2ST zf&26pdfFh@6dht6nkxz9M)HCRA(xbqvB1wr_-Pno>eK7D;*b`ac$3_Jv^2au-Q++$wW^gBC$hluu>-Vj=|AhRPH_0ms)xR;lwF zqy*usGR0Hp+#oIU(AiJ;mfG^UmySeBj4b+e@$zjdDipNL{`1NC4eCMIpYtT9fgv5V zuYayNpa1trBoc{4B9TZW5{X12kw_#Gi9{m){15#$Llp7S@m>G`002ovPDHLkV1gy+ B*;oJo literal 0 HcmV?d00001 diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index a5fbea63..77b96367 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -5,34 +5,70 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY +from coretk.widgets import CodeText class CheckLight(Dialog): def __init__(self, master, app): super().__init__(master, app, "CEL", modal=True) self.app = app - - self.columnconfigure(0, weight=1) + self.tree = None self.draw() def draw(self): row = 0 frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Check Emulation Light") label.grid(row=0, column=0) - frame.grid(row=row, column=0) + frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") row = row + 1 + frame = ttk.Frame(self) - button = ttk.Button(frame, text="Reset CEL") - button.grid(row=0, column=0) - button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) - button.grid(row=0, column=1) - button = ttk.Button(frame, text="View node log") - button.grid(row=0, column=2) - button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=3) + frame.columnconfigure(0, weight=1) frame.grid(row=row, column=0, sticky="nsew") - ++row + self.tree = ttk.Treeview( + frame, columns=("time", "level", "node", "source"), show="headings" + ) + self.tree.grid(row=0, column=0, sticky="nsew") + self.tree.column("time", stretch=tk.YES) + self.tree.heading("time", text="time") + self.tree.column("level", stretch=tk.YES) + self.tree.heading("level", text="level") + self.tree.column("node", stretch=tk.YES) + self.tree.heading("node", text="node") + self.tree.column("source", stretch=tk.YES) + self.tree.heading("source", text="source") + + yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=yscrollbar.set) + + xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) + xscrollbar.grid(row=1, sticky="ew") + self.tree.configure(xscrollcommand=xscrollbar.set) + row = row + 1 + + text = CodeText(self) + text.grid(row=row, column=0, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=1) + frame.columnconfigure(3, weight=1) + button = ttk.Button(frame, text="Reset CEL") + button.grid(row=0, column=0, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) + button.grid(row=0, column=1, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="View node log") + button.grid(row=0, column=2, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="Close", command=self.destroy) + button.grid(row=0, column=3, sticky="nsew", padx=PADX) + frame.grid(row=row, column=0, sticky="nsew") + row = row + 1 def daemon_log(self): dialog = DaemonLog(self, self.app) @@ -48,8 +84,20 @@ class DaemonLog(Dialog): def draw(self): frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=9) label = ttk.Label(frame, text="File: ") label.grid(row=0, column=0) - entry = ttk.Entry(frame, textvariable=self.path, state="readonly") - entry.grid(row=0, column=1) - frame.grid(row=0, column=0) + entry = ttk.Entry(frame, textvariable=self.path, state="disabled") + entry.grid(row=0, column=1, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + try: + file = open("/var/log/core-daemon.log", "r") + log = file.readlines() + except FileNotFoundError: + log = "Log file not found" + text = CodeText(self) + text.insert("1.0", log) + text.see("end") + text.config(state=tk.DISABLED) + text.grid(row=1, column=0, sticky="nsew") diff --git a/coretk/coretk/images.py b/coretk/coretk/images.py index db7928b9..9d282dbb 100644 --- a/coretk/coretk/images.py +++ b/coretk/coretk/images.py @@ -67,3 +67,4 @@ class ImageEnum(Enum): ANTENNA = "antenna" DOCKER = "docker" LXC = "lxc" + ALERT = "alert" diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index f45dce9f..797d61a8 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -3,6 +3,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.cel import CheckLight +from coretk.images import ImageEnum, Images class StatusBar(ttk.Frame): @@ -54,9 +55,11 @@ class StatusBar(ttk.Frame): ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - self.emulation_light = ttk.Label( - self, text="CEL TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + image = Images.get(ImageEnum.ALERT, 18) + self.emulation_light = ttk.Button( + self, image=image, text="Alert", compound="left" ) + self.emulation_light.image = image self.emulation_light.bind("", self.cel_callback) self.emulation_light.grid(row=0, column=4, sticky="ew") From d34a58dff0f50f7194e2f2f26e0140375ce6e07a Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 11:04:55 -0800 Subject: [PATCH 253/305] small edits on cel dialog --- coretk/coretk/dialogs/cel.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 77b96367..7f95fc79 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -5,6 +5,7 @@ import tkinter as tk from tkinter import ttk from coretk.dialogs.dialog import Dialog +from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY from coretk.widgets import CodeText @@ -14,14 +15,20 @@ class CheckLight(Dialog): super().__init__(master, app, "CEL", modal=True) self.app = app self.tree = None + self.text = None self.draw() def draw(self): row = 0 frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + image = Images.get(ImageEnum.ALERT, 18) + label = ttk.Label(frame, image=image) + label.image = image + label.grid(row=0, column=0, sticky="e") label = ttk.Label(frame, text="Check Emulation Light") - label.grid(row=0, column=0) + label.grid(row=0, column=1, sticky="w") frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") row = row + 1 @@ -50,8 +57,8 @@ class CheckLight(Dialog): self.tree.configure(xscrollcommand=xscrollbar.set) row = row + 1 - text = CodeText(self) - text.grid(row=row, column=0, sticky="nsew") + self.text = CodeText(self) + self.text.grid(row=row, column=0, sticky="nsew") row = row + 1 frame = ttk.Frame(self) @@ -59,7 +66,7 @@ class CheckLight(Dialog): frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - button = ttk.Button(frame, text="Reset CEL") + button = ttk.Button(frame, text="Reset CEL", command=self.reset_cel) button.grid(row=0, column=0, sticky="nsew", padx=PADX) button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) button.grid(row=0, column=1, sticky="nsew", padx=PADX) @@ -70,6 +77,9 @@ class CheckLight(Dialog): frame.grid(row=row, column=0, sticky="nsew") row = row + 1 + def reset_cel(self): + self.text.delete("1.0", tk.END) + def daemon_log(self): dialog = DaemonLog(self, self.app) dialog.show() From 4e9de862a30e169a04f1c002556918577391fc80 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 11:06:52 -0800 Subject: [PATCH 254/305] updated nodes to store actual positions, instead of scaled ones, updated shapes to return actual positions for metadata, added some logic for resetting the canvas and drawn nodes when resizing, convert node update coords into scaled coords for moving nodes --- coretk/coretk/appconfig.py | 25 +- coretk/coretk/coreclient.py | 12 +- coretk/coretk/data/mobility/sample1.scen | 28 + coretk/coretk/data/xmls/sample1.xml | 786 ++++++++++---------- coretk/coretk/dialogs/canvassizeandscale.py | 8 +- coretk/coretk/graph/graph.py | 88 ++- coretk/coretk/graph/node.py | 17 +- coretk/coretk/graph/shape.py | 10 + coretk/coretk/menuaction.py | 6 +- coretk/coretk/menubar.py | 15 +- 10 files changed, 535 insertions(+), 460 deletions(-) create mode 100644 coretk/coretk/data/mobility/sample1.scen diff --git a/coretk/coretk/appconfig.py b/coretk/coretk/appconfig.py index b2606994..33b08793 100644 --- a/coretk/coretk/appconfig.py +++ b/coretk/coretk/appconfig.py @@ -14,7 +14,7 @@ CUSTOM_EMANE_PATH = HOME_PATH.joinpath("custom_emane") CUSTOM_SERVICE_PATH = HOME_PATH.joinpath("custom_services") ICONS_PATH = HOME_PATH.joinpath("icons") MOBILITY_PATH = HOME_PATH.joinpath("mobility") -XML_PATH = HOME_PATH.joinpath("xml") +XMLS_PATH = HOME_PATH.joinpath("xmls") CONFIG_PATH = HOME_PATH.joinpath("gui.yaml") # local paths @@ -22,6 +22,7 @@ DATA_PATH = Path(__file__).parent.joinpath("data") LOCAL_ICONS_PATH = DATA_PATH.joinpath("icons").absolute() LOCAL_BACKGROUND_PATH = DATA_PATH.joinpath("backgrounds").absolute() LOCAL_XMLS_PATH = DATA_PATH.joinpath("xmls").absolute() +LOCAL_MOBILITY_PATH = DATA_PATH.joinpath("mobility").absolute() # configuration data TERMINALS = [ @@ -43,6 +44,12 @@ class IndentDumper(yaml.Dumper): return super().increase_indent(flow, False) +def copy_files(current_path, new_path): + for current_file in current_path.glob("*"): + new_file = new_path.joinpath(current_file.name) + shutil.copy(current_file, new_file) + + def check_directory(): if HOME_PATH.exists(): logging.info("~/.coretk exists") @@ -54,16 +61,12 @@ def check_directory(): CUSTOM_SERVICE_PATH.mkdir() ICONS_PATH.mkdir() MOBILITY_PATH.mkdir() - XML_PATH.mkdir() - for image in LOCAL_ICONS_PATH.glob("*"): - new_image = ICONS_PATH.joinpath(image.name) - shutil.copy(image, new_image) - for background in LOCAL_BACKGROUND_PATH.glob("*"): - new_background = BACKGROUNDS_PATH.joinpath(background.name) - shutil.copy(background, new_background) - for xml_file in LOCAL_XMLS_PATH.glob("*"): - new_xml = XML_PATH.joinpath(xml_file.name) - shutil.copy(xml_file, new_xml) + XMLS_PATH.mkdir() + + copy_files(LOCAL_ICONS_PATH, ICONS_PATH) + copy_files(LOCAL_BACKGROUND_PATH, BACKGROUNDS_PATH) + copy_files(LOCAL_XMLS_PATH, XMLS_PATH) + copy_files(LOCAL_MOBILITY_PATH, MOBILITY_PATH) if "TERM" in os.environ: terminal = TERMINALS[0] diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 36562cce..57224a93 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -300,14 +300,16 @@ class CoreClient: width = self.app.guiconfig["preferences"]["width"] height = self.app.guiconfig["preferences"]["height"] - width, height = canvas_config.get("dimensions", [width, height]) - self.app.canvas.redraw_canvas(width, height) + dimensions = canvas_config.get("dimensions", [width, height]) + self.app.canvas.redraw_canvas(dimensions) wallpaper = canvas_config.get("wallpaper") if wallpaper: wallpaper = str(appconfig.BACKGROUNDS_PATH.joinpath(wallpaper)) - self.app.canvas.set_wallpaper(wallpaper) - self.app.canvas.update_grid() + self.app.canvas.set_wallpaper(wallpaper) + else: + self.app.canvas.redraw_canvas() + self.app.canvas.set_wallpaper(None) # load saved shapes shapes_config = config.get("shapes") @@ -482,7 +484,7 @@ class CoreClient: "wallpaper-style": self.app.canvas.scale_option.get(), "gridlines": self.app.canvas.show_grid.get(), "fit_image": self.app.canvas.adjust_to_dim.get(), - "dimensions": self.app.canvas.width_and_height(), + "dimensions": self.app.canvas.current_dimensions, } canvas_config = json.dumps(canvas_config) diff --git a/coretk/coretk/data/mobility/sample1.scen b/coretk/coretk/data/mobility/sample1.scen new file mode 100644 index 00000000..c2fc5a44 --- /dev/null +++ b/coretk/coretk/data/mobility/sample1.scen @@ -0,0 +1,28 @@ +# +# nodes: 4, max time: 27.000000, max x: 600.00, max y: 600.00 +# nominal range: 300.00 link bw: 54000000.00 +# pause: 30.00, min speed 1.50 max speed: 4.50 + +$node_(6) set X_ 780.0 +$node_(6) set Y_ 228.0 +$node_(6) set Z_ 0.00 +$node_(7) set X_ 816.0 +$node_(7) set Y_ 348.0 +$node_(7) set Z_ 0.00 +$node_(8) set X_ 672.0 +$node_(8) set Y_ 420.0 +$node_(8) set Z_ 0.00 +$node_(9) set X_ 672.0 +$node_(9) set Y_ 96.0 +$node_(9) set Z_ 0.00 +$ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" +$ns_ at 2.00 "$node_(7) setdest 400.0 288.0 15.0" +$ns_ at 1.00 "$node_(8) setdest 590.0 520.0 17.0" +$ns_ at 3.00 "$node_(9) setdest 720.0 300.0 20.0" +$ns_ at 8.00 "$node_(7) setdest 600.0 350.0 10.0" +$ns_ at 9.00 "$node_(8) setdest 730.0 300.0 15.0" +$ns_ at 10.00 "$node_(6) setdest 600.0 108.0 10.0" +$ns_ at 16.00 "$node_(9) setdest 672.0 96.0 20.0" +$ns_ at 17.00 "$node_(7) setdest 816.0 348.0 20.0" +$ns_ at 18.00 "$node_(6) setdest 780.0 228.0 25.0" +$ns_ at 22.00 "$node_(8) setdest 672.0 420.0 20.0" diff --git a/coretk/coretk/data/xmls/sample1.xml b/coretk/coretk/data/xmls/sample1.xml index 8c61b7de..afec8874 100644 --- a/coretk/coretk/data/xmls/sample1.xml +++ b/coretk/coretk/data/xmls/sample1.xml @@ -1,5 +1,5 @@ - + @@ -18,15 +18,6 @@ - - - - - - - - - @@ -36,6 +27,23 @@ + + + + + + + + + + + + + + + + + @@ -44,6 +52,14 @@ + + + + + + + + @@ -61,28 +77,12 @@ - - - - - - - - - - - - - - - - @@ -117,62 +117,62 @@ - - + + - - + + - - + + - - - - - - + + + + + + - - - - - - - - - + + + + + + + + + @@ -188,7 +188,7 @@ - + @@ -367,6 +367,177 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.1.1/24 + ipv6 address a:1::1/64 +! +interface eth1 + ip address 10.0.2.1/24 + ipv6 address a:2::1/64 +! +router ospf + router-id 10.0.1.1 + network 10.0.1.0/24 area 0 + network 10.0.2.0/24 area 0 +! +router ospf6 + router-id 10.0.1.1 + interface eth0 area 0.0.0.0 + interface eth1 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospfd + + + killall ospfd + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 @@ -550,7 +721,7 @@ bootquagga - + /usr/local/etc/quagga /var/run/quagga @@ -566,22 +737,20 @@ bootquagga interface eth0 - ip address 10.0.1.1/24 - ipv6 address a:1::1/64 -! -interface eth1 - ip address 10.0.2.1/24 - ipv6 address a:2::1/64 -! -router ospf - router-id 10.0.1.1 - network 10.0.1.0/24 area 0 - network 10.0.2.0/24 area 0 + ip address 10.0.0.9/32 + ipv6 address a::9/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa ! router ospf6 - router-id 10.0.1.1 + router-id 10.0.0.9 interface eth0 area 0.0.0.0 - interface eth1 area 0.0.0.0 ! #!/bin/sh @@ -681,15 +850,7 @@ bootquagga - - - pidof ospfd - - - killall ospfd - - - + pidof ospf6d @@ -697,7 +858,7 @@ bootquagga killall ospf6d - + sh ipforward.sh @@ -715,9 +876,6 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth1.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth1.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth1.rp_filter=0 @@ -876,6 +1034,164 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 + + + + + + /usr/local/etc/quagga + /var/run/quagga + + + sh quaggaboot.sh zebra + + + pidof zebra + + + killall zebra + + + interface eth0 + ip address 10.0.0.7/32 + ipv6 address a::7/128 + ipv6 ospf6 instance-id 65 + ipv6 ospf6 hello-interval 2 + ipv6 ospf6 dead-interval 6 + ipv6 ospf6 retransmit-interval 5 + ipv6 ospf6 network manet-designated-router + ipv6 ospf6 diffhellos + ipv6 ospf6 adjacencyconnectivity uniconnected + ipv6 ospf6 lsafullness mincostlsa +! +router ospf6 + router-id 10.0.0.7 + interface eth0 area 0.0.0.0 +! + + #!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf +QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" +QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" +QUAGGA_STATE_DIR=/var/run/quagga + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \<${r}\>" $QUAGGA_CONF; then + bootdaemon "${r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga + + service integrated-vtysh-config + + + + + + pidof ospf6d + + + killall ospf6d + + + + + sh ipforward.sh + + + #!/bin/sh +# auto-generated by IPForward service (utility.py) +/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 +/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 +/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 +/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 +/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1221,164 +1537,6 @@ bootquagga /sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 /sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 /sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.7/32 - ipv6 address a::7/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.7 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1391,164 +1549,6 @@ bootquagga # auto-generated by DefaultRoute service (utility.py) ip route add default via 10.0.1.1 ip route add default via a:1::1 - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - interface eth0 - ip address 10.0.0.9/32 - ipv6 address a::9/128 - ipv6 ospf6 instance-id 65 - ipv6 ospf6 hello-interval 2 - ipv6 ospf6 dead-interval 6 - ipv6 ospf6 retransmit-interval 5 - ipv6 ospf6 network manet-designated-router - ipv6 ospf6 diffhellos - ipv6 ospf6 adjacencyconnectivity uniconnected - ipv6 ospf6 lsafullness mincostlsa -! -router ospf6 - router-id 10.0.0.9 - interface eth0 area 0.0.0.0 -! - - #!/bin/sh -# auto-generated by zebra service (quagga.py) -QUAGGA_CONF=/usr/local/etc/quagga/Quagga.conf -QUAGGA_SBIN_SEARCH="/usr/local/sbin /usr/sbin /usr/lib/quagga" -QUAGGA_BIN_SEARCH="/usr/local/bin /usr/bin /usr/lib/quagga" -QUAGGA_STATE_DIR=/var/run/quagga - -searchforprog() -{ - prog=$1 - searchpath=$@ - ret= - for p in $searchpath; do - if [ -x $p/$prog ]; then - ret=$p - break - fi - done - echo $ret -} - -confcheck() -{ - CONF_DIR=`dirname $QUAGGA_CONF` - # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga - - service integrated-vtysh-config - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - #!/bin/sh -# auto-generated by IPForward service (utility.py) -/sbin/sysctl -w net.ipv4.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.all.forwarding=1 -/sbin/sysctl -w net.ipv6.conf.default.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.all.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.default.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.all.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.default.rp_filter=0 -/sbin/sysctl -w net.ipv4.conf.eth0.forwarding=1 -/sbin/sysctl -w net.ipv4.conf.eth0.send_redirects=0 -/sbin/sysctl -w net.ipv4.conf.eth0.rp_filter=0 @@ -1842,8 +1842,8 @@ bootquagga - - + + diff --git a/coretk/coretk/dialogs/canvassizeandscale.py b/coretk/coretk/dialogs/canvassizeandscale.py index 5a472104..0a113936 100644 --- a/coretk/coretk/dialogs/canvassizeandscale.py +++ b/coretk/coretk/dialogs/canvassizeandscale.py @@ -21,11 +21,7 @@ class SizeAndScaleDialog(Dialog): self.canvas = self.app.canvas self.validation = app.validation self.section_font = font.Font(weight="bold") - # get current canvas dimensions - plot = self.canvas.find_withtag("rectangle") - x0, y0, x1, y1 = self.canvas.bbox(plot[0]) - width = abs(x0 - x1) - 2 - height = abs(y0 - y1) - 2 + width, height = self.canvas.current_dimensions self.pixel_width = tk.IntVar(value=width) self.pixel_height = tk.IntVar(value=height) location = self.app.core.location @@ -232,7 +228,7 @@ class SizeAndScaleDialog(Dialog): def click_apply(self): width, height = self.pixel_width.get(), self.pixel_height.get() - self.canvas.redraw_canvas(width, height) + self.canvas.redraw_canvas((width, height)) if self.canvas.wallpaper: self.canvas.redraw_wallpaper() location = self.app.core.location diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e79e0f9c..42d2c4de 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -14,19 +14,13 @@ from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape from coretk.nodeutils import NodeUtils -SCROLL_BUFFER = 25 ZOOM_IN = 1.1 ZOOM_OUT = 0.9 class CanvasGraph(tk.Canvas): def __init__(self, master, core, width, height): - super().__init__( - master, - highlightthickness=0, - background="#cccccc", - scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER), - ) + super().__init__(master, highlightthickness=0, background="#cccccc") self.app = master self.core = core self.mode = GraphMode.SELECT @@ -44,8 +38,8 @@ class CanvasGraph(tk.Canvas): self.grid = None self.throughput_draw = Throughput(self, core) self.shape_drawing = False - self.default_width = width - self.default_height = height + self.default_dimensions = (width, height) + self.current_dimensions = self.default_dimensions self.ratio = 1.0 self.offset = (0, 0) self.cursor = (0, 0) @@ -66,17 +60,22 @@ class CanvasGraph(tk.Canvas): self.draw_canvas() self.draw_grid() - def draw_canvas(self): + def draw_canvas(self, dimensions=None): + if self.grid is not None: + self.delete(self.grid) + if not dimensions: + dimensions = self.default_dimensions + self.current_dimensions = dimensions self.grid = self.create_rectangle( 0, 0, - self.default_width, - self.default_height, + *dimensions, outline="#000000", fill="#ffffff", width=1, tags="rectangle", ) + self.configure(scrollregion=self.bbox(tk.ALL)) def reset_and_redraw(self, session): """ @@ -121,6 +120,16 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + def get_actual_coords(self, x, y): + actual_x = (x - self.offset[0]) / self.ratio + actual_y = (y - self.offset[1]) / self.ratio + return actual_x, actual_y + + def get_scaled_coords(self, x, y): + scaled_x = (x * self.ratio) + self.offset[0] + scaled_y = (y * self.ratio) + self.offset[1] + return scaled_x, scaled_y + def draw_grid(self): """ Create grid. @@ -177,7 +186,9 @@ class CanvasGraph(tk.Canvas): # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) - node = CanvasNode(self.master, core_node, image) + x = core_node.position.x + y = core_node.position.y + node = CanvasNode(self.master, x, y, core_node, image) self.nodes[node.id] = node self.core.canvas_nodes[core_node.id] = node @@ -422,8 +433,8 @@ class CanvasGraph(tk.Canvas): if not factor: factor = ZOOM_IN if event.delta > 0 else ZOOM_OUT event.x, event.y = self.canvasx(event.x), self.canvasy(event.y) - self.scale("all", event.x, event.y, factor, factor) - self.configure(scrollregion=self.bbox("all")) + self.scale(tk.ALL, event.x, event.y, factor, factor) + self.configure(scrollregion=self.bbox(tk.ALL)) self.ratio *= float(factor) self.offset = ( self.offset[0] * factor + event.x * (1 - factor), @@ -444,7 +455,10 @@ class CanvasGraph(tk.Canvas): x, y = self.canvas_xy(event) self.cursor = x, y selected = self.get_selected(event) - logging.debug("click press: %s", selected) + logging.debug("click press(%s): %s", self.cursor, selected) + x_check = self.cursor[0] - self.offset[0] + y_check = self.cursor[1] - self.offset[1] + logging.debug("clock press ofset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.mode == GraphMode.EDGE and is_node: x, y = self.coords(selected) @@ -466,6 +480,11 @@ class CanvasGraph(tk.Canvas): node = self.nodes[selected] self.select_object(node.id) self.selected = selected + logging.info( + "selected coords: (%s, %s)", + node.core_node.position.x, + node.core_node.position.y, + ) else: logging.debug("create selection box") if self.mode == GraphMode.SELECT: @@ -557,10 +576,14 @@ class CanvasGraph(tk.Canvas): def add_node(self, x, y): if self.selected is None or self.selected in self.shapes: + actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( - int(x), int(y), self.node_draw.node_type, self.node_draw.model + int(actual_x), + int(actual_y), + self.node_draw.node_type, + self.node_draw.model, ) - node = CanvasNode(self.master, core_node, self.node_draw.image) + node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node self.nodes[node.id] = node return node @@ -652,18 +675,24 @@ class CanvasGraph(tk.Canvas): def resize_to_wallpaper(self): self.delete(self.wallpaper_id) image = ImageTk.PhotoImage(self.wallpaper) - self.redraw_canvas(image.width(), image.height()) + self.redraw_canvas((image.width(), image.height())) self.draw_wallpaper(image) - def redraw_canvas(self, width, height): - """ - redraw grid with new dimension + def redraw_canvas(self, dimensions=None): + logging.info("redrawing canvas to dimensions: %s", dimensions) - :return: nothing - """ - # resize canvas and scrollregion - self.config(scrollregion=(0, 0, width + SCROLL_BUFFER, height + SCROLL_BUFFER)) - self.coords(self.grid, 0, 0, width, height) + # reset scale and move back to original position + logging.info("resetting scaling: %s %s", self.ratio, self.offset) + factor = 1 / self.ratio + self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) + self.move(tk.ALL, -self.offset[0], -self.offset[1]) + + # reset ratio and offset + self.ratio = 1.0 + self.offset = (0, 0) + + # redraw canvas rectangle + self.draw_canvas(dimensions) # redraw gridlines to new canvas size self.delete(tags.GRIDLINE) @@ -672,10 +701,11 @@ class CanvasGraph(tk.Canvas): def redraw_wallpaper(self): if self.adjust_to_dim.get(): + logging.info("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) - logging.info("canvas scale option: %s", option) + logging.info("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -698,7 +728,7 @@ class CanvasGraph(tk.Canvas): def set_wallpaper(self, filename): logging.info("setting wallpaper: %s", filename) - if filename is not None: + if filename: img = Image.open(filename) self.wallpaper = img self.wallpaper_file = filename diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index eadcd0df..a0f406f7 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -20,13 +20,11 @@ NODE_TEXT_OFFSET = 5 class CanvasNode: - def __init__(self, app, core_node, image): + def __init__(self, app, x, y, core_node, image): self.app = app self.canvas = app.canvas self.image = image self.core_node = core_node - x = self.core_node.position.x - y = self.core_node.position.y self.id = self.canvas.create_image( x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE ) @@ -98,8 +96,10 @@ class CanvasNode: return image_box[3] + NODE_TEXT_OFFSET def move(self, x, y): - x_offset = x - self.core_node.position.x - y_offset = y - self.core_node.position.y + x, y = self.canvas.get_scaled_coords(x, y) + current_x, current_y = self.canvas.coords(self.id) + x_offset = x - current_x + y_offset = y - current_y self.motion(x_offset, y_offset, update=False) def motion(self, x_offset, y_offset, update=True): @@ -107,8 +107,6 @@ class CanvasNode: self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) # move antennae for antenna_id in self.antennae: @@ -131,7 +129,10 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) - # update core with new location + # set actual coords for node and update core is running + real_x, real_y = self.canvas.get_actual_coords(x, y) + self.core_node.position.x = int(real_x) + self.core_node.position.y = int(real_y) if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 80621107..99a18a9f 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -147,6 +147,16 @@ class Shape: def metadata(self): coords = self.canvas.coords(self.id) + # update coords to actual positions + if len(coords) == 4: + x1, y1, x2, y2 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + x2, y2 = self.canvas.get_actual_coords(x2, y2) + coords = (x1, y1, x2, y2) + else: + x1, y1 = coords + x1, y1 = self.canvas.get_actual_coords(x1, y1) + coords = (x1, y1) return { "type": self.shape_type.value, "iconcoords": coords, diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 3115cd58..616e9bc1 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -10,7 +10,7 @@ from tkinter import filedialog, messagebox import grpc -from coretk.appconfig import XML_PATH +from coretk.appconfig import XMLS_PATH from coretk.dialogs.about import AboutDialog from coretk.dialogs.canvassizeandscale import SizeAndScaleDialog from coretk.dialogs.canvaswallpaper import CanvasBackgroundDialog @@ -82,7 +82,7 @@ class MenuAction: def file_save_as_xml(self, event=None): logging.info("menuaction.py file_save_as_xml()") file_path = filedialog.asksaveasfilename( - initialdir=str(XML_PATH), + initialdir=str(XMLS_PATH), title="Save As", filetypes=(("EmulationScript XML files", "*.xml"), ("All files", "*")), defaultextension=".xml", @@ -93,7 +93,7 @@ class MenuAction: def file_open_xml(self, event=None): logging.info("menuaction.py file_open_xml()") file_path = filedialog.askopenfilename( - initialdir=str(XML_PATH), + initialdir=str(XMLS_PATH), title="Open", filetypes=(("XML Files", "*.xml"), ("All Files", "*")), ) diff --git a/coretk/coretk/menubar.py b/coretk/coretk/menubar.py index 2521bd97..460ce2bf 100644 --- a/coretk/coretk/menubar.py +++ b/coretk/coretk/menubar.py @@ -46,7 +46,12 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) - menu.add_command(label="New Session", accelerator="Ctrl+N", state=tk.DISABLED) + menu.add_command( + label="New Session", + accelerator="Ctrl+N", + command=self.app.core.create_new_session, + ) + self.app.bind_all("", lambda e: self.app.core.create_new_session()) menu.add_command( label="Open...", command=self.menuaction.file_open_xml, accelerator="Ctrl+O" ) @@ -104,10 +109,6 @@ class Menubar(tk.Menu): :return: nothing """ menu = tk.Menu(self) - menu.add_command(label="New", state=tk.DISABLED) - menu.add_command(label="Manage...", state=tk.DISABLED) - menu.add_command(label="Delete", state=tk.DISABLED) - menu.add_separator() menu.add_command( label="Size/scale...", command=self.menuaction.canvas_size_and_scale ) @@ -115,6 +116,10 @@ class Menubar(tk.Menu): label="Wallpaper...", command=self.menuaction.canvas_set_wallpaper ) menu.add_separator() + menu.add_command(label="New", state=tk.DISABLED) + menu.add_command(label="Manage...", state=tk.DISABLED) + menu.add_command(label="Delete", state=tk.DISABLED) + menu.add_separator() menu.add_command(label="Previous", accelerator="PgUp", state=tk.DISABLED) menu.add_command(label="Next", accelerator="PgDown", state=tk.DISABLED) menu.add_command(label="First", accelerator="Home", state=tk.DISABLED) From 136121ca18da21d4016a02900405036aeaf5159f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:34:50 -0800 Subject: [PATCH 255/305] fixed issue saving wallpaper metadata when None --- coretk/coretk/coreclient.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 57224a93..1cf99526 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -479,8 +479,11 @@ class CoreClient: def set_metadata(self): # create canvas data + wallpaper = None + if self.app.canvas.wallpaper_file: + wallpaper = Path(self.app.canvas.wallpaper_file).name canvas_config = { - "wallpaper": Path(self.app.canvas.wallpaper_file).name, + "wallpaper": wallpaper, "wallpaper-style": self.app.canvas.scale_option.get(), "gridlines": self.app.canvas.show_grid.get(), "fit_image": self.app.canvas.adjust_to_dim.get(), From f154733e2ec0d9fb7a68e0b2d9617733ce8c732c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 12:49:29 -0800 Subject: [PATCH 256/305] fixed toolbar updating with the smaller icons from picked nodes/annotations --- coretk/coretk/toolbar.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 72288b71..1a0cac0b 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -14,11 +14,11 @@ from coretk.nodeutils import NodeUtils from coretk.themes import Styles from coretk.tooltip import Tooltip -WIDTH = 32 +TOOLBAR_SIZE = 32 PICKER_SIZE = 24 -def icon(image_enum, width=WIDTH): +def icon(image_enum, width=TOOLBAR_SIZE): return Images.get(image_enum, width) @@ -146,17 +146,23 @@ class Toolbar(ttk.Frame): self.node_picker = ttk.Frame(self.master) # draw default nodes for node_draw in NodeUtils.NODES: + toolbar_image = icon(node_draw.image_enum) image = icon(node_draw.image_enum, PICKER_SIZE) - func = partial(self.update_button, self.node_button, image, node_draw) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) self.create_picker_button(image, func, self.node_picker, node_draw.label) # draw custom nodes for name in sorted(self.app.core.custom_nodes): node_draw = self.app.core.custom_nodes[name] - image = Images.get_custom(node_draw.image_file, WIDTH) - func = partial(self.update_button, self.node_button, image, node_draw) + toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE) + image = Images.get_custom(node_draw.image_file, PICKER_SIZE) + func = partial( + self.update_button, self.node_button, toolbar_image, node_draw + ) self.create_picker_button(image, func, self.node_picker, name) # draw edit node - image = icon(ImageEnum.EDITNODE) + image = icon(ImageEnum.EDITNODE, PICKER_SIZE) self.create_picker_button( image, self.click_edit_node, self.node_picker, "Custom" ) @@ -276,10 +282,13 @@ class Toolbar(ttk.Frame): self.hide_pickers() self.network_picker = ttk.Frame(self.master) for node_draw in NodeUtils.NETWORK_NODES: + toolbar_image = icon(node_draw.image_enum) image = icon(node_draw.image_enum, PICKER_SIZE) self.create_picker_button( image, - partial(self.update_button, self.network_button, image, node_draw), + partial( + self.update_button, self.network_button, toolbar_image, node_draw + ), self.network_picker, node_draw.label, ) @@ -318,10 +327,11 @@ class Toolbar(ttk.Frame): (ImageEnum.TEXT, ShapeType.TEXT), ] for image_enum, shape_type in nodes: + toolbar_image = icon(image_enum) image = icon(image_enum, PICKER_SIZE) self.create_picker_button( image, - partial(self.update_annotation, image, shape_type), + partial(self.update_annotation, toolbar_image, shape_type), self.annotation_picker, shape_type.value, ) From 2344e026ff21e95d6e9cdee2dcbcc545a79d9421 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 16:17:33 -0800 Subject: [PATCH 257/305] check emulation light, fix one line of backend code --- coretk/coretk/coreclient.py | 9 +++- coretk/coretk/dialogs/cel.py | 78 +++++++++++++++++++++++++++++++--- coretk/coretk/statusbar.py | 1 + coretk/coretk/toolbar.py | 1 + daemon/core/api/grpc/server.py | 2 +- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 57224a93..fbc6fa8e 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -149,8 +149,8 @@ class CoreClient: self.handle_node_event(event.node_event) elif event.HasField("config_event"): logging.info("config event: %s", event) - elif event.HasField("throughput_event"): - logging.info("throughput event: %s", event) + elif event.HasField("exception_event"): + self.handle_exception_event(event.exception_event) else: logging.info("unhandled event: %s", event) @@ -182,6 +182,11 @@ class CoreClient: event.interface_throughputs ) + def handle_exception_event(self, event): + print(event) + print(event.node_id) + self.app.statusbar.core_alarms.append(event) + def join_session(self, session_id, query_location=True): # update session and title self.session_id = session_id diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index 7f95fc79..fb821e04 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -4,6 +4,9 @@ check engine light import tkinter as tk from tkinter import ttk +from grpc import RpcError + +from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY @@ -36,17 +39,43 @@ class CheckLight(Dialog): frame.columnconfigure(0, weight=1) frame.grid(row=row, column=0, sticky="nsew") self.tree = ttk.Treeview( - frame, columns=("time", "level", "node", "source"), show="headings" + frame, + columns=("time", "level", "session_id", "node", "source"), + show="headings", ) self.tree.grid(row=0, column=0, sticky="nsew") self.tree.column("time", stretch=tk.YES) - self.tree.heading("time", text="time") + self.tree.heading("time", text="time", anchor="w") self.tree.column("level", stretch=tk.YES) - self.tree.heading("level", text="level") + self.tree.heading("level", text="level", anchor="w") + self.tree.column("session_id", stretch=tk.YES) + self.tree.heading("session_id", text="session id", anchor="w") self.tree.column("node", stretch=tk.YES) - self.tree.heading("node", text="node") + self.tree.heading("node", text="node", anchor="w") self.tree.column("source", stretch=tk.YES) - self.tree.heading("source", text="source") + self.tree.heading("source", text="source", anchor="w") + self.tree.bind("<>", self.click_select) + + for alarm in self.app.statusbar.core_alarms: + level = self.get_level(alarm.level) + self.tree.insert( + "", + tk.END, + text=str(alarm.date), + values=( + alarm.date, + level + " (%s)" % alarm.level, + alarm.session_id, + alarm.node_id, + alarm.source, + ), + tags=(level,), + ) + + self.tree.tag_configure("ERROR", background="#ff6666") + self.tree.tag_configure("FATAL", background="#d9d9d9") + self.tree.tag_configure("WARNING", background="#ffff99") + self.tree.tag_configure("NOTICE", background="#85e085") yscrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview) yscrollbar.grid(row=0, column=1, sticky="ns") @@ -75,15 +104,52 @@ class CheckLight(Dialog): button = ttk.Button(frame, text="Close", command=self.destroy) button.grid(row=0, column=3, sticky="nsew", padx=PADX) frame.grid(row=row, column=0, sticky="nsew") - row = row + 1 def reset_cel(self): self.text.delete("1.0", tk.END) + for item in self.tree.get_children(): + self.tree.delete(item) + self.app.statusbar.core_alarms.clear() def daemon_log(self): dialog = DaemonLog(self, self.app) dialog.show() + def get_level(self, level): + if level == core_pb2.ExceptionLevel.ERROR: + return "ERROR" + if level == core_pb2.ExceptionLevel.FATAL: + return "FATAL" + if level == core_pb2.ExceptionLevel.WARNING: + return "WARNING" + if level == core_pb2.ExceptionLevel.NOTICE: + return "NOTICE" + + def click_select(self, event): + current = self.tree.selection() + values = self.tree.item(current)["values"] + time = values[0] + level = values[1] + session_id = values[2] + node_id = values[3] + source = values[4] + text = "DATE: %s\nLEVEL: %s\nNODE: %s (%s)\nSESSION: %s\nSOURCE: %s\n\n" % ( + time, + level, + node_id, + self.app.core.canvas_nodes[node_id].core_node.name, + session_id, + source, + ) + try: + sid = self.app.core.session_id + self.app.core.client.get_node(sid, node_id) + text = text + "node created" + except RpcError: + text = text + "node not created" + self.text.delete("1.0", "end") + self.text.insert("1.0", text) + class DaemonLog(Dialog): def __init__(self, master, app): diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 797d61a8..945b5336 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -19,6 +19,7 @@ class StatusBar(ttk.Frame): self.memory = None self.emulation_light = None self.running = False + self.core_alarms = [] self.draw() def draw(self): diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 72288b71..24c4a74d 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -216,6 +216,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.statusbar.core_alarms.clear() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT thread = threading.Thread(target=self.app.core.start_session) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 6a6747f1..dac09427 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -620,7 +620,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.ExceptionEvent( node_id=event.node, session_id=int(event.session), - level=event.level.value, + level=event.level, source=event.source, date=event.date, text=event.text, From 89f8b421b8da17802875c8e4fb4291b77db98029 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 12 Dec 2019 16:23:27 -0800 Subject: [PATCH 258/305] check emulation light --- coretk/coretk/dialogs/cel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/cel.py index fb821e04..ee836737 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/cel.py @@ -87,6 +87,7 @@ class CheckLight(Dialog): row = row + 1 self.text = CodeText(self) + self.text.config(state=tk.DISABLED) self.text.grid(row=row, column=0, sticky="nsew") row = row + 1 From eca92af588a821e35075560e0c15b827bf2a6f16 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 20:30:28 -0800 Subject: [PATCH 259/305] removed icon dialog, just use file chooser directly --- coretk/coretk/dialogs/customnodes.py | 15 +++--- coretk/coretk/dialogs/icondialog.py | 68 ---------------------------- coretk/coretk/dialogs/nodeconfig.py | 12 ++--- coretk/coretk/widgets.py | 13 ++++++ 4 files changed, 27 insertions(+), 81 deletions(-) delete mode 100644 coretk/coretk/dialogs/icondialog.py diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index d17d8d19..6aabf417 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -3,11 +3,12 @@ import tkinter as tk from pathlib import Path from tkinter import ttk +from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog +from coretk.images import Images from coretk.nodeutils import NodeDraw from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import CheckboxList, ListboxScroll +from coretk.widgets import CheckboxList, ListboxScroll, image_chooser class ServicesSelectDialog(Dialog): @@ -170,11 +171,11 @@ class CustomNodesDialog(Dialog): self.image_button.config(image="") def click_icon(self): - dialog = IconDialog(self, self.app, self.name.get(), self.image) - dialog.show() - if dialog.image: - self.image = dialog.image - self.image_file = dialog.file_path.get() + file_path = image_chooser(self) + if file_path: + image = Images.create(file_path, nodeutils.ICON_SIZE) + self.image = image + self.image_file = file_path self.image_button.config(image=self.image) def click_services(self): diff --git a/coretk/coretk/dialogs/icondialog.py b/coretk/coretk/dialogs/icondialog.py deleted file mode 100644 index ecbb5d54..00000000 --- a/coretk/coretk/dialogs/icondialog.py +++ /dev/null @@ -1,68 +0,0 @@ -import tkinter as tk -from tkinter import filedialog, ttk - -from coretk import nodeutils -from coretk.appconfig import ICONS_PATH -from coretk.dialogs.dialog import Dialog -from coretk.images import Images -from coretk.themes import PADX, PADY - - -class IconDialog(Dialog): - def __init__(self, master, app, name, image): - super().__init__(master, app, f"{name} Icon", modal=True) - self.file_path = tk.StringVar() - self.image_label = None - self.image = image - self.draw() - - def draw(self): - self.top.columnconfigure(0, weight=1) - - # row one - frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=3) - label = ttk.Label(frame, text="Image") - label.grid(row=0, column=0, sticky="ew", padx=PADX) - entry = ttk.Entry(frame, textvariable=self.file_path) - entry.grid(row=0, column=1, sticky="ew", padx=PADX) - button = ttk.Button(frame, text="...", command=self.click_file) - button.grid(row=0, column=2) - - # row two - self.image_label = ttk.Label(self.top, image=self.image, anchor=tk.CENTER) - self.image_label.grid(pady=PADY, sticky="ew") - - # spacer - self.draw_spacer() - - # row three - frame = ttk.Frame(self.top) - frame.grid(sticky="ew") - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - button = ttk.Button(frame, text="Apply", command=self.destroy) - button.grid(row=0, column=0, sticky="ew", padx=PADX) - - button = ttk.Button(frame, text="Cancel", command=self.click_cancel) - button.grid(row=0, column=1, sticky="ew") - - def click_file(self): - file_path = filedialog.askopenfilename( - initialdir=str(ICONS_PATH), - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) - if file_path: - self.image = Images.create(file_path, nodeutils.ICON_SIZE) - self.image_label.config(image=self.image) - self.file_path.set(file_path) - - def click_cancel(self): - self.image = None - self.destroy() diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 56c2a08d..8f4ad8a9 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -3,12 +3,13 @@ import tkinter as tk from functools import partial from tkinter import ttk +from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.icondialog import IconDialog from coretk.dialogs.nodeservice import NodeService +from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import FrameScroll +from coretk.widgets import FrameScroll, image_chooser def mac_auto(is_auto, entry): @@ -196,10 +197,9 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - dialog = IconDialog(self, self.app, self.node.name, self.canvas_node.image) - dialog.show() - if dialog.image: - self.image = dialog.image + file_path = image_chooser(self) + if file_path: + self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) def config_apply(self): diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 0ba16587..349401c5 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { @@ -229,3 +230,15 @@ class Spinbox(ttk.Entry): def set(self, value): self.tk.call(self._w, "set", value) + + +def image_chooser(parent): + return filedialog.askopenfilename( + parent=parent, + initialdir=str(ICONS_PATH), + title="Select Icon", + filetypes=( + ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), + ("All Files", "*"), + ), + ) From fd838613995c7568f90dddf91894c5edc4d2f219 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:05:45 -0800 Subject: [PATCH 260/305] updated cel to alerts dialog, updated layout to support resizing, added styles for alert button to be different colors --- coretk/coretk/dialogs/{cel.py => alerts.py} | 58 +++++++++------------ coretk/coretk/statusbar.py | 20 +++---- coretk/coretk/themes.py | 24 +++++++++ 3 files changed, 57 insertions(+), 45 deletions(-) rename coretk/coretk/dialogs/{cel.py => alerts.py} (77%) diff --git a/coretk/coretk/dialogs/cel.py b/coretk/coretk/dialogs/alerts.py similarity index 77% rename from coretk/coretk/dialogs/cel.py rename to coretk/coretk/dialogs/alerts.py index ee836737..58f65933 100644 --- a/coretk/coretk/dialogs/cel.py +++ b/coretk/coretk/dialogs/alerts.py @@ -8,36 +8,26 @@ from grpc import RpcError from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog -from coretk.images import ImageEnum, Images from coretk.themes import PADX, PADY from coretk.widgets import CodeText -class CheckLight(Dialog): +class AlertsDialog(Dialog): def __init__(self, master, app): - super().__init__(master, app, "CEL", modal=True) + super().__init__(master, app, "Alerts", modal=True) self.app = app self.tree = None self.text = None self.draw() def draw(self): + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) row = 0 - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=1) - image = Images.get(ImageEnum.ALERT, 18) - label = ttk.Label(frame, image=image) - label.image = image - label.grid(row=0, column=0, sticky="e") - label = ttk.Label(frame, text="Check Emulation Light") - label.grid(row=0, column=1, sticky="w") - frame.grid(row=row, column=0, padx=PADX, pady=PADY, sticky="nsew") - row = row + 1 - - frame = ttk.Frame(self) - frame.columnconfigure(0, weight=1) - frame.grid(row=row, column=0, sticky="nsew") + frame.grid(row=row, column=0, sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("time", "level", "session_id", "node", "source"), @@ -86,27 +76,27 @@ class CheckLight(Dialog): self.tree.configure(xscrollcommand=xscrollbar.set) row = row + 1 - self.text = CodeText(self) + self.text = CodeText(self.top) self.text.config(state=tk.DISABLED) - self.text.grid(row=row, column=0, sticky="nsew") + self.text.grid(row=row, column=0, sticky="nsew", pady=PADY) row = row + 1 - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.grid(row=row, column=0, sticky="nsew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) frame.columnconfigure(3, weight=1) - button = ttk.Button(frame, text="Reset CEL", command=self.reset_cel) - button.grid(row=0, column=0, sticky="nsew", padx=PADX) - button = ttk.Button(frame, text="View core-daemon log", command=self.daemon_log) - button.grid(row=0, column=1, sticky="nsew", padx=PADX) - button = ttk.Button(frame, text="View node log") - button.grid(row=0, column=2, sticky="nsew", padx=PADX) + button = ttk.Button(frame, text="Reset", command=self.reset_alerts) + button.grid(row=0, column=0, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Daemon Log", command=self.daemon_log) + button.grid(row=0, column=1, sticky="ew", padx=PADX) + button = ttk.Button(frame, text="Node Log") + button.grid(row=0, column=2, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Close", command=self.destroy) - button.grid(row=0, column=3, sticky="nsew", padx=PADX) - frame.grid(row=row, column=0, sticky="nsew") + button.grid(row=0, column=3, sticky="ew") - def reset_cel(self): + def reset_alerts(self): self.text.delete("1.0", tk.END) for item in self.tree.get_children(): self.tree.delete(item) @@ -160,20 +150,22 @@ class DaemonLog(Dialog): self.draw() def draw(self): - frame = ttk.Frame(self) + self.top.columnconfigure(0, weight=1) + self.top.rowconfigure(1, weight=1) + frame = ttk.Frame(self.top) + frame.grid(row=0, column=0, sticky="ew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=9) label = ttk.Label(frame, text="File: ") label.grid(row=0, column=0) entry = ttk.Entry(frame, textvariable=self.path, state="disabled") - entry.grid(row=0, column=1, sticky="nsew") - frame.grid(row=0, column=0, sticky="nsew") + entry.grid(row=0, column=1, sticky="ew") try: file = open("/var/log/core-daemon.log", "r") log = file.readlines() except FileNotFoundError: log = "Log file not found" - text = CodeText(self) + text = CodeText(self.top) text.insert("1.0", log) text.see("end") text.config(state=tk.DISABLED) diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 945b5336..0528bbf2 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -2,22 +2,21 @@ import tkinter as tk from tkinter import ttk -from coretk.dialogs.cel import CheckLight -from coretk.images import ImageEnum, Images +from coretk.dialogs.alerts import AlertsDialog +from coretk.themes import Styles class StatusBar(ttk.Frame): def __init__(self, master, app, **kwargs): super().__init__(master, **kwargs) self.app = app - self.status = None self.statusvar = tk.StringVar() self.progress_bar = None self.zoom = None self.cpu_usage = None self.memory = None - self.emulation_light = None + self.alerts_button = None self.running = False self.core_alarms = [] self.draw() @@ -56,16 +55,13 @@ class StatusBar(ttk.Frame): ) self.cpu_usage.grid(row=0, column=3, sticky="ew") - image = Images.get(ImageEnum.ALERT, 18) - self.emulation_light = ttk.Button( - self, image=image, text="Alert", compound="left" + self.alerts_button = ttk.Button( + self, text="Alerts", command=self.click_alerts, style=Styles.green_alert ) - self.emulation_light.image = image - self.emulation_light.bind("", self.cel_callback) - self.emulation_light.grid(row=0, column=4, sticky="ew") + self.alerts_button.grid(row=0, column=4, sticky="ew") - def cel_callback(self, event): - dialog = CheckLight(self.app, self.app) + def click_alerts(self): + dialog = AlertsDialog(self.app, self.app) dialog.show() def start_session_callback(self, process_time): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index cb8a8b06..43b59ffd 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -13,6 +13,9 @@ class Styles: tooltip_frame = "Tooltip.TFrame" service_checkbutton = "Service.TCheckbutton" picker_button = "Picker.TButton" + green_alert = "GAlert.TButton" + red_alert = "RAlert.TButton" + yellow_alert = "YAlert.TButton" class Colors: @@ -163,3 +166,24 @@ def update_menu(style, widget): def theme_change(style, event): style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) + style.configure( + Styles.green_alert, + background="green", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.yellow_alert, + background="yellow", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) + style.configure( + Styles.red_alert, + background="red", + padding=0, + relief=tk.NONE, + font=("TkDefaultFont", 8, "normal"), + ) From d5b2edb6ab43b0c0225146d7e1f8746c443de2a5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:10:30 -0800 Subject: [PATCH 261/305] fixed unit tests for broadcasted exceptions to use proper values --- daemon/tests/test_grpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 56523e81..4446a74e 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1086,7 +1086,9 @@ class TestGrpc: with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - session.exception(ExceptionLevels.FATAL, "test", None, "exception message") + session.exception( + ExceptionLevels.FATAL.value, "test", None, "exception message" + ) # then queue.get(timeout=5) From fb63d7e8b36ef3e25b5f33506139cbfae489e70f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 12 Dec 2019 21:33:51 -0800 Subject: [PATCH 262/305] added canvas boundary checks for mouse interactions --- coretk/coretk/graph/graph.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 42d2c4de..e439b842 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -130,6 +130,12 @@ class CanvasGraph(tk.Canvas): scaled_y = (y * self.ratio) + self.offset[1] return scaled_x, scaled_y + def inside_canvas(self, x, y): + x1, y1, x2, y2 = self.bbox(self.grid) + valid_x = x1 <= x <= x2 + valid_y = y1 <= y <= y2 + return valid_x and valid_y + def draw_grid(self): """ Create grid. @@ -269,13 +275,16 @@ class CanvasGraph(tk.Canvas): :return: nothing """ logging.debug("click release") + x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + if self.context: self.context.unpost() self.context = None else: if self.mode == GraphMode.ANNOTATION: self.focus_set() - x, y = self.canvas_xy(event) if self.shape_drawing: shape = self.shapes[self.selected] shape.shape_complete(x, y) @@ -302,7 +311,6 @@ class CanvasGraph(tk.Canvas): 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) elif self.mode == GraphMode.PICKNODE: self.mode = GraphMode.NODE @@ -453,6 +461,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + self.cursor = x, y selected = self.get_selected(event) logging.debug("click press(%s): %s", self.cursor, selected) @@ -495,6 +506,9 @@ class CanvasGraph(tk.Canvas): def ctrl_click(self, event): # update cursor location x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + self.cursor = x, y # handle multiple selections @@ -515,6 +529,9 @@ class CanvasGraph(tk.Canvas): :return: nothing """ x, y = self.canvas_xy(event) + if not self.inside_canvas(x, y): + return + x_offset = x - self.cursor[0] y_offset = y - self.cursor[1] self.cursor = x, y @@ -531,7 +548,7 @@ class CanvasGraph(tk.Canvas): return # move selected objects - if len(self.selection) > 0: + if self.selection: for selected_id in self.selection: if selected_id in self.shapes: shape = self.shapes[selected_id] From d343bd06552aba1b56178dff058586e5fa27da76 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 08:48:40 -0800 Subject: [PATCH 263/305] updated gui to display custom icons if set, updated grpc to send custon icon and image data when present --- coretk/coretk/dialogs/nodeconfig.py | 6 ++++++ coretk/coretk/graph/graph.py | 9 +++++++++ daemon/core/api/grpc/server.py | 5 +++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 8f4ad8a9..c71a1b34 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -46,6 +46,7 @@ class NodeConfigDialog(Dialog): self.canvas_node = canvas_node self.node = canvas_node.core_node self.image = canvas_node.image + self.image_file = None self.image_button = None self.name = tk.StringVar(value=self.node.name) self.type = tk.StringVar(value=self.node.model) @@ -201,6 +202,7 @@ class NodeConfigDialog(Dialog): if file_path: self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) + self.image_file = file_path def config_apply(self): # update core node @@ -211,6 +213,10 @@ class NodeConfigDialog(Dialog): if NodeUtils.is_container_node(self.node.type) and server != "localhost": self.node.server = server + # set custom icon + if self.image_file: + self.node.icon = self.image_file + # update canvas node self.canvas_node.image = self.image diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e439b842..baa18170 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -4,6 +4,7 @@ import tkinter as tk from PIL import Image, ImageTk from core.api.grpc import core_pb2 +from coretk import nodeutils from coretk.dialogs.shapemod import ShapeDialog from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge @@ -12,6 +13,7 @@ from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape +from coretk.images import Images from coretk.nodeutils import NodeUtils ZOOM_IN = 1.1 @@ -186,12 +188,19 @@ class CanvasGraph(tk.Canvas): """ # draw existing nodes for core_node in session.nodes: + logging.info("drawing core node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas image = NodeUtils.node_icon(core_node.type, core_node.model) + if core_node.icon: + try: + image = Images.create(core_node.icon, nodeutils.ICON_SIZE) + except OSError: + logging.error("invalid icon: %s", core_node.icon) + x = core_node.position.x y = core_node.position.y node = CanvasNode(self.master, x, y, core_node, image) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index dac09427..fd1a73c3 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -393,15 +393,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): position = core_pb2.Position( x=node.position.x, y=node.position.y, z=node.position.z ) - services = getattr(node, "services", []) if services is None: services = [] services = [x.name for x in services] - emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name + image = getattr(node, "image", None) node_proto = core_pb2.Node( id=node.id, @@ -411,6 +410,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): type=node_type.value, position=position, services=services, + icon=node.icon, + image=image, ) if isinstance(node, (DockerNode, LxcNode)): node_proto.image = node.image From 9c302e8bc672c62c752da787bb2f0ddf42d812a8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 09:28:51 -0800 Subject: [PATCH 264/305] added some basic boundary checking for moving nodes and shapes --- coretk/coretk/graph/graph.py | 12 ++++++++++++ coretk/coretk/graph/node.py | 11 ++++++++++- coretk/coretk/graph/shape.py | 6 ++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index baa18170..42dbc934 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -138,6 +138,11 @@ class CanvasGraph(tk.Canvas): valid_y = y1 <= y <= y2 return valid_x and valid_y + def valid_position(self, x1, y1, x2, y2): + valid_topleft = self.inside_canvas(x1, y1) + valid_bottomright = self.inside_canvas(x2, y2) + return valid_topleft and valid_bottomright + def draw_grid(self): """ Create grid. @@ -539,6 +544,13 @@ class CanvasGraph(tk.Canvas): """ x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): + if self.select_box: + self.select_box.delete() + self.select_box = None + if is_draw_shape(self.annotation_type) and self.shape_drawing: + shape = self.shapes.pop(self.selected) + shape.delete() + self.shape_drawing = False return x_offset = x - self.cursor[0] diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index a0f406f7..11c7eaf5 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -103,10 +103,19 @@ class CanvasNode: self.motion(x_offset, y_offset, update=False) def motion(self, x_offset, y_offset, update=True): + original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) + x, y = self.canvas.coords(self.id) + + # check new position + bbox = self.canvas.bbox(self.id) + if not self.canvas.valid_position(*bbox): + self.canvas.coords(self.id, original_position) + return + + # move test and selection box self.canvas.move(self.text_id, x_offset, y_offset) self.canvas.move_selection(self.id, x_offset, y_offset) - x, y = self.canvas.coords(self.id) # move antennae for antenna_id in self.antennae: diff --git a/coretk/coretk/graph/shape.py b/coretk/coretk/graph/shape.py index 99a18a9f..e7277b49 100644 --- a/coretk/coretk/graph/shape.py +++ b/coretk/coretk/graph/shape.py @@ -136,7 +136,13 @@ class Shape: self.canvas.delete(self.id) def motion(self, x_offset, y_offset): + original_position = self.canvas.coords(self.id) self.canvas.move(self.id, x_offset, y_offset) + coords = self.canvas.coords(self.id) + if not self.canvas.valid_position(*coords): + self.canvas.coords(self.id, original_position) + return + self.canvas.move_selection(self.id, x_offset, y_offset) if self.text_id is not None: self.canvas.move(self.text_id, x_offset, y_offset) From 819954a695d1a6e0b2cb721c68ca83b1761543dc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:47:23 -0800 Subject: [PATCH 265/305] updated grpc node positions to use floats, avoids needing to deal with int conversions --- coretk/coretk/coreclient.py | 3 +-- coretk/coretk/graph/graph.py | 5 +---- coretk/coretk/graph/node.py | 9 ++------- daemon/core/api/grpc/server.py | 8 +------- daemon/core/xml/corexml.py | 14 ++++---------- daemon/proto/core/api/grpc/core.proto | 6 +++--- 6 files changed, 12 insertions(+), 33 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index ec6ca772..a5f407b7 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -183,8 +183,7 @@ class CoreClient: ) def handle_exception_event(self, event): - print(event) - print(event.node_id) + logging.info("exception event: %s", event) self.app.statusbar.core_alarms.append(event) def join_session(self, session_id, query_location=True): diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 42dbc934..a2d82dc4 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -616,10 +616,7 @@ class CanvasGraph(tk.Canvas): if self.selected is None or self.selected in self.shapes: actual_x, actual_y = self.get_actual_coords(x, y) core_node = self.core.create_node( - int(actual_x), - int(actual_y), - self.node_draw.node_type, - self.node_draw.model, + actual_x, actual_y, self.node_draw.node_type, self.node_draw.model ) node = CanvasNode(self.master, x, y, core_node, self.node_draw.image) self.core.canvas_nodes[core_node.id] = node diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 11c7eaf5..0d0f8706 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -140,8 +140,8 @@ class CanvasNode: # set actual coords for node and update core is running real_x, real_y = self.canvas.get_actual_coords(x, y) - self.core_node.position.x = int(real_x) - self.core_node.position.y = int(real_y) + self.core_node.position.x = real_x + self.core_node.position.y = real_y if self.app.core.is_runtime() and update: self.app.core.edit_node(self.core_node) @@ -164,11 +164,6 @@ class CanvasNode: else: self.show_config() - def update_coords(self): - x, y = self.canvas.coords(self.id) - self.core_node.position.x = int(x) - self.core_node.position.y = int(y) - def create_context(self): is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index fd1a73c3..4ee32751 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -489,13 +489,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: node event that contains node id, name, model, position, and services :rtype: core.api.grpc.core_pb2.NodeEvent """ - x = None - if event.x_position is not None: - x = int(event.x_position) - y = None - if event.y_position is not None: - y = int(event.y_position) - position = core_pb2.Position(x=x, y=y) + position = core_pb2.Position(x=event.x_position, y=event.y_position) services = event.services or "" services = services.split("|") node_proto = core_pb2.Node( diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 9ba63395..285b7a3b 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -117,14 +117,8 @@ class NodeElement: def add_position(self): x = self.node.position.x - if x is not None: - x = int(x) y = self.node.position.y - if y is not None: - y = int(y) z = self.node.position.z - if z is not None: - z = int(z) lat, lon, alt = None, None, None if x is not None and y is not None: lat, lon, alt = self.session.location.getgeo(x, y, z) @@ -751,8 +745,8 @@ class CoreXmlReader: position_element = device_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) @@ -773,8 +767,8 @@ class CoreXmlReader: position_element = network_element.find("position") if position_element is not None: - x = get_int(position_element, "x") - y = get_int(position_element, "y") + x = get_float(position_element, "x") + y = get_float(position_element, "y") if all([x, y]): options.set_position(x, y) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 57bbf3f4..253102bf 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -949,9 +949,9 @@ message SessionLocation { } message Position { - int32 x = 1; - int32 y = 2; - int32 z = 3; + float x = 1; + float y = 2; + float z = 3; float lat = 4; float lon = 5; float alt = 6; From 358985d1292f08a016eedbcf439f4402d8a3c1d3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:54:42 -0800 Subject: [PATCH 266/305] update to avoid not reusing session ids --- daemon/core/emulator/coreemu.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index cdba4e44..158dc296 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -5,7 +5,6 @@ import signal import sys import core.services -from core.emulator.emudata import IdGen from core.emulator.session import Session from core.services.coreservices import ServiceManager @@ -49,7 +48,6 @@ class CoreEmu: self.config = config # session management - self.session_id_gen = IdGen() self.sessions = {} # load services @@ -79,7 +77,6 @@ class CoreEmu: :return: nothing """ logging.info("shutting down all sessions") - self.session_id_gen.id = 0 sessions = self.sessions.copy() self.sessions.clear() for _id in sessions: @@ -96,11 +93,9 @@ class CoreEmu: :rtype: EmuSession """ if not _id: - while True: - _id = self.session_id_gen.next() - if _id not in self.sessions: - break - + _id = 1 + while _id in self.sessions: + _id += 1 session = _cls(_id, config=self.config) logging.info("created session: %s", _id) self.sessions[_id] = session From b993fadedb91f0439decfd889cd881803d52d909 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:24:35 -0800 Subject: [PATCH 267/305] removed grpc check for getting a node service file, it will return the default value when not currently set --- daemon/core/api/grpc/server.py | 7 ------- daemon/core/services/coreservices.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 4ee32751..27b069f0 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1158,13 +1158,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get node service file: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context) - service = None - for current_service in node.services: - if current_service.name == request.service: - service = current_service - break - if not service: - context.abort(grpc.StatusCode.NOT_FOUND, "service not found") file_data = session.services.get_service_file( node, request.service, request.file ) diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index 361061be..b51eb715 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -396,7 +396,7 @@ class CoreServices: """ Add services to a node. - :param core.coreobj.PyCoreNode node: node to add services to + :param core.nodes.base.CoreNode node: node to add services to :param str node_type: node type to add services to :param list[str] services: names of services to add to node :return: nothing From 9b16f272b86f73c39ab20e60c6ea527caeae79cc Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 11:48:36 -0800 Subject: [PATCH 268/305] added get wlan configs, made use of it in coretk, updated node context to allow wlan config during runtime --- coretk/coretk/coreclient.py | 12 +++++++----- coretk/coretk/graph/node.py | 2 ++ daemon/core/api/grpc/client.py | 12 ++++++++++++ daemon/core/api/grpc/server.py | 25 +++++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 10 ++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index a5f407b7..f9ac0384 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -239,15 +239,17 @@ class CoreClient: node_id, config.model, config.config, interface ) + # get wlan configurations + response = self.client.get_wlan_configs(self.session_id) + for _id in response.configs: + mapped_config = response.configs[_id] + self.wlan_configs[_id] = mapped_config.config + # save and retrieve data, needed for session nodes for node in session.nodes: # get node service config and file config - # get wlan configs for wlan nodes - if node.type == core_pb2.NodeType.WIRELESS_LAN: - response = self.client.get_wlan_config(self.session_id, node.id) - self.wlan_configs[node.id] = response.config # retrieve service configurations data for default nodes - elif node.type == core_pb2.NodeType.DEFAULT: + if node.type == core_pb2.NodeType.DEFAULT: for service in node.services: response = self.client.get_node_service( self.session_id, node.id, service diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 0d0f8706..93b6c390 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -173,6 +173,8 @@ class CanvasNode: context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): context.add_command(label="Services", state=tk.DISABLED) + if is_wlan: + context.add_command(label="WLAN Config", command=self.show_wlan_config) if is_wlan and self.core_node.id in self.app.core.mobility_players: context.add_command( label="Mobility Player", command=self.show_mobility_player diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 05380b79..7887d5e8 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -827,6 +827,18 @@ class CoreGrpcClient: ) return self.stub.ServiceAction(request) + def get_wlan_configs(self, session_id): + """ + Get all wlan configurations. + + :param int session_id: session id + :return: response with a dict of node ids to wlan configurations + :rtype: core_pb2.GetWlanConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetWlanConfigsRequest(session_id=session_id) + return self.stub.GetWlanConfigs(request) + def get_wlan_config(self, session_id, node_id): """ Get wlan configuration for a node. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 27b069f0..9b8c51c7 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1236,6 +1236,31 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.ServiceActionResponse(result=result) + def GetWlanConfigs(self, request, context): + """ + Retrieve all wireless-lan configurations. + + :param core.api.grpc.core_pb2.GetWlanConfigsRequest request: request + :param context: core.api.grpc.core_pb2.GetWlanConfigResponse + :return: all wlan configurations + :rtype: core.api.grpc.core_pb2.GetWlanConfigsResponse + """ + logging.debug("get wlan configs: %s", request) + session = self.get_session(request.session_id, context) + response = core_pb2.GetWlanConfigsResponse() + for node_id in session.mobility.node_configurations: + model_config = session.mobility.node_configurations[node_id] + if node_id == -1: + continue + for model_name in model_config: + if model_name != BasicRangeModel.name: + continue + current_config = session.mobility.get_model_config(node_id, model_name) + config = get_config_options(current_config, BasicRangeModel) + mapped_config = core_pb2.MappedConfig(config=config) + response.configs[node_id].CopyFrom(mapped_config) + return response + def GetWlanConfig(self, request, context): """ Retrieve wireless-lan configuration of a node diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 253102bf..609316f8 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -101,6 +101,8 @@ service CoreApi { } // wlan rpc + rpc GetWlanConfigs (GetWlanConfigsRequest) returns (GetWlanConfigsResponse) { + } rpc GetWlanConfig (GetWlanConfigRequest) returns (GetWlanConfigResponse) { } rpc SetWlanConfig (SetWlanConfigRequest) returns (SetWlanConfigResponse) { @@ -585,6 +587,14 @@ message ServiceActionResponse { bool result = 1; } +message GetWlanConfigsRequest { + int32 session_id = 1; +} + +message GetWlanConfigsResponse { + map configs = 1; +} + message GetWlanConfigRequest { int32 session_id = 1; int32 node_id = 2; From 9ada94107e1db5cbe273dec43ba3d3a7c1ff0d7c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:03:41 -0800 Subject: [PATCH 269/305] changes to grpc get emane model configs to return the interface value and actual node id, instead of coded value that would need to be parsed --- coretk/coretk/coreclient.py | 10 ++++------ daemon/core/api/grpc/grpcutils.py | 16 ++++++++++++++++ daemon/core/api/grpc/server.py | 11 ++++++----- daemon/proto/core/api/grpc/core.proto | 1 + 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index f9ac0384..86451338 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -228,13 +228,11 @@ class CoreClient: # get emane model config response = self.client.get_emane_model_configs(self.session_id) - for _id in response.configs: - config = response.configs[_id] + for node_id in response.configs: + config = response.configs[node_id] interface = None - node_id = _id - if _id >= 1000: - interface = _id % 1000 - node_id = int(_id / 1000) + if config.interface != -1: + interface = config.interface self.set_emane_model_config( node_id, config.model, config.config, interface ) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index cf1250c8..7df86a18 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -221,6 +221,22 @@ def get_emane_model_id(node_id, interface_id): return node_id +def parse_emane_model_id(_id): + """ + Parses EMANE model id to get true node id and interface id. + + :param _id: id to parse + :return: node id and interface id + :rtype: tuple + """ + interface = -1 + node_id = _id + if _id >= 1000: + interface = _id % 1000 + node_id = int(_id / 1000) + return node_id, interface + + def convert_link(session, link_data): """ Convert link_data into core protobuf Link diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 9b8c51c7..0b566333 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1395,17 +1395,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) response = core_pb2.GetEmaneModelConfigsResponse() - for node_id in session.emane.node_configurations: - model_config = session.emane.node_configurations[node_id] - if node_id == -1: + for _id in session.emane.node_configurations: + if _id == -1: continue + model_config = session.emane.node_configurations[_id] for model_name in model_config: model = session.emane.models[model_name] - current_config = session.emane.get_model_config(node_id, model_name) + current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) + node_id, interface = grpcutils.parse_emane_model_id(_id) model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( - model=model_name, config=config + model=model_name, config=config, interface=interface ) response.configs[node_id].CopyFrom(model_config) return response diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 609316f8..fb7aaadc 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -666,6 +666,7 @@ message GetEmaneModelConfigsResponse { message ModelConfig { string model = 1; map config = 2; + int32 interface = 3; } map configs = 1; } From 9d988a4b138f740c36f3671496f1c2325ea1af75 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:18:13 -0800 Subject: [PATCH 270/305] fixed issue in grpc get emane model configs that would allow key collision --- coretk/coretk/coreclient.py | 5 ++--- daemon/core/api/grpc/server.py | 16 ++++++++++------ daemon/proto/core/api/grpc/core.proto | 7 ++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 86451338..bff47501 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -228,13 +228,12 @@ class CoreClient: # get emane model config response = self.client.get_emane_model_configs(self.session_id) - for node_id in response.configs: - config = response.configs[node_id] + for config in response.configs: interface = None if config.interface != -1: interface = config.interface self.set_emane_model_config( - node_id, config.model, config.config, interface + config.node_id, config.model, config.config, interface ) # get wlan configurations diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 0b566333..e552e1a4 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1394,22 +1394,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane model configs: %s", request) session = self.get_session(request.session_id, context) - response = core_pb2.GetEmaneModelConfigsResponse() + + configs = [] for _id in session.emane.node_configurations: if _id == -1: continue - model_config = session.emane.node_configurations[_id] - for model_name in model_config: + model_configs = session.emane.node_configurations[_id] + for model_name in model_configs: model = session.emane.models[model_name] current_config = session.emane.get_model_config(_id, model_name) config = get_config_options(current_config, model) node_id, interface = grpcutils.parse_emane_model_id(_id) model_config = core_pb2.GetEmaneModelConfigsResponse.ModelConfig( - model=model_name, config=config, interface=interface + node_id=node_id, + model=model_name, + interface=interface, + config=config, ) - response.configs[node_id].CopyFrom(model_config) - return response + configs.append(model_config) + return core_pb2.GetEmaneModelConfigsResponse(configs=configs) def SaveXml(self, request, context): """ diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index fb7aaadc..5f6a964b 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -664,11 +664,12 @@ message GetEmaneModelConfigsRequest { message GetEmaneModelConfigsResponse { message ModelConfig { - string model = 1; - map config = 2; + int32 node_id = 1; + string model = 2; int32 interface = 3; + map config = 4; } - map configs = 1; + repeated ModelConfig configs = 1; } message SaveXmlRequest { From 47e087b3651d14a3437f4e468a8bca138428d9ad Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 15:28:22 -0800 Subject: [PATCH 271/305] fixed unit tests for grpc get emane model configs --- daemon/tests/test_grpc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 4446a74e..d4df63d7 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -708,10 +708,11 @@ class TestGrpc: # then assert len(response.configs) == 1 - assert emane_network.id in response.configs - model_config = response.configs[emane_network.id] + model_config = response.configs[0] + assert emane_network.id == model_config.node_id assert model_config.model == EmaneIeee80211abgModel.name assert len(model_config.config) > 0 + assert model_config.interface == -1 def test_set_emane_model_config(self, grpc_server): # given From 2afbf63e4b76b02c1b538fbc937d8e2ccba1f2ef Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Fri, 13 Dec 2019 15:52:48 -0800 Subject: [PATCH 272/305] start working on color picker --- coretk/coretk/dialogs/colorpicker.py | 150 +++++++++++++++++++++++++++ coretk/coretk/validation.py | 32 +++++- 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/dialogs/colorpicker.py diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py new file mode 100644 index 00000000..ded652a0 --- /dev/null +++ b/coretk/coretk/dialogs/colorpicker.py @@ -0,0 +1,150 @@ +""" +custom color picker +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class ColorPicker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "color picker", modal=True) + self.red_entry = None + self.blue_entry = None + self.green_entry = None + self.hex_entry = None + self.display = None + + self.red = tk.StringVar(value=0) + self.blue = tk.StringVar(value=0) + self.green = tk.StringVar(value=0) + self.hex = tk.StringVar(value=initcolor) + + self.draw() + self.set_bindings() + + def draw(self): + edit_frame = ttk.Frame(self) + edit_frame.columnconfigure(0, weight=4) + edit_frame.columnconfigure(1, weight=2) + # the rbg frame + frame = ttk.Frame(edit_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.rowconfigure(0, weight=1) + frame.rowconfigure(1, weight=1) + frame.rowconfigure(2, weight=1) + label = ttk.Label(frame, text="R: ") + label.grid(row=0, column=0) + self.red_entry = ttk.Entry( + frame, + textvariable=self.red, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.red_entry.grid(row=0, column=1, sticky="nsew") + + label = ttk.Label(frame, text="G: ") + label.grid(row=1, column=0) + self.green_entry = ttk.Entry( + frame, + textvariable=self.green, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.green_entry.grid(row=1, column=1, sticky="nsew") + + label = ttk.Label(frame, text="B: ") + label.grid(row=2, column=0) + self.blue_entry = ttk.Entry( + frame, + textvariable=self.blue, + validate="key", + validatecommand=(self.app.validation.rgb, "%P"), + ) + self.blue_entry.grid(row=2, column=1, sticky="nsew") + + frame.grid(row=0, column=0, sticky="nsew") + + # hex code and color display + frame = ttk.Frame(edit_frame) + frame.columnconfigure(0, weight=1) + label = ttk.Label(frame, text="Selection: ") + label.grid(row=0, column=0, sticky="nsew") + self.hex_entry = ttk.Entry( + frame, + textvariable=self.hex, + validate="key", + validatecommand=(self.app.validation.hex, "%P"), + ) + self.hex_entry.grid(row=1, column=0, sticky="nsew") + self.display = ttk.Label(frame, background="white") + self.display.grid(row=2, column=0, sticky="nsew") + frame.grid(row=0, column=1, sticky="nsew") + + edit_frame.grid(row=0, column=0, sticky="nsew") + + # button frame + frame = ttk.Frame(self) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + button = ttk.Button(frame, text="OK", command=self.button_ok) + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") + + def set_bindings(self): + self.red_entry.bind("", lambda x: self.current_focus("rgb")) + self.green_entry.bind("", lambda x: self.current_focus("rgb")) + self.blue_entry.bind("", lambda x: self.current_focus("rgb")) + self.hex_entry.bind("", lambda x: self.current_focus("hex")) + self.red.trace_add("write", self.update_color) + self.green.trace_add("write", self.update_color) + self.blue.trace_add("write", self.update_color) + self.hex.trace_add("write", self.update_color) + + def button_ok(self): + logging.debug("not implemented") + + def get_hex(self): + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + return "#%02x%02x%02x" % (int(red), int(green), int(blue)) + + def current_focus(self, focus): + self.focus = focus + + def update_color(self, arg1, arg2, arg3): + if self.focus == "rgb": + red = self.red_entry.get() + blue = self.blue_entry.get() + green = self.green_entry.get() + if red and blue and green: + hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) + self.hex_entry.delete(0, tk.END) + self.hex_entry.insert(0, hex_code) + self.display.config(background=hex_code) + elif self.focus == "hex": + hex_code = self.hex.get() + if len(hex_code) == 4 or len(hex_code) == 7: + if len(hex_code) == 4: + red = hex_code[1] + green = hex_code[2] + blue = hex_code[3] + else: + red = hex_code[1:3] + green = hex_code[3:5] + blue = hex_code[5:] + else: + return + self.red_entry.delete(0, tk.END) + self.green_entry.delete(0, tk.END) + self.blue_entry.delete(0, tk.END) + self.red_entry.insert(0, "%s" % (int(red, 16))) + self.green_entry.insert(0, "%s" % (int(green, 16))) + self.blue_entry.insert(0, "%s" % (int(blue, 16))) + self.display.config(background=hex_code) diff --git a/coretk/coretk/validation.py b/coretk/coretk/validation.py index c7923229..955a7faf 100644 --- a/coretk/coretk/validation.py +++ b/coretk/coretk/validation.py @@ -15,6 +15,8 @@ class InputValidation: self.positive_float = None self.name = None self.ip4 = None + self.rgb = None + self.hex = None self.register() def register(self): @@ -22,6 +24,8 @@ class InputValidation: self.positive_float = self.master.register(self.check_positive_float) self.name = self.master.register(self.check_node_name) self.ip4 = self.master.register(self.check_ip4) + self.rgb = self.master.register(self.check_rbg) + self.hex = self.master.register(self.check_hex) def ip_focus_out(self, event): value = event.widget.get() @@ -100,7 +104,7 @@ class InputValidation: return False for _8bits in _32bits: if ( - (_8bits and int(_8bits) > 225) + (_8bits and int(_8bits) > 255) or len(_8bits) > 3 or (_8bits.startswith("0") and len(_8bits) > 1) ): @@ -108,3 +112,29 @@ class InputValidation: return True else: return False + + def check_rbg(self, s): + if not s: + return True + if s.startswith("0") and len(s) >= 2: + return False + try: + value = int(s) + if 0 <= value <= 255: + return True + else: + return False + except ValueError: + return False + + def check_hex(self, s): + if not s: + return True + pat = re.compile("^([#]([0-9]|[a-f])+)$|^[#]$") + if pat.match(s): + if 0 <= len(s) <= 7: + return True + else: + return False + else: + return False From 47cc20b5671233ce7990244180034f3e8786636b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 16:11:58 -0800 Subject: [PATCH 273/305] updates to grpc event streaming, client can now listen to a subset of desired events --- daemon/core/api/grpc/client.py | 7 +- daemon/core/api/grpc/events.py | 268 ++++++++++++++++++++++++++ daemon/core/api/grpc/server.py | 217 +-------------------- daemon/proto/core/api/grpc/core.proto | 12 ++ 4 files changed, 293 insertions(+), 211 deletions(-) create mode 100644 daemon/core/api/grpc/events.py diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 7887d5e8..ae193909 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -378,16 +378,17 @@ class CoreGrpcClient: ) return self.stub.AddSessionServer(request) - def events(self, session_id, handler): + def events(self, session_id, handler, events=None): """ Listen for session events. :param int session_id: id of session - :param handler: handler for every event + :param handler: handler for received events + :param list events: events to listen to, defaults to all :return: nothing :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.EventsRequest(session_id=session_id) + request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) start_streamer(stream, handler) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py new file mode 100644 index 00000000..d7a3094e --- /dev/null +++ b/daemon/core/api/grpc/events.py @@ -0,0 +1,268 @@ +import logging +from queue import Empty, Queue + +from core.api.grpc import core_pb2 +from core.api.grpc.grpcutils import convert_value +from core.emulator.data import ( + ConfigData, + EventData, + ExceptionData, + FileData, + LinkData, + NodeData, +) + + +def handle_node_event(event): + """ + Handle node event when there is a node event + + :param core.emulator.data.NodeData event: node data + :return: node event that contains node id, name, model, position, and services + :rtype: core.api.grpc.core_pb2.NodeEvent + """ + position = core_pb2.Position(x=event.x_position, y=event.y_position) + services = event.services or "" + services = services.split("|") + node_proto = core_pb2.Node( + id=event.id, + name=event.name, + model=event.model, + position=position, + services=services, + ) + return core_pb2.NodeEvent(node=node_proto, source=event.source) + + +def handle_link_event(event): + """ + Handle link event when there is a link event + + :param core.emulator.data.LinkData event: link data + :return: link event that has message type and link information + :rtype: core.api.grpc.core_pb2.LinkEvent + """ + interface_one = None + if event.interface1_id is not None: + interface_one = core_pb2.Interface( + id=event.interface1_id, + name=event.interface1_name, + mac=convert_value(event.interface1_mac), + ip4=convert_value(event.interface1_ip4), + ip4mask=event.interface1_ip4_mask, + ip6=convert_value(event.interface1_ip6), + ip6mask=event.interface1_ip6_mask, + ) + + interface_two = None + if event.interface2_id is not None: + interface_two = core_pb2.Interface( + id=event.interface2_id, + name=event.interface2_name, + mac=convert_value(event.interface2_mac), + ip4=convert_value(event.interface2_ip4), + ip4mask=event.interface2_ip4_mask, + ip6=convert_value(event.interface2_ip6), + ip6mask=event.interface2_ip6_mask, + ) + + options = core_pb2.LinkOptions( + opaque=event.opaque, + jitter=event.jitter, + key=event.key, + mburst=event.mburst, + mer=event.mer, + per=event.per, + bandwidth=event.bandwidth, + burst=event.burst, + delay=event.delay, + dup=event.dup, + unidirectional=event.unidirectional, + ) + link = core_pb2.Link( + type=event.link_type, + node_one_id=event.node1_id, + node_two_id=event.node2_id, + interface_one=interface_one, + interface_two=interface_two, + options=options, + ) + return core_pb2.LinkEvent(message_type=event.message_type, link=link) + + +def handle_session_event(event): + """ + Handle session event when there is a session event + + :param core.emulator.data.EventData event: event data + :return: session event + :rtype: core.api.grpc.core_pb2.SessionEvent + """ + event_time = event.time + if event_time is not None: + event_time = float(event_time) + return core_pb2.SessionEvent( + node_id=event.node, + event=event.event_type, + name=event.name, + data=event.data, + time=event_time, + session_id=event.session, + ) + + +def handle_config_event(event): + """ + Handle configuration event when there is configuration event + + :param core.emulator.data.ConfigData event: configuration data + :return: configuration event + :rtype: core.api.grpc.core_pb2.ConfigEvent + """ + session_id = None + if event.session is not None: + session_id = int(event.session) + return core_pb2.ConfigEvent( + message_type=event.message_type, + node_id=event.node, + object=event.object, + type=event.type, + captions=event.captions, + bitmap=event.bitmap, + data_values=event.data_values, + possible_values=event.possible_values, + groups=event.groups, + session_id=session_id, + interface=event.interface_number, + network_id=event.network_id, + opaque=event.opaque, + data_types=event.data_types, + ) + + +def handle_exception_event(event): + """ + Handle exception event when there is exception event + + :param core.emulator.data.ExceptionData event: exception data + :return: exception event + :rtype: core.api.grpc.core_pb2.ExceptionEvent + """ + return core_pb2.ExceptionEvent( + node_id=event.node, + session_id=int(event.session), + level=event.level, + source=event.source, + date=event.date, + text=event.text, + opaque=event.opaque, + ) + + +def handle_file_event(event): + """ + Handle file event + + :param core.emulator.data.FileData event: file data + :return: file event + :rtype: core.api.grpc.core_pb2.FileEvent + """ + return core_pb2.FileEvent( + message_type=event.message_type, + node_id=event.node, + name=event.name, + mode=event.mode, + number=event.number, + type=event.type, + source=event.source, + session_id=event.session, + data=event.data, + compressed_data=event.compressed_data, + ) + + +class EventStreamer: + """ + Processes session events to generate grpc events. + """ + + def __init__(self, session, event_types): + """ + Create a EventStreamer instance. + + :param core.emulator.session.Session session: session to process events for + :param set event_types: types of events to process + """ + self.session = session + self.event_types = event_types + self.queue = Queue() + self.add_handlers() + + def add_handlers(self): + """ + Add a session event handler for desired event types. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.append(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.append(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.append(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.append(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.append(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.append(self.queue.put) + + def process(self): + """ + Process the next event in the queue. + + :return: grpc event, or None when invalid event or queue timeout + :rtype: core.api.grpc.core_pb2.Event + """ + event = core_pb2.Event() + try: + data = self.queue.get(timeout=1) + if isinstance(data, NodeData): + event.node_event.CopyFrom(handle_node_event(data)) + elif isinstance(data, LinkData): + event.link_event.CopyFrom(handle_link_event(data)) + elif isinstance(data, EventData): + event.session_event.CopyFrom(handle_session_event(data)) + elif isinstance(data, ConfigData): + event.config_event.CopyFrom(handle_config_event(data)) + # TODO: remove when config events are fixed + event.config_event.session_id = self.session.id + elif isinstance(data, ExceptionData): + event.exception_event.CopyFrom(handle_exception_event(data)) + elif isinstance(data, FileData): + event.file_event.CopyFrom(handle_file_event(data)) + else: + logging.error("unknown event: %s", data) + event = None + except Empty: + event = None + return event + + def remove_handlers(self): + """ + Remove session event handlers for events being watched. + + :return: nothing + """ + if core_pb2.EventType.NODE in self.event_types: + self.session.node_handlers.remove(self.queue.put) + if core_pb2.EventType.LINK in self.event_types: + self.session.link_handlers.remove(self.queue.put) + if core_pb2.EventType.CONFIG in self.event_types: + self.session.config_handlers.remove(self.queue.put) + if core_pb2.EventType.FILE in self.event_types: + self.session.file_handlers.remove(self.queue.put) + if core_pb2.EventType.EXCEPTION in self.event_types: + self.session.exception_handlers.remove(self.queue.put) + if core_pb2.EventType.SESSION in self.event_types: + self.session.event_handlers.remove(self.queue.put) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e552e1a4..75ce23ba 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -5,27 +5,19 @@ import re import tempfile import time from concurrent import futures -from queue import Empty, Queue import grpc from core.api.grpc import core_pb2, core_pb2_grpc, grpcutils +from core.api.grpc.events import EventStreamer from core.api.grpc.grpcutils import ( - convert_value, get_config_options, get_emane_model_id, get_links, get_net_stats, ) from core.emane.nodes import EmaneNet -from core.emulator.data import ( - ConfigData, - EventData, - ExceptionData, - FileData, - LinkData, - NodeData, -) +from core.emulator.data import LinkData from core.emulator.emudata import LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreCommandError, CoreError @@ -439,210 +431,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): def Events(self, request, context): session = self.get_session(request.session_id, context) - queue = Queue() - session.node_handlers.append(queue.put) - session.link_handlers.append(queue.put) - session.config_handlers.append(queue.put) - session.file_handlers.append(queue.put) - session.exception_handlers.append(queue.put) - session.event_handlers.append(queue.put) + event_types = set(request.events) + if not event_types: + event_types = set(core_pb2.EventType.Enum.values()) + streamer = EventStreamer(session, event_types) while self._is_running(context): - event = core_pb2.Event() - try: - data = queue.get(timeout=1) - if isinstance(data, NodeData): - event.node_event.CopyFrom(self._handle_node_event(data)) - elif isinstance(data, LinkData): - event.link_event.CopyFrom(self._handle_link_event(data)) - elif isinstance(data, EventData): - event.session_event.CopyFrom(self._handle_session_event(data)) - elif isinstance(data, ConfigData): - event.config_event.CopyFrom(self._handle_config_event(data)) - # TODO: remove when config events are fixed - event.config_event.session_id = session.id - elif isinstance(data, ExceptionData): - event.exception_event.CopyFrom(self._handle_exception_event(data)) - elif isinstance(data, FileData): - event.file_event.CopyFrom(self._handle_file_event(data)) - else: - logging.error("unknown event: %s", data) - continue - + event = streamer.process() + if event: yield event - except Empty: - continue - session.node_handlers.remove(queue.put) - session.link_handlers.remove(queue.put) - session.config_handlers.remove(queue.put) - session.file_handlers.remove(queue.put) - session.exception_handlers.remove(queue.put) - session.event_handlers.remove(queue.put) + streamer.remove_handlers() self._cancel_stream(context) - def _handle_node_event(self, event): - """ - Handle node event when there is a node event - - :param core.emulator.data.NodeData event: node data - :return: node event that contains node id, name, model, position, and services - :rtype: core.api.grpc.core_pb2.NodeEvent - """ - position = core_pb2.Position(x=event.x_position, y=event.y_position) - services = event.services or "" - services = services.split("|") - node_proto = core_pb2.Node( - id=event.id, - name=event.name, - model=event.model, - position=position, - services=services, - ) - return core_pb2.NodeEvent(node=node_proto, source=event.source) - - def _handle_link_event(self, event): - """ - Handle link event when there is a link event - - :param core.emulator.data.LinkData event: link data - :return: link event that has message type and link information - :rtype: core.api.grpc.core_pb2.LinkEvent - """ - interface_one = None - if event.interface1_id is not None: - interface_one = core_pb2.Interface( - id=event.interface1_id, - name=event.interface1_name, - mac=convert_value(event.interface1_mac), - ip4=convert_value(event.interface1_ip4), - ip4mask=event.interface1_ip4_mask, - ip6=convert_value(event.interface1_ip6), - ip6mask=event.interface1_ip6_mask, - ) - - interface_two = None - if event.interface2_id is not None: - interface_two = core_pb2.Interface( - id=event.interface2_id, - name=event.interface2_name, - mac=convert_value(event.interface2_mac), - ip4=convert_value(event.interface2_ip4), - ip4mask=event.interface2_ip4_mask, - ip6=convert_value(event.interface2_ip6), - ip6mask=event.interface2_ip6_mask, - ) - - options = core_pb2.LinkOptions( - opaque=event.opaque, - jitter=event.jitter, - key=event.key, - mburst=event.mburst, - mer=event.mer, - per=event.per, - bandwidth=event.bandwidth, - burst=event.burst, - delay=event.delay, - dup=event.dup, - unidirectional=event.unidirectional, - ) - link = core_pb2.Link( - type=event.link_type, - node_one_id=event.node1_id, - node_two_id=event.node2_id, - interface_one=interface_one, - interface_two=interface_two, - options=options, - ) - return core_pb2.LinkEvent(message_type=event.message_type, link=link) - - def _handle_session_event(self, event): - """ - Handle session event when there is a session event - - :param core.emulator.data.EventData event: event data - :return: session event - :rtype: core.api.grpc.core_pb2.SessionEvent - """ - event_time = event.time - if event_time is not None: - event_time = float(event_time) - return core_pb2.SessionEvent( - node_id=event.node, - event=event.event_type, - name=event.name, - data=event.data, - time=event_time, - session_id=event.session, - ) - - def _handle_config_event(self, event): - """ - Handle configuration event when there is configuration event - - :param core.emulator.data.ConfigData event: configuration data - :return: configuration event - :rtype: core.api.grpc.core_pb2.ConfigEvent - """ - session_id = None - if event.session is not None: - session_id = int(event.session) - return core_pb2.ConfigEvent( - message_type=event.message_type, - node_id=event.node, - object=event.object, - type=event.type, - captions=event.captions, - bitmap=event.bitmap, - data_values=event.data_values, - possible_values=event.possible_values, - groups=event.groups, - session_id=session_id, - interface=event.interface_number, - network_id=event.network_id, - opaque=event.opaque, - data_types=event.data_types, - ) - - def _handle_exception_event(self, event): - """ - Handle exception event when there is exception event - - :param core.emulator.data.ExceptionData event: exception data - :return: exception event - :rtype: core.api.grpc.core_pb2.ExceptionEvent - """ - return core_pb2.ExceptionEvent( - node_id=event.node, - session_id=int(event.session), - level=event.level, - source=event.source, - date=event.date, - text=event.text, - opaque=event.opaque, - ) - - def _handle_file_event(self, event): - """ - Handle file event - - :param core.emulator.data.FileData event: file data - :return: file event - :rtype: core.api.grpc.core_pb2.FileEvent - """ - return core_pb2.FileEvent( - message_type=event.message_type, - node_id=event.node, - name=event.name, - mode=event.mode, - number=event.number, - type=event.type, - source=event.source, - session_id=event.session, - data=event.data, - compressed_data=event.compressed_data, - ) - def Throughputs(self, request, context): """ Calculate average throughput after every certain amount of delay time diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 5f6a964b..55ed272e 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -266,6 +266,7 @@ message AddSessionServerResponse { message EventsRequest { int32 session_id = 1; + repeated EventType.Enum events = 2; } message ThroughputsRequest { @@ -742,6 +743,17 @@ message ServiceFileConfig { string data = 4; } +message EventType { + enum Enum { + SESSION = 0; + NODE = 1; + LINK = 2; + CONFIG = 3; + EXCEPTION = 4; + FILE = 5; + } +} + message MessageType { enum Enum { NONE = 0; From 85521e8c8f4e467a0fa73fedf414530539cb8be9 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 13 Dec 2019 18:17:42 -0800 Subject: [PATCH 274/305] added grpc to get current service configurations, fixed bug for core daemon not using custom service configs --- coretk/coretk/coreclient.py | 32 ++++++--------- coretk/coretk/dialogs/serviceconfiguration.py | 2 +- daemon/core/api/grpc/client.py | 12 ++++++ daemon/core/api/grpc/grpcutils.py | 22 +++++++++++ daemon/core/api/grpc/server.py | 39 +++++++++++++------ daemon/core/services/coreservices.py | 7 +++- daemon/proto/core/api/grpc/core.proto | 16 ++++++++ daemon/tests/test_grpc.py | 18 +++++++++ 8 files changed, 112 insertions(+), 36 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index bff47501..776e1750 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -242,27 +242,17 @@ class CoreClient: mapped_config = response.configs[_id] self.wlan_configs[_id] = mapped_config.config - # save and retrieve data, needed for session nodes - for node in session.nodes: - # get node service config and file config - # retrieve service configurations data for default nodes - if node.type == core_pb2.NodeType.DEFAULT: - for service in node.services: - response = self.client.get_node_service( - self.session_id, node.id, service - ) - if node.id not in self.service_configs: - self.service_configs[node.id] = {} - self.service_configs[node.id][service] = response.service - for file in response.service.configs: - response = self.client.get_node_service_file( - self.session_id, node.id, service, file - ) - if node.id not in self.file_configs: - self.file_configs[node.id] = {} - if service not in self.file_configs[node.id]: - self.file_configs[node.id][service] = {} - self.file_configs[node.id][service][file] = response.data + # get service configurations + response = self.client.get_node_service_configs(self.session_id) + for config in response.configs: + service_configs = self.service_configs.setdefault(config.node_id, {}) + service_configs[config.service] = config.data + logging.info("service file configs: %s", config.files) + for file_name in config.files: + file_configs = self.file_configs.setdefault(config.node_id, {}) + files = file_configs.setdefault(config.service, {}) + data = config.files[file_name] + files[file_name] = data # draw session self.app.canvas.reset_and_redraw(session) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 03c74ab2..a53c2aa1 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -323,7 +323,7 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, text="Apply", command=self.click_apply) button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button( - frame, text="Dafults", command=self.click_defaults, state="disabled" + frame, text="Defaults", command=self.click_defaults, state="disabled" ) button.grid(row=0, column=1, sticky="ew", padx=PADX) button = ttk.Button( diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index ae193909..fbafbb44 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -731,6 +731,18 @@ class CoreGrpcClient: ) return self.stub.SetServiceDefaults(request) + def get_node_service_configs(self, session_id): + """ + Get service data for a node. + + :param int session_id: session id + :return: response with all node service configs + :rtype: core_pb2.GetNodeServiceConfigsResponse + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetNodeServiceConfigsRequest(session_id=session_id) + return self.stub.GetNodeServiceConfigs(request) + def get_node_service(self, session_id, node_id, service): """ Get service data for a node. diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7df86a18..4ea752fd 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -352,3 +352,25 @@ def service_configuration(session, config): service.startup = tuple(config.startup) service.validate = tuple(config.validate) service.shutdown = tuple(config.shutdown) + + +def get_service_configuration(service): + """ + Convenience for converting a service to service data proto. + + :param service: service to get proto data for + :return: service proto data + :rtype: core.api.grpc.core_pb2.NodeServiceData + """ + return core_pb2.NodeServiceData( + executables=service.executables, + dependencies=service.dependencies, + dirs=service.dirs, + configs=service.configs, + startup=service.startup, + validate=service.validate, + validation_mode=service.validation_mode.value, + validation_timer=service.validation_timer, + shutdown=service.shutdown, + meta=service.meta, + ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 75ce23ba..1ada5267 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -917,6 +917,32 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ] = service_defaults.services return core_pb2.SetServiceDefaultsResponse(result=True) + def GetNodeServiceConfigs(self, request, context): + """ + Retrieve all node service configurations. + + :param core.api.grpc.core_pb2.GetNodeServiceConfigsRequest request: + get-node-service request + :param grpc.ServicerContext context: context object + :return: all node service configs response + :rtype: core.api.grpc.core_pb2.GetNodeServiceConfigsResponse + """ + logging.debug("get node service configs: %s", request) + session = self.get_session(request.session_id, context) + configs = [] + for node_id, service_configs in session.services.custom_services.items(): + for name in service_configs: + service = session.services.get_service(node_id, name) + service_proto = grpcutils.get_service_configuration(service) + config = core_pb2.GetNodeServiceConfigsResponse.ServiceConfig( + node_id=node_id, + service=name, + data=service_proto, + files=service.config_data, + ) + configs.append(config) + return core_pb2.GetNodeServiceConfigsResponse(configs=configs) + def GetNodeService(self, request, context): """ Retrieve a requested service from a node @@ -932,18 +958,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): service = session.services.get_service( request.node_id, request.service, default_service=True ) - service_proto = core_pb2.NodeServiceData( - executables=service.executables, - dependencies=service.dependencies, - dirs=service.dirs, - configs=service.configs, - startup=service.startup, - validate=service.validate, - validation_mode=service.validation_mode.value, - validation_timer=service.validation_timer, - shutdown=service.shutdown, - meta=service.meta, - ) + service_proto = grpcutils.get_service_configuration(service) return core_pb2.GetNodeServiceResponse(service=service_proto) def GetNodeServiceFile(self, request, context): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index b51eb715..684ccbb9 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -461,8 +461,8 @@ class CoreServices: :param core.netns.vnode.LxcNode node: node to start services on :return: nothing """ - funcs = [] boot_paths = ServiceDependencies(node.services).boot_paths() + funcs = [] for boot_path in boot_paths: args = (node, boot_path) funcs.append((self._start_boot_paths, args, {})) @@ -484,6 +484,7 @@ class CoreServices: " -> ".join([x.name for x in boot_path]), ) for service in boot_path: + service = self.get_service(node.id, service.name, default_service=True) try: self.boot_service(node, service) except Exception: @@ -744,7 +745,9 @@ class CoreServices: config_files = service.get_configs(node) for file_name in config_files: - logging.debug("generating service config: %s", file_name) + logging.debug( + "generating service config custom(%s): %s", service.custom, file_name + ) if service.custom: cfg = service.config_data.get(file_name) if cfg is None: diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 55ed272e..b0a6c0d6 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -89,6 +89,8 @@ service CoreApi { } rpc SetServiceDefaults (SetServiceDefaultsRequest) returns (SetServiceDefaultsResponse) { } + rpc GetNodeServiceConfigs (GetNodeServiceConfigsRequest) returns (GetNodeServiceConfigsResponse) { + } rpc GetNodeService (GetNodeServiceRequest) returns (GetNodeServiceResponse) { } rpc GetNodeServiceFile (GetNodeServiceFileRequest) returns (GetNodeServiceFileResponse) { @@ -538,6 +540,20 @@ message SetServiceDefaultsResponse { bool result = 1; } +message GetNodeServiceConfigsRequest { + int32 session_id = 1; +} + +message GetNodeServiceConfigsResponse { + message ServiceConfig { + int32 node_id = 1; + string service = 2; + NodeServiceData data = 3; + map files = 4; + } + repeated ServiceConfig configs = 1; +} + message GetNodeServiceRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index d4df63d7..796febf7 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -878,6 +878,24 @@ class TestGrpc: assert response.result is True assert session.services.default_services[node_type] == services + def test_get_node_service_configs(self, grpc_server): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node() + service_name = "DefaultRoute" + session.services.set_service(node.id, service_name) + + # then + with client.context_connect(): + response = client.get_node_service_configs(session.id) + + # then + assert len(response.configs) == 1 + service_config = response.configs[0] + assert service_config.node_id == node.id + assert service_config.service == service_name + def test_get_node_service(self, grpc_server): # given client = CoreGrpcClient() From 424f69bb1519f35852d49780b3f73ed02a0145cd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:14:05 -0800 Subject: [PATCH 275/305] updated grpc throughputs to only check a specific session and verify the data being collected and sent is for that session, fixed data from throughputs being in hex getting converted to int, updated coretk to only run throughputs when enabled, updated grpc streams to return the stream to allow it being canceled --- coretk/coretk/coreclient.py | 22 ++++++++++++++++------ coretk/coretk/menuaction.py | 7 +++---- daemon/core/api/grpc/client.py | 12 ++++++++---- daemon/core/api/grpc/server.py | 19 ++++++++++++++----- daemon/proto/core/api/grpc/core.proto | 6 ++++-- daemon/tests/test_grpc.py | 4 ++-- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 776e1750..9f6900cc 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -84,7 +84,7 @@ class CoreClient: self.service_configs = {} self.file_configs = {} self.mobility_players = {} - self.throughput = False + self.handling_throughputs = None def reset(self): # helpers @@ -176,11 +176,22 @@ class CoreClient: canvas_node = self.canvas_nodes[node_id] canvas_node.move(x, y) + def enable_throughputs(self): + self.handling_throughputs = self.client.throughputs( + self.session_id, self.handle_throughputs + ) + + def cancel_throughputs(self): + self.handling_throughputs.cancel() + self.handling_throughputs = None + def handle_throughputs(self, event): - if self.throughput: - self.app.canvas.throughput_draw.process_grpc_throughput_event( - event.interface_throughputs - ) + if event.session_id != self.session_id: + return + logging.info("handling throughputs event: %s", event) + self.app.canvas.throughput_draw.process_grpc_throughput_event( + event.interface_throughputs + ) def handle_exception_event(self, event): logging.info("exception event: %s", event) @@ -200,7 +211,6 @@ class CoreClient: session = response.session self.state = session.state self.client.events(self.session_id, self.handle_events) - self.client.throughputs(self.handle_throughputs) # get location if query_location: diff --git a/coretk/coretk/menuaction.py b/coretk/coretk/menuaction.py index 616e9bc1..ec7e7cb7 100644 --- a/coretk/coretk/menuaction.py +++ b/coretk/coretk/menuaction.py @@ -151,8 +151,7 @@ class MenuAction: dialog.show() def throughput(self): - throughput = self.app.core.throughput - if throughput: - self.app.core.throughput = False + if not self.app.core.handling_throughputs: + self.app.core.enable_throughputs() else: - self.app.core.throughput = True + self.app.core.cancel_throughputs() diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index fbafbb44..bc48c9ab 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -385,23 +385,27 @@ class CoreGrpcClient: :param int session_id: id of session :param handler: handler for received events :param list events: events to listen to, defaults to all - :return: nothing + :return: stream processing events, can be used to cancel stream :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) start_streamer(stream, handler) + return stream - def throughputs(self, handler): + def throughputs(self, session_id, handler): """ Listen for throughput events with information for interfaces and bridges. + :param int session_id: session id :param handler: handler for every event - :return: nothing + :return: stream processing events, can be used to cancel stream + :raises grpc.RpcError: when session doesn't exist """ - request = core_pb2.ThroughputsRequest() + request = core_pb2.ThroughputsRequest(session_id=session_id) stream = self.stub.Throughputs(request) start_streamer(stream, handler) + return stream def add_node(self, session_id, node): """ diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 1ada5267..53c1d6d6 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -27,7 +27,7 @@ from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"\d+") +_INTERFACE_REGEX = re.compile(r"[0-9a-fA-F]+") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -452,9 +452,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param grpc.SrevicerContext context: context object :return: nothing """ + session = self.get_session(request.session_id, context) delay = 3 last_check = None last_stats = None + while self._is_running(context): now = time.monotonic() stats = get_net_stats() @@ -462,7 +464,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # calculate average if last_check is not None: interval = now - last_check - throughputs_event = core_pb2.ThroughputsEvent() + throughputs_event = core_pb2.ThroughputsEvent(session_id=session.id) for key in stats: current_rxtx = stats[key] previous_rxtx = last_stats.get(key) @@ -477,8 +479,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): throughput = rx_kbps + tx_kbps if key.startswith("veth"): key = key.split(".") - node_id = int(_INTERFACE_REGEX.search(key[0]).group()) - interface_id = int(key[1]) + node_id = int(_INTERFACE_REGEX.search(key[0]).group(), base=16) + interface_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue interface_throughput = ( throughputs_event.interface_throughputs.add() ) @@ -487,7 +492,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): interface_throughput.throughput = throughput elif key.startswith("b."): try: - node_id = int(key.split(".")[1]) + key = key.split(".") + node_id = int(key[1], base=16) + session_id = int(key[2], base=16) + if session.id != session_id: + continue bridge_throughput = ( throughputs_event.bridge_throughputs.add() ) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index b0a6c0d6..ee099b31 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -272,11 +272,13 @@ message EventsRequest { } message ThroughputsRequest { + int32 session_id = 1; } message ThroughputsEvent { - repeated BridgeThroughput bridge_throughputs = 1; - repeated InterfaceThroughput interface_throughputs = 2; + int32 session_id = 1; + repeated BridgeThroughput bridge_throughputs = 2; + repeated InterfaceThroughput interface_throughputs = 3; } message InterfaceThroughput { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 796febf7..72e469f3 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1032,7 +1032,7 @@ class TestGrpc: # given client = CoreGrpcClient() - grpc_server.coreemu.create_session() + session = grpc_server.coreemu.create_session() queue = Queue() def handle_event(event_data): @@ -1040,7 +1040,7 @@ class TestGrpc: # then with client.context_connect(): - client.throughputs(handle_event) + client.throughputs(session.id, handle_event) time.sleep(0.1) # then From d248bc09b5ef6a46c4bcf3529bc7e0540ee18d6e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:17:20 -0800 Subject: [PATCH 276/305] added clearing streams to reset for coreclient and set event stream to be cleared --- coretk/coretk/coreclient.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 9f6900cc..b1d4d22f 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -85,6 +85,7 @@ class CoreClient: self.file_configs = {} self.mobility_players = {} self.handling_throughputs = None + self.handling_events = None def reset(self): # helpers @@ -101,6 +102,13 @@ class CoreClient: self.service_configs.clear() self.file_configs.clear() self.mobility_players.clear() + # clear streams + if self.handling_throughputs: + self.handling_throughputs.cancel() + self.handling_throughputs = None + if self.handling_events: + self.handling_events.cancel() + self.handling_events = None def set_observer(self, value): self.observer = value @@ -210,7 +218,9 @@ class CoreClient: response = self.client.get_session(self.session_id) session = response.session self.state = session.state - self.client.events(self.session_id, self.handle_events) + self.handling_events = self.client.events( + self.session_id, self.handle_events + ) # get location if query_location: From 9eaa1fb36c706a5bd60106848ea807b3c9c11fa7 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 11:42:39 -0800 Subject: [PATCH 277/305] more work on color picker --- coretk/coretk/dialogs/colorpicker.py | 139 ++++++++++++++++++--------- coretk/coretk/dialogs/shapemod.py | 14 ++- 2 files changed, 105 insertions(+), 48 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index ded652a0..305be7d4 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -16,60 +16,98 @@ class ColorPicker(Dialog): self.green_entry = None self.hex_entry = None self.display = None - - self.red = tk.StringVar(value=0) - self.blue = tk.StringVar(value=0) - self.green = tk.StringVar(value=0) + self.color = initcolor + red, green, blue = self.get_rgb(initcolor) + self.red = tk.IntVar(value=red) + self.blue = tk.IntVar(value=blue) + self.green = tk.IntVar(value=green) self.hex = tk.StringVar(value=initcolor) + self.red_scale = tk.IntVar(value=red) + self.green_scale = tk.IntVar(value=green) + self.blue_scale = tk.IntVar(value=blue) + def askcolor(self): self.draw() self.set_bindings() + self.show() + return self.color def draw(self): - edit_frame = ttk.Frame(self) - edit_frame.columnconfigure(0, weight=4) - edit_frame.columnconfigure(1, weight=2) - # the rbg frame - frame = ttk.Frame(edit_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.rowconfigure(0, weight=1) - frame.rowconfigure(1, weight=1) - frame.rowconfigure(2, weight=1) + # rgb frames + frame = ttk.Frame(self) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) self.red_entry = ttk.Entry( frame, + width=4, textvariable=self.red, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) self.red_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.red_scale, + command=lambda x: self.scale_callback(self.red_scale, self.red), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew") + frame = ttk.Frame(self) label = ttk.Label(frame, text="G: ") - label.grid(row=1, column=0) + label.grid(row=0, column=0) self.green_entry = ttk.Entry( frame, + width=4, textvariable=self.green, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) - self.green_entry.grid(row=1, column=1, sticky="nsew") + self.green_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.green_scale, + command=lambda x: self.scale_callback(self.green_scale, self.green), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=1, column=0) + frame = ttk.Frame(self) label = ttk.Label(frame, text="B: ") - label.grid(row=2, column=0) + label.grid(row=0, column=0) self.blue_entry = ttk.Entry( frame, + width=4, textvariable=self.blue, validate="key", validatecommand=(self.app.validation.rgb, "%P"), ) - self.blue_entry.grid(row=2, column=1, sticky="nsew") - - frame.grid(row=0, column=0, sticky="nsew") + self.blue_entry.grid(row=0, column=1, sticky="nsew") + scale = ttk.Scale( + frame, + from_=0, + to=255, + value=0, + length=200, + orient=tk.HORIZONTAL, + variable=self.blue_scale, + command=lambda x: self.scale_callback(self.blue_scale, self.blue), + ) + scale.grid(row=0, column=2, sticky="nsew") + frame.grid(row=2, column=0, sticky="nsew") # hex code and color display - frame = ttk.Frame(edit_frame) + frame = ttk.Frame(self) frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Selection: ") label.grid(row=0, column=0, sticky="nsew") @@ -80,11 +118,9 @@ class ColorPicker(Dialog): validatecommand=(self.app.validation.hex, "%P"), ) self.hex_entry.grid(row=1, column=0, sticky="nsew") - self.display = ttk.Label(frame, background="white") + self.display = ttk.Label(frame, background=self.color) self.display.grid(row=2, column=0, sticky="nsew") - frame.grid(row=0, column=1, sticky="nsew") - - edit_frame.grid(row=0, column=0, sticky="nsew") + frame.grid(row=3, column=0, sticky="nsew") # button frame frame = ttk.Frame(self) @@ -94,7 +130,7 @@ class ColorPicker(Dialog): button.grid(row=0, column=0, sticky="nsew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="nsew") - frame.grid(row=1, column=0, sticky="nsew") + frame.grid(row=4, column=0, sticky="nsew") def set_bindings(self): self.red_entry.bind("", lambda x: self.current_focus("rgb")) @@ -108,6 +144,8 @@ class ColorPicker(Dialog): def button_ok(self): logging.debug("not implemented") + self.color = self.hex.get() + self.destroy() def get_hex(self): red = self.red_entry.get() @@ -118,33 +156,48 @@ class ColorPicker(Dialog): def current_focus(self, focus): self.focus = focus - def update_color(self, arg1, arg2, arg3): + def update_color(self, arg1=None, arg2=None, arg3=None): if self.focus == "rgb": red = self.red_entry.get() blue = self.blue_entry.get() green = self.green_entry.get() + self.set_scale(red, green, blue) if red and blue and green: hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) - self.hex_entry.delete(0, tk.END) - self.hex_entry.insert(0, hex_code) + self.hex.set(hex_code) self.display.config(background=hex_code) elif self.focus == "hex": hex_code = self.hex.get() if len(hex_code) == 4 or len(hex_code) == 7: - if len(hex_code) == 4: - red = hex_code[1] - green = hex_code[2] - blue = hex_code[3] - else: - red = hex_code[1:3] - green = hex_code[3:5] - blue = hex_code[5:] + red, green, blue = self.get_rgb(hex_code) else: return - self.red_entry.delete(0, tk.END) - self.green_entry.delete(0, tk.END) - self.blue_entry.delete(0, tk.END) - self.red_entry.insert(0, "%s" % (int(red, 16))) - self.green_entry.insert(0, "%s" % (int(green, 16))) - self.blue_entry.insert(0, "%s" % (int(blue, 16))) + self.set_entry(red, green, blue) + self.set_scale(red, green, blue) self.display.config(background=hex_code) + + def scale_callback(self, var, color_var): + color_var.set(var.get()) + self.focus = "rgb" + self.update_color() + + def set_scale(self, r, g, b): + self.red_scale.set(r) + self.green_scale.set(g) + self.blue_scale.set(b) + + def set_entry(self, r, g, b): + self.red.set(r) + self.green.set(g) + self.blue.set(b) + + def get_rgb(self, hex_code): + if len(hex_code) == 4: + red = hex_code[1] + green = hex_code[2] + blue = hex_code[3] + else: + red = hex_code[1:3] + green = hex_code[3:5] + blue = hex_code[5:] + return int(red, 16), int(green, 16), int(blue, 16) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index a6da0e3b..27bc719f 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -4,6 +4,7 @@ shape input dialog import tkinter as tk from tkinter import colorchooser, font, ttk +from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog from coretk.graph import tags from coretk.graph.shapeutils import is_draw_shape, is_shape_text @@ -134,13 +135,16 @@ class ShapeDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def choose_text_color(self): - color = colorchooser.askcolor(color="black") - self.text_color = color[1] + color_picker = ColorPicker(self, self.app, "#000000") + color = color_picker.askcolor() + self.text_color = color def choose_fill_color(self): - color = colorchooser.askcolor(color=self.fill_color) - self.fill_color = color[1] - self.fill.config(background=color[1], text=color[1]) + color_picker = ColorPicker(self, self.app, self.fill_color) + color = color_picker.askcolor() + # color = colorchooser.askcolor(color=self.fill_color) + self.fill_color = color + self.fill.config(background=color, text=color) def choose_border_color(self): color = colorchooser.askcolor(color=self.border_color) From 34c0b91228a1ed087556f9691807bcc014e0917c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:04:18 -0800 Subject: [PATCH 278/305] update zoom % on statusbar --- coretk/coretk/graph/graph.py | 2 ++ coretk/coretk/statusbar.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index a2d82dc4..e8fd6cff 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -464,6 +464,8 @@ class CanvasGraph(tk.Canvas): ) logging.info("ratio: %s", self.ratio) logging.info("offset: %s", self.offset) + self.app.statusbar.zoom.config(text="%s" % (int(self.ratio * 100)) + "%") + if self.wallpaper: self.redraw_wallpaper() diff --git a/coretk/coretk/statusbar.py b/coretk/coretk/statusbar.py index 0528bbf2..5ed6f09d 100644 --- a/coretk/coretk/statusbar.py +++ b/coretk/coretk/statusbar.py @@ -46,7 +46,11 @@ class StatusBar(ttk.Frame): self.status.grid(row=0, column=1, sticky="ew") self.zoom = ttk.Label( - self, text="ZOOM TBD", anchor=tk.CENTER, borderwidth=1, relief=tk.RIDGE + self, + text="%s" % (int(self.app.canvas.ratio * 100)) + "%", + anchor=tk.CENTER, + borderwidth=1, + relief=tk.RIDGE, ) self.zoom.grid(row=0, column=2, sticky="ew") From 44df926fb9d221fb0c4415d52d121144c1e84ae2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:21:03 -0800 Subject: [PATCH 279/305] updated events streamed from sessions to include session id for easy identification --- coretk/coretk/coreclient.py | 18 ++++++++++++++++-- daemon/core/api/grpc/events.py | 11 +---------- daemon/proto/core/api/grpc/core.proto | 25 +++++++++++-------------- daemon/tests/test_grpc.py | 7 +++++++ 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index b1d4d22f..8b01a468 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -20,6 +20,7 @@ from coretk.graph.shapeutils import ShapeType from coretk.interface import InterfaceManager from coretk.nodeutils import NodeDraw, NodeUtils +GUI_SOURCE = "gui" OBSERVERS = { "processes": "ps", "ifconfig": "ifconfig", @@ -133,6 +134,14 @@ class CoreClient: self.custom_observers[observer.name] = observer def handle_events(self, event): + if event.session_id != self.session_id: + logging.warn( + "ignoring event session(%s) current(%s)", + event.session_id, + self.session_id, + ) + return + if event.HasField("link_event"): logging.info("link event: %s", event) self.handle_link_event(event.link_event) @@ -176,7 +185,7 @@ class CoreClient: logging.warning("unknown link event: %s", event.message_type) def handle_node_event(self, event): - if event.source == "gui": + if event.source == GUI_SOURCE: return node_id = event.node.id x = event.node.position.x @@ -195,6 +204,11 @@ class CoreClient: def handle_throughputs(self, event): if event.session_id != self.session_id: + logging.warn( + "ignoring throughput event session(%s) current(%s)", + event.session_id, + self.session_id, + ) return logging.info("handling throughputs event: %s", event) self.app.canvas.throughput_draw.process_grpc_throughput_event( @@ -424,7 +438,7 @@ class CoreClient: def edit_node(self, core_node): try: self.client.edit_node( - self.session_id, core_node.id, core_node.position, source="gui" + self.session_id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: show_grpc_error(e) diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index d7a3094e..7b4756b1 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -107,7 +107,6 @@ def handle_session_event(event): name=event.name, data=event.data, time=event_time, - session_id=event.session, ) @@ -119,9 +118,6 @@ def handle_config_event(event): :return: configuration event :rtype: core.api.grpc.core_pb2.ConfigEvent """ - session_id = None - if event.session is not None: - session_id = int(event.session) return core_pb2.ConfigEvent( message_type=event.message_type, node_id=event.node, @@ -132,7 +128,6 @@ def handle_config_event(event): data_values=event.data_values, possible_values=event.possible_values, groups=event.groups, - session_id=session_id, interface=event.interface_number, network_id=event.network_id, opaque=event.opaque, @@ -150,7 +145,6 @@ def handle_exception_event(event): """ return core_pb2.ExceptionEvent( node_id=event.node, - session_id=int(event.session), level=event.level, source=event.source, date=event.date, @@ -175,7 +169,6 @@ def handle_file_event(event): number=event.number, type=event.type, source=event.source, - session_id=event.session, data=event.data, compressed_data=event.compressed_data, ) @@ -224,7 +217,7 @@ class EventStreamer: :return: grpc event, or None when invalid event or queue timeout :rtype: core.api.grpc.core_pb2.Event """ - event = core_pb2.Event() + event = core_pb2.Event(session_id=self.session.id) try: data = self.queue.get(timeout=1) if isinstance(data, NodeData): @@ -235,8 +228,6 @@ class EventStreamer: event.session_event.CopyFrom(handle_session_event(data)) elif isinstance(data, ConfigData): event.config_event.CopyFrom(handle_config_event(data)) - # TODO: remove when config events are fixed - event.config_event.session_id = self.session.id elif isinstance(data, ExceptionData): event.exception_event.CopyFrom(handle_exception_event(data)) elif isinstance(data, FileData): diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ee099b31..ec7bf49c 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -301,6 +301,7 @@ message Event { ExceptionEvent exception_event = 5; FileEvent file_event = 6; } + int32 session_id = 7; } message NodeEvent { @@ -319,7 +320,6 @@ message SessionEvent { string name = 3; string data = 4; float time = 5; - int32 session_id = 6; } message ConfigEvent { @@ -333,20 +333,18 @@ message ConfigEvent { string bitmap = 8; string possible_values = 9; string groups = 10; - int32 session_id = 11; - int32 interface = 12; - int32 network_id = 13; - string opaque = 14; + int32 interface = 11; + int32 network_id = 12; + string opaque = 13; } message ExceptionEvent { int32 node_id = 1; - int32 session_id = 2; - ExceptionLevel.Enum level = 3; - string source = 4; - string date = 5; - string text = 6; - string opaque = 7; + ExceptionLevel.Enum level = 2; + string source = 3; + string date = 4; + string text = 5; + string opaque = 6; } message FileEvent { @@ -357,9 +355,8 @@ message FileEvent { int32 number = 5; string type = 6; string source = 7; - int32 session_id = 8; - string data = 9; - string compressed_data = 10; + string data = 8; + string compressed_data = 9; } message AddNodeRequest { diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 72e469f3..89e46f9b 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -990,6 +990,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("node_event") queue.put(event_data) @@ -1014,6 +1015,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("link_event") queue.put(event_data) @@ -1036,6 +1038,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id queue.put(event_data) # then @@ -1053,6 +1056,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("session_event") queue.put(event_data) @@ -1075,6 +1079,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("config_event") queue.put(event_data) @@ -1098,6 +1103,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("exception_event") queue.put(event_data) @@ -1120,6 +1126,7 @@ class TestGrpc: queue = Queue() def handle_event(event_data): + assert event_data.session_id == session.id assert event_data.HasField("file_event") queue.put(event_data) From 585d10dd28a9fb74d00d6c43f97819e7e2ab04ae Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 12:34:22 -0800 Subject: [PATCH 280/305] small tweaks to alert dialog --- coretk/coretk/dialogs/alerts.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/coretk/coretk/dialogs/alerts.py b/coretk/coretk/dialogs/alerts.py index 58f65933..89e78f02 100644 --- a/coretk/coretk/dialogs/alerts.py +++ b/coretk/coretk/dialogs/alerts.py @@ -24,10 +24,11 @@ class AlertsDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) self.top.rowconfigure(1, weight=1) - row = 0 + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) - frame.grid(row=row, column=0, sticky="nsew", pady=PADY) + frame.rowconfigure(0, weight=1) + frame.grid(sticky="nsew", pady=PADY) self.tree = ttk.Treeview( frame, columns=("time", "level", "session_id", "node", "source"), @@ -35,15 +36,15 @@ class AlertsDialog(Dialog): ) self.tree.grid(row=0, column=0, sticky="nsew") self.tree.column("time", stretch=tk.YES) - self.tree.heading("time", text="time", anchor="w") + self.tree.heading("time", text="Time") self.tree.column("level", stretch=tk.YES) - self.tree.heading("level", text="level", anchor="w") + self.tree.heading("level", text="Level") self.tree.column("session_id", stretch=tk.YES) - self.tree.heading("session_id", text="session id", anchor="w") + self.tree.heading("session_id", text="Session ID") self.tree.column("node", stretch=tk.YES) - self.tree.heading("node", text="node", anchor="w") + self.tree.heading("node", text="Node") self.tree.column("source", stretch=tk.YES) - self.tree.heading("source", text="source", anchor="w") + self.tree.heading("source", text="Source") self.tree.bind("<>", self.click_select) for alarm in self.app.statusbar.core_alarms: @@ -74,15 +75,13 @@ class AlertsDialog(Dialog): xscrollbar = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview) xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) - row = row + 1 self.text = CodeText(self.top) self.text.config(state=tk.DISABLED) - self.text.grid(row=row, column=0, sticky="nsew", pady=PADY) - row = row + 1 + self.text.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) - frame.grid(row=row, column=0, sticky="nsew") + frame.grid(sticky="ew") frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.columnconfigure(2, weight=1) From 713c42a64edfcb79c316d24d5e9b5b8621d3c02b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:11:23 -0800 Subject: [PATCH 281/305] added styling for listboxes, made use of listbox scroll where all other basic listboxes were being used --- coretk/coretk/dialogs/hooks.py | 7 ++++--- coretk/coretk/dialogs/observers.py | 19 ++++++------------- coretk/coretk/dialogs/servers.py | 18 ++++++------------ coretk/coretk/themes.py | 17 ++++++++++++++++- coretk/coretk/widgets.py | 2 ++ 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 40101823..9aeebecc 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -4,7 +4,7 @@ from tkinter import ttk from core.api.grpc import core_pb2 from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY -from coretk.widgets import CodeText +from coretk.widgets import CodeText, ListboxScroll class HookDialog(Dialog): @@ -96,8 +96,9 @@ class HooksDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.listbox = tk.Listbox(self.top) - self.listbox.grid(sticky="nsew", pady=PADY) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + self.listbox = listbox_scroll.listbox self.listbox.bind("<>", self.select) for hook_file in self.app.core.hooks: self.listbox.insert(tk.END, hook_file) diff --git a/coretk/coretk/dialogs/observers.py b/coretk/coretk/dialogs/observers.py index f496f7ef..de857b76 100644 --- a/coretk/coretk/dialogs/observers.py +++ b/coretk/coretk/dialogs/observers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import Observer from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY +from coretk.widgets import ListboxScroll class ObserverDialog(Dialog): @@ -27,24 +28,16 @@ class ObserverDialog(Dialog): self.draw_apply_buttons() def draw_listbox(self): - frame = ttk.Frame(self.top) - frame.grid(sticky="nsew", pady=PADY) - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) - - scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - self.observers = tk.Listbox( - frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set - ) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(sticky="nsew", pady=PADY) + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) + self.observers = listbox_scroll.listbox self.observers.grid(row=0, column=0, sticky="nsew") self.observers.bind("<>", self.handle_observer_change) for name in sorted(self.app.core.custom_observers): self.observers.insert(tk.END, name) - scrollbar.config(command=self.observers.yview) - def draw_form_fields(self): frame = ttk.Frame(self.top) frame.grid(sticky="ew", pady=PADY) diff --git a/coretk/coretk/dialogs/servers.py b/coretk/coretk/dialogs/servers.py index ef406ecb..c380c63d 100644 --- a/coretk/coretk/dialogs/servers.py +++ b/coretk/coretk/dialogs/servers.py @@ -4,6 +4,7 @@ from tkinter import ttk from coretk.coreclient import CoreServer from coretk.dialogs.dialog import Dialog from coretk.themes import FRAME_PAD, PADX, PADY +from coretk.widgets import ListboxScroll DEFAULT_NAME = "example" DEFAULT_ADDRESS = "127.0.0.1" @@ -32,25 +33,18 @@ class ServersDialog(Dialog): self.draw_apply_buttons() def draw_servers(self): - frame = ttk.Frame(self.top) - frame.grid(pady=PADY, sticky="nsew") - frame.columnconfigure(0, weight=1) - frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(self.top) + listbox_scroll.grid(pady=PADY, sticky="nsew") + listbox_scroll.columnconfigure(0, weight=1) + listbox_scroll.rowconfigure(0, weight=1) - scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL) - scrollbar.grid(row=0, column=1, sticky="ns") - - self.servers = tk.Listbox( - frame, selectmode=tk.SINGLE, yscrollcommand=scrollbar.set - ) + self.servers = listbox_scroll.listbox self.servers.grid(row=0, column=0, sticky="nsew") self.servers.bind("<>", self.handle_server_change) for server in self.app.core.servers: self.servers.insert(tk.END, server) - scrollbar.config(command=self.servers.yview) - def draw_server_configuration(self): frame = ttk.LabelFrame(self.top, text="Server Configuration", padding=FRAME_PAD) frame.grid(pady=PADY, sticky="ew") diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 43b59ffd..685a8350 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,5 +1,6 @@ import logging import tkinter as tk +from tkinter import ttk THEME_DARK = "black" PADX = (0, 5) @@ -160,7 +161,21 @@ def update_menu(style, widget): if not abg: abg = bg widget.config( - background=bg, foreground=fg, activebackground=abg, activeforeground=fg + background=bg, foreground=fg, activebackground=abg, activeforeground=fg, bd=0 + ) + + +def update_listbox(widget): + style = ttk.Style() + bg = style.lookup(".", "background") + fg = style.lookup(".", "foreground") + widget.config( + background=bg, + foreground=fg, + highlightthickness=1, + highlightcolor="black", + highlightbackground="black", + bd=0, ) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 349401c5..a00b590a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,6 +5,7 @@ from tkinter import filedialog, font, ttk from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 +from coretk import themes from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY @@ -184,6 +185,7 @@ class ListboxScroll(ttk.LabelFrame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) + themes.update_listbox(self.listbox) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 69494b600f31d2d3c411274a9e29904b08760ad0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 13:26:55 -0800 Subject: [PATCH 282/305] fixes for parsing grpc throughputs --- daemon/core/api/grpc/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 53c1d6d6..068d3eeb 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -27,7 +27,7 @@ from core.nodes.lxd import LxcNode from core.services.coreservices import ServiceManager _ONE_DAY_IN_SECONDS = 60 * 60 * 24 -_INTERFACE_REGEX = re.compile(r"[0-9a-fA-F]+") +_INTERFACE_REGEX = re.compile(r"veth(?P[0-9a-fA-F]+)") class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): @@ -479,7 +479,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): throughput = rx_kbps + tx_kbps if key.startswith("veth"): key = key.split(".") - node_id = int(_INTERFACE_REGEX.search(key[0]).group(), base=16) + node_id = _INTERFACE_REGEX.search(key[0]).group("node") + node_id = int(node_id, base=16) interface_id = int(key[1], base=16) session_id = int(key[2], base=16) if session.id != session_id: From c4f21e8a2ee12f7797594715f910b3721e16eb8a Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:17:05 -0800 Subject: [PATCH 283/305] cleaned up listboxscroll widgets to not default to a labeleframe, updated code that really needed a lebelframe --- coretk/coretk/dialogs/customnodes.py | 22 ++++++++++----- coretk/coretk/dialogs/nodeservice.py | 16 ++++++++--- coretk/coretk/dialogs/serviceconfiguration.py | 28 ++++++++++++------- coretk/coretk/widgets.py | 2 +- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 6aabf417..7effd11a 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -24,13 +24,17 @@ class ServicesSelectDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - frame = ttk.Frame(self.top) + frame = ttk.LabelFrame(self.top) frame.grid(stick="nsew", pady=PADY) frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) - self.groups.grid(row=0, column=0, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) @@ -45,8 +49,12 @@ class ServicesSelectDialog(Dialog): ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) - self.current.grid(row=0, column=2, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) @@ -109,12 +117,12 @@ class CustomNodesDialog(Dialog): self.draw_buttons() def draw_node_config(self): - frame = ttk.Frame(self.top) + frame = ttk.LabelFrame(self.top, text="Nodes", padding=FRAME_PAD) frame.grid(sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) frame.rowconfigure(0, weight=1) - self.nodes_list = ListboxScroll(frame, text="Nodes", padding=FRAME_PAD) + self.nodes_list = ListboxScroll(frame) self.nodes_list.grid(row=0, column=0, sticky="nsew", padx=PADX) self.nodes_list.listbox.bind("<>", self.handle_node_select) for name in sorted(self.app.core.custom_nodes): diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 2aa9c6ad..59b8865b 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -40,8 +40,12 @@ class NodeService(Dialog): frame.rowconfigure(0, weight=1) for i in range(3): frame.columnconfigure(i, weight=1) - self.groups = ListboxScroll(frame, text="Groups", padding=FRAME_PAD) - self.groups.grid(row=0, column=0, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Groups", padding=FRAME_PAD) + label_frame.grid(row=0, column=0, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.groups = ListboxScroll(label_frame) + self.groups.grid(sticky="nsew") for group in sorted(self.app.core.services): self.groups.listbox.insert(tk.END, group) self.groups.listbox.bind("<>", self.handle_group_change) @@ -56,8 +60,12 @@ class NodeService(Dialog): ) self.services.grid(row=0, column=1, sticky="nsew") - self.current = ListboxScroll(frame, text="Selected", padding=FRAME_PAD) - self.current.grid(row=0, column=2, sticky="nsew") + label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) + label_frame.grid(row=0, column=2, sticky="nsew") + label_frame.rowconfigure(0, weight=1) + label_frame.columnconfigure(0, weight=1) + self.current = ListboxScroll(label_frame) + self.current.grid(sticky="nsew") for service in sorted(self.current_services): self.current.listbox.insert(tk.END, service) diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index a53c2aa1..0031ee31 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -216,32 +216,32 @@ class ServiceConfiguration(Dialog): tab.columnconfigure(0, weight=1) for i in range(3): tab.rowconfigure(i, weight=1) - self.notebook.add(tab, text="Startup/shutdown") + self.notebook.add(tab, text="Startup/Shutdown") # tab 3 for i in range(3): label_frame = None if i == 0: label_frame = ttk.LabelFrame( - tab, text="Startup commands", padding=FRAME_PAD + tab, text="Startup Commands", padding=FRAME_PAD ) commands = self.startup_commands elif i == 1: label_frame = ttk.LabelFrame( - tab, text="Shutdown commands", padding=FRAME_PAD + tab, text="Shutdown Commands", padding=FRAME_PAD ) commands = self.shutdown_commands elif i == 2: label_frame = ttk.LabelFrame( - tab, text="Validation commands", padding=FRAME_PAD + tab, text="Validation Commands", padding=FRAME_PAD ) commands = self.validation_commands label_frame.columnconfigure(0, weight=1) label_frame.rowconfigure(1, weight=1) - label_frame.grid(row=i, column=0, sticky="nsew") + label_frame.grid(row=i, column=0, sticky="nsew", pady=PADY) frame = ttk.Frame(label_frame) - frame.grid(row=0, column=0, sticky="nsew") + frame.grid(row=0, column=0, sticky="nsew", pady=PADY) frame.columnconfigure(0, weight=1) entry = ttk.Entry(frame, textvariable=tk.StringVar()) entry.grid(row=0, column=0, stick="ew", padx=PADX) @@ -251,7 +251,7 @@ class ServiceConfiguration(Dialog): button = ttk.Button(frame, image=self.editdelete_img) button.grid(row=0, column=2, sticky="ew") button.bind("", self.delete_command) - listbox_scroll = ListboxScroll(label_frame, borderwidth=0) + listbox_scroll = ListboxScroll(label_frame) listbox_scroll.listbox.bind("<>", self.update_entry) for command in commands: listbox_scroll.listbox.insert("end", command) @@ -303,13 +303,21 @@ class ServiceConfiguration(Dialog): ) self.validation_period_entry.grid(row=2, column=1, sticky="ew") - listbox_scroll = ListboxScroll(tab, text="Executables", padding=FRAME_PAD) - listbox_scroll.grid(sticky="nsew", pady=PADY) + label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) + listbox_scroll.grid(sticky="nsew") tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for executable in self.executables: listbox_scroll.listbox.insert("end", executable) - listbox_scroll = ListboxScroll(tab, text="Dependencies", padding=FRAME_PAD) + label_frame = ttk.LabelFrame(tab, text="Dependencies", padding=FRAME_PAD) + label_frame.grid(sticky="nsew", pady=PADY) + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) + listbox_scroll = ListboxScroll(label_frame) listbox_scroll.grid(sticky="nsew") tab.rowconfigure(listbox_scroll.grid_info()["row"], weight=1) for dependency in self.dependencies: diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index a00b590a..feed7958 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -175,7 +175,7 @@ class ConfigFrame(FrameScroll): return {x: self.config[x].value for x in self.config} -class ListboxScroll(ttk.LabelFrame): +class ListboxScroll(ttk.Frame): def __init__(self, master=None, **kw): super().__init__(master, **kw) self.columnconfigure(0, weight=1) From dcdcb6a711b308dd882236aad40c53d2e83a4f0e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:21:30 -0800 Subject: [PATCH 284/305] updated theme style function names, removed unused function --- coretk/coretk/graph/node.py | 2 +- coretk/coretk/themes.py | 13 +++---------- coretk/coretk/widgets.py | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 93b6c390..105e21b7 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -168,7 +168,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) - themes.update_menu(self.app.style, context) + themes.style_menu(self.app.style, context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 685a8350..55b5732d 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -1,4 +1,3 @@ -import logging import tkinter as tk from tkinter import ttk @@ -142,19 +141,13 @@ def load(style): ) -def update_bg(style, event): - logging.info("updating background: %s", event.widget) - bg = style.lookup(".", "background") - event.widget.config(background=bg) - - def theme_change_menu(style, event): if not isinstance(event.widget, tk.Menu): return - update_menu(style, event.widget) + style_menu(style, event.widget) -def update_menu(style, widget): +def style_menu(style, widget): bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") @@ -165,7 +158,7 @@ def update_menu(style, widget): ) -def update_listbox(widget): +def style_listbox(widget): style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index feed7958..9ca96c0e 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -185,7 +185,7 @@ class ListboxScroll(ttk.Frame): self.listbox = tk.Listbox( self, selectmode=tk.SINGLE, yscrollcommand=self.scrollbar.set ) - themes.update_listbox(self.listbox) + themes.style_listbox(self.listbox) self.listbox.grid(row=0, column=0, sticky="nsew") self.scrollbar.config(command=self.listbox.yview) From 5dbd34f230eb20c93d4b3e6a23cc3cfc7d7f87bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:30:38 -0800 Subject: [PATCH 285/305] cleanup for theming widgets --- coretk/coretk/app.py | 7 ++----- coretk/coretk/graph/node.py | 2 +- coretk/coretk/themes.py | 17 +++++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/coretk/coretk/app.py b/coretk/coretk/app.py index 7c2562a9..c8d4e597 100644 --- a/coretk/coretk/app.py +++ b/coretk/coretk/app.py @@ -1,6 +1,5 @@ import logging import tkinter as tk -from functools import partial from tkinter import ttk from coretk import appconfig, themes @@ -42,11 +41,9 @@ class Application(tk.Frame): def setup_theme(self): themes.load(self.style) + self.master.bind_class("Menu", "<>", themes.theme_change_menu) + self.master.bind("<>", themes.theme_change) self.style.theme_use(self.guiconfig["preferences"]["theme"]) - func = partial(themes.theme_change_menu, self.style) - self.master.bind_class("Menu", "<>", func) - func = partial(themes.theme_change, self.style) - self.master.bind("<>", func) def setup_app(self): self.master.title("CORE") diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 105e21b7..e9f59882 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -168,7 +168,7 @@ class CanvasNode: is_wlan = self.core_node.type == NodeType.WIRELESS_LAN is_emane = self.core_node.type == NodeType.EMANE context = tk.Menu(self.canvas) - themes.style_menu(self.app.style, context) + themes.style_menu(context) if self.app.core.is_runtime(): context.add_command(label="Configure", command=self.show_config) if NodeUtils.is_container_node(self.core_node.type): diff --git a/coretk/coretk/themes.py b/coretk/coretk/themes.py index 55b5732d..66565f8d 100644 --- a/coretk/coretk/themes.py +++ b/coretk/coretk/themes.py @@ -141,13 +141,14 @@ def load(style): ) -def theme_change_menu(style, event): +def theme_change_menu(event): if not isinstance(event.widget, tk.Menu): return - style_menu(style, event.widget) + style_menu(event.widget) -def style_menu(style, widget): +def style_menu(widget): + style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") abg = style.lookup(".", "lightcolor") @@ -162,17 +163,21 @@ def style_listbox(widget): style = ttk.Style() bg = style.lookup(".", "background") fg = style.lookup(".", "foreground") + bc = style.lookup(".", "bordercolor") + if not bc: + bc = "black" widget.config( background=bg, foreground=fg, highlightthickness=1, - highlightcolor="black", - highlightbackground="black", + highlightcolor=bc, + highlightbackground=bc, bd=0, ) -def theme_change(style, event): +def theme_change(event): + style = ttk.Style() style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal")) style.configure( Styles.green_alert, From 084aedf3d251846cdbc4afe59dd61bf02757e1e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:34:37 -0800 Subject: [PATCH 286/305] removed services button from node config dialog, limited to context menu for now --- coretk/coretk/dialogs/nodeconfig.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index c71a1b34..cfec3b24 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -5,7 +5,6 @@ from tkinter import ttk from coretk import nodeutils from coretk.dialogs.dialog import Dialog -from coretk.dialogs.nodeservice import NodeService from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY @@ -130,10 +129,6 @@ class NodeConfigDialog(Dialog): combobox.grid(row=row, column=1, sticky="ew") row += 1 - # services - button = ttk.Button(self.top, text="Services", command=self.click_services) - button.grid(sticky="ew", pady=PADY) - # interfaces if self.canvas_node.interfaces: self.draw_interfaces() @@ -193,10 +188,6 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") - def click_services(self): - dialog = NodeService(self, self.app, self.canvas_node) - dialog.show() - def click_icon(self): file_path = image_chooser(self) if file_path: From b7139996e174c65eebd996cf50adc2355f7766be Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:55:05 -0800 Subject: [PATCH 287/305] have a working color picker --- coretk/coretk/dialogs/colorpicker.py | 90 +++++++++++++++++++++------- coretk/coretk/dialogs/shapemod.py | 10 ++-- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index 305be7d4..5b3a4fc4 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -15,6 +15,9 @@ class ColorPicker(Dialog): self.blue_entry = None self.green_entry = None self.hex_entry = None + self.red_label = None + self.green_label = None + self.blue_label = None self.display = None self.color = initcolor red, green, blue = self.get_rgb(initcolor) @@ -25,16 +28,21 @@ class ColorPicker(Dialog): self.red_scale = tk.IntVar(value=red) self.green_scale = tk.IntVar(value=green) self.blue_scale = tk.IntVar(value=blue) - - def askcolor(self): self.draw() self.set_bindings() + + def askcolor(self): self.show() return self.color def draw(self): + self.top.columnconfigure(0, weight=1) # rgb frames - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) self.red_entry = ttk.Entry( @@ -50,15 +58,23 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.red_scale, command=lambda x: self.scale_callback(self.red_scale, self.red), ) scale.grid(row=0, column=2, sticky="nsew") + self.red_label = ttk.Label( + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0) + ) + self.red_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=0, column=0, sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="G: ") label.grid(row=0, column=0) self.green_entry = ttk.Entry( @@ -74,15 +90,23 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.green_scale, command=lambda x: self.scale_callback(self.green_scale, self.green), ) scale.grid(row=0, column=2, sticky="nsew") - frame.grid(row=1, column=0) + self.green_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0) + ) + self.green_label.grid(row=0, column=3, sticky="nsew") + frame.grid(row=1, column=0, sticky="nsew") - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.columnconfigure(2, weight=4) + frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="B: ") label.grid(row=0, column=0) self.blue_entry = ttk.Entry( @@ -98,16 +122,20 @@ class ColorPicker(Dialog): from_=0, to=255, value=0, - length=200, + # length=200, orient=tk.HORIZONTAL, variable=self.blue_scale, command=lambda x: self.scale_callback(self.blue_scale, self.blue), ) scale.grid(row=0, column=2, sticky="nsew") + self.blue_label = ttk.Label( + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()) + ) + self.blue_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=2, column=0, sticky="nsew") # hex code and color display - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) label = ttk.Label(frame, text="Selection: ") label.grid(row=0, column=0, sticky="nsew") @@ -118,12 +146,12 @@ class ColorPicker(Dialog): validatecommand=(self.app.validation.hex, "%P"), ) self.hex_entry.grid(row=1, column=0, sticky="nsew") - self.display = ttk.Label(frame, background=self.color) - self.display.grid(row=2, column=0, sticky="nsew") + self.display = tk.Frame(frame, background=self.color, width=100, height=100) + self.display.grid(row=2, column=0) frame.grid(row=3, column=0, sticky="nsew") # button frame - frame = ttk.Frame(self) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) button = ttk.Button(frame, text="OK", command=self.button_ok) @@ -148,6 +176,12 @@ class ColorPicker(Dialog): self.destroy() def get_hex(self): + """ + convert current RGB values into hex color + + :rtype: str + :return: hex color + """ red = self.red_entry.get() blue = self.blue_entry.get() green = self.green_entry.get() @@ -166,6 +200,7 @@ class ColorPicker(Dialog): hex_code = "#%02x%02x%02x" % (int(red), int(green), int(blue)) self.hex.set(hex_code) self.display.config(background=hex_code) + self.set_label(red, green, blue) elif self.focus == "hex": hex_code = self.hex.get() if len(hex_code) == 4 or len(hex_code) == 7: @@ -175,23 +210,36 @@ class ColorPicker(Dialog): self.set_entry(red, green, blue) self.set_scale(red, green, blue) self.display.config(background=hex_code) + self.set_label(red, green, blue) def scale_callback(self, var, color_var): color_var.set(var.get()) self.focus = "rgb" self.update_color() - def set_scale(self, r, g, b): - self.red_scale.set(r) - self.green_scale.set(g) - self.blue_scale.set(b) + def set_scale(self, red, green, blue): + self.red_scale.set(red) + self.green_scale.set(green) + self.blue_scale.set(blue) - def set_entry(self, r, g, b): - self.red.set(r) - self.green.set(g) - self.blue.set(b) + def set_entry(self, red, green, blue): + self.red.set(red) + self.green.set(green) + self.blue.set(blue) + + def set_label(self, red, green, blue): + self.red_label.configure(background="#%02x%02x%02x" % (int(red), 0, 0)) + self.green_label.configure(background="#%02x%02x%02x" % (0, int(green), 0)) + self.blue_label.configure(background="#%02x%02x%02x" % (0, 0, int(blue))) def get_rgb(self, hex_code): + """ + convert a valid hex code to RGB values + + :param string hex_code: color in hex + :rtype: tuple(int, int, int) + :return: the RGB values + """ if len(hex_code) == 4: red = hex_code[1] green = hex_code[2] diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index 27bc719f..e915e3d9 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -2,7 +2,7 @@ shape input dialog """ import tkinter as tk -from tkinter import colorchooser, font, ttk +from tkinter import font, ttk from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog @@ -147,9 +147,11 @@ class ShapeDialog(Dialog): self.fill.config(background=color, text=color) def choose_border_color(self): - color = colorchooser.askcolor(color=self.border_color) - self.border_color = color[1] - self.border.config(background=color[1], text=color[1]) + color_picker = ColorPicker(self, self.app, self.border_color) + color = color_picker.askcolor() + # color = colorchooser.askcolor(color=self.border_color) + self.border_color = color + self.border.config(background=color, text=color) def cancel(self): self.shape.delete() From 0082d61517604fa3cbf45a4bb9b28af4616f1e8c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 14:55:54 -0800 Subject: [PATCH 288/305] updates to allow wlan config changes during runtime --- coretk/coretk/dialogs/wlanconfig.py | 5 ++++- coretk/coretk/graph/graph.py | 14 ++++++++++---- coretk/coretk/toolbar.py | 2 ++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/coretk/coretk/dialogs/wlanconfig.py b/coretk/coretk/dialogs/wlanconfig.py index aed411b8..20966d2b 100644 --- a/coretk/coretk/dialogs/wlanconfig.py +++ b/coretk/coretk/dialogs/wlanconfig.py @@ -58,6 +58,9 @@ class WlanConfigDialog(Dialog): :return: nothing """ - self.config_frame.parse_config() + config = self.config_frame.parse_config() self.app.core.wlan_configs[self.node.id] = self.config + if self.app.core.is_runtime(): + session_id = self.app.core.session_id + self.app.core.client.set_wlan_config(session_id, self.node.id, config) self.destroy() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index a2d82dc4..acce0e28 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -87,6 +87,9 @@ class CanvasGraph(tk.Canvas): :param core.api.grpc.core_pb2.Session session: session to draw :return: nothing """ + # hide context + self.hide_context() + # delete any existing drawn items for tag in tags.COMPONENT_TAGS: self.delete(tag) @@ -122,6 +125,11 @@ class CanvasGraph(tk.Canvas): self.bind("", lambda e: self.scan_mark(e.x, e.y)) self.bind("", lambda e: self.scan_dragto(e.x, e.y, gain=1)) + def hide_context(self): + if self.context: + self.context.unpost() + self.context = None + def get_actual_coords(self, x, y): actual_x = (x - self.offset[0]) / self.ratio actual_y = (y - self.offset[1]) / self.ratio @@ -294,8 +302,7 @@ class CanvasGraph(tk.Canvas): return if self.context: - self.context.unpost() - self.context = None + self.hide_context() else: if self.mode == GraphMode.ANNOTATION: self.focus_set() @@ -592,8 +599,7 @@ class CanvasGraph(tk.Canvas): self.context = canvas_node.create_context() self.context.post(event.x_root, event.y_root) else: - self.context.unpost() - self.context = None + self.hide_context() def press_delete(self, event): """ diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index df183494..323c5d67 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -222,6 +222,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.canvas.hide_context() self.app.statusbar.core_alarms.clear() self.app.statusbar.progress_bar.start(5) self.app.canvas.mode = GraphMode.SELECT @@ -385,6 +386,7 @@ class Toolbar(ttk.Frame): :return: nothing """ + self.app.canvas.hide_context() self.app.statusbar.progress_bar.start(5) thread = threading.Thread(target=self.app.core.stop_session) thread.start() From 921e002997d9a7f9ae43251a0598a80966ab9e2e Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 16:05:15 -0800 Subject: [PATCH 289/305] small fix for color picker --- coretk/coretk/dialogs/colorpicker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/coretk/coretk/dialogs/colorpicker.py b/coretk/coretk/dialogs/colorpicker.py index 5b3a4fc4..9734468d 100644 --- a/coretk/coretk/dialogs/colorpicker.py +++ b/coretk/coretk/dialogs/colorpicker.py @@ -41,7 +41,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="R: ") label.grid(row=0, column=0) @@ -65,7 +65,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.red_label = ttk.Label( - frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0) + frame, background="#%02x%02x%02x" % (self.red.get(), 0, 0), width=5 ) self.red_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=0, column=0, sticky="nsew") @@ -73,7 +73,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="G: ") label.grid(row=0, column=0) @@ -97,7 +97,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.green_label = ttk.Label( - frame, background="#%02x%02x%02x" % (0, self.green.get(), 0) + frame, background="#%02x%02x%02x" % (0, self.green.get(), 0), width=5 ) self.green_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=1, column=0, sticky="nsew") @@ -105,7 +105,7 @@ class ColorPicker(Dialog): frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=4) + frame.columnconfigure(2, weight=6) frame.columnconfigure(3, weight=2) label = ttk.Label(frame, text="B: ") label.grid(row=0, column=0) @@ -129,7 +129,7 @@ class ColorPicker(Dialog): ) scale.grid(row=0, column=2, sticky="nsew") self.blue_label = ttk.Label( - frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()) + frame, background="#%02x%02x%02x" % (0, 0, self.blue.get()), width=5 ) self.blue_label.grid(row=0, column=3, sticky="nsew") frame.grid(row=2, column=0, sticky="nsew") From cb1322a28a143072a9b8efaa5a1bf8f128c66c9f Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Mon, 16 Dec 2019 16:06:21 -0800 Subject: [PATCH 290/305] small fix for color picker --- coretk/coretk/dialogs/shapemod.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coretk/coretk/dialogs/shapemod.py b/coretk/coretk/dialogs/shapemod.py index e915e3d9..62fed9f9 100644 --- a/coretk/coretk/dialogs/shapemod.py +++ b/coretk/coretk/dialogs/shapemod.py @@ -142,14 +142,12 @@ class ShapeDialog(Dialog): def choose_fill_color(self): color_picker = ColorPicker(self, self.app, self.fill_color) color = color_picker.askcolor() - # color = colorchooser.askcolor(color=self.fill_color) self.fill_color = color self.fill.config(background=color, text=color) def choose_border_color(self): color_picker = ColorPicker(self, self.app, self.border_color) color = color_picker.askcolor() - # color = colorchooser.askcolor(color=self.border_color) self.border_color = color self.border.config(background=color, text=color) From 042a56ecb3db8b84a527b70ede6c288255b56d56 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 16 Dec 2019 20:57:46 -0800 Subject: [PATCH 291/305] updated node dialog to display interfaces in notebook tabs, added emane interface configuration for nodes connected to an emane network --- coretk/coretk/dialogs/nodeconfig.py | 66 +++++++++++++++++++---------- coretk/coretk/graph/node.py | 17 ++++++++ 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index cfec3b24..1137d6b9 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -5,10 +5,11 @@ from tkinter import ttk from coretk import nodeutils from coretk.dialogs.dialog import Dialog +from coretk.dialogs.emaneconfig import EmaneModelDialog from coretk.images import Images from coretk.nodeutils import NodeUtils from coretk.themes import FRAME_PAD, PADX, PADY -from coretk.widgets import FrameScroll, image_chooser +from coretk.widgets import image_chooser def mac_auto(is_auto, entry): @@ -137,42 +138,57 @@ class NodeConfigDialog(Dialog): self.draw_buttons() def draw_interfaces(self): - scroll = FrameScroll(self.top, self.app, text="Interfaces") - scroll.grid(sticky="nsew") - scroll.frame.columnconfigure(0, weight=1) - scroll.frame.rowconfigure(0, weight=1) + notebook = ttk.Notebook(self.top) + notebook.grid(sticky="nsew", pady=PADY) + self.top.rowconfigure(notebook.grid_info()["row"], weight=1) + for interface in self.canvas_node.interfaces: logging.info("interface: %s", interface) - frame = ttk.LabelFrame(scroll.frame, text=interface.name, padding=FRAME_PAD) - frame.grid(sticky="ew", pady=PADY) - frame.columnconfigure(1, weight=1) - frame.columnconfigure(2, weight=1) + tab = ttk.Frame(notebook, padding=FRAME_PAD) + tab.grid(sticky="nsew", pady=PADY) + tab.columnconfigure(1, weight=1) + tab.columnconfigure(2, weight=1) + notebook.add(tab, text=interface.name) - label = ttk.Label(frame, text="MAC") - label.grid(row=0, column=0, padx=PADX, pady=PADY) + row = 0 + emane_node = self.canvas_node.has_emane_link(interface.id) + if emane_node: + emane_model = emane_node.emane.split("_")[1] + button = ttk.Button( + tab, + text=f"Configure EMANE {emane_model}", + command=lambda: self.click_emane_config(emane_model, interface.id), + ) + button.grid(row=row, sticky="ew", columnspan=3, pady=PADY) + row += 1 + + label = ttk.Label(tab, text="MAC") + label.grid(row=row, column=0, padx=PADX, pady=PADY) is_auto = tk.BooleanVar(value=True) - checkbutton = ttk.Checkbutton(frame, text="Auto?", variable=is_auto) + checkbutton = ttk.Checkbutton(tab, text="Auto?", variable=is_auto) checkbutton.var = is_auto - checkbutton.grid(row=0, column=1, padx=PADX) + checkbutton.grid(row=row, column=1, padx=PADX) mac = tk.StringVar(value=interface.mac) - entry = ttk.Entry(frame, textvariable=mac, state=tk.DISABLED) - entry.grid(row=0, column=2, sticky="ew") + entry = ttk.Entry(tab, textvariable=mac, state=tk.DISABLED) + entry.grid(row=row, column=2, sticky="ew") func = partial(mac_auto, is_auto, entry) checkbutton.config(command=func) + row += 1 - label = ttk.Label(frame, text="IPv4") - label.grid(row=1, column=0, padx=PADX, pady=PADY) + label = ttk.Label(tab, text="IPv4") + label.grid(row=row, column=0, padx=PADX, pady=PADY) ip4 = tk.StringVar(value=f"{interface.ip4}/{interface.ip4mask}") - entry = ttk.Entry(frame, textvariable=ip4) + entry = ttk.Entry(tab, textvariable=ip4) entry.bind("", self.app.validation.ip_focus_out) - entry.grid(row=1, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky="ew") + row += 1 - label = ttk.Label(frame, text="IPv6") - label.grid(row=2, column=0, padx=PADX, pady=PADY) + label = ttk.Label(tab, text="IPv6") + label.grid(row=row, column=0, padx=PADX, pady=PADY) ip6 = tk.StringVar(value=f"{interface.ip6}/{interface.ip6mask}") - entry = ttk.Entry(frame, textvariable=ip6) + entry = ttk.Entry(tab, textvariable=ip6) entry.bind("", self.app.validation.ip_focus_out) - entry.grid(row=2, column=1, columnspan=2, sticky="ew") + entry.grid(row=row, column=1, columnspan=2, sticky="ew") self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6) @@ -188,6 +204,10 @@ class NodeConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="ew") + def click_emane_config(self, emane_model, interface_id): + dialog = EmaneModelDialog(self, self.app, self.node, emane_model, interface_id) + dialog.show() + def click_icon(self): file_path = image_chooser(self) if file_path: diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index e9f59882..4836d2d2 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -246,6 +246,23 @@ class CanvasNode: dialog = NodeService(self.app.master, self.app, self) dialog.show() + def has_emane_link(self, interface_id): + result = None + for edge in self.edges: + if self.id == edge.src: + other_id = edge.dst + edge_interface_id = edge.src_interface.id + else: + other_id = edge.src + edge_interface_id = edge.dst_interface.id + if edge_interface_id != interface_id: + continue + other_node = self.canvas.nodes[other_id] + if other_node.core_node.type == NodeType.EMANE: + result = other_node.core_node + break + return result + def wireless_link_selected(self): self.canvas.context = None for canvas_nid in [ From 02695f16721707a1267dddeeb0e51855e885e2d0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 10:01:25 -0800 Subject: [PATCH 292/305] revamped codetext widget to use ttk scrollbars for better theme matching, slight adjustments to service config dialog layout --- coretk/coretk/dialogs/about.py | 8 +++--- coretk/coretk/dialogs/alerts.py | 28 +++++++++---------- coretk/coretk/dialogs/hooks.py | 14 +++++----- coretk/coretk/dialogs/serviceconfiguration.py | 26 +++++++++-------- coretk/coretk/widgets.py | 15 ++++++---- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/coretk/coretk/dialogs/about.py b/coretk/coretk/dialogs/about.py index 28ddea33..9e3ff7a9 100644 --- a/coretk/coretk/dialogs/about.py +++ b/coretk/coretk/dialogs/about.py @@ -38,7 +38,7 @@ class AboutDialog(Dialog): self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - text = CodeText(self.top) - text.insert("1.0", LICENSE) - text.config(state=tk.DISABLED) - text.grid(sticky="nsew") + codetext = CodeText(self.top) + codetext.text.insert("1.0", LICENSE) + codetext.text.config(state=tk.DISABLED) + codetext.grid(sticky="nsew") diff --git a/coretk/coretk/dialogs/alerts.py b/coretk/coretk/dialogs/alerts.py index 89e78f02..e782547f 100644 --- a/coretk/coretk/dialogs/alerts.py +++ b/coretk/coretk/dialogs/alerts.py @@ -17,7 +17,7 @@ class AlertsDialog(Dialog): super().__init__(master, app, "Alerts", modal=True) self.app = app self.tree = None - self.text = None + self.codetext = None self.draw() def draw(self): @@ -76,9 +76,9 @@ class AlertsDialog(Dialog): xscrollbar.grid(row=1, sticky="ew") self.tree.configure(xscrollcommand=xscrollbar.set) - self.text = CodeText(self.top) - self.text.config(state=tk.DISABLED) - self.text.grid(sticky="nsew", pady=PADY) + self.codetext = CodeText(self.top) + self.codetext.text.config(state=tk.DISABLED) + self.codetext.grid(sticky="nsew", pady=PADY) frame = ttk.Frame(self.top) frame.grid(sticky="ew") @@ -96,7 +96,7 @@ class AlertsDialog(Dialog): button.grid(row=0, column=3, sticky="ew") def reset_alerts(self): - self.text.delete("1.0", tk.END) + self.codetext.text.delete("1.0", tk.END) for item in self.tree.get_children(): self.tree.delete(item) self.app.statusbar.core_alarms.clear() @@ -137,8 +137,8 @@ class AlertsDialog(Dialog): text = text + "node created" except RpcError: text = text + "node not created" - self.text.delete("1.0", "end") - self.text.insert("1.0", text) + self.codetext.text.delete("1.0", "end") + self.codetext.text.insert("1.0", text) class DaemonLog(Dialog): @@ -155,8 +155,8 @@ class DaemonLog(Dialog): frame.grid(row=0, column=0, sticky="ew", pady=PADY) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=9) - label = ttk.Label(frame, text="File: ") - label.grid(row=0, column=0) + label = ttk.Label(frame, text="File", anchor="w") + label.grid(row=0, column=0, sticky="ew") entry = ttk.Entry(frame, textvariable=self.path, state="disabled") entry.grid(row=0, column=1, sticky="ew") try: @@ -164,8 +164,8 @@ class DaemonLog(Dialog): log = file.readlines() except FileNotFoundError: log = "Log file not found" - text = CodeText(self.top) - text.insert("1.0", log) - text.see("end") - text.config(state=tk.DISABLED) - text.grid(row=1, column=0, sticky="nsew") + codetext = CodeText(self.top) + codetext.text.insert("1.0", log) + codetext.text.see("end") + codetext.text.config(state=tk.DISABLED) + codetext.grid(row=1, column=0, sticky="nsew") diff --git a/coretk/coretk/dialogs/hooks.py b/coretk/coretk/dialogs/hooks.py index 9aeebecc..37503d66 100644 --- a/coretk/coretk/dialogs/hooks.py +++ b/coretk/coretk/dialogs/hooks.py @@ -11,7 +11,7 @@ class HookDialog(Dialog): def __init__(self, master, app): super().__init__(master, app, "Hook", modal=True) self.name = tk.StringVar() - self.data = None + self.codetext = None self.hook = core_pb2.Hook() self.state = tk.StringVar() self.draw() @@ -41,8 +41,8 @@ class HookDialog(Dialog): combobox.bind("<>", self.state_change) # data - self.data = CodeText(self.top) - self.data.insert( + self.codetext = CodeText(self.top) + self.codetext.text.insert( 1.0, ( "#!/bin/sh\n" @@ -50,7 +50,7 @@ class HookDialog(Dialog): "# specified state\n" ), ) - self.data.grid(sticky="nsew") + self.codetext.grid(sticky="nsew", pady=PADY) # button row frame = ttk.Frame(self.top) @@ -69,13 +69,13 @@ class HookDialog(Dialog): def set(self, hook): self.hook = hook self.name.set(hook.file) - self.data.delete(1.0, tk.END) - self.data.insert(tk.END, hook.data) + self.codetext.text.delete(1.0, tk.END) + self.codetext.text.insert(tk.END, hook.data) state_name = core_pb2.SessionState.Enum.Name(hook.state) self.state.set(state_name) def save(self): - data = self.data.get("1.0", tk.END).strip() + data = self.codetext.text.get("1.0", tk.END).strip() state_value = core_pb2.SessionState.Enum.Value(self.state.get()) self.hook.file = self.name.get() self.hook.data = data diff --git a/coretk/coretk/dialogs/serviceconfiguration.py b/coretk/coretk/dialogs/serviceconfiguration.py index 0031ee31..53aca1b3 100644 --- a/coretk/coretk/dialogs/serviceconfiguration.py +++ b/coretk/coretk/dialogs/serviceconfiguration.py @@ -110,7 +110,7 @@ class ServiceConfiguration(Dialog): # draw notebook self.notebook = ttk.Notebook(self.top) - self.notebook.grid(sticky="nsew") + self.notebook.grid(sticky="nsew", pady=PADY) self.draw_tab_files() self.draw_tab_directories() self.draw_tab_startstop() @@ -192,11 +192,13 @@ class ServiceConfiguration(Dialog): tab.rowconfigure(self.service_file_data.grid_info()["row"], weight=1) if len(self.filenames) > 0: self.filename_combobox.current(0) - self.service_file_data.delete(1.0, "end") - self.service_file_data.insert( + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert( "end", self.temp_service_files[self.filenames[0]] ) - self.service_file_data.bind("", self.update_temp_service_file_data) + self.service_file_data.text.bind( + "", self.update_temp_service_file_data + ) def draw_tab_directories(self): tab = ttk.Frame(self.notebook, padding=FRAME_PAD) @@ -275,14 +277,14 @@ class ServiceConfiguration(Dialog): frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Validation Time") - label.grid(row=0, column=0, sticky="w") + label.grid(row=0, column=0, sticky="w", padx=PADX) self.validation_time_entry = ttk.Entry(frame) self.validation_time_entry.insert("end", self.validation_time) self.validation_time_entry.config(state=tk.DISABLED) - self.validation_time_entry.grid(row=0, column=1, sticky="ew") + self.validation_time_entry.grid(row=0, column=1, sticky="ew", pady=PADY) label = ttk.Label(frame, text="Validation Mode") - label.grid(row=1, column=0, sticky="w") + label.grid(row=1, column=0, sticky="w", padx=PADX) if self.validation_mode == core_pb2.ServiceValidationMode.BLOCKING: mode = "BLOCKING" elif self.validation_mode == core_pb2.ServiceValidationMode.NON_BLOCKING: @@ -294,14 +296,14 @@ class ServiceConfiguration(Dialog): ) self.validation_mode_entry.insert("end", mode) self.validation_mode_entry.config(state=tk.DISABLED) - self.validation_mode_entry.grid(row=1, column=1, sticky="ew") + self.validation_mode_entry.grid(row=1, column=1, sticky="ew", pady=PADY) label = ttk.Label(frame, text="Validation Period") - label.grid(row=2, column=0, sticky="w") + label.grid(row=2, column=0, sticky="w", padx=PADX) self.validation_period_entry = ttk.Entry( frame, state=tk.DISABLED, textvariable=tk.StringVar() ) - self.validation_period_entry.grid(row=2, column=1, sticky="ew") + self.validation_period_entry.grid(row=2, column=1, sticky="ew", pady=PADY) label_frame = ttk.LabelFrame(tab, text="Executables", padding=FRAME_PAD) label_frame.grid(sticky="nsew", pady=PADY) @@ -429,8 +431,8 @@ class ServiceConfiguration(Dialog): def display_service_file_data(self, event): combobox = event.widget filename = combobox.get() - self.service_file_data.delete(1.0, "end") - self.service_file_data.insert("end", self.temp_service_files[filename]) + self.service_file_data.text.delete(1.0, "end") + self.service_file_data.text.insert("end", self.temp_service_files[filename]) def update_temp_service_file_data(self, event): scrolledtext = event.widget diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 9ca96c0e..461cb31a 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -2,7 +2,6 @@ import logging import tkinter as tk from functools import partial from tkinter import filedialog, font, ttk -from tkinter.scrolledtext import ScrolledText from core.api.grpc import core_pb2 from coretk import themes @@ -208,10 +207,13 @@ class CodeFont(font.Font): super().__init__(font="TkFixedFont", color="green") -class CodeText(ScrolledText): +class CodeText(ttk.Frame): def __init__(self, master, **kwargs): - super().__init__( - master, + super().__init__(master, **kwargs) + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.text = tk.Text( + self, bd=0, bg="black", cursor="xterm lime lime", @@ -222,8 +224,11 @@ class CodeText(ScrolledText): selectbackground="lime", selectforeground="black", relief=tk.FLAT, - **kwargs ) + self.text.grid(row=0, column=0, sticky="nsew") + yscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview) + yscrollbar.grid(row=0, column=1, sticky="ns") + self.text.configure(yscrollcommand=yscrollbar.set) class Spinbox(ttk.Entry): From 80609da90a8369398bc76e516d1c1d379d3dc82f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 10:58:03 -0800 Subject: [PATCH 293/305] updates to docs for nrl changes to github --- docs/install.md | 9 ++++----- docs/services.md | 10 +++++----- docs/usage.md | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/install.md b/docs/install.md index 12242b4b..e10c1450 100644 --- a/docs/install.md +++ b/docs/install.md @@ -68,7 +68,7 @@ Virtual networks generally require some form of routing in order to work (e.g. t tables for routing packets from one subnet to another.) CORE builds OSPF routing protocol configurations by default when the blue router node type is used. -* [OSPF MANET Designated Routers](http://www.nrl.navy.mil/itd/ncs/products/ospf-manet) (MDR) - the Quagga routing +* [OSPF MANET Designated Routers](https://github.com/USNavalResearchLaboratory/ospf-mdr) (MDR) - the Quagga routing suite with a modified version of OSPFv3, optimized for use with mobile wireless networks. The **mdr** node type (and the MDR service) requires this variant of Quagga. @@ -77,7 +77,7 @@ suite with a modified version of OSPFv3, optimized for use with mobile wireless There is a built package which can be used. ```shell -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/quagga-0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb +wget https://github.com/USNavalResearchLaboratory/ospf-mdr/releases/download/v0.99.21mr2.2/quagga-mr_0.99.21mr2.2_amd64.deb sudo dpkg -i quagga-mr_0.99.21mr2.2_amd64.deb ``` @@ -89,9 +89,8 @@ Requires building from source, from the latest nightly snapshot. # packages needed beyond what's normally required to build core on ubuntu sudo apt install libtool libreadline-dev autoconf -wget https://downloads.pf.itd.nrl.navy.mil/ospf-manet/nightly_snapshots/quagga-svnsnap.tgz -tar xzf quagga-svnsnap.tgz -cd quagga +git clone https://github.com/USNavalResearchLaboratory/ospf-mdr +cd ospf-mdr ./bootstrap.sh ./configure --disable-doc --enable-user=root --enable-group=root --with-cflags=-ggdb \ --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ diff --git a/docs/services.md b/docs/services.md index f2a1a38a..692bcedd 100644 --- a/docs/services.md +++ b/docs/services.md @@ -312,17 +312,17 @@ Currently the Naval Research Laboratory uses this library to develop a wide vari * arouted #### NRL Installation -In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [official nightly snapshots website](https://downloads.pf.itd.nrl.navy.mil/protolib/nightly_snapshots/). +In order to be able to use the different protocols that NRL offers, you must first download the support library itself. You can get the source code from their [NRL Protolib Repo](https://github.com/USNavalResearchLaboratory/protolib). #### Multi-Generator (MGEN) -Download MGEN from the [NRL MGEN nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/mgen/nightly_snapshots/), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. +Download MGEN from the [NRL MGEN Repo](https://github.com/USNavalResearchLaboratory/mgen), unpack it and copy the protolib library into the main folder *mgen*. Execute the following commands to build the protocol. ```shell cd mgen/makefiles make -f Makefile.{os} mgen ``` #### Neighborhood Discovery Protocol (NHDP) -Download NHDP from the [NRL NHDP nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/nhdp/nightly_snapshots/). +Download NHDP from the [NRL NHDP Repo](https://github.com/USNavalResearchLaboratory/NCS-Downloads/tree/master/nhdp). ```shell sudo apt-get install libpcap-dev libboost-all-dev wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip @@ -339,14 +339,14 @@ make -f Makefile.{os} ``` #### Simplified Multicast Forwarding (SMF) -Download SMF from the [NRL SMF nightly snapshot](https://downloads.pf.itd.nrl.navy.mil/smf/nightly_snapshots/) , unpack it and place the protolib library inside the *smf* main folder. +Download SMF from the [NRL SMF Repo](https://github.com/USNavalResearchLaboratory/nrlsmf) , unpack it and place the protolib library inside the *smf* main folder. ```shell cd mgen/makefiles make -f Makefile.{os} ``` #### Optimized Link State Routing Protocol (OLSR) -To install the OLSR protocol, download their source code from their [nightly snapshots](https://downloads.pf.itd.nrl.navy.mil/olsr/nightly_snapshots/nrlolsr-svnsnap.tgz). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: +To install the OLSR protocol, download their source code from their [NRL OLSR Repo](https://github.com/USNavalResearchLaboratory/nrlolsr). Unpack it and place the previously downloaded protolib library inside the *nrlolsr* main directory. Then execute the following commands: ```shell cd ./unix make -f Makefile.{os} diff --git a/docs/usage.md b/docs/usage.md index 7f8cf672..02e1295f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -650,7 +650,7 @@ Any time you edit the topology file, you will need to stop the emulation if it were running and reload the file. -The **.xml** [file schema is specified by NRL](http://www.nrl.navy.mil/itd/ncs/products/mnmtools) and there are two versions to date: +The **.xml** [file schema is specified by NRL](https://github.com/USNavalResearchLaboratory/NCS-Downloads/blob/master/mnmtools/EmulationScriptSchemaDescription.pdf) and there are two versions to date: version 0.0 and version 1.0, with 1.0 as the current default. CORE can open either XML version. However, the xmlfilever line in **/etc/core/core.conf** controls the version of the XML file From e72f44ed85ab4b6112063c6e32b7e6a15949a1df Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:35:30 -0800 Subject: [PATCH 294/305] updated canvas wallpaper selection to use common image chooser function --- coretk/coretk/dialogs/canvaswallpaper.py | 12 +++--------- coretk/coretk/dialogs/customnodes.py | 3 ++- coretk/coretk/dialogs/nodeconfig.py | 3 ++- coretk/coretk/widgets.py | 7 +++---- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/dialogs/canvaswallpaper.py b/coretk/coretk/dialogs/canvaswallpaper.py index 923e4d5f..570bfa08 100644 --- a/coretk/coretk/dialogs/canvaswallpaper.py +++ b/coretk/coretk/dialogs/canvaswallpaper.py @@ -3,12 +3,13 @@ set wallpaper """ import logging import tkinter as tk -from tkinter import filedialog, ttk +from tkinter import ttk from coretk.appconfig import BACKGROUNDS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images from coretk.themes import PADX, PADY +from coretk.widgets import image_chooser class CanvasBackgroundDialog(Dialog): @@ -126,14 +127,7 @@ class CanvasBackgroundDialog(Dialog): button.grid(row=0, column=1, sticky="ew") def click_open_image(self): - filename = filedialog.askopenfilename( - initialdir=str(BACKGROUNDS_PATH), - title="Open", - filetypes=( - ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), - ("All Files", "*"), - ), - ) + filename = image_chooser(self, BACKGROUNDS_PATH) if filename: self.filename.set(filename) self.draw_preview() diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 7effd11a..6416d24a 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -4,6 +4,7 @@ from pathlib import Path from tkinter import ttk from coretk import nodeutils +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.images import Images from coretk.nodeutils import NodeDraw @@ -179,7 +180,7 @@ class CustomNodesDialog(Dialog): self.image_button.config(image="") def click_icon(self): - file_path = image_chooser(self) + file_path = image_chooser(self, ICONS_PATH) if file_path: image = Images.create(file_path, nodeutils.ICON_SIZE) self.image = image diff --git a/coretk/coretk/dialogs/nodeconfig.py b/coretk/coretk/dialogs/nodeconfig.py index 1137d6b9..4e5bc864 100644 --- a/coretk/coretk/dialogs/nodeconfig.py +++ b/coretk/coretk/dialogs/nodeconfig.py @@ -4,6 +4,7 @@ from functools import partial from tkinter import ttk from coretk import nodeutils +from coretk.appconfig import ICONS_PATH from coretk.dialogs.dialog import Dialog from coretk.dialogs.emaneconfig import EmaneModelDialog from coretk.images import Images @@ -209,7 +210,7 @@ class NodeConfigDialog(Dialog): dialog.show() def click_icon(self): - file_path = image_chooser(self) + file_path = image_chooser(self, ICONS_PATH) if file_path: self.image = Images.create(file_path, nodeutils.ICON_SIZE) self.image_button.config(image=self.image) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 461cb31a..6567ca75 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -5,7 +5,6 @@ from tkinter import filedialog, font, ttk from core.api.grpc import core_pb2 from coretk import themes -from coretk.appconfig import ICONS_PATH from coretk.themes import FRAME_PAD, PADX, PADY INT_TYPES = { @@ -239,11 +238,11 @@ class Spinbox(ttk.Entry): self.tk.call(self._w, "set", value) -def image_chooser(parent): +def image_chooser(parent, path): return filedialog.askopenfilename( parent=parent, - initialdir=str(ICONS_PATH), - title="Select Icon", + initialdir=str(path), + title="Select", filetypes=( ("images", "*.gif *.jpg *.png *.bmp *pcx *.tga ..."), ("All Files", "*"), From 50efd2ebc3f2394a53507e042737919e3f86f47f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:43:49 -0800 Subject: [PATCH 295/305] changes to framescroll widget to use a standard frame by default instead of a label frame --- coretk/coretk/dialogs/customnodes.py | 12 ++++++------ coretk/coretk/dialogs/nodeservice.py | 12 ++++++------ coretk/coretk/widgets.py | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/dialogs/customnodes.py b/coretk/coretk/dialogs/customnodes.py index 6416d24a..94f2a32f 100644 --- a/coretk/coretk/dialogs/customnodes.py +++ b/coretk/coretk/dialogs/customnodes.py @@ -41,14 +41,14 @@ class ServicesSelectDialog(Dialog): self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( - frame, - self.app, - text="Services", - clicked=self.service_clicked, - padding=FRAME_PAD, + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(row=0, column=1, sticky="nsew") + self.services.grid(sticky="nsew") label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) label_frame.grid(row=0, column=2, sticky="nsew") diff --git a/coretk/coretk/dialogs/nodeservice.py b/coretk/coretk/dialogs/nodeservice.py index 59b8865b..8ad87649 100644 --- a/coretk/coretk/dialogs/nodeservice.py +++ b/coretk/coretk/dialogs/nodeservice.py @@ -51,14 +51,14 @@ class NodeService(Dialog): self.groups.listbox.bind("<>", self.handle_group_change) self.groups.listbox.selection_set(0) + label_frame = ttk.LabelFrame(frame, text="Services") + label_frame.grid(row=0, column=1, sticky="nsew") + label_frame.columnconfigure(0, weight=1) + label_frame.rowconfigure(0, weight=1) self.services = CheckboxList( - frame, - self.app, - text="Services", - clicked=self.service_clicked, - padding=FRAME_PAD, + label_frame, self.app, clicked=self.service_clicked, padding=FRAME_PAD ) - self.services.grid(row=0, column=1, sticky="nsew") + self.services.grid(sticky="nsew") label_frame = ttk.LabelFrame(frame, text="Selected", padding=FRAME_PAD) label_frame.grid(row=0, column=2, sticky="nsew") diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 6567ca75..7deb7282 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -25,7 +25,7 @@ def file_button_click(value): value.set(file_path) -class FrameScroll(ttk.LabelFrame): +class FrameScroll(ttk.Frame): def __init__(self, master, app, _cls=ttk.Frame, **kw): super().__init__(master, **kw) self.app = app From d11ce3ef7f3c7072eec092053406a146d0e56775 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:47:05 -0800 Subject: [PATCH 296/305] create layout for link configuration --- coretk/coretk/dialogs/linkconfig.py | 212 ++++++++++++++++++++++++++++ coretk/coretk/graph/edges.py | 28 +++- 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 coretk/coretk/dialogs/linkconfig.py diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py new file mode 100644 index 00000000..3ece4ec0 --- /dev/null +++ b/coretk/coretk/dialogs/linkconfig.py @@ -0,0 +1,212 @@ +""" +link configuration +""" +import logging +import tkinter as tk +from tkinter import ttk + +from coretk.dialogs.dialog import Dialog + + +class LinkConfiguration(Dialog): + def __init__(self, master, app, edge): + super().__init__(master, app, "link configuration", modal=True) + self.app = app + self.edge = edge + self.is_symmetric = True + if self.is_symmetric: + self.symmetry_var = tk.StringVar(value=">>") + else: + self.symmetry_var = tk.StringVar(value="<<") + + self.bandwidth = tk.DoubleVar() + self.delay = tk.DoubleVar() + self.jitter = tk.DoubleVar() + self.loss = tk.DoubleVar() + self.duplicate = tk.DoubleVar() + self.color = "#000000" + self.width = tk.DoubleVar() + + self.down_bandwidth = tk.DoubleVar() + self.down_delay = tk.DoubleVar() + self.down_jitter = tk.DoubleVar() + self.down_loss = tk.DoubleVar() + self.down_duplicate = tk.DoubleVar() + + self.load_link_config() + self.symmetric_frame = None + self.asymmetric_frame = None + + self.draw() + + def draw(self): + self.top.columnconfigure(0, weight=1) + source_name = self.app.canvas.nodes[self.edge.src].core_node.name + dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name + label = ttk.Label( + self.top, + text="Link from %s to %s" % (source_name, dest_name), + anchor=tk.CENTER, + ) + label.grid(row=0, column=0, sticky="nsew") + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=1, column=0, sticky="nsew") + button = ttk.Button(frame, text="unlimited") + button.grid(row=0, column=0, sticky="nsew") + if self.is_symmetric: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + else: + button = ttk.Button( + frame, textvariable=self.symmetry_var, command=self.change_symmetry + ) + button.grid(row=0, column=1, sticky="nsew") + + if self.is_symmetric: + self.symmetric_frame = self.get_frame() + self.symmetric_frame.grid(row=2, column=0, sticky="nsew") + else: + self.asymmetric_frame = self.get_frame() + self.asymmetric_frame.grid(row=2, column=0, sticky="nsew") + + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=1) + frame.grid(row=3, column=0, sticky="nsew") + + button = ttk.Button(frame, text="Apply") + button.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text="Cancel", command=self.destroy) + button.grid(row=0, column=1, sticky="nsew") + + def get_frame(self): + main_frame = ttk.Frame(self.top) + main_frame.columnconfigure(0, weight=1) + if self.is_symmetric: + label_name = "Symmetric link effects: " + else: + label_name = "Asymmetric effects: downstream / upstream " + row = 0 + label = ttk.Label(main_frame, text=label_name, anchor=tk.CENTER) + label.grid(row=row, column=0, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Bandwidth (bps): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.bandwidth) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_bandwidth) + entry.grid(row=0, column=2, sticky="nsew") + + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Delay (us): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.delay) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_delay) + entry.grid(row=0, column=2, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Jitter (us): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.jitter) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_jitter) + entry.grid(row=0, column=2, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Loss (%): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.loss) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_loss) + entry.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Duplicate (%): ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.duplicate) + entry.grid(row=0, column=1, sticky="nsew") + if not self.is_symmetric: + entry = ttk.Entry(frame, textvariable=self.down_duplicate) + entry.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + button = ttk.Button(frame, text=self.color) + button.grid(row=0, column=1, sticky="nsew") + row = row + 1 + + frame = ttk.Frame(main_frame) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text="Width: ") + label.grid(row=0, column=0, sticky="nsew") + entry = ttk.Entry(frame, textvariable=self.width) + entry.grid(row=0, column=1, sticky="nsew") + + return main_frame + + def apply(self): + logging.debug("click apply") + + def change_symmetry(self): + logging.debug("change symmetry") + + if self.is_symmetric: + self.is_symmetric = False + self.symmetry_var.set("<<") + if not self.asymmetric_frame: + self.asymmetric_frame = self.get_frame() + self.symmetric_frame.grid_forget() + self.asymmetric_frame.grid(row=2, column=0) + else: + self.is_symmetric = True + self.symmetry_var.set(">>") + if not self.symmetric_frame: + self.symmetric_frame = self.get_frame() + self.asymmetric_frame.grid_forget() + self.symmetric_frame.grid(row=2, column=0) + + def load_link_config(self): + """ + populate link config to the table + + :return: nothing + """ + width = self.app.canvas.itemcget(self.edge.id, "width") + self.width.set(width) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 94d847bc..ab9447be 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,5 +1,8 @@ +import logging import tkinter as tk +from coretk import themes +from coretk.dialogs.linkconfig import LinkConfiguration from coretk.graph import tags from coretk.nodeutils import NodeUtils @@ -23,7 +26,7 @@ class CanvasEdge: Canvas edge class """ - width = 1.4 + width = 3 def __init__(self, x1, y1, x2, y2, src, canvas): """ @@ -46,6 +49,10 @@ class CanvasEdge: self.token = None self.link_info = None self.throughput = None + self.set_binding() + + def set_binding(self): + self.canvas.tag_bind(self.id, "", self.create_context) def complete(self, dst): self.dst = dst @@ -89,3 +96,22 @@ class CanvasEdge: if self.link_info: self.canvas.delete(self.link_info.id1) self.canvas.delete(self.link_info.id2) + + def create_context(self, event): + logging.debug("create link context") + context = tk.Menu(self.canvas) + themes.style_menu(context) + context.add_command(label="Configure", command=self.configure) + context.add_command(label="Delete") + context.add_command(label="Split") + context.add_command(label="Merge") + if self.canvas.app.core.is_runtime(): + context.entryconfigure(1, state="disabled") + context.entryconfigure(2, state="disabled") + context.entryconfigure(3, state="disabled") + context.post(event.x_root, event.y_root) + + def configure(self): + logging.debug("link configuration") + dialog = LinkConfiguration(self.canvas, self.canvas.app, self) + dialog.show() From 582beea1be8e8ba09a744069b6b48edc7be636e6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:56:22 -0800 Subject: [PATCH 297/305] improvement to configframe, it is now a notebook itself, with scrollable frames for contents in each tab --- coretk/coretk/widgets.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/coretk/coretk/widgets.py b/coretk/coretk/widgets.py index 7deb7282..649dce95 100644 --- a/coretk/coretk/widgets.py +++ b/coretk/coretk/widgets.py @@ -63,9 +63,9 @@ class FrameScroll(ttk.Frame): widget.destroy() -class ConfigFrame(FrameScroll): +class ConfigFrame(ttk.Notebook): def __init__(self, master, app, config, **kw): - super().__init__(master, app, ttk.Notebook, borderwidth=0, **kw) + super().__init__(master, **kw) self.app = app self.config = config self.values = {} @@ -79,17 +79,17 @@ class ConfigFrame(FrameScroll): for group_name in sorted(group_mapping): group = group_mapping[group_name] - frame = ttk.Frame(self.frame, padding=FRAME_PAD) - frame.columnconfigure(1, weight=1) - self.frame.add(frame, text=group_name) + tab = FrameScroll(self, self.app, borderwidth=0, padding=FRAME_PAD) + tab.frame.columnconfigure(1, weight=1) + self.add(tab, text=group_name) for index, option in enumerate(sorted(group, key=lambda x: x.name)): - label = ttk.Label(frame, text=option.label) + label = ttk.Label(tab.frame, text=option.label) label.grid(row=index, pady=PADY, padx=PADX, sticky="w") value = tk.StringVar() if option.type == core_pb2.ConfigOptionType.BOOL: select = tuple(option.select) combobox = ttk.Combobox( - frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state="readonly" ) combobox.grid(row=index, column=1, sticky="ew") if option.value == "1": @@ -100,13 +100,13 @@ class ConfigFrame(FrameScroll): value.set(option.value) select = tuple(option.select) combobox = ttk.Combobox( - frame, textvariable=value, values=select, state="readonly" + tab.frame, textvariable=value, values=select, state="readonly" ) combobox.grid(row=index, column=1, sticky="ew") elif option.type == core_pb2.ConfigOptionType.STRING: value.set(option.value) if "file" in option.label: - file_frame = ttk.Frame(frame) + file_frame = ttk.Frame(tab.frame) file_frame.grid(row=index, column=1, sticky="ew") file_frame.columnconfigure(0, weight=1) entry = ttk.Entry(file_frame, textvariable=value) @@ -117,20 +117,20 @@ class ConfigFrame(FrameScroll): else: if "controlnet" in option.name and "script" not in option.name: entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.ip4, "%P"), ) entry.grid(row=index, column=1, sticky="ew") else: - entry = ttk.Entry(frame, textvariable=value) + entry = ttk.Entry(tab.frame, textvariable=value) entry.grid(row=index, column=1, sticky="ew") elif option.type in INT_TYPES: value.set(option.value) entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.positive_int, "%P"), @@ -143,7 +143,7 @@ class ConfigFrame(FrameScroll): elif option.type == core_pb2.ConfigOptionType.FLOAT: value.set(option.value) entry = ttk.Entry( - frame, + tab.frame, textvariable=value, validate="key", validatecommand=(self.app.validation.positive_float, "%P"), From 752b3956a6a14638740381415ac79e297c8dad8c Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Tue, 17 Dec 2019 11:59:34 -0800 Subject: [PATCH 298/305] create layout for link configuration --- coretk/coretk/dialogs/linkconfig.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 3ece4ec0..bd0a9207 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -24,7 +24,7 @@ class LinkConfiguration(Dialog): self.jitter = tk.DoubleVar() self.loss = tk.DoubleVar() self.duplicate = tk.DoubleVar() - self.color = "#000000" + self.color = tk.StringVar(value="#000000") self.width = tk.DoubleVar() self.down_bandwidth = tk.DoubleVar() @@ -77,7 +77,7 @@ class LinkConfiguration(Dialog): frame.columnconfigure(1, weight=1) frame.grid(row=3, column=0, sticky="nsew") - button = ttk.Button(frame, text="Apply") + button = ttk.Button(frame, text="Apply", command=self.apply) button.grid(row=0, column=0, sticky="nsew") button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky="nsew") @@ -166,7 +166,7 @@ class LinkConfiguration(Dialog): frame.grid(row=row, column=0, sticky="nsew") label = ttk.Label(frame, text="Color: ") label.grid(row=0, column=0, sticky="nsew") - button = ttk.Button(frame, text=self.color) + button = ttk.Button(frame, textvariable=self.color) button.grid(row=0, column=1, sticky="nsew") row = row + 1 @@ -183,6 +183,9 @@ class LinkConfiguration(Dialog): def apply(self): logging.debug("click apply") + width = self.width.get() + self.app.canvas.itemconfigure(self.edge.id, width=width) + self.destroy() def change_symmetry(self): logging.debug("change symmetry") @@ -209,4 +212,6 @@ class LinkConfiguration(Dialog): :return: nothing """ width = self.app.canvas.itemcget(self.edge.id, "width") + # color = self.app.canvas.itemcget(self.edge.id, "fill") self.width.set(width) + # self.color From 7bd8c5a5aa45c70c7ae17db83bb5159130e15c11 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 22:02:56 -0800 Subject: [PATCH 299/305] updates to linkconfig, working configuration for symmetric, updates to change link color and use color picker --- coretk/coretk/dialogs/linkconfig.py | 276 +++++++++++++++++----------- 1 file changed, 171 insertions(+), 105 deletions(-) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index bd0a9207..9a2008ab 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -5,12 +5,14 @@ import logging import tkinter as tk from tkinter import ttk +from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog +from coretk.themes import PADX, PADY class LinkConfiguration(Dialog): def __init__(self, master, app, edge): - super().__init__(master, app, "link configuration", modal=True) + super().__init__(master, app, "Link Configuration", modal=True) self.app = app self.edge = edge self.is_symmetric = True @@ -19,19 +21,20 @@ class LinkConfiguration(Dialog): else: self.symmetry_var = tk.StringVar(value="<<") - self.bandwidth = tk.DoubleVar() - self.delay = tk.DoubleVar() - self.jitter = tk.DoubleVar() - self.loss = tk.DoubleVar() - self.duplicate = tk.DoubleVar() + self.bandwidth = tk.StringVar() + self.delay = tk.StringVar() + self.jitter = tk.StringVar() + self.loss = tk.StringVar() + self.duplicate = tk.StringVar() self.color = tk.StringVar(value="#000000") + self.color_button = None self.width = tk.DoubleVar() - self.down_bandwidth = tk.DoubleVar() - self.down_delay = tk.DoubleVar() - self.down_jitter = tk.DoubleVar() - self.down_loss = tk.DoubleVar() - self.down_duplicate = tk.DoubleVar() + self.down_bandwidth = tk.IntVar(value="") + self.down_delay = tk.IntVar(value="") + self.down_jitter = tk.IntVar(value="") + self.down_loss = tk.DoubleVar(value="") + self.down_duplicate = tk.IntVar(value="") self.load_link_config() self.symmetric_frame = None @@ -44,17 +47,16 @@ class LinkConfiguration(Dialog): source_name = self.app.canvas.nodes[self.edge.src].core_node.name dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name label = ttk.Label( - self.top, - text="Link from %s to %s" % (source_name, dest_name), - anchor=tk.CENTER, + self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER ) - label.grid(row=0, column=0, sticky="nsew") + label.grid(row=0, column=0, sticky="ew", pady=PADY) + frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=1, column=0, sticky="nsew") - button = ttk.Button(frame, text="unlimited") - button.grid(row=0, column=0, sticky="nsew") + frame.grid(row=1, column=0, sticky="ew", pady=PADY) + button = ttk.Button(frame, text="Unlimited") + button.grid(row=0, column=0, sticky="ew", padx=PADX) if self.is_symmetric: button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry @@ -63,128 +65,185 @@ class LinkConfiguration(Dialog): button = ttk.Button( frame, textvariable=self.symmetry_var, command=self.change_symmetry ) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") if self.is_symmetric: self.symmetric_frame = self.get_frame() - self.symmetric_frame.grid(row=2, column=0, sticky="nsew") + self.symmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) else: self.asymmetric_frame = self.get_frame() - self.asymmetric_frame.grid(row=2, column=0, sticky="nsew") + self.asymmetric_frame.grid(row=2, column=0, sticky="ew", pady=PADY) + + self.draw_spacer(row=3) frame = ttk.Frame(self.top) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) - frame.grid(row=3, column=0, sticky="nsew") - - button = ttk.Button(frame, text="Apply", command=self.apply) - button.grid(row=0, column=0, sticky="nsew") + frame.grid(row=4, column=0, sticky="ew") + button = ttk.Button(frame, text="Apply", command=self.click_apply) + button.grid(row=0, column=0, sticky="ew", padx=PADX) button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky="nsew") + button.grid(row=0, column=1, sticky="ew") def get_frame(self): - main_frame = ttk.Frame(self.top) - main_frame.columnconfigure(0, weight=1) + frame = ttk.Frame(self.top) + frame.columnconfigure(1, weight=1) if self.is_symmetric: - label_name = "Symmetric link effects: " + label_name = "Symmetric Link Effects" else: - label_name = "Asymmetric effects: downstream / upstream " + label_name = "Asymmetric Effects: Downstream / Upstream " row = 0 - label = ttk.Label(main_frame, text=label_name, anchor=tk.CENTER) - label.grid(row=row, column=0, sticky="nsew") + label = ttk.Label(frame, text=label_name, anchor=tk.CENTER) + label.grid(row=row, column=0, columnspan=2, sticky="ew", pady=PADY) row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Bandwidth (bps): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.bandwidth) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Bandwidth (bps)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_bandwidth) - entry.grid(row=0, column=2, sticky="nsew") - + entry = ttk.Entry( + frame, + textvariable=self.down_bandwidth, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="nsew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Delay (us): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.delay) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Delay (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_delay) - entry.grid(row=0, column=2, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_delay, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Jitter (us): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.jitter) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Jitter (us)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_jitter) - entry.grid(row=0, column=2, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_jitter, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Loss (%): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.loss) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Loss (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_loss) - entry.grid(row=0, column=1, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_loss, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Duplicate (%): ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.duplicate) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Duplicate (%)") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) if not self.is_symmetric: - entry = ttk.Entry(frame, textvariable=self.down_duplicate) - entry.grid(row=0, column=1, sticky="nsew") + entry = ttk.Entry( + frame, + textvariable=self.down_duplicate, + validate="key", + validatecommand=(self.app.validation.positive_int, "%P"), + ) + entry.grid(row=row, column=2, sticky="ew") row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Color: ") - label.grid(row=0, column=0, sticky="nsew") - button = ttk.Button(frame, textvariable=self.color) - button.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Color") + label.grid(row=row, column=0, sticky="ew") + self.color_button = tk.Button( + frame, + textvariable=self.color, + background=self.color.get(), + bd=0, + relief=tk.FLAT, + highlightthickness=0, + command=self.click_color, + ) + self.color_button.grid(row=row, column=1, sticky="ew", pady=PADY) row = row + 1 - frame = ttk.Frame(main_frame) - frame.columnconfigure(0, weight=1) - frame.columnconfigure(1, weight=4) - frame.grid(row=row, column=0, sticky="nsew") - label = ttk.Label(frame, text="Width: ") - label.grid(row=0, column=0, sticky="nsew") - entry = ttk.Entry(frame, textvariable=self.width) - entry.grid(row=0, column=1, sticky="nsew") + label = ttk.Label(frame, text="Width") + label.grid(row=row, column=0, sticky="ew") + entry = ttk.Entry( + frame, + textvariable=self.width, + validate="key", + validatecommand=(self.app.validation.positive_float, "%P"), + ) + entry.grid(row=row, column=1, sticky="ew", pady=PADY) - return main_frame + return frame - def apply(self): + def click_color(self): + dialog = ColorPicker(self, self.app, self.color.get()) + color = dialog.askcolor() + self.color.set(color) + self.color_button.config(background=color) + + def click_apply(self): logging.debug("click apply") - width = self.width.get() - self.app.canvas.itemconfigure(self.edge.id, width=width) + self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) + self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) + link = self.edge.link_info.link + bandwidth = self.bandwidth.get() + if bandwidth != "": + link.options.bandwidth = int(bandwidth) + jitter = self.jitter.get() + if jitter != "": + link.options.jitter = int(jitter) + delay = self.delay.get() + if delay != "": + link.options.delay = int(delay) + duplicate = self.duplicate.get() + if duplicate != "": + link.options.dup = int(duplicate) + loss = self.loss.get() + if loss != "": + link.options.per = float(loss) self.destroy() def change_symmetry(self): @@ -212,6 +271,13 @@ class LinkConfiguration(Dialog): :return: nothing """ width = self.app.canvas.itemcget(self.edge.id, "width") - # color = self.app.canvas.itemcget(self.edge.id, "fill") self.width.set(width) - # self.color + color = self.app.canvas.itemcget(self.edge.id, "fill") + self.color.set(color) + link = self.edge.link_info.link + if link.HasField("options"): + self.bandwidth.set(str(link.options.bandwidth)) + self.jitter.set(str(link.options.jitter)) + self.duplicate.set(str(link.options.dup)) + self.loss.set(str(link.options.per)) + self.delay.set(str(link.options.delay)) From ccb433a32df81653906b3aaf5f9e8525c96fdaed Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 17 Dec 2019 22:08:14 -0800 Subject: [PATCH 300/305] update to support link config during runtime --- coretk/coretk/dialogs/linkconfig.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 9a2008ab..6f0bd51a 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -244,6 +244,24 @@ class LinkConfiguration(Dialog): loss = self.loss.get() if loss != "": link.options.per = float(loss) + + if self.app.core.is_runtime() and link.HasField("options"): + interface_one = None + if link.HasField("interface_one"): + interface_one = link.interface_one.id + interface_two = None + if link.HasField("interface_two"): + interface_two = link.interface_two.id + session_id = self.app.core.session_id + self.app.core.client.edit_link( + session_id, + link.node_one_id, + link.node_two_id, + link.options, + interface_one, + interface_two, + ) + self.destroy() def change_symmetry(self): From da26e347655568ff433d559ff565291cd341ee49 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 18 Dec 2019 09:49:45 -0800 Subject: [PATCH 301/305] work on marker tool --- coretk/coretk/dialogs/marker.py | 53 +++++++++++++++++++++++++++++++ coretk/coretk/graph/graph.py | 35 +++++++++++++++++--- coretk/coretk/graph/shapeutils.py | 4 +++ coretk/coretk/toolbar.py | 11 ++++++- 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 coretk/coretk/dialogs/marker.py diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py new file mode 100644 index 00000000..8a476548 --- /dev/null +++ b/coretk/coretk/dialogs/marker.py @@ -0,0 +1,53 @@ +""" +marker dialog +""" + +from tkinter import ttk + +from coretk.dialogs.colorpicker import ColorPicker +from coretk.dialogs.dialog import Dialog + +MARKER_THICKNESS = [3, 5, 8, 10] + + +class Marker(Dialog): + def __init__(self, master, app, initcolor="#000000"): + super().__init__(master, app, "marker tool", modal=False) + self.app = app + self.color = initcolor + self.radius = MARKER_THICKNESS[0] + self.draw() + + def draw(self): + button = ttk.Button(self.top, text="clear", command=self.clear_marker) + button.grid(row=0, column=0) + + frame = ttk.Frame(self.top) + frame.grid(row=1, column=0) + + button = ttk.Button(frame, text="radius 1") + button.grid(row=0, column=0) + button = ttk.Button(frame, text="radius 2") + button.grid(row=0, column=1) + button = ttk.Button(frame, text="radius 3") + button.grid(row=1, column=0) + button = ttk.Button(frame, text="radius 4") + button.grid(row=1, column=1) + + label = ttk.Label(self.top, background=self.color) + label.grid(row=2, column=0, sticky="nsew") + label.bind("", self.change_color) + + # button = ttk.Button(self.top, text="color", command=self.change_color) + # button.grid(row=2, column=0) + + def clear_marker(self): + canvas = self.app.canvas + for i in canvas.find_withtag("marker"): + canvas.delete(i) + + def change_color(self, event): + color_picker = ColorPicker(self, self.app, self.color) + color = color_picker.askcolor() + event.widget.configure(background=color) + self.color = color diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 77cc2264..e0878f20 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -12,7 +12,7 @@ from coretk.graph.enums import GraphMode, ScaleOption from coretk.graph.linkinfo import LinkInfo, Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape -from coretk.graph.shapeutils import ShapeType, is_draw_shape +from coretk.graph.shapeutils import ShapeType, is_draw_shape, is_marker from coretk.images import Images from coretk.nodeutils import NodeUtils @@ -45,6 +45,7 @@ class CanvasGraph(tk.Canvas): self.ratio = 1.0 self.offset = (0, 0) self.cursor = (0, 0) + self.marker_tool = None # background related self.wallpaper_id = None @@ -499,10 +500,22 @@ class CanvasGraph(tk.Canvas): self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) if self.mode == GraphMode.ANNOTATION and selected is None: - shape = Shape(self.app, self, self.annotation_type, x, y) - self.selected = shape.id - self.shape_drawing = True - self.shapes[shape.id] = shape + if is_marker(self.annotation_type): + r = self.app.toolbar.marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) + else: + shape = Shape(self.app, self, self.annotation_type, x, y) + self.selected = shape.id + self.shape_drawing = True + self.shapes[shape.id] = shape if selected is not None: if selected not in self.selection: @@ -573,6 +586,18 @@ class CanvasGraph(tk.Canvas): if is_draw_shape(self.annotation_type) and self.shape_drawing: shape = self.shapes[self.selected] shape.shape_motion(x, y) + elif is_marker(self.annotation_type): + marker_tool = self.app.toolbar.marker_tool + r = marker_tool.radius + self.create_oval( + x - r, + y - r, + x + r, + y + r, + fill=self.app.toolbar.marker_tool.color, + outline="", + tags="marker", + ) if self.mode == GraphMode.EDGE: return diff --git a/coretk/coretk/graph/shapeutils.py b/coretk/coretk/graph/shapeutils.py index a566a713..0e2cc29c 100644 --- a/coretk/coretk/graph/shapeutils.py +++ b/coretk/coretk/graph/shapeutils.py @@ -17,3 +17,7 @@ def is_draw_shape(shape_type): def is_shape_text(shape_type): return shape_type == ShapeType.TEXT + + +def is_marker(shape_type): + return shape_type == ShapeType.MARKER diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 323c5d67..96b317c2 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -6,9 +6,10 @@ from tkinter import ttk from tkinter.font import Font from coretk.dialogs.customnodes import CustomNodesDialog +from coretk.dialogs.marker import Marker from coretk.graph import tags from coretk.graph.enums import GraphMode -from coretk.graph.shapeutils import ShapeType +from coretk.graph.shapeutils import ShapeType, is_marker from coretk.images import ImageEnum, Images from coretk.nodeutils import NodeUtils from coretk.themes import Styles @@ -57,6 +58,9 @@ class Toolbar(ttk.Frame): self.network_picker = None self.annotation_picker = None + # dialog + self.marker_tool = None + # draw components self.draw() @@ -401,6 +405,9 @@ class Toolbar(ttk.Frame): self.annotation_button.image = image self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type + if is_marker(shape_type): + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") @@ -410,6 +417,8 @@ class Toolbar(ttk.Frame): def click_marker_button(self): logging.debug("Click on marker button") + dialog = Marker(self.master, self.app) + dialog.show() def click_two_node_button(self): logging.debug("Click TWONODE button") From 5f927f75ba0c3a8cf34e2845193794aaff008654 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Wed, 18 Dec 2019 11:57:47 -0800 Subject: [PATCH 302/305] marker tool --- coretk/coretk/dialogs/marker.py | 32 +++++++++++++++++++------------- coretk/coretk/toolbar.py | 5 +++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py index 8a476548..feb8bd09 100644 --- a/coretk/coretk/dialogs/marker.py +++ b/coretk/coretk/dialogs/marker.py @@ -2,6 +2,8 @@ marker dialog """ +import logging +import tkinter as tk from tkinter import ttk from coretk.dialogs.colorpicker import ColorPicker @@ -16,7 +18,9 @@ class Marker(Dialog): self.app = app self.color = initcolor self.radius = MARKER_THICKNESS[0] + self.marker_thickness = tk.IntVar(value=MARKER_THICKNESS[0]) self.draw() + self.top.bind("", self.close_marker) def draw(self): button = ttk.Button(self.top, text="clear", command=self.clear_marker) @@ -24,23 +28,18 @@ class Marker(Dialog): frame = ttk.Frame(self.top) frame.grid(row=1, column=0) - - button = ttk.Button(frame, text="radius 1") - button.grid(row=0, column=0) - button = ttk.Button(frame, text="radius 2") - button.grid(row=0, column=1) - button = ttk.Button(frame, text="radius 3") - button.grid(row=1, column=0) - button = ttk.Button(frame, text="radius 4") - button.grid(row=1, column=1) - + combobox = ttk.Combobox( + frame, + textvariable=self.marker_thickness, + values=MARKER_THICKNESS, + state="readonly", + ) + combobox.grid(row=0, column=1, sticky="nsew") + combobox.bind("<>", self.change_thickness) label = ttk.Label(self.top, background=self.color) label.grid(row=2, column=0, sticky="nsew") label.bind("", self.change_color) - # button = ttk.Button(self.top, text="color", command=self.change_color) - # button.grid(row=2, column=0) - def clear_marker(self): canvas = self.app.canvas for i in canvas.find_withtag("marker"): @@ -51,3 +50,10 @@ class Marker(Dialog): color = color_picker.askcolor() event.widget.configure(background=color) self.color = color + + def change_thickness(self, event): + self.radius = self.marker_thickness.get() + + def close_marker(self, event): + logging.debug("destroy marker dialog") + self.app.toolbar.marker_tool = None diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 96b317c2..0769380a 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -406,8 +406,9 @@ class Toolbar(ttk.Frame): self.app.canvas.mode = GraphMode.ANNOTATION self.app.canvas.annotation_type = shape_type if is_marker(shape_type): - self.marker_tool = Marker(self.master, self.app) - self.marker_tool.show() + if not self.marker_tool: + self.marker_tool = Marker(self.master, self.app) + self.marker_tool.show() def click_run_button(self): logging.debug("Click on RUN button") From 1884cda27188d5f05bf741269ec05fb042b174e2 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 18 Dec 2019 16:51:05 -0800 Subject: [PATCH 303/305] consolidated logic for drawing edge labels based on link to be contained within the edge class itself --- coretk/coretk/coreclient.py | 8 +-- coretk/coretk/dialogs/linkconfig.py | 4 +- coretk/coretk/graph/edges.py | 71 +++++++++++++++++++++++++-- coretk/coretk/graph/graph.py | 14 +++--- coretk/coretk/graph/linkinfo.py | 75 ----------------------------- coretk/coretk/graph/node.py | 2 +- 6 files changed, 80 insertions(+), 94 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 8b01a468..320c84e0 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -445,7 +445,7 @@ class CoreClient: def start_session(self): nodes = [x.core_node for x in self.canvas_nodes.values()] - links = list(self.links.values()) + links = [x.link for x in self.links.values()] wlan_configs = self.get_wlan_configs_proto() mobility_configs = self.get_mobility_configs_proto() emane_model_configs = self.get_emane_model_configs_proto() @@ -602,7 +602,7 @@ class CoreClient: :return: nothing """ node_protos = [x.core_node for x in self.canvas_nodes.values()] - link_protos = list(self.links.values()) + link_protos = [x.link for x in self.links.values()] if self.state != core_pb2.SessionState.DEFINITION: self.client.set_session_state( self.session_id, core_pb2.SessionState.DEFINITION @@ -813,8 +813,8 @@ class CoreClient: interface_one=src_interface, interface_two=dst_interface, ) - self.links[edge.token] = link - return link + edge.set_link(link) + self.links[edge.token] = edge def get_wlan_configs_proto(self): configs = [] diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index 6f0bd51a..f96765dc 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -228,7 +228,7 @@ class LinkConfiguration(Dialog): logging.debug("click apply") self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) - link = self.edge.link_info.link + link = self.edge.link bandwidth = self.bandwidth.get() if bandwidth != "": link.options.bandwidth = int(bandwidth) @@ -292,7 +292,7 @@ class LinkConfiguration(Dialog): self.width.set(width) color = self.app.canvas.itemcget(self.edge.id, "fill") self.color.set(color) - link = self.edge.link_info.link + link = self.edge.link if link.HasField("options"): self.bandwidth.set(str(link.options.bandwidth)) self.jitter.set(str(link.options.jitter)) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index ab9447be..80193cc8 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -1,11 +1,14 @@ import logging import tkinter as tk +from tkinter.font import Font from coretk import themes from coretk.dialogs.linkconfig import LinkConfiguration from coretk.graph import tags from coretk.nodeutils import NodeUtils +TEXT_DISTANCE = 0.30 + class CanvasWirelessEdge: def __init__(self, token, position, src, dst, canvas): @@ -46,14 +49,74 @@ class CanvasEdge: self.id = self.canvas.create_line( x1, y1, x2, y2, tags=tags.EDGE, width=self.width, fill="#ff0000" ) + self.text_src = None + self.text_dst = None self.token = None - self.link_info = None + self.font = Font(size=8) + self.link = None self.throughput = None self.set_binding() def set_binding(self): self.canvas.tag_bind(self.id, "", self.create_context) + def set_link(self, link): + self.link = link + self.draw_labels() + + def get_coordinates(self): + x1, y1, x2, y2 = self.canvas.coords(self.id) + v1 = x2 - x1 + v2 = y2 - y1 + ux = TEXT_DISTANCE * v1 + uy = TEXT_DISTANCE * v2 + x1 = x1 + ux + y1 = y1 + uy + x2 = x2 - ux + y2 = y2 - uy + return x1, y1, x2, y2 + + def draw_labels(self): + x1, y1, x2, y2 = self.get_coordinates() + label_one = None + if self.link.HasField("interface_one"): + label_one = ( + f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n" + f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n" + ) + label_two = None + if self.link.HasField("interface_two"): + label_two = ( + f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" + f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" + ) + self.text_src = self.canvas.create_text( + x1, + y1, + text=label_one, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + self.text_dst = self.canvas.create_text( + x2, + y2, + text=label_two, + justify=tk.CENTER, + font=self.font, + tags=tags.LINK_INFO, + ) + + def update_labels(self): + """ + Move edge labels based on current position. + + :return: nothing + """ + x1, y1, x2, y2 = self.get_coordinates() + self.canvas.coords(self.text_src, x1, y1) + self.canvas.coords(self.text_dst, x2, y2) + def complete(self, dst): self.dst = dst self.token = tuple(sorted((self.src, self.dst))) @@ -93,9 +156,9 @@ class CanvasEdge: def delete(self): self.canvas.delete(self.id) - if self.link_info: - self.canvas.delete(self.link_info.id1) - self.canvas.delete(self.link_info.id2) + if self.link: + self.canvas.delete(self.text_src) + self.canvas.delete(self.text_dst) def create_context(self, event): logging.debug("create link context") diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index 77cc2264..c448ecd1 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -9,7 +9,7 @@ from coretk.dialogs.shapemod import ShapeDialog from coretk.graph import tags from coretk.graph.edges import CanvasEdge, CanvasWirelessEdge from coretk.graph.enums import GraphMode, ScaleOption -from coretk.graph.linkinfo import LinkInfo, Throughput +from coretk.graph.linkinfo import Throughput from coretk.graph.node import CanvasNode from coretk.graph.shape import Shape from coretk.graph.shapeutils import ShapeType, is_draw_shape @@ -237,14 +237,14 @@ class CanvasGraph(tk.Canvas): canvas_node_one.id, self, ) + edge.set_link(link) edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) edge.dst = canvas_node_two.id edge.check_wireless() canvas_node_one.edges.add(edge) canvas_node_two.edges.add(edge) self.edges[edge.token] = edge - self.core.links[edge.token] = link - edge.link_info = LinkInfo(self, edge, link) + self.core.links[edge.token] = edge if link.HasField("interface_one"): canvas_node_one.interfaces.append(link.interface_one) if link.HasField("interface_two"): @@ -372,8 +372,7 @@ class CanvasGraph(tk.Canvas): node_src.edges.add(edge) node_dst = self.nodes[edge.dst] node_dst.edges.add(edge) - link = self.core.create_link(edge, node_src, node_dst) - edge.link_info = LinkInfo(self, edge, link) + self.core.create_link(edge, node_src, node_dst) def select_object(self, object_id, choose_multiple=False): """ @@ -793,7 +792,7 @@ class CanvasGraph(tk.Canvas): :param CanvasNode dest: destination node :return: nothing """ - if tuple([source.id, dest.id]) not in self.edges: + if (source.id, dest.id) not in self.edges: pos0 = source.core_node.position x0 = pos0.x y0 = pos0.y @@ -802,5 +801,4 @@ class CanvasGraph(tk.Canvas): self.edges[edge.token] = edge self.nodes[source.id].edges.add(edge) self.nodes[dest.id].edges.add(edge) - link = self.core.create_link(edge, source, dest) - edge.link_info = LinkInfo(self, edge, link) + self.core.create_link(edge, source, dest) diff --git a/coretk/coretk/graph/linkinfo.py b/coretk/coretk/graph/linkinfo.py index decca3bb..de4b2c6e 100644 --- a/coretk/coretk/graph/linkinfo.py +++ b/coretk/coretk/graph/linkinfo.py @@ -1,83 +1,8 @@ """ Link information, such as IPv4, IPv6 and throughput drawn in the canvas """ -import tkinter as tk -from tkinter import font from core.api.grpc import core_pb2 -from coretk.graph import tags - -TEXT_DISTANCE = 0.30 - - -class LinkInfo: - def __init__(self, canvas, edge, link): - """ - create an instance of LinkInfo object - :param coretk.graph.Graph canvas: canvas object - :param coretk.graph.CanvasEdge edge: canvas edge onject - :param link: core link to draw info for - """ - self.canvas = canvas - self.edge = edge - self.link = link - self.id1 = None - self.id2 = None - self.font = font.Font(size=8) - self.draw_labels() - - def get_coordinates(self): - x1, y1, x2, y2 = self.canvas.coords(self.edge.id) - v1 = x2 - x1 - v2 = y2 - y1 - ux = TEXT_DISTANCE * v1 - uy = TEXT_DISTANCE * v2 - x1 = x1 + ux - y1 = y1 + uy - x2 = x2 - ux - y2 = y2 - uy - return x1, y1, x2, y2 - - def draw_labels(self): - x1, y1, x2, y2 = self.get_coordinates() - label_one = None - if self.link.HasField("interface_one"): - label_one = ( - f"{self.link.interface_one.ip4}/{self.link.interface_one.ip4mask}\n" - f"{self.link.interface_one.ip6}/{self.link.interface_one.ip6mask}\n" - ) - label_two = None - if self.link.HasField("interface_two"): - label_two = ( - f"{self.link.interface_two.ip4}/{self.link.interface_two.ip4mask}\n" - f"{self.link.interface_two.ip6}/{self.link.interface_two.ip6mask}\n" - ) - self.id1 = self.canvas.create_text( - x1, - y1, - text=label_one, - justify=tk.CENTER, - font=self.font, - tags=tags.LINK_INFO, - ) - self.id2 = self.canvas.create_text( - x2, - y2, - text=label_two, - justify=tk.CENTER, - font=self.font, - tags=tags.LINK_INFO, - ) - - def recalculate_info(self): - """ - move the node info when the canvas node move - - :return: nothing - """ - x1, y1, x2, y2 = self.get_coordinates() - self.canvas.coords(self.id1, x1, y1) - self.canvas.coords(self.id2, x2, y2) class Throughput: diff --git a/coretk/coretk/graph/node.py b/coretk/coretk/graph/node.py index 4836d2d2..a5893ff3 100644 --- a/coretk/coretk/graph/node.py +++ b/coretk/coretk/graph/node.py @@ -129,7 +129,7 @@ class CanvasNode: else: self.canvas.coords(edge.id, x1, y1, x, y) self.canvas.throughput_draw.move(edge) - edge.link_info.recalculate_info() + edge.update_labels() for edge in self.wireless_edges: x1, y1, x2, y2 = self.canvas.coords(edge.id) From 8eb4df7b1d0c8af17684141f8aee74b5546978bb Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 18 Dec 2019 22:09:00 -0800 Subject: [PATCH 304/305] updated linkconfig to support asymmetric links, updated grpc start session to provide asymmetric links, since they currently depend on being processed as a link edit --- coretk/coretk/coreclient.py | 4 + coretk/coretk/dialogs/linkconfig.py | 127 +++++++++++++++++++------- coretk/coretk/graph/edges.py | 1 + coretk/coretk/graph/graph.py | 51 ++++++----- daemon/core/api/grpc/client.py | 3 + daemon/core/api/grpc/grpcutils.py | 25 ++++- daemon/core/api/grpc/server.py | 3 + daemon/core/nodes/base.py | 1 + daemon/core/nodes/network.py | 1 + daemon/proto/core/api/grpc/core.proto | 1 + 10 files changed, 162 insertions(+), 55 deletions(-) diff --git a/coretk/coretk/coreclient.py b/coretk/coretk/coreclient.py index 320c84e0..8f2d479d 100644 --- a/coretk/coretk/coreclient.py +++ b/coretk/coretk/coreclient.py @@ -452,6 +452,9 @@ class CoreClient: hooks = list(self.hooks.values()) service_configs = self.get_service_configs_proto() file_configs = self.get_service_file_configs_proto() + asymmetric_links = [ + x.asymmetric_link for x in self.links.values() if x.asymmetric_link + ] if self.emane_config: emane_config = {x: self.emane_config[x].value for x in self.emane_config} else: @@ -471,6 +474,7 @@ class CoreClient: mobility_configs, service_configs, file_configs, + asymmetric_links, ) self.set_metadata() process_time = time.perf_counter() - start diff --git a/coretk/coretk/dialogs/linkconfig.py b/coretk/coretk/dialogs/linkconfig.py index f96765dc..cf0daafc 100644 --- a/coretk/coretk/dialogs/linkconfig.py +++ b/coretk/coretk/dialogs/linkconfig.py @@ -5,17 +5,34 @@ import logging import tkinter as tk from tkinter import ttk +from core.api.grpc import core_pb2 from coretk.dialogs.colorpicker import ColorPicker from coretk.dialogs.dialog import Dialog from coretk.themes import PADX, PADY +def get_int(var): + value = var.get() + if value != "": + return int(value) + else: + return None + + +def get_float(var): + value = var.get() + if value != "": + return float(value) + else: + return None + + class LinkConfiguration(Dialog): def __init__(self, master, app, edge): super().__init__(master, app, "Link Configuration", modal=True) self.app = app self.edge = edge - self.is_symmetric = True + self.is_symmetric = edge.link.options.unidirectional is False if self.is_symmetric: self.symmetry_var = tk.StringVar(value=">>") else: @@ -26,16 +43,17 @@ class LinkConfiguration(Dialog): self.jitter = tk.StringVar() self.loss = tk.StringVar() self.duplicate = tk.StringVar() + + self.down_bandwidth = tk.StringVar() + self.down_delay = tk.StringVar() + self.down_jitter = tk.StringVar() + self.down_loss = tk.StringVar() + self.down_duplicate = tk.StringVar() + self.color = tk.StringVar(value="#000000") self.color_button = None self.width = tk.DoubleVar() - self.down_bandwidth = tk.IntVar(value="") - self.down_delay = tk.IntVar(value="") - self.down_jitter = tk.IntVar(value="") - self.down_loss = tk.DoubleVar(value="") - self.down_duplicate = tk.IntVar(value="") - self.load_link_config() self.symmetric_frame = None self.asymmetric_frame = None @@ -113,7 +131,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="nsew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Delay (us)") @@ -132,7 +150,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Jitter (us)") @@ -151,7 +169,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Loss (%)") @@ -170,7 +188,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_float, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Duplicate (%)") @@ -189,7 +207,7 @@ class LinkConfiguration(Dialog): validate="key", validatecommand=(self.app.validation.positive_int, "%P"), ) - entry.grid(row=row, column=2, sticky="ew") + entry.grid(row=row, column=2, sticky="ew", pady=PADY) row = row + 1 label = ttk.Label(frame, text="Color") @@ -229,29 +247,56 @@ class LinkConfiguration(Dialog): self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) link = self.edge.link - bandwidth = self.bandwidth.get() - if bandwidth != "": - link.options.bandwidth = int(bandwidth) - jitter = self.jitter.get() - if jitter != "": - link.options.jitter = int(jitter) - delay = self.delay.get() - if delay != "": - link.options.delay = int(delay) - duplicate = self.duplicate.get() - if duplicate != "": - link.options.dup = int(duplicate) - loss = self.loss.get() - if loss != "": - link.options.per = float(loss) + bandwidth = get_int(self.bandwidth) + jitter = get_int(self.jitter) + delay = get_int(self.delay) + duplicate = get_int(self.duplicate) + loss = get_float(self.loss) + options = core_pb2.LinkOptions( + bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, per=loss + ) + link.options.CopyFrom(options) + + interface_one = None + if link.HasField("interface_one"): + interface_one = link.interface_one.id + interface_two = None + if link.HasField("interface_two"): + interface_two = link.interface_two.id + + if not self.is_symmetric: + link.options.unidirectional = True + asym_interface_one = None + if interface_one: + asym_interface_one = core_pb2.Interface(id=interface_one) + asym_interface_two = None + if interface_two: + asym_interface_two = core_pb2.Interface(id=interface_two) + down_bandwidth = get_int(self.down_bandwidth) + down_jitter = get_int(self.down_jitter) + down_delay = get_int(self.down_delay) + down_duplicate = get_int(self.down_duplicate) + down_loss = get_float(self.down_loss) + options = core_pb2.LinkOptions( + bandwidth=down_bandwidth, + jitter=down_jitter, + delay=down_delay, + dup=down_duplicate, + per=down_loss, + unidirectional=True, + ) + self.edge.asymmetric_link = core_pb2.Link( + node_one_id=link.node_two_id, + node_two_id=link.node_one_id, + interface_one=asym_interface_one, + interface_two=asym_interface_two, + options=options, + ) + else: + link.options.unidirectional = False + self.edge.asymmetric_link = None if self.app.core.is_runtime() and link.HasField("options"): - interface_one = None - if link.HasField("interface_one"): - interface_one = link.interface_one.id - interface_two = None - if link.HasField("interface_two"): - interface_two = link.interface_two.id session_id = self.app.core.session_id self.app.core.client.edit_link( session_id, @@ -261,6 +306,15 @@ class LinkConfiguration(Dialog): interface_one, interface_two, ) + if self.edge.asymmetric_link: + self.app.core.client.edit_link( + session_id, + link.node_two_id, + link.node_one_id, + self.edge.asymmetric_link.options, + interface_one, + interface_two, + ) self.destroy() @@ -299,3 +353,10 @@ class LinkConfiguration(Dialog): self.duplicate.set(str(link.options.dup)) self.loss.set(str(link.options.per)) self.delay.set(str(link.options.delay)) + if not self.is_symmetric: + asym_link = self.edge.asymmetric_link + self.down_bandwidth.set(str(asym_link.options.bandwidth)) + self.down_jitter.set(str(asym_link.options.jitter)) + self.down_duplicate.set(str(asym_link.options.dup)) + self.down_loss.set(str(asym_link.options.per)) + self.down_delay.set(str(asym_link.options.delay)) diff --git a/coretk/coretk/graph/edges.py b/coretk/coretk/graph/edges.py index 80193cc8..e25a5305 100644 --- a/coretk/coretk/graph/edges.py +++ b/coretk/coretk/graph/edges.py @@ -54,6 +54,7 @@ class CanvasEdge: self.token = None self.font = Font(size=8) self.link = None + self.asymmetric_link = None self.throughput = None self.set_binding() diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index c448ecd1..1d05d7f5 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -201,12 +201,12 @@ class CanvasGraph(tk.Canvas): """ # draw existing nodes for core_node in session.nodes: - logging.info("drawing core node: %s", core_node) # peer to peer node is not drawn on the GUI if NodeUtils.is_ignore_node(core_node.type): continue # draw nodes on the canvas + logging.info("drawing core node: %s", core_node) image = NodeUtils.node_icon(core_node.type, core_node.model) if core_node.icon: try: @@ -222,33 +222,42 @@ class CanvasGraph(tk.Canvas): # draw existing links for link in session.links: + logging.info("drawing link: %s", link) canvas_node_one = self.core.canvas_nodes[link.node_one_id] node_one = canvas_node_one.core_node canvas_node_two = self.core.canvas_nodes[link.node_two_id] node_two = canvas_node_two.core_node + token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) + if link.type == core_pb2.LinkType.WIRELESS: self.add_wireless_edge(canvas_node_one, canvas_node_two) else: - edge = CanvasEdge( - node_one.position.x, - node_one.position.y, - node_two.position.x, - node_two.position.y, - canvas_node_one.id, - self, - ) - edge.set_link(link) - edge.token = tuple(sorted((canvas_node_one.id, canvas_node_two.id))) - edge.dst = canvas_node_two.id - edge.check_wireless() - canvas_node_one.edges.add(edge) - canvas_node_two.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = edge - if link.HasField("interface_one"): - canvas_node_one.interfaces.append(link.interface_one) - if link.HasField("interface_two"): - canvas_node_two.interfaces.append(link.interface_two) + if token not in self.edges: + edge = CanvasEdge( + node_one.position.x, + node_one.position.y, + node_two.position.x, + node_two.position.y, + canvas_node_one.id, + self, + ) + edge.token = token + edge.dst = canvas_node_two.id + edge.set_link(link) + edge.check_wireless() + canvas_node_one.edges.add(edge) + canvas_node_two.edges.add(edge) + self.edges[edge.token] = edge + self.core.links[edge.token] = edge + if link.HasField("interface_one"): + canvas_node_one.interfaces.append(link.interface_one) + if link.HasField("interface_two"): + canvas_node_two.interfaces.append(link.interface_two) + elif link.options.unidirectional: + edge = self.edges[token] + edge.asymmetric_link = link + else: + logging.error("duplicate link received: %s", link) # raise the nodes so they on top of the links self.tag_raise(tags.NODE) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index bc48c9ab..9aa99349 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -161,6 +161,7 @@ class CoreGrpcClient: mobility_configs=None, service_configs=None, service_file_configs=None, + asymmetric_links=None, ): """ Start a session. @@ -176,6 +177,7 @@ class CoreGrpcClient: :param list mobility_configs: node mobility configurations :param list service_configs: node service configurations :param list service_file_configs: node service file configurations + :param list asymmetric_links: asymmetric links to edit :return: start session response :rtype: core_pb2.StartSessionResponse """ @@ -191,6 +193,7 @@ class CoreGrpcClient: mobility_configs=mobility_configs, service_configs=service_configs, service_file_configs=service_file_configs, + asymmetric_links=asymmetric_links, ) return self.stub.StartSession(request) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 4ea752fd..a3b25541 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -128,7 +128,7 @@ def create_nodes(session, node_protos): def create_links(session, link_protos): """ - Create nodes using a thread pool and wait for completion. + Create links using a thread pool and wait for completion. :param core.emulator.session.Session session: session to create nodes in :param list[core_pb2.Link] link_protos: link proto messages @@ -149,6 +149,29 @@ def create_links(session, link_protos): return results, exceptions +def edit_links(session, link_protos): + """ + Edit links using a thread pool and wait for completion. + + :param core.emulator.session.Session session: session to create nodes in + :param list[core_pb2.Link] link_protos: link proto messages + :return: results and exceptions for created links + :rtype: tuple + """ + funcs = [] + for link_proto in link_protos: + node_one_id = link_proto.node_one_id + node_two_id = link_proto.node_two_id + interface_one, interface_two, options = add_link_data(link_proto) + args = (node_one_id, node_two_id, interface_one.id, interface_two.id, options) + funcs.append((session.update_link, args, {})) + start = time.monotonic() + results, exceptions = utils.threadpool(funcs) + total = time.monotonic() - start + logging.debug("grpc edit links time: %s", total) + return results, exceptions + + def convert_value(value): """ Convert value into string. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 068d3eeb..bf2a8ac8 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -158,6 +158,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): # create links grpcutils.create_links(session, request.links) + # asymmetric links + grpcutils.edit_links(session, request.asymmetric_links) + # set to instantiation and start session.set_state(EventTypes.INSTANTIATION_STATE) session.instantiate() diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index a663741e..3193d954 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -1053,6 +1053,7 @@ class CoreNetworkBase(NodeBase): message_type=0, node1_id=linked_node.id, node2_id=self.id, + link_type=self.linktype, unidirectional=1, delay=netif.getparam("delay"), bandwidth=netif.getparam("bw"), diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index f0639649..5342215b 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -965,6 +965,7 @@ class PtpNet(CoreNetwork): if unidirectional: link_data = LinkData( message_type=0, + link_type=self.linktype, node1_id=if2.node.id, node2_id=if1.node.id, delay=if2.getparam("delay"), diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ec7bf49c..050d5e75 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -150,6 +150,7 @@ message StartSessionRequest { repeated MobilityConfig mobility_configs = 9; repeated ServiceConfig service_configs = 10; repeated ServiceFileConfig service_file_configs = 11; + repeated Link asymmetric_links = 12; } message StartSessionResponse { From 43c8b9e285cae182b6ab233a577d366d44a15f04 Mon Sep 17 00:00:00 2001 From: Huy Pham <42948410+hpham@users.noreply.github.com> Date: Thu, 19 Dec 2019 08:46:21 -0800 Subject: [PATCH 305/305] marker tool --- coretk/coretk/data/icons/markerclear.png | Bin 0 -> 1370 bytes coretk/coretk/dialogs/marker.py | 22 ++++++++++++++++++---- coretk/coretk/graph/graph.py | 9 +++++---- coretk/coretk/toolbar.py | 1 + 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 coretk/coretk/data/icons/markerclear.png diff --git a/coretk/coretk/data/icons/markerclear.png b/coretk/coretk/data/icons/markerclear.png new file mode 100644 index 0000000000000000000000000000000000000000..6f58c0059fa6447d91b1bbeb9d9153ab22af4e95 GIT binary patch literal 1370 zcmV-g1*Q6lP)Kcxg%+F)2cF(Mz(F z;MuF7f>zU}RC+MPG-*Kui5DSWgj#DNf|tZY^N8;_SrPb>p z+fhS|jipL!x2FN|W|#q34~bcKV0D}>!c!@CRQu)dA0X&V>D#_3)pDg7aDVvZX%ga^ zYNyQz0eN?MWkI1%4l#DDeSOXSRRD)%Ynzgom@BZiVV+P^h*??L9B8FajM4oIaE>C?#_hLby-NMDUe>+GDm_aJj& z8qR@(a1I;<0F*y?52)2*>(lAUAu~P+IE0O<^YCu|7F%C|#-pp_?cw{R`v=kS>e<+4 zg42^jW?~AI&4M~zWX?^($sLKMufSu{(dka%`@|(-d-(Nn7$6~W0lS6o6Sah$!mp1< zvxLM2w7r<$HGH3R{~$VEc`NkQ(Qe`U=#-GSfXdZ*Y~EN7^x5=t&xg7?3KIUq2&I0) zko-Bo^H83<99)9p;YcQYv0lQ))C9bnzf1KI1$wT6VwPY__<+tEv6v*-6h5H)PB>x- zwuKMq763#h!N%|b69fRECD|lBA23M( zpd=xE;#nZw_-mLLK48irXgiXJBb&nqOcVgfOV|aI!v{LnavSKLhw_KB zfoFnu`xYRQeLggC0p<568~!RK(#@2Bhoj1;AEUZ>HGF@PBraep+(-X3D3+b91`>3I z4`@0GKT1NZ+lV_!5CWC?Ic?zs8ZSftFCmdV-0c#CK;@HI=GsjW;`G?4x&MvP=WvX33vmq-$`&v+|!vZwJ0yM({G{XWk zqw>P;{aw<>zjy6?!d}~YH{ct=+aQ%CNAXP^ju27sD_N!!QiPFbu;m c3}X-f0nFy52u5{MYybcN07*qoM6N<$f=T6tMF0Q* literal 0 HcmV?d00001 diff --git a/coretk/coretk/dialogs/marker.py b/coretk/coretk/dialogs/marker.py index feb8bd09..5648a89b 100644 --- a/coretk/coretk/dialogs/marker.py +++ b/coretk/coretk/dialogs/marker.py @@ -24,10 +24,14 @@ class Marker(Dialog): def draw(self): button = ttk.Button(self.top, text="clear", command=self.clear_marker) - button.grid(row=0, column=0) + button.grid(row=0, column=0, sticky="nsew") frame = ttk.Frame(self.top) - frame.grid(row=1, column=0) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=1, column=0, sticky="nsew") + label = ttk.Label(frame, text="Thickness: ") + label.grid(row=0, column=0, sticky="nsew") combobox = ttk.Combobox( frame, textvariable=self.marker_thickness, @@ -36,8 +40,14 @@ class Marker(Dialog): ) combobox.grid(row=0, column=1, sticky="nsew") combobox.bind("<>", self.change_thickness) - label = ttk.Label(self.top, background=self.color) - label.grid(row=2, column=0, sticky="nsew") + frame = ttk.Frame(self.top) + frame.columnconfigure(0, weight=1) + frame.columnconfigure(1, weight=4) + frame.grid(row=2, column=0, sticky="nsew") + label = ttk.Label(frame, text="Color: ") + label.grid(row=0, column=0, sticky="nsew") + label = ttk.Label(frame, background=self.color) + label.grid(row=0, column=1, sticky="nsew") label.bind("", self.change_color) def clear_marker(self): @@ -57,3 +67,7 @@ class Marker(Dialog): def close_marker(self, event): logging.debug("destroy marker dialog") self.app.toolbar.marker_tool = None + + def position(self): + print(self.winfo_width(), self.winfo_height()) + self.geometry("+{}+{}".format(self.app.master.winfo_x, self.app.master.winfo_y)) diff --git a/coretk/coretk/graph/graph.py b/coretk/coretk/graph/graph.py index e0878f20..996c85eb 100644 --- a/coretk/coretk/graph/graph.py +++ b/coretk/coretk/graph/graph.py @@ -499,7 +499,7 @@ class CanvasGraph(tk.Canvas): x, y = self.coords(selected) self.drawing_edge = CanvasEdge(x, y, x, y, selected, self) - if self.mode == GraphMode.ANNOTATION and selected is None: + if self.mode == GraphMode.ANNOTATION: if is_marker(self.annotation_type): r = self.app.toolbar.marker_tool.radius self.create_oval( @@ -511,7 +511,8 @@ class CanvasGraph(tk.Canvas): outline="", tags="marker", ) - else: + return + if selected is None: shape = Shape(self.app, self, self.annotation_type, x, y) self.selected = shape.id self.shape_drawing = True @@ -587,8 +588,7 @@ class CanvasGraph(tk.Canvas): shape = self.shapes[self.selected] shape.shape_motion(x, y) elif is_marker(self.annotation_type): - marker_tool = self.app.toolbar.marker_tool - r = marker_tool.radius + r = self.app.toolbar.marker_tool.radius self.create_oval( x - r, y - r, @@ -598,6 +598,7 @@ class CanvasGraph(tk.Canvas): outline="", tags="marker", ) + return if self.mode == GraphMode.EDGE: return diff --git a/coretk/coretk/toolbar.py b/coretk/coretk/toolbar.py index 0769380a..c32b0d75 100644 --- a/coretk/coretk/toolbar.py +++ b/coretk/coretk/toolbar.py @@ -420,6 +420,7 @@ class Toolbar(ttk.Frame): logging.debug("Click on marker button") dialog = Marker(self.master, self.app) dialog.show() + # dialog.position() def click_two_node_button(self): logging.debug("Click TWONODE button")