diff --git a/HISTORY.rst b/HISTORY.rst index a763ab5..88f8164 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,13 @@ ======= History ======= +2024.11.1 -- Enhancement: subflowcharts and menus handling flowcharts + * Added a subflowchart step to allow calling another flowchart from within a + flowchart. + * Added copy/cut/paste for flowcharts in the menus and shortcuts + * Enhanced the menus and bindings for operations on flowcharts so that they act on + the currently active flowchart or subflowchart. + 2024.10.20 -- Improvements in opening flowcharts * Removed directories and files not ending in ".flow" from the list of flowcharts that can be opened. diff --git a/devtools/conda-envs/test_env.yaml b/devtools/conda-envs/test_env.yaml index 7d07d74..189cb8c 100644 --- a/devtools/conda-envs/test_env.yaml +++ b/devtools/conda-envs/test_env.yaml @@ -19,6 +19,7 @@ dependencies: - packaging - pandas - pillow + - pyperclip - pyuca - py-cpuinfo - rdkit diff --git a/seamm/__init__.py b/seamm/__init__.py index f41fae2..26d714e 100644 --- a/seamm/__init__.py +++ b/seamm/__init__.py @@ -38,6 +38,9 @@ wrap_text = textwrap.TextWrapper(width=120) wrap_stdout = textwrap.TextWrapper(width=120) +# Tk Data +tk_data = {} + # Handle versioneer from ._version import get_versions # noqa: E402 diff --git a/seamm/__main__.py b/seamm/__main__.py index 674f837..f5aecb7 100644 --- a/seamm/__main__.py +++ b/seamm/__main__.py @@ -143,6 +143,7 @@ def flowchart(): # Initialize Tk ################################################## root = tk.Tk() + seamm.tk_data["root"] = root Pmw.initialise(root) # Capture the initial font information @@ -176,6 +177,9 @@ def flowchart(): root.title(app_name) + # Metadata about the GUI + seamm.tk_data["menus"] = {} + # The menus menu = tk.Menu(root) @@ -191,30 +195,42 @@ def flowchart(): CmdKey = "Command-" else: CmdKey = "Control-" + seamm.tk_data["CmdKey"] = CmdKey root.config(menu=menu) + seamm.tk_data["root menu"] = menu filemenu = tk.Menu(menu) menu.add_cascade(label="File", menu=filemenu) - filemenu.add_command( - label="New", command=tk_flowchart.new_file, accelerator=CmdKey + "N" - ) - filemenu.add_command( - label="Open...", command=tk_flowchart.flowchart_search, accelerator=CmdKey + "O" - ) - filemenu.add_command( - label="Save...", command=tk_flowchart.save, accelerator=CmdKey + "S" - ) - filemenu.add_command(label="Save as...", command=tk_flowchart.save_file) - filemenu.add_command(label="Publish...", command=tk_flowchart.publish) - filemenu.add_separator() - filemenu.add_command( - label="Run", command=tk_flowchart.run, accelerator=CmdKey + "R" - ) + seamm.tk_data["file menu"] = filemenu + seamm.tk_data["menus"]["File"] = menu_items = {} + menu_items["New"] = (filemenu, "new_file", "N") + menu_items["Open..."] = (filemenu, "flowchart_search", "O") + menu_items["Save"] = (filemenu, "save", "S") + menu_items["Save as..."] = (filemenu, "save_file", "") + menu_items["Publish..."] = (filemenu, "publish", "") + menu_items["sep1"] = (filemenu, "", "") + menu_items["Run"] = (filemenu, "run", "R") + + for item, (_menu, _cmd, _acc) in menu_items.items(): + if _cmd == "": + _menu.add_separator() + elif _acc == "": + _menu.add_command(label=item, command=getattr(tk_flowchart, _cmd)) + else: + _cmd = getattr(tk_flowchart, _cmd) + _menu.add_command( + label=item, + command=_cmd, + accelerator=CmdKey + _acc if _acc != "" else "", + ) + root.bind(f"<{CmdKey}{_acc.upper()}>", _cmd) + root.bind(f"<{CmdKey}{_acc.lower()}>", _cmd) # Control debugging info filemenu.add_separator() debug_menu = tk.Menu(menu) filemenu.add_cascade(label="Debug", menu=debug_menu) + seamm.tk_data["debug menu"] = debug_menu debug_menu.add_radiobutton( label="normal", value=30, @@ -241,16 +257,35 @@ def flowchart(): # Edit menu editmenu = tk.Menu(menu) menu.add_cascade(label="Edit", menu=editmenu) - editmenu.add_command(label="Description...", command=tk_flowchart.properties) - editmenu.add_command( - label="Clean layout", - command=tk_flowchart.clean_layout, - accelerator=CmdKey + "l", - ) + seamm.tk_data["edit menu"] = editmenu + seamm.tk_data["menus"]["Edit"] = menu_items = {} + menu_items["Description..."] = (editmenu, "properties", "") + menu_items["sep1"] = (editmenu, "", "") + menu_items["Cut"] = (editmenu, "cut", "X") + menu_items["Copy"] = (editmenu, "copy_to_clipboard", "C") + menu_items["Paste"] = (editmenu, "paste_from_clipboard", "V") + menu_items["sep2"] = (editmenu, "", "") + menu_items["Clean layout"] = (editmenu, "clean_layout", "L") + + for item, (_menu, _cmd, _acc) in menu_items.items(): + if _cmd == "": + _menu.add_separator() + elif _acc == "": + _menu.add_command(label=item, command=getattr(tk_flowchart, _cmd)) + else: + _cmd = getattr(tk_flowchart, _cmd) + _menu.add_command( + label=item, + command=_cmd, + accelerator=CmdKey + _acc if _acc != "" else "", + ) + root.bind(f"<{CmdKey}{_acc.upper()}>", _cmd) + root.bind(f"<{CmdKey}{_acc.lower()}>", _cmd) # View menu viewmenu = tk.Menu(menu) menu.add_cascade(label="View", menu=viewmenu) + seamm.tk_data["view menu"] = viewmenu viewmenu.add_command( label="Increase font size", command=increase_font_size, accelerator=CmdKey + "+" ) @@ -262,19 +297,11 @@ def flowchart(): # Help menu helpmenu = tk.Menu(menu) menu.add_cascade(label="Help", menu=helpmenu) + seamm.tk_data["help menu"] = helpmenu if sys.platform.startswith("darwin"): root.createcommand("tk::mac::ShowHelp", tk_flowchart.help) - root.bind_all("<" + CmdKey + "N>", tk_flowchart.new_file) - root.bind_all("<" + CmdKey + "n>", tk_flowchart.new_file) - root.bind_all("<" + CmdKey + "O>", tk_flowchart.flowchart_search) - root.bind_all("<" + CmdKey + "o>", tk_flowchart.flowchart_search) - root.bind_all("<" + CmdKey + "R>", tk_flowchart.run) - root.bind_all("<" + CmdKey + "r>", tk_flowchart.run) - root.bind_all("<" + CmdKey + "S>", tk_flowchart.save) - root.bind_all("<" + CmdKey + "s>", tk_flowchart.save) - root.bind_all("<" + CmdKey + "L>", tk_flowchart.clean_layout) - root.bind_all("<" + CmdKey + "l>", tk_flowchart.clean_layout) + # special bindings root.bind_all("<" + CmdKey + "plus>", increase_font_size) root.bind_all("<" + CmdKey + "equal>", increase_font_size) root.bind_all("<" + CmdKey + "minus>", decrease_font_size) @@ -321,6 +348,13 @@ def handle_dbg_level(level): logging.getLogger().setLevel(dbg_level) +def pevent(event): + print(f"pevent: {event}") + print(f" : {event.widget}") + print(f" : {event.char}") + print(f" : {event.keysym}") + + if __name__ == "__main__": locale.setlocale(locale.LC_ALL, "") diff --git a/seamm/flowchart.py b/seamm/flowchart.py index 691c1cc..503c71d 100644 --- a/seamm/flowchart.py +++ b/seamm/flowchart.py @@ -15,6 +15,7 @@ import stat from packaging.version import Version +import pyperclip import seamm from .seammrc import SEAMMrc @@ -468,6 +469,14 @@ def write(self, filename): permissions = stat.S_IMODE(os.lstat(filename).st_mode) os.chmod(filename, permissions | stat.S_IXUSR | stat.S_IXGRP) + def from_clipboard(self): + """Read the flowchart from the clipboard""" + self.from_text(pyperclip.paste()) + + def to_clipboard(self): + """Copy the flowchart to the clipboard""" + pyperclip.copy(self.to_text()) + def to_text(self): """Return the text for the flowchart. diff --git a/seamm/node.py b/seamm/node.py index 688d9a8..be08082 100644 --- a/seamm/node.py +++ b/seamm/node.py @@ -533,6 +533,7 @@ def get_system_configuration( system_db = self.get_variable("_system_db") system = system_db.system + print(f"system = {system}") if system is None: configuration = None else: @@ -740,16 +741,6 @@ def digest(self, strict=False): elif key == "parameters": if self.parameters is not None: hasher.update(bytes(str(self.parameters.to_dict()), "utf-8")) - else: - if self.__class__.__name__ not in ( - "StartNode", - "LAMMPS", - "MOPAC", - "Psi4", - "Join", - "Table", - ): - print(f"{self.__class__.__name__} has no parameters") return hasher.hexdigest() diff --git a/seamm/tk_flowchart.py b/seamm/tk_flowchart.py index 67b275e..bb0fba7 100644 --- a/seamm/tk_flowchart.py +++ b/seamm/tk_flowchart.py @@ -93,7 +93,7 @@ def __init__(self, master=None, flowchart=None, namespace="org.molssi.seamm.tk") self.data = None self._x0 = None self._y0 = None - self.selection = None + self.selection = [] self.active_nodes = [] self.in_callback = False self.canvas_after_callback = None @@ -158,11 +158,6 @@ def __init__(self, master=None, flowchart=None, namespace="org.molssi.seamm.tk") h = int(factor * h) self.working_image = self.prepared_image.resize((w, h)) self.photo = ImageTk.PhotoImage(self.working_image) - # self.background = self.canvas.create_image( - # self.canvas_width / 2, - # self.canvas_height / 2, - # image=self.photo, - # anchor='center') self.background = self.canvas.create_image( 0, 0, image=self.photo, anchor="center" ) @@ -402,6 +397,21 @@ def properties(self): start_node = self.get_node("1") start_node.edit() + def copy_to_clipboard(self, event=None): + """Copy the flowchart to the clipboard.""" + self.update_flowchart() + self.flowchart.to_clipboard() + + def cut(self, event=None): + """Cut the flowchart to the clipboard.""" + self.copy_to_clipboard() + self.clear() + + def paste_from_clipboard(self, event=None): + """Paste the flowchart from the clipboard.""" + self.flowchart.from_clipboard() + self.from_flowchart() + def publish(self, event=None): """Publish the flowchart to a repository such as Zenodo.""" self.update_flowchart() @@ -636,7 +646,7 @@ def end_move(self, event): self._x0 = None self._y0 = None self.mouse_op = None - self.selection = None + self.selection = [] def create_node(self, event): """Create a node using the type in menu. This is a bit tricky because diff --git a/seamm/tk_node.py b/seamm/tk_node.py index 99d0832..01fdde5 100644 --- a/seamm/tk_node.py +++ b/seamm/tk_node.py @@ -159,6 +159,7 @@ def __init__( self._widget = {} self.tk_var = {} self.results_widgets = None + self._gui_data = {} # Because the default for saving properties in the database is True # we need to initialize the results to include them by default @@ -565,6 +566,38 @@ def edit(self): if self.tk_subflowchart is not None: self.tk_subflowchart.push() + # And update the menu items as needed + root = seamm.tk_data["root"] + CmdKey = seamm.tk_data["CmdKey"] + file_menu = seamm.tk_data["file menu"] + + # Disable the Run menu item + self._gui_data["Run state"] = file_menu.entrycget("Run", "state") + file_menu.entryconfigure("Run", state=tk.DISABLED) + + # Menu items and bindings + _w = self.tk_subflowchart.pw.winfo_toplevel() + for name in ("File", "Edit"): + for item, (_menu, _cmd, _acc) in seamm.tk_data["menus"][name].items(): + if _cmd == "": + continue + self._gui_data[f"{item} command"] = _menu.entrycget(item, "command") + self._gui_data[f"{item} menu"] = _menu + _menu.entryconfigure( + item, command=getattr(self.tk_subflowchart, _cmd) + ) + if _acc != "": + u_seq = f"<{CmdKey}{_acc.upper()}>" + l_seq = f"<{CmdKey}{_acc.lower()}>" + self._gui_data[f"{_acc.upper()} binding"] = root.bind_all(u_seq) + self._gui_data[f"{_acc.lower()} binding"] = root.bind_all(l_seq) + if _acc == "R" or _acc == "r": + _w.bind(u_seq, None) + _w.bind(l_seq, None) + else: + _w.bind(u_seq, getattr(self.tk_subflowchart, _cmd)) + _w.bind(l_seq, getattr(self.tk_subflowchart, _cmd)) + self.dialog.activate(geometry="centerscreenfirst") def end_move(self, deltax, deltay): @@ -705,7 +738,37 @@ def from_flowchart(self, tk_flowchart=None, flowchart=None): self.tk_subflowchart.add_edge(node1, node2, **attr) def handle_dialog(self, result): - """Do the right thing when the dialog is closed.""" + """Handle closing the dialog. + + Parameters + ---------- + result : str + The button that was pressed to close the dialog, or None if the x dialog + close button was pressed. + """ + # First restore any menus and bindings that were changed + if self.tk_subflowchart is not None: + # Reset the menu items as needed + CmdKey = seamm.tk_data["CmdKey"] + + # Reset the menu items + for name in ("File", "Edit"): + for item, (_menu, _cmd, _acc) in seamm.tk_data["menus"][name].items(): + if _cmd == "": + continue + _menu.entryconfigure( + item, command=self._gui_data[f"{item} command"] + ) + + # Reset the bindings + if _acc == "R" or _acc == "r": + _menu.entryconfigure("Run", state=self._gui_data["Run state"]) + u_seq = f"<{CmdKey}{_acc.upper()}>" + l_seq = f"<{CmdKey}{_acc.lower()}>" + _w = self.tk_subflowchart.pw.winfo_toplevel() + _w.bind(u_seq, self._gui_data[f"{_acc.upper()} binding"]) + _w.bind(l_seq, self._gui_data[f"{_acc.lower()} binding"]) + if result is None or result == "Cancel": self.dialog.deactivate(result)