"""
New wallpaper configuration GUI for Superpaper.
"""
import os
import time
from operator import itemgetter
from PIL import Image, ImageEnhance, UnidentifiedImageError

import superpaper.sp_logging as sp_logging
import superpaper.wallpaper_processing as wpproc
from superpaper.configuration_dialogs import BrowsePaths, PerspectiveConfig, HelpFrame, HelpPopup
from superpaper.data import GeneralSettingsData, ProfileData, TempProfileData, CLIProfileData, list_profiles
from superpaper.message_dialog import show_message_dialog
from superpaper.sp_paths import PATH, CONFIG_PATH, PROFILES_PATH
from superpaper.wallpaper_processing import NUM_DISPLAYS, get_display_data, change_wallpaper_job, resize_to_fill

try:
    import wx
    import wx.adv
except ImportError:
    exit()

RESOURCES_PATH = os.path.join(PATH, "superpaper/resources")
TRAY_ICON = os.path.join(RESOURCES_PATH, "superpaper.png")

class ConfigFrame(wx.Frame):
    """Wallpaper configuration dialog frame base class."""
    def __init__(self, parent_tray_obj):
        wx.Frame.__init__(self, parent=None, title="Superpaper Wallpaper Configuration")
        self.frame_sizer = wx.BoxSizer(wx.VERTICAL)
        config_panel = WallpaperSettingsPanel(self, parent_tray_obj)
        self.frame_sizer.Add(config_panel, 1, wx.EXPAND)
        self.SetAutoLayout(True)
        self.SetSizer(self.frame_sizer)
        self.SetMinSize((600,600))
        self.SetIcon(wx.Icon(TRAY_ICON, wx.BITMAP_TYPE_PNG))
        self.Fit()
        self.Layout()
        self.Center()
        self.Show()

class WallpaperSettingsPanel(wx.Panel):
    """This class defines the wallpaper config dialog UI."""
    def __init__(self, parent, parent_tray_obj):
        wx.Panel.__init__(self, parent)
        self.frame = parent
        self.parent_tray_obj = parent_tray_obj
        self.sizer_main = wx.BoxSizer(wx.VERTICAL)
        self.sizer_top_half = wx.BoxSizer(wx.HORIZONTAL) # wallpaper/monitor preview
        self.sizer_bottom_half = wx.BoxSizer(wx.VERTICAL) # settings, buttons etc
        # bottom_half: setting sizers
        self.sizer_profiles = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer_setting_sizers = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer_settings_left = wx.BoxSizer(wx.VERTICAL)
        self.sizer_settings_right = wx.BoxSizer(wx.HORIZONTAL)
        # bottom_half: bottom button row
        self.sizer_bottom_buttonrow = wx.BoxSizer(wx.HORIZONTAL)

        self.defdir = GeneralSettingsData().browse_default_dir
        # settings GUI properties
        self.tc_width = 160  # standard width for wx.TextCtrl etc elements.
        self.show_advanced_settings = False
        self.use_multi_image = False
        self.multi_column_listc = False
        BMP_SIZE = 32
        self.tsize = (BMP_SIZE, BMP_SIZE)
        self.image_list = wx.ImageList(BMP_SIZE, BMP_SIZE)

        # top half
        self.resized = False
        # display_data = wpproc.get_display_data()
        self.display_sys = wpproc.DisplaySystem()
        # self.wpprev_pnl = WallpaperPreviewPanel(self.frame, self.display_sys)
        self.wpprev_pnl = WallpaperPreviewPanel(self, self.display_sys)
        self.sizer_top_half.Add(self.wpprev_pnl, 1, wx.CENTER|wx.EXPAND, 5)
        # self.sizer_top_half.SetMinSize((400,200))
        # self.sizer_top_half.SetMinSize()
        self.wpprev_pnl.Bind(wx.EVT_SIZE, self.onResize)
        self.wpprev_pnl.Bind(wx.EVT_IDLE, self.onIdle)

        # bottom half

        # profile sizer contents
        self.create_sizer_profiles()

        # settings sizer left contents
        self.create_sizer_settings_left()

        self.create_sizer_settings_advanced()

        # settings sizer right contents
        self.create_sizer_settings_right()

        # bottom button row contents
        self.create_sizer_bottom_buttonrow()


        # Add sub-sizers to bottom_half
        #    Note: horizontal sizer needs children to have proportion = 1
        #    in order to expand them horizontally instead of vertically.
        self.sizer_setting_sizers.Add(
            self.sizer_settings_left, 1, wx.CENTER|wx.EXPAND|wx.TOP|wx.LEFT, 5
            )
        self.sizer_setting_sizers.Add(
            self.sizer_settings_right, 2, wx.CENTER|wx.EXPAND|wx.TOP|wx.LEFT, 0
            )
        self.sizer_setting_sizers.Add(
            self.sizer_setting_adv, 0.8, wx.CENTER|wx.EXPAND|wx.TOP|wx.RIGHT, 5
            )
        self.sizer_setting_sizers.Layout()

        self.sizer_bottom_half.Add(self.sizer_profiles, 0, wx.CENTER|wx.EXPAND|wx.ALL, 0)
        self.sizer_bottom_half.Add(self.sizer_setting_sizers, 1, wx.CENTER|wx.EXPAND|wx.ALL, 0)
        self.sizer_bottom_half.Add(self.sizer_bottom_buttonrow, 0, wx.CENTER|wx.EXPAND|wx.ALL, 0)
        # self.sizer_bottom_half.SetMinSize(1000,500)

        # Collect items at main sizer
        self.sizer_main.Add(self.sizer_top_half, 1, wx.CENTER|wx.EXPAND|wx.BOTTOM, 5)
        self.sizer_main.Add(self.sizer_bottom_half, 0, wx.CENTER|wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 0)

        self.SetSizer(self.sizer_main)
        self.sizer_main.Fit(parent)

        self.sizer_setting_sizers.Hide(self.sizer_setting_adv)

        ### End __init__.



    #
    # Sizer creation methods
    #
    def create_sizer_profiles(self):
        # choice menu
        self.list_of_profiles = list_profiles()
        self.profnames = []
        for prof in self.list_of_profiles:
            self.profnames.append(prof.name)
        self.profnames.append("Create a new profile")
        self.choice_profiles = wx.Choice(self, -1, name="ProfileChoice", choices=self.profnames)
        self.choice_profiles.Bind(wx.EVT_CHOICE, self.onSelect)
        st_choice_profiles = wx.StaticText(self, -1, "Setting profiles:")
        # name txt ctrl
        st_name = wx.StaticText(self, -1, "Profile name:")
        self.tc_name = wx.TextCtrl(self, -1, size=(self.tc_width, -1))
        self.tc_name.SetMaxLength(14)
        # buttons
        self.button_new = wx.Button(self, label="New")
        self.button_save = wx.Button(self, label="Save")
        self.button_delete = wx.Button(self, label="Delete")
        self.button_new.Bind(wx.EVT_BUTTON, self.onCreateNewProfile)
        self.button_save.Bind(wx.EVT_BUTTON, self.onSave)
        self.button_delete.Bind(wx.EVT_BUTTON, self.onDeleteProfile)

        # Add elements to the sizer
        self.sizer_profiles.Add(st_choice_profiles, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(self.choice_profiles, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(st_name, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(self.tc_name, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(self.button_new, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(self.button_save, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_profiles.Add(self.button_delete, 0, wx.CENTER|wx.ALL, 5)


    def create_sizer_settings_left(self):
        # span mode sizer
        radio_choices_spanmode = ["Simple span", "Advanced span", "Separate image for every display"]
        self.radiobox_spanmode = wx.RadioBox(
            self, wx.ID_ANY,
            label="Span mode",
            choices=radio_choices_spanmode,
            style=wx.RA_VERTICAL
        )
        self.radiobox_spanmode.Bind(wx.EVT_RADIOBOX, self.onSpanRadio)

        # slideshow sizer
        self.sizer_setting_slideshow = wx.StaticBoxSizer(wx.VERTICAL, self, "Wallpaper slideshow")
        statbox_parent_sshow = self.sizer_setting_slideshow.GetStaticBox()
        sizer_sshow_subsettings = wx.GridSizer(2, 5, 5)
        self.st_sshow_sort = wx.StaticText(statbox_parent_sshow, -1, "Slideshow order:")
        self.ch_sshow_sort = wx.Choice(statbox_parent_sshow, -1, name="SortChoice",
                                       #  size=(self.tc_width*0.7, -1),
                                       choices=["Shuffle", "Alphabetical"])
        # ch_sort_size = self.ch_sshow_sort.GetClientSize()
        self.st_sshow_delay = wx.StaticText(statbox_parent_sshow, -1, "Delay (minutes):")
        self.tc_sshow_delay = wx.TextCtrl(
            statbox_parent_sshow, -1,
            # size=(self.tc_width*0.69, -1),
            # size=ch_sort_size,
            style=wx.TE_RIGHT
        )
        self.cb_slideshow = wx.CheckBox(statbox_parent_sshow, -1, "Slideshow")
        self.st_sshow_sort.Disable()
        self.st_sshow_delay.Disable()
        self.tc_sshow_delay.Disable()
        self.ch_sshow_sort.Disable()
        self.cb_slideshow.Bind(wx.EVT_CHECKBOX, self.onCheckboxSlideshow)
        self.sizer_setting_slideshow.Add(self.cb_slideshow, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        sizer_sshow_subsettings.Add(self.st_sshow_delay, 0, wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 0)
        sizer_sshow_subsettings.Add(self.tc_sshow_delay, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 0)
        sizer_sshow_subsettings.Add(self.st_sshow_sort, 0, wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 0)
        sizer_sshow_subsettings.Add(self.ch_sshow_sort, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 0)
        self.sizer_setting_slideshow.Add(sizer_sshow_subsettings, 0, wx.ALIGN_LEFT|wx.LEFT|wx.BOTTOM, 10)
        # self.sizer_setting_slideshow.AddSpacer(5)

        # hotkey sizer
        self.sizer_setting_hotkey = wx.StaticBoxSizer(wx.VERTICAL, self, "Hotkey")
        statbox_parent_hkey = self.sizer_setting_hotkey.GetStaticBox()
        self.cb_hotkey = wx.CheckBox(statbox_parent_hkey, -1, "Bind a hotkey to this profile")
        st_hotkey_bind = wx.StaticText(statbox_parent_hkey, -1, "Hotkey to bind:")
        st_hotkey_bind.Disable()
        self.tc_hotkey_bind = wx.TextCtrl(statbox_parent_hkey, -1, size=(self.tc_width, -1))
        self.tc_hotkey_bind.Disable()
        self.tc_hotkey_bind.SetToolTip(wx.ToolTip("Modifiers: control, alt, shift, super.\nExample: control+super+x"))
        self.hotkey_bind_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.hotkey_bind_sizer.Add(st_hotkey_bind, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
        self.hotkey_bind_sizer.Add(self.tc_hotkey_bind, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
        help_bmp = wx.ArtProvider.GetBitmap(wx.ART_QUESTION, wx.ART_BUTTON, (20, 20))
        self.button_help_hotkey = wx.BitmapButton(statbox_parent_hkey, bitmap=help_bmp, name="butt_help_hk")
        self.button_help_hotkey.Bind(wx.EVT_BUTTON, self.onHelpHotkey)
        self.button_help_hotkey.Disable()
        self.hotkey_bind_sizer.Add(self.button_help_hotkey, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
        self.sizer_setting_hotkey.Add(self.cb_hotkey, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        self.sizer_setting_hotkey.Add(self.hotkey_bind_sizer, 0, wx.CENTER|wx.EXPAND|wx.ALL, 5)
        self.cb_hotkey.Bind(wx.EVT_CHECKBOX, self.onCheckboxHotkey)

        # Add subsizers to the left column sizer
        self.sizer_settings_left.Add(self.radiobox_spanmode, 0, wx.EXPAND, 5)
        self.sizer_settings_left.Add(self.sizer_setting_slideshow, 0, wx.EXPAND, 5)
        self.sizer_settings_left.Add(self.sizer_setting_hotkey, 0, wx.EXPAND, 5)


    def create_sizer_settings_right(self):
        # paths sizer contents
        self.create_sizer_paths()

    def create_sizer_paths(self):
        self.sizer_setting_paths = wx.StaticBoxSizer(wx.VERTICAL, self, "Wallpaper paths")
        self.statbox_parent_paths = self.sizer_setting_paths.GetStaticBox()
        st_paths_info = wx.StaticText(self.statbox_parent_paths, -1, "Browse to add your wallpaper files or source folders here:")
        if self.use_multi_image:
            self.path_listctrl = wx.ListCtrl(self.statbox_parent_paths, -1,
                                              style=wx.LC_REPORT
                                              | wx.BORDER_SIMPLE
                                              | wx.LC_SORT_ASCENDING
                                             )
            self.path_listctrl.InsertColumn(0, 'Display', wx.LIST_FORMAT_RIGHT, width = 100)
            self.path_listctrl.InsertColumn(1, 'Source', width = 400)
        else:
            # show simpler listing without header if only one wallpaper target
            self.path_listctrl = wx.ListCtrl(self.statbox_parent_paths, -1,
                                              style=wx.LC_REPORT
                                              | wx.BORDER_SIMPLE
                                              | wx.LC_NO_HEADER
                                             )
            self.path_listctrl.InsertColumn(0, 'Source', width = 500)
        self.path_listctrl.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)

        self.sizer_setting_paths.Add(st_paths_info, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        self.sizer_setting_paths.Add(
            self.path_listctrl, 1, wx.CENTER|wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5
            )
        # Buttons
        self.sizer_setting_paths_buttons = wx.BoxSizer(wx.HORIZONTAL)
        self.button_browse = wx.Button(self.statbox_parent_paths, label="Browse")
        self.button_remove_source = wx.Button(self.statbox_parent_paths, label="Remove selected source")
        self.button_browse.Bind(wx.EVT_BUTTON, self.onBrowsePaths)
        self.button_remove_source.Bind(wx.EVT_BUTTON, self.onRemoveSource)
        self.sizer_setting_paths_buttons.Add(self.button_browse, 0, wx.CENTER|wx.ALL, 5)
        self.sizer_setting_paths_buttons.Add(self.button_remove_source, 0, wx.CENTER|wx.ALL, 5)
        # add button sizer to parent paths sizer
        self.sizer_setting_paths.Add(self.sizer_setting_paths_buttons, 0, wx.CENTER|wx.EXPAND|wx.ALL, 0)

        self.sizer_settings_right.Add(
            self.sizer_setting_paths, 1, wx.CENTER|wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 5
        )


    def create_sizer_settings_advanced(self):
        """Create sizer for advanced spanning settings."""
        self.sizer_setting_adv = wx.StaticBoxSizer(wx.VERTICAL, self,
                                                   "Advanced wallpaper adjustment")
        statbox_parent_adv = self.sizer_setting_adv.GetStaticBox()

        # Fallback Diagonal Inches
        # self.sizer_setting_diaginch = wx.StaticBoxSizer(wx.VERTICAL, self, "Display diagonal sizes")
        # statbox_parent_diaginch = self.sizer_setting_diaginch.GetStaticBox()
        self.sizer_setting_diaginch = wx.BoxSizer(wx.VERTICAL)
        statbox_parent_diaginch = self
        st_diaginch_override = wx.StaticText(statbox_parent_diaginch, -1,
                                             "Display diagonal sizes:")
        self.button_override = wx.Button(statbox_parent_diaginch, label="Override detected sizes")
        self.button_override.Bind(wx.EVT_BUTTON, self.onOverrideSizes)
        self.sizer_setting_diaginch.Add(st_diaginch_override, 0, wx.ALIGN_LEFT|wx.BOTTOM, 5)
        self.sizer_setting_diaginch.Add(self.button_override, 0,
                                        wx.ALIGN_LEFT|wx.LEFT, 10)

        # Bezels
        # self.sizer_setting_bezels = wx.StaticBoxSizer(wx.VERTICAL, self, "Bezel Correction")
        # statbox_parent_bezels = self.sizer_setting_bezels.GetStaticBox()
        self.sizer_setting_bezels = wx.BoxSizer(wx.VERTICAL)
        statbox_parent_bezels = self
        # self.cb_bezels = wx.CheckBox(statbox_parent_bezels, -1, "Apply bezel correction")
        # self.cb_bezels.Bind(wx.EVT_CHECKBOX, self.onCheckboxBezels)
        # self.sizer_setting_bezels.Add(self.cb_bezels, 0, wx.ALIGN_LEFT|wx.LEFT, 5)
        st_bezels = wx.StaticText(statbox_parent_bezels, -1,
                                  "Adjust bezel sizes:")

        self.sizer_bezel_buttons = wx.BoxSizer(wx.HORIZONTAL)
        self.button_bezels = wx.Button(statbox_parent_bezels, -1, label="Configure")
        self.button_bezels_save = wx.Button(statbox_parent_bezels, -1, label="Save bezels")
        self.button_bezels_canc = wx.Button(statbox_parent_bezels, -1, label="Cancel")
        self.button_bezels.Bind(wx.EVT_BUTTON, self.onConfigureBezels)
        self.button_bezels_save.Bind(wx.EVT_BUTTON, self.onConfigureBezelsSave)
        self.button_bezels_canc.Bind(wx.EVT_BUTTON, self.onConfigureBezelsCanc)
        help_bmp = wx.ArtProvider.GetBitmap(wx.ART_QUESTION, wx.ART_BUTTON, (20, 20))
        self.button_help_bezel = wx.BitmapButton(statbox_parent_bezels, bitmap=help_bmp, name="butt_help_bez")
        self.button_help_bezel.Bind(wx.EVT_BUTTON, self.onHelpBezels)

        self.sizer_bezel_buttons.Add(self.button_bezels, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 10)
        self.sizer_bezel_buttons.Add(self.button_bezels_save, 0, wx.ALIGN_CENTER_VERTICAL|wx.TOP|wx.BOTTOM, 10)
        self.sizer_bezel_buttons.Add(self.button_bezels_canc, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 10)
        self.sizer_bezel_buttons.Add(self.button_help_bezel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 10)
        self.sizer_setting_bezels.Add(st_bezels, 0, wx.ALL, 0)
        self.sizer_setting_bezels.Add(self.sizer_bezel_buttons, 1, wx.EXPAND, 0)
        # self.button_bezels.Disable()
        self.button_bezels_save.Disable()
        self.button_bezels_canc.Disable()
        # self.button_help_bezel.Disable()

        # Offsets
        # self.sizer_setting_offsets = wx.StaticBoxSizer(wx.VERTICAL, self, "Manual Display Offsets")
        # statbox_parent_offsets = self.sizer_setting_offsets.GetStaticBox()
        self.sizer_setting_offsets = wx.BoxSizer(wx.VERTICAL)
        statbox_parent_offsets = self
        self.cb_offsets = wx.CheckBox(statbox_parent_offsets, -1, "Apply manual offsets")
        self.cb_offsets.Bind(wx.EVT_CHECKBOX, self.onCheckboxOffsets)
        st_offsets = wx.StaticText(
            statbox_parent_offsets, -1,
            "Manual offsets in pixels (x,y=px,px):"
        )
        st_offsets.Disable()
        self.sizer_setting_offsets.Add(self.cb_offsets, 0, wx.ALIGN_LEFT|wx.BOTTOM, 5)
        self.sizer_setting_offsets.Add(st_offsets, 0, wx.ALIGN_LEFT|wx.LEFT, 10)
        tc_list_sizer_offs = wx.WrapSizer(wx.HORIZONTAL)
        self.tc_list_offsets = self.list_of_textctrl(statbox_parent_offsets, wpproc.NUM_DISPLAYS)
        for tc in self.tc_list_offsets:
            st = wx.StaticText(statbox_parent_offsets, -1,
                               str(self.tc_list_offsets.index(tc))+":")
            tc_list_sizer_offs.Add(st, 0, wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.TOP|wx.BOTTOM, 5)
            tc_list_sizer_offs.Add(tc, 0, wx.ALIGN_LEFT|wx.ALL, 5)
            tc.SetValue("0,0")
            st.Disable()
            tc.Disable()
        self.sizer_setting_offsets.Add(tc_list_sizer_offs, 0, wx.ALIGN_LEFT|wx.LEFT, 5)

        #Perspective profile
        # self.sizer_setting_persp = wx.StaticBoxSizer(wx.HORIZONTAL, self, "")
        # statbox_parent_persp = self.sizer_setting_persp.GetStaticBox()
        self.sizer_setting_persp = wx.BoxSizer(wx.HORIZONTAL)
        st_perspprof = wx.StaticText(self, -1, "Perspective profile:")
        persp_choices = (["default"]
                         + list(self.display_sys.perspective_dict.keys())
                         + ["disabled"])
        self.ch_persp = wx.Choice(self, -1, name="PerspChoice",
                                       size=(165, -1),
                                       choices=persp_choices)
        self.sizer_setting_persp.Add(st_perspprof, 0, wx.ALIGN_LEFT|wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0)
        self.sizer_setting_persp.Add(self.ch_persp, 0, wx.ALIGN_LEFT|wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5)


        # Add setting subsizers to the adv settings sizer
        self.sizer_setting_adv.Add(self.sizer_setting_diaginch, 0, wx.CENTER|wx.EXPAND|wx.ALL, 5)
        self.sizer_setting_adv.Add(self.sizer_setting_bezels, 0, wx.CENTER|wx.EXPAND|wx.ALL, 5)
        self.sizer_setting_adv.Add(self.sizer_setting_offsets, 0, wx.CENTER|wx.EXPAND|wx.ALL, 5)
        self.sizer_setting_adv.Add(self.sizer_setting_persp, 0, wx.CENTER|wx.EXPAND|wx.ALL, 5)


    def create_sizer_diaginch_override(self):
        self.sizer_setting_diaginch.Clear(True)
        # statbox_parent_diaginch = self.sizer_setting_diaginch.GetStaticBox()
        statbox_parent_diaginch = self
        self.cb_diaginch = wx.CheckBox(statbox_parent_diaginch, -1, "Input display sizes manually")
        self.cb_diaginch.Bind(wx.EVT_CHECKBOX, self.onCheckboxDiaginch)
        st_diaginch = wx.StaticText(
            statbox_parent_diaginch, -1,
            "Display diagonal sizes (inches):"
        )
        st_diaginch.Disable()
        self.sizer_setting_diaginch.Add(self.cb_diaginch, 0, wx.ALIGN_LEFT|wx.LEFT, 0)
        self.sizer_setting_diaginch.Add(st_diaginch, 0, wx.ALIGN_LEFT|wx.LEFT, 10)
        # diag size data for fields
        diags = [str(dsp.diagonal_size()[1]) for dsp in self.display_sys.disp_list]
        # sizer for textctrls
        tc_list_sizer_diag = wx.WrapSizer(wx.HORIZONTAL)
        self.tc_list_diaginch = self.list_of_textctrl(statbox_parent_diaginch, wpproc.NUM_DISPLAYS, fraction=2/5)
        for tc, diag in zip(self.tc_list_diaginch, diags):
            tc_list_sizer_diag.Add(tc, 0, wx.ALIGN_LEFT|wx.ALL, 5)
            tc.ChangeValue(diag)
            tc.Disable()
        self.button_diaginch_save = wx.Button(statbox_parent_diaginch, label="Save")
        self.button_diaginch_save.Bind(wx.EVT_BUTTON, self.onSaveDiagInch)
        tc_list_sizer_diag.Add(self.button_diaginch_save, 0, wx.ALL, 5)
        self.button_diaginch_save.Disable()
        self.sizer_setting_diaginch.Add(tc_list_sizer_diag, 0, wx.ALIGN_LEFT|wx.LEFT, 5)
        self.sizer_setting_adv.Layout()
        self.sizer_main.Layout()
        self.sizer_main.Fit(self.frame)
        # Check cb according to DisplaySystem 'use_user_diags'
        self.cb_diaginch.SetValue(self.display_sys.use_user_diags)
        # Update sizer content based on new cb state
        self.onCheckboxDiaginch(None)

    def create_sizer_bottom_buttonrow(self):
        self.button_help = wx.Button(self, label="Help")
        self.button_align_test = wx.Button(self, label="Align Test")
        self.button_perspectives = wx.Button(self, label="Perspectives")
        self.button_apply = wx.Button(self, label="Apply")
        self.button_close = wx.Button(self, label="Close")

        self.button_apply.Bind(wx.EVT_BUTTON, self.onApply)
        self.button_align_test.Bind(wx.EVT_BUTTON, self.onAlignTest)
        self.button_perspectives.Bind(wx.EVT_BUTTON, self.onPerspectives)
        self.button_help.Bind(wx.EVT_BUTTON, self.onHelp)
        self.button_close.Bind(wx.EVT_BUTTON, self.onClose)

        self.sizer_bottom_buttonrow.Add(self.button_help, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        self.sizer_bottom_buttonrow.Add(self.button_align_test, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        self.sizer_bottom_buttonrow.Hide(self.button_align_test)
        self.sizer_bottom_buttonrow.Add(self.button_perspectives, 0, wx.ALIGN_LEFT|wx.ALL, 5)
        self.sizer_bottom_buttonrow.Hide(self.button_perspectives)
        self.sizer_bottom_buttonrow.Layout()
        self.sizer_bottom_buttonrow.AddStretchSpacer()
        self.sizer_bottom_buttonrow.Add(self.button_apply, 0, wx.ALL, 5)
        self.sizer_bottom_buttonrow.Add(self.button_close, 0, wx.ALL, 5)



    #
    # Profile loading and display methods
    #
    def populate_fields(self, profile):
        """Populates config dialog fields with data from a profile."""
        self.tc_name.ChangeValue(profile.name)

        self.show_advanced_settings = False
        self.use_multi_image = False
        legacy_advanced = bool(
            profile.spanmode == "single" and
            bool(
                profile.ppimode or
                profile.bezels or
                profile.manual_offsets_useronly
            )
        )

        # Basic settings
        if (profile.spanmode == "single" and not legacy_advanced):
            self.radiobox_spanmode.SetSelection(0)
            self.use_multi_image = False
        elif (profile.spanmode == "advanced" or legacy_advanced):
            self.show_advanced_settings = True
            self.use_multi_image = False
            self.radiobox_spanmode.SetSelection(1)
        elif profile.spanmode == "multi":
            self.use_multi_image = True
            self.radiobox_spanmode.SetSelection(2)
        else:
            # default to simple span
            self.radiobox_spanmode.SetSelection(0)

        if profile.slideshow:
            self.cb_slideshow.SetValue(True)
            wx.PostEvent(self.cb_slideshow, wx.CommandEvent(commandEventType=wx.EVT_CHECKBOX.typeId))
            self.tc_sshow_delay.ChangeValue(str(profile.delay_list[0]/60))
            if profile.sortmode == "shuffle":
                self.ch_sshow_sort.SetSelection(0)
            elif profile.sortmode == "alphabetical":
                self.ch_sshow_sort.SetSelection(1)
            else:
                self.ch_sshow_sort.SetSelection(wx.NOT_FOUND)
        else:
            self.cb_slideshow.SetValue(False)
            wx.PostEvent(self.cb_slideshow, wx.CommandEvent(commandEventType=wx.EVT_CHECKBOX.typeId))
            self.tc_sshow_delay.Clear()
            self.ch_sshow_sort.SetSelection(wx.NOT_FOUND)

        if profile.hk_binding:
            self.cb_hotkey.SetValue(True)
            self.tc_hotkey_bind.ChangeValue(self.show_hkbinding(profile.hk_binding))
        else:
            self.cb_hotkey.SetValue(False)
            self.tc_hotkey_bind.Clear()
        wx.PostEvent(self.cb_hotkey, wx.CommandEvent(commandEventType=wx.EVT_CHECKBOX.typeId))


        # Advanced settings
        self.show_adv_setting_sizer(self.show_advanced_settings)
        # profile.inches: not stored in profile anymore
        # profile.bezels: not stored in profile anymore
        if profile.manual_offsets_useronly:
            self.cb_offsets.SetValue(True)
            for tc, off in zip(self.tc_list_offsets, profile.manual_offsets_useronly):
                offstr = "{},{}".format(off[0], off[1])
                tc.SetValue(offstr)
        else:
            self.cb_offsets.SetValue(False)
        wx.PostEvent(self.cb_offsets, wx.CommandEvent(commandEventType=wx.EVT_CHECKBOX.typeId))
        if profile.perspective:
            self.ch_persp.SetSelection(
                self.ch_persp.FindString(profile.perspective, False)
            )
        else:
            self.ch_persp.SetSelection(0)

        # Paths displays: get number to show from profile.
        self.paths_array_to_listctrl(profile.paths_array)

        # Update wallpaper preview from selected profile
        if self.show_advanced_settings:
            display_data = self.display_sys.get_disp_list(True)
        else:
            display_data = self.display_sys.get_disp_list(False)
        self.wpprev_pnl.preview_wallpaper(
            profile.next_wallpaper_files(),
            self.show_advanced_settings,
            self.use_multi_image,
            display_data
        )
        self.wpprev_pnl.toggle_buttons(
            show_config = self.show_advanced_settings,
            in_config = False
        )



    def paths_array_to_listctrl(self, paths_array):
        self.refresh_path_listctrl(self.use_multi_image)
        if self.use_multi_image:
            for plist, idx in zip(paths_array, range(len(paths_array))):
                for pth in plist:
                    self.append_to_listctrl(
                        [str(idx), pth]
                    )
        else:
            for plist in paths_array:
                for pth in plist:
                    self.append_to_listctrl([pth])

    def show_hkbinding(self, hktuple):
        """Format a hotkey tuple into a '+' separated string."""
        if hktuple:
            hkstring = "+".join(hktuple)
            return hkstring
        else:
            return ""


    #
    # Helper methods
    #
    def update_choiceprofile(self):
        """Reload profile list into the choice box."""
        self.list_of_profiles = list_profiles()
        self.profnames = []
        for prof in self.list_of_profiles:
            self.profnames.append(prof.name)
        self.profnames.append("Create a new profile")
        self.choice_profiles.SetItems(self.profnames)

    def list_of_textctrl(self, ctrl_parent, num_disp, fraction = 1/2):
        tcrtl_list = []
        for i in range(num_disp):
            tcrtl_list.append(
                wx.TextCtrl(ctrl_parent, -1,
                    size=(self.tc_width * fraction, -1),
                    style=wx.TE_RIGHT
                )
            )
        return tcrtl_list

    def sizer_toggle_children(self, sizer, bool_state, toggle_cb=False):
        for child in sizer.GetChildren():
            if child.IsSizer():
                self.sizer_toggle_children(child.GetSizer(), bool_state)
            else:
                widget = child.GetWindow()
                if (
                    isinstance(widget, wx.TextCtrl) or
                    isinstance(widget, wx.StaticText) or
                    isinstance(widget, wx.Choice) or
                    isinstance(widget, wx.Button) or
                    isinstance(widget, wx.CheckBox) and toggle_cb
                ):
                    widget.Enable(bool_state)

    def toggle_radio_and_profile_choice(self, enable):
        """Toggle enabled state of span mode radiobox and profile sizer children."""
        self.radiobox_spanmode.Enable(enable)
        self.sizer_toggle_children(self.sizer_profiles, enable)
        self.sizer_toggle_children(self.sizer_setting_diaginch, enable, True)
        if enable:
            try:
                diag_cb_state = self.cb_diaginch.GetValue()
                self.sizer_toggle_children(self.sizer_setting_diaginch, diag_cb_state)
            except AttributeError:
                pass

    def show_adv_setting_sizer(self, show_bool):
        """Show/Hide the sizer for advanced spanning settings."""
        self.sizer_setting_sizers.Show(self.sizer_setting_adv, show=show_bool)
        self.toggle_bezel_buttons(enable_config_butt=True)
        # To only reveal sizer sit no frame resize
        self.sizer_main.Layout()
        # to re-layout the whole window making it wider run:
        # self.sizer_setting_adv.Layout()
        # self.sizer_main.Fit(self.frame)
        self.sizer_bottom_buttonrow.Show(self.button_align_test, show=show_bool)
        self.sizer_bottom_buttonrow.Show(self.button_perspectives, show=show_bool)
        self.sizer_bottom_buttonrow.Layout()

    def toggle_bezel_buttons(self, bezel_mode = False, enable_config_butt = True):
        """Show/Hide bezel config buttons.

        If not in bezel mode show config button, and if in it hide config and show
        save and cancel buttons.
        enable_config_butt optional controls whether the config button should be
        enabled/disabled."""
        # self.sizer_bezel_buttons.Show(self.button_bezels, show=not bezel_mode)
        # self.sizer_bezel_buttons.Show(self.button_bezels_save, show=bezel_mode)
        # self.sizer_bezel_buttons.Show(self.button_bezels_canc, show=bezel_mode)
        # self.sizer_bezel_buttons.Layout()
        self.button_bezels.Enable(enable_config_butt)
        self.button_bezels_save.Enable(bezel_mode)
        self.button_bezels_canc.Enable(bezel_mode)
        self.button_help_bezel.Enable(True)

    def refresh_path_listctrl(self, use_multi_image, migrate_paths=False):
        if use_multi_image == self.multi_column_listc and migrate_paths:
            self.sizer_main.Layout()
        else:
            if migrate_paths and self.path_listctrl.GetItemCount():
                # warn that paths can't be migrated
                msg = ("Wallpaper sources cannot be migrated between span"
                       " and multi image, continue?"
                       "\n"
                       "Saved sources are not affected until you overwrite.")
                res = show_message_dialog(msg, style="YES_NO")
                if not res:
                    # user canceled
                    return False
            self.path_listctrl.Destroy()
            self.image_list.RemoveAll()
            if use_multi_image:
                self.multi_column_listc = True
                self.path_listctrl = wx.ListCtrl(self.statbox_parent_paths, -1,
                                                 style=wx.LC_REPORT
                                                 | wx.BORDER_SIMPLE
                                                 | wx.LC_SORT_ASCENDING
                                                )
                self.path_listctrl.InsertColumn(0, 'Display', wx.LIST_FORMAT_RIGHT, width=100)
                self.path_listctrl.InsertColumn(1, 'Source', width=400)
            else:
                self.multi_column_listc = False
                # show simpler listing without header if only one wallpaper target
                self.path_listctrl = wx.ListCtrl(self.statbox_parent_paths, -1,
                                                 style=wx.LC_REPORT
                                                 | wx.BORDER_SIMPLE
                                                 | wx.LC_NO_HEADER
                                                )
                self.path_listctrl.InsertColumn(0, 'Source', width=500)
            self.path_listctrl.SetImageList(self.image_list, wx.IMAGE_LIST_SMALL)
            self.sizer_setting_paths.Insert(1, self.path_listctrl, 1,
                                            wx.CENTER | wx.EXPAND | wx.ALL, 5)
            self.path_listctrl.InvalidateBestSize()
            self.sizer_main.Layout()
        return True

    def test_diag_value(self, inch_str):
        """Test that entered inch_str is a valid size and return it."""
        try:
            num = float(inch_str)
            if num > 0:
                return num
            else:
                return False
        except ValueError:
            return False


    #
    # Event methods
    #
    def onResize(self, event):
        self.resized = True
        self.wpprev_pnl.refresh_preview()

    def onIdle(self, event):
        leftdown = wx.GetMouseState().LeftIsDown()
        update = bool(self.resized and not leftdown)
        if update:
            self.wpprev_pnl.full_refresh_preview(update, self.show_advanced_settings, self.use_multi_image)
            self.resized = False
        else:
            event.Skip()


    def onSpanRadio(self, event):
        old_adv_set = self.show_advanced_settings
        old_mult_img_set = self.use_multi_image
        selection = self.radiobox_spanmode.GetSelection()
        if selection == 1:
            self.show_advanced_settings = True
            self.use_multi_image = False
        elif selection == 2:
            self.show_advanced_settings = False
            self.use_multi_image = True
        else:
            self.show_advanced_settings = False
            self.use_multi_image = False
        cont = self.refresh_path_listctrl(self.use_multi_image, migrate_paths=True)
        if not cont:
            self.show_advanced_settings = old_adv_set
            self.use_multi_image = old_mult_img_set
            if old_adv_set and not old_mult_img_set:
                self.radiobox_spanmode.SetSelection(1)
            elif not old_adv_set and old_mult_img_set:
                self.radiobox_spanmode.SetSelection(2)
            else:
                self.radiobox_spanmode.SetSelection(0)
            return
        self.show_adv_setting_sizer(self.show_advanced_settings)
        display_data = self.display_sys.get_disp_list(self.show_advanced_settings)
        self.wpprev_pnl.update_display_data(
            display_data,
            self.show_advanced_settings,
            self.use_multi_image
        )
        self.wpprev_pnl.toggle_buttons(
            show_config = self.show_advanced_settings,
            in_config = False
        )


    def onCheckboxSlideshow(self, event):
        cb_state = self.cb_slideshow.GetValue()
        sizer = self.sizer_setting_slideshow
        self.sizer_toggle_children(sizer, cb_state)

    def onCheckboxHotkey(self, event):
        cb_state = self.cb_hotkey.GetValue()
        sizer = self.hotkey_bind_sizer
        self.sizer_toggle_children(sizer, cb_state)

    def onCheckboxBezels(self, event):
        cb_state = self.cb_bezels.GetValue()
        sizer = self.sizer_setting_bezels
        self.sizer_toggle_children(sizer, cb_state)

    def onCheckboxOffsets(self, event):
        cb_state = self.cb_offsets.GetValue()
        sizer = self.sizer_setting_offsets
        self.sizer_toggle_children(sizer, cb_state)

    def onCheckboxDiaginch(self, event):
        cb_state = self.cb_diaginch.GetValue()
        sizer = self.sizer_setting_diaginch
        self.sizer_toggle_children(sizer, cb_state)
        if cb_state == False:
            # revert to automatic detection and save
            self.display_sys.update_display_diags("auto")
            self.display_sys.save_system()
            diags = [str(dsp.diagonal_size()[1]) for dsp in self.display_sys.disp_list]
            for tc, diag in zip(self.tc_list_diaginch, diags):
                tc.ChangeValue(diag)
            display_data = self.display_sys.get_disp_list(self.show_advanced_settings)
            self.wpprev_pnl.update_display_data(
                display_data,
                self.show_advanced_settings,
                self.use_multi_image
            )

    #
    # ListCtrl methods
    #

    def append_to_listctrl(self, data_row):
        if (self.use_multi_image and len(data_row) == 2):
            img_id = self.add_to_imagelist(data_row[1])
            index = self.path_listctrl.InsertItem(self.path_listctrl.GetItemCount(), data_row[0], img_id)
            self.path_listctrl.SetItem(index, 1, data_row[1])
        elif (not self.use_multi_image and len(data_row) == 1):
            img_id = self.add_to_imagelist(data_row[0])
            index = self.path_listctrl.InsertItem(self.path_listctrl.GetItemCount(), data_row[0], img_id)
        else:
            sp_logging.G_LOGGER.info("UseMultImg: %s. Bad data_row: %s",
                                     self.use_multi_image, data_row)

    def add_to_imagelist(self, path):
        folder_bmp =  wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_TOOLBAR, self.tsize)
        if os.path.isdir(path):
            img_id = self.image_list.Add(folder_bmp)
        else:
            thumb_bmp = self.create_thumb_bmp(path)
            img_id = self.image_list.Add(thumb_bmp)
        return img_id

    def create_thumb_bmp(self, filename):
        wximg = wx.Image(filename, type=wx.BITMAP_TYPE_ANY)
        imgsize = wximg.GetSize()
        w2h_ratio = imgsize[0]/imgsize[1]
        if w2h_ratio > 1:
            target_w = self.tsize[0]
            target_h = target_w/w2h_ratio
            pos = (0, round((target_w - target_h)/2))
        else:
            target_h = self.tsize[1]
            target_w = target_h*w2h_ratio
            pos = (round((target_h - target_w)/2), 0)
        bmp = wximg.Scale(
            target_w,
            target_h,
            quality=wx.IMAGE_QUALITY_BOX_AVERAGE
            ).Resize(
                self.tsize, pos
            ).ConvertToBitmap()
        return bmp

    def populate_lc_browse(self, pathslist, imglist):
        for path_item in pathslist:
            self.append_to_listctrl(path_item)

    #
    # Top level button definitions
    #
    def onOverrideSizes(self, event):
        self.create_sizer_diaginch_override()

    def onSaveDiagInch(self, event):
        """Save user modified display sizes to DisplaySystem."""
        inches = []
        for tc in self.tc_list_diaginch:
            tc_val = tc.GetValue()
            user_inch = self.test_diag_value(tc_val)
            if user_inch:
                inches.append(user_inch)
            else:
                # error msg
                msg = ("Display size must be a positive number, "
                       "'{}' was entered.".format(tc_val))
                sp_logging.G_LOGGER.info(msg)
                dial = wx.MessageDialog(self, msg, "Error", wx.OK|wx.STAY_ON_TOP|wx.CENTRE)
                dial.ShowModal()
                return -1
        self.display_sys.update_display_diags(inches)
        self.display_sys.save_system()
        display_data = self.display_sys.get_disp_list(self.show_advanced_settings)
        self.wpprev_pnl.update_display_data(
            display_data,
            self.show_advanced_settings,
            self.use_multi_image
        )

    def onConfigureBezels(self, event):
        """Start bezel size config mode."""
        self.toggle_radio_and_profile_choice(False)
        self.wpprev_pnl.start_bezel_config()
        self.button_bezels.Disable()
        self.button_bezels_save.Enable()
        self.button_bezels_canc.Enable()

    def onConfigureBezelsSave(self, event):
        """Save out of bezel size config mode."""
        self.toggle_radio_and_profile_choice(True)
        self.wpprev_pnl.bezel_config_save()
        self.button_bezels_save.Disable()
        self.button_bezels_canc.Disable()
        self.button_bezels.Enable()

    def onConfigureBezelsCanc(self, event):
        """Cancel out of bezel size config mode."""
        self.toggle_radio_and_profile_choice(True)
        self.wpprev_pnl.bezel_config_cancel()
        self.button_bezels_save.Disable()
        self.button_bezels_canc.Disable()
        self.button_bezels.Enable()

    def onBrowsePaths(self, event):
        """Opens the pick paths dialog."""
        dlg = BrowsePaths(self, self.use_multi_image, self.defdir)
        res = dlg.ShowModal()
        if res == wx.ID_OK:
            path_list_data = dlg.path_list_data
            image_list = dlg.il
            self.defdir = dlg.defdir
            self.populate_lc_browse(path_list_data, image_list)
        dlg.Destroy()

    def onRemoveSource(self, event):
        """Removes selection from wallpaper source ListCtrl."""
        item = self.path_listctrl.GetFocusedItem()
        if item != -1:
            self.path_listctrl.DeleteItem(item)

    def onClose(self, event):
        """Closes the profile config panel."""
        self.frame.Close(True)

    def onSelect(self, event):
        """Acts once a profile is picked in the dropdown menu."""
        event_object = event.GetEventObject()
        if event_object.GetName() == "ProfileChoice":
            item = event.GetSelection()
            if event.GetString() == "Create a new profile":
                self.onCreateNewProfile(event)
            else:
                self.populate_fields(self.list_of_profiles[item])
        else:
            pass

    def onApply(self, event):
        """Applies the currently open profile. Saves it first."""
        busy = wx.BusyCursor()
        saved_file = self.onSave(None)
        sp_logging.G_LOGGER.info("onApply profile: saved %s", saved_file)
        if saved_file:
            saved_profile = ProfileData(saved_file)
            self.parent_tray_obj.reload_profiles(event)
            wx.Yield()
            thrd = self.parent_tray_obj.start_profile(event, saved_profile, force_reload=True)
            if thrd:
                while thrd.is_alive():
                    time.sleep(0.5)
        else:
            pass
        del busy

    def onSave(self, event):
        """Saves currently open profile into file. A test method is called to verify data."""
        busy = None
        if event:
            busy = wx.BusyCursor()
        tmp_profile = TempProfileData()
        tmp_profile.name = self.tc_name.GetLineText(0)
        tmp_profile.slideshow = self.cb_slideshow.GetValue()
        if tmp_profile.slideshow:
            tmp_profile.delay = str(60*float(self.tc_sshow_delay.GetLineText(0))) # save delay as seconds for compatibility!
            tmp_profile.sortmode = self.ch_sshow_sort.GetString(self.ch_sshow_sort.GetSelection()).lower()
        if self.cb_hotkey.GetValue():
            tmp_profile.hk_binding = self.tc_hotkey_bind.GetLineText(0)

        # span mode
        span_sel = self.radiobox_spanmode.GetSelection()
        if span_sel == 0:
            tmp_profile.spanmode = "single"
        elif span_sel == 1:
            tmp_profile.spanmode = "advanced"
        elif span_sel == 2:
            tmp_profile.spanmode = "multi"

        # manual offsets
        if self.cb_offsets.GetValue():
            offs_strs = []
            for tc in self.tc_list_offsets:
                line = tc.GetLineText(0)
                if line:
                    offs_strs.append(line)
                else:
                    # if offset field is empty, assume user wants no offset
                    offs_strs.append("0,0")
            tmp_profile.manual_offsets = ";".join(offs_strs)
        # perspective
        tmp_profile.perspective = self.ch_persp.GetString(
            self.ch_persp.GetSelection()
        )

        # Paths
        # extract data from path_listctrl
        path_lc_contents = []
        columns = self.path_listctrl.GetColumnCount()
        for idx in range(self.path_listctrl.GetItemCount()):
            item_dat = []
            for col in range(columns):
                item_dat.append(self.path_listctrl.GetItemText(idx, col))
            path_lc_contents.append(item_dat)

        # format paths
        paths_array = []
        if columns == 1:
            flat_contents = [path for row in path_lc_contents for path in row]
            semicol_sep_paths = ";".join(flat_contents)
            tmp_profile.paths_array.append(semicol_sep_paths)
        else:
            path_lc_contents = sorted(path_lc_contents, key=itemgetter(0))
            paths_dict = {}
            for row in path_lc_contents:
                disp_id, path_item = row
                if disp_id in paths_dict:
                    paths_dict[disp_id].append(path_item)
                else:
                    paths_dict[disp_id] = [path_item]
            # print(paths_dict)
            for disp_id in paths_dict:
                semicol_sep_paths = ";".join(paths_dict[disp_id])
                tmp_profile.paths_array.append(semicol_sep_paths)

        # log
        sp_logging.G_LOGGER.info(tmp_profile.name)
        sp_logging.G_LOGGER.info(tmp_profile.spanmode)
        sp_logging.G_LOGGER.info(tmp_profile.slideshow)
        sp_logging.G_LOGGER.info(tmp_profile.delay)
        sp_logging.G_LOGGER.info(tmp_profile.sortmode)
        sp_logging.G_LOGGER.info(tmp_profile.manual_offsets)
        sp_logging.G_LOGGER.info(tmp_profile.hk_binding)
        sp_logging.G_LOGGER.info(tmp_profile.paths_array)

        # test collected data and save if it is valid, otherwise pass
        if tmp_profile.test_save():
            saved_file = tmp_profile.save()
            self.update_choiceprofile()
            self.parent_tray_obj.reload_profiles(event)
            self.parent_tray_obj.register_hotkeys()
            self.choice_profiles.SetSelection(self.choice_profiles.FindString(tmp_profile.name))
            # Update wallpaper preview from selected profile
            saved_profile = ProfileData(saved_file)
            if self.show_advanced_settings:
                display_data = self.display_sys.get_disp_list(True)
            else:
                display_data = self.display_sys.get_disp_list(False)
            self.wpprev_pnl.preview_wallpaper(
                saved_profile.next_wallpaper_files(),
                self.show_advanced_settings,
                self.use_multi_image,
                display_data
            )
            del busy
            return saved_file
        else:
            sp_logging.G_LOGGER.info("test_save failed.")
            del busy
            return None


    def onCreateNewProfile(self, event):
        """Empties the wallpaper profile config fields."""
        self.choice_profiles.SetSelection(
            self.choice_profiles.FindString("Create a new profile")
            )

        self.tc_name.ChangeValue("")

        self.refresh_path_listctrl(False, migrate_paths=False)

        self.radiobox_spanmode.SetSelection(0)
        self.onSpanRadio(None)

        self.cb_slideshow.SetValue(False)
        self.tc_sshow_delay.ChangeValue("")
        self.ch_sshow_sort.SetSelection(wx.NOT_FOUND)
        self.onCheckboxSlideshow(None)

        self.cb_offsets.SetValue(False)
        for tc in self.tc_list_offsets:
            tc.SetValue("0,0")
        self.onCheckboxOffsets(None)

        self.cb_hotkey.SetValue(False)
        self.tc_hotkey_bind.ChangeValue("")
        self.onCheckboxHotkey(None)

        # refresh wallpaper preview back to black previews
        self.wpprev_pnl.draw_displays()
        self.Refresh()
        self.Update()


    def onDeleteProfile(self, event):
        """Deletes the currently selected profile after getting confirmation."""
        profname = self.tc_name.GetLineText(0)
        fname = os.path.join(PROFILES_PATH, profname + ".profile")
        file_exists = os.path.isfile(fname)
        if not file_exists:
            msg = "Selected profile is not saved."
            show_message_dialog(msg, "Error")
            return
        # Open confirmation dialog
        dlg = wx.MessageDialog(None,
                               "Do you want to delete profile: {}?".format(profname),
                               'Confirm Delete',
                               wx.YES_NO | wx.ICON_QUESTION)
        result = dlg.ShowModal()
        if result == wx.ID_YES and file_exists:
            os.remove(fname)
            self.update_choiceprofile()
            self.onCreateNewProfile(None)
        else:
            pass

    def onAlignTest(self, event):
        """Align test, takes alignment settings from open profile and sets a test image wp."""
        # Use the settings currently written out in the fields!
        testimage = [os.path.join(PATH, "superpaper/resources/test.png")]
        if not os.path.isfile(testimage[0]):
            msg = "Test image not found in {}.".format(testimage)
            show_message_dialog(msg, "Error")

        inches = [dsp.diagonal_size()[1] for dsp in self.display_sys.disp_list]

        offsets = []
        for off_tc in self.tc_list_offsets:
            off = off_tc.GetLineText(0).split(",")
            try:
                offsets.append([int(off[0]), int(off[1])])
            except (IndexError, ValueError):
                show_message_dialog(
                    "Offsets must be integer pairs separated with a comma!\n"
                    "Problematic offset is {}".format(off)
                    )
                return -1
        flat_offsets = []
        for off in offsets:
            for pix in off:
                flat_offsets.append(pix)

        perspective = self.ch_persp.GetString(
            self.ch_persp.GetSelection()
        )

        busy = wx.BusyCursor()

        # Use the simplified CLI profile class
        wpproc.refresh_display_data()
        profile = CLIProfileData(testimage,
                                 None,
                                 inches,
                                 None,
                                 flat_offsets,
                                 perspective
                                )
        thrd = change_wallpaper_job(profile)
        while thrd.is_alive():
            time.sleep(0.5)
        del busy

    def onPerspectives(self, event):
        """Open perspective configuration dialog."""
        dlg = PerspectiveConfig(self)
        res = dlg.ShowModal()
        # if res == wx.ID_OK:
            # pass
        dlg.Destroy()
        # Update perspective profile choices
        open_item = self.choice_profiles.GetSelection()
        if (self.choice_profiles.GetString(open_item) == "Create a new profile"
            or not self.choice_profiles.GetString(open_item)):
            old_persp_str = "default"
        else:
            old_persp_str = self.list_of_profiles[open_item].perspective
        persp_choices = (["default"]
                         + list(self.display_sys.perspective_dict.keys())
                         + ["disabled"])
        self.ch_persp.SetItems(persp_choices)
        if old_persp_str in persp_choices:
            self.ch_persp.SetSelection(self.ch_persp.FindString(old_persp_str))
        else:
            self.ch_persp.SetSelection(0)
            self.onSave(None)

    def onHelp(self, event):
        """Open help dialog."""
        help_frame = HelpFrame(self)

    def onHelpHotkey(self, evt):
        """Popup hotkey help."""
        text = ("Bind a hotkey to start this profile. Choose max 3\n"
                "modfiers out of: control, alt, shift, super(=win).\n"
                "Example: control+super+x")
        pop = HelpPopup(self, text)
        btn = evt.GetEventObject()
        pos = btn.ClientToScreen((0, 0))
        sz = btn.GetSize()
        pop.Position(pos, (0, sz[1]))
        pop.Popup()

    def onHelpBezels(self, evt):
        """Popup bezel config help."""
        text = ("Configure your bezels in the wallpaper preview\n"
                "panel with the buttons placed at the right and\n"
                "bottom edges of displays. Bezels between displays\n"
                "are meaningful. Adjacent bezel pair thicknesses are\n"
                "grouped together with the gap in between to a single\n"
                "number.")
        pop = HelpPopup(self, text)
        btn = evt.GetEventObject()
        pos = btn.ClientToScreen((0, 0))
        sz = btn.GetSize()
        pop.Position(pos, (0, sz[1]))
        pop.Popup()






class WallpaperPreviewPanel(wx.Panel):
    """
    Wallpaper & monitor preview panel.

    Previews wallpaper settings as applied to the input image.
    In the advanced mode allows the user to enter their monitor
    setup, which will then be saved into a file as a monitor
    configuration. Method looks up saved setups to see if one
    exists that matches the given resolutions, offsets and sizes.
    """
    def __init__(self, parent, display_sys, image_list = None, use_ppi_px = False, use_multi_image = False):
        self.preview_size = (1080,400)
        wx.Panel.__init__(self, parent, size=self.preview_size)
        self.frame = parent

        # Buttons
        self.config_mode = False
        self.bezel_conifg_mode = False
        self.create_buttons(use_ppi_px)

        # Colour definitions
        self.clr_prw_mntr = wx.Colour(0, 0, 0, alpha=wx.ALPHA_OPAQUE)
        self.clr_prw_bkg = wx.Colour(30, 30, 30, alpha=wx.ALPHA_OPAQUE)
        self.SetBackgroundColour(self.clr_prw_bkg)
        
        # Display data and sizes
        self.display_sys = display_sys
        self.display_data = self.display_sys.get_disp_list()
        self.dtop_canvas_px = self.get_canvas(self.display_data)
        self.dtop_canvas_relsz, self.dtop_canvas_pos, scaling_fac = self.fit_canvas_wrkarea(self.dtop_canvas_px)
        self.display_rel_sizes = self.displays_on_canvas(self.display_data, self.dtop_canvas_pos, scaling_fac)

        # bitmaps to be shown
        self.use_multi_image = use_multi_image
        self.current_preview_images = []
        self.preview_img_list = []
        self.bmp_list = []

        # Draw preview
        self.draw_displays()

        # Create bezel buttons for displays in preview
        self.bez_buttons = []
        self.create_bezel_buttons()

        self.draggable_shapes = []
        self.Bind(wx.EVT_PAINT, self.OnPaint)



    #
    # UI drawing methods
    #
    def draw_displays(self, use_ppi_px = False, use_multi_image = False):
        work_sz = self.GetSize()

        # draw canvas
        bmp_canv = wx.Bitmap.FromRGBA(self.dtop_canvas_relsz[0], self.dtop_canvas_relsz[1], red=0, green=0, blue=0, alpha=255)
        if not self.preview_img_list:
            # preview StaticBitmaps don't exist yet
            self.bmp_list.append(bmp_canv)
            self.st_bmp_canvas = wx.StaticBitmap(self, wx.ID_ANY, bmp_canv)
            self.st_bmp_canvas.SetPosition(self.dtop_canvas_pos)
            self.st_bmp_canvas.Hide()

            # draw monitor previews
            for disp in self.display_rel_sizes:
                size = disp[0]
                offs = disp[1]
                bmp = wx.Bitmap.FromRGBA(size[0], size[1], red=0, green=0, blue=0, alpha=255)
                self.bmp_list.append(bmp)
                st_bmp = wx.StaticBitmap(self, wx.ID_ANY, bmp)
                st_bmp.Hide()
                # st_bmp.SetScaleMode(wx.Scale_AspectFill)  # New in wxpython 4.1
                st_bmp.SetPosition(offs)
                self.preview_img_list.append(st_bmp)
        else:
            # previews exist and should be blanked
            self.current_preview_images = [] # drop chached image list

            self.st_bmp_canvas.SetBitmap(bmp_canv)
            self.st_bmp_canvas.SetPosition(self.dtop_canvas_pos)
            # self.st_bmp_canvas.Hide()

            # blank monitor previews
            for disp, st_bmp in zip(self.display_rel_sizes, self.preview_img_list):
                size = disp[0]
                offs = disp[1]
                bmp = wx.Bitmap.FromRGBA(size[0], size[1], red=0, green=0, blue=0, alpha=255)
                st_bmp.SetBitmap(bmp)
                st_bmp.SetPosition(offs)
                # st_bmp.Hide()
        self.draw_monitor_numbers(use_ppi_px)
        self.Refresh()

    def resize_displays(self, use_ppi_px):
        if use_ppi_px:
            for (disp,
                 img_sz,
                 bez_szs,
                 st_bmp) in zip(self.display_rel_sizes,
                                self.img_rel_sizes,
                                self.bz_rel_sizes,
                                self.preview_img_list):
                size, offs = disp
                bmp = wx.Bitmap.FromRGBA(img_sz[0], img_sz[1], red=0, green=0, blue=0, alpha=255)
                bmp_w_bez = self.bezels_to_bitmap(bmp, size, bez_szs)
                st_bmp.SetSize(size)
                st_bmp.SetPosition(offs)
                st_bmp.SetBitmap(bmp_w_bez)
        else:
            for disp, st_bmp in zip(self.display_rel_sizes, self.preview_img_list):
                size = disp[0]
                offs = disp[1]
                bmp = wx.Bitmap.FromRGBA(size[0], size[1], red=0, green=0, blue=0, alpha=255)
                st_bmp.SetBitmap(bmp)
                st_bmp.SetPosition(offs)
                st_bmp.SetSize(size)
        self.draw_monitor_numbers(use_ppi_px)

    def draw_monitor_numbers(self, use_ppi_px):
        font = wx.Font(24, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        font_clr = wx.Colour(60, 60, 60, alpha=wx.ALPHA_OPAQUE)

        for st_bmp in self.preview_img_list:
            bmp = st_bmp.GetBitmap()
            dc = wx.MemoryDC(bmp)
            text = str(self.preview_img_list.index(st_bmp))
            dc.SetTextForeground(font_clr)
            dc.SetFont(font)
            dc.DrawText(text, 5, 5)
            del dc
            st_bmp.SetBitmap(bmp)

        if use_ppi_px:
            self.draw_monitor_sizes()

    def draw_monitor_sizes(self):
        font = wx.Font(24, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_LIGHT)
        font_clr = wx.Colour(60, 60, 60, alpha=wx.ALPHA_OPAQUE)

        for st_bmp, img_sz, dsp in zip(self.preview_img_list,
                                       self.img_rel_sizes,
                                       self.display_sys.disp_list):
            bmp = st_bmp.GetBitmap()
            dc = wx.MemoryDC(bmp)
            text = str(dsp.diagonal_size()[1]) + '"'
            dc.SetTextForeground(font_clr)
            dc.SetFont(font)
            # bmp_w, bmp_h = dc.GetSize()
            bmp_w, bmp_h = img_sz
            text_w, text_h = dc.GetTextExtent(text)
            pos_w = bmp_w - text_w - 5
            pos_h = 5
            dc.DrawText(text, pos_w, pos_h)
            del dc
            st_bmp.SetBitmap(bmp)


    def refresh_preview(self, use_ppi_px = False):
        if not self.config_mode or not self.bezel_conifg_mode:
            self.dtop_canvas_px = self.get_canvas(self.display_data, use_ppi_px)
            self.dtop_canvas_relsz, self.dtop_canvas_pos, scaling_fac = self.fit_canvas_wrkarea(self.dtop_canvas_px)
            self.st_bmp_canvas.SetPosition(self.dtop_canvas_pos)
            self.st_bmp_canvas.SetSize(self.dtop_canvas_relsz)

            if use_ppi_px:
                (self.display_rel_sizes,
                    self.img_rel_sizes,
                    self.bz_rel_sizes) = self.displays_on_canvas(
                        self.display_data, self.dtop_canvas_pos,
                        scaling_fac, use_ppi_px
                    )
            else:
                self.display_rel_sizes = self.displays_on_canvas(
                    self.display_data, self.dtop_canvas_pos, scaling_fac
                )
            for disp, st_bmp in zip(self.display_rel_sizes, self.preview_img_list):
                size = disp[0]
                offs = disp[1]
                st_bmp.SetPosition(offs)
                st_bmp.SetSize(size)
        # if self.bezel_conifg_mode:
            # pass
        # self.st_bmp_canvas.Hide()

        self.move_buttons()
        self.move_bezel_buttons()

    def full_refresh_preview(self, is_resized, use_ppi_px, use_multi_image):
        self.use_multi_image = use_multi_image
        if is_resized and not self.config_mode:
            dtop_canvas_relsz, dtop_canvas_pos, scaling_fac = self.fit_canvas_wrkarea(self.dtop_canvas_px)
            # if (self.current_preview_images and dtop_canvas_relsz is not self.dtop_canvas_relsz):
            if (self.current_preview_images):
                self.preview_wallpaper(self.current_preview_images, use_ppi_px, use_multi_image)
                self.move_bezel_buttons()
                # self.st_bmp_canvas.Hide()
            else:
                self.refresh_preview(use_ppi_px)
                self.resize_displays(use_ppi_px)
                self.move_bezel_buttons()
                self.Refresh()
                # self.st_bmp_canvas.Hide()


    def preview_wallpaper(self, image_list, use_ppi_px=False, use_multi_image=False, display_data=None):
        self.use_multi_image = use_multi_image
        if display_data:
            self.display_data = display_data
        self.refresh_preview(use_ppi_px)
        self.current_preview_images = image_list

        if use_multi_image:
            # hide canvas and set images to monitor previews
            # self.st_bmp_canvas.Hide()
            # self.Refresh()
            for img_nm, st_bmp in zip(image_list, self.preview_img_list):
                prev_sz = st_bmp.GetSize()
                st_bmp.SetBitmap(self.resize_and_bitmap(img_nm, prev_sz))
                # st_bmp.Show()
        elif use_ppi_px:
            img = image_list[0]
            # set canvas to fit with keeping aspect the image, with dim/blur
            # and crop pieces to show on monitor previews unaltered.
            # With use_ppi_px any provided bezels will be drawn.
            canv_sz = self.st_bmp_canvas.GetSize()
            bmp_clr, bmp_bw = self.resize_and_bitmap(img, canv_sz, True)
            self.st_bmp_canvas.SetBitmap(bmp_bw)
            # self.st_bmp_canvas.Show()

            canvas_pos = self.dtop_canvas_pos
            for (disp,
                 img_sz,
                 bez_szs,
                 st_bmp) in zip(self.display_rel_sizes,
                                self.img_rel_sizes,
                                self.bz_rel_sizes,
                                self.preview_img_list):
                sz = disp[0]
                pos = (disp[1][0] - canvas_pos[0], disp[1][1] - canvas_pos[1])
                crop = bmp_clr.GetSubBitmap(wx.Rect(pos, img_sz))
                crop_w_bez = self.bezels_to_bitmap(crop, sz, bez_szs)
                st_bmp.SetBitmap(crop_w_bez)
                # st_bmp.Show()
        else:
            img = image_list[0]
            # set canvas to fit with keeping aspect the image, with dim/blur
            # and crop pieces to show on monitor previews unaltered.
            canv_sz = self.st_bmp_canvas.GetSize()
            bmp_clr, bmp_bw = self.resize_and_bitmap(img, canv_sz, True)
            self.st_bmp_canvas.SetBitmap(bmp_bw)
            # self.st_bmp_canvas.Show()

            canvas_pos = self.dtop_canvas_pos
            for disp, st_bmp in zip(self.display_rel_sizes, self.preview_img_list):
                sz = disp[0]
                pos = (disp[1][0] - canvas_pos[0], disp[1][1] - canvas_pos[1])
                crop = bmp_clr.GetSubBitmap(wx.Rect(pos, sz))
                st_bmp.SetBitmap(crop)
                # st_bmp.Show()
        self.draw_monitor_numbers(use_ppi_px)
        self.Refresh()

    def resize_and_bitmap(self, fname, size, enhance_color=False):
        """Take filename of an image and resize and center crop it to size."""
        try:
            pil = resize_to_fill(Image.open(fname), size, quality="fast")
        except UnidentifiedImageError:
            msg = ("Opening image '%s' failed with PIL.UnidentifiedImageError."
                   "It could be corrupted or is of foreign type.") % fname
            sp_logging.G_LOGGER.info(msg)
            # show_message_dialog(msg)
            black_bmp = wx.Bitmap.FromRGBA(size[0], size[1], red=0, green=0, blue=0, alpha=255)
            if enhance_color:
                return (black_bmp, black_bmp)
            return black_bmp
        img = wx.Image(pil.size[0], pil.size[1])
        img.SetData(pil.convert("RGB").tobytes())
        if enhance_color:
            converter = ImageEnhance.Color(pil)
            pilenh_bw = converter.enhance(0.25)
            brightns = ImageEnhance.Brightness(pilenh_bw)
            pilenh = brightns.enhance(0.45)
            imgenh = wx.Image(pil.size[0], pil.size[1])
            imgenh.SetData(pilenh.convert("RGB").tobytes())
            return (img.ConvertToBitmap(), imgenh.ConvertToBitmap())
        return img.ConvertToBitmap()

    def bezels_to_bitmap(self, bmp, disp_sz, bez_rects):
        """Add bezel rectangles ( right_bez , bottom_bez ) to given bitmap."""
        sp_logging.G_LOGGER.info("bezels_to_bitmap: bez_rects: %s", bez_rects)
        right_bez, bottom_bez = bez_rects
        if (right_bez == (0, 0) and bottom_bez == (0, 0)):
            return bmp
        # bmp into wx.Image and new output
        img = bmp.ConvertToImage()
        img_sz = img.GetSize()
        img_out = wx.Image(disp_sz[0], disp_sz[1])
        img_out.Paste(img, 0, 0)

        # Add bezels sequentially to the Image
        # bottom bez
        if bottom_bez != (0, 0):
            b_bez_bmp = wx.Bitmap.FromRGBA(bottom_bez[0], bottom_bez[1],
                                           red=5, green=5, blue=5, alpha=100)
            b_bez_img = b_bez_bmp.ConvertToImage()
            img_out.Paste(b_bez_img, 0, img_sz[1])

        # right bez: is longer if bottom bez is present
        if right_bez != (0, 0):
            r_bez_bmp = wx.Bitmap.FromRGBA(right_bez[0], right_bez[1],
                                           red=5, green=5, blue=5, alpha=100)
            r_bez_img = r_bez_bmp.ConvertToImage()
            img_out.Paste(r_bez_img, img_sz[0], 0)

        # Convert Image back to wx.Bitmap
        return img_out.ConvertToBitmap()


    def update_display_data(self, display_data, use_ppi_px, use_multi_image):
        self.display_data = display_data
        self.refresh_preview()
        self.full_refresh_preview(True, use_ppi_px, use_multi_image)


    #
    # Data analysis methods
    #
    def get_canvas(self, disp_data, use_ppi_px = False):
        """Returns a size tuple for the desktop are in pixels or millimeters."""
        if use_ppi_px:
            rightmost_edge = max(
                [disp.resolution[0] + disp.digital_offset[0] + disp.ppi_norm_bezels[0] for disp in disp_data]
            )
            bottommost_edge = max(
                [disp.resolution[1] + disp.digital_offset[1] + disp.ppi_norm_bezels[1] for disp in disp_data]
            )
        else:
            rightmost_edge = max(
                [disp.resolution[0] + disp.digital_offset[0] for disp in disp_data]
            )
            bottommost_edge = max(
                [disp.resolution[1] + disp.digital_offset[1] for disp in disp_data]
            )
        return (rightmost_edge, bottommost_edge)

    def fit_canvas_wrkarea(self, canvas_px):
        """Compute canvas size relative to the background panel size.

        Returns a size tuple in so that along the longer
        edge of the canvas, at most 90% of the panel dimensions are used.
        
        Input is either canvas size in true pixels or in PPI normalized
        pixels."""
        rel_factor = 0.9
        rel_achor_gap = (1-rel_factor)/2
        work_sz = self.GetSize()
        w2h_ratio_worksz = work_sz[0]/work_sz[1]
        w2h_ratio = canvas_px[0]/canvas_px[1]
        if w2h_ratio > w2h_ratio_worksz:
            # canvas is wider than working area
            # limit width to 90% of working area
            new_width = rel_factor * work_sz[0]
            scaling_fac = new_width/canvas_px[0]
            new_height = scaling_fac * canvas_px[1]
            anchor_left = rel_achor_gap * work_sz[0]
            anchor_top = (work_sz[1] - new_height) / 2
        else:
            # canvas is taller than working area
            new_height = rel_factor * work_sz[1]
            scaling_fac = new_height/canvas_px[1]
            new_width = scaling_fac * canvas_px[0]
            anchor_left = (work_sz[0] - new_width)/2
            anchor_top = rel_achor_gap * work_sz[1]
        canvas_rel = (new_width, new_height)
        canvas_rel_pos = (anchor_left, anchor_top)
        return (canvas_rel, canvas_rel_pos, scaling_fac)

    def displays_on_canvas(self, disp_data, canvas_pos, scaling_fac, use_ppi_px=False):
        """Return sizes and positions of displays in disp_data on the working area.

        if use_ppi_px == True, returned display sizes contain bezels and lists of
        image sizes and bezel rectangles are returned separately.
        bz_szs is a list of size tuples pairs, each in pair for each possible bezel.
        """
        if use_ppi_px:
            display_szs_pos = []
            image_szs = []
            bz_szs = []
            for disp in disp_data:
                res = disp.resolution
                doff = disp.digital_offset
                off = canvas_pos
                bez = disp.ppi_norm_bezels
                display_szs_pos.append(
                    (
                        # tuple 1: size = res + bez
                        (
                            scaling_fac * (res[0] + bez[0]),
                            scaling_fac * (res[1] + bez[1])
                        ),
                        # tuple 2: pos
                        (
                            (scaling_fac * doff[0]) + off[0],
                            (scaling_fac * doff[1]) + off[1]
                        )
                    )
                )
                image_szs.append(
                    (
                        scaling_fac * res[0],
                        scaling_fac * res[1]
                    )
                )
                if bez[0] != 0:
                    right_bez = (scaling_fac * bez[0],
                                 scaling_fac * (res[1] + bez[1]))
                else:
                    right_bez = (0, 0)
                if bez[1] != 0:
                    bottom_bez = (scaling_fac * res[0], scaling_fac * bez[1])
                else:
                    bottom_bez = (0, 0)
                bz_szs.append(
                    (right_bez, bottom_bez)
                )
            return [display_szs_pos, image_szs, bz_szs]
        else:
            display_szs_pos = []
            for disp in disp_data:
                doff = disp.digital_offset
                off = canvas_pos
                display_szs_pos.append(
                    (
                        tuple([px*scaling_fac for px in disp.resolution]),
                        tuple([doff[0]*scaling_fac + off[0], doff[1]*scaling_fac + off[1]])
                    )
                )
            return display_szs_pos



    #
    # Buttons
    #
    def create_buttons(self, use_ppi_px):
        """Create buttons for display preview positioning config."""
        # Buttons - show only if use_ppi_px == True
        self.button_config = wx.Button(self, label="Positions")
        self.button_save = wx.Button(self, label="Save")
        self.button_reset = wx.Button(self, label="Reset")
        self.button_cancel = wx.Button(self, label="Cancel")
        help_bmp = wx.ArtProvider.GetBitmap(wx.ART_QUESTION, wx.ART_BUTTON, (20, 20))
        self.button_help = wx.BitmapButton(self, bitmap=help_bmp, name="butt_help")


        self.button_config.Bind(wx.EVT_BUTTON, self.onConfigure)
        self.button_save.Bind(wx.EVT_BUTTON, self.onSave)
        self.button_reset.Bind(wx.EVT_BUTTON, self.onReset)
        self.button_cancel.Bind(wx.EVT_BUTTON, self.onCancel)
        self.button_help.Bind(wx.EVT_BUTTON, self.onHelp)

        self.move_buttons()

        self.button_config.Show(use_ppi_px)
        self.button_save.Show(False)
        self.button_reset.Show(False)
        self.button_cancel.Show(False)

    def move_buttons(self):
        """Position display config buttons to bottom right corner."""
        sz_area = self.GetSize()
        sz_butt = self.button_config.GetDefaultSize()
        sz_help = self.button_help.GetSize()
        self.butt_gap = 10
        self.button_config.SetPosition(
            (
                sz_area[0] - sz_butt[0] - self.butt_gap,
                sz_area[1] - sz_butt[1] - self.butt_gap
            )
        )
        self.button_save.SetPosition(
            (
                sz_area[0] - 2*(sz_butt[0] + self.butt_gap),
                sz_area[1] - sz_butt[1] - self.butt_gap
            )
        )
        self.button_reset.SetPosition(
            (
                sz_area[0] - sz_butt[0] - self.butt_gap,
                sz_area[1] - 2*(sz_butt[1] + self.butt_gap)
            )
        )
        self.button_cancel.SetPosition(
            (
                sz_area[0] - sz_butt[0] - self.butt_gap,
                sz_area[1] - sz_butt[1] - self.butt_gap
            )
        )
        self.button_help.SetPosition(
            (
                sz_area[0] - sz_help[0] - self.butt_gap,
                self.butt_gap
            )
        )

    def toggle_buttons(self, show_config, in_config):
        """Toggle visibility of display positioning config buttons."""
        self.button_config.Show(show_config)
        self.button_save.Show(in_config)
        self.button_reset.Show(in_config)
        self.button_cancel.Show(in_config)

    def onConfigure(self, evt):
        """Start diplay position config mode."""
        self.frame.toggle_radio_and_profile_choice(False)
        self.frame.toggle_bezel_buttons(False, False)
        self.config_mode = True
        self.toggle_buttons(False, True)
        self.show_staticbmps(False)
        self.create_shapes()
        self.Refresh()

    def onSave(self, evt):
        """Save current Display offsets into DisplaySystem."""
        self.config_mode = False
        self.bind_movement_binds(False)
        self.toggle_buttons(True, False)
        # Export and save offsets to DisplaySystem
        self.export_offsets(self.display_sys)
        self.display_sys.save_system()
        display_data = self.display_sys.get_disp_list(use_ppi_norm = True)
        # Full redraw of preview with new offset data
        if self.current_preview_images:
            self.preview_wallpaper(self.current_preview_images, True, False, display_data=display_data)
        else:
            self.display_data = display_data
            self.refresh_preview(True)
            self.resize_displays(True)
            # self.show_staticbmps(True)
        self.draggable_shapes = []  # Destroys DragShapes
        self.frame.toggle_radio_and_profile_choice(True)
        self.frame.toggle_bezel_buttons(False, True)
        self.Refresh()

    def onReset(self, evt):
        """Reset Display preview positions to the initial guess."""
        # Back up current offsets
        ppinorm_offs = self.display_sys.get_ppinorm_offsets()
        # Compute and get reset offsets
        self.display_sys.compute_initial_preview_offsets()
        display_data = self.display_sys.get_disp_list(use_ppi_norm = True)
        dtop_canvas_px = self.get_canvas(display_data, True)
        dtop_canvas_relsz, dtop_canvas_pos, scaling_fac = self.fit_canvas_wrkarea(dtop_canvas_px)
        (display_rel_sizes,
            img_rel_sizes,
            bz_rel_sizes) = self.displays_on_canvas(
                display_data, dtop_canvas_pos,
                scaling_fac, True
            )
        # Move display preview draggable_shapes
        for shp, off in zip(self.draggable_shapes, display_rel_sizes):
            shp.pos = off[1]
        # Restore backed up offsets
        self.display_sys.update_ppinorm_offsets(ppinorm_offs)
        self.Refresh()

    def onCancel(self, evt):
        """Cancel out of diplay position config mode."""
        self.config_mode = False
        self.bind_movement_binds(False)
        self.toggle_buttons(True, False)
        self.draggable_shapes = []  # Destroys DragShapes
        self.refresh_preview()
        self.full_refresh_preview(True, True, False)
        # self.show_staticbmps(True)
        self.frame.toggle_radio_and_profile_choice(True)
        self.frame.toggle_bezel_buttons(False, True)
        self.Refresh()

    def onHelp(self, evt):
        """Popup a help dialog."""
        text = ("Preview of your wallpaper settings.\n"
                "In 'advanced span' mode you need use the 'Positions'\n"
                "tool to move the display previews by dragging to\n"
                "as accurately as possible represent the actual\n"
                "positions of you displays on your desk."
        )
        use_per = self.display_sys.use_perspective
        persname = self.frame.ch_persp.GetString(self.frame.ch_persp.GetSelection())
        pop = HelpPopup(self, text,
                        show_image_quality=True,
                        use_perspective=use_per,
                        persp_name=persname)
        btn = evt.GetEventObject()
        pos = btn.ClientToScreen( (0,0) )
        sz =  btn.GetSize()
        pop.Position(pos, (0, sz[1]))
        pop.Popup()



    def show_staticbmps(self, show):
        """Show/Hide StaticBitmaps."""
        if self.current_preview_images:
            self.st_bmp_canvas.Show(show)
        else:
            self.st_bmp_canvas.Show(False)
        for st_bmp in self.preview_img_list:
            st_bmp.Show(show)

    def export_offsets(self, display_sys):
        """Read dragged preview positions, normalize them to be positive,
        and scale sizes up to old canvas size."""
        # DragShape sizes and positions need to be scaled up by true_canvas_old_w/preview_canv_w
        prev_canv_w = self.st_bmp_canvas.GetSize()[0]
        true_canv_w = self.get_canvas(display_sys.get_disp_list(True))[0]
        scaling = true_canv_w / prev_canv_w
        sanitzed_offs = self.sanitize_shape_offs()
        ppi_norm_offsets = []
        for off in sanitzed_offs:
            ppi_norm_offsets.append(
                (
                    off[0]*scaling,
                    off[1]*scaling
                )
            )
        display_sys.update_ppinorm_offsets(ppi_norm_offsets, bezels_included = False)

    def sanitize_shape_offs(self):
        """Return shapes' relative offsets, anchoring to (0,0)."""
        sanitized_offs = []
        leftmost_offset = min([shape.pos[0] for shape in self.draggable_shapes])
        topmost_offset = min([shape.pos[1] for shape in self.draggable_shapes])
        for shape in self.draggable_shapes:
            sanitized_offs.append(
                (
                    shape.pos[0] - leftmost_offset,
                    shape.pos[1] - topmost_offset
                )
            )
        return sanitized_offs

    #
    # DragImage methods
    #

    class DragShape:
        def __init__(self, bmp):
            self.bmp = bmp
            self.pos = (0,0)
            self.shown = True
            self.text = None
            self.fullscreen = False

        def HitTest(self, pt):
            rect = self.GetRect()
            return rect.Contains(pt)

        def GetRect(self):
            return wx.Rect(self.pos[0], self.pos[1],
                        self.bmp.GetWidth(), self.bmp.GetHeight())

        def Draw(self, dc, op = wx.COPY):
            if self.bmp.IsOk():
                memDC = wx.MemoryDC()
                memDC.SelectObject(self.bmp)

                dc.Blit(self.pos[0], self.pos[1],
                        self.bmp.GetWidth(), self.bmp.GetHeight(),
                        memDC, 0, 0, op, True)

                return True
            else:
                return False



    def create_shapes(self, enable_movement=True):
        """Create draggable objects from display previews."""
        self.draggable_shapes = []
        self.drag_image = None
        self.drag_shape = None

        for st_bmp in self.preview_img_list:
            shape = self.DragShape(st_bmp.GetBitmap())
            shape.pos = st_bmp.GetPosition()
            self.draggable_shapes.append(shape)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        if enable_movement:
            self.bind_movement_binds(True)

    def bind_movement_binds(self, toggle):
        """Bind or unbind DragImage dragging bindings."""
        if toggle:
            self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
            self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
            self.Bind(wx.EVT_MOTION, self.OnMotion)
            self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
        else:
            self.Unbind(wx.EVT_LEFT_DOWN)
            self.Unbind(wx.EVT_LEFT_UP)
            self.Unbind(wx.EVT_MOTION)
            self.Unbind(wx.EVT_LEAVE_WINDOW)


    def draw_shapes(self, dc):
        for shape in self.draggable_shapes:
            if shape.shown:
                shape.Draw(dc)

    def draw_canvas(self, dc, draw=True):
        if self.st_bmp_canvas:
            pos = self.st_bmp_canvas.GetPosition()
            bmp = self.st_bmp_canvas.GetBitmap()
            bmp_sz = bmp.GetSize()
            if not draw:
                bmp = wx.Bitmap.FromRGBA(bmp_sz[0], bmp_sz[1], red=30, green=30, blue=30, alpha=255)
            op = wx.COPY
            if bmp.IsOk():
                memDC = wx.MemoryDC()
                # memDC.SelectObject(wx.NullBitmap)
                memDC.SelectObject(bmp)

                dc.Blit(pos[0], pos[1],
                        bmp_sz[0], bmp_sz[1],
                        memDC, 0, 0, op, True)

                return True
            else:
                return False

    def draw_st_bmps(self, dc):
        for st_bmp in self.preview_img_list:
            pos = st_bmp.GetPosition()
            bmp = st_bmp.GetBitmap()
            self.draw_bmp(dc, pos, bmp)

    def draw_bmp(self, dc, pos, bmp):
        bmp_sz = bmp.GetSize()
        op = wx.COPY
        if bmp.IsOk():
            memDC = wx.MemoryDC()
            memDC.SelectObject(bmp)

            dc.Blit(pos[0], pos[1],
                    bmp_sz[0], bmp_sz[1],
                    memDC, 0, 0, op, True)
            return True
        else:
            return False

    def find_shape(self, pt):
        for shape in self.draggable_shapes:
            if shape.HitTest(pt):
                return shape
        return None

    def OnPaint(self, evt):
        dc = wx.PaintDC(self)
        # Hiding bitmap widgets, probably unnecessary
        # for st_bmp in self.preview_img_list:
            # st_bmp.Hide()
        
        # Canvas drawing
        if (not self.config_mode
            and not self.use_multi_image
            and self.current_preview_images):
            # print("Drawing canvas: ", self.config_mode, not self.use_multi_image, self.current_preview_images)
            self.draw_canvas(dc)
        else:
            # print("Skipping canvas: ", self.config_mode, not self.use_multi_image, self.current_preview_images)
            self.draw_canvas(dc, False)

        # Display drawing
        if self.config_mode:
            self.draw_shapes(dc)
        else:
            self.draw_st_bmps(dc)

    def OnLeftDown(self, evt):
        # Did the mouse go down on one of our shapes?
        shape = self.find_shape(evt.GetPosition())

        # If a shape was 'hit', then set that as the shape we're going to
        # drag around. Get our start position. Dragging has not yet started.
        # That will happen once the mouse moves, OR the mouse is released.
        if shape:
            self.drag_shape = shape
            self.dragStartPos = evt.GetPosition()

    def OnLeftUp(self, evt):
        if not self.drag_image or not self.drag_shape:
            self.drag_image = None
            self.drag_shape = None
            return

        # Hide the image, end dragging, and nuke out the drag image.
        self.drag_image.Hide()
        self.drag_image.EndDrag()
        self.drag_image = None

        self.drag_shape.pos = (
            self.drag_shape.pos[0] + evt.GetPosition()[0] - self.dragStartPos[0],
            self.drag_shape.pos[1] + evt.GetPosition()[1] - self.dragStartPos[1]
            )

        self.drag_shape.shown = True
        self.RefreshRect(self.drag_shape.GetRect())
        self.drag_shape = None


    def OnMotion(self, evt):
        # Ignore mouse movement if we're not dragging.
        if not self.drag_shape or not evt.Dragging() or not evt.LeftIsDown():
            return

        # if we have a shape, but haven't started dragging yet
        if self.drag_shape and not self.drag_image:

            # only start the drag after having moved a couple pixels
            tolerance = 2
            pt = evt.GetPosition()
            dx = abs(pt.x - self.dragStartPos.x)
            dy = abs(pt.y - self.dragStartPos.y)
            if dx <= tolerance and dy <= tolerance:
                return

            # refresh the area of the window where the shape was so it
            # will get erased.
            self.drag_shape.shown = False
            self.RefreshRect(self.drag_shape.GetRect(), True)
            self.Update()

            item = self.drag_shape.text if self.drag_shape.text else self.drag_shape.bmp
            self.drag_image = wx.DragImage(item,
                                         wx.Cursor(wx.CURSOR_HAND))

            hotspot = self.dragStartPos - self.drag_shape.pos
            self.drag_image.BeginDrag(hotspot, self, self.drag_shape.fullscreen)

            self.drag_image.Move(pt)
            self.drag_image.Show()

        # if we have shape and image then move it, posibly highlighting another shape.
        elif self.drag_shape and self.drag_image:
            # now move it and show it again if needed
            self.drag_image.Move(evt.GetPosition())


    def OnLeaveWindow(self, evt):
        """On leavewindow event drop dragged image by simulating a left up event."""
        self.OnLeftUp(evt)


    #
    # Bezel Configuration mode
    #
    def start_bezel_config(self):
        """Enters bezel config mode.

        Reveals buttons on each display right and bottom edges
        to allow adding bezels. Additionally hides display
        position config button(s)."""
        # TODO Change background color?
        self.old_bezels = self.display_sys.bezels_in_mm()
        self.old_ppinorm_offs = self.display_sys.get_ppinorm_offsets()
        self.bezel_conifg_mode = True
        # Draw bitmaps manually, widgets can't overlap
        # self.show_staticbmps(False)
        # self.create_shapes(enable_movement=False)
        self.show_bezel_buttons(True)
        # Hide preview positioning config button
        self.toggle_buttons(False, False)

    def bezel_config_save(self):
        """Saves bezel values for the active DisplaySystem."""
        self.bezel_conifg_mode = False
        self.show_bezel_buttons(False)
        # Show preview positioning config button
        self.toggle_buttons(True, False)
        # self.draggable_shapes = []  # Destroys DragShapes / manually drawn previews
        self.full_refresh_preview(True, True, False)
        # self.show_staticbmps(True)
        self.Refresh()
        # trigger a DisplaySystem save.
        self.display_sys.save_system()

    def bezel_config_cancel(self):
        """Exits out of the bezel config mode without saving."""
        self.bezel_conifg_mode = False
        self.show_bezel_buttons(False)
        # Show preview positioning config button
        self.toggle_buttons(True, False)
        self.display_sys.update_bezels(self.old_bezels)
        self.display_sys.update_ppinorm_offsets(self.old_ppinorm_offs)
        for pops, bez_mms in zip(self.bezel_popups, self.old_bezels):
            pops[0].set_bezel_value(bez_mms[0])
            pops[1].set_bezel_value(bez_mms[1])
        self.display_data = self.display_sys.get_disp_list(True)
        # self.draggable_shapes = []  # Destroys DragShapes / manually drawn previews
        self.full_refresh_preview(True, True, False)
        # self.show_staticbmps(True)
        self.Refresh()

    def create_bezel_buttons(self):
        # load icons into bitmaps
        rb_png = os.path.join(RESOURCES_PATH, "icons8-merge-vertical-96.png")
        bb_png = os.path.join(RESOURCES_PATH, "icons8-merge-horizontal-96.png")
        rb_img = wx.Image(rb_png, type=wx.BITMAP_TYPE_ANY)
        bb_img = wx.Image(bb_png, type=wx.BITMAP_TYPE_ANY)
        rb_bmp = rb_img.Scale(20, 20).Resize((20, 20), (0, 0)).ConvertToBitmap()
        bb_bmp = bb_img.Scale(20, 20).Resize((20, 20), (0, 0)).ConvertToBitmap()

        # create bitmap buttons
        for st_bmp in self.preview_img_list:
            butts = []
            butt_rb = wx.BitmapButton(self, bitmap=rb_bmp, name="butt_bez_r", style=wx.BORDER_NONE)
            butt_bb = wx.BitmapButton(self, bitmap=bb_bmp, name="butt_bez_b", style=wx.BORDER_NONE)
            bez_butt_color = wx.Colour(41, 47, 52)
            butt_rb.SetBackgroundColour(bez_butt_color)
            butt_bb.SetBackgroundColour(bez_butt_color)
            self.bez_butt_sz = butt_rb.GetSize()
            pos_rb, pos_bb = self.bezel_button_positions(st_bmp)
            butt_rb.SetPosition((pos_rb[0], pos_rb[1]))
            butt_bb.SetPosition((pos_bb[0], pos_bb[1]))
            butt_rb.Bind(wx.EVT_BUTTON, self.onBezelButton)
            butt_bb.Bind(wx.EVT_BUTTON, self.onBezelButton)
            self.bez_buttons.append(
                (
                    butt_rb,
                    butt_bb
                )
            )
        self.show_bezel_buttons(False)
        self.create_bezel_popups()

    def create_bezel_popups(self):
        self.bezel_popups = []
        bezel_mm = self.display_sys.bezels_in_mm()
        for butts, bez_mm in zip(self.bez_buttons, bezel_mm):
            pop_rb = self.popup_at_button(butts[0])
            pop_rb.set_bezel_value(bez_mm[0])
            pop_bb = self.popup_at_button(butts[1])
            pop_bb.set_bezel_value(bez_mm[1])
            self.bezel_popups.append(
                (
                    pop_rb,
                    pop_bb
                )
            )

    def popup_at_button(self, button):
        """Initialize a popup at button position."""
        pop = self.BezelEntryPopup(self, wx.SIMPLE_BORDER)
        return pop

    def move_popup_to_button(self, pop, button):
        """Move pop next to its associated button."""
        butt_name = button.GetName()
        pos = button.ClientToScreen( (0, 0) )
        butt_sz = button.GetSize()
        pop_sz = pop.GetSize()
        if butt_name == "butt_bez_r":
            # Center pop vertically to button
            y_cntr = (- pop_sz[1] + butt_sz[1])/2
            pop.Position(pos, (- pop_sz[0], y_cntr))
        else:
            # Center pop horizontally to button
            x_cntr = (- pop_sz[0] + butt_sz[0])/2
            pop.Position(pos, (x_cntr, - pop_sz[1]))

    def show_bezel_buttons(self, show):
        """Show/Hide the bezel buttons."""
        for butt in self.bez_buttons:
            butt[0].Show(show)
            butt[1].Show(show)

    def bezel_button_positions(self, st_bmp):
        """Return the mid points on the screen of the right and bottom edges
        of the given StaticBitmap."""
        sz = st_bmp.GetSize()
        pos = st_bmp.GetPosition()
        bsz = self.bez_butt_sz
        pos_rb = (sz[0] + pos[0] - bsz[0]/2, sz[1]/2 + pos[1] - bsz[1]/2)
        pos_bb = (sz[0]/2 + pos[0] - bsz[0]/2, sz[1] + pos[1] - bsz[1]/2)
        return [pos_rb, pos_bb]

    def move_bezel_buttons(self):
        """Move bezel buttons after a resize."""
        for butts, st_bmp in zip(self.bez_buttons, self.preview_img_list):
            pos_rb, pos_bb = self.bezel_button_positions(st_bmp)
            butts[0].SetPosition((pos_rb[0], pos_rb[1]))
            butts[1].SetPosition((pos_bb[0], pos_bb[1]))

    def move_bezel_popups(self):
        """Move bezel popups to their respective buttons."""
        for butts, pops in zip(self.bez_buttons, self.bezel_popups):
            self.move_popup_to_button(pops[0], butts[0])
            self.move_popup_to_button(pops[1], butts[1])


    def onBezelButton(self, event):
        self.move_bezel_popups()
        #Get button instance and find it in list
        button = event.GetEventObject()
        for butt_pair in self.bez_buttons:
            if button in butt_pair:
                button_pos = (self.bez_buttons.index(butt_pair), butt_pair.index(button))

        # Pick and show respective popup
        pop = self.bezel_popups[button_pos[0]][button_pos[1]]
        pop.Popup()


    #
    # Bezel entry pop-up
    #

    class BezelEntryPopup(wx.PopupTransientWindow):
        """Popup that is shown when a bezel button is pressed in bezel config."""
        def __init__(self, parent, style):
            wx.PopupTransientWindow.__init__(self, parent, style)
            self.preview = parent
            pnl = wx.Panel(self)
            # pnl.SetBackgroundColour("CADET BLUE")

            st = wx.StaticText(pnl, -1,
                            "Enter the size of adjacent bezels and gap\n"
                            "in millimeters:")
            # self.tc_bez = wx.TextCtrl(pnl, -1, size=(100, -1))
            self.tc_bez = wx.TextCtrl(pnl, -1, style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER)
            self.tc_bez.Bind(wx.EVT_TEXT_ENTER, self.OnEnter)
            self.current_bez_val = None
            butt_save = wx.Button(pnl, label="Apply")
            butt_canc = wx.Button(pnl, label="Cancel")
            butt_save.Bind(wx.EVT_BUTTON, self.onApply)
            butt_canc.Bind(wx.EVT_BUTTON, self.onCancel)
            butt_sizer = wx.BoxSizer(wx.HORIZONTAL)
            # butt_sizer.AddStretchSpacer()
            butt_sizer.Add(self.tc_bez, 0, wx.ALL, 5)
            butt_sizer.Add(butt_save, 0, wx.ALL, 5)
            butt_sizer.Add(butt_canc, 0, wx.ALL, 5)

            sizer = wx.BoxSizer(wx.VERTICAL)
            sizer.Add(st, 0, wx.ALL, 5)
            # sizer.Add(self.tc_bez, 0, wx.ALL, 5)
            sizer.Add(butt_sizer, 0, wx.ALL|wx.EXPAND, 0)
            pnl.SetSizer(sizer)

            sizer.Fit(pnl)
            sizer.Fit(self)
            self.Layout()

        def ProcessLeftDown(self, evt):
            return wx.PopupTransientWindow.ProcessLeftDown(self, evt)

        def OnEnter(self, evt):
            """Bind pressing Enter in the txtctrl to apply entered value."""
            self.onApply(evt)

        def OnDismiss(self):
            self.onCancel(None)

        def onApply(self, event):
            entered_val = self.test_bezel_value()
            if entered_val is False:
                # Abort applying and alert user but don't
                # Dismiss popup to fascilitate fixing.
                msg = ("Bezel thickness must be a non-negative number, "
                       "'{}' was entered.".format(self.tc_bez.GetValue()))
                sp_logging.G_LOGGER.info(msg)
                self.Hide()
                dial = wx.MessageDialog(self, msg, "Error", wx.OK|wx.STAY_ON_TOP|wx.CENTRE)
                dial.ShowModal()
                self.Show()
                return -1
            self.current_bez_val = entered_val
            display_sys = self.preview.display_sys
            pops = self.preview.bezel_popups
            bezel_mms = []
            for pop_pair in pops:
                bezel_mms.append(
                    (
                        pop_pair[0].bezel_value(),
                        pop_pair[1].bezel_value()
                    )
                )
            self.Dismiss()
            # propagate values and refresh preview
            self.preview.display_sys.update_bezels(bezel_mms)
            self.preview.display_data = self.preview.display_sys.get_disp_list(True)
            self.preview.full_refresh_preview(True, True, False)
            # self.preview.show_staticbmps(False) # Use PaintDC drawing separately from staticbitmaps
            # self.preview.draggable_shapes = []
            # self.preview.create_shapes(enable_movement=False)

        def onCancel(self, event):
            if self.current_bez_val:
                self.tc_bez.SetValue(str(self.current_bez_val))
            else:
                self.tc_bez.SetValue("0.0")
            self.Dismiss()

        def bezel_value(self):
            """Return the entered bezel thickness as a float."""
            bez = self.tc_bez.GetValue()
            return float(bez)

        def set_bezel_value(self, val):
            """Write val to TextCtrl."""
            self.tc_bez.SetValue(str(val))
            self.current_bez_val = str(val)

        def test_bezel_value(self):
            """Test that entered value in tc_bez is valid and return it."""
            val = self.tc_bez.GetValue()
            try:
                num = float(val)
                if num >= 0:
                    return num
                else:
                    return False
            except ValueError:
                return False