Switch to side-by-side view

--- 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()