import collections
import os
import random
import traceback
import typing

from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG

from hydrus.core import HydrusConstants as HC
from hydrus.core import HydrusData
from hydrus.core import HydrusExceptions
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTags
from hydrus.core import HydrusText
from hydrus.core.files.images import HydrusImageHandling

from hydrus.client import ClientApplicationCommand as CAC
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientFilesPhysical
from hydrus.client import ClientGlobals as CG
from hydrus.client.importing.options import FileImportOptions
from hydrus.client.gui import ClientGUIDialogs
from hydrus.client.gui import ClientGUIDialogsMessage
from hydrus.client.gui import ClientGUIDialogsQuick
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import ClientGUIScrolledPanels
from hydrus.client.gui import ClientGUIScrolledPanelsEdit
from hydrus.client.gui import ClientGUIShortcuts
from hydrus.client.gui import ClientGUIStyle
from hydrus.client.gui import ClientGUITags
from hydrus.client.gui import ClientGUITagSorting
from hydrus.client.gui import ClientGUITime
from hydrus.client.gui import ClientGUITopLevelWindowsPanels
from hydrus.client.gui import QtPorting as QP
from hydrus.client.gui.importing import ClientGUIImport
from hydrus.client.gui.importing import ClientGUIImportOptions
from hydrus.client.gui.lists import ClientGUIListBoxes
from hydrus.client.gui.lists import ClientGUIListConstants as CGLC
from hydrus.client.gui.lists import ClientGUIListCtrl
from hydrus.client.gui.pages import ClientGUIResultsSortCollect
from hydrus.client.gui.search import ClientGUIACDropdown
from hydrus.client.gui.search import ClientGUILocation
from hydrus.client.gui.widgets import ClientGUIColourPicker
from hydrus.client.gui.widgets import ClientGUICommon
from hydrus.client.gui.widgets import ClientGUIControls
from hydrus.client.media import ClientMedia
from hydrus.client.metadata import ClientContentUpdates
from hydrus.client.metadata import ClientTags
from hydrus.client.networking import ClientNetworkingSessions

class ManageOptionsPanel( ClientGUIScrolledPanels.ManagePanel ):
    
    def __init__( self, parent ):
        
        ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
        
        self._original_options = dict( HC.options )
        
        self._new_options = CG.client_controller.new_options
        self._original_new_options = self._new_options.Duplicate()
        
        self._listbook = ClientGUICommon.ListBook( self )
        
        self._listbook.AddPage( 'gui', 'gui', self._GUIPanel( self._listbook ) ) # leave this at the top, to make it default page
        self._listbook.AddPage( 'gui pages', 'gui pages', self._GUIPagesPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'connection', 'connection', self._ConnectionPanel( self._listbook ) )
        self._listbook.AddPage( 'external programs', 'external programs', self._ExternalProgramsPanel( self._listbook ) )
        self._listbook.AddPage( 'files and trash', 'files and trash', self._FilesAndTrashPanel( self._listbook ) )
        self._listbook.AddPage( 'file viewing statistics', 'file viewing statistics', self._FileViewingStatisticsPanel( self._listbook ) )
        self._listbook.AddPage( 'speed and memory', 'speed and memory', self._SpeedAndMemoryPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'maintenance and processing', 'maintenance and processing', self._MaintenanceAndProcessingPanel( self._listbook ) )
        self._listbook.AddPage( 'media', 'media', self._MediaPanel( self._listbook ) )
        self._listbook.AddPage( 'audio', 'audio', self._AudioPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'system tray', 'system tray', self._SystemTrayPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'search', 'search', self._SearchPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'colours', 'colours', self._ColoursPanel( self._listbook ) )
        self._listbook.AddPage( 'popups', 'popups', self._PopupPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'regex favourites', 'regex favourites', self._RegexPanel( self._listbook ) )
        self._listbook.AddPage( 'sort/collect', 'sort/collect', self._SortCollectPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'downloading', 'downloading', self._DownloadingPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'duplicates', 'duplicates', self._DuplicatesPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'importing', 'importing', self._ImportingPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'style', 'style', self._StylePanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'tag presentation', 'tag presentation', self._TagPresentationPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'tag suggestions', 'tag suggestions', self._TagSuggestionsPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'tags', 'tags', self._TagsPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'thumbnails', 'thumbnails', self._ThumbnailsPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'system', 'system', self._SystemPanel( self._listbook, self._new_options ) )
        self._listbook.AddPage( 'notes', 'notes', self._NotesPanel( self._listbook, self._new_options ) )
        
        #
        
        vbox = QP.VBoxLayout()
        
        QP.AddToLayout( vbox, self._listbook, CC.FLAGS_EXPAND_BOTH_WAYS )
        
        self.widget().setLayout( vbox )
        
    
    class _AudioPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #self._media_viewer_uses_its_own_audio_volume = QW.QCheckBox( self )
            self._preview_uses_its_own_audio_volume = QW.QCheckBox( self )
            
            self._has_audio_label = QW.QLineEdit( self )
            
            #
            
            tt = 'If unchecked, this media canvas will use the \'global\' audio volume slider. If checked, this media canvas will have its own separate one.'
            tt += os.linesep * 2
            tt += 'Keep this on if you would like the preview viewer to be quieter than the main media viewer.'
            
            #self._media_viewer_uses_its_own_audio_volume.setChecked( self._new_options.GetBoolean( 'media_viewer_uses_its_own_audio_volume' ) )
            self._preview_uses_its_own_audio_volume.setChecked( self._new_options.GetBoolean( 'preview_uses_its_own_audio_volume' ) )
            
            #self._media_viewer_uses_its_own_audio_volume.setToolTip( tt )
            self._preview_uses_its_own_audio_volume.setToolTip( tt )
            
            self._has_audio_label.setText( self._new_options.GetString( 'has_audio_label' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'The preview window has its own volume: ', self._preview_uses_its_own_audio_volume ) )
            #rows.append( ( 'The media viewer has its own volume: ', self._media_viewer_uses_its_own_audio_volume ) )
            rows.append( ( 'Label for files with audio: ', self._has_audio_label ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            #self._new_options.SetBoolean( 'media_viewer_uses_its_own_audio_volume', self._media_viewer_uses_its_own_audio_volume.isChecked() )
            self._new_options.SetBoolean( 'preview_uses_its_own_audio_volume', self._preview_uses_its_own_audio_volume.isChecked() )
            
            self._new_options.SetString( 'has_audio_label', self._has_audio_label.text() )
            
        
    
    class _ColoursPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            coloursets_panel = ClientGUICommon.StaticBox( self, 'coloursets' )
            
            self._current_colourset = ClientGUICommon.BetterChoice( coloursets_panel )
            
            self._current_colourset.addItem( 'default', 'default' )
            self._current_colourset.addItem( 'darkmode', 'darkmode' )
            
            self._current_colourset.SetValue( self._new_options.GetString( 'current_colourset' ) )
            
            self._notebook = QW.QTabWidget( coloursets_panel )
            
            self._gui_colours = {}
            
            for colourset in ( 'default', 'darkmode' ):
                
                self._gui_colours[ colourset ] = {}
                
                colour_panel = QW.QWidget( self._notebook )
                
                colour_types = []
                
                colour_types.append( CC.COLOUR_THUMB_BACKGROUND )
                colour_types.append( CC.COLOUR_THUMB_BACKGROUND_SELECTED )
                colour_types.append( CC.COLOUR_THUMB_BACKGROUND_REMOTE )
                colour_types.append( CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED )
                colour_types.append( CC.COLOUR_THUMB_BORDER )
                colour_types.append( CC.COLOUR_THUMB_BORDER_SELECTED )
                colour_types.append( CC.COLOUR_THUMB_BORDER_REMOTE )
                colour_types.append( CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED )
                colour_types.append( CC.COLOUR_THUMBGRID_BACKGROUND )
                colour_types.append( CC.COLOUR_AUTOCOMPLETE_BACKGROUND )
                colour_types.append( CC.COLOUR_MEDIA_BACKGROUND )
                colour_types.append( CC.COLOUR_MEDIA_TEXT )
                colour_types.append( CC.COLOUR_TAGS_BOX )
                
                for colour_type in colour_types:
                    
                    ctrl = ClientGUIColourPicker.ColourPickerButton( colour_panel )
                    
                    ctrl.setMaximumWidth( 20 )
                    
                    ctrl.SetColour( self._new_options.GetColour( colour_type, colourset ) )
                    
                    self._gui_colours[ colourset ][ colour_type ] = ctrl
                    
                
                #
                
                rows = []
                
                hbox = QP.HBoxLayout()
                
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BACKGROUND], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BACKGROUND_SELECTED], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BACKGROUND_REMOTE], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BACKGROUND_REMOTE_SELECTED], CC.FLAGS_CENTER_PERPENDICULAR )
                
                rows.append( ( 'thumbnail background (local: normal/selected, not local: normal/selected): ', hbox ) )
                
                hbox = QP.HBoxLayout()
                
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BORDER], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BORDER_SELECTED], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BORDER_REMOTE], CC.FLAGS_CENTER_PERPENDICULAR )
                QP.AddToLayout( hbox, self._gui_colours[colourset][CC.COLOUR_THUMB_BORDER_REMOTE_SELECTED], CC.FLAGS_CENTER_PERPENDICULAR )
                
                rows.append( ( 'thumbnail border (local: normal/selected, not local: normal/selected): ', hbox ) )
                
                rows.append( ( 'thumbnail grid background: ', self._gui_colours[ colourset ][ CC.COLOUR_THUMBGRID_BACKGROUND ] ) )
                rows.append( ( 'autocomplete background: ', self._gui_colours[ colourset ][ CC.COLOUR_AUTOCOMPLETE_BACKGROUND ] ) )
                rows.append( ( 'media viewer background: ', self._gui_colours[ colourset ][ CC.COLOUR_MEDIA_BACKGROUND ] ) )
                rows.append( ( 'media viewer text: ', self._gui_colours[ colourset ][ CC.COLOUR_MEDIA_TEXT ] ) )
                rows.append( ( 'tags box background: ', self._gui_colours[ colourset ][ CC.COLOUR_TAGS_BOX ] ) )
                
                gridbox = ClientGUICommon.WrapInGrid( colour_panel, rows )
                
                colour_panel.setLayout( gridbox )
                
                select = colourset == 'default'
                
                self._notebook.addTab( colour_panel, colourset )
                if select: self._notebook.setCurrentWidget( colour_panel )
                
            
            #
            
            coloursets_panel.Add( ClientGUICommon.WrapInText( self._current_colourset, coloursets_panel, 'current colourset: ' ), CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            coloursets_panel.Add( self._notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, coloursets_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            for colourset in self._gui_colours:
                
                for ( colour_type, ctrl ) in list(self._gui_colours[ colourset ].items()):
                    
                    colour = ctrl.GetColour()
                    
                    self._new_options.SetColour( colour_type, colourset, colour )
                    
                
            
            self._new_options.SetString( 'current_colourset', self._current_colourset.GetValue() )
            
        
    
    class _ConnectionPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            general = ClientGUICommon.StaticBox( self, 'general' )
            
            self._verify_regular_https = QW.QCheckBox( general )
            
            if self._new_options.GetBoolean( 'advanced_mode' ):
                
                network_timeout_min = 1
                network_timeout_max = 86400 * 30
                
                error_wait_time_min = 1
                error_wait_time_max = 86400 * 30
                
                max_network_jobs_max = 1000
                
                max_network_jobs_per_domain_max = 100
                
            else:
                
                network_timeout_min = 3
                network_timeout_max = 600
                
                error_wait_time_min = 3
                error_wait_time_max = 1800
                
                max_network_jobs_max = 30
                
                max_network_jobs_per_domain_max = 5
                
            
            self._max_connection_attempts_allowed = ClientGUICommon.BetterSpinBox( general, min = 1, max = 10 )
            self._max_connection_attempts_allowed.setToolTip( 'This refers to timeouts when actually making the initial connection.' )
            
            self._max_request_attempts_allowed_get = ClientGUICommon.BetterSpinBox( general, min = 1, max = 10 )
            self._max_request_attempts_allowed_get.setToolTip( 'This refers to timeouts when waiting for a response to our GET requests, whether that is the start or an interruption part way through.' )
            
            self._network_timeout = ClientGUICommon.BetterSpinBox( general, min = network_timeout_min, max = network_timeout_max )
            self._network_timeout.setToolTip( 'If a network connection cannot be made in this duration or, once started, it experiences inactivity for six times this duration, it will be considered dead and retried or abandoned.' )
            
            self._connection_error_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
            self._connection_error_wait_time.setToolTip( 'If a network connection times out as above, it will wait increasing multiples of this base time before retrying.' )
            
            self._serverside_bandwidth_wait_time = ClientGUICommon.BetterSpinBox( general, min = error_wait_time_min, max = error_wait_time_max )
            self._serverside_bandwidth_wait_time.setToolTip( 'If a server returns a failure status code indicating it is short on bandwidth, the network job will wait increasing multiples of this base time before retrying.' )
            
            self._domain_network_infrastructure_error_velocity = ClientGUITime.VelocityCtrl( general, 0, 100, 30, hours = True, minutes = True, seconds = True, per_phrase = 'within', unit = 'errors' )
            
            self._max_network_jobs = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_max )
            self._max_network_jobs_per_domain = ClientGUICommon.BetterSpinBox( general, min = 1, max = max_network_jobs_per_domain_max )
            
            #
            
            proxy_panel = ClientGUICommon.StaticBox( self, 'proxy settings' )
            
            self._http_proxy = ClientGUICommon.NoneableTextCtrl( proxy_panel )
            self._https_proxy = ClientGUICommon.NoneableTextCtrl( proxy_panel )
            self._no_proxy = ClientGUICommon.NoneableTextCtrl( proxy_panel )
            
            #
            
            self._verify_regular_https.setChecked( self._new_options.GetBoolean( 'verify_regular_https' ) )
            
            self._http_proxy.SetValue( self._new_options.GetNoneableString( 'http_proxy' ) )
            self._https_proxy.SetValue( self._new_options.GetNoneableString( 'https_proxy' ) )
            self._no_proxy.SetValue( self._new_options.GetNoneableString( 'no_proxy' ) )
            
            self._max_connection_attempts_allowed.setValue( self._new_options.GetInteger( 'max_connection_attempts_allowed' ) )
            self._max_request_attempts_allowed_get.setValue( self._new_options.GetInteger( 'max_request_attempts_allowed_get' ) )
            self._network_timeout.setValue( self._new_options.GetInteger( 'network_timeout' ) )
            self._connection_error_wait_time.setValue( self._new_options.GetInteger( 'connection_error_wait_time' ) )
            self._serverside_bandwidth_wait_time.setValue( self._new_options.GetInteger( 'serverside_bandwidth_wait_time' ) )
            
            number = self._new_options.GetInteger( 'domain_network_infrastructure_error_number' )
            time_delta = self._new_options.GetInteger( 'domain_network_infrastructure_error_time_delta' )
            
            self._domain_network_infrastructure_error_velocity.SetValue( ( number, time_delta ) )
            
            self._max_network_jobs.setValue( self._new_options.GetInteger( 'max_network_jobs' ) )
            self._max_network_jobs_per_domain.setValue( self._new_options.GetInteger( 'max_network_jobs_per_domain' ) )
            
            #
            
            if self._new_options.GetBoolean( 'advanced_mode' ):
                
                label = 'As you are in advanced mode, these options have very low and high limits. Be very careful about lowering delay time or raising max number of connections too far, as things will break.'
                
                st = ClientGUICommon.BetterStaticText( general, label = label )
                st.setObjectName( 'HydrusWarning' )
                
                st.setWordWrap( True )
                
                general.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
                
            
            rows = []
            
            rows.append( ( 'max connection attempts allowed per request: ', self._max_connection_attempts_allowed ) )
            rows.append( ( 'max retries allowed per request: ', self._max_request_attempts_allowed_get ) )
            rows.append( ( 'network timeout (seconds): ', self._network_timeout ) )
            rows.append( ( 'connection error retry wait (seconds): ', self._connection_error_wait_time ) )
            rows.append( ( 'serverside bandwidth retry wait (seconds): ', self._serverside_bandwidth_wait_time ) )
            rows.append( ( 'Halt new jobs as long as this many network infrastructure errors on their domain (0 for never wait): ', self._domain_network_infrastructure_error_velocity ) )
            rows.append( ( 'max number of simultaneous active network jobs: ', self._max_network_jobs ) )
            rows.append( ( 'max number of simultaneous active network jobs per domain: ', self._max_network_jobs_per_domain ) )
            rows.append( ( 'BUGFIX: verify regular https traffic:', self._verify_regular_https ) )
            
            gridbox = ClientGUICommon.WrapInGrid( general, rows )
            
            general.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            text = 'Enter strings such as "http://ip:port" or "http://user:pass@ip:port" to use for http and https traffic. It should take effect immediately on dialog ok.'
            text += os.linesep * 2
            text += 'NO PROXY DOES NOT WORK UNLESS YOU HAVE A CUSTOM BUILD OF REQUESTS, SORRY! no_proxy takes the form of comma-separated hosts/domains, just as in curl or the NO_PROXY environment variable. When http and/or https proxies are set, they will not be used for these.'
            text += os.linesep * 2
            
            if ClientNetworkingSessions.SOCKS_PROXY_OK:
                
                text += 'It looks like you have socks support! You should also be able to enter (socks4 or) "socks5://ip:port".'
                text += os.linesep
                text += 'Use socks4a or socks5h to force remote DNS resolution, on the proxy server.'
                
            else:
                
                text += 'It does not look like you have socks support! If you want it, try adding "pysocks" (or "requests[socks]")!'
                
            
            st = ClientGUICommon.BetterStaticText( proxy_panel, text )
            
            st.setWordWrap( True )
            
            proxy_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'http: ', self._http_proxy ) )
            rows.append( ( 'https: ', self._https_proxy ) )
            rows.append( ( 'no_proxy: ', self._no_proxy ) )
            
            gridbox = ClientGUICommon.WrapInGrid( proxy_panel, rows )
            
            proxy_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, general, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, proxy_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetBoolean( 'verify_regular_https', self._verify_regular_https.isChecked() )
            
            self._new_options.SetNoneableString( 'http_proxy', self._http_proxy.GetValue() )
            self._new_options.SetNoneableString( 'https_proxy', self._https_proxy.GetValue() )
            self._new_options.SetNoneableString( 'no_proxy', self._no_proxy.GetValue() )
            
            self._new_options.SetInteger( 'max_connection_attempts_allowed', self._max_connection_attempts_allowed.value() )
            self._new_options.SetInteger( 'max_request_attempts_allowed_get', self._max_request_attempts_allowed_get.value() )
            self._new_options.SetInteger( 'network_timeout', self._network_timeout.value() )
            self._new_options.SetInteger( 'connection_error_wait_time', self._connection_error_wait_time.value() )
            self._new_options.SetInteger( 'serverside_bandwidth_wait_time', self._serverside_bandwidth_wait_time.value() )
            
            ( number, time_delta ) = self._domain_network_infrastructure_error_velocity.GetValue()
            
            self._new_options.SetInteger( 'domain_network_infrastructure_error_number', number )
            self._new_options.SetInteger( 'domain_network_infrastructure_error_time_delta', time_delta )
            
            self._new_options.SetInteger( 'max_network_jobs', self._max_network_jobs.value() )
            self._new_options.SetInteger( 'max_network_jobs_per_domain', self._max_network_jobs_per_domain.value() )
            
        
    
    class _DownloadingPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            gallery_downloader = ClientGUICommon.StaticBox( self, 'gallery downloader' )
            
            gug_key_and_name = CG.client_controller.network_engine.domain_manager.GetDefaultGUGKeyAndName()
            
            self._default_gug = ClientGUIImport.GUGKeyAndNameSelector( gallery_downloader, gug_key_and_name )
            
            self._gallery_page_wait_period_pages = ClientGUICommon.BetterSpinBox( gallery_downloader, min=1, max=3600 )
            self._gallery_file_limit = ClientGUICommon.NoneableSpinCtrl( gallery_downloader, none_phrase = 'no limit', min = 1, max = 1000000 )
            
            self._highlight_new_query = QW.QCheckBox( gallery_downloader )
            
            #
            
            subscriptions = ClientGUICommon.StaticBox( self, 'subscriptions' )
            
            self._gallery_page_wait_period_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=3600 )
            self._max_simultaneous_subscriptions = ClientGUICommon.BetterSpinBox( subscriptions, min=1, max=100 )
            
            self._subscription_file_error_cancel_threshold = ClientGUICommon.NoneableSpinCtrl( subscriptions, min = 1, max = 1000000, unit = 'errors' )
            self._subscription_file_error_cancel_threshold.setToolTip( 'This is a simple patch and will be replaced with a better "retry network errors later" system at some point, but is useful to increase if you have subs to unreliable websites.' )
            
            self._process_subs_in_random_order = QW.QCheckBox( subscriptions )
            self._process_subs_in_random_order.setToolTip( 'Processing in random order is useful whenever bandwidth is tight, as it stops an \'aardvark\' subscription from always getting first whack at what is available. Otherwise, they will be processed in alphabetical order.' )
            
            checker_options = self._new_options.GetDefaultSubscriptionCheckerOptions()
            
            self._subscription_checker_options = ClientGUIImport.CheckerOptionsButton( subscriptions, checker_options )
            
            #
            
            watchers = ClientGUICommon.StaticBox( self, 'watchers' )
            
            self._watcher_page_wait_period = ClientGUICommon.BetterSpinBox( watchers, min=1, max=3600 )
            self._highlight_new_watcher = QW.QCheckBox( watchers )
            
            checker_options = self._new_options.GetDefaultWatcherCheckerOptions()
            
            self._watcher_checker_options = ClientGUIImport.CheckerOptionsButton( watchers, checker_options )
            
            #
            
            misc = ClientGUICommon.StaticBox( self, 'misc' )
            
            self._pause_character = QW.QLineEdit( misc )
            self._stop_character = QW.QLineEdit( misc )
            self._show_new_on_file_seed_short_summary = QW.QCheckBox( misc )
            self._show_deleted_on_file_seed_short_summary = QW.QCheckBox( misc )
            
            if self._new_options.GetBoolean( 'advanced_mode' ):
                
                delay_min = 1
                
            else:
                
                delay_min = 600
                
            
            self._subscription_network_error_delay = ClientGUITime.TimeDeltaButton( misc, min = delay_min, days = True, hours = True, minutes = True, seconds = True )
            self._subscription_other_error_delay = ClientGUITime.TimeDeltaButton( misc, min = delay_min, days = True, hours = True, minutes = True, seconds = True )
            self._downloader_network_error_delay = ClientGUITime.TimeDeltaButton( misc, min = delay_min, days = True, hours = True, minutes = True, seconds = True )
            
            #
            
            gallery_page_tt = 'Gallery page fetches are heavy requests with unusual fetch-time requirements. It is important they not wait too long, but it is also useful to throttle them:'
            gallery_page_tt += os.linesep * 2
            gallery_page_tt += '- So they do not compete with file downloads for bandwidth, leading to very unbalanced 20/4400-type queues.'
            gallery_page_tt += os.linesep
            gallery_page_tt += '- So you do not get 1000 items in your queue before realising you did not like that tag anyway.'
            gallery_page_tt += os.linesep
            gallery_page_tt += '- To give servers a break (some gallery pages can be CPU-expensive to generate).'
            gallery_page_tt += os.linesep * 2
            gallery_page_tt += 'These delays/lots are per-domain.'
            gallery_page_tt += os.linesep * 2
            gallery_page_tt += 'If you do not understand this stuff, you can just leave it alone.'
            
            self._gallery_page_wait_period_pages.setValue( self._new_options.GetInteger( 'gallery_page_wait_period_pages' ) )
            self._gallery_page_wait_period_pages.setToolTip( gallery_page_tt )
            self._gallery_file_limit.SetValue( HC.options['gallery_file_limit'] )
            
            self._highlight_new_query.setChecked( self._new_options.GetBoolean( 'highlight_new_query' ) )
            
            self._gallery_page_wait_period_subscriptions.setValue( self._new_options.GetInteger( 'gallery_page_wait_period_subscriptions' ) )
            self._gallery_page_wait_period_subscriptions.setToolTip( gallery_page_tt )
            self._max_simultaneous_subscriptions.setValue( self._new_options.GetInteger( 'max_simultaneous_subscriptions' ) )
            
            self._subscription_file_error_cancel_threshold.SetValue( self._new_options.GetNoneableInteger( 'subscription_file_error_cancel_threshold' ) )
            
            self._process_subs_in_random_order.setChecked( self._new_options.GetBoolean( 'process_subs_in_random_order' ) )
            
            self._pause_character.setText( self._new_options.GetString( 'pause_character' ) )
            self._stop_character.setText( self._new_options.GetString( 'stop_character' ) )
            self._show_new_on_file_seed_short_summary.setChecked( self._new_options.GetBoolean( 'show_new_on_file_seed_short_summary' ) )
            self._show_deleted_on_file_seed_short_summary.setChecked( self._new_options.GetBoolean( 'show_deleted_on_file_seed_short_summary' ) )
            
            self._watcher_page_wait_period.setValue( self._new_options.GetInteger( 'watcher_page_wait_period' ) )
            self._watcher_page_wait_period.setToolTip( gallery_page_tt )
            self._highlight_new_watcher.setChecked( self._new_options.GetBoolean( 'highlight_new_watcher' ) )
            
            self._subscription_network_error_delay.SetValue( self._new_options.GetInteger( 'subscription_network_error_delay' ) )
            self._subscription_other_error_delay.SetValue( self._new_options.GetInteger( 'subscription_other_error_delay' ) )
            self._downloader_network_error_delay.SetValue( self._new_options.GetInteger( 'downloader_network_error_delay' ) )
            
            #
            
            rows = []
            
            rows.append( ( 'Default download source:', self._default_gug ) )
            rows.append( ( 'If new query entered and no current highlight, highlight the new query:', self._highlight_new_query ) )
            rows.append( ( 'Additional fixed time (in seconds) to wait between gallery page fetches:', self._gallery_page_wait_period_pages ) )
            rows.append( ( 'By default, stop searching once this many files are found:', self._gallery_file_limit ) )
            
            gridbox = ClientGUICommon.WrapInGrid( gallery_downloader, rows )
            
            gallery_downloader.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Additional fixed time (in seconds) to wait between gallery page fetches:', self._gallery_page_wait_period_subscriptions ) )
            rows.append( ( 'Maximum number of subscriptions that can sync simultaneously:', self._max_simultaneous_subscriptions ) )
            rows.append( ( 'If a subscription has this many failed file imports, stop and continue later:', self._subscription_file_error_cancel_threshold ) )
            rows.append( ( 'Sync subscriptions in random order:', self._process_subs_in_random_order ) )
            
            gridbox = ClientGUICommon.WrapInGrid( subscriptions, rows )
            
            subscriptions.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            subscriptions.Add( self._subscription_checker_options, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Additional fixed time (in seconds) to wait between watcher checks:', self._watcher_page_wait_period ) )
            rows.append( ( 'If new watcher entered and no current highlight, highlight the new watcher:', self._highlight_new_watcher ) )
            
            gridbox = ClientGUICommon.WrapInGrid( watchers, rows )
            
            watchers.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            watchers.Add( self._watcher_checker_options, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Pause character:', self._pause_character ) )
            rows.append( ( 'Stop character:', self._stop_character ) )
            rows.append( ( 'Show a \'N\' (for \'new\') count on short file import summaries:', self._show_new_on_file_seed_short_summary ) )
            rows.append( ( 'Show a \'D\' (for \'deleted\') count on short file import summaries:', self._show_deleted_on_file_seed_short_summary ) )
            rows.append( ( 'Delay time on a gallery/watcher network error:', self._downloader_network_error_delay ) )
            rows.append( ( 'Delay time on a subscription network error:', self._subscription_network_error_delay ) )
            rows.append( ( 'Delay time on a subscription other error:', self._subscription_other_error_delay ) )
            
            gridbox = ClientGUICommon.WrapInGrid( misc, rows )
            
            misc.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, gallery_downloader, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, subscriptions, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, watchers, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, misc, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            CG.client_controller.network_engine.domain_manager.SetDefaultGUGKeyAndName( self._default_gug.GetValue() )
            
            self._new_options.SetInteger( 'gallery_page_wait_period_pages', self._gallery_page_wait_period_pages.value() )
            HC.options[ 'gallery_file_limit' ] = self._gallery_file_limit.GetValue()
            self._new_options.SetBoolean( 'highlight_new_query', self._highlight_new_query.isChecked() )
            
            self._new_options.SetInteger( 'gallery_page_wait_period_subscriptions', self._gallery_page_wait_period_subscriptions.value() )
            self._new_options.SetInteger( 'max_simultaneous_subscriptions', self._max_simultaneous_subscriptions.value() )
            self._new_options.SetNoneableInteger( 'subscription_file_error_cancel_threshold', self._subscription_file_error_cancel_threshold.GetValue() )
            self._new_options.SetBoolean( 'process_subs_in_random_order', self._process_subs_in_random_order.isChecked() )
            
            self._new_options.SetInteger( 'watcher_page_wait_period', self._watcher_page_wait_period.value() )
            self._new_options.SetBoolean( 'highlight_new_watcher', self._highlight_new_watcher.isChecked() )
            
            self._new_options.SetDefaultWatcherCheckerOptions( self._watcher_checker_options.GetValue() )
            self._new_options.SetDefaultSubscriptionCheckerOptions( self._subscription_checker_options.GetValue() )
            
            self._new_options.SetString( 'pause_character', self._pause_character.text() )
            self._new_options.SetString( 'stop_character', self._stop_character.text() )
            self._new_options.SetBoolean( 'show_new_on_file_seed_short_summary', self._show_new_on_file_seed_short_summary.isChecked() )
            self._new_options.SetBoolean( 'show_deleted_on_file_seed_short_summary', self._show_deleted_on_file_seed_short_summary.isChecked() )
            
            self._new_options.SetInteger( 'subscription_network_error_delay', self._subscription_network_error_delay.GetValue() )
            self._new_options.SetInteger( 'subscription_other_error_delay', self._subscription_other_error_delay.GetValue() )
            self._new_options.SetInteger( 'downloader_network_error_delay', self._downloader_network_error_delay.GetValue() )
            
        
    
    class _DuplicatesPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            weights_panel = ClientGUICommon.StaticBox( self, 'duplicate filter comparison score weights' )
            
            self._duplicate_comparison_score_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_much_higher_jpeg_quality = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_much_higher_filesize = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_much_higher_resolution = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_more_tags = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_older = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_nicer_ratio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            self._duplicate_comparison_score_has_audio = ClientGUICommon.BetterSpinBox( weights_panel, min=-100, max=100 )
            
            self._duplicate_comparison_score_nicer_ratio.setToolTip( 'For instance, 16:9 vs 640:357.')
            
            batches_panel = ClientGUICommon.StaticBox( self, 'duplicate filter batches' )
            
            self._duplicate_filter_max_batch_size = ClientGUICommon.BetterSpinBox( batches_panel, min = 5, max = 1024 )
            
            colours_panel = ClientGUICommon.StaticBox( self, 'colours' )
            
            self._duplicate_background_switch_intensity_a = ClientGUICommon.NoneableSpinCtrl( colours_panel, none_phrase = 'do not change', min = 1, max = 9 )
            self._duplicate_background_switch_intensity_a.setToolTip( 'This changes the background colour when you are looking at A. If you have a pure white/black background and do not have transparent images to show with checkerboard, it helps to highlight transparency vs opaque white/black image background.' )
            
            self._duplicate_background_switch_intensity_b = ClientGUICommon.NoneableSpinCtrl( colours_panel, none_phrase = 'do not change', min = 1, max = 9 )
            self._duplicate_background_switch_intensity_b.setToolTip( 'This changes the background colour when you are looking at B. Making it different to the A value helps to highlight switches between the two.' )
            
            self._draw_transparency_checkerboard_media_canvas_duplicates = QW.QCheckBox( colours_panel )
            self._draw_transparency_checkerboard_media_canvas_duplicates.setToolTip( 'Same as the setting in _media_, but only for the duplicate filter. Only applies if that _media_ setting is unchecked.' )
            
            #
            
            self._duplicate_comparison_score_higher_jpeg_quality.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_higher_jpeg_quality' ) )
            self._duplicate_comparison_score_much_higher_jpeg_quality.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_much_higher_jpeg_quality' ) )
            self._duplicate_comparison_score_higher_filesize.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_higher_filesize' ) )
            self._duplicate_comparison_score_much_higher_filesize.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_much_higher_filesize' ) )
            self._duplicate_comparison_score_higher_resolution.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_higher_resolution' ) )
            self._duplicate_comparison_score_much_higher_resolution.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_much_higher_resolution' ) )
            self._duplicate_comparison_score_more_tags.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_more_tags' ) )
            self._duplicate_comparison_score_older.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_older' ) )
            self._duplicate_comparison_score_nicer_ratio.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_nicer_ratio' ) )
            self._duplicate_comparison_score_has_audio.setValue( self._new_options.GetInteger( 'duplicate_comparison_score_has_audio' ) )
            
            self._duplicate_filter_max_batch_size.setValue( self._new_options.GetInteger( 'duplicate_filter_max_batch_size' ) )
            
            self._duplicate_background_switch_intensity_a.SetValue( self._new_options.GetNoneableInteger( 'duplicate_background_switch_intensity_a' ) )
            self._duplicate_background_switch_intensity_b.SetValue( self._new_options.GetNoneableInteger( 'duplicate_background_switch_intensity_b' ) )
            self._draw_transparency_checkerboard_media_canvas_duplicates.setChecked( self._new_options.GetBoolean( 'draw_transparency_checkerboard_media_canvas_duplicates' ) )
            
            #
            
            rows = []
            
            rows.append( ( 'Score for jpeg with non-trivially higher jpeg quality:', self._duplicate_comparison_score_higher_jpeg_quality ) )
            rows.append( ( 'Score for jpeg with significantly higher jpeg quality:', self._duplicate_comparison_score_much_higher_jpeg_quality ) )
            rows.append( ( 'Score for file with non-trivially higher filesize:', self._duplicate_comparison_score_higher_filesize ) )
            rows.append( ( 'Score for file with significantly higher filesize:', self._duplicate_comparison_score_much_higher_filesize ) )
            rows.append( ( 'Score for file with higher resolution (as num pixels):', self._duplicate_comparison_score_higher_resolution ) )
            rows.append( ( 'Score for file with significantly higher resolution (as num pixels):', self._duplicate_comparison_score_much_higher_resolution ) )
            rows.append( ( 'Score for file with more tags:', self._duplicate_comparison_score_more_tags ) )
            rows.append( ( 'Score for file with non-trivially earlier import time:', self._duplicate_comparison_score_older ) )
            rows.append( ( 'Score for file with \'nicer\' resolution ratio:', self._duplicate_comparison_score_nicer_ratio ) )
            rows.append( ( 'Score for file with audio:', self._duplicate_comparison_score_has_audio ) )
            
            gridbox = ClientGUICommon.WrapInGrid( weights_panel, rows )
            
            label = 'When processing potential duplicate pairs in the duplicate filter, the client tries to present the \'best\' file first. It judges the two files on a variety of potential differences, each with a score. The file with the greatest total score is presented first. Here you can tinker with these scores.'
            label += os.linesep * 2
            label += 'I recommend you leave all these as positive numbers, but if you wish, you can set a negative number to reduce the score.'
            
            st = ClientGUICommon.BetterStaticText( weights_panel, label )
            st.setWordWrap( True )
            
            weights_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            weights_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Max size of duplicate filter pair batches:', self._duplicate_filter_max_batch_size ) )
            
            gridbox = ClientGUICommon.WrapInGrid( batches_panel, rows )
            
            batches_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            st = ClientGUICommon.BetterStaticText( colours_panel, label = 'The duplicate filter can darken/lighten your normal background colour. This highlights the transitions between A and B and, if your background colour is normally pure white or black, can differentiate transparency vs white/black opaque image background.' )
            st.setWordWrap( True )
            
            colours_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'background light/dark switch intensity for A:', self._duplicate_background_switch_intensity_a ) )
            rows.append( ( 'background light/dark switch intensity for B:', self._duplicate_background_switch_intensity_b ) )
            rows.append( ( 'draw image transparency as checkerboard in the duplicate filter:', self._draw_transparency_checkerboard_media_canvas_duplicates ) )
            
            gridbox = ClientGUICommon.WrapInGrid( batches_panel, rows )
            
            colours_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, weights_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, batches_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, colours_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetInteger( 'duplicate_comparison_score_higher_jpeg_quality', self._duplicate_comparison_score_higher_jpeg_quality.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_much_higher_jpeg_quality', self._duplicate_comparison_score_much_higher_jpeg_quality.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_higher_filesize', self._duplicate_comparison_score_higher_filesize.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_much_higher_filesize', self._duplicate_comparison_score_much_higher_filesize.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_higher_resolution', self._duplicate_comparison_score_higher_resolution.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_much_higher_resolution', self._duplicate_comparison_score_much_higher_resolution.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_more_tags', self._duplicate_comparison_score_more_tags.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_older', self._duplicate_comparison_score_older.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_nicer_ratio', self._duplicate_comparison_score_nicer_ratio.value() )
            self._new_options.SetInteger( 'duplicate_comparison_score_has_audio', self._duplicate_comparison_score_has_audio.value() )
            
            self._new_options.SetInteger( 'duplicate_filter_max_batch_size', self._duplicate_filter_max_batch_size.value() )
            
            self._new_options.SetNoneableInteger( 'duplicate_background_switch_intensity_a', self._duplicate_background_switch_intensity_a.GetValue() )
            self._new_options.SetNoneableInteger( 'duplicate_background_switch_intensity_b', self._duplicate_background_switch_intensity_b.GetValue() )
            self._new_options.SetBoolean( 'draw_transparency_checkerboard_media_canvas_duplicates', self._draw_transparency_checkerboard_media_canvas_duplicates.isChecked() )
            
        
    
    class _ExternalProgramsPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            browser_panel = ClientGUICommon.StaticBox( self, 'web browser launch path' )
            
            self._web_browser_path = QW.QLineEdit( browser_panel )
            
            web_browser_path = self._new_options.GetNoneableString( 'web_browser_path' )
            
            if web_browser_path is not None:
                
                self._web_browser_path.setText( web_browser_path )
                
            
            #
            
            mime_panel = ClientGUICommon.StaticBox( self, '\'open externally\' launch paths' )
            
            self._mime_launch_listctrl = ClientGUIListCtrl.BetterListCtrl( mime_panel, CGLC.COLUMN_LIST_EXTERNAL_PROGRAMS.ID, 15, self._ConvertMimeToListCtrlTuples, activation_callback = self._EditMimeLaunch )
            
            for mime in HC.SEARCHABLE_MIMES:
                
                launch_path = self._new_options.GetMimeLaunch( mime )
                
                self._mime_launch_listctrl.AddDatas( [ ( mime, launch_path ) ] )
                
            
            self._mime_launch_listctrl.Sort()
            
            #
            
            vbox = QP.VBoxLayout()
            
            text = 'By default, when you ask to open a URL, hydrus will send it to your OS, and that figures out what your "default" web browser is. These OS launch commands can be buggy, though, and sometimes drop #anchor components. If this happens to you, set the specific launch command for your web browser here.'
            text += os.linesep * 2
            text += 'The command here must include a "%path%" component, normally ideally within those quote marks, which is where hydrus will place the URL when it executes the command. A good example would be:'
            text += os.linesep * 2
            text += 'C:\\program files\\firefox\\firefox.exe "%path%"'
            
            st = ClientGUICommon.BetterStaticText( browser_panel, text )
            st.setWordWrap( True )
            
            browser_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Manual web browser launch command: ', self._web_browser_path ) )
            
            gridbox = ClientGUICommon.WrapInGrid( mime_panel, rows )
            
            browser_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            text = 'Similarly, when you ask to open a file "externally", hydrus will send it to your OS, and that figures out your "default" program. This may fail or direct to a program you do not want for several reasons, so you can set a specific override here.'
            text += os.linesep * 2
            text += 'Again, make sure you include the "%path%" component. Most programs are going to be like \'program_exe "%path%"\', but some may need a profile switch or "-o" open command or similar.'
            
            st = ClientGUICommon.BetterStaticText( mime_panel, text )
            st.setWordWrap( True )
            
            mime_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            mime_panel.Add( self._mime_launch_listctrl, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            QP.AddToLayout( vbox, browser_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, mime_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
        
        def _ConvertMimeToListCtrlTuples( self, data ):
            
            ( mime, launch_path ) = data
            
            pretty_mime = HC.mime_string_lookup[ mime ]
            
            if launch_path is None:
                
                pretty_launch_path = 'default: {}'.format( HydrusPaths.GetDefaultLaunchPath() )
                
            else:
                
                pretty_launch_path = launch_path
                
            
            display_tuple = ( pretty_mime, pretty_launch_path )
            sort_tuple = display_tuple
            
            return ( display_tuple, sort_tuple )
            
        
        def _EditMimeLaunch( self ):
            
            edited_datas = []
            
            for ( mime, launch_path ) in self._mime_launch_listctrl.GetData( only_selected = True ):
                
                message = 'Enter the new launch path for {}'.format( HC.mime_string_lookup[ mime ] )
                message += os.linesep * 2
                message += 'Hydrus will insert the file\'s full path wherever you put %path%, even multiple times!'
                message += os.linesep * 2
                message += 'Set as blank to reset to default.'
                
                if launch_path is None:
                    
                    default = 'program "%path%"'
                    
                else:
                    
                    default = launch_path
                    
                
                with ClientGUIDialogs.DialogTextEntry( self, message, default = default, allow_blank = True ) as dlg:
                    
                    if dlg.exec() == QW.QDialog.Accepted:
                        
                        new_launch_path = dlg.GetValue()
                        
                        if new_launch_path == '':
                            
                            new_launch_path = None
                            
                        
                        if new_launch_path not in ( launch_path, default ):
                            
                            if new_launch_path is not None and '%path%' not in new_launch_path:
                                
                                message = f'Hey, your command "{new_launch_path}" did not include %path%--it probably is not going to work! Are you sure this is ok?'
                                
                                result = ClientGUIDialogsQuick.GetYesNo( self, message )
                                
                                if result != QW.QDialog.Accepted:
                                    
                                    break
                                    
                                
                            
                            self._mime_launch_listctrl.DeleteDatas( [ ( mime, launch_path ) ] )
                            
                            edited_data = ( mime, new_launch_path )
                            
                            self._mime_launch_listctrl.AddDatas( [ edited_data ] )
                            
                            edited_datas.append( edited_data )
                            
                        
                    else:
                        
                        break
                        
                    
                
            
            self._mime_launch_listctrl.SelectDatas( edited_datas )
            
            self._mime_launch_listctrl.Sort()
            
        
        def UpdateOptions( self ):
            
            web_browser_path = self._web_browser_path.text()
            
            if web_browser_path == '':
                
                web_browser_path = None
                
            
            self._new_options.SetNoneableString( 'web_browser_path', web_browser_path )
            
            for ( mime, launch_path ) in self._mime_launch_listctrl.GetData():
                
                self._new_options.SetMimeLaunch( mime, launch_path )
                
            
        
    
    class _FilesAndTrashPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            self._export_location = QP.DirPickerCtrl( self )
            
            self._prefix_hash_when_copying = QW.QCheckBox( self )
            self._prefix_hash_when_copying.setToolTip( 'If you often paste hashes into boorus, check this to automatically prefix with the type, like "md5:2496dabcbd69e3c56a5d8caabb7acde5".' )
            
            self._delete_to_recycle_bin = QW.QCheckBox( self )
            
            self._ms_to_wait_between_physical_file_deletes = ClientGUICommon.BetterSpinBox( self, min=20, max = 5000 )
            tt = 'Deleting a file from a hard disk can be resource expensive, so when files leave the trash, the actual physical file delete happens later, in the background. The operation is spread out so as not to give you lag spikes.'
            self._ms_to_wait_between_physical_file_deletes.setToolTip( tt )
            
            self._confirm_trash = QW.QCheckBox( self )
            self._confirm_archive = QW.QCheckBox( self )
            
            self._confirm_multiple_local_file_services_copy = QW.QCheckBox( self )
            self._confirm_multiple_local_file_services_move = QW.QCheckBox( self )
            
            self._remove_filtered_files = QW.QCheckBox( self )
            self._remove_trashed_files = QW.QCheckBox( self )
            self._remove_local_domain_moved_files = QW.QCheckBox( self )
            
            self._trash_max_age = ClientGUICommon.NoneableSpinCtrl( self, '', none_phrase = 'no age limit', min = 0, max = 8640 )
            self._trash_max_size = ClientGUICommon.NoneableSpinCtrl( self, '', none_phrase = 'no size limit', min = 0, max = 20480 )
            
            delete_lock_panel = ClientGUICommon.StaticBox( self, 'delete lock' )
            
            self._delete_lock_for_archived_files = QW.QCheckBox( delete_lock_panel )
            
            advanced_file_deletion_panel = ClientGUICommon.StaticBox( self, 'advanced file deletion and custom reasons' )
            
            self._use_advanced_file_deletion_dialog = QW.QCheckBox( advanced_file_deletion_panel )
            self._use_advanced_file_deletion_dialog.setToolTip( 'If this is set, the client will present a more complicated file deletion confirmation dialog that will permit you to set your own deletion reason and perform \'clean\' deletes that leave no deletion record (making later re-import easier).' )
            
            self._remember_last_advanced_file_deletion_special_action = QW.QCheckBox( advanced_file_deletion_panel )
            self._remember_last_advanced_file_deletion_special_action.setToolTip( 'This will try to remember and restore the last action you set, whether that was trash, physical delete, or physical delete and clear history.')
            
            self._remember_last_advanced_file_deletion_reason = QW.QCheckBox( advanced_file_deletion_panel )
            self._remember_last_advanced_file_deletion_reason.setToolTip( 'This will remember and restore the last reason you set for a delete.' )
            
            self._advanced_file_deletion_reasons = ClientGUIListBoxes.QueueListBox( advanced_file_deletion_panel, 5, str, add_callable = self._AddAFDR, edit_callable = self._EditAFDR )
            
            #
            
            if HC.options[ 'export_path' ] is not None:
                
                abs_path = HydrusPaths.ConvertPortablePathToAbsPath( HC.options[ 'export_path' ] )
                
                if abs_path is not None:
                    
                    self._export_location.SetPath( abs_path )
                    
                
            
            self._prefix_hash_when_copying.setChecked( self._new_options.GetBoolean( 'prefix_hash_when_copying' ) )
            
            self._delete_to_recycle_bin.setChecked( HC.options[ 'delete_to_recycle_bin' ] )
            
            self._ms_to_wait_between_physical_file_deletes.setValue( self._new_options.GetInteger( 'ms_to_wait_between_physical_file_deletes' ) )
            
            self._confirm_trash.setChecked( HC.options[ 'confirm_trash' ] )
            tt = 'If there is only one place to delete the file from, you will get no delete dialog--it will just be deleted immediately. Applies the same way to undelete.'
            self._confirm_trash.setToolTip( tt )
            
            self._confirm_archive.setChecked( HC.options[ 'confirm_archive' ] )
            
            self._confirm_multiple_local_file_services_copy.setChecked( self._new_options.GetBoolean( 'confirm_multiple_local_file_services_copy' ) )
            self._confirm_multiple_local_file_services_move.setChecked( self._new_options.GetBoolean( 'confirm_multiple_local_file_services_move' ) )
            
            self._remove_filtered_files.setChecked( HC.options[ 'remove_filtered_files' ] )
            self._remove_trashed_files.setChecked( HC.options[ 'remove_trashed_files' ] )
            self._remove_local_domain_moved_files.setChecked( self._new_options.GetBoolean( 'remove_local_domain_moved_files' ) )
            self._trash_max_age.SetValue( HC.options[ 'trash_max_age' ] )
            self._trash_max_size.SetValue( HC.options[ 'trash_max_size' ] )
            
            self._delete_lock_for_archived_files.setChecked( self._new_options.GetBoolean( 'delete_lock_for_archived_files' ) )
            
            self._use_advanced_file_deletion_dialog.setChecked( self._new_options.GetBoolean( 'use_advanced_file_deletion_dialog' ) )
            
            self._use_advanced_file_deletion_dialog.clicked.connect( self._UpdateAdvancedControls )
            
            self._remember_last_advanced_file_deletion_special_action.setChecked( CG.client_controller.new_options.GetBoolean( 'remember_last_advanced_file_deletion_special_action' ) )
            self._remember_last_advanced_file_deletion_reason.setChecked( CG.client_controller.new_options.GetBoolean( 'remember_last_advanced_file_deletion_reason' ) )
            
            self._advanced_file_deletion_reasons.AddDatas( self._new_options.GetStringList( 'advanced_file_deletion_reasons' ) )
            
            self._UpdateAdvancedControls()
            
            #
            
            vbox = QP.VBoxLayout()
            
            text = 'If you set the default export directory blank, the client will use \'hydrus_export\' under the current user\'s home directory.'
            
            QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText(self,text), CC.FLAGS_CENTER )
            
            rows = []
            
            rows.append( ( 'When copying file hashes, prefix with booru-friendly hash type: ', self._prefix_hash_when_copying ) )
            rows.append( ( 'Confirm sending files to trash: ', self._confirm_trash ) )
            rows.append( ( 'Confirm sending more than one file to archive or inbox: ', self._confirm_archive ) )
            rows.append( ( 'Confirm when copying files across local file services: ', self._confirm_multiple_local_file_services_copy ) )
            rows.append( ( 'Confirm when moving files across local file services: ', self._confirm_multiple_local_file_services_move ) )
            rows.append( ( 'When physically deleting files or folders, send them to the OS\'s recycle bin: ', self._delete_to_recycle_bin ) )
            rows.append( ( 'When maintenance physically deletes files, wait this many ms between each delete: ', self._ms_to_wait_between_physical_file_deletes ) )
            rows.append( ( 'Remove files from view when they are filtered: ', self._remove_filtered_files ) )
            rows.append( ( 'Remove files from view when they are sent to the trash: ', self._remove_trashed_files ) )
            rows.append( ( 'Remove files from view when they are moved to another local file domain: ', self._remove_local_domain_moved_files ) )
            rows.append( ( 'Number of hours a file can be in the trash before being deleted: ', self._trash_max_age ) )
            rows.append( ( 'Maximum size of trash (MB): ', self._trash_max_size ) )
            rows.append( ( 'Default export directory: ', self._export_location ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Do not permit archived files to be trashed or deleted: ', self._delete_lock_for_archived_files ) )
            
            gridbox = ClientGUICommon.WrapInGrid( delete_lock_panel, rows )
            
            delete_lock_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, delete_lock_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Use the advanced file deletion dialog: ', self._use_advanced_file_deletion_dialog ) )
            rows.append( ( 'Remember the last action: ', self._remember_last_advanced_file_deletion_special_action ) )
            rows.append( ( 'Remember the last reason: ', self._remember_last_advanced_file_deletion_reason ) )
            
            gridbox = ClientGUICommon.WrapInGrid( advanced_file_deletion_panel, rows )
            
            advanced_file_deletion_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            advanced_file_deletion_panel.Add( self._advanced_file_deletion_reasons, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            QP.AddToLayout( vbox, advanced_file_deletion_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
        
        def _AddAFDR( self ):
            
            reason = 'I do not like the file.'
            
            return self._EditAFDR( reason )
            
        
        def _EditAFDR( self, reason ):
            
            with ClientGUIDialogs.DialogTextEntry( self, 'enter the reason', default = reason, allow_blank = False ) as dlg:
                
                if dlg.exec() == QW.QDialog.Accepted:
                    
                    reason = dlg.GetValue()
                    
                    return reason
                    
                else:
                    
                    raise HydrusExceptions.VetoException()
                    
                
            
        
        def _UpdateAdvancedControls( self ):
            
            advanced_enabled = self._use_advanced_file_deletion_dialog.isChecked()
            
            self._remember_last_advanced_file_deletion_special_action.setEnabled( advanced_enabled )
            self._remember_last_advanced_file_deletion_reason.setEnabled( advanced_enabled )
            self._advanced_file_deletion_reasons.setEnabled( advanced_enabled )
            
        
        def UpdateOptions( self ):
            
            HC.options[ 'export_path' ] = HydrusPaths.ConvertAbsPathToPortablePath( self._export_location.GetPath() )
            
            self._new_options.SetBoolean( 'prefix_hash_when_copying', self._prefix_hash_when_copying.isChecked() )
            
            HC.options[ 'delete_to_recycle_bin' ] = self._delete_to_recycle_bin.isChecked()
            HC.options[ 'confirm_trash' ] = self._confirm_trash.isChecked()
            HC.options[ 'confirm_archive' ] = self._confirm_archive.isChecked()
            HC.options[ 'remove_filtered_files' ] = self._remove_filtered_files.isChecked()
            HC.options[ 'remove_trashed_files' ] = self._remove_trashed_files.isChecked()
            self._new_options.SetBoolean( 'remove_local_domain_moved_files', self._remove_local_domain_moved_files.isChecked() )
            HC.options[ 'trash_max_age' ] = self._trash_max_age.GetValue()
            HC.options[ 'trash_max_size' ] = self._trash_max_size.GetValue()
            
            self._new_options.SetInteger( 'ms_to_wait_between_physical_file_deletes', self._ms_to_wait_between_physical_file_deletes.value() )
            
            self._new_options.SetBoolean( 'confirm_multiple_local_file_services_copy', self._confirm_multiple_local_file_services_copy.isChecked() )
            self._new_options.SetBoolean( 'confirm_multiple_local_file_services_move', self._confirm_multiple_local_file_services_move.isChecked() )
            
            self._new_options.SetBoolean( 'delete_lock_for_archived_files', self._delete_lock_for_archived_files.isChecked() )
            
            self._new_options.SetBoolean( 'use_advanced_file_deletion_dialog', self._use_advanced_file_deletion_dialog.isChecked() )
            
            self._new_options.SetStringList( 'advanced_file_deletion_reasons', self._advanced_file_deletion_reasons.GetData() )
            
            CG.client_controller.new_options.SetBoolean( 'remember_last_advanced_file_deletion_special_action', self._remember_last_advanced_file_deletion_special_action.isChecked() )
            CG.client_controller.new_options.SetBoolean( 'remember_last_advanced_file_deletion_reason', self._remember_last_advanced_file_deletion_reason.isChecked() )
            
        
    
    class _FileViewingStatisticsPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            self._file_viewing_statistics_active = QW.QCheckBox( self )
            self._file_viewing_statistics_active_on_archive_delete_filter = QW.QCheckBox( self )
            self._file_viewing_statistics_active_on_dupe_filter = QW.QCheckBox( self )
            self._file_viewing_statistics_media_min_time = ClientGUICommon.NoneableSpinCtrl( self )
            self._file_viewing_statistics_media_max_time = ClientGUICommon.NoneableSpinCtrl( self )
            max_tt = 'If you view a file for a very long time, the amount of viewtime recorded is clipped to this. This stops an outrageous viewtime being saved because you left something open in the background. If the media you view has duration, like a video, the max viewtime is five times its length or this, whichever is larger.'
            self._file_viewing_statistics_media_max_time.setToolTip( max_tt )
            
            self._file_viewing_statistics_preview_min_time = ClientGUICommon.NoneableSpinCtrl( self )
            self._file_viewing_statistics_preview_max_time = ClientGUICommon.NoneableSpinCtrl( self )
            self._file_viewing_statistics_preview_max_time.setToolTip( max_tt )
            
            self._file_viewing_stats_menu_display = ClientGUICommon.BetterChoice( self )
            
            self._file_viewing_stats_menu_display.addItem( 'do not show', CC.FILE_VIEWING_STATS_MENU_DISPLAY_NONE )
            self._file_viewing_stats_menu_display.addItem( 'show media', CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_ONLY )
            self._file_viewing_stats_menu_display.addItem( 'show media, and put preview in a submenu', CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_AND_PREVIEW_IN_SUBMENU )
            self._file_viewing_stats_menu_display.addItem( 'show media and preview in two lines', CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_AND_PREVIEW_STACKED )
            self._file_viewing_stats_menu_display.addItem( 'show media and preview combined', CC.FILE_VIEWING_STATS_MENU_DISPLAY_MEDIA_AND_PREVIEW_SUMMED )
            
            #
            
            self._file_viewing_statistics_active.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active' ) )
            self._file_viewing_statistics_active_on_archive_delete_filter.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter' ) )
            self._file_viewing_statistics_active_on_dupe_filter.setChecked( self._new_options.GetBoolean( 'file_viewing_statistics_active_on_dupe_filter' ) )
            self._file_viewing_statistics_media_min_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_media_min_time' ) )
            self._file_viewing_statistics_media_max_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_media_max_time' ) )
            self._file_viewing_statistics_preview_min_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_preview_min_time' ) )
            self._file_viewing_statistics_preview_max_time.SetValue( self._new_options.GetNoneableInteger( 'file_viewing_statistics_preview_max_time' ) )
            
            self._file_viewing_stats_menu_display.SetValue( self._new_options.GetInteger( 'file_viewing_stats_menu_display' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Enable file viewing statistics tracking?:', self._file_viewing_statistics_active ) )
            rows.append( ( 'Enable file viewing statistics tracking in the archive/delete filter?:', self._file_viewing_statistics_active_on_archive_delete_filter ) )
            rows.append( ( 'Enable file viewing statistics tracking in the duplicate filter?:', self._file_viewing_statistics_active_on_dupe_filter ) )
            rows.append( ( 'Min time to view on media viewer to count as a view (seconds):', self._file_viewing_statistics_media_min_time ) )
            rows.append( ( 'Cap any view on the media viewer to this maximum time (seconds):', self._file_viewing_statistics_media_max_time ) )
            rows.append( ( 'Min time to view on preview viewer to count as a view (seconds):', self._file_viewing_statistics_preview_min_time ) )
            rows.append( ( 'Cap any view on the preview viewer to this maximum time (seconds):', self._file_viewing_statistics_preview_max_time ) )
            rows.append( ( 'Show media/preview viewing stats on media right-click menus?:', self._file_viewing_stats_menu_display ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetBoolean( 'file_viewing_statistics_active', self._file_viewing_statistics_active.isChecked() )
            self._new_options.SetBoolean( 'file_viewing_statistics_active_on_archive_delete_filter', self._file_viewing_statistics_active_on_archive_delete_filter.isChecked() )
            self._new_options.SetBoolean( 'file_viewing_statistics_active_on_dupe_filter', self._file_viewing_statistics_active_on_dupe_filter.isChecked() )
            self._new_options.SetNoneableInteger( 'file_viewing_statistics_media_min_time', self._file_viewing_statistics_media_min_time.GetValue() )
            self._new_options.SetNoneableInteger( 'file_viewing_statistics_media_max_time', self._file_viewing_statistics_media_max_time.GetValue() )
            self._new_options.SetNoneableInteger( 'file_viewing_statistics_preview_min_time', self._file_viewing_statistics_preview_min_time.GetValue() )
            self._new_options.SetNoneableInteger( 'file_viewing_statistics_preview_max_time', self._file_viewing_statistics_preview_max_time.GetValue() )
            
            self._new_options.SetInteger( 'file_viewing_stats_menu_display', self._file_viewing_stats_menu_display.GetValue() )
            
        
    
    class _GUIPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._main_gui_panel = ClientGUICommon.StaticBox( self, 'main window' )
            
            self._app_display_name = QW.QLineEdit( self._main_gui_panel )
            self._app_display_name.setToolTip( 'This is placed in every window title, with current version name. Rename if you want to personalise or differentiate.' )
            
            self._confirm_client_exit = QW.QCheckBox( self._main_gui_panel )
            
            self._activate_window_on_tag_search_page_activation = QW.QCheckBox( self._main_gui_panel )
            
            tt = 'Middle-clicking one or more tags in a taglist will cause the creation of a new search page for those tags. If you do this from the media viewer or a child manage tags dialog, do you want to switch immediately to the main gui?'
            
            self._activate_window_on_tag_search_page_activation.setToolTip( tt )
            
            #
            
            self._misc_panel = ClientGUICommon.StaticBox( self, 'misc' )
            
            self._always_show_iso_time = QW.QCheckBox( self._misc_panel )
            tt = 'In many places across the program (typically import status lists), the client will state a timestamp as "5 days ago". If you would prefer a standard ISO string, like "2018-03-01 12:40:23", check this.'
            self._always_show_iso_time.setToolTip( tt )
            
            self._menu_choice_buttons_can_mouse_scroll = QW.QCheckBox( self._misc_panel )
            tt = 'Many buttons that produce menus when clicked are also "scrollable", so if you wheel your mouse over them, the selection will scroll through the underlying menu. If this is annoying for you, turn it off here!'
            self._menu_choice_buttons_can_mouse_scroll.setToolTip( tt )
            
            self._use_native_menubar = QW.QCheckBox( self._misc_panel )
            tt = 'macOS and some Linux allows to embed the main GUI menubar into the OS. This can be buggy! Requires restart.'
            self._use_native_menubar.setToolTip( tt )
            
            self._human_bytes_sig_figs = ClientGUICommon.BetterSpinBox( self._misc_panel, min = 1, max = 6 )
            self._human_bytes_sig_figs.setToolTip( 'When the program presents a bytes size above 1KB, like 21.3KB or 4.11GB, how many total digits do we want in the number? 2 or 3 is best.')
            
            self._discord_dnd_fix = QW.QCheckBox( self._misc_panel )
            self._discord_dnd_fix.setToolTip( 'This makes small file drag-and-drops a little laggier in exchange for Discord support. It also lets you set custom filenames for drag and drop exports.' )
            
            self._discord_dnd_filename_pattern = QW.QLineEdit( self._misc_panel )
            self._discord_dnd_filename_pattern.setToolTip( 'When the above is enabled, this export phrase will rename your files. If no filename can be generated, hash will be used instead.' )
            
            self._secret_discord_dnd_fix = QW.QCheckBox( self._misc_panel )
            self._secret_discord_dnd_fix.setToolTip( 'This saves the lag but is potentially dangerous, as it (may) treat the from-db-files-drag as a move rather than a copy and hence only works when the drop destination will not consume the files. It requires an additional secret Alternate key to unlock.' )
            
            self._do_macos_debug_dialog_menus = QW.QCheckBox( self._misc_panel )
            self._do_macos_debug_dialog_menus.setToolTip( 'There is a bug in Big Sur Qt regarding interacting with some menus in dialogs. The menus show but cannot be clicked. This shows the menu items in a debug dialog instead.' )
            
            self._use_qt_file_dialogs = QW.QCheckBox( self._misc_panel )
            self._use_qt_file_dialogs.setToolTip( 'If you get crashes opening file/directory dialogs, try this.' )
            
            #
            
            frame_locations_panel = ClientGUICommon.StaticBox( self, 'frame locations' )
            
            self._disable_get_safe_position_test = QW.QCheckBox( self._misc_panel )
            self._disable_get_safe_position_test.setToolTip( 'If your windows keep getting \'rescued\' despite being in a good location, try this.' )
            
            self._frame_locations = ClientGUIListCtrl.BetterListCtrl( frame_locations_panel, CGLC.COLUMN_LIST_FRAME_LOCATIONS.ID, 15, data_to_tuples_func = lambda x: (self._GetPrettyFrameLocationInfo( x ), self._GetPrettyFrameLocationInfo( x )), activation_callback = self.EditFrameLocations )
            
            self._frame_locations_edit_button = QW.QPushButton( 'edit', frame_locations_panel )
            self._frame_locations_edit_button.clicked.connect( self.EditFrameLocations )
            
            #
            
            self._new_options = CG.client_controller.new_options
            
            self._app_display_name.setText( self._new_options.GetString( 'app_display_name' ) )
            
            self._confirm_client_exit.setChecked( HC.options[ 'confirm_client_exit' ] )
            
            self._activate_window_on_tag_search_page_activation.setChecked( self._new_options.GetBoolean( 'activate_window_on_tag_search_page_activation' ) )
            
            self._always_show_iso_time.setChecked( self._new_options.GetBoolean( 'always_show_iso_time' ) )
            
            self._menu_choice_buttons_can_mouse_scroll.setChecked( self._new_options.GetBoolean( 'menu_choice_buttons_can_mouse_scroll' ) )
            
            self._use_native_menubar.setChecked( self._new_options.GetBoolean( 'use_native_menubar' ) )
            
            self._human_bytes_sig_figs.setValue( self._new_options.GetInteger( 'human_bytes_sig_figs' ) )
            
            self._discord_dnd_fix.setChecked( self._new_options.GetBoolean( 'discord_dnd_fix' ) )
            
            self._discord_dnd_filename_pattern.setText( self._new_options.GetString( 'discord_dnd_filename_pattern' ) )
            
            self._export_pattern_button = ClientGUICommon.ExportPatternButton( self )
            
            self._secret_discord_dnd_fix.setChecked( self._new_options.GetBoolean( 'secret_discord_dnd_fix' ) )
            
            self._do_macos_debug_dialog_menus.setChecked( self._new_options.GetBoolean( 'do_macos_debug_dialog_menus' ) )
            
            self._use_qt_file_dialogs.setChecked( self._new_options.GetBoolean( 'use_qt_file_dialogs' ) )
            
            self._disable_get_safe_position_test.setChecked( self._new_options.GetBoolean( 'disable_get_safe_position_test' ) )
            
            for ( name, info ) in self._new_options.GetFrameLocations():
                
                listctrl_list = QP.ListsToTuples( [ name ] + list( info ) )
                
                self._frame_locations.AddDatas( ( listctrl_list, ) )
                
            
            self._frame_locations.Sort()
            
            #
            
            rows = []
            
            rows.append( ( 'Application display name: ', self._app_display_name ) )
            rows.append( ( 'Confirm client exit: ', self._confirm_client_exit ) )
            rows.append( ( 'Switch to main window when opening tag search page from media viewer: ', self._activate_window_on_tag_search_page_activation ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._main_gui_panel, rows )
            
            self._main_gui_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Prefer ISO time ("2018-03-01 12:40:23") to "5 days ago": ', self._always_show_iso_time ) )
            rows.append( ( 'Mouse wheel can "scroll" through menu buttons: ', self._menu_choice_buttons_can_mouse_scroll ) )
            rows.append( ( 'Copy temp files for drag-and-drop (works for <=25, <200MB file DnDs--fixes Discord!): ', self._discord_dnd_fix ) )
            rows.append( ( 'Drag-and-drop export filename pattern: ', self._discord_dnd_filename_pattern ) )
            rows.append( ( '', self._export_pattern_button ) )
            rows.append( ( 'Use Native MenuBar (if available): ', self._use_native_menubar ) )
            rows.append( ( 'EXPERIMENTAL: Bytes strings >1KB pseudo significant figures: ', self._human_bytes_sig_figs ) )
            rows.append( ( 'EXPERIMENTAL BUGFIX: Secret discord file drag-and-drop fix: ', self._secret_discord_dnd_fix ) )
            rows.append( ( 'BUGFIX: If on macOS, show dialog menus in a debug menu: ', self._do_macos_debug_dialog_menus ) )
            rows.append( ( 'ANTI-CRASH BUGFIX: Use Qt file/directory selection dialogs, rather than OS native: ', self._use_qt_file_dialogs ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            self._misc_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            text = 'Here you can override the current and default values for many frame and dialog sizing and positioning variables.'
            text += os.linesep
            text += 'This is an advanced control. If you aren\'t confident of what you are doing here, come back later!'
            
            st = ClientGUICommon.BetterStaticText( frame_locations_panel, label = text )
            st.setWordWrap( True )
            
            frame_locations_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'BUGFIX: Disable off-screen window rescue: ', self._disable_get_safe_position_test ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            frame_locations_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            frame_locations_panel.Add( self._frame_locations, CC.FLAGS_EXPAND_BOTH_WAYS )
            frame_locations_panel.Add( self._frame_locations_edit_button, CC.FLAGS_ON_RIGHT )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._main_gui_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            QP.AddToLayout( vbox, self._misc_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            QP.AddToLayout( vbox, frame_locations_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
            self._discord_dnd_fix.clicked.connect( self._UpdateDnDFilenameEnabled )
            
            self._UpdateDnDFilenameEnabled()
            
        
        def _GetPrettyFrameLocationInfo( self, listctrl_list ):
            
            pretty_listctrl_list = []
            
            for item in listctrl_list:
                
                pretty_listctrl_list.append( str( item ) )
                
            
            return pretty_listctrl_list
            
        
        def _UpdateDnDFilenameEnabled( self ):
            
            enabled = self._discord_dnd_fix.isChecked()
            
            self._discord_dnd_filename_pattern.setEnabled( enabled )
            self._export_pattern_button.setEnabled( enabled )
            
        
        def EditFrameLocations( self ):
            
            for listctrl_list in self._frame_locations.GetData( only_selected = True ):
                
                title = 'set frame location information'
                
                with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg:
                    
                    panel = ClientGUIScrolledPanelsEdit.EditFrameLocationPanel( dlg, listctrl_list )
                    
                    dlg.SetPanel( panel )
                    
                    if dlg.exec() == QW.QDialog.Accepted:
                        
                        new_listctrl_list = panel.GetValue()
                        
                        self._frame_locations.ReplaceData( listctrl_list, new_listctrl_list )
                        
                    
                
            
        
        def UpdateOptions( self ):
            
            HC.options[ 'confirm_client_exit' ] = self._confirm_client_exit.isChecked()
            
            self._new_options.SetBoolean( 'always_show_iso_time', self._always_show_iso_time.isChecked() )
            self._new_options.SetBoolean( 'menu_choice_buttons_can_mouse_scroll', self._menu_choice_buttons_can_mouse_scroll.isChecked() )
            self._new_options.SetBoolean( 'use_native_menubar', self._use_native_menubar.isChecked() )
            
            self._new_options.SetInteger( 'human_bytes_sig_figs', self._human_bytes_sig_figs.value() )
            
            self._new_options.SetBoolean( 'activate_window_on_tag_search_page_activation', self._activate_window_on_tag_search_page_activation.isChecked() )
            
            app_display_name = self._app_display_name.text()
            
            if app_display_name == '':
                
                app_display_name = 'hydrus client'
                
            
            self._new_options.SetString( 'app_display_name', app_display_name )
            
            self._new_options.SetBoolean( 'discord_dnd_fix', self._discord_dnd_fix.isChecked() )
            self._new_options.SetString( 'discord_dnd_filename_pattern', self._discord_dnd_filename_pattern.text() )
            self._new_options.SetBoolean( 'secret_discord_dnd_fix', self._secret_discord_dnd_fix.isChecked() )
            self._new_options.SetBoolean( 'do_macos_debug_dialog_menus', self._do_macos_debug_dialog_menus.isChecked() )
            self._new_options.SetBoolean( 'use_qt_file_dialogs', self._use_qt_file_dialogs.isChecked() )
            
            self._new_options.SetBoolean( 'disable_get_safe_position_test', self._disable_get_safe_position_test.isChecked() )
            
            for listctrl_list in self._frame_locations.GetData():
                
                ( name, remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen ) = listctrl_list
                
                self._new_options.SetFrameLocation( name, remember_size, remember_position, last_size, last_position, default_gravity, default_position, maximised, fullscreen )
                
            
        
    
    class _GUIPagesPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            self._sessions_panel = ClientGUICommon.StaticBox( self, 'sessions' )
            
            self._default_gui_session = QW.QComboBox( self._sessions_panel )
            
            self._last_session_save_period_minutes = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 1440 )
            
            self._only_save_last_session_during_idle = QW.QCheckBox( self._sessions_panel )
            
            self._only_save_last_session_during_idle.setToolTip( 'This is useful if you usually have a very large session (200,000+ files/import items open) and a client that is always on.' )
            
            self._number_of_gui_session_backups = ClientGUICommon.BetterSpinBox( self._sessions_panel, min = 1, max = 32 )
            
            self._number_of_gui_session_backups.setToolTip( 'The client keeps multiple rolling backups of your gui sessions. If you have very large sessions, you might like to reduce this number.' )
            
            self._show_session_size_warnings = QW.QCheckBox( self._sessions_panel )
            
            self._show_session_size_warnings.setToolTip( 'This will give you a once-per-boot warning popup if your active session contains more than 10M weight.' )
            
            #
            
            self._pages_panel = ClientGUICommon.StaticBox( self, 'pages' )
            
            self._default_new_page_goes = ClientGUICommon.BetterChoice( self._pages_panel )
            
            for value in [ CC.NEW_PAGE_GOES_FAR_LEFT, CC.NEW_PAGE_GOES_LEFT_OF_CURRENT, CC.NEW_PAGE_GOES_RIGHT_OF_CURRENT, CC.NEW_PAGE_GOES_FAR_RIGHT ]:
                
                self._default_new_page_goes.addItem( CC.new_page_goes_string_lookup[ value], value )
                
            
            self._notebook_tab_alignment = ClientGUICommon.BetterChoice( self._pages_panel )
            
            for value in [ CC.DIRECTION_UP, CC.DIRECTION_LEFT, CC.DIRECTION_RIGHT, CC.DIRECTION_DOWN ]:
                
                self._notebook_tab_alignment.addItem( CC.directions_alignment_string_lookup[ value ], value )
                
            
            self._total_pages_warning = ClientGUICommon.BetterSpinBox( self._pages_panel, min=5, max=65565 )
            
            tt = 'If you have a gigantic session, or you have very page-spammy subscriptions, you can try boosting this, but be warned it may lead to resource limit crashes. The best solution to a large session is to make it smaller!'
            
            self._total_pages_warning.setToolTip( tt )
            
            self._page_drop_chase_normally = QW.QCheckBox( self._pages_panel )
            self._page_drop_chase_normally.setToolTip( 'When you drop a page to a new location, should hydrus follow the page selection to the new location?' )
            self._page_drop_chase_with_shift = QW.QCheckBox( self._pages_panel )
            self._page_drop_chase_with_shift.setToolTip( 'When you drop a page to a new location with shift held down, should hydrus follow the page selection to the new location?' )
            
            self._page_drag_change_tab_normally = QW.QCheckBox( self._pages_panel )
            self._page_drag_change_tab_normally.setToolTip( 'When you drag media or a page to a new location, should hydrus navigate and change tabs as you move the mouse around?' )
            self._page_drag_change_tab_with_shift = QW.QCheckBox( self._pages_panel )
            self._page_drag_change_tab_with_shift.setToolTip( 'When you drag media or a page to a new location with shift held down, should hydrus navigate and change tabs as you move the mouse around?' )
            
            self._wheel_scrolls_tab_bar = QW.QCheckBox( self._pages_panel )
            self._wheel_scrolls_tab_bar.setToolTip( 'When you scroll your mouse wheel over some tabs, the normal behaviour is to change the tab selection. If you often have overloaded tab bars, you might like to have the mouse wheel actually scroll the tab bar itself.' )
            
            self._disable_page_tab_dnd = QW.QCheckBox( self._pages_panel )
            
            self._disable_page_tab_dnd.setToolTip( 'Trying to debug some client hangs!' )
            
            self._force_hide_page_signal_on_new_page = QW.QCheckBox( self._pages_panel )
            
            self._force_hide_page_signal_on_new_page.setToolTip( 'If your video still plays with sound in the preview viewer when you create a new page, please try this.' )
            
            #
            
            self._page_names_panel = ClientGUICommon.StaticBox( self._pages_panel, 'page tab names' )
            
            self._max_page_name_chars = ClientGUICommon.BetterSpinBox( self._page_names_panel, min=1, max=256 )
            self._elide_page_tab_names = QW.QCheckBox( self._page_names_panel )
            
            self._page_file_count_display = ClientGUICommon.BetterChoice( self._page_names_panel )
            
            for display_type in ( CC.PAGE_FILE_COUNT_DISPLAY_ALL, CC.PAGE_FILE_COUNT_DISPLAY_ONLY_IMPORTERS, CC.PAGE_FILE_COUNT_DISPLAY_NONE ):
                
                self._page_file_count_display.addItem( CC.page_file_count_display_string_lookup[ display_type], display_type )
                
            
            self._import_page_progress_display = QW.QCheckBox( self._page_names_panel )
            
            #
            
            self._controls_panel = ClientGUICommon.StaticBox( self, 'controls and preview' )
            
            self._set_search_focus_on_page_change = QW.QCheckBox( self._controls_panel )
            
            self._hide_preview = QW.QCheckBox( self._controls_panel )
            
            #
            
            gui_session_names = CG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_GUI_SESSION_CONTAINER )
            
            if CC.LAST_SESSION_SESSION_NAME not in gui_session_names:
                
                gui_session_names.insert( 0, CC.LAST_SESSION_SESSION_NAME )
                
            
            self._default_gui_session.addItem( 'just a blank page', None )
            
            for name in gui_session_names:
                
                self._default_gui_session.addItem( name, name )
                
            
            try:
                
                QP.SetStringSelection( self._default_gui_session, HC.options['default_gui_session'] )
                
            except:
                
                self._default_gui_session.setCurrentIndex( 0 )
                
            
            self._last_session_save_period_minutes.setValue( self._new_options.GetInteger( 'last_session_save_period_minutes' ) )
            
            self._only_save_last_session_during_idle.setChecked( self._new_options.GetBoolean( 'only_save_last_session_during_idle' ) )
            
            self._number_of_gui_session_backups.setValue( self._new_options.GetInteger( 'number_of_gui_session_backups' ) )
            
            self._show_session_size_warnings.setChecked( self._new_options.GetBoolean( 'show_session_size_warnings' ) )
            
            self._default_new_page_goes.SetValue( self._new_options.GetInteger( 'default_new_page_goes' ) )
            
            self._notebook_tab_alignment.SetValue( self._new_options.GetInteger( 'notebook_tab_alignment' ) )
            
            self._max_page_name_chars.setValue( self._new_options.GetInteger( 'max_page_name_chars' ) )
            
            self._elide_page_tab_names.setChecked( self._new_options.GetBoolean( 'elide_page_tab_names' ) )
            
            self._page_file_count_display.SetValue( self._new_options.GetInteger( 'page_file_count_display' ) )
            
            self._import_page_progress_display.setChecked( self._new_options.GetBoolean( 'import_page_progress_display' ) )
            
            self._page_drop_chase_normally.setChecked( self._new_options.GetBoolean( 'page_drop_chase_normally' ) )
            self._page_drop_chase_with_shift.setChecked( self._new_options.GetBoolean( 'page_drop_chase_with_shift' ) )
            self._page_drag_change_tab_normally.setChecked( self._new_options.GetBoolean( 'page_drag_change_tab_normally' ) )
            self._page_drag_change_tab_with_shift.setChecked( self._new_options.GetBoolean( 'page_drag_change_tab_with_shift' ) )
            
            self._wheel_scrolls_tab_bar.setChecked( self._new_options.GetBoolean( 'wheel_scrolls_tab_bar' ) )
            
            self._total_pages_warning.setValue( self._new_options.GetInteger( 'total_pages_warning' ) )
            
            self._disable_page_tab_dnd.setChecked( self._new_options.GetBoolean( 'disable_page_tab_dnd' ) )
            
            self._force_hide_page_signal_on_new_page.setChecked( self._new_options.GetBoolean( 'force_hide_page_signal_on_new_page' ) )
            
            self._set_search_focus_on_page_change.setChecked( self._new_options.GetBoolean( 'set_search_focus_on_page_change' ) )
            
            self._hide_preview.setChecked( HC.options[ 'hide_preview' ] )
            
            #
            
            rows = []
            
            rows.append( ( 'Default session on startup: ', self._default_gui_session ) )
            rows.append( ( 'If \'last session\' above, autosave it how often (minutes)?', self._last_session_save_period_minutes ) )
            rows.append( ( 'If \'last session\' above, only autosave during idle time?', self._only_save_last_session_during_idle ) )
            rows.append( ( 'Number of session backups to keep: ', self._number_of_gui_session_backups ) )
            rows.append( ( 'Show warning popup if session size exceeds 10,000,000: ', self._show_session_size_warnings ) )
            
            sessions_gridbox = ClientGUICommon.WrapInGrid( self._sessions_panel, rows )
            
            self._sessions_panel.Add( sessions_gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
            
            rows = []
            
            rows.append( ( 'By default, put new page tabs on: ', self._default_new_page_goes ) )
            rows.append( ( 'Notebook tab alignment: ', self._notebook_tab_alignment ) )
            rows.append( ( 'Selection chases dropped page after drag and drop: ', self._page_drop_chase_normally ) )
            rows.append( ( '  With shift held down?: ', self._page_drop_chase_with_shift ) )
            rows.append( ( 'Navigate tabs during drag and drop: ', self._page_drag_change_tab_normally ) )
            rows.append( ( '  With shift held down?: ', self._page_drag_change_tab_with_shift ) )
            rows.append( ( 'EXPERIMENTAL: Mouse wheel scrolls tab bar, not page selection: ', self._wheel_scrolls_tab_bar ) )
            rows.append( ( 'Warn at this many total pages: ', self._total_pages_warning ) )
            rows.append( ( 'BUGFIX: Disable all page tab drag and drop: ', self._disable_page_tab_dnd ) )
            rows.append( ( 'BUGFIX: Force \'hide page\' signal when creating a new page: ', self._force_hide_page_signal_on_new_page ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._pages_panel, rows )
            
            rows = []
            
            rows.append( ( 'Max characters to display in a page name: ', self._max_page_name_chars ) )
            rows.append( ( 'When there are too many tabs to fit, \'...\' elide their names so they fit: ', self._elide_page_tab_names ) )
            rows.append( ( 'Show page file count after its name: ', self._page_file_count_display ) )
            rows.append( ( 'Show import page x/y progress after its name: ', self._import_page_progress_display ) )
            
            page_names_gridbox = ClientGUICommon.WrapInGrid( self._page_names_panel, rows )
            
            label = 'If you have enough pages in a row, left/right arrows will appear to navigate them back and forth.'
            label += os.linesep
            label += 'Due to an unfortunate Qt issue, the tab bar will scroll so the current tab is right-most visible whenever you change page or a page is renamed. This is very annoying to live with.'
            label += os.linesep
            label += 'Therefore, do not put import pages in a long row of tabs, as it will reset scroll position on every progress update. Try to avoid long rows in general.'
            label += os.linesep
            label += 'Just make some nested \'page of pages\' so they are not all in the same row.'
            
            st = ClientGUICommon.BetterStaticText( self._page_names_panel, label )
            
            st.setToolTip( 'https://bugreports.qt.io/browse/QTBUG-45381' )
            
            st.setWordWrap( True )
            
            self._page_names_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            self._page_names_panel.Add( page_names_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            self._pages_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            self._pages_panel.Add( self._page_names_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'When switching to a page, focus its text input field (if any): ', self._set_search_focus_on_page_change ) )
            rows.append( ( 'Hide the bottom-left preview window: ', self._hide_preview ) )
            gridbox = ClientGUICommon.WrapInGrid( self._controls_panel, rows )
            
            self._controls_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._sessions_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            QP.AddToLayout( vbox, self._pages_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._controls_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            HC.options[ 'default_gui_session' ] = self._default_gui_session.currentText()
            
            self._new_options.SetInteger( 'notebook_tab_alignment', self._notebook_tab_alignment.GetValue() )
            
            self._new_options.SetInteger( 'last_session_save_period_minutes', self._last_session_save_period_minutes.value() )
            
            self._new_options.SetInteger( 'number_of_gui_session_backups', self._number_of_gui_session_backups.value() )
            
            self._new_options.SetBoolean( 'show_session_size_warnings', self._show_session_size_warnings.isChecked() )
            
            self._new_options.SetBoolean( 'only_save_last_session_during_idle', self._only_save_last_session_during_idle.isChecked() )
            
            self._new_options.SetInteger( 'default_new_page_goes', self._default_new_page_goes.GetValue() )
            
            self._new_options.SetInteger( 'max_page_name_chars', self._max_page_name_chars.value() )
            
            self._new_options.SetBoolean( 'elide_page_tab_names', self._elide_page_tab_names.isChecked() )
            
            self._new_options.SetInteger( 'page_file_count_display', self._page_file_count_display.GetValue() )
            self._new_options.SetBoolean( 'import_page_progress_display', self._import_page_progress_display.isChecked() )
            
            self._new_options.SetInteger( 'total_pages_warning', self._total_pages_warning.value() )
            
            self._new_options.SetBoolean( 'disable_page_tab_dnd', self._disable_page_tab_dnd.isChecked() )
            self._new_options.SetBoolean( 'force_hide_page_signal_on_new_page', self._force_hide_page_signal_on_new_page.isChecked() )
            
            self._new_options.SetBoolean( 'page_drop_chase_normally', self._page_drop_chase_normally.isChecked() )
            self._new_options.SetBoolean( 'page_drop_chase_with_shift', self._page_drop_chase_with_shift.isChecked() )
            self._new_options.SetBoolean( 'page_drag_change_tab_normally', self._page_drag_change_tab_normally.isChecked() )
            self._new_options.SetBoolean( 'page_drag_change_tab_with_shift', self._page_drag_change_tab_with_shift.isChecked() )
            
            self._new_options.SetBoolean( 'wheel_scrolls_tab_bar', self._wheel_scrolls_tab_bar.isChecked() )
            
            self._new_options.SetBoolean( 'set_search_focus_on_page_change', self._set_search_focus_on_page_change.isChecked() )
            
            HC.options[ 'hide_preview' ] = self._hide_preview.isChecked()
            
        
    
    class _ImportingPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            default_fios = ClientGUICommon.StaticBox( self, 'default file import options' )
            
            show_downloader_options = True
            
            quiet_file_import_options = self._new_options.GetDefaultFileImportOptions( FileImportOptions.IMPORT_TYPE_QUIET )
            
            show_downloader_options = True
            allow_default_selection = False
            
            self._quiet_fios = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
            
            self._quiet_fios.SetFileImportOptions( quiet_file_import_options )
            
            loud_file_import_options = self._new_options.GetDefaultFileImportOptions( FileImportOptions.IMPORT_TYPE_LOUD )
            
            self._loud_fios = ClientGUIImportOptions.ImportOptionsButton( self, show_downloader_options, allow_default_selection )
            
            self._loud_fios.SetFileImportOptions( loud_file_import_options )
            
            #
            
            rows = []
            
            rows.append( ( 'For \'quiet\' import contexts: import folders, subscriptions, Client API:', self._quiet_fios ) )
            rows.append( ( 'For \'loud\' import contexts: downloader pages:', self._loud_fios ) )
            
            gridbox = ClientGUICommon.WrapInGrid( default_fios, rows )
            
            default_fios.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, default_fios, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetDefaultFileImportOptions( FileImportOptions.IMPORT_TYPE_QUIET, self._quiet_fios.GetFileImportOptions() )
            self._new_options.SetDefaultFileImportOptions( FileImportOptions.IMPORT_TYPE_LOUD, self._loud_fios.GetFileImportOptions() )
            
        
    
    class _MaintenanceAndProcessingPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            self._jobs_panel = ClientGUICommon.StaticBox( self, 'when to run high cpu jobs' )
            
            #
            
            self._idle_panel = ClientGUICommon.StaticBox( self._jobs_panel, 'idle' )
            
            self._idle_normal = QW.QCheckBox( self._idle_panel )
            self._idle_normal.clicked.connect( self._EnableDisableIdleNormal )
            
            self._idle_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore normal browsing' )
            self._idle_mouse_period = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore mouse movements' )
            self._idle_mode_client_api_timeout = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, '', min = 1, max = 1000, multiplier = 60, unit = 'minutes', none_phrase = 'ignore client api' )
            self._system_busy_cpu_percent = ClientGUICommon.BetterSpinBox( self._idle_panel, min = 5, max = 99 )
            self._system_busy_cpu_count = ClientGUICommon.NoneableSpinCtrl( self._idle_panel, min = 1, max = 64, unit = 'cores', none_phrase = 'ignore cpu usage' )
            
            #
            
            self._shutdown_panel = ClientGUICommon.StaticBox( self._jobs_panel, 'shutdown' )
            
            self._idle_shutdown = ClientGUICommon.BetterChoice( self._shutdown_panel )
            
            for idle_id in ( CC.IDLE_NOT_ON_SHUTDOWN, CC.IDLE_ON_SHUTDOWN, CC.IDLE_ON_SHUTDOWN_ASK_FIRST ):
                
                self._idle_shutdown.addItem( CC.idle_string_lookup[ idle_id], idle_id )
                
            
            self._idle_shutdown.currentIndexChanged.connect( self._EnableDisableIdleShutdown )
            
            self._idle_shutdown_max_minutes = ClientGUICommon.BetterSpinBox( self._shutdown_panel, min=1, max=1440 )
            self._shutdown_work_period = ClientGUITime.TimeDeltaButton( self._shutdown_panel, min = 60, days = True, hours = True, minutes = True )
            
            #
            
            self._file_maintenance_panel = ClientGUICommon.StaticBox( self, 'file maintenance' )
            
            min_unit_value = 1
            max_unit_value = 1000
            min_time_delta = 1
            
            self._file_maintenance_during_idle = QW.QCheckBox( self._file_maintenance_panel )
            
            self._file_maintenance_idle_throttle_velocity = ClientGUITime.VelocityCtrl( self._file_maintenance_panel, min_unit_value, max_unit_value, min_time_delta, minutes = True, seconds = True, per_phrase = 'every', unit = 'heavy work units' )
            
            self._file_maintenance_during_active = QW.QCheckBox( self._file_maintenance_panel )
            
            self._file_maintenance_active_throttle_velocity = ClientGUITime.VelocityCtrl( self._file_maintenance_panel, min_unit_value, max_unit_value, min_time_delta, minutes = True, seconds = True, per_phrase = 'every', unit = 'heavy work units' )
            
            tt = 'Different jobs will count for more or less weight. A file metadata reparse will count as one work unit, but quicker jobs like checking for file presence will count as fractions of one and will will work more frequently.'
            tt += os.linesep * 2
            tt += 'Please note that this throttle is not rigorous for long timescales, as file processing history is not currently saved on client exit. If you restart the client, the file manager thinks it has run 0 jobs and will be happy to run until the throttle kicks in again.'
            
            self._file_maintenance_idle_throttle_velocity.setToolTip( tt )
            self._file_maintenance_active_throttle_velocity.setToolTip( tt )
            
            #
            
            self._repository_processing_panel = ClientGUICommon.StaticBox( self, 'repository processing' )
            
            self._repository_processing_work_time_very_idle = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. Very Idle is after an hour of idle mode.'
            self._repository_processing_work_time_very_idle.setToolTip( tt )
            
            self._repository_processing_rest_percentage_very_idle = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. Very Idle is after an hour of idle mode.'
            self._repository_processing_rest_percentage_very_idle.setToolTip( tt )
            
            self._repository_processing_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
            self._repository_processing_work_time_idle.setToolTip( tt )
            
            self._repository_processing_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
            self._repository_processing_rest_percentage_idle.setToolTip( tt )
            
            self._repository_processing_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._repository_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
            self._repository_processing_work_time_normal.setToolTip( tt )
            
            self._repository_processing_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._repository_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Repository processing operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
            self._repository_processing_rest_percentage_normal.setToolTip( tt )
            
            #
            
            self._tag_display_processing_panel = ClientGUICommon.StaticBox( self, 'sibling/parent sync processing' )
            
            self._tag_display_maintenance_during_idle = QW.QCheckBox( self._tag_display_processing_panel )
            self._tag_display_maintenance_during_active = QW.QCheckBox( self._tag_display_processing_panel )
            tt = 'This can be a real killer. If you are catching up with the PTR and notice a lot of lag bumps, sometimes several seconds long, try turning this off.'
            self._tag_display_maintenance_during_active.setToolTip( tt )
            
            self._tag_display_processing_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
            self._tag_display_processing_work_time_idle.setToolTip( tt )
            
            self._tag_display_processing_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
            self._tag_display_processing_rest_percentage_idle.setToolTip( tt )
            
            self._tag_display_processing_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
            self._tag_display_processing_work_time_normal.setToolTip( tt )
            
            self._tag_display_processing_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
            self._tag_display_processing_rest_percentage_normal.setToolTip( tt )
            
            self._tag_display_processing_work_time_work_hard = ClientGUITime.TimeDeltaCtrl( self._tag_display_processing_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force it to work hard through the dialog.'
            self._tag_display_processing_work_time_work_hard.setToolTip( tt )
            
            self._tag_display_processing_rest_percentage_work_hard = ClientGUICommon.BetterSpinBox( self._tag_display_processing_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Sibling/parent sync operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force it to work hard through the dialog.'
            self._tag_display_processing_rest_percentage_work_hard.setToolTip( tt )
            
            #
            
            self._duplicates_panel = ClientGUICommon.StaticBox( self, 'potential duplicates search' )
            
            self._maintain_similar_files_duplicate_pairs_during_idle = QW.QCheckBox( self._duplicates_panel )
            
            self._potential_duplicates_search_work_time = ClientGUITime.TimeDeltaCtrl( self._duplicates_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Potential search operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this, and on large databases the minimum work time may be upwards of several seconds.'
            self._potential_duplicates_search_work_time.setToolTip( tt )
            
            self._potential_duplicates_search_rest_percentage = ClientGUICommon.BetterSpinBox( self._duplicates_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Potential search operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, as a percentage of the last work time.'
            self._potential_duplicates_search_rest_percentage.setToolTip( tt )
            
            #
            
            self._deferred_table_delete_panel = ClientGUICommon.StaticBox( self, 'deferred table delete' )
            
            self._deferred_table_delete_work_time_idle = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for idle mode.'
            self._deferred_table_delete_work_time_idle.setToolTip( tt )
            
            self._deferred_table_delete_rest_percentage_idle = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for idle mode.'
            self._deferred_table_delete_rest_percentage_idle.setToolTip( tt )
            
            self._deferred_table_delete_work_time_normal = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force-start work from review services.'
            self._deferred_table_delete_work_time_normal.setToolTip( tt )
            
            self._deferred_table_delete_rest_percentage_normal = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force-start work from review services.'
            self._deferred_table_delete_rest_percentage_normal.setToolTip( tt )
            
            self._deferred_table_delete_work_time_work_hard = ClientGUITime.TimeDeltaCtrl( self._deferred_table_delete_panel, min = 0.1, seconds = True, milliseconds = True )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should work for in each work packet. Actual work time will normally be a little larger than this. This is for when you force it to work hard through the dialog.'
            self._deferred_table_delete_work_time_work_hard.setToolTip( tt )
            
            self._deferred_table_delete_rest_percentage_work_hard = ClientGUICommon.BetterSpinBox( self._deferred_table_delete_panel, min = 0, max = 100000 )
            tt = 'DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING. Deferred table delete operates on a work-rest cycle. This setting determines how long it should wait before starting a new work packet, in multiples of the last work time. This is for when you force it to work hard through the dialog.'
            self._deferred_table_delete_rest_percentage_work_hard.setToolTip( tt )
            
            #
            
            self._idle_normal.setChecked( HC.options[ 'idle_normal' ] )
            self._idle_period.SetValue( HC.options['idle_period'] )
            self._idle_mouse_period.SetValue( HC.options['idle_mouse_period'] )
            self._idle_mode_client_api_timeout.SetValue( self._new_options.GetNoneableInteger( 'idle_mode_client_api_timeout' ) )
            self._system_busy_cpu_percent.setValue( self._new_options.GetInteger( 'system_busy_cpu_percent' ) )
            self._system_busy_cpu_count.SetValue( self._new_options.GetNoneableInteger( 'system_busy_cpu_count' ) )
            
            self._idle_shutdown.SetValue( HC.options[ 'idle_shutdown' ] )
            self._idle_shutdown_max_minutes.setValue( HC.options['idle_shutdown_max_minutes'] )
            self._shutdown_work_period.SetValue( self._new_options.GetInteger( 'shutdown_work_period' ) )
            
            self._file_maintenance_during_idle.setChecked( self._new_options.GetBoolean( 'file_maintenance_during_idle' ) )
            
            file_maintenance_idle_throttle_files = self._new_options.GetInteger( 'file_maintenance_idle_throttle_files' )
            file_maintenance_idle_throttle_time_delta = self._new_options.GetInteger( 'file_maintenance_idle_throttle_time_delta' )
            
            file_maintenance_idle_throttle_velocity = ( file_maintenance_idle_throttle_files, file_maintenance_idle_throttle_time_delta )
            
            self._file_maintenance_idle_throttle_velocity.SetValue( file_maintenance_idle_throttle_velocity )
            
            self._file_maintenance_during_active.setChecked( self._new_options.GetBoolean( 'file_maintenance_during_active' ) )
            
            file_maintenance_active_throttle_files = self._new_options.GetInteger( 'file_maintenance_active_throttle_files' )
            file_maintenance_active_throttle_time_delta = self._new_options.GetInteger( 'file_maintenance_active_throttle_time_delta' )
            
            file_maintenance_active_throttle_velocity = ( file_maintenance_active_throttle_files, file_maintenance_active_throttle_time_delta )
            
            self._file_maintenance_active_throttle_velocity.SetValue( file_maintenance_active_throttle_velocity )
            
            self._repository_processing_work_time_very_idle.SetValue( self._new_options.GetInteger( 'repository_processing_work_time_ms_very_idle' ) / 1000 )
            self._repository_processing_rest_percentage_very_idle.setValue( self._new_options.GetInteger( 'repository_processing_rest_percentage_very_idle' ) )
            
            self._repository_processing_work_time_idle.SetValue( self._new_options.GetInteger( 'repository_processing_work_time_ms_idle' ) / 1000 )
            self._repository_processing_rest_percentage_idle.setValue( self._new_options.GetInteger( 'repository_processing_rest_percentage_idle' ) )
            
            self._repository_processing_work_time_normal.SetValue( self._new_options.GetInteger( 'repository_processing_work_time_ms_normal' ) / 1000 )
            self._repository_processing_rest_percentage_normal.setValue( self._new_options.GetInteger( 'repository_processing_rest_percentage_normal' ) )
            
            self._tag_display_maintenance_during_idle.setChecked( self._new_options.GetBoolean( 'tag_display_maintenance_during_idle' ) )
            self._tag_display_maintenance_during_active.setChecked( self._new_options.GetBoolean( 'tag_display_maintenance_during_active' ) )
            
            self._tag_display_processing_work_time_idle.SetValue( self._new_options.GetInteger( 'tag_display_processing_work_time_ms_idle' ) / 1000 )
            self._tag_display_processing_rest_percentage_idle.setValue( self._new_options.GetInteger( 'tag_display_processing_rest_percentage_idle' ) )
            
            self._tag_display_processing_work_time_normal.SetValue( self._new_options.GetInteger( 'tag_display_processing_work_time_ms_normal' ) / 1000 )
            self._tag_display_processing_rest_percentage_normal.setValue( self._new_options.GetInteger( 'tag_display_processing_rest_percentage_normal' ) )
            
            self._tag_display_processing_work_time_work_hard.SetValue( self._new_options.GetInteger( 'tag_display_processing_work_time_ms_work_hard' ) / 1000 )
            self._tag_display_processing_rest_percentage_work_hard.setValue( self._new_options.GetInteger( 'tag_display_processing_rest_percentage_work_hard' ) )
            
            self._maintain_similar_files_duplicate_pairs_during_idle.setChecked( self._new_options.GetBoolean( 'maintain_similar_files_duplicate_pairs_during_idle' ) )
            self._potential_duplicates_search_work_time.SetValue( self._new_options.GetInteger( 'potential_duplicates_search_work_time_ms' ) / 1000 )
            self._potential_duplicates_search_rest_percentage.setValue( self._new_options.GetInteger( 'potential_duplicates_search_rest_percentage' ) )
            
            self._deferred_table_delete_work_time_idle.SetValue( self._new_options.GetInteger( 'deferred_table_delete_work_time_ms_idle' ) / 1000 )
            self._deferred_table_delete_rest_percentage_idle.setValue( self._new_options.GetInteger( 'deferred_table_delete_rest_percentage_idle' ) )
            
            self._deferred_table_delete_work_time_normal.SetValue( self._new_options.GetInteger( 'deferred_table_delete_work_time_ms_normal' ) / 1000 )
            self._deferred_table_delete_rest_percentage_normal.setValue( self._new_options.GetInteger( 'deferred_table_delete_rest_percentage_normal' ) )
            
            self._deferred_table_delete_work_time_work_hard.SetValue( self._new_options.GetInteger( 'deferred_table_delete_work_time_ms_work_hard' ) / 1000 )
            self._deferred_table_delete_rest_percentage_work_hard.setValue( self._new_options.GetInteger( 'deferred_table_delete_rest_percentage_work_hard' ) )
            
            #
            
            rows = []
            
            rows.append( ( 'Run maintenance jobs when the client is idle and the system is not otherwise busy: ', self._idle_normal ) )
            rows.append( ( 'Permit idle mode if no general browsing activity has occurred in the past: ', self._idle_period ) )
            rows.append( ( 'Permit idle mode if the mouse has not been moved in the past: ', self._idle_mouse_period ) )
            rows.append( ( 'Permit idle mode if no Client API requests in the past: ', self._idle_mode_client_api_timeout ) )
            
            hbox = QP.HBoxLayout()
            
            QP.AddToLayout( hbox, self._system_busy_cpu_percent, CC.FLAGS_CENTER )
            QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._idle_panel, label = '% on ' ), CC.FLAGS_CENTER )
            QP.AddToLayout( hbox, self._system_busy_cpu_count, CC.FLAGS_CENTER )
            
            import psutil
            
            num_cores = psutil.cpu_count()
            
            QP.AddToLayout( hbox, ClientGUICommon.BetterStaticText( self._idle_panel, label = '(you appear to have {} cores)'.format( num_cores ) ), CC.FLAGS_CENTER )
            
            rows.append( ( 'Consider the system busy if CPU usage is above: ', hbox ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._idle_panel, rows )
            
            self._idle_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Run jobs on shutdown: ', self._idle_shutdown ) )
            rows.append( ( 'Only run shutdown jobs once per: ', self._shutdown_work_period ) )
            rows.append( ( 'Max number of minutes to run shutdown jobs: ', self._idle_shutdown_max_minutes ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._shutdown_panel, rows )
            
            self._shutdown_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            text = '***'
            text += os.linesep
            text +='If you are a new user or do not completely understand these options, please do not touch them! Do not set the client to be idle all the time unless you know what you are doing or are testing something and are prepared for potential problems!'
            text += os.linesep
            text += '***'
            text += os.linesep * 2
            text += 'Sometimes, the client needs to do some heavy maintenance. This could be reformatting the database to keep it running fast or processing a large number of tags from a repository. Typically, these jobs will not allow you to use the gui while they run, and on slower computers--or those with not much memory--they can take a long time to complete.'
            text += os.linesep * 2
            text += 'You can set these jobs to run only when the client is idle, or only during shutdown, or neither, or both. If you leave the client on all the time in the background, focusing on \'idle time\' processing is often ideal. If you have a slow computer, relying on \'shutdown\' processing (which you can manually start when convenient), is often better.'
            text += os.linesep * 2
            text += 'If the client switches from idle to not idle during a job, it will try to abandon it and give you back control. This is not always possible, and even when it is, it will sometimes take several minutes, particularly on slower machines or those on HDDs rather than SSDs.'
            text += os.linesep * 2
            text += 'If the client believes the system is busy, it will generally not start jobs.'
            
            st = ClientGUICommon.BetterStaticText( self._jobs_panel, label = text )
            st.setWordWrap( True )
            
            self._jobs_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            self._jobs_panel.Add( self._idle_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            self._jobs_panel.Add( self._shutdown_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            message = 'Scheduled jobs such as reparsing file metadata and regenerating thumbnails are performed in the background.'
            
            self._file_maintenance_panel.Add( ClientGUICommon.BetterStaticText( self._file_maintenance_panel, label = message ), CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Run file maintenance during idle time: ', self._file_maintenance_during_idle ) )
            rows.append( ( 'Idle throttle: ', self._file_maintenance_idle_throttle_velocity ) )
            rows.append( ( 'Run file maintenance during normal time: ', self._file_maintenance_during_active ) )
            rows.append( ( 'Normal throttle: ', self._file_maintenance_active_throttle_velocity ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._file_maintenance_panel, rows )
            
            self._file_maintenance_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            message = 'Repository processing takes a lot of CPU and works best when it can rip for long periods in idle time.'
            
            self._repository_processing_panel.Add( ClientGUICommon.BetterStaticText( self._repository_processing_panel, label = message ), CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( '"Very idle" ideal work packet time: ', self._repository_processing_work_time_very_idle ) )
            rows.append( ( '"Very idle" rest time percentage: ', self._repository_processing_rest_percentage_very_idle ) )
            rows.append( ( '"Idle" ideal work packet time: ', self._repository_processing_work_time_idle ) )
            rows.append( ( '"Idle" rest time percentage: ', self._repository_processing_rest_percentage_idle ) )
            rows.append( ( '"Normal" ideal work packet time: ', self._repository_processing_work_time_normal ) )
            rows.append( ( '"Normal" rest time percentage: ', self._repository_processing_rest_percentage_normal ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._repository_processing_panel, rows )
            
            self._repository_processing_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            message = 'The database compiles sibling and parent implication calculations in the background. This can use a LOT of CPU in big bumps.'
            
            self._tag_display_processing_panel.Add( ClientGUICommon.BetterStaticText( self._tag_display_processing_panel, label = message ), CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Do work in "idle" time: ', self._tag_display_maintenance_during_idle ) )
            rows.append( ( '"Idle" ideal work packet time: ', self._tag_display_processing_work_time_idle ) )
            rows.append( ( '"Idle" rest time percentage: ', self._tag_display_processing_rest_percentage_idle ) )
            rows.append( ( 'Do work in "normal" time: ', self._tag_display_maintenance_during_active ) )
            rows.append( ( '"Normal" ideal work packet time: ', self._tag_display_processing_work_time_normal ) )
            rows.append( ( '"Normal" rest time percentage: ', self._tag_display_processing_rest_percentage_normal ) )
            rows.append( ( '"Work hard" ideal work packet time: ', self._tag_display_processing_work_time_work_hard ) )
            rows.append( ( '"Work hard" rest time percentage: ', self._tag_display_processing_rest_percentage_work_hard ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._tag_display_processing_panel, rows )
            
            self._tag_display_processing_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            message = 'The search for potential duplicate file pairs (as on the duplicates page) can keep up to date automatically in idle time and shutdown.'
            
            self._duplicates_panel.Add( ClientGUICommon.BetterStaticText( self._duplicates_panel, label = message ), CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Search for potential duplicates in idle time/shutdown: ', self._maintain_similar_files_duplicate_pairs_during_idle ) )
            rows.append( ( '"Idle" ideal work packet time: ', self._potential_duplicates_search_work_time ) )
            rows.append( ( '"Idle" rest time percentage: ', self._potential_duplicates_search_rest_percentage ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._duplicates_panel, rows )
            
            self._duplicates_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            message = 'The database deletes old data in the background.'
            
            self._deferred_table_delete_panel.Add( ClientGUICommon.BetterStaticText( self._deferred_table_delete_panel, label = message ), CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( '"Idle" ideal work packet time: ', self._deferred_table_delete_work_time_idle ) )
            rows.append( ( '"Idle" rest time percentage: ', self._deferred_table_delete_rest_percentage_idle ) )
            rows.append( ( '"Normal" ideal work packet time: ', self._deferred_table_delete_work_time_normal ) )
            rows.append( ( '"Normal" rest time percentage: ', self._deferred_table_delete_rest_percentage_normal ) )
            rows.append( ( '"Work hard" ideal work packet time: ', self._deferred_table_delete_work_time_work_hard ) )
            rows.append( ( '"Work hard" rest time percentage: ', self._deferred_table_delete_rest_percentage_work_hard ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._deferred_table_delete_panel, rows )
            
            self._deferred_table_delete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._jobs_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._file_maintenance_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._repository_processing_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._tag_display_processing_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._duplicates_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._deferred_table_delete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
            self._EnableDisableIdleNormal()
            self._EnableDisableIdleShutdown()
            
            self._system_busy_cpu_count.valueChanged.connect( self._EnableDisableCPUPercent )
            
        
        def _EnableDisableCPUPercent( self ):
            
            enabled = self._system_busy_cpu_count.isEnabled() and self._system_busy_cpu_count.GetValue() is not None
            
            self._system_busy_cpu_percent.setEnabled( enabled )
            
        
        def _EnableDisableIdleNormal( self ):
            
            enabled = self._idle_normal.isChecked()
            
            self._idle_period.setEnabled( enabled )
            self._idle_mouse_period.setEnabled( enabled )
            self._idle_mode_client_api_timeout.setEnabled( enabled )
            self._system_busy_cpu_count.setEnabled( enabled )
            
            self._EnableDisableCPUPercent()
            
        
        def _EnableDisableIdleShutdown( self ):
            
            enabled = self._idle_shutdown.GetValue() != CC.IDLE_NOT_ON_SHUTDOWN
            
            self._shutdown_work_period.setEnabled( enabled )
            self._idle_shutdown_max_minutes.setEnabled( enabled )
            
        
        def UpdateOptions( self ):
            
            HC.options[ 'idle_normal' ] = self._idle_normal.isChecked()
            
            HC.options[ 'idle_period' ] = self._idle_period.GetValue()
            HC.options[ 'idle_mouse_period' ] = self._idle_mouse_period.GetValue()
            self._new_options.SetNoneableInteger( 'idle_mode_client_api_timeout', self._idle_mode_client_api_timeout.GetValue() )
            
            self._new_options.SetInteger( 'system_busy_cpu_percent', self._system_busy_cpu_percent.value() )
            self._new_options.SetNoneableInteger( 'system_busy_cpu_count', self._system_busy_cpu_count.GetValue() )
            
            HC.options[ 'idle_shutdown' ] = self._idle_shutdown.GetValue()
            HC.options[ 'idle_shutdown_max_minutes' ] = self._idle_shutdown_max_minutes.value()
            
            self._new_options.SetInteger( 'shutdown_work_period', self._shutdown_work_period.GetValue() )
            
            self._new_options.SetBoolean( 'file_maintenance_during_idle', self._file_maintenance_during_idle.isChecked() )
            
            file_maintenance_idle_throttle_velocity = self._file_maintenance_idle_throttle_velocity.GetValue()
            
            ( file_maintenance_idle_throttle_files, file_maintenance_idle_throttle_time_delta ) = file_maintenance_idle_throttle_velocity
            
            self._new_options.SetInteger( 'file_maintenance_idle_throttle_files', file_maintenance_idle_throttle_files )
            self._new_options.SetInteger( 'file_maintenance_idle_throttle_time_delta', file_maintenance_idle_throttle_time_delta )
            
            self._new_options.SetBoolean( 'file_maintenance_during_active', self._file_maintenance_during_active.isChecked() )
            
            file_maintenance_active_throttle_velocity = self._file_maintenance_active_throttle_velocity.GetValue()
            
            ( file_maintenance_active_throttle_files, file_maintenance_active_throttle_time_delta ) = file_maintenance_active_throttle_velocity
            
            self._new_options.SetInteger( 'file_maintenance_active_throttle_files', file_maintenance_active_throttle_files )
            self._new_options.SetInteger( 'file_maintenance_active_throttle_time_delta', file_maintenance_active_throttle_time_delta )
            
            self._new_options.SetInteger( 'repository_processing_work_time_ms_very_idle', int( self._repository_processing_work_time_very_idle.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'repository_processing_rest_percentage_very_idle', self._repository_processing_rest_percentage_very_idle.value() )
            
            self._new_options.SetInteger( 'repository_processing_work_time_ms_idle', int( self._repository_processing_work_time_idle.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'repository_processing_rest_percentage_idle', self._repository_processing_rest_percentage_idle.value() )
            
            self._new_options.SetInteger( 'repository_processing_work_time_ms_normal', int( self._repository_processing_work_time_normal.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'repository_processing_rest_percentage_normal', self._repository_processing_rest_percentage_normal.value() )
            
            self._new_options.SetBoolean( 'tag_display_maintenance_during_idle', self._tag_display_maintenance_during_idle.isChecked() )
            self._new_options.SetBoolean( 'tag_display_maintenance_during_active', self._tag_display_maintenance_during_active.isChecked() )
            
            self._new_options.SetInteger( 'tag_display_processing_work_time_ms_idle', int( self._tag_display_processing_work_time_idle.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'tag_display_processing_rest_percentage_idle', self._tag_display_processing_rest_percentage_idle.value() )
            
            self._new_options.SetInteger( 'tag_display_processing_work_time_ms_normal', int( self._tag_display_processing_work_time_normal.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'tag_display_processing_rest_percentage_normal', self._tag_display_processing_rest_percentage_normal.value() )
            
            self._new_options.SetInteger( 'tag_display_processing_work_time_ms_work_hard', int( self._tag_display_processing_work_time_work_hard.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'tag_display_processing_rest_percentage_work_hard', self._tag_display_processing_rest_percentage_work_hard.value() )
            
            self._new_options.SetBoolean( 'maintain_similar_files_duplicate_pairs_during_idle', self._maintain_similar_files_duplicate_pairs_during_idle.isChecked() )
            self._new_options.SetInteger( 'potential_duplicates_search_work_time_ms', int( self._potential_duplicates_search_work_time.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'potential_duplicates_search_rest_percentage', self._potential_duplicates_search_rest_percentage.value() )
            
            self._new_options.SetInteger( 'deferred_table_delete_work_time_ms_idle', int( self._deferred_table_delete_work_time_idle.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'deferred_table_delete_rest_percentage_idle', self._deferred_table_delete_rest_percentage_idle.value() )
            
            self._new_options.SetInteger( 'deferred_table_delete_work_time_ms_normal', int( self._deferred_table_delete_work_time_normal.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'deferred_table_delete_rest_percentage_normal', self._deferred_table_delete_rest_percentage_normal.value() )
            
            self._new_options.SetInteger( 'deferred_table_delete_work_time_ms_work_hard', int( self._deferred_table_delete_work_time_work_hard.GetValue() * 1000 ) )
            self._new_options.SetInteger( 'deferred_table_delete_rest_percentage_work_hard', self._deferred_table_delete_rest_percentage_work_hard.value() )
            
        
    
    class _MediaPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = CG.client_controller.new_options
            
            #
            
            animations_panel = ClientGUICommon.StaticBox( self, 'animations' )
            
            self._animated_scanbar_height = ClientGUICommon.BetterSpinBox( animations_panel, min=1, max=255 )
            self._animated_scanbar_hide_height = ClientGUICommon.NoneableSpinCtrl( animations_panel, none_phrase = 'no, hide it', min = 1, max = 255, unit = 'px' )
            self._animated_scanbar_nub_width = ClientGUICommon.BetterSpinBox( animations_panel, min=1, max=63 )
            
            self._animation_start_position = ClientGUICommon.BetterSpinBox( animations_panel, min=0, max=100 )
            
            self._always_loop_animations = QW.QCheckBox( animations_panel )
            self._always_loop_animations.setToolTip( 'Some GIFS and APNGs have metadata specifying how many times they should be played, usually 1. Uncheck this to obey that number.' )
            
            #
            
            system_panel = ClientGUICommon.StaticBox( self, 'system' )
            
            self._mpv_conf_path = QP.FilePickerCtrl( system_panel, starting_directory = os.path.join( HC.STATIC_DIR, 'mpv-conf' ) )
            
            self._use_system_ffmpeg = QW.QCheckBox( system_panel )
            self._use_system_ffmpeg.setToolTip( 'Check this to always default to the system ffmpeg in your path, rather than using the static ffmpeg in hydrus\'s bin directory. (requires restart)' )
            
            self._load_images_with_pil = QW.QCheckBox( system_panel )
            self._load_images_with_pil.setToolTip( 'We are dropping CV and moving to PIL exclusively. If you want to help test, please turn this on and send hydev any images that render wrong!' )
            
            #
            
            media_viewer_panel = ClientGUICommon.StaticBox( self, 'media viewer' )
            
            self._media_viewer_cursor_autohide_time_ms = ClientGUICommon.NoneableSpinCtrl( media_viewer_panel, none_phrase = 'do not autohide', min = 100, max = 100000, unit = 'ms' )
            
            self._media_zooms = QW.QLineEdit( media_viewer_panel )
            self._media_zooms.setToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will be what the program steps through as you zoom a media up and down.' )
            self._media_zooms.textChanged.connect( self.EventZoomsChanged )
            
            from hydrus.client.gui.canvas import ClientGUICanvasMedia
            
            self._media_viewer_zoom_center = ClientGUICommon.BetterChoice( media_viewer_panel )
            
            for zoom_centerpoint_type in ClientGUICanvasMedia.ZOOM_CENTERPOINT_TYPES:
                
                self._media_viewer_zoom_center.addItem( ClientGUICanvasMedia.zoom_centerpoints_str_lookup[ zoom_centerpoint_type ], zoom_centerpoint_type )
                
            
            tt = 'When you zoom in or out, there is a centerpoint about which the image zooms. This point \'stays still\' while the image expands or shrinks around it. Different centerpoints give different feels, especially if you drag images around a bit before zooming.'
            
            self._media_viewer_zoom_center.setToolTip( tt )
            
            self._draw_transparency_checkerboard_media_canvas = QW.QCheckBox( media_viewer_panel )
            self._draw_transparency_checkerboard_media_canvas.setToolTip( 'If unchecked, will fill in with the normal background colour. Does not apply to MPV.' )
            
            self._hide_uninteresting_local_import_time = QW.QCheckBox( media_viewer_panel )
            self._hide_uninteresting_local_import_time.setToolTip( 'If the file was imported at a similar time to when it was added to its current services (i.e. the number of seconds since both events differs by less than 10%), hide the import time in the top of the media viewer.' )
            
            self._hide_uninteresting_modified_time = QW.QCheckBox( media_viewer_panel )
            self._hide_uninteresting_modified_time.setToolTip( 'If the file has a modified time similar to its import time (i.e. the number of seconds since both events differs by less than 10%), hide the modified time in the top of the media viewer.' )
            
            self._anchor_and_hide_canvas_drags = QW.QCheckBox( media_viewer_panel )
            self._touchscreen_canvas_drags_unanchor = QW.QCheckBox( media_viewer_panel )
            
            #
            
            slideshow_panel = ClientGUICommon.StaticBox( media_viewer_panel, 'slideshows' )
            
            self._slideshow_durations = QW.QLineEdit( slideshow_panel )
            self._slideshow_durations.setToolTip( 'This is a bit hacky, but whatever you have here, in comma-separated floats, will end up in the slideshow menu in the media viewer.' )
            self._slideshow_durations.textChanged.connect( self.EventSlideshowChanged )
            
            self._slideshow_always_play_duration_media_once_through = QW.QCheckBox( slideshow_panel )
            self._slideshow_always_play_duration_media_once_through.setToolTip( 'If this is on, then a slideshow will not move on until the current duration-having media has played once through.' )
            self._slideshow_always_play_duration_media_once_through.clicked.connect( self.EventSlideshowChanged )
            
            self._slideshow_short_duration_loop_seconds = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 86400, unit = 's' )
            tt = '(Ensures very short loops play for a bit, but not five minutes) A slideshow will move on early if the current duration-having media has a duration less than this many seconds (and this is less than the overall slideshow period).'
            self._slideshow_short_duration_loop_seconds.setToolTip( tt )
            
            self._slideshow_short_duration_loop_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 99, unit = '%' )
            tt = '(Ensures short videos play for a bit, but not twenty minutes) A slideshow will move on early if the current duration-having media has a duration less than this percentage of the overall slideshow period.'
            self._slideshow_short_duration_loop_percentage.setToolTip( tt )
            
            self._slideshow_short_duration_cutoff_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 99, unit = '%' )
            tt = '(Ensures that slightly shorter videos move the slideshow cleanly along as soon as they are done) A slideshow will move on early if the current duration-having media will have played exactly once through between this many percent and 100% of the slideshow period.'
            self._slideshow_short_duration_cutoff_percentage.setToolTip( tt )
            
            self._slideshow_long_duration_overspill_percentage = ClientGUICommon.NoneableSpinCtrl( slideshow_panel, none_phrase = 'do not use', min = 1, max = 500, unit = '%' )
            tt = '(Ensures slightly longer videos will not get cut off right at the end) A slideshow will delay moving on if playing the current duration-having media would stretch the overall slideshow period less than this amount.'
            self._slideshow_long_duration_overspill_percentage.setToolTip( tt )
            
            #
            
            filetype_handling_panel = ClientGUICommon.StaticBox( media_viewer_panel, 'media viewer filetype handling' )
            
            media_viewer_list_panel = ClientGUIListCtrl.BetterListCtrlPanel( filetype_handling_panel )
            
            self._media_viewer_options = ClientGUIListCtrl.BetterListCtrl( media_viewer_list_panel, CGLC.COLUMN_LIST_MEDIA_VIEWER_OPTIONS.ID, 20, data_to_tuples_func = self._GetListCtrlData, activation_callback = self.EditMediaViewerOptions, use_simple_delete = True )
            
            media_viewer_list_panel.SetListCtrl( self._media_viewer_options )
            
            media_viewer_list_panel.AddButton( 'add', self.AddMediaViewerOptions, enabled_check_func = self._CanAddMediaViewOption )
            media_viewer_list_panel.AddButton( 'edit', self.EditMediaViewerOptions, enabled_only_on_selection = True )
            media_viewer_list_panel.AddDeleteButton( enabled_check_func = self._CanDeleteMediaViewOptions )
            
            #
            
            self._animation_start_position.setValue( int( HC.options['animation_start_position'] * 100.0 ) )
            self._hide_uninteresting_local_import_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_local_import_time' ) )
            self._hide_uninteresting_modified_time.setChecked( self._new_options.GetBoolean( 'hide_uninteresting_modified_time' ) )
            self._load_images_with_pil.setChecked( self._new_options.GetBoolean( 'load_images_with_pil' ) )
            self._use_system_ffmpeg.setChecked( self._new_options.GetBoolean( 'use_system_ffmpeg' ) )
            self._always_loop_animations.setChecked( self._new_options.GetBoolean( 'always_loop_gifs' ) )
            self._draw_transparency_checkerboard_media_canvas.setChecked( self._new_options.GetBoolean( 'draw_transparency_checkerboard_media_canvas' ) )
            self._media_viewer_cursor_autohide_time_ms.SetValue( self._new_options.GetNoneableInteger( 'media_viewer_cursor_autohide_time_ms' ) )
            self._anchor_and_hide_canvas_drags.setChecked( self._new_options.GetBoolean( 'anchor_and_hide_canvas_drags' ) )
            self._touchscreen_canvas_drags_unanchor.setChecked( self._new_options.GetBoolean( 'touchscreen_canvas_drags_unanchor' ) )
            self._animated_scanbar_height.setValue( self._new_options.GetInteger( 'animated_scanbar_height' ) )
            self._animated_scanbar_nub_width.setValue( self._new_options.GetInteger( 'animated_scanbar_nub_width' ) )
            
            self._animated_scanbar_hide_height.SetValue( 5 )
            self._animated_scanbar_hide_height.SetValue( self._new_options.GetNoneableInteger( 'animated_scanbar_hide_height' ) )
            
            self._media_viewer_zoom_center.SetValue( self._new_options.GetInteger( 'media_viewer_zoom_center' ) )
            
            slideshow_durations = self._new_options.GetSlideshowDurations()
            
            self._slideshow_durations.setText( ','.join( ( str( slideshow_duration ) for slideshow_duration in slideshow_durations ) ) )
            
            self._slideshow_always_play_duration_media_once_through.setChecked( self._new_options.GetBoolean( 'slideshow_always_play_duration_media_once_through' ) )
            self._slideshow_short_duration_loop_seconds.SetValue( self._new_options.GetNoneableInteger( 'slideshow_short_duration_loop_seconds' ) )
            self._slideshow_short_duration_loop_percentage.SetValue( self._new_options.GetNoneableInteger( 'slideshow_short_duration_loop_percentage' ) )
            self._slideshow_short_duration_cutoff_percentage.SetValue( self._new_options.GetNoneableInteger( 'slideshow_short_duration_cutoff_percentage' ) )
            self._slideshow_long_duration_overspill_percentage.SetValue( self._new_options.GetNoneableInteger( 'slideshow_long_duration_overspill_percentage' ) )
            
            media_zooms = self._new_options.GetMediaZooms()
            
            self._media_zooms.setText( ','.join( ( str( media_zoom ) for media_zoom in media_zooms ) ) )
            
            all_media_view_options = self._new_options.GetMediaViewOptions()
            
            for ( mime, view_options ) in all_media_view_options.items():
                
                data = QP.ListsToTuples( [ mime ] + list( view_options ) )
                
                self._media_viewer_options.AddDatas( ( data, ) )
                
            
            self._media_viewer_options.Sort()
            
            #
            
            vbox = QP.VBoxLayout()
            
            #
            
            rows = []
            
            rows.append( ( 'Time until mouse cursor autohides on media viewer:', self._media_viewer_cursor_autohide_time_ms ) )
            rows.append( ( 'Media zooms:', self._media_zooms ) )
            rows.append( ( 'Centerpoint for media zooming:', self._media_viewer_zoom_center ) )
            rows.append( ( 'Draw image transparency as checkerboard:', self._draw_transparency_checkerboard_media_canvas ) )
            rows.append( ( 'Hide uninteresting import times:', self._hide_uninteresting_local_import_time ) )
            rows.append( ( 'Hide uninteresting modified times:', self._hide_uninteresting_modified_time ) )
            rows.append( ( 'RECOMMEND WINDOWS ONLY: Hide and anchor mouse cursor on media viewer drags:', self._anchor_and_hide_canvas_drags ) )
            rows.append( ( 'RECOMMEND WINDOWS ONLY: If set to hide and anchor, undo on apparent touchscreen drag:', self._touchscreen_canvas_drags_unanchor ) )
            
            media_viewer_gridbox = ClientGUICommon.WrapInGrid( media_viewer_panel, rows )
            
            rows = []
            
            rows.append( ( 'Slideshow durations:', self._slideshow_durations ) )
            rows.append( ( 'Always play media once through before moving on:', self._slideshow_always_play_duration_media_once_through ) )
            rows.append( ( 'Slideshow short-media skip seconds threshold:', self._slideshow_short_duration_loop_seconds ) )
            rows.append( ( 'Slideshow short-media skip percentage threshold:', self._slideshow_short_duration_loop_percentage ) )
            rows.append( ( 'Slideshow shorter-media cutoff percentage threshold:', self._slideshow_short_duration_cutoff_percentage ) )
            rows.append( ( 'Slideshow long-media allowed delay percentage threshold:', self._slideshow_long_duration_overspill_percentage ) )
            
            slideshow_gridbox = ClientGUICommon.WrapInGrid( slideshow_panel, rows )
            
            slideshow_panel.Add( slideshow_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            filetype_handling_panel.Add( media_viewer_list_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            media_viewer_panel.Add( media_viewer_gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            media_viewer_panel.Add( slideshow_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            media_viewer_panel.Add( filetype_handling_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            QP.AddToLayout( vbox, media_viewer_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            rows = []
            
            rows.append( ( 'Animation scanbar height:', self._animated_scanbar_height ) )
            rows.append( ( 'Animation scanbar height when mouse away:', self._animated_scanbar_hide_height ) )
            rows.append( ( 'Animation scanbar nub width:', self._animated_scanbar_nub_width ) )
            rows.append( ( 'Start animations this % in:', self._animation_start_position ) )
            rows.append( ( 'Always Loop GIFs/APNGs:', self._always_loop_animations ) )
            
            gridbox = ClientGUICommon.WrapInGrid( animations_panel, rows )
            
            animations_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, animations_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Set a new mpv.conf on dialog ok?:', self._mpv_conf_path ) )
            rows.append( ( 'Prefer system FFMPEG:', self._use_system_ffmpeg ) )
            rows.append( ( 'IN TESTING: Load images with PIL:', self._load_images_with_pil ) )
            
            gridbox = ClientGUICommon.WrapInGrid( system_panel, rows )
            
            system_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, system_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            self.setLayout( vbox )
            
        
        def _CanAddMediaViewOption( self ):
            
            return len( self._GetUnsetMediaViewFiletypes() ) > 0
            
        
        def _CanDeleteMediaViewOptions( self ):
            
            deletable_mimes = set( HC.SEARCHABLE_MIMES )
            
            selected_mimes = set()
            
            for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData( only_selected = True ):
                
                selected_mimes.add( mime )
                
            
            if len( selected_mimes ) == 0:
                
                return False
                
            
            all_selected_are_deletable = selected_mimes.issubset( deletable_mimes )
            
            return all_selected_are_deletable
            
        
        def _GetCopyOfGeneralMediaViewOptions( self, desired_mime ):
            
            general_mime_type = HC.mimes_to_general_mimetypes[ desired_mime ]
            
            for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
                
                if mime == general_mime_type:
                    
                    view_options = ( desired_mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info )
                    
                    return view_options
                    
                
            
        
        def _GetUnsetMediaViewFiletypes( self ):
            
            editable_mimes = set( HC.SEARCHABLE_MIMES )
            
            set_mimes = set()
            
            for ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) in self._media_viewer_options.GetData():
                
                set_mimes.add( mime )
                
            
            unset_mimes = editable_mimes.difference( set_mimes )
            
            return unset_mimes
            
        
        def _GetListCtrlData( self, data ):
            
            ( mime, media_show_action, media_start_paused, media_start_with_embed, preview_show_action, preview_start_paused, preview_start_with_embed, zoom_info ) = data
            
            pretty_mime = self._GetPrettyMime( mime )
            
            pretty_media_show_action = CC.media_viewer_action_string_lookup[ media_show_action ]
            
            if media_start_paused:
                
                pretty_media_show_action += ', start paused'
                
            
            if media_start_with_embed:
                
                pretty_media_show_action += ', start with embed button'
                
            
            pretty_preview_show_action = CC.media_viewer_action_string_lookup[ preview_show_action ]
            
            if preview_start_paused:
                
                pretty_preview_show_action += ', start paused'
                
            
            if preview_start_with_embed:
                
                pretty_preview_show_action += ', start with embed button'
                
            
            no_show = { media_show_action, preview_show_action }.isdisjoint( { CC.MEDIA_VIEWER_ACTION_SHOW_WITH_NATIVE, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_MPV, CC.MEDIA_VIEWER_ACTION_SHOW_WITH_QMEDIAPLAYER } )
            
            if no_show:
                
                pretty_zoom_info = ''
                
            else:
                
                pretty_zoom_info = str( zoom_info )
                
            
            display_tuple = ( pretty_mime, pretty_media_show_action, pretty_preview_show_action, pretty_zoom_info )
            sort_tuple = ( pretty_mime, pretty_media_show_action, pretty_preview_show_action, pretty_zoom_info )
            
            return ( display_tuple, sort_tuple )
            
        
        def _GetPrettyMime( self, mime ):
            
            pretty_mime = HC.mime_string_lookup[ mime ]
            
            if mime not in HC.GENERAL_FILETYPES:
                
                pretty_mime = '{}: {}'.format( HC.mime_string_lookup[ HC.mimes_to_general_mimetypes[ mime ] ], pretty_mime )
                
            
            return pretty_mime
            
        
        def AddMediaViewerOptions( self ):
            
            unset_filetypes = self._GetUnsetMediaViewFiletypes()
            
            if len( unset_filetypes ) == 0:
                
                ClientGUIDialogsMessage.ShowWarning( self, 'You cannot add any more specific filetype options!' )
                
                return
                
            
            choice_tuples = [ ( self._GetPrettyMime( mime ), mime ) for mime in unset_filetypes ]
            
            try:
                
                mime = ClientGUIDialogsQuick.SelectFromList( self, 'select the filetype to add', choice_tuples, sort_tuples = True )
                
            except HydrusExceptions.CancelledException:
                
                return
                
            
            data = self._GetCopyOfGeneralMediaViewOptions( mime )
            
            title = 'add media view options information'
            
            with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg:
                
                panel = ClientGUIScrolledPanelsEdit.EditMediaViewOptionsPanel( dlg, data )
                
                dlg.SetPanel( panel )
                
                if dlg.exec() == QW.QDialog.Accepted:
                    
                    new_data = panel.GetValue()
                    
                    self._media_viewer_options.AddDatas( ( new_data, ) )
                    
                
            
        
        def EditMediaViewerOptions( self ):
            
            for data in self._media_viewer_options.GetData( only_selected = True ):
                
                title = 'edit media view options information'
                
                with ClientGUITopLevelWindowsPanels.DialogEdit( self, title ) as dlg:
                    
                    panel = ClientGUIScrolledPanelsEdit.EditMediaViewOptionsPanel( dlg, data )
                    
                    dlg.SetPanel( panel )
                    
                    if dlg.exec() == QW.QDialog.Accepted:
                        
                        new_data = panel.GetValue()
                        
                        self._media_viewer_options.ReplaceData( data, new_data )
                        
                    
                
            
        
        def EventSlideshowChanged( self, text ):
            
            try:
                
                slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
                
                self._slideshow_durations.setObjectName( '' )
                
            except ValueError:
                
                self._slideshow_durations.setObjectName( 'HydrusInvalid' )
                
            
            self._slideshow_durations.style().polish( self._slideshow_durations )
            
            self._slideshow_durations.update()
            
            always_once_through = self._slideshow_always_play_duration_media_once_through.isChecked()
            
            self._slideshow_long_duration_overspill_percentage.setEnabled( not always_once_through )
            
        
        def EventZoomsChanged( self, text ):
            
            try:
                
                media_zooms = [ float( media_zoom ) for media_zoom in self._media_zooms.text().split( ',' ) ]
                
                self._media_zooms.setObjectName( '' )
                
            except ValueError:
                
                self._media_zooms.setObjectName( 'HydrusInvalid' )
                
            
            self._media_zooms.style().polish( self._media_zooms )
            
            self._media_zooms.update()
            
        
        def UpdateOptions( self ):
            
            HC.options[ 'animation_start_position' ] = self._animation_start_position.value() / 100.0
            
            self._new_options.SetBoolean( 'hide_uninteresting_local_import_time', self._hide_uninteresting_local_import_time.isChecked() )
            self._new_options.SetBoolean( 'hide_uninteresting_modified_time', self._hide_uninteresting_modified_time.isChecked() )
            self._new_options.SetBoolean( 'load_images_with_pil', self._load_images_with_pil.isChecked() )
            self._new_options.SetBoolean( 'use_system_ffmpeg', self._use_system_ffmpeg.isChecked() )
            self._new_options.SetBoolean( 'always_loop_gifs', self._always_loop_animations.isChecked() )
            self._new_options.SetBoolean( 'draw_transparency_checkerboard_media_canvas', self._draw_transparency_checkerboard_media_canvas.isChecked() )
            self._new_options.SetBoolean( 'anchor_and_hide_canvas_drags', self._anchor_and_hide_canvas_drags.isChecked() )
            self._new_options.SetBoolean( 'touchscreen_canvas_drags_unanchor', self._touchscreen_canvas_drags_unanchor.isChecked() )
            
            self._new_options.SetNoneableInteger( 'media_viewer_cursor_autohide_time_ms', self._media_viewer_cursor_autohide_time_ms.GetValue() )
            
            mpv_conf_path = self._mpv_conf_path.GetPath()
            
            if mpv_conf_path is not None and mpv_conf_path != '' and os.path.exists( mpv_conf_path ) and os.path.isfile( mpv_conf_path ):
                
                dest_mpv_conf_path = CG.client_controller.GetMPVConfPath()
                
                try:
                    
                    HydrusPaths.MirrorFile( mpv_conf_path, dest_mpv_conf_path )
                    
                except Exception as e:
                    
                    HydrusData.ShowText( 'Could not set the mpv conf path "{}" to "{}"! Error follows!'.format( mpv_conf_path, dest_mpv_conf_path ) )
                    HydrusData.ShowException( e )
                    
                
            
            self._new_options.SetInteger( 'animated_scanbar_height', self._animated_scanbar_height.value() )
            self._new_options.SetInteger( 'animated_scanbar_nub_width', self._animated_scanbar_nub_width.value() )
            
            self._new_options.SetNoneableInteger( 'animated_scanbar_hide_height', self._animated_scanbar_hide_height.GetValue() )
            
            self._new_options.SetInteger( 'media_viewer_zoom_center', self._media_viewer_zoom_center.GetValue() )
            
            try:
                
                slideshow_durations = [ float( slideshow_duration ) for slideshow_duration in self._slideshow_durations.text().split( ',' ) ]
                
                slideshow_durations = [ slideshow_duration for slideshow_duration in slideshow_durations if slideshow_duration > 0.0 ]
                
                if len( slideshow_durations ) > 0:
                    
                    self._new_options.SetSlideshowDurations( slideshow_durations )
                    
                
            except ValueError:
                
                HydrusData.ShowText( 'Could not parse those slideshow durations, so they were not saved!' )
                
            
            self._new_options.SetBoolean( 'slideshow_always_play_duration_media_once_through', self._slideshow_always_play_duration_media_once_through.isChecked() )
            self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_percentage', self._slideshow_short_duration_loop_percentage.GetValue() )
            self._new_options.SetNoneableInteger( 'slideshow_short_duration_loop_seconds', self._slideshow_short_duration_loop_seconds.GetValue() )
            self._new_options.SetNoneableInteger( 'slideshow_short_duration_cutoff_percentage', self._slideshow_short_duration_cutoff_percentage.GetValue() )
            self._new_options.SetNoneableInteger( 'slideshow_long_duration_overspill_percentage', self._slideshow_long_duration_overspill_percentage.GetValue() )
            
            try:
                
                media_zooms = [ float( media_zoom ) for media_zoom in self._media_zooms.text().split( ',' ) ]
                
                media_zooms = [ media_zoom for media_zoom in media_zooms if media_zoom > 0.0 ]
                
                if len( media_zooms ) > 0:
                    
                    self._new_options.SetMediaZooms( media_zooms )
                    
                
            except ValueError:
                
                HydrusData.ShowText( 'Could not parse those zooms, so they were not saved!' )
                
            
            mimes_to_media_view_options = {}
            
            for data in self._media_viewer_options.GetData():
                
                data = list( data )
                
                mime = data[0]
                
                value = data[1:]
                
                mimes_to_media_view_options[ mime ] = value
                
            
            self._new_options.SetMediaViewOptions( mimes_to_media_view_options )
            
        
    
    class _NotesPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            self._start_note_editing_at_end = QW.QCheckBox( self )
            self._start_note_editing_at_end.setToolTip( 'Otherwise, start the text cursor at the start of the document.' )
            
            self._start_note_editing_at_end.setChecked( self._new_options.GetBoolean( 'start_note_editing_at_end' ) )
            
            vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Start editing notes with the text cursor at the end of the document: ', self._start_note_editing_at_end ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetBoolean( 'start_note_editing_at_end', self._start_note_editing_at_end.isChecked() )
            
        
    
    class _PopupPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            self._popup_panel = ClientGUICommon.StaticBox( self, 'popup window toaster' )
            
            self._popup_message_character_width = ClientGUICommon.BetterSpinBox( self._popup_panel, min = 16, max = 256 )
            
            self._popup_message_force_min_width = QW.QCheckBox( self._popup_panel )
            
            self._freeze_message_manager_when_mouse_on_other_monitor = QW.QCheckBox( self._popup_panel )
            self._freeze_message_manager_when_mouse_on_other_monitor.setToolTip( 'This is useful if you have a virtual desktop and find the popup manager restores strangely when you hop back to the hydrus display.' )
            
            self._freeze_message_manager_when_main_gui_minimised = QW.QCheckBox( self._popup_panel )
            self._freeze_message_manager_when_main_gui_minimised.setToolTip( 'This is useful if the popup toaster restores strangely after minimised changes.' )
            
            self._notify_client_api_cookies = QW.QCheckBox( self._popup_panel )
            self._notify_client_api_cookies.setToolTip( 'This will make a short-lived popup message every time you get new cookie or http header information over the Client API.' )
            
            #
            
            self._popup_message_character_width.setValue( self._new_options.GetInteger( 'popup_message_character_width' ) )
            
            self._popup_message_force_min_width.setChecked( self._new_options.GetBoolean( 'popup_message_force_min_width' ) )
            
            self._freeze_message_manager_when_mouse_on_other_monitor.setChecked( self._new_options.GetBoolean( 'freeze_message_manager_when_mouse_on_other_monitor' ) )
            self._freeze_message_manager_when_main_gui_minimised.setChecked( self._new_options.GetBoolean( 'freeze_message_manager_when_main_gui_minimised' ) )
            
            self._notify_client_api_cookies.setChecked( self._new_options.GetBoolean( 'notify_client_api_cookies' ) )
            
            #
            
            rows = []
            
            rows.append( ( 'Approximate max width of popup messages (in characters): ', self._popup_message_character_width ) )
            rows.append( ( 'BUGFIX: Force this width as the fixed width for all popup messages: ', self._popup_message_force_min_width ) )
            rows.append( ( 'Freeze the popup toaster when mouse is on another display: ', self._freeze_message_manager_when_mouse_on_other_monitor ) )
            rows.append( ( 'Freeze the popup toaster when the main gui is minimised: ', self._freeze_message_manager_when_main_gui_minimised ) )
            rows.append( ( 'Make a short-lived popup on cookie/header updates through the Client API: ', self._notify_client_api_cookies ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._popup_panel, rows )
            
            self._popup_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._popup_panel, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetInteger( 'popup_message_character_width', self._popup_message_character_width.value() )
            
            self._new_options.SetBoolean( 'popup_message_force_min_width', self._popup_message_force_min_width.isChecked() )
            
            self._new_options.SetBoolean( 'freeze_message_manager_when_mouse_on_other_monitor', self._freeze_message_manager_when_mouse_on_other_monitor.isChecked() )
            self._new_options.SetBoolean( 'freeze_message_manager_when_main_gui_minimised', self._freeze_message_manager_when_main_gui_minimised.isChecked() )
            
            self._new_options.SetBoolean( 'notify_client_api_cookies', self._notify_client_api_cookies.isChecked() )
            
        
    
    class _RegexPanel( QW.QWidget ):
        
        def __init__( self, parent ):
            
            QW.QWidget.__init__( self, parent )
            
            regex_favourites = HC.options[ 'regex_favourites' ]
            
            self._regex_panel = ClientGUIScrolledPanelsEdit.EditRegexFavourites( self, regex_favourites )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._regex_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            regex_favourites = self._regex_panel.GetValue()
            
            HC.options[ 'regex_favourites' ] = regex_favourites
            
        
    
    class _SearchPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            self._read_autocomplete_panel = ClientGUICommon.StaticBox( self, 'file search autocomplete' )
            
            location_context = self._new_options.GetDefaultLocalLocationContext()
            
            self._default_local_location_context = ClientGUILocation.LocationSearchContextButton( self._read_autocomplete_panel, location_context )
            self._default_local_location_context.setToolTip( 'This initialised into a bunch of dialogs across the program as a fallback. You can probably leave it alone forever, but if you delete or move away from \'my files\' as your main place to do work, please update it here.' )
            
            self._default_local_location_context.SetOnlyImportableDomainsAllowed( True )
            
            self._default_tag_service_search_page = ClientGUICommon.BetterChoice( self._read_autocomplete_panel )
            
            self._default_search_synchronised = QW.QCheckBox( self._read_autocomplete_panel )
            tt = 'This refers to the button on the autocomplete dropdown that enables new searches to start. If this is on, then new search pages will search as soon as you enter the first search predicate. If off, no search will happen until you switch it back on.'
            self._default_search_synchronised.setToolTip( tt )
            
            self._autocomplete_float_main_gui = QW.QCheckBox( self._read_autocomplete_panel )
            tt = 'The autocomplete dropdown can either \'float\' on top of the main window, or if that does not work well for you, it can embed into the parent page panel.'
            self._autocomplete_float_main_gui.setToolTip( tt )
            
            self._ac_read_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._read_autocomplete_panel, min = 1, max = 128 )
            
            self._always_show_system_everything = QW.QCheckBox( self._read_autocomplete_panel )
            tt = 'After users get some experience with the program and a larger collection, they tend to have less use for system:everything.'
            self._always_show_system_everything.setToolTip( tt )
            
            self._filter_inbox_and_archive_predicates = QW.QCheckBox( self._read_autocomplete_panel )
            tt = 'If everything is current in the inbox (or archive), then there is no use listing it or its opposite--it either does not change the search or it produces nothing. If you find it jarring though, turn it off here!'
            self._filter_inbox_and_archive_predicates.setToolTip( tt )
            
            #
            
            self._write_autocomplete_panel = ClientGUICommon.StaticBox( self, 'tag edit autocomplete' )
            
            self._default_tag_service_tab = ClientGUICommon.BetterChoice( self._write_autocomplete_panel )
            
            self._save_default_tag_service_tab_on_change = QW.QCheckBox( self._write_autocomplete_panel )
            
            self._ac_write_list_height_num_chars = ClientGUICommon.BetterSpinBox( self._write_autocomplete_panel, min = 1, max = 128 )
            
            #
            
            misc_panel = ClientGUICommon.StaticBox( self, 'file search' )
            
            self._forced_search_limit = ClientGUICommon.NoneableSpinCtrl( misc_panel, '', min = 1, max = 100000 )
            self._forced_search_limit.setToolTip( 'This is overruled if you set an explicit system:limit larger than it.' )
            
            #
            
            self._default_tag_service_search_page.addItem( 'all known tags', CC.COMBINED_TAG_SERVICE_KEY )
            
            services = CG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
            
            for service in services:
                
                self._default_tag_service_tab.addItem( service.GetName(), service.GetServiceKey() )
                
                self._default_tag_service_search_page.addItem( service.GetName(), service.GetServiceKey() )
                
            
            self._default_tag_service_tab.SetValue( self._new_options.GetKey( 'default_tag_service_tab' ) )
            
            self._save_default_tag_service_tab_on_change.setChecked( self._new_options.GetBoolean( 'save_default_tag_service_tab_on_change' ) )
            
            self._default_tag_service_search_page.SetValue( self._new_options.GetKey( 'default_tag_service_search_page' ) )
            
            self._default_search_synchronised.setChecked( self._new_options.GetBoolean( 'default_search_synchronised' ) )
            
            self._autocomplete_float_main_gui.setChecked( self._new_options.GetBoolean( 'autocomplete_float_main_gui' ) )
            
            self._ac_read_list_height_num_chars.setValue( self._new_options.GetInteger( 'ac_read_list_height_num_chars' ) )
            self._ac_write_list_height_num_chars.setValue( self._new_options.GetInteger( 'ac_write_list_height_num_chars' ) )
            
            self._always_show_system_everything.setChecked( self._new_options.GetBoolean( 'always_show_system_everything' ) )
            
            self._filter_inbox_and_archive_predicates.setChecked( self._new_options.GetBoolean( 'filter_inbox_and_archive_predicates' ) )
            
            self._forced_search_limit.SetValue( self._new_options.GetNoneableInteger( 'forced_search_limit' ) )
            
            #
            
            message = 'This tag autocomplete appears in file search pages and other places where you use tags and system predicates to search for files.'
            
            st = ClientGUICommon.BetterStaticText( self._read_autocomplete_panel, label = message )
            
            self._read_autocomplete_panel.Add( st, CC.FLAGS_CENTER )
            
            rows = []
            
            rows.append( ( 'Default/Fallback local file search location: ', self._default_local_location_context ) )
            rows.append( ( 'Default tag service in search pages: ', self._default_tag_service_search_page ) )
            rows.append( ( 'Autocomplete dropdown floats over file search pages: ', self._autocomplete_float_main_gui ) )
            rows.append( ( 'Autocomplete list height: ', self._ac_read_list_height_num_chars ) )
            rows.append( ( 'Start new search pages in \'searching immediately\': ', self._default_search_synchronised ) )
            rows.append( ( 'show system:everything even if total files is over 10,000: ', self._always_show_system_everything ) )
            rows.append( ( 'hide inbox and archive system predicates if either has no files: ', self._filter_inbox_and_archive_predicates ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._read_autocomplete_panel, rows )
            
            self._read_autocomplete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
            
            #
            
            message = 'This tag autocomplete appears in the manage tags dialog and other places where you edit a list of tags.'
            
            st = ClientGUICommon.BetterStaticText( self._write_autocomplete_panel, label = message )
            
            self._write_autocomplete_panel.Add( st, CC.FLAGS_CENTER )
            
            rows = []
            
            rows.append( ( 'Remember last used default tag service in manage tag dialogs: ', self._save_default_tag_service_tab_on_change ) )
            rows.append( ( 'Default tag service in manage tag dialogs: ', self._default_tag_service_tab ) )
            rows.append( ( 'Autocomplete list height: ', self._ac_write_list_height_num_chars ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._write_autocomplete_panel, rows )
            
            self._write_autocomplete_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_BOTH_WAYS )
            
            #
            
            rows = []
            
            rows.append( ( 'Forced system:limit for all searches: ', self._forced_search_limit ) )
            
            gridbox = ClientGUICommon.WrapInGrid( misc_panel, rows )
            
            misc_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._read_autocomplete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, misc_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._write_autocomplete_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
            self._UpdateDefaultTagServiceControl()
            
            self._save_default_tag_service_tab_on_change.clicked.connect( self._UpdateDefaultTagServiceControl )
            
        
        def _UpdateDefaultTagServiceControl( self ):
            
            enabled = not self._save_default_tag_service_tab_on_change.isChecked()
            
            self._default_tag_service_tab.setEnabled( enabled )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetKey( 'default_tag_service_search_page', self._default_tag_service_search_page.GetValue() )
            
            self._new_options.SetDefaultLocalLocationContext( self._default_local_location_context.GetValue() )
            
            self._new_options.SetBoolean( 'default_search_synchronised', self._default_search_synchronised.isChecked() )
            
            self._new_options.SetBoolean( 'autocomplete_float_main_gui', self._autocomplete_float_main_gui.isChecked() )
            
            self._new_options.SetInteger( 'ac_read_list_height_num_chars', self._ac_read_list_height_num_chars.value() )
            self._new_options.SetInteger( 'ac_write_list_height_num_chars', self._ac_write_list_height_num_chars.value() )
            
            self._new_options.SetBoolean( 'always_show_system_everything', self._always_show_system_everything.isChecked() )
            self._new_options.SetBoolean( 'filter_inbox_and_archive_predicates', self._filter_inbox_and_archive_predicates.isChecked() )
            
            self._new_options.SetNoneableInteger( 'forced_search_limit', self._forced_search_limit.GetValue() )
            
        
    
    class _SortCollectPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            self._tag_sort_panel = ClientGUICommon.StaticBox( self, 'tag sort' )
            
            self._default_tag_sort_search_page = ClientGUITagSorting.TagSortControl( self._tag_sort_panel, self._new_options.GetDefaultTagSort( CC.TAG_PRESENTATION_SEARCH_PAGE ) )
            self._default_tag_sort_search_page_manage_tags = ClientGUITagSorting.TagSortControl( self._tag_sort_panel, self._new_options.GetDefaultTagSort( CC.TAG_PRESENTATION_SEARCH_PAGE_MANAGE_TAGS ), show_siblings = True )
            self._default_tag_sort_media_viewer = ClientGUITagSorting.TagSortControl( self._tag_sort_panel, self._new_options.GetDefaultTagSort( CC.TAG_PRESENTATION_MEDIA_VIEWER ) )
            self._default_tag_sort_media_viewer_manage_tags = ClientGUITagSorting.TagSortControl( self._tag_sort_panel, self._new_options.GetDefaultTagSort( CC.TAG_PRESENTATION_MEDIA_VIEWER_MANAGE_TAGS ), show_siblings = True )
            
            self._file_sort_panel = ClientGUICommon.StaticBox( self, 'file sort' )
            
            default_sort = self._new_options.GetDefaultSort()
            
            self._default_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self._file_sort_panel, media_sort = default_sort )
            
            if self._default_media_sort.GetSort() != default_sort:
                
                media_sort = ClientMedia.MediaSort( ( 'system', CC.SORT_FILES_BY_FILESIZE ), CC.SORT_ASC )
                
                self._default_media_sort.SetSort( media_sort )
                
            
            fallback_sort = self._new_options.GetFallbackSort()
            
            self._fallback_media_sort = ClientGUIResultsSortCollect.MediaSortControl( self._file_sort_panel, media_sort = fallback_sort )
            
            if self._fallback_media_sort.GetSort() != fallback_sort:
                
                media_sort = ClientMedia.MediaSort( ( 'system', CC.SORT_FILES_BY_IMPORT_TIME ), CC.SORT_ASC )
                
                self._fallback_media_sort.SetSort( media_sort )
                
            
            self._save_page_sort_on_change = QW.QCheckBox( self._file_sort_panel )
            
            self._default_media_collect = ClientGUIResultsSortCollect.MediaCollectControl( self._file_sort_panel )
            
            namespace_sorting_box = ClientGUICommon.StaticBox( self._file_sort_panel, 'namespace file sorting' )
            
            self._namespace_sort_by = ClientGUIListBoxes.QueueListBox( namespace_sorting_box, 8, self._ConvertNamespaceTupleToSortString, self._AddNamespaceSort, self._EditNamespaceSort )
            
            #
            
            self._namespace_sort_by.AddDatas( [ media_sort.sort_type[1] for media_sort in CG.client_controller.new_options.GetDefaultNamespaceSorts() ] )
            
            self._save_page_sort_on_change.setChecked( self._new_options.GetBoolean( 'save_page_sort_on_change' ) )
            
            #
            
            sort_by_text = 'You can manage your namespace sorting schemes here.'
            sort_by_text += os.linesep
            sort_by_text += 'The client will sort media by comparing their namespaces, moving from left to right until an inequality is found.'
            sort_by_text += os.linesep
            sort_by_text += 'Any namespaces here will also appear in your collect-by dropdowns.'
            
            namespace_sorting_box.Add( ClientGUICommon.BetterStaticText( namespace_sorting_box, sort_by_text ), CC.FLAGS_EXPAND_PERPENDICULAR )
            namespace_sorting_box.Add( self._namespace_sort_by, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            rows = []
            
            rows.append( ( 'Default tag sort in search pages: ', self._default_tag_sort_search_page ) )
            rows.append( ( 'Default tag sort in search page manage tags dialogs: ', self._default_tag_sort_search_page_manage_tags ) )
            rows.append( ( 'Default tag sort in the media viewer: ', self._default_tag_sort_media_viewer ) )
            rows.append( ( 'Default tag sort in media viewer manage tags dialogs: ', self._default_tag_sort_media_viewer_manage_tags ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self._tag_sort_panel, rows )
            
            self._tag_sort_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Default file sort: ', self._default_media_sort ) )
            rows.append( ( 'Secondary file sort (when primary gives two equal values): ', self._fallback_media_sort ) )
            rows.append( ( 'Update default file sort every time a new sort is manually chosen: ', self._save_page_sort_on_change ) )
            rows.append( ( 'Default collect: ', self._default_media_collect ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            self._file_sort_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            self._file_sort_panel.Add( namespace_sorting_box, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, self._tag_sort_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( vbox, self._file_sort_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
        
        def _AddNamespaceSort( self ):
            
            default = ( ( 'creator', 'series', 'page' ), ClientTags.TAG_DISPLAY_DISPLAY_ACTUAL )
            
            return self._EditNamespaceSort( default )
            
        
        def _ConvertNamespaceTupleToSortString( self, sort_data ):
            
            ( namespaces, tag_display_type ) = sort_data
            
            return '-'.join( namespaces )
            
        
        def _EditNamespaceSort( self, sort_data ):
            
            return ClientGUITags.EditNamespaceSort( self, sort_data )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetDefaultTagSort( CC.TAG_PRESENTATION_SEARCH_PAGE, self._default_tag_sort_search_page.GetValue() )
            self._new_options.SetDefaultTagSort( CC.TAG_PRESENTATION_SEARCH_PAGE_MANAGE_TAGS, self._default_tag_sort_search_page_manage_tags.GetValue() )
            self._new_options.SetDefaultTagSort( CC.TAG_PRESENTATION_MEDIA_VIEWER, self._default_tag_sort_media_viewer.GetValue() )
            self._new_options.SetDefaultTagSort( CC.TAG_PRESENTATION_MEDIA_VIEWER_MANAGE_TAGS, self._default_tag_sort_media_viewer_manage_tags.GetValue() )
            
            self._new_options.SetDefaultSort( self._default_media_sort.GetSort() )
            self._new_options.SetFallbackSort( self._fallback_media_sort.GetSort() )
            self._new_options.SetBoolean( 'save_page_sort_on_change', self._save_page_sort_on_change.isChecked() )
            self._new_options.SetDefaultCollect( self._default_media_collect.GetValue() )
            
            namespace_sorts = [ ClientMedia.MediaSort( sort_type = ( 'namespaces', sort_data ) ) for sort_data in self._namespace_sort_by.GetData() ]
            
            self._new_options.SetDefaultNamespaceSorts( namespace_sorts )
            
        
    
    class _SpeedAndMemoryPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            thumbnail_cache_panel = ClientGUICommon.StaticBox( self, 'thumbnail cache' )
            
            self._thumbnail_cache_size = ClientGUIControls.BytesControl( thumbnail_cache_panel )
            self._thumbnail_cache_size.valueChanged.connect( self.EventThumbnailsUpdate )
            
            tt = 'When thumbnails are loaded from disk, their bitmaps are saved for a while in memory so near-future access is super fast. If the total store of thumbnails exceeds this size setting, the least-recent-to-be-accessed will be discarded until the total size is less than it again.'
            tt += os.linesep * 2
            tt += 'Most thumbnails are RGB, which means their size here is roughly [width x height x 3].'
            
            self._thumbnail_cache_size.setToolTip( tt )
            
            self._estimated_number_thumbnails = QW.QLabel( '', thumbnail_cache_panel )
            
            self._thumbnail_cache_timeout = ClientGUITime.TimeDeltaButton( thumbnail_cache_panel, min = 300, days = True, hours = True, minutes = True )
            
            tt = 'The amount of not-accessed time after which a thumbnail will naturally be removed from the cache.'
            
            self._thumbnail_cache_timeout.setToolTip( tt )
            
            image_cache_panel = ClientGUICommon.StaticBox( self, 'image cache' )
            
            self._image_cache_size = ClientGUIControls.BytesControl( image_cache_panel )
            self._image_cache_size.valueChanged.connect( self.EventImageCacheUpdate )
            
            tt = 'When images are loaded from disk, their 100% zoom renders are saved for a while in memory so near-future access is super fast. If the total store of images exceeds this size setting, the least-recent-to-be-accessed will be discarded until the total size is less than it again.'
            tt += os.linesep * 2
            tt += 'Most images are RGB, which means their size here is roughly [width x height x 3], with those dimensions being at 100% zoom.'
            
            self._image_cache_size.setToolTip( tt )
            
            self._estimated_number_fullscreens = QW.QLabel( '', image_cache_panel )
            
            self._image_cache_timeout = ClientGUITime.TimeDeltaButton( image_cache_panel, min = 300, days = True, hours = True, minutes = True )
            
            tt = 'The amount of not-accessed time after which a rendered image will naturally be removed from the cache.'
            
            self._image_cache_timeout.setToolTip( tt )
            
            self._image_cache_storage_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 20, max = 50 )
            
            tt = 'This option sets how much of the cache can go towards one image. If an image\'s total size (usually width x height x 3) is too large compared to the cache, it should not be cached or it will just flush everything else out in one stroke.'
            
            self._image_cache_storage_limit_percentage.setToolTip( tt )
            
            self._image_cache_storage_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
            
            tt = 'This represents the typical size we are talking about at this percentage level. Could be wider or taller, but overall should have the same number of pixels. Anything smaller will be saved in the cache after load, anything larger will be loaded on demand and forgotten as soon as you navigate away. If you want to have persistent fast access to images bigger than this, increase the total image cache size and/or the max % value permitted.'
            
            self._image_cache_storage_limit_percentage_st.setToolTip( tt )
            
            self._image_cache_prefetch_limit_percentage = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 5, max = 20 )
            
            tt = 'If you are browsing many big files, this option stops the prefetcher from overloading your cache by loading up seven or more gigantic images that each competitively flush each other out and need to be re-rendered over and over.'
            
            self._image_cache_prefetch_limit_percentage.setToolTip( tt )
            
            self._image_cache_prefetch_limit_percentage_st = ClientGUICommon.BetterStaticText( image_cache_panel, label = '' )
            
            tt = 'This represents the typical size we are talking about at this percentage level. Could be wider or taller, but overall should have the same number of pixels. Anything smaller will be pre-fetched, anything larger will be loaded on demand. If you want images bigger than this to load fast as you browse, increase the total image cache size and/or the max % value permitted.'
            
            self._image_cache_prefetch_limit_percentage_st.setToolTip( tt )
            
            self._media_viewer_prefetch_delay_base_ms = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 2000 )
            
            tt = 'How long to wait, after the current image is rendered, to start rendering neighbours. Does not matter so much any more, but if you have CPU lag, you can try boosting it a bit.'
            
            self._media_viewer_prefetch_delay_base_ms.setToolTip( tt )
            
            self._media_viewer_prefetch_num_previous = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 50 )
            self._media_viewer_prefetch_num_next = ClientGUICommon.BetterSpinBox( image_cache_panel, min = 0, max = 50 )
            
            self._prefetch_label_warning = ClientGUICommon.BetterStaticText( image_cache_panel )
            self._prefetch_label_warning.setToolTip( 'If you boost the prefetch numbers, make sure your image cache is big enough to handle it! Doubly so if you frequently load images that at 100% are far larger than your screen size. You really don\'t want to be prefetching more than your cache can hold!' )
            
            image_tile_cache_panel = ClientGUICommon.StaticBox( self, 'image tile cache' )
            
            self._image_tile_cache_size = ClientGUIControls.BytesControl( image_tile_cache_panel )
            self._image_tile_cache_size.valueChanged.connect( self.EventImageTilesUpdate )
            
            tt = 'Zooming and displaying an image is expensive. When an image is rendered to screen at a particular zoom, the client breaks the virtual canvas into tiles and only scales and draws the image onto the viewable ones. As you pan around, new tiles may be needed and old ones discarded. It is all cached so you can pan and zoom over the same areas quickly.'
            
            self._image_tile_cache_size.setToolTip( tt )
            
            self._estimated_number_image_tiles = QW.QLabel( '', image_tile_cache_panel )
            
            tt = 'You do not need to go crazy here unless you do a huge amount of zooming and really need multiple zoom levels cached for 10+ files you are comparing with each other.'
            
            self._estimated_number_image_tiles.setToolTip( tt )
            
            self._image_tile_cache_timeout = ClientGUITime.TimeDeltaButton( image_tile_cache_panel, min = 300, hours = True, minutes = True )
            
            tt = 'The amount of not-accessed time after which a rendered tile will naturally be removed from the cache.'
            
            self._image_tile_cache_timeout.setToolTip( tt )
            
            self._ideal_tile_dimension = ClientGUICommon.BetterSpinBox( image_tile_cache_panel, min = 256, max = 4096 )
            
            tt = 'This is the screen-visible square size the system will aim for. Smaller tiles are more memory efficient but prone to warping and other artifacts. Extreme values may waste CPU.'
            
            self._ideal_tile_dimension.setToolTip( tt )
            
            #
            
            buffer_panel = ClientGUICommon.StaticBox( self, 'video buffer' )
            
            self._video_buffer_size = ClientGUIControls.BytesControl( buffer_panel )
            self._video_buffer_size.valueChanged.connect( self.EventVideoBufferUpdate )
            
            self._estimated_number_video_frames = QW.QLabel( '', buffer_panel )
            
            #
            
            self._thumbnail_cache_size.SetValue( self._new_options.GetInteger( 'thumbnail_cache_size' ) )
            self._image_cache_size.SetValue( self._new_options.GetInteger( 'image_cache_size' ) )
            self._image_tile_cache_size.SetValue( self._new_options.GetInteger( 'image_tile_cache_size' ) )
            
            self._thumbnail_cache_timeout.SetValue( self._new_options.GetInteger( 'thumbnail_cache_timeout' ) )
            self._image_cache_timeout.SetValue( self._new_options.GetInteger( 'image_cache_timeout' ) )
            self._image_tile_cache_timeout.SetValue( self._new_options.GetInteger( 'image_tile_cache_timeout' ) )
            
            self._ideal_tile_dimension.setValue( self._new_options.GetInteger( 'ideal_tile_dimension' ) )
            
            self._video_buffer_size.SetValue( self._new_options.GetInteger( 'video_buffer_size' ) )
            
            self._media_viewer_prefetch_delay_base_ms.setValue( self._new_options.GetInteger( 'media_viewer_prefetch_delay_base_ms' ) )
            self._media_viewer_prefetch_num_previous.setValue( self._new_options.GetInteger( 'media_viewer_prefetch_num_previous' ) )
            self._media_viewer_prefetch_num_next.setValue( self._new_options.GetInteger( 'media_viewer_prefetch_num_next' ) )
            
            self._image_cache_storage_limit_percentage.setValue( self._new_options.GetInteger( 'image_cache_storage_limit_percentage' ) )
            self._image_cache_prefetch_limit_percentage.setValue( self._new_options.GetInteger( 'image_cache_prefetch_limit_percentage' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            text = 'These options are advanced! PROTIP: Do not go crazy here.'
            
            st = ClientGUICommon.BetterStaticText( self, text )
            
            QP.AddToLayout( vbox, st, CC.FLAGS_CENTER )
            
            #
            
            thumbnails_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( thumbnails_sizer, self._thumbnail_cache_size, CC.FLAGS_CENTER )
            QP.AddToLayout( thumbnails_sizer, self._estimated_number_thumbnails, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            fullscreens_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( fullscreens_sizer, self._image_cache_size, CC.FLAGS_CENTER_PERPENDICULAR )
            QP.AddToLayout( fullscreens_sizer, self._estimated_number_fullscreens, CC.FLAGS_CENTER_PERPENDICULAR )
            
            image_tiles_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( image_tiles_sizer, self._image_tile_cache_size, CC.FLAGS_CENTER_PERPENDICULAR )
            QP.AddToLayout( image_tiles_sizer, self._estimated_number_image_tiles, CC.FLAGS_CENTER_PERPENDICULAR )
            
            image_cache_storage_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( image_cache_storage_sizer, self._image_cache_storage_limit_percentage, CC.FLAGS_CENTER )
            QP.AddToLayout( image_cache_storage_sizer, self._image_cache_storage_limit_percentage_st, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            image_cache_prefetch_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( image_cache_prefetch_sizer, self._image_cache_prefetch_limit_percentage, CC.FLAGS_CENTER )
            QP.AddToLayout( image_cache_prefetch_sizer, self._image_cache_prefetch_limit_percentage_st, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            video_buffer_sizer = QP.HBoxLayout()
            
            QP.AddToLayout( video_buffer_sizer, self._video_buffer_size, CC.FLAGS_CENTER_PERPENDICULAR )
            QP.AddToLayout( video_buffer_sizer, self._estimated_number_video_frames, CC.FLAGS_CENTER_PERPENDICULAR )
            
            #
            
            text = 'Does not change much, thumbs are cheap.'
            
            st = ClientGUICommon.BetterStaticText( thumbnail_cache_panel, text )
            
            thumbnail_cache_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Memory reserved for thumbnail cache:', thumbnails_sizer ) )
            rows.append( ( 'Thumbnail cache timeout:', self._thumbnail_cache_timeout ) )
            
            gridbox = ClientGUICommon.WrapInGrid( thumbnail_cache_panel, rows )
            
            thumbnail_cache_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, thumbnail_cache_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            text = 'Important if you want smooth navigation between different images in the media viewer. If you deal with huge images, bump up cache size and max size that can be cached or prefetched, but be prepared to pay the memory price.'
            text += os.linesep * 2
            text += 'Allowing more prefetch is great, but it needs CPU.'
            
            st = ClientGUICommon.BetterStaticText( image_cache_panel, text )
            
            st.setWordWrap( True )
            
            image_cache_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Memory reserved for image cache:', fullscreens_sizer ) )
            rows.append( ( 'Image cache timeout:', self._image_cache_timeout ) )
            rows.append( ( 'Maximum image size (in % of cache) that can be cached:', image_cache_storage_sizer ) )
            rows.append( ( 'Maximum image size (in % of cache) that will be prefetched:', image_cache_prefetch_sizer ) )
            rows.append( ( 'Base ms delay for media viewer neighbour render prefetch:', self._media_viewer_prefetch_delay_base_ms ) )
            rows.append( ( 'Num previous to prefetch:', self._media_viewer_prefetch_num_previous ) )
            rows.append( ( 'Num next to prefetch:', self._media_viewer_prefetch_num_next ) )
            rows.append( ( 'Prefetch numbers are good?:', self._prefetch_label_warning ) )
            
            gridbox = ClientGUICommon.WrapInGrid( image_cache_panel, rows )
            
            image_cache_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, image_cache_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            text = 'Important if you do a lot of zooming in and out on the same image or a small number of comparison images.'
            
            st = ClientGUICommon.BetterStaticText( image_tile_cache_panel, text )
            
            image_tile_cache_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Memory reserved for image tile cache:', image_tiles_sizer ) )
            rows.append( ( 'Image tile cache timeout:', self._image_tile_cache_timeout ) )
            rows.append( ( 'Ideal tile width/height px:', self._ideal_tile_dimension ) )
            
            gridbox = ClientGUICommon.WrapInGrid( image_tile_cache_panel, rows )
            
            image_tile_cache_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, image_tile_cache_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            text = 'This old option does not apply to mpv! It only applies to the native hydrus animation renderer!'
            text += os.linesep
            text += 'Hydrus video rendering is CPU intensive.'
            text += os.linesep
            text += 'If you have a lot of memory, you can set a generous potential video buffer to compensate.'
            text += os.linesep
            text += 'If the video buffer can hold an entire video, it only needs to be rendered once and will play and loop very smoothly.'
            text += os.linesep
            text += 'PROTIP: Do not go crazy here.'
            
            st = ClientGUICommon.BetterStaticText( buffer_panel, text )
            
            st.setWordWrap( True )
            
            buffer_panel.Add( st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Memory for video buffer: ', video_buffer_sizer ) )
            
            gridbox = ClientGUICommon.WrapInGrid( buffer_panel, rows )
            
            buffer_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, buffer_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
            #
            
            self._image_cache_storage_limit_percentage.valueChanged.connect( self.EventImageCacheUpdate )
            self._image_cache_prefetch_limit_percentage.valueChanged.connect( self.EventImageCacheUpdate )
            
            self._media_viewer_prefetch_num_previous.valueChanged.connect( self.EventImageCacheUpdate )
            self._media_viewer_prefetch_num_next.valueChanged.connect( self.EventImageCacheUpdate )
            
            self.EventImageCacheUpdate()
            self.EventThumbnailsUpdate()
            self.EventImageTilesUpdate()
            self.EventVideoBufferUpdate()
            
        
        def EventImageCacheUpdate( self ):
            
            cache_size = self._image_cache_size.GetValue()
            
            display_size = ClientGUIFunctions.GetDisplaySize( self )
            
            estimated_bytes_per_fullscreen = 3 * display_size.width() * display_size.height()
            
            image_cache_estimate = cache_size // estimated_bytes_per_fullscreen
            
            self._estimated_number_fullscreens.setText( '(about {}-{} images the size of your screen)'.format( HydrusData.ToHumanInt( image_cache_estimate // 2 ), HydrusData.ToHumanInt( image_cache_estimate * 2 ) ) )
            
            num_pixels = cache_size * ( self._image_cache_storage_limit_percentage.value() / 100 ) / 3
            
            unit_square = num_pixels / ( 16 * 9 )
            
            unit_length = unit_square ** 0.5
            
            resolution = ( int( 16 * unit_length ), int( 9 * unit_length ) )
            
            self._image_cache_storage_limit_percentage_st.setText( '% - {} pixels, or about a {} image'.format( HydrusData.ToHumanInt( num_pixels ), HydrusData.ConvertResolutionToPrettyString( resolution ) ) )
            
            num_pixels = cache_size * ( self._image_cache_prefetch_limit_percentage.value() / 100 ) / 3
            
            unit_square = num_pixels / ( 16 * 9 )
            
            unit_length = unit_square ** 0.5
            
            resolution = ( int( 16 * unit_length ), int( 9 * unit_length ) )
            
            self._image_cache_prefetch_limit_percentage_st.setText( '% - {} pixels, or about a {} image'.format( HydrusData.ToHumanInt( num_pixels ), HydrusData.ConvertResolutionToPrettyString( resolution ) ) )
            
            #
            
            num_prefetch = 1 + self._media_viewer_prefetch_num_previous.value() + self._media_viewer_prefetch_num_next.value()
            
            if num_prefetch > image_cache_estimate // 2:
                
                label = 'No, reduce or make your image cache bigger!'
                object_name = 'HydrusWarning'
                
            else:
                
                label = 'Yes, looks good!'
                object_name = ''
                
            
            self._prefetch_label_warning.setText( label )
            
            if object_name != self._prefetch_label_warning.objectName():
                
                self._prefetch_label_warning.setObjectName( object_name )
                
                self._prefetch_label_warning.style().polish( self._prefetch_label_warning )
                
            
        
        def EventImageTilesUpdate( self ):
            
            value = self._image_tile_cache_size.GetValue()
            
            display_size = ClientGUIFunctions.GetDisplaySize( self )
            
            estimated_bytes_per_fullscreen = 3 * display_size.width() * display_size.height()
            
            estimate = value // estimated_bytes_per_fullscreen
            
            self._estimated_number_image_tiles.setText( '(about {} fullscreens)'.format( HydrusData.ToHumanInt( estimate ) ) )
            
        
        def EventThumbnailsUpdate( self ):
            
            value = self._thumbnail_cache_size.GetValue()
            
            ( thumbnail_width, thumbnail_height ) = HC.options[ 'thumbnail_dimensions' ]
            
            res_string = HydrusData.ConvertResolutionToPrettyString( ( thumbnail_width, thumbnail_height ) )
            
            estimated_bytes_per_thumb = 3 * thumbnail_width * thumbnail_height
            
            estimated_thumbs = value // estimated_bytes_per_thumb
            
            self._estimated_number_thumbnails.setText( '(at '+res_string+', about '+HydrusData.ToHumanInt(estimated_thumbs)+' thumbnails)' )
            
        
        def EventVideoBufferUpdate( self ):
            
            value = self._video_buffer_size.GetValue()
            
            estimated_720p_frames = int( value // ( 1280 * 720 * 3 ) )
            
            self._estimated_number_video_frames.setText( '(about '+HydrusData.ToHumanInt(estimated_720p_frames)+' frames of 720p video)' )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetInteger( 'thumbnail_cache_size', self._thumbnail_cache_size.GetValue() )
            self._new_options.SetInteger( 'image_cache_size', self._image_cache_size.GetValue() )
            self._new_options.SetInteger( 'image_tile_cache_size', self._image_tile_cache_size.GetValue() )
            
            self._new_options.SetInteger( 'thumbnail_cache_timeout', self._thumbnail_cache_timeout.GetValue() )
            self._new_options.SetInteger( 'image_cache_timeout', self._image_cache_timeout.GetValue() )
            self._new_options.SetInteger( 'image_tile_cache_timeout', self._image_tile_cache_timeout.GetValue() )
            
            self._new_options.SetInteger( 'ideal_tile_dimension', self._ideal_tile_dimension.value() )
            
            self._new_options.SetInteger( 'media_viewer_prefetch_delay_base_ms', self._media_viewer_prefetch_delay_base_ms.value() )
            self._new_options.SetInteger( 'media_viewer_prefetch_num_previous', self._media_viewer_prefetch_num_previous.value() )
            self._new_options.SetInteger( 'media_viewer_prefetch_num_next', self._media_viewer_prefetch_num_next.value() )
            
            self._new_options.SetInteger( 'image_cache_storage_limit_percentage', self._image_cache_storage_limit_percentage.value() )
            self._new_options.SetInteger( 'image_cache_prefetch_limit_percentage', self._image_cache_prefetch_limit_percentage.value() )
            
            self._new_options.SetInteger( 'video_buffer_size', self._video_buffer_size.GetValue() )
            
        
    
    class _StylePanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            self._qt_style_name = ClientGUICommon.BetterChoice( self )
            self._qt_stylesheet_name = ClientGUICommon.BetterChoice( self )
            
            self._qt_style_name.addItem( 'use default ("{}")'.format( ClientGUIStyle.ORIGINAL_STYLE_NAME ), None )
            
            try:
                
                for name in ClientGUIStyle.GetAvailableStyles():
                    
                    self._qt_style_name.addItem( name, name )
                    
                
            except HydrusExceptions.DataMissing as e:
                
                HydrusData.ShowException( e )
                
            
            self._qt_stylesheet_name.addItem( 'use default', None )
            
            try:
                
                for name in ClientGUIStyle.GetAvailableStylesheets():
                    
                    self._qt_stylesheet_name.addItem( name, name )
                    
                
            except HydrusExceptions.DataMissing as e:
                
                HydrusData.ShowException( e )
                
            
            #
            
            self._qt_style_name.SetValue( self._new_options.GetNoneableString( 'qt_style_name' ) )
            self._qt_stylesheet_name.SetValue( self._new_options.GetNoneableString( 'qt_stylesheet_name' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            #
            
            text = 'The current styles are what your Qt has available, the stylesheets are what .css and .qss files are currently in install_dir/static/qss.'
            text += '\n' * 2
            text += 'Note that there are several colours not handled by this yet. Check out the "colours" page of this options to change them.'
            
            st = ClientGUICommon.BetterStaticText( self, label = text )
            
            QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Qt style:', self._qt_style_name ) )
            rows.append( ( 'Qt stylesheet:', self._qt_stylesheet_name ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            self.setLayout( vbox )
            
            self._qt_style_name.currentIndexChanged.connect( self.StyleChanged )
            self._qt_stylesheet_name.currentIndexChanged.connect( self.StyleChanged )
            
        
        def StyleChanged( self ):
            
            qt_style_name = self._qt_style_name.GetValue()
            qt_stylesheet_name = self._qt_stylesheet_name.GetValue()
            
            try:
                
                if qt_style_name is None:
                    
                    ClientGUIStyle.SetStyleFromName( ClientGUIStyle.ORIGINAL_STYLE_NAME )
                    
                else:
                    
                    ClientGUIStyle.SetStyleFromName( qt_style_name )
                    
                
            except Exception as e:
                
                HydrusData.PrintException( e )
                
                ClientGUIDialogsMessage.ShowCritical( self, 'Critical', f'Could not apply style: {e}' )
                
            
            try:
                
                if qt_stylesheet_name is None:
                    
                    ClientGUIStyle.ClearStylesheet()
                    
                else:
                    
                    ClientGUIStyle.SetStylesheetFromPath( qt_stylesheet_name )
                    
                
            except Exception as e:
                
                HydrusData.PrintException( e )
                
                ClientGUIDialogsMessage.ShowCritical( self, 'Critical', f'Could not apply stylesheet: {e}' )
                
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetNoneableString( 'qt_style_name', self._qt_style_name.GetValue() )
            self._new_options.SetNoneableString( 'qt_stylesheet_name', self._qt_stylesheet_name.GetValue() )
            
        
    
    class _SystemPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            sleep_panel = ClientGUICommon.StaticBox( self, 'system sleep' )
            
            self._wake_delay_period = ClientGUICommon.BetterSpinBox( sleep_panel, min = 0, max = 60 )
            
            tt = 'It sometimes takes a few seconds for your network adapter to reconnect after a wake. This adds a grace period after a detected wake-from-sleep to allow your OS to sort that out before Hydrus starts making requests.'
            
            self._wake_delay_period.setToolTip( tt )
            
            self._file_system_waits_on_wakeup = QW.QCheckBox( sleep_panel )
            self._file_system_waits_on_wakeup.setToolTip( 'This is useful if your hydrus is stored on a NAS that takes a few seconds to get going after your machine resumes from sleep.' )
            
            #
            
            self._wake_delay_period.setValue( self._new_options.GetInteger( 'wake_delay_period' ) )
            
            self._file_system_waits_on_wakeup.setChecked( self._new_options.GetBoolean( 'file_system_waits_on_wakeup' ) )
            
            #
            
            rows = []
            
            rows.append( ( 'After a wake from system sleep, wait this many seconds before allowing new network access:', self._wake_delay_period ) )
            rows.append( ( 'Include the file system in this wait: ', self._file_system_waits_on_wakeup ) )
            
            gridbox = ClientGUICommon.WrapInGrid( sleep_panel, rows )
            
            sleep_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, sleep_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetInteger( 'wake_delay_period', self._wake_delay_period.value() )
            self._new_options.SetBoolean( 'file_system_waits_on_wakeup', self._file_system_waits_on_wakeup.isChecked() )
            
        
    
    class _SystemTrayPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            self._always_show_system_tray_icon = QW.QCheckBox( self )
            self._minimise_client_to_system_tray = QW.QCheckBox( self )
            self._close_client_to_system_tray = QW.QCheckBox( self )
            self._start_client_in_system_tray = QW.QCheckBox( self )
            
            #
            
            self._always_show_system_tray_icon.setChecked( self._new_options.GetBoolean( 'always_show_system_tray_icon' ) )
            self._minimise_client_to_system_tray.setChecked( self._new_options.GetBoolean( 'minimise_client_to_system_tray' ) )
            self._close_client_to_system_tray.setChecked( self._new_options.GetBoolean( 'close_client_to_system_tray' ) )
            self._start_client_in_system_tray.setChecked( self._new_options.GetBoolean( 'start_client_in_system_tray' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Always show the hydrus system tray icon: ', self._always_show_system_tray_icon ) )
            rows.append( ( 'Minimise the main window to system tray: ', self._minimise_client_to_system_tray ) )
            rows.append( ( 'Close the main window to system tray: ', self._close_client_to_system_tray ) )
            rows.append( ( 'Start the client minimised to system tray: ', self._start_client_in_system_tray ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            from hydrus.client.gui import ClientGUISystemTray
            
            if not ClientGUISystemTray.SystemTrayAvailable():
                
                QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( self, 'Unfortunately, your system does not seem to have a supported system tray.' ), CC.FLAGS_EXPAND_PERPENDICULAR )
                
                self._always_show_system_tray_icon.setEnabled( False )
                self._minimise_client_to_system_tray.setEnabled( False )
                self._close_client_to_system_tray.setEnabled( False )
                self._start_client_in_system_tray.setEnabled( False )
                
            elif not HC.PLATFORM_WINDOWS:
                
                if not CG.client_controller.new_options.GetBoolean( 'advanced_mode' ):
                    
                    label = 'This is turned off for non-advanced non-Windows users for now.'
                    
                    self._always_show_system_tray_icon.setEnabled( False )
                    self._minimise_client_to_system_tray.setEnabled( False )
                    self._close_client_to_system_tray.setEnabled( False )
                    self._start_client_in_system_tray.setEnabled( False )
                    
                else:
                    
                    label = 'This can be buggy/crashy on non-Windows, hydev will keep working on this.'
                    
                
                QP.AddToLayout( vbox, ClientGUICommon.BetterStaticText( self, label ), CC.FLAGS_EXPAND_PERPENDICULAR )
                
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_PERPENDICULAR )
            vbox.addStretch( 1 )
            
            self.setLayout( vbox )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetBoolean( 'always_show_system_tray_icon', self._always_show_system_tray_icon.isChecked() )
            self._new_options.SetBoolean( 'minimise_client_to_system_tray', self._minimise_client_to_system_tray.isChecked() )
            self._new_options.SetBoolean( 'close_client_to_system_tray', self._close_client_to_system_tray.isChecked() )
            self._new_options.SetBoolean( 'start_client_in_system_tray', self._start_client_in_system_tray.isChecked() )
            
        
    
    class _TagsPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            general_panel = ClientGUICommon.StaticBox( self, 'general tag options' )
            
            self._expand_parents_on_storage_taglists = QW.QCheckBox( general_panel )
            self._expand_parents_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
            
            self._show_parent_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
            self._show_parent_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
            self._show_sibling_decorators_on_storage_taglists = QW.QCheckBox( general_panel )
            self._show_sibling_decorators_on_storage_autocomplete_taglists = QW.QCheckBox( general_panel )
            
            self._num_recent_petition_reasons = ClientGUICommon.BetterSpinBox( general_panel, initial = 5, min = 0, max = 100 )
            tt = 'In manage tags, tag siblings, and tag parents, you may be asked to provide a reason with a petition you make to a hydrus repository. There are some fixed reasons, but the dialog can also remember what you recently typed. This controls how many recent reasons it will remember.'
            self._num_recent_petition_reasons.setToolTip( tt )
            
            self._ac_select_first_with_count = QW.QCheckBox( general_panel )
            
            #
            
            favourites_panel = ClientGUICommon.StaticBox( self, 'favourite tags' )
            
            desc = 'These tags will appear in your tag autocomplete results area, under the \'favourites\' tab.'
            
            favourites_st = ClientGUICommon.BetterStaticText( favourites_panel, desc )
            favourites_st.setWordWrap( True )
            
            default_location_context = CG.client_controller.new_options.GetDefaultLocalLocationContext()
            
            self._favourites = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( favourites_panel, CC.COMBINED_TAG_SERVICE_KEY, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE )
            self._favourites_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( favourites_panel, self._favourites.AddTags, default_location_context, CC.COMBINED_TAG_SERVICE_KEY, show_paste_button = True )
            
            #
            
            self._expand_parents_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_taglists' ) )
            self._expand_parents_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents hang below tags.' )
            
            self._expand_parents_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'expand_parents_on_storage_autocomplete_taglists' ) )
            self._expand_parents_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
            
            self._show_parent_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_taglists' ) )
            self._show_parent_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and implied parents either hang below tags or summarise in a suffix.' )
            
            self._show_parent_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists' ) )
            self._show_parent_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
            
            self._show_sibling_decorators_on_storage_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_taglists' ) )
            self._show_sibling_decorators_on_storage_taglists.setToolTip( 'This affects taglists in places like the manage tags dialog, where you edit tags as they actually are, and siblings summarise in a suffix.' )
            
            self._show_sibling_decorators_on_storage_autocomplete_taglists.setChecked( self._new_options.GetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists' ) )
            self._show_sibling_decorators_on_storage_autocomplete_taglists.setToolTip( 'This affects the autocomplete results taglist.' )
            
            self._num_recent_petition_reasons.setValue( self._new_options.GetInteger( 'num_recent_petition_reasons' ) )
            
            self._ac_select_first_with_count.setChecked( self._new_options.GetBoolean( 'ac_select_first_with_count' ) )
            
            #
            
            self._favourites.SetTags( self._new_options.GetStringList( 'favourite_tags' ) )
            
            #
            
            vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Show parent info by default on edit/write taglists: ', self._show_parent_decorators_on_storage_taglists ) )
            rows.append( ( 'Show parent info by default on edit/write autocomplete taglists: ', self._show_parent_decorators_on_storage_autocomplete_taglists ) )
            rows.append( ( 'Show parents expanded by default on edit/write taglists: ', self._expand_parents_on_storage_taglists ) )
            rows.append( ( 'Show parents expanded by default on edit/write autocomplete taglists: ', self._expand_parents_on_storage_autocomplete_taglists ) )
            rows.append( ( 'Show sibling info by default on edit/write taglists: ', self._show_sibling_decorators_on_storage_taglists ) )
            rows.append( ( 'Show sibling info by default on edit/write autocomplete taglists: ', self._show_sibling_decorators_on_storage_autocomplete_taglists ) )
            rows.append( ( 'Number of recent petition reasons to remember in dialogs: ', self._num_recent_petition_reasons ) )
            rows.append( ( 'By default, select the first tag result with actual count in write-autocomplete: ', self._ac_select_first_with_count ) )
            
            gridbox = ClientGUICommon.WrapInGrid( general_panel, rows )
            
            general_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, general_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            favourites_panel.Add( favourites_st, CC.FLAGS_EXPAND_PERPENDICULAR )
            favourites_panel.Add( self._favourites, CC.FLAGS_EXPAND_BOTH_WAYS )
            favourites_panel.Add( self._favourites_input )
            
            QP.AddToLayout( vbox, favourites_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            self.setLayout( vbox )
            
            #
            
            self._favourites_input.tagsPasted.connect( self.AddTagsOnlyAdd )
            
        
        def AddTagsOnlyAdd( self, tags ):
            
            current_tags = self._favourites.GetTags()
            
            tags = { tag for tag in tags if tag not in current_tags }
            
            if len( tags ) > 0:
                
                self._favourites.AddTags( tags )
                
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetBoolean( 'show_parent_decorators_on_storage_taglists', self._show_parent_decorators_on_storage_taglists.isChecked() )
            self._new_options.SetBoolean( 'show_parent_decorators_on_storage_autocomplete_taglists', self._show_parent_decorators_on_storage_autocomplete_taglists.isChecked() )
            self._new_options.SetBoolean( 'expand_parents_on_storage_taglists', self._expand_parents_on_storage_taglists.isChecked() )
            self._new_options.SetBoolean( 'expand_parents_on_storage_autocomplete_taglists', self._expand_parents_on_storage_autocomplete_taglists.isChecked() )
            self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_taglists', self._show_sibling_decorators_on_storage_taglists.isChecked() )
            self._new_options.SetBoolean( 'show_sibling_decorators_on_storage_autocomplete_taglists', self._show_sibling_decorators_on_storage_autocomplete_taglists.isChecked() )
            
            self._new_options.SetInteger( 'num_recent_petition_reasons', self._num_recent_petition_reasons.value() )
            
            self._new_options.SetBoolean( 'ac_select_first_with_count', self._ac_select_first_with_count.isChecked() )
            
            #
            
            self._new_options.SetStringList( 'favourite_tags', list( self._favourites.GetTags() ) )
            
        
    
    class _TagPresentationPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            #
            
            tag_summary_generator = self._new_options.GetTagSummaryGenerator( 'thumbnail_top' )
            
            self._thumbnail_top = ClientGUITags.TagSummaryGeneratorButton( self, tag_summary_generator )
            
            tag_summary_generator = self._new_options.GetTagSummaryGenerator( 'thumbnail_bottom_right' )
            
            self._thumbnail_bottom_right = ClientGUITags.TagSummaryGeneratorButton( self, tag_summary_generator )
            
            tag_summary_generator = self._new_options.GetTagSummaryGenerator( 'media_viewer_top' )
            
            self._media_viewer_top = ClientGUITags.TagSummaryGeneratorButton( self, tag_summary_generator )
            
            #
            
            render_panel = ClientGUICommon.StaticBox( self, 'namespace rendering' )
            
            render_st = ClientGUICommon.BetterStaticText( render_panel, label = 'Namespaced tags are stored and directly edited in hydrus as "namespace:subtag", but most presentation windows can display them differently.' )
            
            self._show_namespaces = QW.QCheckBox( render_panel )
            self._show_number_namespaces = QW.QCheckBox( render_panel )
            self._show_number_namespaces.setToolTip( 'This lets unnamespaced "16:9" show as that, not hiding the "16".' )
            self._show_subtag_number_namespaces = QW.QCheckBox( render_panel )
            self._show_subtag_number_namespaces.setToolTip( 'This lets unnamespaced "page:3" show as that, not hiding the "page" where it can get mixed with chapter etc...' )
            self._namespace_connector = QW.QLineEdit( render_panel )
            self._sibling_connector = QW.QLineEdit( render_panel )
            
            self._fade_sibling_connector = QW.QCheckBox( render_panel )
            tt = 'If set, then if the sibling goes from one namespace to another, that colour will fade across the distance of the sibling connector. Just a bit of fun.'
            self._fade_sibling_connector.setToolTip( tt )
            
            self._sibling_connector_custom_namespace_colour = ClientGUICommon.NoneableTextCtrl( render_panel, none_phrase = 'use ideal tag colour' )
            tt = 'The sibling connector can use a particular namespace\'s colour.'
            self._sibling_connector_custom_namespace_colour.setToolTip( tt )
            
            self._or_connector = QW.QLineEdit( render_panel )
            tt = 'When an OR predicate is rendered, it splits the components by this text.'
            self._or_connector.setToolTip( tt )
            
            self._or_connector_custom_namespace_colour = QW.QLineEdit( render_panel )
            tt = 'The OR connector can use a particular namespace\'s colour.'
            self._or_connector_custom_namespace_colour.setToolTip( tt )
            
            self._replace_tag_underscores_with_spaces = QW.QCheckBox( render_panel )
            
            #
            
            namespace_colours_panel = ClientGUICommon.StaticBox( self, 'namespace colours' )
            
            self._namespace_colours = ClientGUIListBoxes.ListBoxTagsColourOptions( namespace_colours_panel, HC.options[ 'namespace_colours' ] )
            
            self._add_namespace_colour = ClientGUICommon.BetterButton( self, 'add', self._AddNamespaceColour )
            self._edit_namespace_colour = ClientGUICommon.BetterButton( self, 'edit', self._EditNamespaceColour )
            self._delete_namespace_colour = ClientGUICommon.BetterButton( self, 'delete', self._DeleteNamespaceColour )
            
            #
            
            self._show_namespaces.setChecked( new_options.GetBoolean( 'show_namespaces' ) )
            self._show_number_namespaces.setChecked( new_options.GetBoolean( 'show_number_namespaces' ) )
            self._show_subtag_number_namespaces.setChecked( new_options.GetBoolean( 'show_subtag_number_namespaces' ) )
            self._namespace_connector.setText( new_options.GetString( 'namespace_connector' ) )
            self._replace_tag_underscores_with_spaces.setChecked( new_options.GetBoolean( 'replace_tag_underscores_with_spaces' ) )
            self._sibling_connector.setText( new_options.GetString( 'sibling_connector' ) )
            self._fade_sibling_connector.setChecked( new_options.GetBoolean( 'fade_sibling_connector' ) )
            self._sibling_connector_custom_namespace_colour.SetValue( new_options.GetNoneableString( 'sibling_connector_custom_namespace_colour' ) )
            self._or_connector.setText( new_options.GetString( 'or_connector' ) )
            self._or_connector_custom_namespace_colour.setText( new_options.GetNoneableString( 'or_connector_custom_namespace_colour' ) )
            
            #
            
            namespace_colours_panel.Add( self._namespace_colours, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            hbox = QP.HBoxLayout()
            
            QP.AddToLayout( hbox, self._add_namespace_colour, CC.FLAGS_EXPAND_BOTH_WAYS )
            QP.AddToLayout( hbox, self._edit_namespace_colour, CC.FLAGS_EXPAND_BOTH_WAYS )
            QP.AddToLayout( hbox, self._delete_namespace_colour, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            namespace_colours_panel.Add( hbox, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            vbox = QP.VBoxLayout()
            
            #
            
            rows = []
            
            rows.append( ( 'On thumbnail top:', self._thumbnail_top ) )
            rows.append( ( 'On thumbnail bottom-right:', self._thumbnail_bottom_right ) )
            rows.append( ( 'On media viewer top:', self._media_viewer_top ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            #
            
            rows = []
            
            rows.append( ( 'Show namespaces: ', self._show_namespaces ) )
            rows.append( ( 'Unless namespace is a number: ', self._show_number_namespaces ) )
            rows.append( ( 'Unless subtag is a number: ', self._show_subtag_number_namespaces ) )
            rows.append( ( 'If shown, namespace connecting string: ', self._namespace_connector ) )
            rows.append( ( 'Sibling connecting string: ', self._sibling_connector ) )
            rows.append( ( 'Fade the colour of the sibling connector string on Qt6: ', self._fade_sibling_connector ) )
            rows.append( ( 'Namespace for the colour of the sibling connecting string: ', self._sibling_connector_custom_namespace_colour ) )
            rows.append( ( 'OR connecting string: ', self._or_connector ) )
            rows.append( ( 'Namespace for the colour of the OR connecting string: ', self._or_connector_custom_namespace_colour ) )
            rows.append( ( 'EXPERIMENTAL: Replace all underscores with spaces: ', self._replace_tag_underscores_with_spaces ) )
            
            gridbox = ClientGUICommon.WrapInGrid( render_panel, rows )
            
            render_panel.Add( render_st, CC.FLAGS_EXPAND_PERPENDICULAR )
            render_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            QP.AddToLayout( vbox, render_panel, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            #
            
            QP.AddToLayout( vbox, namespace_colours_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            self._NamespacesUpdated()
            self._SiblingColourStuffClicked()
            
            self._show_namespaces.clicked.connect( self._NamespacesUpdated )
            self._fade_sibling_connector.clicked.connect( self._SiblingColourStuffClicked )
            
            self.setLayout( vbox )
            
        
        def _AddNamespaceColour( self ):
            
            with ClientGUIDialogs.DialogTextEntry( self, 'Enter the namespace', allow_blank = False ) as dlg:
                
                if dlg.exec() == QW.QDialog.Accepted:
                    
                    namespace = dlg.GetValue()
                    
                    namespace = namespace.lower().strip()
                    
                    if namespace.endswith( ':' ):
                        
                        namespace = namespace[:-1]
                        
                    
                    if namespace != 'system':
                        
                        namespace = HydrusTags.StripTextOfGumpf( namespace )
                        
                    
                    if namespace in ( '', ':' ):
                        
                        ClientGUIDialogsMessage.ShowWarning( self, 'Sorry, that namespace means unnamespaced/default namespaced, which are already listed.' )
                        
                        return
                        
                    
                    existing_namespaces = self._namespace_colours.GetNamespaceColours().keys()
                    
                    if namespace in existing_namespaces:
                        
                        ClientGUIDialogsMessage.ShowWarning( self, 'Sorry, that namespace is already listed!' )
                        
                        return
                        
                    
                    self._namespace_colours.SetNamespaceColour( namespace, QG.QColor( random.randint(0,255), random.randint(0,255), random.randint(0,255) ) )
                    
                
            
        
        def _DeleteNamespaceColour( self ):
            
            self._namespace_colours.DeleteSelected()
            
        
        def _EditNamespaceColour( self ):
            
            results = self._namespace_colours.GetSelectedNamespaceColours()
            
            for ( namespace, ( r, g, b ) ) in list( results.items() ):
                
                colour = QG.QColor( r, g, b )
                
                colour = ClientGUIColourPicker.EditColour( self, colour )
                
                self._namespace_colours.SetNamespaceColour( namespace, colour )
                
            
        
        def _SiblingColourStuffClicked( self ):
            
            choice_available = not self._fade_sibling_connector.isChecked()
            
            self._sibling_connector_custom_namespace_colour.setEnabled( choice_available )
            
        
        def _NamespacesUpdated( self ):
            
            self._show_number_namespaces.setEnabled( not self._show_namespaces.isChecked() )
            self._show_subtag_number_namespaces.setEnabled( not self._show_namespaces.isChecked() )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetTagSummaryGenerator( 'thumbnail_top', self._thumbnail_top.GetValue() )
            self._new_options.SetTagSummaryGenerator( 'thumbnail_bottom_right', self._thumbnail_bottom_right.GetValue() )
            self._new_options.SetTagSummaryGenerator( 'media_viewer_top', self._media_viewer_top.GetValue() )
            
            self._new_options.SetBoolean( 'show_namespaces', self._show_namespaces.isChecked() )
            self._new_options.SetBoolean( 'show_number_namespaces', self._show_number_namespaces.isChecked() )
            self._new_options.SetBoolean( 'show_subtag_number_namespaces', self._show_subtag_number_namespaces.isChecked() )
            self._new_options.SetString( 'namespace_connector', self._namespace_connector.text() )
            self._new_options.SetBoolean( 'replace_tag_underscores_with_spaces', self._replace_tag_underscores_with_spaces.isChecked() )
            self._new_options.SetString( 'sibling_connector', self._sibling_connector.text() )
            self._new_options.SetBoolean( 'fade_sibling_connector', self._fade_sibling_connector.isChecked() )
            
            self._new_options.SetNoneableString( 'sibling_connector_custom_namespace_colour', self._sibling_connector_custom_namespace_colour.GetValue() )
            
            self._new_options.SetString( 'or_connector', self._or_connector.text() )
            self._new_options.SetNoneableString( 'or_connector_custom_namespace_colour', self._or_connector_custom_namespace_colour.text() )
            
            HC.options[ 'namespace_colours' ] = self._namespace_colours.GetNamespaceColours()
            
        
    
    class _TagSuggestionsPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            suggested_tags_panel = ClientGUICommon.StaticBox( self, 'suggested tags' )
            
            self._suggested_tags_width = ClientGUICommon.BetterSpinBox( suggested_tags_panel, min=20, max=65535 )
            
            self._suggested_tags_layout = ClientGUICommon.BetterChoice( suggested_tags_panel )
            
            self._suggested_tags_layout.addItem( 'notebook', 'notebook' )
            self._suggested_tags_layout.addItem( 'side-by-side', 'columns' )
            
            self._default_suggested_tags_notebook_page = ClientGUICommon.BetterChoice( suggested_tags_panel )
            
            for item in [ 'favourites', 'related', 'file_lookup_scripts', 'recent' ]:
                
                label = 'most used' if item == 'favourites' else item
                
                self._default_suggested_tags_notebook_page.addItem( label, item )
                
            
            suggest_tags_panel_notebook = QW.QTabWidget( suggested_tags_panel )
            
            #
            
            suggested_tags_favourites_panel = QW.QWidget( suggest_tags_panel_notebook )
            
            suggested_tags_favourites_panel.setMinimumWidth( 400 )
            
            self._suggested_favourites_services = ClientGUICommon.BetterChoice( suggested_tags_favourites_panel )
            
            tag_services = CG.client_controller.services_manager.GetServices( HC.REAL_TAG_SERVICES )
            
            for tag_service in tag_services:
                
                self._suggested_favourites_services.addItem( tag_service.GetName(), tag_service.GetServiceKey() )
                
            
            self._suggested_favourites = ClientGUIListBoxes.ListBoxTagsStringsAddRemove( suggested_tags_favourites_panel, CC.COMBINED_TAG_SERVICE_KEY, tag_display_type = ClientTags.TAG_DISPLAY_STORAGE )
            
            self._current_suggested_favourites_service = None
            
            self._suggested_favourites_dict = {}
            
            default_location_context = CG.client_controller.new_options.GetDefaultLocalLocationContext()
            
            self._suggested_favourites_input = ClientGUIACDropdown.AutoCompleteDropdownTagsWrite( suggested_tags_favourites_panel, self._suggested_favourites.AddTags, default_location_context, CC.COMBINED_TAG_SERVICE_KEY, show_paste_button = True )
            
            #
            
            suggested_tags_related_panel = QW.QWidget( suggest_tags_panel_notebook )
            
            self._show_related_tags = QW.QCheckBox( suggested_tags_related_panel )
            
            self._related_tags_search_1_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
            self._related_tags_search_2_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
            self._related_tags_search_3_duration_ms = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min=50, max=60000 )
            
            self._related_tags_concurrence_threshold_percent = ClientGUICommon.BetterSpinBox( suggested_tags_related_panel, min = 1, max = 100 )
            tt = 'The related tags system looks for tags that tend to be used on the same files. Here you can set how strict it is. How many percent of tag A\'s files must tag B on for tag B to be a good suggestion? Higher numbers will mean fewer but more relevant suggestions.'
            self._related_tags_concurrence_threshold_percent.setToolTip( tt )
            
            #
            
            search_tag_slices_weight_box = ClientGUICommon.StaticBox( suggested_tags_related_panel, 'adjust scores by search tags' )
            
            search_tag_slices_weight_panel = ClientGUIListCtrl.BetterListCtrlPanel( search_tag_slices_weight_box )
            
            self._search_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( search_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditSearchTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteSearchTagSliceWeight )
            tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by the tag type that searched for them. Set to 0 to not search with that type of tag.'
            self._search_tag_slices_weights.setToolTip( tt )
            
            search_tag_slices_weight_panel.SetListCtrl( self._search_tag_slices_weights )
            
            search_tag_slices_weight_panel.AddButton( 'add', self._AddSearchTagSliceWeight )
            search_tag_slices_weight_panel.AddButton( 'edit', self._EditSearchTagSliceWeight, enabled_only_on_single_selection = True )
            search_tag_slices_weight_panel.AddDeleteButton( enabled_check_func = self._CanDeleteSearchTagSliceWeight )
            
            #
            
            result_tag_slices_weight_box = ClientGUICommon.StaticBox( suggested_tags_related_panel, 'adjust scores by suggested tags' )
            
            result_tag_slices_weight_panel = ClientGUIListCtrl.BetterListCtrlPanel( result_tag_slices_weight_box )
            
            self._result_tag_slices_weights = ClientGUIListCtrl.BetterListCtrl( result_tag_slices_weight_panel, CGLC.COLUMN_LIST_TAG_SLICE_WEIGHT.ID, 8, self._ConvertTagSliceAndWeightToListCtrlTuples, activation_callback = self._EditResultTagSliceWeight, use_simple_delete = True, can_delete_callback = self._CanDeleteResultTagSliceWeight )
            tt = 'ADVANCED! These weights adjust the ranking scores of suggested tags by their tag type. Set to 0 to not suggest that type of tag at all.'
            self._result_tag_slices_weights.setToolTip( tt )
            
            result_tag_slices_weight_panel.SetListCtrl( self._result_tag_slices_weights )
            
            result_tag_slices_weight_panel.AddButton( 'add', self._AddResultTagSliceWeight )
            result_tag_slices_weight_panel.AddButton( 'edit', self._EditResultTagSliceWeight, enabled_only_on_single_selection = True )
            result_tag_slices_weight_panel.AddDeleteButton( enabled_check_func = self._CanDeleteResultTagSliceWeight )
            
            #
            
            suggested_tags_file_lookup_script_panel = QW.QWidget( suggest_tags_panel_notebook )
            
            self._show_file_lookup_script_tags = QW.QCheckBox( suggested_tags_file_lookup_script_panel )
            
            self._favourite_file_lookup_script = ClientGUICommon.BetterChoice( suggested_tags_file_lookup_script_panel )
            
            script_names = sorted( CG.client_controller.Read( 'serialisable_names', HydrusSerialisable.SERIALISABLE_TYPE_PARSE_ROOT_FILE_LOOKUP ) )
            
            for name in script_names:
                
                self._favourite_file_lookup_script.addItem( name, name )
                
            
            #
            
            suggested_tags_recent_panel = QW.QWidget( suggest_tags_panel_notebook )
            
            self._num_recent_tags = ClientGUICommon.NoneableSpinCtrl( suggested_tags_recent_panel, 'number of recent tags to show', min = 1, none_phrase = 'do not show' )
            
            #
            
            self._suggested_tags_width.setValue( self._new_options.GetInteger( 'suggested_tags_width' ) )
            
            self._suggested_tags_layout.SetValue( self._new_options.GetNoneableString( 'suggested_tags_layout' ) )
            
            self._default_suggested_tags_notebook_page.SetValue( self._new_options.GetString( 'default_suggested_tags_notebook_page' ) )
            
            #
            
            self._show_related_tags.setChecked( self._new_options.GetBoolean( 'show_related_tags' ) )
            
            self._related_tags_search_1_duration_ms.setValue( self._new_options.GetInteger( 'related_tags_search_1_duration_ms' ) )
            self._related_tags_search_2_duration_ms.setValue( self._new_options.GetInteger( 'related_tags_search_2_duration_ms' ) )
            self._related_tags_search_3_duration_ms.setValue( self._new_options.GetInteger( 'related_tags_search_3_duration_ms' ) )
            
            self._related_tags_concurrence_threshold_percent.setValue( self._new_options.GetInteger( 'related_tags_concurrence_threshold_percent' ) )

            ( related_tags_search_tag_slices_weight_percent, related_tags_result_tag_slices_weight_percent ) = self._new_options.GetRelatedTagsTagSliceWeights()
            
            self._search_tag_slices_weights.SetData( related_tags_search_tag_slices_weight_percent )
            self._result_tag_slices_weights.SetData( related_tags_result_tag_slices_weight_percent )
            
            self._show_file_lookup_script_tags.setChecked( self._new_options.GetBoolean( 'show_file_lookup_script_tags' ) )
            
            self._favourite_file_lookup_script.SetValue( self._new_options.GetNoneableString( 'favourite_file_lookup_script' ) )
            
            self._num_recent_tags.SetValue( self._new_options.GetNoneableInteger( 'num_recent_tags' ) )
            
            #
            
            panel_vbox = QP.VBoxLayout()
            
            st = ClientGUICommon.BetterStaticText( suggested_tags_favourites_panel, 'Add your most used tags for each particular service here, and then you can just double-click to add, rather than typing every time.' )
            st.setWordWrap( True )
            
            QP.AddToLayout( panel_vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            rows = []
            
            rows.append( ( 'Tag service: ', self._suggested_favourites_services ) )
            
            gridbox = ClientGUICommon.WrapInGrid( suggested_tags_related_panel, rows )
            
            QP.AddToLayout( panel_vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            QP.AddToLayout( panel_vbox, self._suggested_favourites, CC.FLAGS_EXPAND_BOTH_WAYS )
            QP.AddToLayout( panel_vbox, self._suggested_favourites_input, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            suggested_tags_favourites_panel.setLayout( panel_vbox )
            
            #
            
            panel_vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Show related tags: ', self._show_related_tags ) )
            rows.append( ( 'Initial/Quick search duration (ms): ', self._related_tags_search_1_duration_ms ) )
            rows.append( ( 'Medium search duration (ms): ', self._related_tags_search_2_duration_ms ) )
            rows.append( ( 'Thorough search duration (ms): ', self._related_tags_search_3_duration_ms ) )
            rows.append( ( 'Tag concurrence threshold %: ', self._related_tags_concurrence_threshold_percent ) )
            
            gridbox = ClientGUICommon.WrapInGrid( suggested_tags_related_panel, rows )
            
            search_tag_slices_weight_box.Add( search_tag_slices_weight_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            result_tag_slices_weight_box.Add( result_tag_slices_weight_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            desc = 'This will search the database for tags statistically related to what your files already have. It only searches within the specific service atm. The score weights are advanced, so only change them if you know what is going on!'
            st = ClientGUICommon.BetterStaticText( suggested_tags_related_panel, desc )
            st.setWordWrap( True )
            
            QP.AddToLayout( panel_vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( panel_vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            QP.AddToLayout( panel_vbox, search_tag_slices_weight_box, CC.FLAGS_EXPAND_BOTH_WAYS )
            QP.AddToLayout( panel_vbox, result_tag_slices_weight_box, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            suggested_tags_related_panel.setLayout( panel_vbox )
            
            #
            
            panel_vbox = QP.VBoxLayout()
            
            rows = []
            
            rows.append( ( 'Show file lookup scripts on single-file manage tags windows: ', self._show_file_lookup_script_tags ) )
            rows.append( ( 'Favourite file lookup script: ', self._favourite_file_lookup_script ) )
            
            gridbox = ClientGUICommon.WrapInGrid( suggested_tags_file_lookup_script_panel, rows )
            
            desc = 'This is an increasingly defunct system, do not expect miracles!'
            st = ClientGUICommon.BetterStaticText( suggested_tags_related_panel, desc )
            st.setWordWrap( True )
            
            QP.AddToLayout( panel_vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( panel_vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            suggested_tags_file_lookup_script_panel.setLayout( panel_vbox )
            
            #
            
            panel_vbox = QP.VBoxLayout()
            
            desc = 'This simply saves the last n tags you have added for each service.'
            st = ClientGUICommon.BetterStaticText( suggested_tags_related_panel, desc )
            st.setWordWrap( True )
            
            QP.AddToLayout( panel_vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
            QP.AddToLayout( panel_vbox, self._num_recent_tags, CC.FLAGS_EXPAND_PERPENDICULAR )
            
            panel_vbox.addStretch( 1 )
            
            suggested_tags_recent_panel.setLayout( panel_vbox )
            
            #
            
            suggest_tags_panel_notebook.addTab( suggested_tags_favourites_panel, 'most used' )
            suggest_tags_panel_notebook.addTab( suggested_tags_related_panel, 'related' )
            suggest_tags_panel_notebook.addTab( suggested_tags_file_lookup_script_panel, 'file lookup scripts' )
            suggest_tags_panel_notebook.addTab( suggested_tags_recent_panel, 'recent' )
            
            #
            
            rows = []
            
            rows.append( ( 'Width of suggested tags columns: ', self._suggested_tags_width ) )
            rows.append( ( 'Column layout: ', self._suggested_tags_layout ) )
            rows.append( ( 'Default notebook page: ', self._default_suggested_tags_notebook_page ) )
            
            gridbox = ClientGUICommon.WrapInGrid( suggested_tags_panel, rows )
            
            desc = 'The manage tags dialog can provide several kinds of tag suggestions.'
            
            suggested_tags_panel.Add( ClientGUICommon.BetterStaticText( suggested_tags_panel, desc ), CC.FLAGS_EXPAND_PERPENDICULAR )
            suggested_tags_panel.Add( gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            suggested_tags_panel.Add( suggest_tags_panel_notebook, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            #
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, suggested_tags_panel, CC.FLAGS_EXPAND_BOTH_WAYS )
            
            self.setLayout( vbox )
            
            #
            
            self._suggested_favourites_services.currentIndexChanged.connect( self.EventSuggestedFavouritesService )
            self._suggested_tags_layout.currentIndexChanged.connect( self._NotifyLayoutChanged )
            
            self._NotifyLayoutChanged()
            self.EventSuggestedFavouritesService( None )
            
        
        def _AddResultTagSliceWeight( self ):
            
            self._AddTagSliceWeight( self._result_tag_slices_weights )
            
        
        def _AddSearchTagSliceWeight( self ):
            
            self._AddTagSliceWeight( self._search_tag_slices_weights )
            
        
        def _AddTagSliceWeight( self, list_ctrl ):
            
            message = 'enter namespace'
            
            with ClientGUIDialogs.DialogTextEntry( self, message, allow_blank = False ) as dlg:
                
                if dlg.exec() == QW.QDialog.Accepted:
                    
                    tag_slice = dlg.GetValue()
                    
                    if tag_slice in ( '', ':' ):
                        
                        ClientGUIDialogsMessage.ShowWarning( self, 'Sorry, you cannot re-add unnamespaced or namespaced!' )
                        
                        return
                        
                    
                    if not tag_slice.endswith( ':' ):
                        
                        tag_slice = tag_slice + ':'
                        
                    
                    existing_tag_slices = { existing_tag_slice for ( existing_tag_slice, existing_weight ) in list_ctrl.GetData() }
                    
                    if tag_slice in existing_tag_slices:
                        
                        ClientGUIDialogsMessage.ShowWarning( self, 'Sorry, that namespace already exists!' )
                        
                        return
                        
                    
                    with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'set weight' ) as dlg_2:
                        
                        panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg_2 )
                        
                        control = ClientGUICommon.BetterSpinBox( panel, initial = 100, min = 0, max = 10000 )
                        
                        panel.SetControl( control )
                        
                        dlg_2.SetPanel( panel )
                        
                        if dlg_2.exec() == QW.QDialog.Accepted:
                            
                            weight = control.value()
                            
                            new_data = ( tag_slice, weight )
                            
                            list_ctrl.AddDatas( [ new_data ] )
                            
                            list_ctrl.Sort()
                            
                        
                    
                
            
        
        def _CanDeleteResultTagSliceWeight( self ) -> bool:
            
            return self._CanDeleteTagSliceWeight( self._result_tag_slices_weights )
            
        
        def _CanDeleteSearchTagSliceWeight( self ) -> bool:
            
            return self._CanDeleteTagSliceWeight( self._search_tag_slices_weights )
            
        
        def _CanDeleteTagSliceWeight( self, list_ctrl ) -> bool:
            
            selected_tag_slices_and_weights = list_ctrl.GetData( only_selected = True )
            
            for ( tag_slice, weight ) in selected_tag_slices_and_weights:
                
                if tag_slice in ( '', ':' ):
                    
                    return False
                    
                
            
            return True
            
        
        def _ConvertTagSliceAndWeightToListCtrlTuples( self, tag_slice_and_weight ):
            
            ( tag_slice, weight ) = tag_slice_and_weight
            
            pretty_tag_slice = HydrusTags.ConvertTagSliceToPrettyString( tag_slice )
            sort_tag_slice = pretty_tag_slice
            
            pretty_weight = HydrusData.ToHumanInt( weight ) + '%'
            
            display_tuple = ( pretty_tag_slice, pretty_weight )
            sort_tuple = ( sort_tag_slice, weight )
            
            return ( display_tuple, sort_tuple )
            
        
        def _EditResultTagSliceWeight( self ):
            
            self._EditTagSliceWeight( self._result_tag_slices_weights )
            
        
        def _EditSearchTagSliceWeight( self ):
            
            self._EditTagSliceWeight( self._search_tag_slices_weights )
            
        
        def _EditTagSliceWeight( self, list_ctrl ):
            
            selected_tag_slices_and_weights = list_ctrl.GetData( only_selected = True )
            
            if len( selected_tag_slices_and_weights ) > 0:
                
                original_data = selected_tag_slices_and_weights[0]
                
                ( tag_slice, weight ) = original_data
                
                with ClientGUITopLevelWindowsPanels.DialogEdit( self, 'edit weight' ) as dlg:
                    
                    panel = ClientGUIScrolledPanels.EditSingleCtrlPanel( dlg )
                    
                    control = ClientGUICommon.BetterSpinBox( panel, initial = weight, min = 0, max = 10000 )
                    
                    panel.SetControl( control )
                    
                    dlg.SetPanel( panel )
                    
                    if dlg.exec() == QW.QDialog.Accepted:
                        
                        edited_weight = control.value()
                        
                        list_ctrl.DeleteDatas( [ original_data ] )
                        
                        edited_data = ( tag_slice, edited_weight )
                        
                        list_ctrl.AddDatas( [ edited_data ] )
                        
                        list_ctrl.Sort()
                        
                    
                
            
        
        def _NotifyLayoutChanged( self ):
            
            enable_default_page = self._suggested_tags_layout.GetValue() == 'notebook'
            
            self._default_suggested_tags_notebook_page.setEnabled( enable_default_page )
            
        
        def _SaveCurrentSuggestedFavourites( self ):
            
            if self._current_suggested_favourites_service is not None:
                
                self._suggested_favourites_dict[ self._current_suggested_favourites_service ] = self._suggested_favourites.GetTags()
                
            
        
        def EventSuggestedFavouritesService( self, index ):
            
            self._SaveCurrentSuggestedFavourites()
            
            self._current_suggested_favourites_service = self._suggested_favourites_services.GetValue()
            
            if self._current_suggested_favourites_service in self._suggested_favourites_dict:
                
                favourites = self._suggested_favourites_dict[ self._current_suggested_favourites_service ]
                
            else:
                
                favourites = self._new_options.GetSuggestedTagsFavourites( self._current_suggested_favourites_service )
                
            
            self._suggested_favourites.SetTagServiceKey( self._current_suggested_favourites_service )
            
            self._suggested_favourites.SetTags( favourites )
            
            self._suggested_favourites_input.SetTagServiceKey( self._current_suggested_favourites_service )
            self._suggested_favourites_input.SetDisplayTagServiceKey( self._current_suggested_favourites_service )
            
        
        def UpdateOptions( self ):
            
            self._new_options.SetInteger( 'suggested_tags_width', self._suggested_tags_width.value() )
            self._new_options.SetNoneableString( 'suggested_tags_layout', self._suggested_tags_layout.GetValue() )
            
            self._new_options.SetString( 'default_suggested_tags_notebook_page', self._default_suggested_tags_notebook_page.GetValue() )
            
            self._SaveCurrentSuggestedFavourites()
            
            for ( service_key, favourites ) in list(self._suggested_favourites_dict.items()):
                
                self._new_options.SetSuggestedTagsFavourites( service_key, favourites )
                
            
            self._new_options.SetBoolean( 'show_related_tags', self._show_related_tags.isChecked() )
            
            self._new_options.SetInteger( 'related_tags_search_1_duration_ms', self._related_tags_search_1_duration_ms.value() )
            self._new_options.SetInteger( 'related_tags_search_2_duration_ms', self._related_tags_search_2_duration_ms.value() )
            self._new_options.SetInteger( 'related_tags_search_3_duration_ms', self._related_tags_search_3_duration_ms.value() )
            
            self._new_options.SetInteger( 'related_tags_concurrence_threshold_percent', self._related_tags_concurrence_threshold_percent.value() )
            
            related_tags_search_tag_slices_weight_percent = self._search_tag_slices_weights.GetData()
            related_tags_result_tag_slices_weight_percent = self._result_tag_slices_weights.GetData()
            
            self._new_options.SetRelatedTagsTagSliceWeights( related_tags_search_tag_slices_weight_percent, related_tags_result_tag_slices_weight_percent )
            
            self._new_options.SetBoolean( 'show_file_lookup_script_tags', self._show_file_lookup_script_tags.isChecked() )
            self._new_options.SetNoneableString( 'favourite_file_lookup_script', self._favourite_file_lookup_script.GetValue() )
            
            self._new_options.SetNoneableInteger( 'num_recent_tags', self._num_recent_tags.GetValue() )
            
        
    
    class _ThumbnailsPanel( QW.QWidget ):
        
        def __init__( self, parent, new_options ):
            
            QW.QWidget.__init__( self, parent )
            
            self._new_options = new_options
            
            self._thumbnail_width = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
            self._thumbnail_height = ClientGUICommon.BetterSpinBox( self, min=20, max=2048 )
            
            self._thumbnail_border = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
            self._thumbnail_margin = ClientGUICommon.BetterSpinBox( self, min=0, max=20 )
            
            self._thumbnail_scale_type = ClientGUICommon.BetterChoice( self )
            
            for t in ( HydrusImageHandling.THUMBNAIL_SCALE_DOWN_ONLY, HydrusImageHandling.THUMBNAIL_SCALE_TO_FIT, HydrusImageHandling.THUMBNAIL_SCALE_TO_FILL ):
                
                self._thumbnail_scale_type.addItem( HydrusImageHandling.thumbnail_scale_str_lookup[ t ], t )
                
            
            # I tried <100%, but Qt seems to cap it to 1.0. Sad!
            self._thumbnail_dpr_percentage = ClientGUICommon.BetterSpinBox( self, min = 100, max = 800 )
            tt = 'If your OS runs at an UI scale greater than 100%, mirror it here and your thumbnails will look crisp. If you have multiple monitors at different UI scales, or you change UI scale regularly, set it to the largest one you use.'
            tt += os.linesep * 2
            tt += 'I believe the UI scale on the monitor this dialog opened on was {}'.format( HydrusData.ConvertFloatToPercentage( self.devicePixelRatio() ) )
            self._thumbnail_dpr_percentage.setToolTip( tt )
            
            self._video_thumbnail_percentage_in = ClientGUICommon.BetterSpinBox( self, min=0, max=100 )
            
            self._thumbnail_visibility_scroll_percent = ClientGUICommon.BetterSpinBox( self, min=1, max=99 )
            self._thumbnail_visibility_scroll_percent.setToolTip( 'Lower numbers will cause fewer scrolls, higher numbers more.' )
            
            self._allow_blurhash_fallback = QW.QCheckBox( self )
            tt = 'If hydrus does not have a thumbnail for a file (e.g. you are looking at a deleted file, or one unexpectedly missing), but it does know its blurhash, it will generate a blurry thumbnail based off that blurhash. Turning this behaviour off here will make it always show the default "hydrus" thumbnail.'
            self._allow_blurhash_fallback.setToolTip( tt )
            
            self._fade_thumbnails = QW.QCheckBox( self )
            tt = 'Whenever thumbnails change (appearing on a page, selecting, an icon or tag banner changes), they normally fade from the old to the new. If you would rather they change instantly, in one frame, uncheck this.'
            self._fade_thumbnails.setToolTip( tt )
            
            self._focus_preview_on_ctrl_click = QW.QCheckBox( self )
            self._focus_preview_on_ctrl_click_only_static = QW.QCheckBox( self )
            self._focus_preview_on_shift_click = QW.QCheckBox( self )
            self._focus_preview_on_shift_click_only_static = QW.QCheckBox( self )
            
            self._thumbnail_scroll_rate = QW.QLineEdit( self )
            
            self._media_background_bmp_path = QP.FilePickerCtrl( self )
            
            #
            
            ( thumbnail_width, thumbnail_height ) = HC.options[ 'thumbnail_dimensions' ]
            
            self._thumbnail_width.setValue( thumbnail_width )
            self._thumbnail_height.setValue( thumbnail_height )
            
            self._thumbnail_border.setValue( self._new_options.GetInteger( 'thumbnail_border' ) )
            self._thumbnail_margin.setValue( self._new_options.GetInteger( 'thumbnail_margin' ) )
            
            self._thumbnail_scale_type.SetValue( self._new_options.GetInteger( 'thumbnail_scale_type' ) )
            self._thumbnail_dpr_percentage.setValue( self._new_options.GetInteger( 'thumbnail_dpr_percent' ) )
            
            self._video_thumbnail_percentage_in.setValue( self._new_options.GetInteger( 'video_thumbnail_percentage_in' ) )
            
            self._allow_blurhash_fallback.setChecked( self._new_options.GetBoolean( 'allow_blurhash_fallback' ) )
            
            self._fade_thumbnails.setChecked( self._new_options.GetBoolean( 'fade_thumbnails' ) )
            
            self._focus_preview_on_ctrl_click.setChecked( self._new_options.GetBoolean( 'focus_preview_on_ctrl_click' ) )
            self._focus_preview_on_ctrl_click_only_static.setChecked( self._new_options.GetBoolean( 'focus_preview_on_ctrl_click_only_static' ) )
            self._focus_preview_on_shift_click.setChecked( self._new_options.GetBoolean( 'focus_preview_on_shift_click' ) )
            self._focus_preview_on_shift_click_only_static.setChecked( self._new_options.GetBoolean( 'focus_preview_on_shift_click_only_static' ) )
            
            self._thumbnail_visibility_scroll_percent.setValue( self._new_options.GetInteger( 'thumbnail_visibility_scroll_percent' ) )
            
            self._thumbnail_scroll_rate.setText( self._new_options.GetString( 'thumbnail_scroll_rate' ) )
            
            media_background_bmp_path = self._new_options.GetNoneableString( 'media_background_bmp_path' )
            
            if media_background_bmp_path is not None:
                
                self._media_background_bmp_path.SetPath( media_background_bmp_path )
                
            
            #
            
            rows = []
            
            rows.append( ( 'Thumbnail width: ', self._thumbnail_width ) )
            rows.append( ( 'Thumbnail height: ', self._thumbnail_height ) )
            rows.append( ( 'Thumbnail border: ', self._thumbnail_border ) )
            rows.append( ( 'Thumbnail margin: ', self._thumbnail_margin ) )
            rows.append( ( 'Thumbnail scaling: ', self._thumbnail_scale_type ) )
            rows.append( ( 'Thumbnail UI-scale supersampling %: ', self._thumbnail_dpr_percentage ) )
            rows.append( ( 'On ctrl-click, focus thumbnails in the preview window: ', self._focus_preview_on_ctrl_click ) )
            rows.append( ( '  Only on files with no duration: ', self._focus_preview_on_ctrl_click_only_static ) )
            rows.append( ( 'On shift-click, focus thumbnails in the preview window: ', self._focus_preview_on_shift_click ) )
            rows.append( ( '  Only on files with no duration: ', self._focus_preview_on_shift_click_only_static ) )
            rows.append( ( 'Generate video thumbnails this % in: ', self._video_thumbnail_percentage_in ) )
            rows.append( ( 'Use blurhash missing thumbnail fallback: ', self._allow_blurhash_fallback ) )
            rows.append( ( 'Fade thumbnails: ', self._fade_thumbnails ) )
            rows.append( ( 'Do not scroll down on key navigation if thumbnail at least this % visible: ', self._thumbnail_visibility_scroll_percent ) )
            rows.append( ( 'EXPERIMENTAL: Scroll thumbnails at this rate per scroll tick: ', self._thumbnail_scroll_rate ) )
            rows.append( ( 'EXPERIMENTAL: Image path for thumbnail panel background image (set blank to clear): ', self._media_background_bmp_path ) )
            
            gridbox = ClientGUICommon.WrapInGrid( self, rows )
            
            vbox = QP.VBoxLayout()
            
            QP.AddToLayout( vbox, gridbox, CC.FLAGS_EXPAND_SIZER_PERPENDICULAR )
            
            self.setLayout( vbox )
            
            self._UpdatePreviewCheckboxes()
            
        
        def _UpdatePreviewCheckboxes( self ):
            
            self._focus_preview_on_ctrl_click_only_static.setEnabled( self._focus_preview_on_ctrl_click.isChecked() )
            self._focus_preview_on_shift_click_only_static.setEnabled( self._focus_preview_on_shift_click.isChecked() )
            
        
        def UpdateOptions( self ):
            
            new_thumbnail_dimensions = [self._thumbnail_width.value(), self._thumbnail_height.value()]
            
            HC.options[ 'thumbnail_dimensions' ] = new_thumbnail_dimensions
            
            self._new_options.SetInteger( 'thumbnail_border', self._thumbnail_border.value() )
            self._new_options.SetInteger( 'thumbnail_margin', self._thumbnail_margin.value() )
            
            self._new_options.SetInteger( 'thumbnail_scale_type', self._thumbnail_scale_type.GetValue() )
            self._new_options.SetInteger( 'thumbnail_dpr_percent', self._thumbnail_dpr_percentage.value() )
            
            self._new_options.SetInteger( 'video_thumbnail_percentage_in', self._video_thumbnail_percentage_in.value() )
            
            self._new_options.SetBoolean( 'focus_preview_on_ctrl_click', self._focus_preview_on_ctrl_click.isChecked() )
            self._new_options.SetBoolean( 'focus_preview_on_ctrl_click_only_static', self._focus_preview_on_ctrl_click_only_static.isChecked() )
            self._new_options.SetBoolean( 'focus_preview_on_shift_click', self._focus_preview_on_shift_click.isChecked() )
            self._new_options.SetBoolean( 'focus_preview_on_shift_click_only_static', self._focus_preview_on_shift_click_only_static.isChecked() )
            
            self._new_options.SetBoolean( 'allow_blurhash_fallback', self._allow_blurhash_fallback.isChecked() )
            
            self._new_options.SetBoolean( 'fade_thumbnails', self._fade_thumbnails.isChecked() )
            
            try:
                
                thumbnail_scroll_rate = self._thumbnail_scroll_rate.text()
                
                float( thumbnail_scroll_rate )
                
                self._new_options.SetString( 'thumbnail_scroll_rate', thumbnail_scroll_rate )
                
            except:
                
                pass
                
            
            self._new_options.SetInteger( 'thumbnail_visibility_scroll_percent', self._thumbnail_visibility_scroll_percent.value() )
            
            media_background_bmp_path = self._media_background_bmp_path.GetPath()
            
            if media_background_bmp_path == '':
                
                media_background_bmp_path = None
                
            
            self._new_options.SetNoneableString( 'media_background_bmp_path', media_background_bmp_path )
            
        
    
    def CommitChanges( self ):
        
        for page in self._listbook.GetActivePages():
            
            page.UpdateOptions()
            
        
        try:
            
            CG.client_controller.WriteSynchronous( 'save_options', HC.options )
            
            CG.client_controller.WriteSynchronous( 'serialisable', self._new_options )
            
            # we do this to convert tuples to lists and so on
            test_new_options = self._new_options.Duplicate()
            
            if test_new_options.GetMediaViewOptions() != self._original_new_options.GetMediaViewOptions():
                
                CG.client_controller.pub( 'clear_image_tile_cache' )
                
            
            res_changed = HC.options[ 'thumbnail_dimensions' ] != self._original_options[ 'thumbnail_dimensions' ]
            type_changed = test_new_options.GetInteger( 'thumbnail_scale_type' ) != self._original_new_options.GetInteger( 'thumbnail_scale_type' )
            dpr_changed = test_new_options.GetInteger( 'thumbnail_dpr_percent' ) != self._original_new_options.GetInteger( 'thumbnail_dpr_percent' )
            
            if res_changed or type_changed or dpr_changed:
                
                CG.client_controller.pub( 'reset_thumbnail_cache' )
                
            
        except Exception as e:
            
            HydrusData.PrintException( e )
            
            ClientGUIDialogsMessage.ShowCritical( self, 'Problem saving options!', str( e ) )
            
        
    
class ManageURLsPanel( CAC.ApplicationCommandProcessorMixin, ClientGUIScrolledPanels.ManagePanel ):
    
    def __init__( self, parent, medias: typing.Collection[ ClientMedia.MediaSingleton ] ):
        
        ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
        CAC.ApplicationCommandProcessorMixin.__init__( self )
        
        self._current_media = [ m.Duplicate() for m in medias ]
        
        self._multiple_files_warning = ClientGUICommon.BetterStaticText( self, label = 'Warning: you are editing urls for multiple files!\nBe very careful about adding URLs here, as they will apply to everything.\nAdding the same URL to multiple files is only appropriate for gallery-type URLs!' )
        self._multiple_files_warning.setObjectName( 'HydrusWarning' )
        
        if len( self._current_media ) == 1:
            
            self._multiple_files_warning.hide()
            
        
        self._urls_listbox = QW.QListWidget( self )
        self._urls_listbox.setSortingEnabled( True )
        self._urls_listbox.setSelectionMode( QW.QAbstractItemView.ExtendedSelection )
        self._urls_listbox.itemDoubleClicked.connect( self.EventListDoubleClick )
        self._listbox_event_filter = QP.WidgetEventFilter( self._urls_listbox )
        self._listbox_event_filter.EVT_KEY_DOWN( self.EventListKeyDown )
        
        ( width, height ) = ClientGUIFunctions.ConvertTextToPixels( self._urls_listbox, ( 120, 10 ) )
        
        self._urls_listbox.setMinimumWidth( width )
        self._urls_listbox.setMinimumHeight( height )
        
        self._url_input = QW.QLineEdit( self )
        self._url_input.installEventFilter( ClientGUICommon.TextCatchEnterEventFilter( self._url_input, self.AddURL ) )
        
        self._copy_button = ClientGUICommon.BetterButton( self, 'copy all', self._Copy )
        self._paste_button = ClientGUICommon.BetterButton( self, 'paste', self._Paste )
        
        self._urls_to_add = set()
        self._urls_to_remove = set()
        
        #
        
        self._pending_content_updates = []
        
        self._current_urls_count = collections.Counter()
        
        self._UpdateList()
        
        #
        
        hbox = QP.HBoxLayout()
        
        QP.AddToLayout( hbox, self._copy_button, CC.FLAGS_CENTER_PERPENDICULAR )
        QP.AddToLayout( hbox, self._paste_button, CC.FLAGS_CENTER_PERPENDICULAR )
        
        vbox = QP.VBoxLayout()
        
        QP.AddToLayout( vbox, self._multiple_files_warning, CC.FLAGS_EXPAND_PERPENDICULAR )
        QP.AddToLayout( vbox, self._urls_listbox, CC.FLAGS_EXPAND_BOTH_WAYS )
        QP.AddToLayout( vbox, self._url_input, CC.FLAGS_EXPAND_PERPENDICULAR )
        QP.AddToLayout( vbox, hbox, CC.FLAGS_ON_RIGHT )
        
        self.widget().setLayout( vbox )
        
        self._my_shortcut_handler = ClientGUIShortcuts.ShortcutsHandler( self, [ 'global', 'media', 'main_gui' ] )
        
        ClientGUIFunctions.SetFocusLater( self._url_input )
        
    
    def _Copy( self ):
        
        urls = sorted( self._current_urls_count.keys() )
        
        text = os.linesep.join( urls )
        
        CG.client_controller.pub( 'clipboard', 'text', text )
        
    
    def _EnterURLs( self, urls, only_add = False ):
        
        normalised_urls = []
        weird_urls = []
        
        for url in urls:
            
            try:
                
                normalised_url = CG.client_controller.network_engine.domain_manager.NormaliseURL( url )
                
                normalised_urls.append( normalised_url )
                
            except HydrusExceptions.URLClassException:
                
                weird_urls.append( url )
                
            
        
        if len( weird_urls ) > 0:
            
            message = 'The URLs:'
            message += '\n' * 2
            message += '\n'.join( weird_urls )
            message += '\n' * 2
            message += '--did not parse. Normally I would not recommend importing invalid URLs, but do you want to force it anyway?'
            
            result = ClientGUIDialogsQuick.GetYesNo( self, message )
            
            if result != QW.QDialog.Accepted:
                
                return False
                
            
            normalised_urls.extend( weird_urls )
            
        
        normalised_urls = HydrusData.DedupeList( normalised_urls )
        
        for normalised_url in normalised_urls:
            
            addee_media = set()
            
            for m in self._current_media:
                
                locations_manager = m.GetLocationsManager()
                
                if normalised_url not in locations_manager.GetURLs():
                    
                    addee_media.add( m )
                    
                
            
            if len( addee_media ) > 0:
                
                addee_hashes = { m.GetHash() for m in addee_media }
                
                content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_ADD, ( ( normalised_url, ), addee_hashes ) )
                
                for m in addee_media:
                    
                    m.GetMediaResult().ProcessContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update )
                    
                
                self._pending_content_updates.append( content_update )
                
            
        
        #
        
        self._UpdateList()
        
    
    def _Paste( self ):
        
        try:
            
            raw_text = CG.client_controller.GetClipboardText()
            
        except HydrusExceptions.DataMissing as e:
            
            ClientGUIDialogsMessage.ShowWarning( self, str(e) )
            
            return
            
        
        try:
            
            urls = HydrusText.DeserialiseNewlinedTexts( raw_text )
            
            self._EnterURLs( urls, only_add = True )
            
        except Exception as e:
            
            ClientGUIFunctions.PresentClipboardParseError( self, raw_text, 'Lines of URLs', e )
            
        
    
    def _RemoveURL( self, url ):
        
        removee_media = set()
        
        for m in self._current_media:
            
            locations_manager = m.GetLocationsManager()
            
            if url in locations_manager.GetURLs():
                
                removee_media.add( m )
                
            
        
        if len( removee_media ) > 0:
            
            removee_hashes = { m.GetHash() for m in removee_media }
            
            content_update = ClientContentUpdates.ContentUpdate( HC.CONTENT_TYPE_URLS, HC.CONTENT_UPDATE_DELETE, ( ( url, ), removee_hashes ) )
            
            for m in removee_media:
                
                m.GetMediaResult().ProcessContentUpdate( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, content_update )
                
            
            self._pending_content_updates.append( content_update )
            
        
        #
        
        self._UpdateList()
        
    
    def _SetSearchFocus( self ):
        
        self._url_input.setFocus( QC.Qt.OtherFocusReason )
        
    
    def _UpdateList( self ):
        
        self._urls_listbox.clear()
        
        self._current_urls_count = collections.Counter()
        
        for m in self._current_media:
            
            locations_manager = m.GetLocationsManager()
            
            for url in locations_manager.GetURLs():
                
                self._current_urls_count[ url ] += 1
                
            
        
        for ( url, count ) in self._current_urls_count.items():
            
            if len( self._current_media ) == 1:
                
                label = url
                
            else:
                
                label = '{} ({})'.format( url, count )
                
            item = QW.QListWidgetItem()
            item.setText( label )
            item.setData( QC.Qt.UserRole, url )
            self._urls_listbox.addItem( item )
            
        
    
    def EventListDoubleClick( self, item ):
    
        urls = [ QP.GetClientData( self._urls_listbox, selection.row() ) for selection in list( self._urls_listbox.selectedIndexes() ) ]
        
        for url in urls:
            
            self._RemoveURL( url )
            
        
        if len( urls ) == 1:
            
            url = urls[0]
            
            self._url_input.setText( url )
            
        
    
    def EventListKeyDown( self, event ):
        
        ( modifier, key ) = ClientGUIShortcuts.ConvertKeyEventToSimpleTuple( event )
        
        if key in ClientGUIShortcuts.DELETE_KEYS_QT:
            
            urls = [ QP.GetClientData( self._urls_listbox, selection.row() ) for selection in list( self._urls_listbox.selectedIndexes() ) ]
            
            for url in urls:
                
                self._RemoveURL( url )
                
            
        else:
            
            return True # was: event.ignore()
            
        
    
    def AddURL( self ):
        
        url = self._url_input.text()
        
        if url == '':
            
            self.parentWidget().DoOK()
            
        else:
            
            try:
                
                self._EnterURLs( [ url ] )
                
                self._url_input.clear()
                
            except Exception as e:
                
                ClientGUIDialogsMessage.ShowCritical( self, 'Problem with URL!', f'I could not add that URL: {e}' )
                
            
        
    
    def CommitChanges( self ):
        
        if len( self._pending_content_updates ) > 0:
            
            content_update_package = ClientContentUpdates.ContentUpdatePackage.STATICCreateFromContentUpdates( CC.COMBINED_LOCAL_FILE_SERVICE_KEY, self._pending_content_updates )
            
            CG.client_controller.WriteSynchronous( 'content_updates', content_update_package )
            
        
    
    def ProcessApplicationCommand( self, command: CAC.ApplicationCommand ):
        
        command_processed = True
        
        if command.IsSimpleCommand():
            
            action = command.GetSimpleAction()
            
            if action == CAC.SIMPLE_MANAGE_FILE_URLS:
                
                self._OKParent()
                
            elif action == CAC.SIMPLE_SET_SEARCH_FOCUS:
                
                self._SetSearchFocus()
                
            else:
                
                command_processed = False
                
            
        else:
            
            command_processed = False
            
        
        return command_processed
        
    
class RepairFileSystemPanel( ClientGUIScrolledPanels.ManagePanel ):
    
    def __init__( self, parent, missing_subfolders: typing.Collection[ ClientFilesPhysical.FilesStorageSubfolder ] ):
        
        ClientGUIScrolledPanels.ManagePanel.__init__( self, parent )
        
        # TODO: This needs another pass as we move to multiple locations and other tech
        # if someone has f10 and we are expecting 16 lots of f10x, or vice versa, (e.g. on an out of sync db recovery, not uncommon) we'll need to handle that
        
        self._only_thumbs = True
        
        self._missing_subfolders_to_new_subfolders = {}
        
        text = 'This dialog has launched because some expected file storage directories were not found. This is a serious error. You have two options:'
        text += os.linesep * 2
        text += '1) If you know what these should be (e.g. you recently remapped their external drive to another location), update the paths here manually. For most users, this will be clicking _add a possibly correct location_ and then select the new folder where the subdirectories all went. You can repeat this if your folders are missing in multiple locations. Check everything reports _ok!_'
        text += os.linesep * 2
        text += 'Although it is best if you can find everything, you only _have_ to fix the subdirectories starting with \'f\', which store your original files. Those starting \'t\' and \'r\' are for your thumbnails, which can be regenerated with a bit of work.'
        text += os.linesep * 2
        text += 'Then hit \'apply\', and the client will launch. You should double-check all your locations under \'database->move media files\' immediately.'
        text += os.linesep * 2
        text += '2) If the locations are not available, or you do not know what they should be, or you wish to fix this outside of the program, hit \'cancel\' to gracefully cancel client boot. Feel free to contact hydrus dev for help. Regardless of the situation, the document at "install_dir/db/help my media files are broke.txt" may be useful background reading.'
        
        if self._only_thumbs:
            
            text += os.linesep * 2
            text += 'SPECIAL NOTE FOR YOUR SITUATION: The only paths missing are thumbnail paths. If you cannot recover these folders, you can hit apply to create empty paths at the original or corrected locations and then run a maintenance routine to regenerate the thumbnails from their originals.'
            
        
        st = ClientGUICommon.BetterStaticText( self, text )
        st.setWordWrap( True )
        
        self._locations = ClientGUIListCtrl.BetterListCtrl( self, CGLC.COLUMN_LIST_REPAIR_LOCATIONS.ID, 12, self._ConvertPrefixToListCtrlTuples, activation_callback = self._SetLocations )
        
        self._set_button = ClientGUICommon.BetterButton( self, 'set correct location', self._SetLocations )
        self._add_button = ClientGUICommon.BetterButton( self, 'add a possibly correct location (let the client figure out what it contains)', self._AddLocation )
        
        # add a button here for 'try to fill them in for me'. you give it a dir, and it tries to figure out and fill in the prefixes for you
        
        #
        
        self._locations.AddDatas( missing_subfolders )
        
        self._locations.Sort()
        
        #
        
        vbox = QP.VBoxLayout()
        
        QP.AddToLayout( vbox, st, CC.FLAGS_EXPAND_PERPENDICULAR )
        QP.AddToLayout( vbox, self._locations, CC.FLAGS_EXPAND_BOTH_WAYS )
        QP.AddToLayout( vbox, self._set_button, CC.FLAGS_ON_RIGHT )
        QP.AddToLayout( vbox, self._add_button, CC.FLAGS_ON_RIGHT )
        
        self.widget().setLayout( vbox )
        
    
    def _AddLocation( self ):
        
        with QP.DirDialog( self, 'Select the potential correct location.' ) as dlg:
            
            if dlg.exec() == QW.QDialog.Accepted:
                
                path = dlg.GetPath()
                
                base_location = ClientFilesPhysical.FilesStorageBaseLocation( path, 0 )
                
                for subfolder in self._locations.GetData():
                    
                    new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, base_location )
                    
                    ok = new_subfolder.PathExists()
                    
                    if ok:
                        
                        self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok )
                        
                    
                
                self._locations.UpdateDatas()
                
            
        
    
    def _ConvertPrefixToListCtrlTuples( self, subfolder ):
        
        prefix = subfolder.prefix
        incorrect_base_location = subfolder.base_location
        
        if subfolder in self._missing_subfolders_to_new_subfolders:
            
            ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ]
            
            correct_base_location = new_subfolder.base_location
            
            if ok:
                
                pretty_ok = 'ok!'
                
            else:
                
                pretty_ok = 'not found'
                
            
            pretty_correct_base_location = correct_base_location.path
            
        else:
            
            pretty_correct_base_location = ''
            ok = None
            pretty_ok = ''
            
        
        pretty_incorrect_base_location = incorrect_base_location.path
        pretty_prefix = prefix
        
        display_tuple = ( pretty_incorrect_base_location, pretty_prefix, pretty_correct_base_location, pretty_ok )
        sort_tuple = ( pretty_incorrect_base_location, prefix, pretty_correct_base_location, ok )
        
        return ( display_tuple, sort_tuple )
        
    
    def _GetValue( self ):
        
        correct_rows = []
        
        thumb_problems = False
        
        for subfolder in self._locations.GetData():
            
            prefix = subfolder.prefix
            
            if subfolder not in self._missing_subfolders_to_new_subfolders:
                
                if prefix.startswith( 'f' ):
                    
                    raise HydrusExceptions.VetoException( 'You did not correct all the file locations!' )
                    
                else:
                    
                    thumb_problems = True
                    
                    new_subfolder = subfolder
                    
                
            else:
                
                ( new_subfolder, ok ) = self._missing_subfolders_to_new_subfolders[ subfolder ]
                
                if not ok:
                    
                    if prefix.startswith( 'f' ):
                        
                        raise HydrusExceptions.VetoException( 'You did not find all the correct file locations!' )
                        
                    else:
                        
                        thumb_problems = True
                        
                    
                
            
            correct_rows.append( ( subfolder, new_subfolder ) )
            
        
        return ( correct_rows, thumb_problems )
        
    
    def _SetLocations( self ):
        
        subfolders = self._locations.GetData( only_selected = True )
        
        if len( subfolders ) > 0:
            
            with QP.DirDialog( self, 'Select correct location.' ) as dlg:
                
                if dlg.exec() == QW.QDialog.Accepted:
                    
                    path = dlg.GetPath()
                    
                    base_location = ClientFilesPhysical.FilesStorageBaseLocation( path, 0 )
                    
                    for subfolder in subfolders:
                        
                        new_subfolder = ClientFilesPhysical.FilesStorageSubfolder( subfolder.prefix, base_location )
                        
                        ok = new_subfolder.PathExists()
                        
                        self._missing_subfolders_to_new_subfolders[ subfolder ] = ( new_subfolder, ok )
                        
                    
                    self._locations.UpdateDatas()
                    
                
            
        
    
    def CheckValid( self ):
        
        # raises veto if invalid
        self._GetValue()
        
    
    def CommitChanges( self ):
        
        ( correct_rows, thumb_problems ) = self._GetValue()
        
        CG.client_controller.WriteSynchronous( 'repair_client_files', correct_rows )
        
    
    def UserIsOKToOK( self ):
        
        ( correct_rows, thumb_problems ) = self._GetValue()
        
        if thumb_problems:
            
            message = 'Some or all of your incorrect paths have not been corrected, but they are all thumbnail paths.'
            message += os.linesep * 2
            message += 'Would you like instead to create new empty subdirectories at the previous (or corrected, if you have entered them) locations?'
            message += os.linesep * 2
            message += 'You can run database->regenerate->thumbnails to fill them up again.'
            
            result = ClientGUIDialogsQuick.GetYesNo( self, message )
            
            if result != QW.QDialog.Accepted:
                
                return False
                
            
        
        return True
        
