From 3fe9797577adac8b7efaf60370bb92725e560cec Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:54:47 -0500 Subject: [PATCH 01/17] Argument parsing Added -c and --configurator as input arguments that are meant to launch the configuration gui --- src/navigate/main.py | 42 ++++++++++++++++------------ src/navigate/tools/main_functions.py | 17 +++++++++++ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/navigate/main.py b/src/navigate/main.py index 034758176..c7cc5f77d 100644 --- a/src/navigate/main.py +++ b/src/navigate/main.py @@ -34,7 +34,6 @@ import tkinter as tk import platform import os -import warnings # Third Party Imports @@ -68,7 +67,7 @@ def main(): --waveform_constants-path --rest-api-file --waveform-templates-file - --logging-config + --logging-confi Returns ------- @@ -78,12 +77,14 @@ def main(): -------- >>> python main.py --synthetic-hardware """ - if platform.system() != 'Windows': - print("WARNING: navigate was built to operate on a Windows platform. " - "While much of the software will work for evaluation purposes, some " - "unanticipated behaviors may occur. For example, it is known that the " - "Tkinter-based GUI does not grid symmetrically, nor resize properly " - "on MacOS. Testing on Linux operating systems has not been performed.") + if platform.system() != "Windows": + print( + "WARNING: navigate was built to operate on a Windows platform. " + "While much of the software will work for evaluation purposes, some " + "unanticipated behaviors may occur. For example, it is known that the " + "Tkinter-based GUI does not grid symmetrically, nor resize properly " + "on MacOS. Testing on Linux operating systems has not been performed." + ) # Start the GUI, withdraw main screen, and show splash screen. root = tk.Tk() @@ -106,20 +107,25 @@ def main(): rest_api_path, waveform_templates_path, logging_path, + configurator, ) = evaluate_parser_input_arguments(args) log_setup("logging.yml", logging_path) - Controller( - root, - splash_screen, - configuration_path, - experiment_path, - waveform_constants_path, - rest_api_path, - waveform_templates_path, - args, - ) + if args.configurator: + print("Configurator") + else: + Controller( + root, + splash_screen, + configuration_path, + experiment_path, + waveform_constants_path, + rest_api_path, + waveform_templates_path, + args, + ) + root.mainloop() diff --git a/src/navigate/tools/main_functions.py b/src/navigate/tools/main_functions.py index bb271ee89..78ef28719 100644 --- a/src/navigate/tools/main_functions.py +++ b/src/navigate/tools/main_functions.py @@ -68,6 +68,8 @@ def evaluate_parser_input_arguments(args): Path to waveform templates file logging_path Path to non-default logging location + configurator + Boolean, True if configurator is enabled """ # Retrieve the Default Configuration paths ( @@ -79,6 +81,11 @@ def evaluate_parser_input_arguments(args): ) = get_configuration_paths() # Evaluate Input Arguments + if args.configurator: + configurator = True + else: + configurator = False + if args.config_file: assert args.config_file.exists(), "Configuration file Path {} not valid".format( args.config_file @@ -127,6 +134,7 @@ def evaluate_parser_input_arguments(args): rest_api_path, waveform_templates_path, logging_path, + configurator, ) @@ -146,6 +154,15 @@ def create_parser(): input_args = parser.add_argument_group("Input Arguments") + input_args.add_argument( + "-c", + "--configurator", + required=False, + default=False, + action="store_true", + help="Configurator - " "GUI for preparing a configuration.yaml file..", + ) + input_args.add_argument( "-sh", "--synthetic-hardware", From 7f3a331586e8cc172a23b88ab1ffa23287a7d719 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Sun, 17 Mar 2024 20:04:38 -0500 Subject: [PATCH 02/17] ConfiguratorController & ConfiguratorApp Adding the requisite controller and view elements. --- src/navigate/controller/configurator.py | 54 ++++++++++++ src/navigate/main.py | 3 +- .../view/configurator_application_window.py | 87 +++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/navigate/controller/configurator.py create mode 100644 src/navigate/view/configurator_application_window.py diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py new file mode 100644 index 000000000..7f4c70e94 --- /dev/null +++ b/src/navigate/controller/configurator.py @@ -0,0 +1,54 @@ +# Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center. +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only +# (subject to the limitations in the disclaimer below) +# provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * 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. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. 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 HOLDER 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. + +# Standard Library Imports + +# Third Party Imports + +# Local Imports +from navigate.view.configurator_application_window import ConfiguratorApp + +# Logger Setup +import logging + +p = __name__.split(".")[1] +logger = logging.getLogger(p) + + +class Configurator: + """Navigate Configurator""" + + def __init__(self, root, splash_screen): + # destroy splash screen and show main screen + splash_screen.destroy() + root.deiconify() + + self.view = ConfiguratorApp(root) diff --git a/src/navigate/main.py b/src/navigate/main.py index c7cc5f77d..0b15de318 100644 --- a/src/navigate/main.py +++ b/src/navigate/main.py @@ -39,6 +39,7 @@ # Local Imports from navigate.controller.controller import Controller +from navigate.controller.configurator import Configurator from navigate.log_files.log_functions import log_setup from navigate.view.splash_screen import SplashScreen from navigate.tools.main_functions import ( @@ -113,7 +114,7 @@ def main(): log_setup("logging.yml", logging_path) if args.configurator: - print("Configurator") + Configurator(root, splash_screen) else: Controller( root, diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py new file mode 100644 index 000000000..980c2eeec --- /dev/null +++ b/src/navigate/view/configurator_application_window.py @@ -0,0 +1,87 @@ +# Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * 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. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. 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 HOLDER 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. + +# Standard Library Imports +import tkinter as tk +from tkinter import ttk +import logging +from pathlib import Path + +# Third Party Imports + +# Local Imports +from navigate.view.custom_widgets.scrollbars import ScrolledFrame + +# Logger Setup +p = __name__.split(".")[1] + + +class ConfiguratorApp(ttk.Frame): + def __init__(self, root, *args, **kwargs): + """Initiates the main application window + + Parameters + ---------- + root : tk.Tk + The main window of the application + *args + Variable length argument list + **kwargs + Arbitrary keyword arguments + """ + + # Inits this class as a frame subclass with the root as its parent + #: ScrolledFrame: The scrollable version of the main frame for the application + self.scroll_frame = ScrolledFrame(root) + self.scroll_frame.grid(row=0, column=0, sticky=tk.NSEW) + + ttk.Frame.__init__(self, self.scroll_frame.interior, *args, **kwargs) + + # Initialize Logger + #: logging.Logger: The logger for this class + self.logger = logging.getLogger(p) + + # This starts the main window config, and makes sure that any child + # widgets can be resized with the window + #: tk.Tk: The main window of the application + self.root = root + self.root.title("navigate Configurator") + + view_directory = Path(__file__).resolve().parent + try: + photo_image = view_directory.joinpath("icon", "mic.png") + self.root.iconphoto(True, tk.PhotoImage(file=photo_image)) + except tk.TclError: + pass + self.root.resizable(True, True) + self.root.geometry("") + tk.Grid.columnconfigure(root, "all", weight=1) + tk.Grid.rowconfigure(root, "all", weight=1) From dd35511263ec95883e95e0d7a5dcb56eca9ff9c0 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Mon, 18 Mar 2024 06:31:14 -0500 Subject: [PATCH 03/17] Tkinter windows --- src/navigate/controller/configurator.py | 42 ++++++- .../view/configurator_application_window.py | 108 +++++++++++++++--- src/navigate/view/main_application_window.py | 11 +- 3 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 7f4c70e94..13604855a 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -30,11 +30,12 @@ # POSSIBILITY OF SUCH DAMAGE. # Standard Library Imports +from time import sleep # Third Party Imports # Local Imports -from navigate.view.configurator_application_window import ConfiguratorApp +from navigate.view.configurator_application_window import ConfigurationAssistant # Logger Setup import logging @@ -47,8 +48,41 @@ class Configurator: """Navigate Configurator""" def __init__(self, root, splash_screen): - # destroy splash screen and show main screen + """Initiates the configurator application window. + + Parameters + ---------- + root : tk.Tk + The main window of the application + splash_screen : SplashScreen + The splash screen of the application + """ + self.root = root + + # Show the splash screen for 1 second and then destroy it. + sleep(1) splash_screen.destroy() - root.deiconify() + self.root.deiconify() + self.view = ConfigurationAssistant(root) + + self.view.initial_window.continue_button.config(command=self.on_continue) + self.view.initial_window.cancel_button.config(command=self.on_cancel) + + def on_cancel(self): + """Closes the window and exits the program""" + self.root.destroy() + exit() + + def on_continue(self): + """Evaluate the number of configurations and create the configuration window""" + try: + num_configs = int(self.view.initial_window.num_configs_entry.get()) + self.create_config_window(num_configs) + except ValueError: + print("Please enter a valid number") + + def create_config_window(self, num_configs): + + # self.view = ConfigurationAssistant(self.root) - self.view = ConfiguratorApp(root) + print("Initial Window Destroyed") diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index 980c2eeec..16c79a1b2 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -38,13 +38,12 @@ # Third Party Imports # Local Imports -from navigate.view.custom_widgets.scrollbars import ScrolledFrame # Logger Setup p = __name__.split(".")[1] -class ConfiguratorApp(ttk.Frame): +class ConfigurationAssistant(ttk.Frame): def __init__(self, root, *args, **kwargs): """Initiates the main application window @@ -58,22 +57,12 @@ def __init__(self, root, *args, **kwargs): Arbitrary keyword arguments """ - # Inits this class as a frame subclass with the root as its parent - #: ScrolledFrame: The scrollable version of the main frame for the application - self.scroll_frame = ScrolledFrame(root) - self.scroll_frame.grid(row=0, column=0, sticky=tk.NSEW) - - ttk.Frame.__init__(self, self.scroll_frame.interior, *args, **kwargs) - - # Initialize Logger #: logging.Logger: The logger for this class self.logger = logging.getLogger(p) - # This starts the main window config, and makes sure that any child - # widgets can be resized with the window #: tk.Tk: The main window of the application self.root = root - self.root.title("navigate Configurator") + self.root.title("navigate Configuration Assistant") view_directory = Path(__file__).resolve().parent try: @@ -81,7 +70,94 @@ def __init__(self, root, *args, **kwargs): self.root.iconphoto(True, tk.PhotoImage(file=photo_image)) except tk.TclError: pass + self.root.resizable(True, True) - self.root.geometry("") - tk.Grid.columnconfigure(root, "all", weight=1) - tk.Grid.rowconfigure(root, "all", weight=1) + self.root.geometry("512x512") + self.root.rowconfigure(0, weight=1) + self.root.columnconfigure(0, weight=1) + + #: ttk.Frame: The top frame of the application + self.top_frame = ttk.Frame(self.root) + self.top_frame.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW) + + #: ttk.Frame: The main frame of the application + self.main_frame = ttk.Frame(self.root) + self.main_frame.grid(row=1, column=0, sticky=tk.NSEW) + + #: ttk.Frame: The top frame of the application + self.initial_window = TopFrame(self.top_frame, self.root) + + #: ttk.Frame: The main frame of the application + self.microscope_window = MicroscopeConfiguratorWindow( + self.main_frame, self.root + ) + + +class TopFrame(ttk.Frame): + """Top Frame for Configuration Assistant. + + This class is the initial window for the configurator application. + It contains the following: + - Entry for number of configurations + - Continue button + - Cancel button + """ + + def __init__(self, main_frame, root, *args, **kwargs): + """Initialize Acquire Bar. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + + #: ttk.Frame: The main frame of the application + self.main_frame = main_frame + ttk.Frame.__init__(self, self.main_frame, *args, **kwargs) + + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) + + # Entry for number of configurations + tk.Label(root, text="Number of Microscopes:").grid(row=0, column=0) + + #: tk.Entry: The entry for the number of configurations to create. + self.num_configs_entry = tk.Entry(root) + self.num_configs_entry.grid(row=0, column=1) + + #: tk.Button: The button to continue to the next window. + self.continue_button = tk.Button(root, text="Continue") + self.continue_button.grid(row=0, column=2) + + #: tk.Button: The button to cancel the application. + self.cancel_button = tk.Button(root, text="Cancel") + self.cancel_button.grid(row=0, column=3) + + +class MicroscopeConfiguratorWindow(ttk.Frame): + def __init__(self, main_frame, root, *args, **kwargs): + """Initialize Acquire Bar. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + # ttk.Frame.__init__(self, self.main_frame, *args, **kwargs) + + # Entry for number of configurations + tk.Label(root, text="Microscope Configurator").grid(row=1, column=0) diff --git a/src/navigate/view/main_application_window.py b/src/navigate/view/main_application_window.py index dc2986848..acb964282 100644 --- a/src/navigate/view/main_application_window.py +++ b/src/navigate/view/main_application_window.py @@ -59,7 +59,7 @@ class MainApp(ttk.Frame): Adds the options for each file menu. It then sets up the frames, then grids the frames. - Finally it uses the notebook classes to put them into the respective frames on the + Finally, it uses the notebook classes to put them into the respective frames on the tk.Grid. Each of the notebook classes includes tab classes and inits those etc. The second parameter in each classes __init__ function is the parent. @@ -108,12 +108,9 @@ def __init__(self, root, *args, **kwargs): ttk.Frame.__init__(self, self.scroll_frame.interior, *args, **kwargs) - # Initialize Logger #: logging.Logger: The logger for this class self.logger = logging.getLogger(p) - # This starts the main window config, and makes sure that any child - # widgets can be resized with the window #: tk.Tk: The main window of the application self.root = root self.root.title("navigate") @@ -125,8 +122,10 @@ def __init__(self, root, *args, **kwargs): self.root.iconphoto(True, tk.PhotoImage(file=photo_image)) except tk.TclError: pass + self.root.resizable(True, True) self.root.geometry("") + tk.Grid.columnconfigure(root, "all", weight=1) tk.Grid.rowconfigure(root, "all", weight=1) @@ -159,11 +158,11 @@ def __init__(self, root, *args, **kwargs): self.frame_top_right.grid(row=1, column=1, sticky=tk.NSEW, padx=3, pady=3) self.frame_bottom_right.grid(row=2, column=1, sticky=tk.NSEW, padx=3, pady=3) - # Putting Notebooks into frames, tabs are held within the class of each - # notebook #: SettingsNotebook: The settings notebook for the application self.settings = SettingsNotebook(self.frame_left, self.root) + #: CameraNotebook: The camera notebook for the application self.camera_waveform = CameraNotebook(self.frame_top_right, self.root) + #: AcquireBar: The acquire bar for the application self.acqbar = AcquireBar(self.top_frame, self.root) From 9eeca5edfd812a95933abe483c6bfa8d4711056a Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:46:30 -0500 Subject: [PATCH 04/17] Configuration assistant screen laid out a bit better --- src/navigate/controller/configurator.py | 42 ++++++--- .../view/configurator_application_window.py | 90 ++++++++++++++----- 2 files changed, 100 insertions(+), 32 deletions(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 13604855a..a30a96c51 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -30,12 +30,13 @@ # POSSIBILITY OF SUCH DAMAGE. # Standard Library Imports -from time import sleep +import tkinter as tk # Third Party Imports # Local Imports -from navigate.view.configurator_application_window import ConfigurationAssistant +from navigate.view.configurator_application_window import ConfigurationAssistantWindow +from navigate.view.configurator_application_window import MicroscopeTab # Logger Setup import logging @@ -60,13 +61,13 @@ def __init__(self, root, splash_screen): self.root = root # Show the splash screen for 1 second and then destroy it. - sleep(1) + # sleep(1) splash_screen.destroy() self.root.deiconify() - self.view = ConfigurationAssistant(root) + self.view = ConfigurationAssistantWindow(root) - self.view.initial_window.continue_button.config(command=self.on_continue) - self.view.initial_window.cancel_button.config(command=self.on_cancel) + self.view.top_window.continue_button.config(command=self.on_continue) + self.view.top_window.cancel_button.config(command=self.on_cancel) def on_cancel(self): """Closes the window and exits the program""" @@ -76,13 +77,32 @@ def on_cancel(self): def on_continue(self): """Evaluate the number of configurations and create the configuration window""" try: - num_configs = int(self.view.initial_window.num_configs_entry.get()) + num_configs = int(self.view.top_window.num_configs_entry.get()) self.create_config_window(num_configs) except ValueError: print("Please enter a valid number") def create_config_window(self, num_configs): - - # self.view = ConfigurationAssistant(self.root) - - print("Initial Window Destroyed") + """Creates the configuration window""" + + tab_list = [] + self.view.microscope_window.set_tablist(tab_list) + + for i in range(num_configs): + tab_name = f"Microscope {i}" + setattr( + self.view.microscope_window, + f"microscope_tab_{i}", + MicroscopeTab(self.view.microscope_window, name=tab_name, index=i), + ) + tab_list.append(getattr(self.view.microscope_window, f"microscope_tab_{i}")) + + self.view.microscope_window.set_tablist(tab_list) + + # Adding tabs to self notebook + for i in range(num_configs): + self.view.microscope_window.add( + getattr(self.view.microscope_window, f"microscope_tab_{i}"), + text=f"Microscope {i}", + sticky=tk.NSEW, + ) diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index 16c79a1b2..5699212f0 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -38,12 +38,13 @@ # Third Party Imports # Local Imports +from navigate.view.custom_widgets.DockableNotebook import DockableNotebook # Logger Setup p = __name__.split(".")[1] -class ConfigurationAssistant(ttk.Frame): +class ConfigurationAssistantWindow(ttk.Frame): def __init__(self, root, *args, **kwargs): """Initiates the main application window @@ -56,14 +57,15 @@ def __init__(self, root, *args, **kwargs): **kwargs Arbitrary keyword arguments """ + #: tk.Tk: The main window of the application + self.root = root + self.root.title("Configuration Assistant") + + ttk.Frame.__init__(self, self.root, *args, **kwargs) #: logging.Logger: The logger for this class self.logger = logging.getLogger(p) - #: tk.Tk: The main window of the application - self.root = root - self.root.title("navigate Configuration Assistant") - view_directory = Path(__file__).resolve().parent try: photo_image = view_directory.joinpath("icon", "mic.png") @@ -72,28 +74,28 @@ def __init__(self, root, *args, **kwargs): pass self.root.resizable(True, True) - self.root.geometry("512x512") + self.root.geometry("") self.root.rowconfigure(0, weight=1) self.root.columnconfigure(0, weight=1) #: ttk.Frame: The top frame of the application self.top_frame = ttk.Frame(self.root) - self.top_frame.grid(row=0, column=0, columnspan=2, sticky=tk.NSEW) #: ttk.Frame: The main frame of the application - self.main_frame = ttk.Frame(self.root) - self.main_frame.grid(row=1, column=0, sticky=tk.NSEW) + self.microscope_frame = ttk.Frame(self.root) + + self.grid(column=0, row=0, sticky=tk.NSEW) + self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=3, pady=3) + self.microscope_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=3, pady=3) #: ttk.Frame: The top frame of the application - self.initial_window = TopFrame(self.top_frame, self.root) + self.top_window = TopWindow(self.top_frame, self.root) #: ttk.Frame: The main frame of the application - self.microscope_window = MicroscopeConfiguratorWindow( - self.main_frame, self.root - ) + self.microscope_window = MicroscopeWindow(self.microscope_frame, self.root) -class TopFrame(ttk.Frame): +class TopWindow(ttk.Frame): """Top Frame for Configuration Assistant. This class is the initial window for the configurator application. @@ -119,8 +121,8 @@ def __init__(self, main_frame, root, *args, **kwargs): """ #: ttk.Frame: The main frame of the application - self.main_frame = main_frame - ttk.Frame.__init__(self, self.main_frame, *args, **kwargs) + self.microscope_frame = main_frame + ttk.Frame.__init__(self, self.microscope_frame, *args, **kwargs) # Formatting tk.Grid.columnconfigure(self, "all", weight=1) @@ -142,8 +144,8 @@ def __init__(self, main_frame, root, *args, **kwargs): self.cancel_button.grid(row=0, column=3) -class MicroscopeConfiguratorWindow(ttk.Frame): - def __init__(self, main_frame, root, *args, **kwargs): +class MicroscopeWindow(DockableNotebook): + def __init__(self, microscope_frame, root, *args, **kwargs): """Initialize Acquire Bar. Parameters @@ -157,7 +159,53 @@ def __init__(self, main_frame, root, *args, **kwargs): **kwargs Arbitrary keyword arguments. """ - # ttk.Frame.__init__(self, self.main_frame, *args, **kwargs) + DockableNotebook.__init__(self, microscope_frame, root, *args, **kwargs) + self.grid(row=2, column=0) + + # self.microscope_tab_1 = MicroscopeTab(self, name="Microscope 1", index=0) + # self.microscope_tab_2 = MicroscopeTab(self, name="Microscope 2", index=1) + # + # tab_list = [self.microscope_tab_1, self.microscope_tab_2] + # self.set_tablist(tab_list) + # + # # Adding tabs to self notebook + # self.add(self.microscope_tab_1, text="Microscope 1", sticky=tk.NSEW) + # self.add(self.microscope_tab_2, text="Microscope 2", sticky=tk.NSEW) + + # hardware = [ + # "camera", + # "daq", + # "filer_wheel", + # "galvo", + # "lasers", + # "mirrors", + # "remote_focus", + # "shutter", + # "stages", + # "zoom", + # ] + # index = 0 + # + # for device in hardware: + # print(device) + # setattr(self, device, GenericConfiguratorTab(self, + # name=device, + # index=index) + # ) + # index += 1 + + +class MicroscopeTab(ttk.Frame): + def __init__(self, parent, name, index, *args, **kwargs): + + # Init Frame + tk.Frame.__init__(self, *args, **kwargs) + + #: int: The index of the tab + self.index = index - # Entry for number of configurations - tk.Label(root, text="Microscope Configurator").grid(row=1, column=0) + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) + + print("Name: ", name) From 96b62a5567723354fd22337ea6c019e8aa5dd417 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:16:56 -0500 Subject: [PATCH 05/17] Update configurator.py Starting to work... --- src/navigate/controller/configurator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index a30a96c51..ac792dc1e 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -83,11 +83,23 @@ def on_continue(self): print("Please enter a valid number") def create_config_window(self, num_configs): - """Creates the configuration window""" + """Creates the configuration window tabs.""" + # Initialize and set the tab list tab_list = [] self.view.microscope_window.set_tablist(tab_list) + # Check to see if the microscope_window has a pre-existing attribute + for attr in dir(self.view.microscope_window): + if attr.startswith("microscope_tab_"): + print(attr) + print(attr[-1]) + self.view.microscope_window.forget( + getattr(self.view.microscope_window, attr) + ) + delattr(self.view.microscope_window, attr) + + # Create the tabs for i in range(num_configs): tab_name = f"Microscope {i}" setattr( From 066fc7092a5aed60bba027b614b9003bdb7da250 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:41:02 -0500 Subject: [PATCH 06/17] Adding hardware devices. --- src/navigate/controller/configurator.py | 13 +++++-- .../view/configurator_application_window.py | 38 +++++++++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index ac792dc1e..26f752909 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -36,7 +36,10 @@ # Local Imports from navigate.view.configurator_application_window import ConfigurationAssistantWindow -from navigate.view.configurator_application_window import MicroscopeTab +from navigate.view.configurator_application_window import ( + MicroscopeTab, + MicroscopeWindow, +) # Logger Setup import logging @@ -85,15 +88,17 @@ def on_continue(self): def create_config_window(self, num_configs): """Creates the configuration window tabs.""" + self.view.microscope_window = MicroscopeWindow( + self.view.microscope_frame, self.view.root + ) + # Initialize and set the tab list tab_list = [] self.view.microscope_window.set_tablist(tab_list) - # Check to see if the microscope_window has a pre-existing attribute for attr in dir(self.view.microscope_window): if attr.startswith("microscope_tab_"): - print(attr) - print(attr[-1]) + # Delete the tab. self.view.microscope_window.forget( getattr(self.view.microscope_window, attr) ) diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index 5699212f0..9f3e953e9 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -92,7 +92,7 @@ def __init__(self, root, *args, **kwargs): self.top_window = TopWindow(self.top_frame, self.root) #: ttk.Frame: The main frame of the application - self.microscope_window = MicroscopeWindow(self.microscope_frame, self.root) + # self.microscope_window = MicroscopeWindow(self.microscope_frame, self.root) class TopWindow(ttk.Frame): @@ -162,11 +162,35 @@ def __init__(self, microscope_frame, root, *args, **kwargs): DockableNotebook.__init__(self, microscope_frame, root, *args, **kwargs) self.grid(row=2, column=0) - # self.microscope_tab_1 = MicroscopeTab(self, name="Microscope 1", index=0) - # self.microscope_tab_2 = MicroscopeTab(self, name="Microscope 2", index=1) - # - # tab_list = [self.microscope_tab_1, self.microscope_tab_2] - # self.set_tablist(tab_list) + tab_list = [] + hardware = { + "camera": "Camera", + "daq": "Data Acquisition Card", + "filer_wheel": "Filter Wheel", + "galvo": "Galvo", + "lasers": "Lasers", + "mirrors": "Adaptive Optics", + "remote_focus": "Remote Focus Devices", + "shutter": "Shutters", + "stages": "Stages", + "zoom": "Zoom Devices", + } + + tab_names = list(hardware.values()) + self.set_tablist(tab_list) + + for key in hardware: + setattr( + self, + f"{key}_tab", + MicroscopeTab( + self, name=hardware[key], index=tab_names.index(hardware[key]) + ), + ) + tab_list.append(getattr(self, f"{key}_tab")) + self.add(getattr(self, f"{key}_tab"), text=hardware[key], sticky=tk.NSEW) + self.set_tablist(tab_list) + # # # Adding tabs to self notebook # self.add(self.microscope_tab_1, text="Microscope 1", sticky=tk.NSEW) @@ -207,5 +231,3 @@ def __init__(self, parent, name, index, *args, **kwargs): # Formatting tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - - print("Name: ", name) From 93fe861357854aa6d467824d1f5d243487f87f74 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Mon, 18 Mar 2024 22:14:19 -0500 Subject: [PATCH 07/17] Hardware tab now functioning. --- src/navigate/controller/configurator.py | 12 ++-- .../view/configurator_application_window.py | 69 ++++++------------- 2 files changed, 27 insertions(+), 54 deletions(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 26f752909..686bf401f 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -92,10 +92,6 @@ def create_config_window(self, num_configs): self.view.microscope_frame, self.view.root ) - # Initialize and set the tab list - tab_list = [] - self.view.microscope_window.set_tablist(tab_list) - for attr in dir(self.view.microscope_window): if attr.startswith("microscope_tab_"): # Delete the tab. @@ -105,12 +101,18 @@ def create_config_window(self, num_configs): delattr(self.view.microscope_window, attr) # Create the tabs + tab_list = [] for i in range(num_configs): tab_name = f"Microscope {i}" setattr( self.view.microscope_window, f"microscope_tab_{i}", - MicroscopeTab(self.view.microscope_window, name=tab_name, index=i), + MicroscopeTab( + self.view.microscope_window, + name=tab_name, + index=i, + root=self.view.root, + ), ) tab_list.append(getattr(self.view.microscope_window, f"microscope_tab_{i}")) diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index 9f3e953e9..c809136b1 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -145,22 +145,23 @@ def __init__(self, main_frame, root, *args, **kwargs): class MicroscopeWindow(DockableNotebook): - def __init__(self, microscope_frame, root, *args, **kwargs): - """Initialize Acquire Bar. + def __init__(self, frame, root, *args, **kwargs): + DockableNotebook.__init__(self, frame, root, *args, **kwargs) + self.grid(row=0, column=0, sticky=tk.NSEW) - Parameters - ---------- - main_frame : ttk.Frame - Window to place widgets in. - root : tk.Tk - Root window of the application. - *args - Variable length argument list. - **kwargs - Arbitrary keyword arguments. - """ - DockableNotebook.__init__(self, microscope_frame, root, *args, **kwargs) - self.grid(row=2, column=0) + +class MicroscopeTab(DockableNotebook): + def __init__(self, parent, name, index, root, *args, **kwargs): + + # Init Frame + DockableNotebook.__init__(self, parent, root, *args, **kwargs) + + #: int: The index of the tab + self.index = index + + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) tab_list = [] hardware = { @@ -173,7 +174,7 @@ def __init__(self, microscope_frame, root, *args, **kwargs): "remote_focus": "Remote Focus Devices", "shutter": "Shutters", "stages": "Stages", - "zoom": "Zoom Devices", + "zoom": "Zoom Device", } tab_names = list(hardware.values()) @@ -183,45 +184,15 @@ def __init__(self, microscope_frame, root, *args, **kwargs): setattr( self, f"{key}_tab", - MicroscopeTab( - self, name=hardware[key], index=tab_names.index(hardware[key]) - ), + HardwareTab(index=tab_names.index(hardware[key])), ) tab_list.append(getattr(self, f"{key}_tab")) self.add(getattr(self, f"{key}_tab"), text=hardware[key], sticky=tk.NSEW) self.set_tablist(tab_list) - # - # # Adding tabs to self notebook - # self.add(self.microscope_tab_1, text="Microscope 1", sticky=tk.NSEW) - # self.add(self.microscope_tab_2, text="Microscope 2", sticky=tk.NSEW) - - # hardware = [ - # "camera", - # "daq", - # "filer_wheel", - # "galvo", - # "lasers", - # "mirrors", - # "remote_focus", - # "shutter", - # "stages", - # "zoom", - # ] - # index = 0 - # - # for device in hardware: - # print(device) - # setattr(self, device, GenericConfiguratorTab(self, - # name=device, - # index=index) - # ) - # index += 1 - - -class MicroscopeTab(ttk.Frame): - def __init__(self, parent, name, index, *args, **kwargs): +class HardwareTab(ttk.Frame): + def __init__(self, index, *args, **kwargs): # Init Frame tk.Frame.__init__(self, *args, **kwargs) From 9665f6093309fd4473b398ea65727ebe6a5c7829 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Tue, 19 Mar 2024 08:54:41 -0500 Subject: [PATCH 08/17] Added devices... Now there is automatically a way to select the devices types, which are stored in __init__. Next I am thinking we should automatically create yet another frame after they select the device type and show two columns. One for mandatory parameters, and another for optional parameters. There are more details lurking. For some devices, like cameras, we will need to specify the number of devices. Will move to a PR so that it isn't too big. --- src/navigate/controller/configurator.py | 12 +++++++ src/navigate/model/devices/camera/__init__.py | 10 +++++- src/navigate/model/devices/daq/__init__.py | 4 ++- .../model/devices/filter_wheel/__init__.py | 8 ++++- src/navigate/model/devices/galvo/__init__.py | 4 ++- src/navigate/model/devices/lasers/__init__.py | 4 ++- .../model/devices/mirrors/__init__.py | 4 ++- .../model/devices/remote_focus/__init__.py | 8 ++++- .../model/devices/shutter/__init__.py | 4 ++- src/navigate/model/devices/stages/__init__.py | 12 ++++++- src/navigate/model/devices/zoom/__init__.py | 7 +++- .../view/configurator_application_window.py | 34 ++++++++++++------- 12 files changed, 89 insertions(+), 22 deletions(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 686bf401f..3e1db14c7 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -125,3 +125,15 @@ def create_config_window(self, num_configs): text=f"Microscope {i}", sticky=tk.NSEW, ) + + def device_selected(self, event): + """Handle the event when a device is selected from the dropdown.""" + # # Get the selected device name + # selected_device_name = self.view.microscope_frame.get() + # # Find the key in the dictionary that corresponds to the selected value + # selected_key = next( + # key for key, value in device_types.items() + # if value == selected_device_name) + # print(f"Selected Device Key: {selected_key}") + # print(f"Selected Device Name: {selected_device_name}") + pass diff --git a/src/navigate/model/devices/camera/__init__.py b/src/navigate/model/devices/camera/__init__.py index 368df1a98..b784ae042 100644 --- a/src/navigate/model/devices/camera/__init__.py +++ b/src/navigate/model/devices/camera/__init__.py @@ -1,3 +1,11 @@ """ Camera devices. -""" \ No newline at end of file +""" + +device_types = { + "hamamatsu_lightning": "Hamamatsu ORCA Lightning", + "hamamatsu_fire": "Hamamatsu ORCA Fire", + "hamamatsu_flash": "Hamamatsu Flash 4.0", + "photometics": "Photometrics Iris 15B", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/daq/__init__.py b/src/navigate/model/devices/daq/__init__.py index e10fb61c7..4324bdfdf 100644 --- a/src/navigate/model/devices/daq/__init__.py +++ b/src/navigate/model/devices/daq/__init__.py @@ -1,3 +1,5 @@ """ Data acquisition card devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "National Instruments", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/filter_wheel/__init__.py b/src/navigate/model/devices/filter_wheel/__init__.py index 6002cd8d1..2839ffbde 100644 --- a/src/navigate/model/devices/filter_wheel/__init__.py +++ b/src/navigate/model/devices/filter_wheel/__init__.py @@ -1,3 +1,9 @@ """ Filter wheel devices. -""" \ No newline at end of file +""" + +device_types = { + "asi": "Applied Scientific Instrumentation", + "sutter": "Sutter Instruments", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/galvo/__init__.py b/src/navigate/model/devices/galvo/__init__.py index 9ffdceafb..e4a89bdc4 100644 --- a/src/navigate/model/devices/galvo/__init__.py +++ b/src/navigate/model/devices/galvo/__init__.py @@ -1,3 +1,5 @@ """ Galvanometer devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "Analog Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/lasers/__init__.py b/src/navigate/model/devices/lasers/__init__.py index ddb463779..0c5141736 100644 --- a/src/navigate/model/devices/lasers/__init__.py +++ b/src/navigate/model/devices/lasers/__init__.py @@ -1,3 +1,5 @@ """ Laser devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "Analog Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/mirrors/__init__.py b/src/navigate/model/devices/mirrors/__init__.py index a5d8c3579..70aa4cd77 100644 --- a/src/navigate/model/devices/mirrors/__init__.py +++ b/src/navigate/model/devices/mirrors/__init__.py @@ -1,3 +1,5 @@ """ Deformable mirror devices. -""" \ No newline at end of file +""" + +device_types = {"imop": "Imagine Optics", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/remote_focus/__init__.py b/src/navigate/model/devices/remote_focus/__init__.py index 4a2e563ba..3cb3e5094 100644 --- a/src/navigate/model/devices/remote_focus/__init__.py +++ b/src/navigate/model/devices/remote_focus/__init__.py @@ -1,3 +1,9 @@ """ Remote focus devices. -""" \ No newline at end of file +""" + +device_types = { + "equipment_solutions": "Equipment Solutions", + "ni": "Analog Device", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/shutter/__init__.py b/src/navigate/model/devices/shutter/__init__.py index cb0ac8bdd..9c78de06e 100644 --- a/src/navigate/model/devices/shutter/__init__.py +++ b/src/navigate/model/devices/shutter/__init__.py @@ -1,3 +1,5 @@ """ Shutter devices. -""" \ No newline at end of file +""" + +device_types = {"ttl": "Analog/Digital Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/stages/__init__.py b/src/navigate/model/devices/stages/__init__.py index 0e4c1bb5e..c27a43f6d 100644 --- a/src/navigate/model/devices/stages/__init__.py +++ b/src/navigate/model/devices/stages/__init__.py @@ -1,3 +1,13 @@ """ Stage devices. -""" \ No newline at end of file +""" + +device_types = { + "asi": "Applied Scientific Instrumentation", + "galvo": "Analog/Digital Device", + "mcl": "Mad City Labs", + "pi": "Physik Instrumente", + "sutter": "Sutter Instruments", + "synthetic": "Virtual Device", + "tl_kcube_inertial": "ThorLabs KCube Inertial Device", +} diff --git a/src/navigate/model/devices/zoom/__init__.py b/src/navigate/model/devices/zoom/__init__.py index 76fa3a981..d19569bba 100644 --- a/src/navigate/model/devices/zoom/__init__.py +++ b/src/navigate/model/devices/zoom/__init__.py @@ -1,3 +1,8 @@ """ Zoom devices. -""" \ No newline at end of file +""" + +device_types = { + "dynamixel": "Dynamixel", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index c809136b1..e2368c7c9 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -1,6 +1,5 @@ # Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center. # All rights reserved. - # Redistribution and use in source and binary forms, with or without # modification, are permitted for academic and research use only (subject to the # limitations in the disclaimer below) provided that the following conditions are met: @@ -34,6 +33,7 @@ from tkinter import ttk import logging from pathlib import Path +import importlib # Third Party Imports @@ -86,14 +86,13 @@ def __init__(self, root, *args, **kwargs): self.grid(column=0, row=0, sticky=tk.NSEW) self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=3, pady=3) - self.microscope_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=3, pady=3) + self.microscope_frame.grid( + row=1, column=0, columnspan=4, sticky=tk.NSEW, padx=3, pady=3 + ) #: ttk.Frame: The top frame of the application self.top_window = TopWindow(self.top_frame, self.root) - #: ttk.Frame: The main frame of the application - # self.microscope_window = MicroscopeWindow(self.microscope_frame, self.root) - class TopWindow(ttk.Frame): """Top Frame for Configuration Assistant. @@ -167,7 +166,7 @@ def __init__(self, parent, name, index, root, *args, **kwargs): hardware = { "camera": "Camera", "daq": "Data Acquisition Card", - "filer_wheel": "Filter Wheel", + "filter_wheel": "Filter Wheel", "galvo": "Galvo", "lasers": "Lasers", "mirrors": "Adaptive Optics", @@ -181,18 +180,15 @@ def __init__(self, parent, name, index, root, *args, **kwargs): self.set_tablist(tab_list) for key in hardware: - setattr( - self, - f"{key}_tab", - HardwareTab(index=tab_names.index(hardware[key])), - ) + index = tab_names.index(hardware[key]) + setattr(self, f"{key}_tab", HardwareTab(hardware_type=key, index=index)) tab_list.append(getattr(self, f"{key}_tab")) self.add(getattr(self, f"{key}_tab"), text=hardware[key], sticky=tk.NSEW) self.set_tablist(tab_list) class HardwareTab(ttk.Frame): - def __init__(self, index, *args, **kwargs): + def __init__(self, hardware_type, index, *args, **kwargs): # Init Frame tk.Frame.__init__(self, *args, **kwargs) @@ -202,3 +198,17 @@ def __init__(self, index, *args, **kwargs): # Formatting tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) + + # Import __init__ file for hardware type + base_module_path = "navigate.model.devices" + full_module_path = f"{base_module_path}.{hardware_type}" + module = importlib.import_module(full_module_path) + device_types = getattr(module, "device_types") + + label = ttk.Label(self, text="Select a device:") + label.pack(pady=5, side=tk.LEFT) + device_var = tk.StringVar() + dropdown = ttk.Combobox(self, textvariable=device_var, state="readonly") + dropdown["values"] = list(device_types.values()) + dropdown.pack(pady=5) + # dropdown.bind("<>", on_device_selected) From b2c1e624d7ed9a87826de64c53758922bb58cd5e Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Tue, 19 Mar 2024 08:59:08 -0500 Subject: [PATCH 09/17] Provide a warning... --- src/navigate/controller/configurator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 3e1db14c7..29d957363 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -31,6 +31,7 @@ # Standard Library Imports import tkinter as tk +from time import sleep # Third Party Imports @@ -64,7 +65,7 @@ def __init__(self, root, splash_screen): self.root = root # Show the splash screen for 1 second and then destroy it. - # sleep(1) + sleep(1) splash_screen.destroy() self.root.deiconify() self.view = ConfigurationAssistantWindow(root) @@ -72,6 +73,11 @@ def __init__(self, root, splash_screen): self.view.top_window.continue_button.config(command=self.on_continue) self.view.top_window.cancel_button.config(command=self.on_cancel) + print( + "WARNING: The Configuration Assistant is not fully implemented. " + "Users are still required to manually configure their system." + ) + def on_cancel(self): """Closes the window and exits the program""" self.root.destroy() From 02567174bbc6e83ce9afee40f3a6059c3e93f006 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 3 May 2024 15:41:47 -0700 Subject: [PATCH 10/17] configuration gui and save to yaml file --- src/navigate/config/configuration_database.py | 237 ++++++++++++++++ src/navigate/controller/configurator.py | 173 +++++++++--- .../view/configurator_application_window.py | 266 ++++++++++++++---- .../view/custom_widgets/CollapsibleFrame.py | 33 +++ 4 files changed, 611 insertions(+), 98 deletions(-) create mode 100644 src/navigate/config/configuration_database.py create mode 100644 src/navigate/view/custom_widgets/CollapsibleFrame.py diff --git a/src/navigate/config/configuration_database.py b/src/navigate/config/configuration_database.py new file mode 100644 index 000000000..00818e9c5 --- /dev/null +++ b/src/navigate/config/configuration_database.py @@ -0,0 +1,237 @@ +camera_device_types = { + "Hamamatsu ORCA Lightning": "HamamatsuOrcaLightning", + "Hamamatsu ORCA Fire": "HamamatsuOrcaFire", + "Hamamatsu Flash 4.0": "HamamatsuOrca", + "Photometrics Iris 15B": "Photometrics", + "Virtual Device": "synthetic" +} + +camera_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", camera_device_types], + "hardware/serial_number": ["Serial Number", "Input", "string", None], + "defect_correct_mode": ["Defect Correct Mode", "Combobox", "string", {"On": 2.0, "Off": 1.0}], + "delay": ["Delay (ms)", "Spinbox", "float", None], + "flip_x": ["Flip X", "Checkbutton", "bool", None], + "flip_y": ["Flip Y", "Checkbutton", "bool", None], +} + +filter_wheel_device_types = { + "Sutter Instruments": "SutterFilterWheel", + "Applied Scientific Instrumentation": "ASI", + "Virtual Device": "synthetic", +} + +filter_wheel_widgets = { + "filter_name": ["Filter Name", "Input", "string", None], + "filter_value": ["Filter Value", "Input", "string", None], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"format": "item(filter_name,filter_value)"} +} + +filter_wheel_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", filter_wheel_device_types,], + "hardware/wheel_number": ["Number of Wheels", "Spinbox", "int", None], + "hardware/port": ["Serial Port", "Input", "string", None], + "hardware/baudrate": ["Baudrate", "Input", "int", None], + "filter_wheel_delay": ["Filter Wheel Delay (s)", "Input", "float", None], + "button_1": ["Add Available Filters", "Button", {"widgets":filter_wheel_widgets, "ref": "available_filters", "direction": "horizon"}] +} + +daq_device_types = { + "National Instruments": "NI", +} + +daq_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", daq_device_types,], + "sample_rate": ["Sample Rate", "Input", "int", None], + "master_trigger_out_line": ["Master Trigger Out", "Input", "string", None], + "camera_trigger_out_line": ["Camera Trigger Out", "Input", "string", None], + "trigger_source": ["Trigger Source", "Input", "string", None], + "laser_port_switcher": ["Laser Switcher Port", "Input", "string", None], + "laser_switch_state": ["Laser Switch On State", "Combobox", "bool", [True, False]], +} + +shutter_device_types = { + "Analog/Digital Device": "NI", + "Virtual Device": "synthetic", +} + +shutter_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", shutter_device_types,], + "channel": ["NI Channel", "Input", "string", None], + "min": ["Minimum Voltage", "Spinbox", "float", None], + "max": ["Maximum Voltage", "Spinbox", "float", None], + "frame_config": {"ref": "hardware"} +} + +stage_device_types = { + "Applied Scientific Instrumentation": "ASI", + "Analog/Digital Device": "GalvoNIStage", + "Mad City Labs": "MCL", + "Physik Instrumente": "PI", + "Sutter Instruments": "MP285", + "ThorLabs KCube Inertial Device": "Thorlabs", + "Virtual Device": "synthetic", +} + +stage_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", stage_device_types,], + "serial_number": ["Serial Number", "Input", "string", None], + "axes": ["Axes", "Input", "string", None,], + "axes_mapping": ["Axes Mapping", "Input", "string", None], + "volts_per_micron": ["Volts Per Micron", "Spinbox", "float", {"from": 0, "to": 100, "step":0.1}, ("type", "GalvoNIStage")], + "min": ["Minimum Volts", "Spinbox", "float", {"from": 0, "to": 5, "step": 0.1}, ("type", "GalvoNIStage")], + "max": ["Maximum Volts", "Spinbox", "float", {"from": 1, "to": 100, "step": 0.1}, ("type", "GalvoNIStage")], + "controllername": ["Controller Name", "Input", "string", None, ("type", "PI")], + "stages": ["PI Stages", "Input", "string", None, ("type", "PI")], + "refmode": ["REF Modes", "Input", "string", None, ("type", "PI")], + "port": ["Serial Port", "Input", "string", None], + "baudrate": ["Baudrate", "Input", "int", None], + "button_2": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Stage", "ref": "hardware", "format": "list-dict"} +} + +stage_top_widgets = { + "button_1": ["Add New Stage Device", "Button", {"widgets": stage_hardware_widgets, "ref": "hardware", "parent": "hardware"}], +} + +stage_constants_widgets = { + "x_min": ["Min X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "x_max": ["Max X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "y_min": ["Min Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "y_max": ["Max Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "z_min": ["Min Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "z_max": ["Max Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "theta_min": ["Min Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "theta_max": ["Max Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "f_min": ["Min Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "f_max": ["Max Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "x_offset": ["Offset of X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "y_offset": ["Offset of Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "z_offset": ["Offset of Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "theta_offset": ["Offset of Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "f_offset": ["Offset of Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "frame_config": {"collapsible": True, "title": "Stage Constants"} +} + +remote_focus_device_types = { + "Equipment Solutions": "EquipmentSolutions", + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +remote_focus_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", remote_focus_device_types], + "channel": ["DAQ Channel", "Input", "string", None], + "min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "comport": ["Serial Port", "Input", "string", None], + "baudrate": ["Baudrate", "Input", "int", None], + "frame_config": {"ref": "hardware"} +} + +galvo_device_types = { + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +waveform_types = { + "Sine": "sine", + "Sawtooth": "sawtooth", + "Square": "square", +} + +galvo_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", galvo_device_types], + "hardware/channel": ["DAQ Channel", "Input", "string", None], + "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "waveform": ["Waveform", "Combobox", "string", waveform_types], + "phase": ["Phase", "Input", "string", None], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Galvo Device", "ref": "None", "format": "list-dict"} +} + +galvo_top_widgets = { + "button_1": ["Add New Device", "Button", {"widgets": galvo_hardware_widgets, "parent": "hardware"}], +} + +zoom_device_types = { + "Dynamixel": "DynamixelZoom", + "Virtual Device": "synthetic" +} + +zoom_position_widgets = { + "zoom_value": ["Zoom Value", "Input", "string", None], + "position": ["Position", "Input", "float", None], + "pixel_size": ["Pixel Size (um)", "Input", "float", None], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"ref": "position;pixel_size", "format": "item(zoom_value, position);item(zoom_value, pixel_size)"} +} + +zoom_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", zoom_device_types,], + "servo_id": ["Servo ID", "Input", "string", None], + "button_1": ["Add Zoom Value", "Button", {"widgets":zoom_position_widgets, "ref": "position;pixel_size", "direction": "horizon"}] +} + +mirror_device_types = { + "Imagine Optics": "ImagineOpticsMirror", + "Virtual Device": "SyntheticMirror" +} + +mirror_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", mirror_device_types,], + "frame_config": {"ref": "hardware"} +} + +laser_device_types = { + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +laser_hardware_widgets = { + "wavelength": ["Wavelength", "Input", "int", None], + "onoff": ["On/Off Setting", "Label", None, None], + "onoff/hardware/type": ["Type", "Combobox", "string", laser_device_types], + "onoff/hardware/channel": ["DAQ Channel", "Input", "string", None], + "onoff/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], + "onoff/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], + "power": ["Power Setting", "Label", None, None], + "power/hardware/type": ["Type", "Combobox", "string", laser_device_types], + "power/hardware/channel": ["DAQ Channel", "Input", "string", None], + "power/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], + "power/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Wavelength"} +} + +laser_top_widgets = { + "button_1": ["Add Wavelength", "Button", {"widgets": laser_hardware_widgets, "ref": "", "parent": "hardware"}], +} + +hardwares_dict = { + "Camera": camera_hardware_widgets, + "Data Acquisition Card": daq_hardware_widgets, + "Filter Wheel": filter_wheel_hardware_widgets, + "Galvo": (galvo_top_widgets, galvo_hardware_widgets, None), + "Lasers": (laser_top_widgets, laser_hardware_widgets, None), + "Remote Focus Devices": remote_focus_hardware_widgets, + "Adaptive Optics": mirror_hardware_widgets, + "Shutters": shutter_hardware_widgets, + "Stages": (stage_top_widgets, stage_hardware_widgets, stage_constants_widgets), + "Zoom Device": zoom_hardware_widgets +} + +hardwares_config_name_dict = { + "Camera": "camera", + "Data Acquisition Card": "daq", + "Filter Wheel": "filter_wheel", + "Galvo": "galvo", + "Lasers": "lasers", + "Remote Focus Devices": "remote_focus_device", + "Adaptive Optics": "mirror", + "Shutters": "shutter", + "Stages": "stage", + "Zoom Device": "zoom", +} \ No newline at end of file diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 29d957363..f649c7892 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -32,6 +32,7 @@ # Standard Library Imports import tkinter as tk from time import sleep +from tkinter import filedialog # Third Party Imports @@ -41,6 +42,10 @@ MicroscopeTab, MicroscopeWindow, ) +from navigate.config.configuration_database import ( + hardwares_dict, + hardwares_config_name_dict, +) # Logger Setup import logging @@ -69,9 +74,15 @@ def __init__(self, root, splash_screen): splash_screen.destroy() self.root.deiconify() self.view = ConfigurationAssistantWindow(root) + self.view.microscope_window = MicroscopeWindow( + self.view.microscope_frame, self.view.root + ) - self.view.top_window.continue_button.config(command=self.on_continue) + self.view.top_window.add_button.config(command=self.add_microscope) + self.view.top_window.save_button.config(command=self.save) self.view.top_window.cancel_button.config(command=self.on_cancel) + self.create_config_window(0) + self.microscope_id = 1 print( "WARNING: The Configuration Assistant is not fully implemented. " @@ -83,54 +94,130 @@ def on_cancel(self): self.root.destroy() exit() - def on_continue(self): + def add_microscope(self): """Evaluate the number of configurations and create the configuration window""" - try: - num_configs = int(self.view.top_window.num_configs_entry.get()) - self.create_config_window(num_configs) - except ValueError: - print("Please enter a valid number") - - def create_config_window(self, num_configs): + self.create_config_window(self.microscope_id) + self.microscope_id += 1 + + def save(self): + def set_value(temp_dict, key_list, value): + if type(key_list) is list: + for i in range(len(key_list)-1): + k = key_list[i] + temp_dict[k] = temp_dict.get(k, {}) + temp_dict = temp_dict[k] + temp_dict[key_list[-1]] = value + + filename = filedialog.asksaveasfilename( + defaultextension=".yml", filetypes=[("Yaml file", "*.yml *.yaml")] + ) + if not filename: + return + config_dict = {} + for tab_index in self.view.microscope_window.tabs(): + microscope_name = self.view.microscope_window.tab(tab_index, "text") + microscope_tab = self.view.microscope_window.nametowidget(tab_index) + microscope_dict = {} + config_dict[microscope_name] = microscope_dict + for hardware_tab_index in microscope_tab.tabs(): + hardware_name = microscope_tab.tab(hardware_tab_index, "text") + hardware_tab = microscope_tab.nametowidget(hardware_tab_index) + hardware_dict = {} + microscope_dict[hardwares_config_name_dict.get(hardware_name, hardware_name)] = hardware_dict + for variable_list in hardware_tab.variables_list: + if variable_list is None: + continue + variables, value_dict, ref, format = variable_list + if format is None: + format = "" + temp_dict = hardware_dict + if ref is not None: + if format.startswith("list"): + hardware_dict[ref] = hardware_dict.get(ref, []) + temp_dict = {} + hardware_dict[ref].append(temp_dict) + elif format.startswith("item"): + format_list = format.split(";") + ref_list = ref.split(";") + for i, format in enumerate(format_list): + ref = ref_list[i] + hardware_dict[ref] = hardware_dict.get(ref, {}) + temp_dict = hardware_dict[ref] + k_idx = format[format.index("(")+1: format.index(",")].strip() + v_idx = format[format.index(",")+1:format.index(")")].strip() + k = variables[k_idx].get() + if k_idx in value_dict: + k = value_dict[k_idx][v] + v = variables[v_idx].get() + if v_idx in value_dict: + v = value_dict[v_idx][v] + temp_dict[k] = v + continue + else: + temp_dict = {} + hardware_dict[ref] = hardware_dict.get("ref", temp_dict) + for k, var in variables.items(): + if k in value_dict: + v = value_dict[k][var.get()] + else: + v = var.get() + set_value(temp_dict, k.split("/"), v) + + self.write_to_yaml(config_dict, filename) + + def write_to_yaml(self, config, filename): + + def write_func(prefix, config_dict, f): + for k in config_dict: + if type(config_dict[k]) == dict: + f.write(f"{prefix}{k}:\n") + write_func(prefix+" "*2, config_dict[k], f) + elif type(config_dict[k]) == list: + list_prefix = " " + if k != "None": + f.write(f"{prefix}{k}:\n") + list_prefix = " " * 2 + for list_item in config_dict[k]: + f.write(f"{prefix}{list_prefix}-\n") + write_func(prefix+list_prefix*2, list_item, f) + else: + f.write(f"{prefix}{k}: {config_dict[k]}\n") + + with open(filename, "w") as f: + f.write("microscopes:\n") + write_func(" ", config, f) + + + def create_config_window(self, id): """Creates the configuration window tabs.""" - self.view.microscope_window = MicroscopeWindow( - self.view.microscope_frame, self.view.root + tab_name = "Microscope " + str(id) + microscope_tab = MicroscopeTab( + self.view.microscope_window, + name=tab_name, + index=id, + root=self.view.root, + ) + setattr( + self.view.microscope_window, + f"microscope_tab_{id}", + microscope_tab, ) - - for attr in dir(self.view.microscope_window): - if attr.startswith("microscope_tab_"): - # Delete the tab. - self.view.microscope_window.forget( - getattr(self.view.microscope_window, attr) - ) - delattr(self.view.microscope_window, attr) - - # Create the tabs - tab_list = [] - for i in range(num_configs): - tab_name = f"Microscope {i}" - setattr( - self.view.microscope_window, - f"microscope_tab_{i}", - MicroscopeTab( - self.view.microscope_window, - name=tab_name, - index=i, - root=self.view.root, - ), - ) - tab_list.append(getattr(self.view.microscope_window, f"microscope_tab_{i}")) - - self.view.microscope_window.set_tablist(tab_list) + self.view.microscope_window.tab_list.append(tab_name) + for hardware_type, widgets in hardwares_dict.items(): + if not widgets: + continue + if type(widgets) == dict: + microscope_tab.create_hardware_tab(hardware_type, widgets) + else: + microscope_tab.create_hardware_tab(hardware_type, hardware_widgets=widgets[1], widgets=widgets[2], top_widgets=widgets[0]) # Adding tabs to self notebook - for i in range(num_configs): - self.view.microscope_window.add( - getattr(self.view.microscope_window, f"microscope_tab_{i}"), - text=f"Microscope {i}", - sticky=tk.NSEW, - ) + self.view.microscope_window.add( + getattr(self.view.microscope_window, f"microscope_tab_{id}"), + text=tab_name, + sticky=tk.NSEW, + ) def device_selected(self, event): """Handle the event when a device is selected from the dropdown.""" diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index e2368c7c9..c0c666155 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -30,7 +30,7 @@ # Standard Library Imports import tkinter as tk -from tkinter import ttk +from tkinter import ttk, simpledialog import logging from pathlib import Path import importlib @@ -39,11 +39,25 @@ # Local Imports from navigate.view.custom_widgets.DockableNotebook import DockableNotebook +from navigate.view.custom_widgets.CollapsibleFrame import CollapsibleFrame # Logger Setup p = __name__.split(".")[1] - +widget_types = { + "Combobox": ttk.Combobox, + "Input": ttk.Entry, + "Spinbox": ttk.Spinbox, + "Checkbutton": ttk.Checkbutton, + "Button": ttk.Button +} + +variable_types = { + "string": tk.StringVar, + "float": tk.DoubleVar, + "bool": tk.BooleanVar, + "int": tk.IntVar +} class ConfigurationAssistantWindow(ttk.Frame): def __init__(self, root, *args, **kwargs): """Initiates the main application window @@ -127,20 +141,18 @@ def __init__(self, main_frame, root, *args, **kwargs): tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - # Entry for number of configurations - tk.Label(root, text="Number of Microscopes:").grid(row=0, column=0) - - #: tk.Entry: The entry for the number of configurations to create. - self.num_configs_entry = tk.Entry(root) - self.num_configs_entry.grid(row=0, column=1) + self.add_button = tk.Button(root, text="Add A Microscope") + self.add_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) + self.add_button.config(width=15) - #: tk.Button: The button to continue to the next window. - self.continue_button = tk.Button(root, text="Continue") - self.continue_button.grid(row=0, column=2) + self.save_button = tk.Button(root, text="Save") + self.save_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button.config(width=15) #: tk.Button: The button to cancel the application. self.cancel_button = tk.Button(root, text="Cancel") - self.cancel_button.grid(row=0, column=3) + self.cancel_button.grid(row=0, column=3, sticky=tk.NE, padx=3, pady=(10, 1)) + self.cancel_button.config(width=15) class MicroscopeWindow(DockableNotebook): @@ -148,6 +160,30 @@ def __init__(self, frame, root, *args, **kwargs): DockableNotebook.__init__(self, frame, root, *args, **kwargs) self.grid(row=0, column=0, sticky=tk.NSEW) + self.menu.delete("Popout Tab") + self.menu.add_command(label="Rename", command=self.rename_microscope) + self.menu.add_command(label="Delete", command=self.delete_microscope) + + def rename_microscope(self): + """Rename microscope""" + + result = simpledialog.askstring("Input", "Enter microscope name:") + if result: + tab = self.select() + tab_name = self.tab(tab)["text"] + self.tab(tab, text=result) + self.tab_list.remove(tab_name) + self.tab_list.append(result) + + def delete_microscope(self): + tab = self.select() + tab_name = self.tab(tab)["text"] + current_tab_index = self.index("current") + if current_tab_index >= 0: + self.forget(current_tab_index) + self.tab_list.remove(tab_name) + + class MicroscopeTab(DockableNotebook): def __init__(self, parent, name, index, root, *args, **kwargs): @@ -155,60 +191,180 @@ def __init__(self, parent, name, index, root, *args, **kwargs): # Init Frame DockableNotebook.__init__(self, parent, root, *args, **kwargs) - #: int: The index of the tab - self.index = index - # Formatting tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - tab_list = [] - hardware = { - "camera": "Camera", - "daq": "Data Acquisition Card", - "filter_wheel": "Filter Wheel", - "galvo": "Galvo", - "lasers": "Lasers", - "mirrors": "Adaptive Optics", - "remote_focus": "Remote Focus Devices", - "shutter": "Shutters", - "stages": "Stages", - "zoom": "Zoom Device", - } - - tab_names = list(hardware.values()) - self.set_tablist(tab_list) - - for key in hardware: - index = tab_names.index(hardware[key]) - setattr(self, f"{key}_tab", HardwareTab(hardware_type=key, index=index)) - tab_list.append(getattr(self, f"{key}_tab")) - self.add(getattr(self, f"{key}_tab"), text=hardware[key], sticky=tk.NSEW) - self.set_tablist(tab_list) + + def create_hardware_tab(self, name, hardware_widgets, widgets=None, top_widgets=None): + tab = HardwareTab(name, hardware_widgets, widgets=widgets, top_widgets=top_widgets) + self.tab_list.append(name) + self.add(tab, text=name, sticky=tk.NSEW) class HardwareTab(ttk.Frame): - def __init__(self, hardware_type, index, *args, **kwargs): + def __init__(self, name, hardware_widgets, *args, widgets=None, top_widgets=None, **kwargs): # Init Frame tk.Frame.__init__(self, *args, **kwargs) - - #: int: The index of the tab - self.index = index + + self.name = name # Formatting tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - # Import __init__ file for hardware type - base_module_path = "navigate.model.devices" - full_module_path = f"{base_module_path}.{hardware_type}" - module = importlib.import_module(full_module_path) - device_types = getattr(module, "device_types") - - label = ttk.Label(self, text="Select a device:") - label.pack(pady=5, side=tk.LEFT) - device_var = tk.StringVar() - dropdown = ttk.Combobox(self, textvariable=device_var, state="readonly") - dropdown["values"] = list(device_types.values()) - dropdown.pack(pady=5) - # dropdown.bind("<>", on_device_selected) + self.top_frame = ttk.Frame(self) + self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=20) + + self.hardware_frame = ttk.Frame(self) + self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=20) + + self.bottom_frame = ttk.Frame(self) + self.bottom_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=20) + self.frame_row = 0 + self.row_offset = self.frame_row + 1 + + self.variables = {} + self.values_dict = {} + self.variables_list = [] + + self.build_widgets(top_widgets, parent=self.top_frame) + + self.build_widgets(hardware_widgets, parent=self.hardware_frame) + + self.build_widgets(widgets) + + + def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): + """Build hardware widgets + + Parameters + ---------- + hardware_widgets: dict + name: (display_name, widget_type, value_type, values, condition) + """ + if hardware_widgets is None: + return + if type(frame) is CollapsibleFrame: + content_frame = frame.content_frame + else: + content_frame = frame + i = 0 + for k, v in hardware_widgets.items(): + if k == "frame_config": + continue + if v[1] == "Label": + label = ttk.Label(content_frame, text=v[0]) + label.grid(row=i, column=0, sticky=tk.NW, padx=3) + seperator = ttk.Separator(content_frame) + seperator.grid(row=i+1, columnspan=2, sticky=tk.NSEW, padx=3) + i += 2 + continue + elif v[1] != "Button": + self.variables[k] = variable_types[v[2]]() + label_text = v[0] + " :" if v[0][-1] != ":" else v[0] + label = ttk.Label(content_frame, text=label_text) + if direction == "vertical": + label.grid(row=i, column=0, sticky=tk.NW, padx=(3, 10), pady=3) + else: + label.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) + i += 1 + if v[1] == "Checkbutton": + widget = widget_types[v[1]](content_frame, text="", variable=self.variables[k]) + else: + widget = widget_types[v[1]](content_frame, textvariable=self.variables[k], width=30) + if v[1] == "Combobox": + if type(v[3]) == list: + v[3] = dict([(t, t) for t in v[3]]) + self.values_dict[k] = v[3] + temp = list(v[3].keys()) + widget.config(values=temp) + if v[2] == "bool": + widget.set(str(temp[-1])) + else: + widget.set(temp[-1]) + elif v[1] == "Spinbox": + if type(v[3]) != dict: + v[3] = {} + widget.config(from_=v[3].get("from", 0)) + widget.config(to=v[3].get("to", 100000)) + widget.config(increment=v[3].get("step", 1)) + widget.set(v[3].get("from", 0)) + else: + widget = ttk.Button(content_frame, text=v[0], command=self.build_event_handler(hardware_widgets, k, frame, self.frame_row)) + if direction == "vertical": + widget.grid(row=i, column=1, sticky=tk.NSEW, padx=5, pady=3) + else: + widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) + i += 1 + + + def build_widgets(self, widgets, *args, parent=None, **kwargs): + if not widgets: + return + if parent is None: + parent = self.bottom_frame + collapsible = False + title = "Hardware" + format = None + temp_ref = None + if "frame_config" in widgets: + collapsible = widgets["frame_config"].get("collapsible", False) + title = widgets["frame_config"].get("title", "Hardware") + format = widgets["frame_config"].get("format", None) + temp_ref = widgets["frame_config"].get("ref", None) + if collapsible: + self.foldAllFrames() + frame = CollapsibleFrame(parent=parent, title=title) + # only display one callapsible frame at a time + frame.label.bind("", self.create_toggle_function(frame)) + else: + frame = ttk.Frame(parent) + frame.grid(row=self.frame_row, column=0, sticky=tk.NSEW, padx=20) + self.frame_row += 1 + + ref = None + direction = "vertical" + if kwargs: + ref = kwargs.get("ref", None) + direction = kwargs.get("direction", "vertical") + ref = ref or temp_ref + self.variables = {} + self.values_dict = {} + self.variables_list.append((self.variables, self.values_dict, ref, format)) + self.build_hardware_widgets(widgets, frame=frame, direction=direction) + + def foldAllFrames(self, except_frame=None): + for child in self.hardware_frame.winfo_children(): + if isinstance(child, CollapsibleFrame) and child is not except_frame: + child.fold() + for child in self.bottom_frame.winfo_children(): + if isinstance(child, CollapsibleFrame) and child is not except_frame: + child.fold() + + def create_toggle_function(self, frame): + + def func(event): + self.foldAllFrames(frame) + frame.toggle_visibility() + + return func + + def build_event_handler(self, hardware_widgets, key, frame, frame_id): + + def func(*args, **kwargs): + v = hardware_widgets[key] + if "widgets" in v[2]: + if "parent" in v[2]: + parent = self.hardware_frame if v[2]["parent"].startswith("hardware") else None + else: + parent_id = frame.winfo_parent() + parent = self.nametowidget(parent_id) + widgets = hardware_widgets if v[2]["widgets"] == "self" else v[2]["widgets"] + self.build_widgets(widgets, parent=parent, ref=v[2].get("ref", None), direction=v[2].get("direction", "vertical")) + # collaps other frame + elif v[2].get("delete", False): + frame.grid_remove() + self.variables_list[frame_id-self.row_offset] = None + + return func diff --git a/src/navigate/view/custom_widgets/CollapsibleFrame.py b/src/navigate/view/custom_widgets/CollapsibleFrame.py new file mode 100644 index 000000000..f6236f2e4 --- /dev/null +++ b/src/navigate/view/custom_widgets/CollapsibleFrame.py @@ -0,0 +1,33 @@ +import tkinter as tk + +class CollapsibleFrame(tk.Frame): + def __init__(self, parent, title="", *args, **kwargs): + tk.Frame.__init__(self, parent, *args, **kwargs) + + self.title = title + self.visible = False + + # Create a label to act as a title/header + self.label = tk.Label(self, text=self.title, bg="lightgrey", relief="raised", padx=5) + self.label.grid(row=0, column=0, sticky=tk.NSEW) + + # Create a frame to hold the contents of the collapsible frame + self.content_frame = tk.Frame(self) + self.toggle_visibility() + + self.label.bind("", lambda event: self.toggle_visibility()) + + def toggle_visibility(self): + if self.visible: + self.label["text"] = self.title + " " + "\u25BC" + self.content_frame.grid_forget() # Hide the content frame + self.visible = False + else: + self.label["text"] = self.title + " " + "\u25B2" + self.content_frame.grid(row=1, column=0, sticky=tk.NSEW) # Show the content frame + self.visible = True + + def fold(self): + if self.visible: + self.toggle_visibility() + From e1da1e76f0b10c4958706588fbfcdfb9d2971496 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Sat, 4 May 2024 18:30:33 -0700 Subject: [PATCH 11/17] remove "gui" section from configuration --- .../camera_setting_controller.py | 12 +++---- .../channel_setting_controller.py | 36 +++++++++---------- .../channels_tab_controller.py | 7 ++-- .../view/main_window_content/channels_tab.py | 20 +++++------ 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/navigate/controller/sub_controllers/camera_setting_controller.py b/src/navigate/controller/sub_controllers/camera_setting_controller.py index 76415b145..bc4b5f71b 100644 --- a/src/navigate/controller/sub_controllers/camera_setting_controller.py +++ b/src/navigate/controller/sub_controllers/camera_setting_controller.py @@ -540,12 +540,12 @@ def update_camera_device_related_setting(self): self.trigger_active = camera_config_dict["trigger_active"] self.readout_speed = camera_config_dict["readout_speed"] # framerate_widgets - self.framerate_widgets["exposure_time"].widget.min = camera_config_dict[ - "exposure_time_range" - ]["min"] - self.framerate_widgets["exposure_time"].widget.max = camera_config_dict[ - "exposure_time_range" - ]["max"] + # self.framerate_widgets["exposure_time"].widget.min = camera_config_dict[ + # "exposure_time_range" + # ]["min"] + # self.framerate_widgets["exposure_time"].widget.max = camera_config_dict[ + # "exposure_time_range" + # ]["max"] # roi max width and height self.roi_widgets["Width"].widget.config(to=self.default_width) diff --git a/src/navigate/controller/sub_controllers/channel_setting_controller.py b/src/navigate/controller/sub_controllers/channel_setting_controller.py index 61714abc8..eb2b087bc 100644 --- a/src/navigate/controller/sub_controllers/channel_setting_controller.py +++ b/src/navigate/controller/sub_controllers/channel_setting_controller.py @@ -286,19 +286,19 @@ def update_setting_dict(setting_dict, widget_name): except Exception: setting_dict[widget_name] = 0 return False - ref_name = ( - "exposure_time" - if widget_name == "camera_exposure_time" - else widget_name - ) - setting_range = self.parent_controller.parent_controller.configuration[ - "configuration" - ]["gui"]["channels"][ref_name] - if ( - setting_dict[widget_name] < setting_range["min"] - or setting_dict[widget_name] > setting_range["max"] - ): - return False + # ref_name = ( + # "exposure_time" + # if widget_name == "camera_exposure_time" + # else widget_name + # ) + # setting_range = self.parent_controller.parent_controller.configuration[ + # "configuration" + # ]["gui"]["channels"][ref_name] + # if ( + # setting_dict[widget_name] < setting_range["min"] + # or setting_dict[widget_name] > setting_range["max"] + # ): + # return False else: setting_dict[widget_name] = channel_vals[widget_name].get() @@ -429,20 +429,20 @@ def verify_experiment_values(self): Warning info """ selected_channel_num = 0 - setting_range = self.configuration_controller.gui_setting["channels"] for channel_key in self.channel_setting_dict.keys(): setting_dict = self.channel_setting_dict[channel_key] + idx = int(channel_key[len("channel_"):]) - 1 if setting_dict["is_selected"]: selected_channel_num += 1 # laser power - if setting_dict["laser_power"] < setting_range["laser_power"]["min"]: + if setting_dict["laser_power"] < self.view.laserpower_pulldowns[idx]["from"]: return f"Laser power below configured threshold. Please adjust to meet or exceed the specified minimum in the configuration.yaml({setting_range['laser_power']['min']})." - elif setting_dict["laser_power"] > setting_range["laser_power"]["max"]: + elif setting_dict["laser_power"] > self.view.laserpower_pulldowns[idx]["to"]: return f"Laser power exceeds configured maximum. Please adjust to meet or be below the specified maximum in the configuration.yaml({setting_range['laser_power']['max']})." # exposure time - if setting_dict["camera_exposure_time"] < setting_range["exposure_time"]["min"]: + if setting_dict["camera_exposure_time"] < self.view.exptime_pulldowns[idx]["from"]: return f"Exposure time below configured threshold.Please adjust to meet or exceed the specified minimum in the configuration.yaml({setting_range['exposure_time']['min']})." - elif setting_dict["camera_exposure_time"] > setting_range["exposure_time"]["max"]: + elif setting_dict["camera_exposure_time"] > self.view.exptime_pulldowns[idx]["to"]: return f"Exposure time exceeds configured maximum. Please adjust to meet or be below the specified maximum in the configuration.yaml({setting_range['exposure_time']['max']})" if selected_channel_num == 0: diff --git a/src/navigate/controller/sub_controllers/channels_tab_controller.py b/src/navigate/controller/sub_controllers/channels_tab_controller.py index b32be536d..d6123d970 100644 --- a/src/navigate/controller/sub_controllers/channels_tab_controller.py +++ b/src/navigate/controller/sub_controllers/channels_tab_controller.py @@ -113,8 +113,6 @@ def __init__(self, view, parent_controller=None): self.z_origin = 0 #: float: The focus origin of the stack. self.focus_origin = 0 - #: float: The stage velocity. - self.stage_velocity = None #: float: The filter wheel delay. self.filter_wheel_delay = None #: dict: The microscope state dictionary. @@ -181,10 +179,9 @@ def initialize(self): config = self.parent_controller.configuration_controller self.stack_acq_widgets["cycling"].widget["values"] = ["Per Z", "Per Stack"] - self.stage_velocity = config.stage_setting_dict["velocity"] self.filter_wheel_delay = config.filter_wheel_setting_dict["filter_wheel_delay"] self.channel_setting_controller.initialize() - self.set_spinbox_range_limits(config.configuration["configuration"]["gui"]) + # self.set_spinbox_range_limits(config.configuration["configuration"]["gui"]) self.show_verbose_info("channels tab has been initialized") def populate_experiment_values(self): @@ -648,7 +645,7 @@ def update_timepoint_setting(self, call_parent=False): # time. Probably assemble a matrix of all the positions and then do # the calculations. - stage_delay = 0 # distance[max_distance_idx]/self.stage_velocity + stage_delay = 0 # TODO False value. # If we were actually acquiring the data, we would call the function to diff --git a/src/navigate/view/main_window_content/channels_tab.py b/src/navigate/view/main_window_content/channels_tab.py index 0474a9f1f..58a1ccef5 100644 --- a/src/navigate/view/main_window_content/channels_tab.py +++ b/src/navigate/view/main_window_content/channels_tab.py @@ -289,9 +289,9 @@ def populate_frame(self, channels): ValidatedSpinbox( self.frame_columns[4], from_=0, - to=5000.0, + to=1000.0, textvariable=self.exptime_variables[num], - increment=25, + increment=5, width=5, font=tk.font.Font(size=11), ) @@ -306,9 +306,9 @@ def populate_frame(self, channels): ValidatedSpinbox( self.frame_columns[5], from_=0, - to=5000.0, + to=1000.0, textvariable=self.interval_variables[num], - increment=1, + increment=5, width=3, font=tk.font.Font(size=11), ) @@ -394,7 +394,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=start_labels[i], input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"from_": 0.0, "to": 10000, "increment": 0.5, "width": 6}, + input_args={"from_": -5000, "to": 10000, "increment": 1, "width": 6}, ) self.inputs[start_names[i]].grid( row=i + 1, column=0, sticky="N", pady=2, padx=(6, 0) @@ -420,7 +420,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=end_labels[i], input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"from_": 0.0, "to": 10000, "increment": 0.5, "width": 6}, + input_args={"from_": -5000, "to": 10000, "increment": 1, "width": 6}, ) self.inputs[end_names[i]].grid( row=i + 1, column=1, sticky="N", pady=2, padx=(6, 0) @@ -439,7 +439,7 @@ def __init__(self, settings_tab, *args, **kwargs): parent=self.pos_slice, input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"width": 6}, + input_args={"from_": 0.1, "to": 1000, "increment": 0.1, "width": 6}, ) self.inputs["step_size"].grid(row=1, column=2, sticky="N", padx=6) @@ -455,7 +455,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=slice_labels[i], input_class=ttk.Spinbox, input_var=tk.DoubleVar(), - input_args={"increment": 0.5, "width": 6}, + input_args={"from_": 0.1, "to": 1000, "increment": 0.1, "width": 6}, ) self.inputs[slice_names[i]].widget.configure(state="disabled") self.inputs[slice_names[i]].grid( @@ -603,7 +603,7 @@ def __init__(self, settings_tab, *args, **kwargs): from_=0, to=5000.0, textvariable=self.stack_acq_spinval, # this holds the data in the entry - increment=25, + increment=1, width=6, ) self.stack_acq_spinbox.grid(row=2, column=1, sticky=tk.NSEW, pady=2) @@ -623,7 +623,7 @@ def __init__(self, settings_tab, *args, **kwargs): from_=0, to=5000.0, textvariable=self.stack_pause_spinval, - increment=25, + increment=1, width=6, ) self.stack_pause_spinbox.grid(row=0, column=3, sticky=tk.NSEW, pady=2) From bb0eb174fee5491d4dfe783f5ca363779147d57c Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Sat, 4 May 2024 18:32:10 -0700 Subject: [PATCH 12/17] remove "sweep_time" from configuration --- src/navigate/model/devices/galvo/galvo_base.py | 4 +--- src/navigate/model/devices/remote_focus/remote_focus_base.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/navigate/model/devices/galvo/galvo_base.py b/src/navigate/model/devices/galvo/galvo_base.py index 37b40e432..74173041d 100644 --- a/src/navigate/model/devices/galvo/galvo_base.py +++ b/src/navigate/model/devices/galvo/galvo_base.py @@ -81,9 +81,7 @@ def __init__(self, microscope_name, device_connection, configuration, galvo_id=0 ]["daq"]["sample_rate"] #: float: Sweep time. - self.sweep_time = configuration["configuration"]["microscopes"][ - microscope_name - ]["daq"]["sweep_time"] + self.sweep_time = 0 #: float: Camera delay self.camera_delay = configuration["configuration"]["microscopes"][ diff --git a/src/navigate/model/devices/remote_focus/remote_focus_base.py b/src/navigate/model/devices/remote_focus/remote_focus_base.py index 51adbd8a4..7480fabbb 100644 --- a/src/navigate/model/devices/remote_focus/remote_focus_base.py +++ b/src/navigate/model/devices/remote_focus/remote_focus_base.py @@ -80,9 +80,7 @@ def __init__(self, microscope_name, device_connection, configuration): ]["daq"]["sample_rate"] #: float: Sweep time of the DAQ. - self.sweep_time = configuration["configuration"]["microscopes"][ - microscope_name - ]["daq"]["sweep_time"] + self.sweep_time = 0 #: float: Camera delay percent. self.camera_delay = configuration["configuration"]["microscopes"][ From 9b8863a72a7192dd5b3695ea712a12f55d0d7e14 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Sat, 4 May 2024 18:32:43 -0700 Subject: [PATCH 13/17] generate hardware header section --- src/navigate/config/config.py | 76 +++++++++++++++---- src/navigate/config/configuration_database.py | 18 +++-- src/navigate/controller/configurator.py | 2 +- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index 8ddc7653d..5b4ac2acb 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -44,7 +44,8 @@ import yaml # Local Imports - +from navigate.tools.common_functions import build_ref_name +from navigate.tools.file_functions import save_yaml_file def get_navigate_path(): """Establish a program home directory in AppData/Local/.navigate for Windows @@ -764,10 +765,8 @@ def verify_waveform_constants(manager, configuration): waveform_dict[microscope_name][zoom], laser, { - "amplitude": config_dict["remote_focus_device"][ - "amplitude" - ], - "offset": config_dict["remote_focus_device"]["offset"], + "amplitude": 0, + "offset": 0, # "percent_smoothing": "0", # "delay": config_dict["remote_focus_device"][ # "delay" @@ -864,9 +863,9 @@ def verify_waveform_constants(manager, configuration): waveform_dict[microscope_name], zoom, { - "amplitude": "0.11", - "offset": config_dict["galvo"][i]["offset"], - "frequency": config_dict["galvo"][i]["frequency"], + "amplitude": "0", + "offset": 0, + "frequency": 10, }, ) else: @@ -896,12 +895,8 @@ def verify_waveform_constants(manager, configuration): other_constants_dict = { "remote_focus_settle_duration": "0", "percent_smoothing": "0", - "remote_focus_delay": config_dict["remote_focus_device"][ - "delay" - ], - "remote_focus_ramp_falling": config_dict["remote_focus_device"][ - "ramp_falling" - ] + "remote_focus_delay": "0", + "remote_focus_ramp_falling": "5" } if ( "other_constants" not in waveform_dict.keys() @@ -924,7 +919,16 @@ def verify_configuration(manager, configuration): Supports old version of configurations. """ + channel_count = 5 + # generate hardware header section device_config = configuration["configuration"]["microscopes"] + hardware_dict = {} + ref_list = { + "camera": [], + "stage": [], + "zoom": None, + "mirror": None, + } for microscope_name in device_config.keys(): # camera # delay_percent -> delay @@ -938,3 +942,47 @@ def verify_configuration(manager, configuration): remote_focus_config["ramp_falling"] = remote_focus_config.get("ramp_falling_percent", 5) if "delay" not in remote_focus_config.keys(): remote_focus_config["delay"] = remote_focus_config.get("delay_percent", 0) + + # daq + daq_type = device_config[microscope_name]["daq"]["hardware"]["type"] + if not daq_type.lower().startswith("synthetic"): + hardware_dict["daq"] = {"type": daq_type} + + # camera + if "camera" not in hardware_dict: + hardware_dict["camera"] = [] + camera_idx = build_ref_name("-", camera_config["hardware"]["type"], camera_config["hardware"]["serial_number"]) + if camera_idx not in ref_list["camera"]: + ref_list["camera"].append(camera_idx) + hardware_dict["camera"].append(camera_config["hardware"]) + + channel_count = max(channel_count, camera_config["count"]) + + # zoom (one zoom) + if "zoom" not in hardware_dict: + zoom_config = device_config[microscope_name]["zoom"]["hardware"] + # zoom_idx = build_ref_name("-", zoom_config["type"], zoom_config["servo_id"]) + hardware_dict["zoom"] = zoom_config + + # filter wheel + if "filter_wheel" not in hardware_dict: + filter_wheel_config = device_config[microscope_name]["filter_wheel"]["hardware"] + hardware_dict["filter_wheel"] = filter_wheel_config + # stage + if "stage" not in hardware_dict: + hardware_dict["stage"] = [] + stages = device_config[microscope_name]["stage"]["hardware"] + if type(stages) != ListProxy: + stages = [stages] + for i, stage in enumerate(stages): + stage_idx = build_ref_name("-", stage["type"], stage["serial_number"]) + if stage_idx not in ref_list["stage"]: + hardware_dict["stage"].append(stage) + + # mirror + if "mirror" in device_config[microscope_name].keys() and "mirror" not in hardware_dict: + hardware_dict["mirror"] = device_config[microscope_name]["mirror"]["hardware"] + + update_config_dict(manager, configuration["configuration"], "hardware", hardware_dict) + + update_config_dict(manager, configuration["configuration"], "gui", {"channels": {"count": channel_count}}) diff --git a/src/navigate/config/configuration_database.py b/src/navigate/config/configuration_database.py index 00818e9c5..783eab391 100644 --- a/src/navigate/config/configuration_database.py +++ b/src/navigate/config/configuration_database.py @@ -9,10 +9,12 @@ camera_hardware_widgets = { "hardware/type": ["Device Type", "Combobox", "string", camera_device_types], "hardware/serial_number": ["Serial Number", "Input", "string", None], + "hardware/camera_connection": ["Camera Connection", "Input", "string",], "defect_correct_mode": ["Defect Correct Mode", "Combobox", "string", {"On": 2.0, "Off": 1.0}], "delay": ["Delay (ms)", "Spinbox", "float", None], "flip_x": ["Flip X", "Checkbutton", "bool", None], "flip_y": ["Flip Y", "Checkbutton", "bool", None], + "count": ["Microscope Channel Count", "Spinbox", "int", {"from": 1, "to": 10, "step": 1}] } filter_wheel_device_types = { @@ -96,6 +98,7 @@ } stage_constants_widgets = { + "joystick_axes": ["Joystick Axes", "Input", "string", None], "x_min": ["Min X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], "x_max": ["Max X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], "y_min": ["Min Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], @@ -123,8 +126,8 @@ remote_focus_hardware_widgets = { "type": ["Device Type", "Combobox", "string", remote_focus_device_types], "channel": ["DAQ Channel", "Input", "string", None], - "min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], - "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 1}], + "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 1}], "comport": ["Serial Port", "Input", "string", None], "baudrate": ["Baudrate", "Input", "int", None], "frame_config": {"ref": "hardware"} @@ -144,7 +147,7 @@ galvo_hardware_widgets = { "hardware/type": ["Device Type", "Combobox", "string", galvo_device_types], "hardware/channel": ["DAQ Channel", "Input", "string", None], - "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], + "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 0.1}], "hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], "waveform": ["Waveform", "Combobox", "string", waveform_types], "phase": ["Phase", "Input", "string", None], @@ -172,7 +175,10 @@ zoom_hardware_widgets = { "type": ["Device Type", "Combobox", "string", zoom_device_types,], "servo_id": ["Servo ID", "Input", "string", None], - "button_1": ["Add Zoom Value", "Button", {"widgets":zoom_position_widgets, "ref": "position;pixel_size", "direction": "horizon"}] + "port": ["Serial Port", "Input", "string", None], + "baudrate": ["Baudrate", "Input", "int", None], + "button_1": ["Add Zoom Value", "Button", {"widgets":zoom_position_widgets, "ref": "position;pixel_size", "direction": "horizon"}], + "frame_config": {"ref": "hardware"} } mirror_device_types = { @@ -203,11 +209,11 @@ "power/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], "power/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], "button_1": ["Delete", "Button", {"delete": True}], - "frame_config": {"collapsible": True, "title": "Wavelength"} + "frame_config": {"collapsible": True, "title": "Wavelength", "format": "list-dict", "ref": "None"} } laser_top_widgets = { - "button_1": ["Add Wavelength", "Button", {"widgets": laser_hardware_widgets, "ref": "", "parent": "hardware"}], + "button_1": ["Add Wavelength", "Button", {"widgets": laser_hardware_widgets, "parent": "hardware"}], } hardwares_dict = { diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index f649c7892..4a1c8535e 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -191,7 +191,7 @@ def write_func(prefix, config_dict, f): def create_config_window(self, id): """Creates the configuration window tabs.""" - tab_name = "Microscope " + str(id) + tab_name = "Microscope-" + str(id) microscope_tab = MicroscopeTab( self.view.microscope_window, name=tab_name, From a3643b2fd7d36433920e3c1ccc023afb29753a67 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Sat, 4 May 2024 18:38:42 -0700 Subject: [PATCH 14/17] fix error --- src/navigate/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index 5b4ac2acb..a7496123d 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -956,7 +956,7 @@ def verify_configuration(manager, configuration): ref_list["camera"].append(camera_idx) hardware_dict["camera"].append(camera_config["hardware"]) - channel_count = max(channel_count, camera_config["count"]) + channel_count = max(channel_count, camera_config.get("count", 5)) # zoom (one zoom) if "zoom" not in hardware_dict: From 4b906ca828072764fe5a6df0ac32e2ba51719299 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Sat, 4 May 2024 21:14:09 -0700 Subject: [PATCH 15/17] fix tests --- src/navigate/config/config.py | 1 - test/config/test_config.py | 1 + .../sub_controllers/test_camera_setting_controller.py | 8 -------- test/model/devices/galvo/test_galvo_base.py | 2 +- test/model/devices/galvo/test_galvo_ni.py | 1 - 5 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index a7496123d..f0948b27a 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -45,7 +45,6 @@ # Local Imports from navigate.tools.common_functions import build_ref_name -from navigate.tools.file_functions import save_yaml_file def get_navigate_path(): """Establish a program home directory in AppData/Local/.navigate for Windows diff --git a/test/config/test_config.py b/test/config/test_config.py index b1b8c268c..9fae9a515 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -78,6 +78,7 @@ def test_config_methods(): "verify_waveform_constants", "verify_configuration", "yaml", + "build_ref_name" ] for method in methods: assert method in desired_methods diff --git a/test/controller/sub_controllers/test_camera_setting_controller.py b/test/controller/sub_controllers/test_camera_setting_controller.py index 260534b02..3d9afbf46 100644 --- a/test/controller/sub_controllers/test_camera_setting_controller.py +++ b/test/controller/sub_controllers/test_camera_setting_controller.py @@ -115,14 +115,6 @@ def test_init(self): assert self.camera_settings.mode_widgets["Pixels"].widget.cget("increment") == 1 # Framerate - assert ( - self.camera_settings.framerate_widgets["exposure_time"].widget.min - == camera_config_dict["exposure_time_range"]["min"] - ) - assert ( - self.camera_settings.framerate_widgets["exposure_time"].widget.max - == camera_config_dict["exposure_time_range"]["max"] - ) assert ( str(self.camera_settings.framerate_widgets["exposure_time"].widget["state"]) == "disabled" diff --git a/test/model/devices/galvo/test_galvo_base.py b/test/model/devices/galvo/test_galvo_base.py index 4be9e0910..066fad05d 100644 --- a/test/model/devices/galvo/test_galvo_base.py +++ b/test/model/devices/galvo/test_galvo_base.py @@ -85,7 +85,7 @@ def test_galvo_base_initialization(self): assert self.galvo.microscope_name == "Mesoscale" assert self.galvo.galvo_name == "Galvo 0" assert self.galvo.sample_rate == 100000 - assert self.galvo.sweep_time == 0.2 + assert self.galvo.camera_delay == self.configuration["configuration"]["microscopes"][ self.microscope_name ]["camera"]["delay"] / 1000 diff --git a/test/model/devices/galvo/test_galvo_ni.py b/test/model/devices/galvo/test_galvo_ni.py index 77c5a425b..1132e6aad 100644 --- a/test/model/devices/galvo/test_galvo_ni.py +++ b/test/model/devices/galvo/test_galvo_ni.py @@ -83,7 +83,6 @@ def test_galvo_ni_initialization(self): assert self.galvo.microscope_name == "Mesoscale" assert self.galvo.galvo_name == "Galvo 0" assert self.galvo.sample_rate == 100000 - assert self.galvo.sweep_time == 0.2 assert self.galvo.camera_delay == self.configuration["configuration"]["microscopes"][ self.microscope_name ]["camera"]["delay"] / 1000 From 03fbe1b72715e8b60896859d4afbaa1d77f603d0 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Thu, 9 May 2024 21:58:55 -0700 Subject: [PATCH 16/17] load configuration file --- src/navigate/config/config.py | 4 + src/navigate/config/configuration_database.py | 178 +++++++------- src/navigate/controller/configurator.py | 223 ++++++++++++++++-- .../view/configurator_application_window.py | 113 ++++++--- 4 files changed, 380 insertions(+), 138 deletions(-) diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index f0948b27a..1b538c1fc 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -982,6 +982,10 @@ def verify_configuration(manager, configuration): if "mirror" in device_config[microscope_name].keys() and "mirror" not in hardware_dict: hardware_dict["mirror"] = device_config[microscope_name]["mirror"]["hardware"] + if "daq" not in hardware_dict: + hardware_dict["daq"] = { + "type": "synthetic" + } update_config_dict(manager, configuration["configuration"], "hardware", hardware_dict) update_config_dict(manager, configuration["configuration"], "gui", {"channels": {"count": channel_count}}) diff --git a/src/navigate/config/configuration_database.py b/src/navigate/config/configuration_database.py index 783eab391..2c69fd408 100644 --- a/src/navigate/config/configuration_database.py +++ b/src/navigate/config/configuration_database.py @@ -7,14 +7,14 @@ } camera_hardware_widgets = { - "hardware/type": ["Device Type", "Combobox", "string", camera_device_types], - "hardware/serial_number": ["Serial Number", "Input", "string", None], - "hardware/camera_connection": ["Camera Connection", "Input", "string",], - "defect_correct_mode": ["Defect Correct Mode", "Combobox", "string", {"On": 2.0, "Off": 1.0}], - "delay": ["Delay (ms)", "Spinbox", "float", None], - "flip_x": ["Flip X", "Checkbutton", "bool", None], - "flip_y": ["Flip Y", "Checkbutton", "bool", None], - "count": ["Microscope Channel Count", "Spinbox", "int", {"from": 1, "to": 10, "step": 1}] + "hardware/type": ["Device Type", "Combobox", "string", camera_device_types, None], + "hardware/serial_number": ["Serial Number", "Input", "string", None, 'Example: "302352"'], + "hardware/camera_connection": ["Camera Connection", "Input", "string", None, "*Photometrics Iris 15B only"], + "defect_correct_mode": ["Defect Correct Mode", "Combobox", "string", {"On": 2.0, "Off": 1.0}, None], + "delay": ["Delay (ms)", "Spinbox", "float", None, None], + "flip_x": ["Flip X", "Checkbutton", "bool", None, None], + "flip_y": ["Flip Y", "Checkbutton", "bool", None, None], + "count": ["Microscope Channel Count", "Spinbox", "int", {"from": 5, "to": 10, "step": 1}, None] } filter_wheel_device_types = { @@ -24,18 +24,18 @@ } filter_wheel_widgets = { - "filter_name": ["Filter Name", "Input", "string", None], - "filter_value": ["Filter Value", "Input", "string", None], + "filter_name": ["Filter Name", "Input", "string", None, "Example: Empty-Alignment"], + "filter_value": ["Filter Value", "Input", "string", None, "Example: 0"], "button_1": ["Delete", "Button", {"delete": True}], - "frame_config": {"format": "item(filter_name,filter_value)"} + "frame_config": {"ref": "available_filters", "format": "item(filter_name,filter_value),", "direction": "horizon"} } filter_wheel_hardware_widgets = { - "hardware/type": ["Device Type", "Combobox", "string", filter_wheel_device_types,], - "hardware/wheel_number": ["Number of Wheels", "Spinbox", "int", None], - "hardware/port": ["Serial Port", "Input", "string", None], - "hardware/baudrate": ["Baudrate", "Input", "int", None], - "filter_wheel_delay": ["Filter Wheel Delay (s)", "Input", "float", None], + "hardware/type": ["Device Type", "Combobox", "string", filter_wheel_device_types, None], + "hardware/wheel_number": ["Number of Wheels", "Spinbox", "int", None, "Example: 1"], + "hardware/port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "hardware/baudrate": ["Baudrate", "Input", "int", None, "Example: 9200"], + "filter_wheel_delay": ["Filter Wheel Delay (s)", "Input", "float", None, "Example: 0.03"], "button_1": ["Add Available Filters", "Button", {"widgets":filter_wheel_widgets, "ref": "available_filters", "direction": "horizon"}] } @@ -44,13 +44,13 @@ } daq_hardware_widgets = { - "hardware/type": ["Device Type", "Combobox", "string", daq_device_types,], - "sample_rate": ["Sample Rate", "Input", "int", None], - "master_trigger_out_line": ["Master Trigger Out", "Input", "string", None], - "camera_trigger_out_line": ["Camera Trigger Out", "Input", "string", None], - "trigger_source": ["Trigger Source", "Input", "string", None], - "laser_port_switcher": ["Laser Switcher Port", "Input", "string", None], - "laser_switch_state": ["Laser Switch On State", "Combobox", "bool", [True, False]], + "hardware/type": ["Device Type", "Combobox", "string", daq_device_types, None], + "sample_rate": ["Sample Rate", "Input", "int", None, "Example: 9600"], + "master_trigger_out_line": ["Master Trigger Out", "Input", "string", None, "Example: PXI6259/port0/line1"], + "camera_trigger_out_line": ["Camera Trigger Out", "Input", "string", None, "Example: /PXI6259/ctr0"], + "trigger_source": ["Trigger Source", "Input", "string", None, "Example: /PXI6259/PFI0"], + "laser_port_switcher": ["Laser Switcher Port", "Input", "string", None, "Example: PXI6733/port0/line0"], + "laser_switch_state": ["Laser Switch On State", "Combobox", "bool", [True, False], None], } shutter_device_types = { @@ -59,10 +59,10 @@ } shutter_hardware_widgets = { - "type": ["Device Type", "Combobox", "string", shutter_device_types,], - "channel": ["NI Channel", "Input", "string", None], - "min": ["Minimum Voltage", "Spinbox", "float", None], - "max": ["Maximum Voltage", "Spinbox", "float", None], + "type": ["Device Type", "Combobox", "string", shutter_device_types, None], + "channel": ["NI Channel", "Input", "string", None, "Example: PXI6259/port0/line0"], + "min": ["Minimum Voltage", "Spinbox", "float", None, "Example: 0"], + "max": ["Maximum Voltage", "Spinbox", "float", None, "Example: 5"], "frame_config": {"ref": "hardware"} } @@ -77,18 +77,18 @@ } stage_hardware_widgets = { - "type": ["Device Type", "Combobox", "string", stage_device_types,], - "serial_number": ["Serial Number", "Input", "string", None], - "axes": ["Axes", "Input", "string", None,], - "axes_mapping": ["Axes Mapping", "Input", "string", None], - "volts_per_micron": ["Volts Per Micron", "Spinbox", "float", {"from": 0, "to": 100, "step":0.1}, ("type", "GalvoNIStage")], - "min": ["Minimum Volts", "Spinbox", "float", {"from": 0, "to": 5, "step": 0.1}, ("type", "GalvoNIStage")], - "max": ["Maximum Volts", "Spinbox", "float", {"from": 1, "to": 100, "step": 0.1}, ("type", "GalvoNIStage")], - "controllername": ["Controller Name", "Input", "string", None, ("type", "PI")], - "stages": ["PI Stages", "Input", "string", None, ("type", "PI")], - "refmode": ["REF Modes", "Input", "string", None, ("type", "PI")], - "port": ["Serial Port", "Input", "string", None], - "baudrate": ["Baudrate", "Input", "int", None], + "type": ["Device Type", "Combobox", "string", stage_device_types, None], + "serial_number": ["Serial Number", "Input", "string", None, None], + "axes": ["Axes", "Input", "string", None, "Example: [x, y, z]"], + "axes_mapping": ["Axes Mapping", "Input", "string", None, "Example: [X, M, Y]"], + "volts_per_micron": ["Volts Per Micron", "Spinbox", "float", {"from": 0, "to": 100, "step":0.1}, "*Analog/Digital Device only"], + "min": ["Minimum Volts", "Spinbox", "float", {"from": 0, "to": 5, "step": 0.1}, "*Analog/Digital Device only",], + "max": ["Maximum Volts", "Spinbox", "float", {"from": 1, "to": 100, "step": 0.1}, "*Analog/Digital Device only",], + "controllername": ["Controller Name", "Input", "string", None, "*Physik Instrumente only. Example: 'C-884'"], + "stages": ["PI Stages", "Input", "string", None, "*Physik Instrumente only. Example: L-509.20DG10 L-509.40DG10"], + "refmode": ["REF Modes", "Input", "string", None, "*Physik Instrumente only. Example: FRF FRF"], + "port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "baudrate": ["Baudrate", "Input", "int", None, "Example: 9200"], "button_2": ["Delete", "Button", {"delete": True}], "frame_config": {"collapsible": True, "title": "Stage", "ref": "hardware", "format": "list-dict"} } @@ -98,22 +98,22 @@ } stage_constants_widgets = { - "joystick_axes": ["Joystick Axes", "Input", "string", None], - "x_min": ["Min X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "x_max": ["Max X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "y_min": ["Min Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "y_max": ["Max Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "z_min": ["Min Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "z_max": ["Max Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "theta_min": ["Min Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "theta_max": ["Max Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "f_min": ["Min Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "f_max": ["Max Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "x_offset": ["Offset of X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "y_offset": ["Offset of Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "z_offset": ["Offset of Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "theta_offset": ["Offset of Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], - "f_offset": ["Offset of Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}], + "joystick_axes": ["Joystick Axes", "Input", "string", None, "Example: [x, y, z]"], + "x_min": ["Min X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "x_max": ["Max X", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "y_min": ["Min Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "y_max": ["Max Y", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "z_min": ["Min Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "z_max": ["Max Z", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "theta_min": ["Min Theta", "Spinbox", "float", {"from": 0, "to": 360, "step": 1000}, None], + "theta_max": ["Max Theta", "Spinbox", "float", {"from": 0, "to": 360, "step": 1000}, None], + "f_min": ["Min Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "f_max": ["Max Focus", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "x_offset": ["Offset of X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "y_offset": ["Offset of Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "z_offset": ["Offset of Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "theta_offset": ["Offset of Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "f_offset": ["Offset of Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], "frame_config": {"collapsible": True, "title": "Stage Constants"} } @@ -124,12 +124,12 @@ } remote_focus_hardware_widgets = { - "type": ["Device Type", "Combobox", "string", remote_focus_device_types], - "channel": ["DAQ Channel", "Input", "string", None], - "min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 1}], - "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 1}], - "comport": ["Serial Port", "Input", "string", None], - "baudrate": ["Baudrate", "Input", "int", None], + "type": ["Device Type", "Combobox", "string", remote_focus_device_types, None], + "channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6259/ao3"], + "min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 1}, None], + "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 1}, None], + "comport": ["Serial Port", "Input", "string", None, "*Equipment Solutions only"], + "baudrate": ["Baudrate", "Input", "int", None, "*Equipment Solutions only. Example: 9200"], "frame_config": {"ref": "hardware"} } @@ -145,12 +145,12 @@ } galvo_hardware_widgets = { - "hardware/type": ["Device Type", "Combobox", "string", galvo_device_types], - "hardware/channel": ["DAQ Channel", "Input", "string", None], - "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 0.1}], - "hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}], - "waveform": ["Waveform", "Combobox", "string", waveform_types], - "phase": ["Phase", "Input", "string", None], + "hardware/type": ["Device Type", "Combobox", "string", galvo_device_types, None], + "hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6259/ao1"], + "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 0.1}, None], + "hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}, None], + "waveform": ["Waveform", "Combobox", "string", waveform_types, None], + "phase": ["Phase", "Input", "string", None, "Example: 1.57"], "button_1": ["Delete", "Button", {"delete": True}], "frame_config": {"collapsible": True, "title": "Galvo Device", "ref": "None", "format": "list-dict"} } @@ -165,18 +165,18 @@ } zoom_position_widgets = { - "zoom_value": ["Zoom Value", "Input", "string", None], - "position": ["Position", "Input", "float", None], - "pixel_size": ["Pixel Size (um)", "Input", "float", None], + "zoom_value": ["Zoom Value", "Input", "string", None, "Example: 16x"], + "position": ["Position", "Input", "float", None, "Example: 1000"], + "pixel_size": ["Pixel Size (um)", "Input", "float", None, "Example: 0.5"], "button_1": ["Delete", "Button", {"delete": True}], - "frame_config": {"ref": "position;pixel_size", "format": "item(zoom_value, position);item(zoom_value, pixel_size)"} + "frame_config": {"ref": "position;pixel_size", "format": "item(zoom_value, position);item(zoom_value, pixel_size)", "direction": "horizon"} } zoom_hardware_widgets = { - "type": ["Device Type", "Combobox", "string", zoom_device_types,], - "servo_id": ["Servo ID", "Input", "string", None], - "port": ["Serial Port", "Input", "string", None], - "baudrate": ["Baudrate", "Input", "int", None], + "type": ["Device Type", "Combobox", "string", zoom_device_types, None], + "servo_id": ["Servo ID", "Input", "string", None, "Example: 1"], + "port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "baudrate": ["Baudrate", "Input", "int", None, "Example: 9600"], "button_1": ["Add Zoom Value", "Button", {"widgets":zoom_position_widgets, "ref": "position;pixel_size", "direction": "horizon"}], "frame_config": {"ref": "hardware"} } @@ -187,7 +187,7 @@ } mirror_hardware_widgets = { - "type": ["Device Type", "Combobox", "string", mirror_device_types,], + "type": ["Device Type", "Combobox", "string", mirror_device_types, None], "frame_config": {"ref": "hardware"} } @@ -197,17 +197,17 @@ } laser_hardware_widgets = { - "wavelength": ["Wavelength", "Input", "int", None], - "onoff": ["On/Off Setting", "Label", None, None], - "onoff/hardware/type": ["Type", "Combobox", "string", laser_device_types], - "onoff/hardware/channel": ["DAQ Channel", "Input", "string", None], - "onoff/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], - "onoff/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], - "power": ["Power Setting", "Label", None, None], - "power/hardware/type": ["Type", "Combobox", "string", laser_device_types], - "power/hardware/channel": ["DAQ Channel", "Input", "string", None], - "power/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], - "power/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}], + "wavelength": ["Wavelength", "Input", "int", None, None, "Example: 488"], + "onoff": ["On/Off Setting", "Label", None, None, None], + "onoff/hardware/type": ["Type", "Combobox", "string", laser_device_types, None], + "onoff/hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6733/port0/line2"], + "onoff/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "onoff/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "power": ["Power Setting", "Label", None, None, None], + "power/hardware/type": ["Type", "Combobox", "string", laser_device_types, None], + "power/hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6733/ao0"], + "power/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "power/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], "button_1": ["Delete", "Button", {"delete": True}], "frame_config": {"collapsible": True, "title": "Wavelength", "format": "list-dict", "ref": "None"} } @@ -219,14 +219,14 @@ hardwares_dict = { "Camera": camera_hardware_widgets, "Data Acquisition Card": daq_hardware_widgets, - "Filter Wheel": filter_wheel_hardware_widgets, + "Filter Wheel": (None, filter_wheel_hardware_widgets, filter_wheel_widgets), "Galvo": (galvo_top_widgets, galvo_hardware_widgets, None), "Lasers": (laser_top_widgets, laser_hardware_widgets, None), "Remote Focus Devices": remote_focus_hardware_widgets, "Adaptive Optics": mirror_hardware_widgets, "Shutters": shutter_hardware_widgets, "Stages": (stage_top_widgets, stage_hardware_widgets, stage_constants_widgets), - "Zoom Device": zoom_hardware_widgets + "Zoom Device": (None, zoom_hardware_widgets, zoom_position_widgets) } hardwares_config_name_dict = { diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 4a1c8535e..65e704a85 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -46,6 +46,7 @@ hardwares_dict, hardwares_config_name_dict, ) +from navigate.tools.file_functions import load_yaml_file # Logger Setup import logging @@ -79,6 +80,7 @@ def __init__(self, root, splash_screen): ) self.view.top_window.add_button.config(command=self.add_microscope) + self.view.top_window.load_button.config(command=self.load_configuration) self.view.top_window.save_button.config(command=self.save) self.view.top_window.cancel_button.config(command=self.on_cancel) self.create_config_window(0) @@ -100,9 +102,22 @@ def add_microscope(self): self.microscope_id += 1 def save(self): + """Save configuration file""" + def set_value(temp_dict, key_list, value): + """Set value + + Parameters + ---------- + temp_dict: dict + Target dictionary + key_list: list + keyword list + value: any + value of the item + """ if type(key_list) is list: - for i in range(len(key_list)-1): + for i in range(len(key_list) - 1): k = key_list[i] temp_dict[k] = temp_dict.get(k, {}) temp_dict = temp_dict[k] @@ -123,7 +138,9 @@ def set_value(temp_dict, key_list, value): hardware_name = microscope_tab.tab(hardware_tab_index, "text") hardware_tab = microscope_tab.nametowidget(hardware_tab_index) hardware_dict = {} - microscope_dict[hardwares_config_name_dict.get(hardware_name, hardware_name)] = hardware_dict + microscope_dict[ + hardwares_config_name_dict.get(hardware_name, hardware_name) + ] = hardware_dict for variable_list in hardware_tab.variables_list: if variable_list is None: continue @@ -143,11 +160,20 @@ def set_value(temp_dict, key_list, value): ref = ref_list[i] hardware_dict[ref] = hardware_dict.get(ref, {}) temp_dict = hardware_dict[ref] - k_idx = format[format.index("(")+1: format.index(",")].strip() - v_idx = format[format.index(",")+1:format.index(")")].strip() + k_idx = format[ + format.index("(") + 1 : format.index(",") + ].strip() + v_idx = format[ + format.index(",") + 1 : format.index(")") + ].strip() k = variables[k_idx].get() + if k.strip() == "": + print( + f"Notice: {hardware_name} has an empty value! Please double check!" + ) + if k_idx in value_dict: - k = value_dict[k_idx][v] + k = value_dict[k_idx][v] v = variables[v_idx].get() if v_idx in value_dict: v = value_dict[v_idx][v] @@ -157,21 +183,36 @@ def set_value(temp_dict, key_list, value): temp_dict = {} hardware_dict[ref] = hardware_dict.get("ref", temp_dict) for k, var in variables.items(): - if k in value_dict: - v = value_dict[k][var.get()] - else: - v = var.get() + try: + if k in value_dict: + v = value_dict[k][var.get()] + else: + v = var.get() + except tk._tkinter.TclError: + v = "" + print( + f"Notice: {hardware_name} has an empty value! Please double check!" + ) set_value(temp_dict, k.split("/"), v) self.write_to_yaml(config_dict, filename) def write_to_yaml(self, config, filename): + """write yaml file + + Parameters + ---------- + config: dict + configuration dictionary + filename: str + yaml file name + """ def write_func(prefix, config_dict, f): for k in config_dict: if type(config_dict[k]) == dict: f.write(f"{prefix}{k}:\n") - write_func(prefix+" "*2, config_dict[k], f) + write_func(prefix + " " * 2, config_dict[k], f) elif type(config_dict[k]) == list: list_prefix = " " if k != "None": @@ -179,25 +220,24 @@ def write_func(prefix, config_dict, f): list_prefix = " " * 2 for list_item in config_dict[k]: f.write(f"{prefix}{list_prefix}-\n") - write_func(prefix+list_prefix*2, list_item, f) + write_func(prefix + list_prefix * 2, list_item, f) else: f.write(f"{prefix}{k}: {config_dict[k]}\n") - + with open(filename, "w") as f: f.write("microscopes:\n") write_func(" ", config, f) - def create_config_window(self, id): """Creates the configuration window tabs.""" tab_name = "Microscope-" + str(id) microscope_tab = MicroscopeTab( - self.view.microscope_window, - name=tab_name, - index=id, - root=self.view.root, - ) + self.view.microscope_window, + name=tab_name, + index=id, + root=self.view.root, + ) setattr( self.view.microscope_window, f"microscope_tab_{id}", @@ -210,15 +250,158 @@ def create_config_window(self, id): if type(widgets) == dict: microscope_tab.create_hardware_tab(hardware_type, widgets) else: - microscope_tab.create_hardware_tab(hardware_type, hardware_widgets=widgets[1], widgets=widgets[2], top_widgets=widgets[0]) + microscope_tab.create_hardware_tab( + hardware_type, + hardware_widgets=widgets[1], + widgets=widgets[2], + top_widgets=widgets[0], + ) # Adding tabs to self notebook self.view.microscope_window.add( - getattr(self.view.microscope_window, f"microscope_tab_{id}"), + microscope_tab, text=tab_name, sticky=tk.NSEW, ) + def load_configuration(self): + """Load configuration""" + + def get_widget_value(name, value_dict): + value = value_dict + for key in name.split("/"): + if key.strip() == "": + return value + value = value.get(key, None) + if value is None: + return None + return value + + def get_widgets_value(widgets, value_dict): + temp = {} + for key in widgets: + if key == "frame_config": + continue + if widgets[key][1] in ["Button", "Label"]: + continue + value = get_widget_value(key, value_dict) + if widgets[key][1] != "Spinbox" and widgets[key][3]: + reverse_value_dict = dict( + map(lambda v: (v[1], v[0]), widgets[key][3].items()) + ) + temp[key] = reverse_value_dict[value] + else: + temp[key] = value + return temp + + def build_widgets_value(widgets, value_dict): + if widgets is None or value_dict is None: + return [None] + result = [] + ref = "" + format = "" + if "frame_config" in widgets: + ref = widgets["frame_config"].get("ref", "") + format = widgets["frame_config"].get("format", "") + if format.startswith("list"): + if ref != "" and ref.lower() != "none": + value_dict = get_widget_value(ref, value_dict) + if type(value_dict) is not list: + return [None] + for i in range(len(value_dict)): + result.append(get_widgets_value(widgets, value_dict[i])) + elif format.startswith("item"): + format_list = format.split(";") + ref_list = ref.split(";") + for i, format_item in enumerate(format_list): + k_idx = format_item[ + format_item.index("(") + 1 : format_item.index(",") + ].strip() + v_idx = format_item[ + format_item.index(",") + 1 : format_item.index(")") + ].strip() + temp_widget_values = get_widget_value(ref_list[i], value_dict) + for j, k in enumerate(temp_widget_values.keys()): + if len(result) < j + 1: + result.append({k_idx: k, v_idx: temp_widget_values[k]}) + else: + result[j][k_idx] = k + result[j][v_idx] = temp_widget_values[k] + else: + if ref != "" and ref.lower() != "none": + value_dict = get_widget_value(ref, value_dict) + result.append(get_widgets_value(widgets, value_dict)) + + return result + + file_name = filedialog.askopenfilename( + defaultextension=".yml", filetypes=[("Yaml file", "*.yml *.yaml")] + ) + if not file_name: + return + # delete microscopes + for index in range(self.view.microscope_window.index("end")): + tab_id = self.view.microscope_window.tabs()[index] + self.view.microscope_window.forget(tab_id) + self.view.microscope_window.tab_list = [] + + config_dict = load_yaml_file(file_name) + for i, microscope_name in enumerate(config_dict["microscopes"].keys()): + microscope_tab = MicroscopeTab( + self.view.microscope_window, + name=microscope_name, + index=i, + root=self.view.root, + ) + self.view.microscope_window.add( + microscope_tab, + text=microscope_name, + sticky=tk.NSEW, + ) + self.view.microscope_window.tab_list.append(microscope_name) + for hardware_type, widgets in hardwares_dict.items(): + hardware_ref_name = hardwares_config_name_dict[hardware_type] + # build dictionary values for widgets + if type(widgets) == dict: + try: + widgets_value = build_widgets_value( + widgets, + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ) + except: + widgets_value = [None] + microscope_tab.create_hardware_tab( + hardware_type, widgets, hardware_widgets_value=widgets_value + ) + else: + try: + widgets_value = [ + build_widgets_value( + widgets[1], + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ), + build_widgets_value( + widgets[2], + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ), + ] + except: + widgets_value = [[None], [None]] + microscope_tab.create_hardware_tab( + hardware_type, + hardware_widgets=widgets[1], + widgets=widgets[2], + top_widgets=widgets[0], + hardware_widgets_value=widgets_value[0], + constants_widgets_value=widgets_value[1], + ) + def device_selected(self, event): """Handle the event when a device is selected from the dropdown.""" # # Get the selected device name diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index c0c666155..f6a4bfd89 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -49,15 +49,17 @@ "Input": ttk.Entry, "Spinbox": ttk.Spinbox, "Checkbutton": ttk.Checkbutton, - "Button": ttk.Button + "Button": ttk.Button, } variable_types = { "string": tk.StringVar, "float": tk.DoubleVar, "bool": tk.BooleanVar, - "int": tk.IntVar + "int": tk.IntVar, } + + class ConfigurationAssistantWindow(ttk.Frame): def __init__(self, root, *args, **kwargs): """Initiates the main application window @@ -145,8 +147,11 @@ def __init__(self, main_frame, root, *args, **kwargs): self.add_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) self.add_button.config(width=15) + self.load_button = tk.Button(root, text="Load Configuration") + self.load_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button = tk.Button(root, text="Save") - self.save_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button.grid(row=0, column=2, sticky=tk.NE, padx=3, pady=(10, 1)) self.save_button.config(width=15) #: tk.Button: The button to cancel the application. @@ -184,7 +189,6 @@ def delete_microscope(self): self.tab_list.remove(tab_name) - class MicroscopeTab(DockableNotebook): def __init__(self, parent, name, index, root, *args, **kwargs): @@ -195,18 +199,31 @@ def __init__(self, parent, name, index, root, *args, **kwargs): tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - - def create_hardware_tab(self, name, hardware_widgets, widgets=None, top_widgets=None): - tab = HardwareTab(name, hardware_widgets, widgets=widgets, top_widgets=top_widgets) + def create_hardware_tab( + self, name, hardware_widgets, widgets=None, top_widgets=None, **kwargs + ): + tab = HardwareTab( + name, hardware_widgets, widgets=widgets, top_widgets=top_widgets, **kwargs + ) self.tab_list.append(name) self.add(tab, text=name, sticky=tk.NSEW) class HardwareTab(ttk.Frame): - def __init__(self, name, hardware_widgets, *args, widgets=None, top_widgets=None, **kwargs): + def __init__( + self, + name, + hardware_widgets, + *args, + widgets=None, + top_widgets=None, + hardware_widgets_value=[None], + constants_widgets_value=[None], + **kwargs + ): # Init Frame tk.Frame.__init__(self, *args, **kwargs) - + self.name = name # Formatting @@ -215,7 +232,7 @@ def __init__(self, name, hardware_widgets, *args, widgets=None, top_widgets=None self.top_frame = ttk.Frame(self) self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=20) - + self.hardware_frame = ttk.Frame(self) self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=20) @@ -230,14 +247,19 @@ def __init__(self, name, hardware_widgets, *args, widgets=None, top_widgets=None self.build_widgets(top_widgets, parent=self.top_frame) - self.build_widgets(hardware_widgets, parent=self.hardware_frame) + for widgets_value in hardware_widgets_value: + self.build_widgets( + hardware_widgets, + parent=self.hardware_frame, + widgets_value=widgets_value, + ) - self.build_widgets(widgets) + for widgets_value in constants_widgets_value: + self.build_widgets(widgets, widgets_value=widgets_value) - def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): """Build hardware widgets - + Parameters ---------- hardware_widgets: dict @@ -257,7 +279,7 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): label = ttk.Label(content_frame, text=v[0]) label.grid(row=i, column=0, sticky=tk.NW, padx=3) seperator = ttk.Separator(content_frame) - seperator.grid(row=i+1, columnspan=2, sticky=tk.NSEW, padx=3) + seperator.grid(row=i + 1, columnspan=2, sticky=tk.NSEW, padx=3) i += 2 continue elif v[1] != "Button": @@ -270,9 +292,13 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): label.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) i += 1 if v[1] == "Checkbutton": - widget = widget_types[v[1]](content_frame, text="", variable=self.variables[k]) + widget = widget_types[v[1]]( + content_frame, text="", variable=self.variables[k] + ) else: - widget = widget_types[v[1]](content_frame, textvariable=self.variables[k], width=30) + widget = widget_types[v[1]]( + content_frame, textvariable=self.variables[k], width=30 + ) if v[1] == "Combobox": if type(v[3]) == list: v[3] = dict([(t, t) for t in v[3]]) @@ -291,15 +317,27 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): widget.config(increment=v[3].get("step", 1)) widget.set(v[3].get("from", 0)) else: - widget = ttk.Button(content_frame, text=v[0], command=self.build_event_handler(hardware_widgets, k, frame, self.frame_row)) + widget = ttk.Button( + content_frame, + text=v[0], + command=self.build_event_handler( + hardware_widgets, k, frame, self.frame_row + ), + ) if direction == "vertical": widget.grid(row=i, column=1, sticky=tk.NSEW, padx=5, pady=3) else: widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) - i += 1 + if len(v) >= 5 and v[4]: + label = ttk.Label(content_frame, text=v[4]) + if direction == "vertical": + label.grid(row=i, column=2, sticky=tk.NW, padx=(10, 10), pady=3) + else: + label.grid(row=1, column=i, sticky=tk.NW, padx=(10, 3), pady=0) + i += 1 - def build_widgets(self, widgets, *args, parent=None, **kwargs): + def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwargs): if not widgets: return if parent is None: @@ -308,11 +346,13 @@ def build_widgets(self, widgets, *args, parent=None, **kwargs): title = "Hardware" format = None temp_ref = None + direction = "vertical" if "frame_config" in widgets: collapsible = widgets["frame_config"].get("collapsible", False) title = widgets["frame_config"].get("title", "Hardware") format = widgets["frame_config"].get("format", None) temp_ref = widgets["frame_config"].get("ref", None) + direction = widgets["frame_config"].get("direction", "vertical") if collapsible: self.foldAllFrames() frame = CollapsibleFrame(parent=parent, title=title) @@ -322,9 +362,8 @@ def build_widgets(self, widgets, *args, parent=None, **kwargs): frame = ttk.Frame(parent) frame.grid(row=self.frame_row, column=0, sticky=tk.NSEW, padx=20) self.frame_row += 1 - + ref = None - direction = "vertical" if kwargs: ref = kwargs.get("ref", None) direction = kwargs.get("direction", "vertical") @@ -334,6 +373,13 @@ def build_widgets(self, widgets, *args, parent=None, **kwargs): self.variables_list.append((self.variables, self.values_dict, ref, format)) self.build_hardware_widgets(widgets, frame=frame, direction=direction) + if widgets_value: + for k, v in widgets_value.items(): + try: + self.variables[k].set(v) + except (TypeError, ValueError): + pass + def foldAllFrames(self, except_frame=None): for child in self.hardware_frame.winfo_children(): if isinstance(child, CollapsibleFrame) and child is not except_frame: @@ -343,28 +389,37 @@ def foldAllFrames(self, except_frame=None): child.fold() def create_toggle_function(self, frame): - def func(event): self.foldAllFrames(frame) frame.toggle_visibility() - + return func def build_event_handler(self, hardware_widgets, key, frame, frame_id): - def func(*args, **kwargs): v = hardware_widgets[key] if "widgets" in v[2]: if "parent" in v[2]: - parent = self.hardware_frame if v[2]["parent"].startswith("hardware") else None + parent = ( + self.hardware_frame + if v[2]["parent"].startswith("hardware") + else None + ) else: parent_id = frame.winfo_parent() parent = self.nametowidget(parent_id) - widgets = hardware_widgets if v[2]["widgets"] == "self" else v[2]["widgets"] - self.build_widgets(widgets, parent=parent, ref=v[2].get("ref", None), direction=v[2].get("direction", "vertical")) + widgets = ( + hardware_widgets if v[2]["widgets"] == "self" else v[2]["widgets"] + ) + self.build_widgets( + widgets, + parent=parent, + ref=v[2].get("ref", None), + direction=v[2].get("direction", "vertical"), + ) # collaps other frame elif v[2].get("delete", False): frame.grid_remove() - self.variables_list[frame_id-self.row_offset] = None + self.variables_list[frame_id - self.row_offset] = None return func From b4e72575b8fc9d2215169c038add7fc8b886beec Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 10 May 2024 14:36:15 -0700 Subject: [PATCH 17/17] Add scrollbar --- src/navigate/config/config.py | 5 +- src/navigate/controller/configurator.py | 64 ++++-- .../view/configurator_application_window.py | 195 +++++++++++++++--- 3 files changed, 220 insertions(+), 44 deletions(-) diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index 1b538c1fc..fdbe70047 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -955,7 +955,10 @@ def verify_configuration(manager, configuration): ref_list["camera"].append(camera_idx) hardware_dict["camera"].append(camera_config["hardware"]) - channel_count = max(channel_count, camera_config.get("count", 5)) + try: + channel_count = max(channel_count, camera_config.get("count", 5)) + except TypeError: + channel_count = 5 # zoom (one zoom) if "zoom" not in hardware_dict: diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 65e704a85..73ce28b7b 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -32,7 +32,7 @@ # Standard Library Imports import tkinter as tk from time import sleep -from tkinter import filedialog +from tkinter import filedialog, messagebox # Third Party Imports @@ -80,6 +80,7 @@ def __init__(self, root, splash_screen): ) self.view.top_window.add_button.config(command=self.add_microscope) + self.view.top_window.new_button.config(command=self.new_configuration) self.view.top_window.load_button.config(command=self.load_configuration) self.view.top_window.save_button.config(command=self.save) self.view.top_window.cancel_button.config(command=self.on_cancel) @@ -97,10 +98,24 @@ def on_cancel(self): exit() def add_microscope(self): - """Evaluate the number of configurations and create the configuration window""" + """Add a new microscope tab""" self.create_config_window(self.microscope_id) self.microscope_id += 1 + def delete_microscopes(self): + """Delete all microscopes""" + # delete microscopes + for index in range(self.view.microscope_window.index("end")): + tab_id = self.view.microscope_window.tabs()[index] + self.view.microscope_window.forget(tab_id) + self.view.microscope_window.tab_list = [] + self.microscope_id = 0 + + def new_configuration(self): + """Create new configurations""" + self.delete_microscopes() + self.create_config_window(self.microscope_id) + def save(self): """Save configuration file""" @@ -128,6 +143,8 @@ def set_value(temp_dict, key_list, value): ) if not filename: return + # warning_info + warning_info = {} config_dict = {} for tab_index in self.view.microscope_window.tabs(): microscope_name = self.view.microscope_window.tab(tab_index, "text") @@ -168,8 +185,9 @@ def set_value(temp_dict, key_list, value): ].strip() k = variables[k_idx].get() if k.strip() == "": + warning_info[hardware_name] = True print( - f"Notice: {hardware_name} has an empty value! Please double check!" + f"Notice: {hardware_name} has an empty value {ref}! Please double check if it's okay!" ) if k_idx in value_dict: @@ -191,11 +209,18 @@ def set_value(temp_dict, key_list, value): except tk._tkinter.TclError: v = "" print( - f"Notice: {hardware_name} has an empty value! Please double check!" + f"Notice: {hardware_name} has an empty value {k}! Please double check!" ) + warning_info[hardware_name] = True set_value(temp_dict, k.split("/"), v) self.write_to_yaml(config_dict, filename) + # display warning + if warning_info: + messagebox.showwarning( + title="Configuration", + message=f"There are empty value(s) with {', '.join(warning_info.keys())}. Please double check!" + ) def write_to_yaml(self, config, filename): """write yaml file @@ -234,15 +259,8 @@ def create_config_window(self, id): tab_name = "Microscope-" + str(id) microscope_tab = MicroscopeTab( self.view.microscope_window, - name=tab_name, - index=id, root=self.view.root, ) - setattr( - self.view.microscope_window, - f"microscope_tab_{id}", - microscope_tab, - ) self.view.microscope_window.tab_list.append(tab_name) for hardware_type, widgets in hardwares_dict.items(): if not widgets: @@ -268,6 +286,7 @@ def load_configuration(self): """Load configuration""" def get_widget_value(name, value_dict): + """Get the value from a dict""" value = value_dict for key in name.split("/"): if key.strip() == "": @@ -278,6 +297,7 @@ def get_widget_value(name, value_dict): return value def get_widgets_value(widgets, value_dict): + """Get all key-value from valude_dict, keys are from widgets""" temp = {} for key in widgets: if key == "frame_config": @@ -285,6 +305,7 @@ def get_widgets_value(widgets, value_dict): if widgets[key][1] in ["Button", "Label"]: continue value = get_widget_value(key, value_dict) + # widgets[key][3] is the value mapping dict if widgets[key][1] != "Spinbox" and widgets[key][3]: reverse_value_dict = dict( map(lambda v: (v[1], v[0]), widgets[key][3].items()) @@ -295,6 +316,7 @@ def get_widgets_value(widgets, value_dict): return temp def build_widgets_value(widgets, value_dict): + """According to valude_dict build values for widgets""" if widgets is None or value_dict is None: return [None] result = [] @@ -333,24 +355,27 @@ def build_widgets_value(widgets, value_dict): result.append(get_widgets_value(widgets, value_dict)) return result - + # ask file name file_name = filedialog.askopenfilename( defaultextension=".yml", filetypes=[("Yaml file", "*.yml *.yaml")] ) if not file_name: return - # delete microscopes - for index in range(self.view.microscope_window.index("end")): - tab_id = self.view.microscope_window.tabs()[index] - self.view.microscope_window.forget(tab_id) - self.view.microscope_window.tab_list = [] + # read configuration.yaml config_dict = load_yaml_file(file_name) + if config_dict is None or "microscopes" not in config_dict: + messagebox.showerror( + title="Configuration", + message="It's not a valid configuration.yaml file!" + ) + return + + self.delete_microscopes() + for i, microscope_name in enumerate(config_dict["microscopes"].keys()): microscope_tab = MicroscopeTab( self.view.microscope_window, - name=microscope_name, - index=i, root=self.view.root, ) self.view.microscope_window.add( @@ -359,6 +384,7 @@ def build_widgets_value(widgets, value_dict): sticky=tk.NSEW, ) self.view.microscope_window.tab_list.append(microscope_name) + for hardware_type, widgets in hardwares_dict.items(): hardware_ref_name = hardwares_config_name_dict[hardware_type] # build dictionary values for widgets diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index f6a4bfd89..884cc462e 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -89,7 +89,7 @@ def __init__(self, root, *args, **kwargs): except tk.TclError: pass - self.root.resizable(True, True) + self.root.resizable(False, False) self.root.geometry("") self.root.rowconfigure(0, weight=1) self.root.columnconfigure(0, weight=1) @@ -103,7 +103,7 @@ def __init__(self, root, *args, **kwargs): self.grid(column=0, row=0, sticky=tk.NSEW) self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=3, pady=3) self.microscope_frame.grid( - row=1, column=0, columnspan=4, sticky=tk.NSEW, padx=3, pady=3 + row=1, column=0, columnspan=5, sticky=tk.NSEW, padx=3, pady=3 ) #: ttk.Frame: The top frame of the application @@ -121,7 +121,7 @@ class TopWindow(ttk.Frame): """ def __init__(self, main_frame, root, *args, **kwargs): - """Initialize Acquire Bar. + """Initialize Top Frame. Parameters ---------- @@ -143,25 +143,44 @@ def __init__(self, main_frame, root, *args, **kwargs): tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) - self.add_button = tk.Button(root, text="Add A Microscope") - self.add_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) - self.add_button.config(width=15) + self.new_button = tk.Button(root, text="New Configuration") + self.new_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) + self.new_button.config(width=15) self.load_button = tk.Button(root, text="Load Configuration") self.load_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.load_button.config(width=15) + + self.add_button = tk.Button(root, text="Add A Microscope") + self.add_button.grid(row=0, column=2, sticky=tk.NE, padx=3, pady=(10, 1)) + self.add_button.config(width=15) self.save_button = tk.Button(root, text="Save") - self.save_button.grid(row=0, column=2, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button.grid(row=0, column=3, sticky=tk.NE, padx=3, pady=(10, 1)) self.save_button.config(width=15) #: tk.Button: The button to cancel the application. self.cancel_button = tk.Button(root, text="Cancel") - self.cancel_button.grid(row=0, column=3, sticky=tk.NE, padx=3, pady=(10, 1)) + self.cancel_button.grid(row=0, column=4, sticky=tk.NE, padx=3, pady=(10, 1)) self.cancel_button.config(width=15) class MicroscopeWindow(DockableNotebook): def __init__(self, frame, root, *args, **kwargs): + """Initialize Microscope Frame. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + DockableNotebook.__init__(self, frame, root, *args, **kwargs) self.grid(row=0, column=0, sticky=tk.NSEW) @@ -181,6 +200,7 @@ def rename_microscope(self): self.tab_list.append(result) def delete_microscope(self): + """Delete selected microscope""" tab = self.select() tab_name = self.tab(tab)["text"] current_tab_index = self.index("current") @@ -190,7 +210,20 @@ def delete_microscope(self): class MicroscopeTab(DockableNotebook): - def __init__(self, parent, name, index, root, *args, **kwargs): + def __init__(self, parent, root, *args, **kwargs): + """Initialize Microscope Tab. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ # Init Frame DockableNotebook.__init__(self, parent, root, *args, **kwargs) @@ -202,6 +235,23 @@ def __init__(self, parent, name, index, root, *args, **kwargs): def create_hardware_tab( self, name, hardware_widgets, widgets=None, top_widgets=None, **kwargs ): + """Create hardware tab + + Parameters + ---------- + name : str + tab name/hardware name + hardware_widgets : dict + hardware widgets dict + widgets : dict + constants widgets dict + top_widgets : dict + button widgets dict + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments + """ tab = HardwareTab( name, hardware_widgets, widgets=widgets, top_widgets=top_widgets, **kwargs ) @@ -221,6 +271,27 @@ def __init__( constants_widgets_value=[None], **kwargs ): + """Initialize Microscope Tab. + + Parameters + ---------- + name : str + tab name/hardware name + hardware_widgets : dict + hardware widgets dict + widgets : dict + constants widgets dict + top_widgets : dict + button widgets dict + hardware_widgets_value : list[dict] + list of values for hardware widgets + constants_widgets_value : list[dict] + list of values for constants widgets + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments + """ # Init Frame tk.Frame.__init__(self, *args, **kwargs) @@ -229,15 +300,32 @@ def __init__( # Formatting tk.Grid.columnconfigure(self, "all", weight=1) tk.Grid.rowconfigure(self, "all", weight=1) + scroll_frame = ttk.Frame(self) + scroll_frame.grid(row=3, column=0, sticky=tk.NSEW) + canvas = tk.Canvas(scroll_frame, width=1000, height=500) + scrollbar = ttk.Scrollbar(scroll_frame, orient="vertical", command=canvas.yview) + content_frame = ttk.Frame(canvas) + + content_frame.bind( + "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=content_frame, anchor="nw") + + canvas.configure(yscrollcommand=scrollbar.set) - self.top_frame = ttk.Frame(self) - self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=20) + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") - self.hardware_frame = ttk.Frame(self) - self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=20) + self.top_frame = ttk.Frame(content_frame) - self.bottom_frame = ttk.Frame(self) - self.bottom_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=20) + self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=10) + + self.hardware_frame = ttk.Frame(content_frame) + self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10) + + self.bottom_frame = ttk.Frame(content_frame) + self.bottom_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10) self.frame_row = 0 self.row_offset = self.frame_row + 1 @@ -254,16 +342,28 @@ def __init__( widgets_value=widgets_value, ) + count = 0 for widgets_value in constants_widgets_value: self.build_widgets(widgets, widgets_value=widgets_value) + # if self.name in ["Filter Wheel"]: + # count += 1 + # print("building widgets value:", self.name, widgets_value) + # if count > 4: + + # if count >= 10: + # break - def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): - """Build hardware widgets + def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): + """create widgets Parameters ---------- - hardware_widgets: dict - name: (display_name, widget_type, value_type, values, condition) + hardware_widgets : dict + name: (display_name, widget_type, value_type, values, info) + frame : tk.Frame + the parent frame for widgets + direction : str + direction of the widget layouts """ if hardware_widgets is None: return @@ -289,7 +389,7 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): if direction == "vertical": label.grid(row=i, column=0, sticky=tk.NW, padx=(3, 10), pady=3) else: - label.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) + label.grid(row=0, column=i, sticky=tk.NW, padx=(5, 3), pady=3) i += 1 if v[1] == "Checkbutton": widget = widget_types[v[1]]( @@ -327,7 +427,7 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): if direction == "vertical": widget.grid(row=i, column=1, sticky=tk.NSEW, padx=5, pady=3) else: - widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=3) + widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=(3, 0)) if len(v) >= 5 and v[4]: label = ttk.Label(content_frame, text=v[4]) @@ -338,6 +438,21 @@ def build_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): i += 1 def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwargs): + """Build widgets + + Parameters + ---------- + widgets : dict + widget dict + parent : frame + parent frame to put widgets + widgets_value : dict + valude dict of widgets + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments, ref="reference name", direction="vertical" + """ if not widgets: return if parent is None: @@ -371,16 +486,28 @@ def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwarg self.variables = {} self.values_dict = {} self.variables_list.append((self.variables, self.values_dict, ref, format)) - self.build_hardware_widgets(widgets, frame=frame, direction=direction) - + self.create_hardware_widgets(widgets, frame=frame, direction=direction) + if widgets_value: for k, v in widgets_value.items(): try: - self.variables[k].set(v) + if k == "axes": + print("*** type", type(v), v, str(v)) + self.variables[k].set(str(v)) except (TypeError, ValueError): pass + except tk._tkinter.TclError: + pass + def foldAllFrames(self, except_frame=None): + """Fold all collapsible frames except one frame + + Parameters + ---------- + except_frame : tk.Frame + the unfold frame + """ for child in self.hardware_frame.winfo_children(): if isinstance(child, CollapsibleFrame) and child is not except_frame: child.fold() @@ -389,6 +516,13 @@ def foldAllFrames(self, except_frame=None): child.fold() def create_toggle_function(self, frame): + """Toggle collapsible frame + + Parameters + ---------- + frame : tk.Frame + the frame to toggle + """ def func(event): self.foldAllFrames(frame) frame.toggle_visibility() @@ -396,6 +530,19 @@ def func(event): return func def build_event_handler(self, hardware_widgets, key, frame, frame_id): + """Build button event handler + + Parameters + ---------- + hardware_widgets : dict + widget dict containing the button + key : str + reference of the button + frame : tk.Frame + the frame to put/delete widgets + frame_id : int + index of the frame + """ def func(*args, **kwargs): v = hardware_widgets[key] if "widgets" in v[2]: