text: #!/usr/bin/env python
  # -*- coding: utf-8 -*-
  
  # dialogs.dvha_app.py
  """
  Dialogs used in the main view of DVHA (e.g., query design, importing, etc.)
  """
  # Copyright (c) 2016-2019 Dan Cutright
  # This file is part of DVH Analytics, released under a BSD license.
  #    See the file LICENSE included with this distribution, also
  #    available at https://github.com/cutright/DVH-Analytics
  
  import wx
  import wx.adv
  from dateutil.parser import parse as parse_date
  import matplotlib.colors as plot_colors
  import numpy as np
  import time
  from os.path import isdir
  from dvha.tools.utilities import get_selected_listctrl_items, MessageDialog, get_window_size,\
      get_installed_python_libraries, set_msw_background_color, set_frame_icon, backup_sqlite_db
  from dvha.db import sql_columns
  from dvha.db.sql_connector import DVH_SQL
  from dvha.models.data_table import DataTable
  from dvha.paths import LICENSE_PATH
  from dvha.options import DefaultOptions
  from dvha.tools.errors import ErrorDialog
  
  
  class DatePicker(wx.Dialog):
      """
      Pop-up window to select a date to ensure proper formatting (over typing in a date into a text_ctrl directly)
      """
      def __init__(self, title='', initial_date=None, action=None, sql_date_format=False):
          """
          :param title: optional title for the wx.Dialog
          :type title: str
          :param initial_date: optional initial date
          :type initial_date: str
          :param action: pointer to function to be executed on wx.ID_OK or
          :param sql_date_format: Set to True for SQL compatible format
          :type sql_date_format: bool
          """
          wx.Dialog.__init__(self, None, title=title)
  
          self.sql_date_format = sql_date_format
  
          self.calendar_ctrl = wx.adv.CalendarCtrl(self, wx.ID_ANY,
                                                   style=wx.adv.CAL_SHOW_HOLIDAYS | wx.adv.CAL_SHOW_SURROUNDING_WEEKS)
          if initial_date and initial_date.lower() != 'none':
              self.calendar_ctrl.SetDate(parse_date(initial_date))
  
          self.button = {'ok': wx.Button(self, wx.ID_OK, "OK"),
                         'delete': wx.Button(self, wx.ID_ANY, "Delete"),
                         'cancel': wx.Button(self, wx.ID_CANCEL, "Cancel")}
  
          self.button['delete'].Enable(not sql_date_format)
  
          self.none = False  # If True after close of this dialog, user deleted the value
  
          self.action = action
  
          self.__do_layout()
          self.__do_bind()
  
          self.run()
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_main = wx.BoxSizer(wx.VERTICAL)
          sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
          sizer_main.Add(self.calendar_ctrl, 0, wx.ALIGN_CENTER | wx.ALL, 5)
          for button in self.button.values():
              sizer_buttons.Add(button, 0, wx.ALL, 5)
          sizer_main.Add(sizer_buttons, 1, wx.ALIGN_CENTER | wx.BOTTOM | wx.TOP, 10)
          sizer_wrapper.Add(sizer_main, 1, wx.ALL | wx.EXPAND, 10)
          self.SetSizer(sizer_wrapper)
          sizer_wrapper.Fit(self)
          self.Layout()
          self.Center()
  
      def __do_bind(self):
          self.Bind(wx.EVT_BUTTON, self.on_delete, id=self.button['delete'].GetId())
  
      @property
      def date(self):
          if self.none:
              return ''
          date = self.calendar_ctrl.GetDate()
          date = "%s/%s/%s" % (date.month+1, date.day, date.year)
          if self.sql_date_format:
              return str(parse_date(date).date())
          return date
  
      def on_delete(self, evt):
          self.none = True
          self.Close()  # resolve dialog with wx.ID_CANCEL
  
      def run(self):
          res = self.ShowModal()
          if res == wx.ID_OK or (res == wx.ID_CANCEL and self.none):
              if self.action is not None:
                  self.action(self.date)
          self.Destroy()
  
  
  class AddEndpointDialog(wx.Dialog):
      """
      Add a new column to the endpoint table of the Endpoint tab
      """
      def __init__(self):
          wx.Dialog.__init__(self, None, title='Add Endpoint')
  
          self.combo_box_output = wx.ComboBox(self, wx.ID_ANY,
                                              choices=["Dose (Gy)", "Dose(%)", "Volume (cc)", "Volume (%)"],
                                              style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.text_input = wx.TextCtrl(self, wx.ID_ANY, "")
          self.radio_box_units = wx.RadioBox(self, wx.ID_ANY, "", choices=["cc ", "% "], majorDimension=1,
                                             style=wx.RA_SPECIFY_ROWS)
          self.button_ok = wx.Button(self, wx.ID_OK, "OK")
          self.button_ok.Disable()
          self.button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
  
          self.__do_bind()
          self.__set_properties()
          self.__do_layout()
  
      def __set_properties(self):
          self.radio_box_units.SetSelection(0)
          self.combo_box_output.SetValue('Dose (Gy)')
  
      def __do_bind(self):
          self.Bind(wx.EVT_COMBOBOX, self.combo_box_ticker, id=self.combo_box_output.GetId())
          self.Bind(wx.EVT_TEXT, self.text_input_ticker, id=self.text_input.GetId())
          self.Bind(wx.EVT_RADIOBOX, self.radio_box_ticker, id=self.radio_box_units.GetId())
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_buttons_wrapper = wx.BoxSizer(wx.HORIZONTAL)
          sizer_buttons = wx.BoxSizer(wx.HORIZONTAL)
          sizer_input = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, ""), wx.HORIZONTAL)
          sizer_input_units = wx.BoxSizer(wx.VERTICAL)
          sizer_input_value = wx.BoxSizer(wx.VERTICAL)
          sizer_output = wx.BoxSizer(wx.VERTICAL)
  
          label_ouput = wx.StaticText(self, wx.ID_ANY, "Output:")
          sizer_output.Add(label_ouput, 0, wx.BOTTOM | wx.EXPAND, 8)
          sizer_output.Add(self.combo_box_output, 0, wx.EXPAND, 0)
          sizer_input.Add(sizer_output, 1, wx.ALL | wx.EXPAND, 5)
  
          self.label_input_value = wx.StaticText(self, wx.ID_ANY, "Input Volume (cc):")
          sizer_input_value.Add(self.label_input_value, 0, wx.BOTTOM | wx.EXPAND, 8)
          sizer_input_value.Add(self.text_input, 0, wx.EXPAND | wx.LEFT, 5)
          sizer_input.Add(sizer_input_value, 1, wx.ALL | wx.EXPAND, 5)
  
          label_input_units = wx.StaticText(self, wx.ID_ANY, "Input Units:")
          sizer_input_units.Add(label_input_units, 0, wx.BOTTOM | wx.EXPAND, 3)
          sizer_input_units.Add(self.radio_box_units, 0, wx.EXPAND, 0)
          sizer_input.Add(sizer_input_units, 1, wx.ALL | wx.EXPAND, 5)
          sizer_wrapper.Add(sizer_input, 0, wx.ALL | wx.EXPAND, 10)
  
          self.text_short_hand = wx.StaticText(self, wx.ID_ANY, "Short-hand: ")
          sizer_wrapper.Add(self.text_short_hand, 0, wx.ALL, 5)
          sizer_buttons.Add(self.button_ok, 0, wx.ALL, 5)
          sizer_buttons.Add(self.button_cancel, 0, wx.ALL | wx.EXPAND, 5)
          sizer_buttons_wrapper.Add(sizer_buttons, 0, wx.ALL | wx.EXPAND, 5)
          sizer_wrapper.Add(sizer_buttons_wrapper, 0, wx.ALIGN_CENTER | wx.ALL, 5)
  
          self.SetSizer(sizer_wrapper)
          sizer_wrapper.Fit(self)
          self.Layout()
          self.Center()
  
      def combo_box_ticker(self, evt):
          self.update_radio_box_choices()
          self.update_label_input()
          self.update_short_hand()
  
      def text_input_ticker(self, evt):
          self.update_short_hand()
  
      def radio_box_ticker(self, evt):
          self.update_label_input()
          self.update_short_hand()
  
      def update_label_input(self):
          new_label = "%s (%s):" % (['Input Dose', 'Input Volume']['Dose' in self.combo_box_output.GetValue()],
                                    self.radio_box_units.GetItemLabel(self.radio_box_units.GetSelection()).strip())
          self.label_input_value.SetLabelText(new_label)
  
      def update_radio_box_choices(self):
          choice_1 = ['Gy', 'cc']['Dose' in self.combo_box_output.GetValue()]
          self.radio_box_units.SetItemLabel(0, choice_1)
  
      def update_short_hand(self):
          short_hand = 'Short-hand: '
          value = self.text_input.GetValue()
          if value:
              prepend = ['V_', 'D_']['Dose' in self.combo_box_output.GetValue()]
              units = self.radio_box_units.GetItemLabel(self.radio_box_units.GetSelection()).strip()
              short_hand = short_hand + prepend + value + units
          self.text_short_hand.SetLabelText(short_hand)
          self.set_button_ok_enable(value)
  
      def set_button_ok_enable(self, value):
          try:
              if value.isdigit():
                  value = int(value)
              else:
                  value = float(value)
          except ValueError:
              self.text_short_hand.SetLabelText('Short-hand: ')
          self.button_ok.Enable(type(value) in [int, float])
  
      @property
      def is_endpoint_valid(self):
          return bool(len(self.short_hand_label))
  
      @property
      def short_hand_label(self):
          return self.text_short_hand.GetLabel().replace('Short-hand: ', '').strip()
  
      @property
      def output_type(self):
          return ['absolute', 'relative']['%' in self.combo_box_output.GetValue()]
  
      @property
      def input_type(self):
          return ['absolute', 'relative'][self.radio_box_units.GetSelection()]
  
      @property
      def units_in(self):
          return self.radio_box_units.GetItemLabel(self.radio_box_units.GetSelection()).replace('%', '').strip()
  
      @property
      def units_out(self):
          return self.combo_box_output.GetValue().split('(')[1][:-1].replace('%', '').strip()
  
      @property
      def input_value(self):
          try:
              return float(self.text_input.GetValue())
          except ValueError:
              return 0.
  
      @property
      def endpoint_row(self):
          return [self.short_hand_label,
                  self.output_type,
                  self.input_type,
                  self.input_value,
                  self.units_in,
                  self.units_out]
  
  
  class SelectFromListDialog(wx.Dialog):
      """
      Select an item in a list
      """
      def __init__(self, title, column, choices, exclude=None, size=None, column_width=200, selections=None):
          wx.Dialog.__init__(self, None, title=title)
  
          self.column = column
          self.choices = choices
          self.exclude = [exclude, []][exclude is None]
          self.size = size
          self.column_width = column_width
  
          self.list_ctrl = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_REPORT)
          self.button_select_all = wx.Button(self, wx.ID_ANY, "Select All")
          self.button_deselect_all = wx.Button(self, wx.ID_ANY, "Deselect All")
          self.button_ok = wx.Button(self, wx.ID_OK, "OK")
          self.button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
  
          self.Bind(wx.EVT_BUTTON, self.select_all, id=self.button_select_all.GetId())
          self.Bind(wx.EVT_BUTTON, self.deselect_all, id=self.button_deselect_all.GetId())
  
          self.__set_properties()
          self.__do_layout()
  
          if selections:
              self.set_selection(selections)
  
      def __set_properties(self):
          self.list_ctrl.AppendColumn(self.column, format=wx.LIST_FORMAT_LEFT, width=self.column_width)
  
          for choice in self.choices:
              if choice not in self.exclude:
                  self.list_ctrl.InsertItem(50000, choice)
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_ok_cancel = wx.BoxSizer(wx.HORIZONTAL)
          sizer_select = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, ""), wx.VERTICAL)
          sizer_select_buttons = wx.BoxSizer(wx.HORIZONTAL)
  
          sizer_select.Add(self.list_ctrl, 1, wx.ALL | wx.EXPAND, 5)
          sizer_select_buttons.Add(self.button_select_all, 0, wx.ALL, 5)
          sizer_select_buttons.Add(self.button_deselect_all, 0, wx.ALL, 5)
          sizer_select.Add(sizer_select_buttons, 0, wx.ALIGN_CENTER | wx.ALL, 0)
          sizer_wrapper.Add(sizer_select, 1, wx.ALL | wx.EXPAND, 5)
  
          sizer_ok_cancel.Add(self.button_ok, 0, wx.ALL, 5)
          sizer_ok_cancel.Add(self.button_cancel, 0, wx.ALL, 5)
          sizer_wrapper.Add(sizer_ok_cancel, 0, wx.ALIGN_CENTER | wx.ALL, 5)
  
          self.SetSizer(sizer_wrapper)
          if self.size:
              self.SetSize(self.size)
          else:
              sizer_wrapper.Fit(self)
          self.Layout()
          self.Center()
  
      @property
      def selected_indices(self):
          return get_selected_listctrl_items(self.list_ctrl)
  
      @property
      def selected_values(self):
          return [self.list_ctrl.GetItem(i, 0).GetText() for i in self.selected_indices]
  
      @property
      def item_count(self):
          return len(self.choices) - len(self.exclude)
  
      def select_all(self, evt):
          self.apply_global_selection()
  
      def deselect_all(self, evt):
          self.apply_global_selection(on=0)
  
      def apply_global_selection(self, on=1):
          for i in range(self.item_count):
              self.list_ctrl.Select(i, on=on)
  
      def set_selection(self, selections):
          for selection in selections:
              if selection in self.choices:
                  index = self.choices.index(selection)
                  self.list_ctrl.Select(index, on=True)
  
  
  class DelEndpointDialog(SelectFromListDialog):
      def __init__(self, endpoints):
          SelectFromListDialog.__init__(self, "Delete Endpoint", "Endpoints", endpoints,
                                        exclude={'MRN', 'Tx Site', 'ROI Name', 'Volume (cc)'})
  
  
  class SelectRegressionVariablesDialog(SelectFromListDialog):
      def __init__(self, dependent_variable, independent_variable_choices, selections=None):
          self.dependent_variable = dependent_variable
          self.independent_variable_choices = independent_variable_choices
          size = get_window_size(1, 0.8)
          SelectFromListDialog.__init__(self, "Select Variables for %s" % dependent_variable,
                                        "Independent Variables", independent_variable_choices,
                                        size=(350, size[1]), column_width=300, selections=selections)
  
  
  def query_dlg(parent, query_type, set_values=False):
      """
      Function to create either QueryCategoryDialog or QueryNumericalDialog
      :param parent: pointer to app object
      :type parent: DVHAMainFrame
      :param query_type: either 'categorical' or 'numerical'
      :type query_type: str
      :param set_values: If True, pre-populate dialog values with the currently selected row (i.e., edit a row)
      :type set_values: bool
      """
      dlg = {'categorical': QueryCategoryDialog,
             'numerical': QueryNumericalDialog}[query_type]()
      data_table = {'categorical': parent.data_table_categorical,
                    'numerical': parent.data_table_numerical}[query_type]
      selected_index = {'categorical': parent.selected_index_categorical,
                        'numerical': parent.selected_index_numerical}[query_type]
      if set_values:
          dlg.set_values(data_table.get_row(selected_index))
  
      res = dlg.ShowModal()
      if res == wx.ID_OK:
          row = dlg.get_values()
          if set_values:
              data_table.edit_row(row, selected_index)
          else:
              data_table.append_row(row)
          parent.update_all_query_buttons()
      dlg.Destroy()
  
  
  class QueryCategoryDialog(wx.Dialog):
      """
      Add/Edit query parameters for categorical data
      """
      def __init__(self):
          wx.Dialog.__init__(self, None, title='Query by Categorical Data')
  
          self.selector_categories = sql_columns.categorical
  
          selector_options = list(self.selector_categories)
          selector_options.sort()
  
          self.combo_box_1 = wx.ComboBox(self, wx.ID_ANY, choices=selector_options, style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_2 = wx.ComboBox(self, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.checkbox_1 = wx.CheckBox(self, wx.ID_ANY, "Exclude")
          self.button_OK = wx.Button(self, wx.ID_OK, "OK")
          self.button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
  
          self.__do_layout()
  
          self.combo_box_1.SetValue('ROI Institutional Category')
          self.update_category_2(None)
          self.Bind(wx.EVT_COMBOBOX, self.update_category_2, id=self.combo_box_1.GetId())
  
          self.Fit()
          self.Center()
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_vbox = wx.BoxSizer(wx.VERTICAL)
          sizer_ok_cancel = wx.BoxSizer(wx.HORIZONTAL)
          sizer_widgets = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, ""), wx.HORIZONTAL)
          sizer_category_2 = wx.BoxSizer(wx.VERTICAL)
          sizer_category_1 = wx.BoxSizer(wx.VERTICAL)
          label_category_1 = wx.StaticText(self, wx.ID_ANY, "Category 1:")
          sizer_category_1.Add(label_category_1, 0, wx.ALL | wx.EXPAND, 5)
          sizer_category_1.Add(self.combo_box_1, 0, wx.ALL, 5)
          sizer_widgets.Add(sizer_category_1, 1, wx.EXPAND, 0)
          label_category_2 = wx.StaticText(self, wx.ID_ANY, "Category 2:")
          sizer_category_2.Add(label_category_2, 0, wx.ALL | wx.EXPAND, 5)
          sizer_category_2.Add(self.combo_box_2, 0, wx.EXPAND | wx.ALL, 5)
          sizer_widgets.Add(sizer_category_2, 1, wx.EXPAND, 0)
          sizer_widgets.Add(self.checkbox_1, 0, wx.ALL | wx.EXPAND, 5)
          sizer_vbox.Add(sizer_widgets, 0, wx.ALL | wx.EXPAND, 5)
          sizer_ok_cancel.Add(self.button_OK, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
          sizer_ok_cancel.Add(self.button_cancel, 0, wx.LEFT | wx.RIGHT, 5)
          sizer_vbox.Add(sizer_ok_cancel, 0, wx.ALIGN_RIGHT | wx.ALL, 5)
          sizer_wrapper.Add(sizer_vbox, 0, wx.ALL | wx.EXPAND, 5)
          self.SetSizer(sizer_wrapper)
  
      def update_category_2(self, evt):
          key = self.combo_box_1.GetValue()
          table = self.selector_categories[key]['table']
          col = self.selector_categories[key]['var_name']
          with DVH_SQL() as cnx:
              options = cnx.get_unique_values(table, col)
          self.combo_box_2.Clear()
          self.combo_box_2.Append(options)
          if options:
              self.combo_box_2.SetValue(options[0])
  
      def set_category_1(self, value):
          self.combo_box_1.SetValue(value)
          self.update_category_2(None)
  
      def set_category_2(self, value):
          self.combo_box_2.SetValue(value)
  
      def set_check_box_not(self, value):
          self.checkbox_1.SetValue(value)
  
      def set_values(self, values):
          self.set_category_1(values[0])
          self.set_category_2(values[1])
          self.set_check_box_not({'Include': False, 'Exclude': True}[values[2]])
  
      def get_values(self):
          return [self.combo_box_1.GetValue(),
                  self.combo_box_2.GetValue(),
                  ['Include', 'Exclude'][self.checkbox_1.GetValue()]]
  
  
  class QueryNumericalDialog(wx.Dialog):
      """
      Add/Edit query parameters for numerical data
      """
      def __init__(self):
          wx.Dialog.__init__(self, None, title='Query by Numerical Data')
  
          self.numerical_categories = sql_columns.numerical
  
          numerical_options = list(self.numerical_categories)
          numerical_options.sort()
  
          self.combo_box_1 = wx.ComboBox(self, wx.ID_ANY, choices=numerical_options, style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.text_ctrl_min = wx.TextCtrl(self, wx.ID_ANY, "")
          self.text_ctrl_max = wx.TextCtrl(self, wx.ID_ANY, "")
          self.checkbox_1 = wx.CheckBox(self, wx.ID_ANY, "Exclude")
          self.button_OK = wx.Button(self, wx.ID_OK, "OK")
          self.button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
          self.button_date_picker = wx.Button(self, wx.ID_ANY, "Edit Dates")
  
          self.__do_layout()
          self.__do_bind()
  
          self.combo_box_1.SetValue("ROI Max Dose")
          self.update_range(None)
  
      def __do_bind(self):
          self.Bind(wx.EVT_COMBOBOX, self.update_range, id=self.combo_box_1.GetId())
          self.Bind(wx.EVT_BUTTON, self.on_date_picker, id=self.button_date_picker.GetId())
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_vbox = wx.BoxSizer(wx.VERTICAL)
          sizer_ok_cancel = wx.BoxSizer(wx.HORIZONTAL)
          sizer_widgets = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, ""), wx.HORIZONTAL)
          sizer_max = wx.BoxSizer(wx.VERTICAL)
          sizer_min = wx.BoxSizer(wx.VERTICAL)
          sizer_category_1 = wx.BoxSizer(wx.VERTICAL)
          label_category = wx.StaticText(self, wx.ID_ANY, "Category:")
          sizer_category_1.Add(label_category, 0, wx.ALL | wx.EXPAND, 5)
          sizer_category_1.Add(self.combo_box_1, 0, wx.ALL, 5)
          sizer_widgets.Add(sizer_category_1, 1, wx.EXPAND, 0)
          self.label_min = wx.StaticText(self, wx.ID_ANY, "Min:")
          sizer_min.Add(self.label_min, 0, wx.ALL | wx.EXPAND, 5)
          sizer_min.Add(self.text_ctrl_min, 0, wx.ALL, 5)
          sizer_widgets.Add(sizer_min, 0, wx.EXPAND, 0)
          self.label_max = wx.StaticText(self, wx.ID_ANY, "Max:")
          sizer_max.Add(self.label_max, 0, wx.ALL | wx.EXPAND, 5)
          sizer_max.Add(self.text_ctrl_max, 0, wx.ALL, 5)
          sizer_widgets.Add(sizer_max, 0, wx.EXPAND, 0)
          sizer_widgets.Add(self.checkbox_1, 0, wx.ALL | wx.EXPAND, 5)
          sizer_vbox.Add(sizer_widgets, 0, wx.ALL | wx.EXPAND, 5)
          sizer_ok_cancel.Add(self.button_date_picker, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
          sizer_ok_cancel.Add(self.button_OK, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
          sizer_ok_cancel.Add(self.button_cancel, 0, wx.LEFT | wx.RIGHT, 5)
          sizer_vbox.Add(sizer_ok_cancel, 0, wx.ALIGN_RIGHT | wx.ALL, 5)
          sizer_wrapper.Add(sizer_vbox, 0, wx.ALL | wx.EXPAND, 5)
          self.SetSizer(sizer_wrapper)
  
          self.Fit()
          self.Center()
  
      def update_range(self, evt):
          key = self.combo_box_1.GetValue()
          table = self.numerical_categories[key]['table']
          col = self.numerical_categories[key]['var_name']
          units = self.numerical_categories[key]['units']
          with DVH_SQL() as cnx:
              min_value = cnx.get_min_value(table, col)
              max_value = cnx.get_max_value(table, col)
  
          self.button_date_picker.Enable('date' in key.lower())
          self.text_ctrl_min.Enable('date' not in key.lower())
          self.text_ctrl_max.Enable('date' not in key.lower())
  
          self.set_min_value(min_value)
          self.set_max_value(max_value)
  
          if 'date' in key.lower():
              self.update_min_max_text('Start:', 'End:')
              self.on_date_picker()
          elif units:
              self.update_min_max_text('Min (%s):' % units, 'Max (%s):' % units)
          else:
              self.update_min_max_text('Min:', 'Max:')
  
          self.clean_numeric_input()
  
      def update_min_max_text(self, min_text, max_text):
          self.label_min.SetLabelText(min_text)
          self.label_max.SetLabelText(max_text)
  
      def set_category(self, value):
          self.combo_box_1.SetValue(value)
          self.update_range(None)
  
      def set_min_value(self, value):
          self.text_ctrl_min.SetValue(str(value))
  
      def set_max_value(self, value):
          self.text_ctrl_max.SetValue(str(value))
  
      def set_check_box_not(self, value):
          self.checkbox_1.SetValue(value)
  
      def set_values(self, values):
          self.set_category(values[0])
          self.set_min_value(str(values[1]))
          self.set_max_value(str(values[2]))
          self.set_check_box_not({'Include': False, 'Exclude': True}[values[3]])
  
      def get_values(self):
          return [self.combo_box_1.GetValue(),
                  self.text_ctrl_min.GetValue(),
                  self.text_ctrl_max.GetValue(),
                  ['Include', 'Exclude'][self.checkbox_1.GetValue()]]
  
      def validated_text(self, input_type):
          old_value = {'min': self.text_ctrl_min.GetValue(), 'max': self.text_ctrl_max.GetValue()}[input_type]
  
          try:
              new_value = float(old_value)
          except ValueError:
              key = self.combo_box_1.GetValue()
              table = self.numerical_categories[key]['table']
              col = self.numerical_categories[key]['var_name']
              with DVH_SQL() as cnx:
                  if input_type == 'min':
                      new_value = cnx.get_min_value(table, col)
                  else:
                      new_value = cnx.get_max_value(table, col)
          return new_value
  
      def on_date_picker(self, *evt):
          DatePicker(initial_date=self.text_ctrl_min.GetValue(), title='Start Date',
                     action=self.set_min_value, sql_date_format=True)
          time.sleep(0.3)  # Immediately loading the next DatePicker looks like a glitch
          DatePicker(initial_date=self.text_ctrl_max.GetValue(), title='End Date',
                     action=self.set_max_value, sql_date_format=True)
  
      def clean_numeric_input(self):
          for text_ctrl in [self.text_ctrl_min, self.text_ctrl_max]:
              try:
                  value = float(text_ctrl.GetValue())
                  func = [np.floor, np.ceil][text_ctrl == self.text_ctrl_max]
                  text_ctrl.SetValue("%0.2f" % func(value))
              except ValueError:
                  pass
  
  
  class UserSettings(wx.Frame):
      """
      Customize directories and visual settings for DVHA
      """
      def __init__(self, parent):
          """
          :param parent: main application frame
          """
          wx.Frame.__init__(self, None, title="User Settings")
  
          self.parent = parent
          self.options = parent.options
          self.options.edit_detected = False
  
          colors = list(plot_colors.cnames)
          colors.sort()
  
          color_variables = self.get_option_choices('COLOR')
          size_variables = self.get_option_choices('SIZE')
          width_variables = self.get_option_choices('LINE_WIDTH')
          line_dash_variables = self.get_option_choices('LINE_DASH')
          alpha_variables = self.get_option_choices('ALPHA')
  
          line_style_options = ['solid', 'dashed', 'dotted', 'dotdash', 'dashdot']
  
          # self.SetSize((500, 580))
          self.text_ctrl_inbox = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_DONTWRAP)
          self.button_inbox = wx.Button(self, wx.ID_ANY, u"…")
          self.text_ctrl_imported = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_DONTWRAP)
          self.button_imported = wx.Button(self, wx.ID_ANY, u"…")
  
          self.checkbox_dicom_dvh = wx.CheckBox(self, wx.ID_ANY, "Import DICOM DVH if available")
          self.dvh_bin_width_input = wx.TextCtrl(self, wx.ID_ANY, str(self.options.dvh_bin_width))
          self.dvh_bin_max_dose = wx.TextCtrl(self, wx.ID_ANY, "")
          self.dvh_bin_max_dose_units = wx.ComboBox(self, wx.ID_ANY, choices=self.options.dvh_bin_max_dose_options,
                                                    style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_colors_category = wx.ComboBox(self, wx.ID_ANY, choices=color_variables,
                                                       style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_colors_selection = wx.ComboBox(self, wx.ID_ANY, choices=colors,
                                                        style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_sizes_category = wx.ComboBox(self, wx.ID_ANY, choices=size_variables,
                                                      style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.spin_ctrl_sizes_input = wx.SpinCtrl(self, wx.ID_ANY, "0", min=0, max=20, style=wx.SP_ARROW_KEYS)
          self.combo_box_line_widths_category = wx.ComboBox(self, wx.ID_ANY, choices=width_variables,
                                                            style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.spin_ctrl_line_widths_input = wx.SpinCtrl(self, wx.ID_ANY, "0", min=0, max=10, style=wx.SP_ARROW_KEYS)
          self.combo_box_line_styles_category = wx.ComboBox(self, wx.ID_ANY, choices=line_dash_variables,
                                                            style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_line_styles_selection = wx.ComboBox(self, wx.ID_ANY, choices=line_style_options,
                                                             style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.combo_box_alpha_category = wx.ComboBox(self, wx.ID_ANY, choices=alpha_variables,
                                                      style=wx.CB_DROPDOWN | wx.CB_READONLY)
          self.spin_ctrl_alpha_input = wx.SpinCtrlDouble(self, wx.ID_ANY, "0", min=0, max=1.0, style=wx.SP_ARROW_KEYS)
          self.button_restore_defaults = wx.Button(self, wx.ID_ANY, "Restore Defaults")
          self.button_ok = wx.Button(self, wx.ID_OK, "OK")
          self.button_cancel = wx.Button(self, wx.ID_CANCEL, "Cancel")
          self.button_apply = wx.Button(self, wx.ID_ANY, 'Apply')
  
          self.__set_properties()
          self.__do_layout()
          self.__do_bind()
  
          self.refresh_options()
          self.load_paths()
  
          self.is_edited = False
  
          set_msw_background_color(self)
          set_frame_icon(self)
  
      def __set_properties(self):
          self.text_ctrl_inbox.SetToolTip("Default directory for batch processing of incoming DICOM files")
          self.text_ctrl_inbox.SetMinSize((100, 21))
          self.button_inbox.SetMinSize((40, 21))
          self.text_ctrl_imported.SetToolTip("Directory for post-processed DICOM files")
          self.text_ctrl_imported.SetMinSize((100, 21))
          self.button_imported.SetMinSize((40, 21))
          self.checkbox_dicom_dvh.SetValue(self.options.USE_DICOM_DVH)
          self.checkbox_dicom_dvh.SetToolTip("If a DICOM RT-Dose file has a DVH Sequence, use this DVH instead of "
                                             "recalculating during import.")
          self.dvh_bin_width_input.SetToolTip("Value must be an integer.")
          self.dvh_bin_width_input.SetMinSize((50, 21))
          self.dvh_bin_max_dose.SetToolTip("Prevent memory issues if dose grid contains very large, unrealistic doses")
          self.dvh_bin_max_dose.SetValue(str(self.options.dvh_bin_max_dose[self.options.dvh_bin_max_dose_units]))
          self.dvh_bin_max_dose_units.SetValue(self.options.dvh_bin_max_dose_units)
          # self.dvh_bin_width_input.SetMinSize((50, 22))
          # self.dvh_bin_max_dose_units.SetMinSize((50, 22))
          self.combo_box_colors_category.SetMinSize((250, self.combo_box_colors_category.GetSize()[1]))
          self.combo_box_colors_selection.SetMinSize((145, self.combo_box_colors_selection.GetSize()[1]))
          self.combo_box_sizes_category.SetMinSize((250, self.combo_box_sizes_category.GetSize()[1]))
          self.spin_ctrl_sizes_input.SetMinSize((50, 22))
          self.combo_box_line_widths_category.SetMinSize((250, self.combo_box_line_widths_category.GetSize()[1]))
          self.spin_ctrl_line_widths_input.SetMinSize((50, 22))
          self.combo_box_line_styles_category.SetMinSize((250, self.combo_box_line_styles_category.GetSize()[1]))
          self.combo_box_line_styles_selection.SetMinSize((145, self.combo_box_line_styles_selection.GetSize()[1]))
          self.combo_box_alpha_category.SetMinSize((250, self.combo_box_alpha_category.GetSize()[1]))
          self.spin_ctrl_alpha_input.SetMinSize((70, 22))
  
          self.spin_ctrl_alpha_input.SetIncrement(0.1)
  
          # Windows needs this done explicitly or the value will be an empty string
          self.combo_box_alpha_category.SetValue('IQR Alpha')
          self.combo_box_colors_category.SetValue('Plot Color')
          self.combo_box_line_styles_category.SetValue('DVH Line Dash Selection')
          self.combo_box_line_widths_category.SetValue('DVH Line Width Selection')
          self.combo_box_sizes_category.SetValue('Plot Axis Label Font Size')
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_ok_cancel = wx.BoxSizer(wx.HORIZONTAL)
          sizer_plot_options = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "Plot Options"), wx.VERTICAL)
          sizer_dvh_options = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "DVH Options"), wx.VERTICAL)
          sizer_dvh_bin_width = wx.BoxSizer(wx.HORIZONTAL)
          sizer_dvh_bin_max = wx.BoxSizer(wx.HORIZONTAL)
          sizer_alpha = wx.BoxSizer(wx.VERTICAL)
          sizer_alpha_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_line_styles = wx.BoxSizer(wx.VERTICAL)
          sizer_line_styles_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_line_widths = wx.BoxSizer(wx.VERTICAL)
          sizer_line_widths_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_sizes = wx.BoxSizer(wx.VERTICAL)
          sizer_sizes_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_colors = wx.BoxSizer(wx.VERTICAL)
          sizer_colors_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_dicom_directories = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, "DICOM Directories"), wx.VERTICAL)
          sizer_imported_wrapper = wx.BoxSizer(wx.HORIZONTAL)
          sizer_imported = wx.BoxSizer(wx.VERTICAL)
          sizer_imported_input = wx.BoxSizer(wx.HORIZONTAL)
          sizer_inbox_wrapper = wx.BoxSizer(wx.HORIZONTAL)
          sizer_inbox = wx.BoxSizer(wx.VERTICAL)
          sizer_inbox_input = wx.BoxSizer(wx.HORIZONTAL)
  
          label_inbox = wx.StaticText(self, wx.ID_ANY, "Inbox:")
          label_inbox.SetToolTip("Default directory for batch processing of incoming DICOM files")
          sizer_inbox.Add(label_inbox, 0, 0, 5)
          sizer_inbox_input.Add(self.text_ctrl_inbox, 1, wx.ALL, 5)
          sizer_inbox_input.Add(self.button_inbox, 0, wx.ALL, 5)
          sizer_inbox.Add(sizer_inbox_input, 1, wx.EXPAND, 0)
          sizer_inbox_wrapper.Add(sizer_inbox, 1, wx.EXPAND, 0)
          sizer_dicom_directories.Add(sizer_inbox_wrapper, 1, wx.EXPAND, 0)
  
          label_imported = wx.StaticText(self, wx.ID_ANY, "Imported:")
          label_imported.SetToolTip("Directory for post-processed DICOM files")
          sizer_imported.Add(label_imported, 0, 0, 5)
          sizer_imported_input.Add(self.text_ctrl_imported, 1, wx.ALL, 5)
          sizer_imported_input.Add(self.button_imported, 0, wx.ALL, 5)
          sizer_imported.Add(sizer_imported_input, 1, wx.EXPAND, 0)
          sizer_imported_wrapper.Add(sizer_imported, 1, wx.EXPAND, 0)
          sizer_dicom_directories.Add(sizer_imported_wrapper, 1, wx.EXPAND, 0)
          sizer_wrapper.Add(sizer_dicom_directories, 0, wx.ALL | wx.EXPAND, 10)
  
          sizer_dvh_options.Add(self.checkbox_dicom_dvh, 0, wx.ALL, 5)
          sizer_dvh_options.Add((20, 10), 0, 0, 0)
          label_dvh_bin_width = wx.StaticText(self, wx.ID_ANY, "DVH Bin Width (cGy):")
          label_dvh_bin_width.SetToolTip("Value must be an integer")
          sizer_dvh_bin_width.Add(label_dvh_bin_width, 0, wx.EXPAND | wx.TOP | wx.LEFT, 5)
          sizer_dvh_bin_width.Add(self.dvh_bin_width_input, 1, wx.ALL, 5)
          label_max_dose_bin = wx.StaticText(self, wx.ID_ANY, "Max Dose Bin Limit:")
          label_max_dose_bin.SetToolTip("Prevent memory issues if dose grid contains very large, unrealistic doses")
          sizer_dvh_bin_max.Add(label_max_dose_bin, 0, wx.EXPAND | wx.TOP | wx.LEFT, 5)
          sizer_dvh_bin_max.Add((20, 20), 0, 0, 0)
          sizer_dvh_bin_max.Add(self.dvh_bin_max_dose, 0, wx.EXPAND, 0)
          sizer_dvh_bin_max.Add((20, 20), 0, 0, 0)
          sizer_dvh_bin_max.Add(self.dvh_bin_max_dose_units, 0, wx.EXPAND, 0)
          sizer_dvh_options.Add(sizer_dvh_bin_width, 0, wx.BOTTOM, 10)
          sizer_dvh_options.Add(sizer_dvh_bin_max, 0, 0, 0)
  
          sizer_wrapper.Add(sizer_dvh_options, 0, wx.ALL | wx.EXPAND, 10)
  
          label_colors = wx.StaticText(self, wx.ID_ANY, "Colors:")
          sizer_colors.Add(label_colors, 0, 0, 0)
          sizer_colors_input.Add(self.combo_box_colors_category, 0, 0, 0)
          sizer_colors_input.Add((20, 20), 0, 0, 0)
          sizer_colors_input.Add(self.combo_box_colors_selection, 0, 0, 0)
          sizer_colors.Add(sizer_colors_input, 1, wx.EXPAND, 0)
          sizer_plot_options.Add(sizer_colors, 1, wx.EXPAND, 0)
  
          label_sizes = wx.StaticText(self, wx.ID_ANY, "Sizes:")
          sizer_sizes.Add(label_sizes, 0, 0, 0)
          sizer_sizes_input.Add(self.combo_box_sizes_category, 0, 0, 0)
          sizer_sizes_input.Add((20, 20), 0, 0, 0)
          sizer_sizes_input.Add(self.spin_ctrl_sizes_input, 0, 0, 0)
          sizer_sizes.Add(sizer_sizes_input, 1, wx.EXPAND, 0)
          sizer_plot_options.Add(sizer_sizes, 1, wx.EXPAND, 0)
  
          label_line_widths = wx.StaticText(self, wx.ID_ANY, "Line Widths:")
          sizer_line_widths.Add(label_line_widths, 0, 0, 0)
          sizer_line_widths_input.Add(self.combo_box_line_widths_category, 0, 0, 0)
          sizer_line_widths_input.Add((20, 20), 0, 0, 0)
          sizer_line_widths_input.Add(self.spin_ctrl_line_widths_input, 0, 0, 0)
          sizer_line_widths.Add(sizer_line_widths_input, 1, wx.EXPAND, 0)
          sizer_plot_options.Add(sizer_line_widths, 1, wx.EXPAND, 0)
  
          label_line_styles = wx.StaticText(self, wx.ID_ANY, "Line Styles:")
          sizer_line_styles.Add(label_line_styles, 0, 0, 0)
          sizer_line_styles_input.Add(self.combo_box_line_styles_category, 0, 0, 0)
          sizer_line_styles_input.Add((20, 20), 0, 0, 0)
          sizer_line_styles_input.Add(self.combo_box_line_styles_selection, 0, 0, 0)
          sizer_line_styles.Add(sizer_line_styles_input, 1, wx.EXPAND, 0)
          sizer_plot_options.Add(sizer_line_styles, 1, wx.EXPAND, 0)
  
          label_alpha = wx.StaticText(self, wx.ID_ANY, "Alpha:")
          sizer_alpha.Add(label_alpha, 0, 0, 0)
          sizer_alpha_input.Add(self.combo_box_alpha_category, 0, 0, 0)
          sizer_alpha_input.Add((20, 20), 0, 0, 0)
          sizer_alpha_input.Add(self.spin_ctrl_alpha_input, 0, 0, 0)
          sizer_alpha.Add(sizer_alpha_input, 1, wx.EXPAND, 0)
          sizer_plot_options.Add(sizer_alpha, 1, wx.EXPAND, 0)
          sizer_wrapper.Add(sizer_plot_options, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10)
  
          sizer_ok_cancel.Add(self.button_restore_defaults, 0, wx.RIGHT, 20)
          sizer_ok_cancel.Add(self.button_apply, 0, wx.LEFT | wx.RIGHT, 5)
          sizer_ok_cancel.Add(self.button_ok, 0, wx.LEFT | wx.RIGHT, 5)
          sizer_ok_cancel.Add(self.button_cancel, 0, wx.LEFT | wx.RIGHT, 5)
          sizer_wrapper.Add(sizer_ok_cancel, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
  
          self.SetSizer(sizer_wrapper)
          self.Layout()
          self.Fit()
  
          self.options.set_window_position(self, 'user_settings')
  
      def __do_bind(self):
          self.Bind(wx.EVT_BUTTON, self.inbox_dir_dlg, id=self.button_inbox.GetId())
          self.Bind(wx.EVT_BUTTON, self.imported_dir_dlg, id=self.button_imported.GetId())
  
          self.Bind(wx.EVT_CHECKBOX, self.on_use_dicom_dvh, id=self.checkbox_dicom_dvh.GetId())
  
          self.Bind(wx.EVT_TEXT, self.update_dvh_bin_width_val, id=self.dvh_bin_width_input.GetId())
          self.Bind(wx.EVT_TEXT, self.update_dvh_bin_max_dose_val, id=self.dvh_bin_max_dose.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_dvh_bin_max_dose_units_val, id=self.dvh_bin_max_dose_units.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_input_colors_var, id=self.combo_box_colors_category.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_size_var, id=self.combo_box_sizes_category.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_line_width_var, id=self.combo_box_line_widths_category.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_line_style_var, id=self.combo_box_line_styles_category.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_alpha_var, id=self.combo_box_alpha_category.GetId())
  
          self.Bind(wx.EVT_COMBOBOX, self.update_input_colors_val, id=self.combo_box_colors_selection.GetId())
          self.Bind(wx.EVT_TEXT, self.update_size_val, id=self.spin_ctrl_sizes_input.GetId())
          self.Bind(wx.EVT_TEXT, self.update_line_width_val, id=self.spin_ctrl_line_widths_input.GetId())
          self.Bind(wx.EVT_COMBOBOX, self.update_line_style_val, id=self.combo_box_line_styles_selection.GetId())
          self.Bind(wx.EVT_TEXT, self.update_alpha_val, id=self.spin_ctrl_alpha_input.GetId())
  
          self.Bind(wx.EVT_BUTTON, self.restore_defaults, id=self.button_restore_defaults.GetId())
          self.Bind(wx.EVT_BUTTON, self.on_apply, id=self.button_apply.GetId())
          self.Bind(wx.EVT_BUTTON, self.on_ok, id=wx.ID_OK)
          self.Bind(wx.EVT_BUTTON, self.on_cancel, id=wx.ID_CANCEL)
  
          self.Bind(wx.EVT_CLOSE, self.on_cancel)
  
      def on_ok(self, *evt):
          if self.options.is_edited:  # Tracks edits since last redraw
              self.apply_and_redraw_plots()
          self.close()
  
      def on_cancel(self, *evt):
          if self.is_edited:  # Tracks edits since last options save
              self.options.load()
              self.apply_and_redraw_plots()
          self.close()
  
      def close(self, *evt):
          self.save_window_position()
          self.options.save()
          self.parent.user_settings = None
          self.Destroy()
  
      def save_window_position(self):
          self.options.save_window_position(self, 'user_settings')
  
      def inbox_dir_dlg(self, evt):
          self.dir_dlg('inbox', self.text_ctrl_inbox)
  
      def imported_dir_dlg(self, evt):
          self.dir_dlg('imported', self.text_ctrl_imported)
  
      def dir_dlg(self, dir_type, text_ctrl):
          """
          Create a DirDialog and edit the associated TextCtrl
          :param dir_type: the directory type to be displayed in DirDialog title
          :type dir_type: str
          :param text_ctrl: the associated TextCtrl from the user settings dialog
          :type text_ctrl: TextCtrl
          """
          starting_dir = text_ctrl.GetValue()
          if not isdir(starting_dir):
              starting_dir = ""
          dlg = wx.DirDialog(self, "Select %s directory" % dir_type, starting_dir,
                             wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST)
          if dlg.ShowModal() == wx.ID_OK:
              text_ctrl.SetValue(dlg.GetPath())
              option_attr = 'INBOX_DIR' if dir_type == 'inbox' else 'IMPORTED_DIR'
              self.options.set_option(option_attr, dlg.GetPath())
          dlg.Destroy()
  
      def get_option_choices(self, category):
          """
          Lookup properties in Options.option_attr that fit the specified category
          :param category: COLOR, SIZE, ALPHA, LINE_WIDTH, LINE_DASH
          :type category: str
          :return: all options with the category in their name
          :rtype: list
          """
          choices = [self.clean_option_variable(c) for c in self.options.option_attr if c.find(category) > -1]
          choices.sort()
          return choices
  
      @staticmethod
      def clean_option_variable(option_variable, inverse=False):
          """
          Convert option variable between UI and python format
          :param option_variable: option available for edit user settings UI
          :type option_variable: str
          :param inverse: True to return python format, False to return UI format
          :type inverse: bool
          :return: formatted option variable
          :rtype: str
          """
          if inverse:
              return option_variable.upper().replace(' ', '_')
          else:
              return option_variable.replace('_', ' ').title().replace('Dvh', 'DVH').replace('Iqr', 'IQR')
  
      def update_input_colors_var(self, *args):
          var = self.clean_option_variable(self.combo_box_colors_category.GetValue(), inverse=True)
          val = getattr(self.options, var)
          self.combo_box_colors_selection.SetValue(val)
  
      def update_input_colors_val(self, *args):
          var = self.clean_option_variable(self.combo_box_colors_category.GetValue(), inverse=True)
          val = self.combo_box_colors_selection.GetValue()
          self.options.set_option(var, val)
  
      def update_size_var(self, *args):
          var = self.clean_option_variable(self.combo_box_sizes_category.GetValue(), inverse=True)
          try:
              val = getattr(self.options, var).replace('pt', '')
          except AttributeError:
              val = str(getattr(self.options, var))
          try:
              val = int(float(val))
          except ValueError:
              pass
          self.spin_ctrl_sizes_input.SetValue(val)
  
      def update_size_val(self, *args):
          new = self.spin_ctrl_sizes_input.GetValue()
          if 'Font' in self.combo_box_sizes_category.GetValue():
              try:
                  val = str(int(new)) + 'pt'
              except ValueError:
                  val = '10pt'
          else:
              try:
                  val = float(new)
              except ValueError:
                  val = 1.
  
          var = self.clean_option_variable(self.combo_box_sizes_category.GetValue(), inverse=True)
          self.options.set_option(var, val)
  
      def update_line_width_var(self, *args):
          var = self.clean_option_variable(self.combo_box_line_widths_category.GetValue(), inverse=True)
          val = str(getattr(self.options, var))
          try:
              val = int(float(val))
          except ValueError:
              pass
          self.spin_ctrl_line_widths_input.SetValue(val)
  
      def update_line_width_val(self, *args):
          new = self.spin_ctrl_line_widths_input.GetValue()
          try:
              val = int(float(new))
          except ValueError:
              val = 1
          var = self.clean_option_variable(self.combo_box_line_widths_category.GetValue(), inverse=True)
          self.options.set_option(var, val)
  
      def update_line_style_var(self, *args):
          var = self.clean_option_variable(self.combo_box_line_styles_category.GetValue(), inverse=True)
          self.combo_box_line_styles_selection.SetValue(getattr(self.options, var))
  
      def update_line_style_val(self, *args):
          var = self.clean_option_variable(self.combo_box_line_styles_category.GetValue(), inverse=True)
          val = self.combo_box_line_styles_selection.GetValue()
          self.options.set_option(var, val)
  
      def update_alpha_var(self, *args):
          var = self.clean_option_variable(self.combo_box_alpha_category.GetValue(), inverse=True)
          self.spin_ctrl_alpha_input.SetValue(str(getattr(self.options, var)))
  
      def update_alpha_val(self, *args):
          new = self.spin_ctrl_alpha_input.GetValue()
          try:
              val = float(new)
          except ValueError:
              val = 1.
          var = self.clean_option_variable(self.combo_box_alpha_category.GetValue(), inverse=True)
          self.options.set_option(var, val)
  
      def update_dvh_bin_width_val(self, *args):
          new = self.dvh_bin_width_input.GetValue()
          try:
              val = abs(int(new))
              self.options.set_option('dvh_bin_width', val)
          except ValueError:
              if new != '':
                  self.dvh_bin_width_input.SetValue(str(self.options.dvh_bin_width))
  
      def update_dvh_bin_width_var(self, *args):
          self.dvh_bin_width_input.SetValue(str(self.options.dvh_bin_width))
  
      def update_dvh_bin_max_dose_val(self, *args):
          new_val = self.dvh_bin_max_dose.GetValue()
          units = self.dvh_bin_max_dose_units.GetValue()
          try:
              val = abs(float(new_val))
              new = {key: value for key, value in self.options.dvh_bin_max_dose.items()}
              new[units] = val
              self.options.set_option('dvh_bin_max_dose', new)
          except ValueError:
              if new_val != '':
                  self.dvh_bin_max_dose.SetValue(str(self.options.dvh_bin_max_dose[units]))
  
      def update_dvh_bin_max_dose_var(self, *args):
          units = self.dvh_bin_max_dose_units.GetValue()
          self.dvh_bin_max_dose.SetValue(str(self.options.dvh_bin_max_dose[units]))
  
      def update_dvh_bin_max_dose_units_val(self, *args):
          new = self.dvh_bin_max_dose_units.GetValue()
          self.options.set_option('dvh_bin_max_dose_units', new)
          self.dvh_bin_max_dose.SetValue(str(self.options.dvh_bin_max_dose[new]))
  
      def update_dvh_bin_max_dose_units_var(self, *args):
          self.dvh_bin_max_dose_units.SetValue(self.options.dvh_bin_max_dose_units)
  
      def refresh_options(self):
          self.update_dvh_bin_width_var()
          self.update_dvh_bin_max_dose_var()
          self.update_dvh_bin_max_dose_units_var()
          self.update_alpha_var()
          self.update_input_colors_var()
          self.update_line_style_var()
          self.update_line_width_var()
          self.update_size_var()
  
      def load_paths(self):
          self.text_ctrl_inbox.SetValue(self.options.INBOX_DIR)
          self.text_ctrl_imported.SetValue(self.options.IMPORTED_DIR)
  
      def restore_defaults(self, *args):
          MessageDialog(self, "Restore default preferences?", action_yes_func=self.options.restore_defaults)
          self.update_size_val()
          self.refresh_options()
          self.on_apply()
  
      def on_use_dicom_dvh(self, *evt):
          self.options.set_option('USE_DICOM_DVH', self.checkbox_dicom_dvh.GetValue())
  
      def on_apply(self, *evt):
          self.apply_and_redraw_plots()
          self.is_edited = True  # Used to track edits since last options save
          self.options.is_edited = False  # Used to track edits since redraw, is set to True on options.set_option()
  
      def apply_and_redraw_plots(self):
          self.parent.apply_plot_options()
          self.parent.redraw_plots()
  
  
  class About(wx.Dialog):
      """
      Simple dialog to display the LICENSE file and a brief text header in a scrollable window
      """
      def __init__(self, *evt):
          wx.Dialog.__init__(self, None, title='About DVH Analytics')
  
          scrolled_window = wx.ScrolledWindow(self, wx.ID_ANY)
  
          with open(LICENSE_PATH, 'r', encoding="utf8") as license_file:
              license_text = ''.join([line for line in license_file])
  
          license_text = "DVH Analytics v%s\ndvhanalytics.com\n\n%s" % (DefaultOptions().VERSION, license_text)
  
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_text = wx.BoxSizer(wx.VERTICAL)
  
          scrolled_window.SetScrollRate(20, 20)
  
          license_text = wx.StaticText(scrolled_window, wx.ID_ANY, license_text)
          sizer_text.Add(license_text, 0, wx.EXPAND | wx.ALL, 5)
          scrolled_window.SetSizer(sizer_text)
          sizer_wrapper.Add(scrolled_window, 1, wx.EXPAND, 0)
  
          self.SetBackgroundColour(wx.WHITE)
  
          self.SetSizer(sizer_wrapper)
          self.SetSize((750, 900))
          self.Center()
  
          self.ShowModal()
          self.Destroy()
  
  
  class PythonLibraries(wx.Dialog):
      """Simple dialog to display the installed python libraries"""
      def __init__(self, *evt):
          wx.Dialog.__init__(self, None, title='Installed Python Libraries')
  
          self.list_ctrl = wx.ListCtrl(self, wx.ID_ANY,
                                       style=wx.BORDER_SUNKEN | wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES)
          self.data_table = DataTable(self.list_ctrl, widths=[200, 150])
  
          self.__set_data()
          self.__do_layout()
  
          self.run()
  
      def __set_data(self):
          columns = ['Library', 'Version']
          try:
              libraries = get_installed_python_libraries()
          except Exception:
              libraries = {'Library': ['pip list failed'], 'Version': [' ']}
          self.data_table.set_data(libraries, columns)
  
      def __do_layout(self):
          sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
          sizer_main = wx.BoxSizer(wx.VERTICAL)
  
          note = wx.StaticText(self, wx.ID_ANY, "NOTE: If running from source, this might reflect global packages.\n"
                                                "If running from an executable, this should be accurate.")
          note.Wrap(500)
  
          sizer_main.Add(note, 0, wx.EXPAND | wx.ALL, 5)
          sizer_main.Add(self.list_ctrl, 1, wx.EXPAND, 0)
          sizer_wrapper.Add(sizer_main, 1, wx.EXPAND | wx.ALL, 10)
  
          self.SetBackgroundColour(wx.WHITE)
          self.SetSizer(sizer_wrapper)
          self.SetSize(get_window_size(0.3, 0.8))
          self.Center()
  
      def run(self):
          self.ShowModal()
          self.Destroy()
  
  
  def do_sqlite_backup(parent, options):
      flags = wx.ICON_ERROR | wx.OK | wx.OK_DEFAULT
      if options.DB_TYPE_GRPS[1] == 'sqlite':
          try:
              file_paths = backup_sqlite_db(options)
              if file_paths is not None:
                  msg = "Current DB: %s\nCopied to: %s" % (tuple(file_paths))
                  caption = "SQLite DB Backup Successful"
                  flags = wx.OK | wx.OK_DEFAULT
              else:
                  msg = "Your SQLite DB was not backed up for an unknown reason."
                  caption = "SQLite DB Backup Unsuccessful"
          except Exception as e:
              msg = str(e)
              caption = "SQLite DB Backup Error"
      else:
          msg = "This feature only applies to SQLite users. PostgreSQL users should contact their DB " \
                "administrator or look into the pg_dump command."
          caption = "SQLite DB Backup Error"
  
      ErrorDialog(parent, msg, caption, flags=flags)
  