--- a +++ b/dosma/gui/preferences_viewer.py @@ -0,0 +1,290 @@ +import tkinter as tk +from tkinter import ttk +from typing import Dict + +import Pmw + +from dosma.cli import GPU_KEY +from dosma.core.io.format_io import ImageDataFormat +from dosma.defaults import preferences +from dosma.utils import env + +if env.package_available("tensorflow"): + from tensorflow.python.client import device_lib +else: + device_lib = None + + +CUDA_DEVICES_STR = "CUDA_VISIBLE_DEVICES" +SUPPORTED_IMAGE_DATA_FORMATS = list(ImageDataFormat) +LARGE_FONT = ("Verdana", 18) + + +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +TYPE_CAST = {bool: tk.BooleanVar, str: tk.StringVar, int: tk.IntVar, float: tk.DoubleVar} + + +class CommandLineFlagGUI: + def __init__(self, cmd_line_metadata: dict, **kwargs): + self._cmd_line_metadata = cmd_line_metadata + self._name = cmd_line_metadata["name"] + self._default = cmd_line_metadata["default"] + self._help = cmd_line_metadata["help"] + self._type = cmd_line_metadata["type"] + self._choices = ( + cmd_line_metadata["choices"] if "choices" in cmd_line_metadata.keys() else None + ) + + # Tkinter Variable used for binding with GUI. + self._tk_var = TYPE_CAST[self._type]() + self._tk_var.set(self._default) + + # GUI formatting. + self._label_format = kwargs.get("label_format") if "label_format" in kwargs else "%s: " + + # Memory + self._widget = None + + def draw(self, frame: tk.Frame, **kwargs): + """Draw element best suited for command line metadata. + :param frame: The frame where to add GUI element + :type frame: A Tkinter frame + """ + draw_cmd = { + str: lambda root: self._draw_str_gui(root), + bool: lambda root: self._draw_bool_gui(root), + int: lambda root: self._draw_number_gui(root, "float"), + float: lambda root: self._draw_number_gui(root, "float"), + } + + padx = kwargs.get("padx") if "padx" in kwargs else 5 + balloon = kwargs.get("balloon") if "balloon" in kwargs else None + + hbox = tk.Frame(frame) + hbox.pack(side="top", anchor="nw") + + label = tk.Label(hbox, text=self._label_format % self._name) + label.pack(side="left", anchor="nw", padx=padx) + + if self._choices: + t = self._draw_list_gui(hbox) + else: + t = draw_cmd[self._type](hbox) + + t.pack(side="left", anchor="nw", padx=padx) + self._widget = t + + if balloon: + balloon.bind(label, self._help) + + return hbox + + def _draw_str_gui(self, root): + return tk.Entry(root, textvariable=self.tk_var) + + def _draw_bool_gui(self, root): + return tk.Checkbutton(root, variable=self.tk_var) + + def _draw_number_gui(self, root, dtype="float"): + vcmd = ( + root.register(self._validate_number), + dtype, + "%d", + "%i", + "%P", + "%s", + "%S", + "%v", + "%V", + "%W", + ) + return tk.Entry(root, textvariable=self.tk_var, validate="all", validatecommand=vcmd) + + def _draw_int_gui(self, root): + vcmd = ( + root.register(self._validate_number), + "float", + "%d", + "%i", + "%P", + "%s", + "%S", + "%v", + "%V", + "%W", + ) + return tk.Entry(root, textvariable=self.tk_var, validate="all", validatecommand=vcmd) + + def _validate_number( + self, + dtype, + action, + index, + value_if_allowed, + prior_value, + text, + validation_type, + trigger_type, + widget_name, + ): + if trigger_type not in ["key", "focusout"]: + return True + + if not value_if_allowed: + if trigger_type == "key": + return True + else: + self.tk_var.set(self._default) + self._widget["validate"] = validation_type + return False + + try: + eval(dtype)(value_if_allowed) + return True + except ValueError: + return False + + def _draw_list_gui(self, root): + options = self._choices + return tk.OptionMenu(root, self.tk_var, *options) + + @property + def tk_var(self): + return self._tk_var + + +class PreferencesManager(metaclass=Singleton): + def __init__(self): + self.frame = None + self.balloon = None + + self.gui_manager: Dict = {} + self.gui_elements: Dict = {} + + self._init_gpu_preferences() + + # Init preferences + self._preference_elements = {} + preferences_metadata = preferences.cmd_line_flags() + for preference in preferences_metadata.keys(): + self._preference_elements[preference] = CommandLineFlagGUI( + preferences_metadata[preference], label_format="%s:\t" + ) + + def _init_gpu_preferences(self): + gpu_vars = [] + if device_lib is not None: + local_device_protos = device_lib.list_local_devices() + for x in local_device_protos: + if x.device_type == "GPU": + bool_var = tk.BooleanVar() + x_id = x.name.split(":")[-1] + gpu_vars.append((x_id, bool_var)) + + self.gui_manager["gpu"] = gpu_vars + + @property + def gpus(self) -> str: + if device_lib is None: + return None + + gpu_ids = [] + local_device_protos = device_lib.list_local_devices() + for x in local_device_protos: + if x.device_type == "GPU": + x_id = x.name.split(":")[-1] + gpu_ids.append(x_id) + + if len(gpu_ids) == 0: + return None + + gpu_str = "" + for ind, var in enumerate(self.gui_manager["gpu"]): + if not var.get(): + continue + gpu_str += "%s," % gpu_ids[ind] + + if len(gpu_str) == 0: + return None + + gpu_str = gpu_str[:-1] # remove last comma + return gpu_str + + def _restore_preference_default(self): + for preference in self._preference_elements.keys(): + self._preference_elements[preference].tk_var.set(preferences.get(preference)) + + def __display_gui(self): + self._restore_preference_default() + self.balloon = Pmw.Balloon() + + _label = tk.Label(self.frame, text="Preferences", font=LARGE_FONT) + _label.pack() + + hboxes = [] + # show gpu options + if self.gui_manager["gpu"]: + f = tk.Frame(self.frame) + gpu_checkboxes = [] + gpu_label = tk.Label(f, text="GPU:") + self.balloon.bind(gpu_label, "Select gpus to use for analysis") + gpu_label.pack() + for x_id, bool_var in self.gui_manager["gpu"]: + c = tk.Checkbutton(f, text=x_id, variable=bool_var) + c.pack() + gpu_checkboxes.append(c) + hboxes.append(f) + + # Add command line governed preferences + for preference in self._preference_elements.keys(): + hbox = self._preference_elements[preference].draw(self.frame, balloon=self.balloon) + hboxes.append(hbox) + + for f in hboxes: + f.pack(side="top", anchor="w", pady=10) + + # Add apply settings and save preferences buttons + # apply settings: save preferences for this session only + # save settings: changes preferences in file + hbox = tk.Frame(self.frame) + apply_settings_button = ttk.Button( + hbox, text="Apply Settings", command=lambda: self._apply_settings() + ) + self.balloon.bind(apply_settings_button, "Apply settings for this session") + save_settings_button = ttk.Button( + hbox, text="Save Settings", command=lambda: self._save_settings() + ) + self.balloon.bind(save_settings_button, "Save settings for all future sessions") + apply_settings_button.pack(side="left", anchor="nw", padx=5) + save_settings_button.pack(side="left", anchor="nw", padx=5) + hbox.pack(side="bottom", anchor="se") + + def _apply_settings(self): + for preference in self._preference_elements.keys(): + new_val = self._preference_elements[preference].tk_var.get() + preferences.set(preference, new_val) + + def _save_settings(self): + self._apply_settings() + preferences.save() + + def show_window(self, parent): + window = tk.Toplevel(parent) + self.frame = window + self.__display_gui() + + def get_cmd_line_str(self): + gpus = self.gpus + cmd_line_str = "" + if gpus: + cmd_line_str += "--%s %s " % (GPU_KEY, gpus) + + return cmd_line_str.strip()