"""Implement the "server settings" dialog.""" import os import shutil import logging import traceback from PyQt5 import uic from PyQt5.QtWidgets import QDialog, QFileDialog, QGroupBox from PyQt5.QtGui import QIcon from vasl_templates.main import app_settings from vasl_templates.main_window import MainWindow from vasl_templates.utils import show_msg_store from vasl_templates.webapp.vassal import VassalShim, SUPPORTED_VASSAL_VERSIONS_DISPLAY from vasl_templates.webapp.vasl_mod import set_vasl_mod, SUPPORTED_VASL_MOD_VERSIONS_DISPLAY from vasl_templates.webapp.utils import MsgStore # --------------------------------------------------------------------- _EXE_FSPEC = [ "Executable files (*.exe)" ] if os.name == "nt" else [] SERVER_SETTINGS = { "vassal-dir": { "type": "dir", "name": "VASSAL directory" }, "vasl-mod": { "type": "file", "name": "VASL module", "fspec": ["VASL module files (*.vmod)"] }, "vasl-extns-dir": { "type": "dir", "name": "VASL extensions directory" }, "boards-dir": { "type": "dir", "name": "VASL boards directory" }, "java-path": { "type": "file", "name": "Java executable", "allow_on_path": True, "fspec": _EXE_FSPEC }, "webdriver-path": { "type": "file", "name": "webdriver", "allow_on_path": True, "fspec": _EXE_FSPEC }, "chapter-h-notes-dir": { "type": "dir", "name": "Chapter H notes directory" }, "chapter-h-image-scaling": { "type": "int", "name": "Chapter H image scaling" }, "user-files-dir": { "type": "dir", "name": "user files directory", "allow_urls": True }, } # --------------------------------------------------------------------- class ServerSettingsDialog( QDialog ): """Let the user configure the server settings.""" def __init__( self, parent ) : # initialize super().__init__( parent=parent ) # initialize the UI base_dir = os.path.split( __file__ )[0] dname = os.path.join( base_dir, "ui/server_settings.ui" ) uic.loadUi( dname, self ) self.setFixedSize( self.size() ) # initialize the UI for key in SERVER_SETTINGS: btn = getattr( self, "select_{}_button".format( key.replace("-","_") ), None ) if btn: btn.setIcon( QIcon( os.path.join( base_dir, "resources/file_browser.png" ) ) ) self.vassal_dir.setToolTip( "Supported versions: {}".format( SUPPORTED_VASSAL_VERSIONS_DISPLAY ) ) self.vasl_mod.setToolTip( "Supported versions: {}".format( SUPPORTED_VASL_MOD_VERSIONS_DISPLAY ) ) self.webdriver_path.setToolTip( "Configure either geckodriver or chromedriver here." ) # initialize the UI for attr in dir(self): attr = getattr( self, attr ) if isinstance( attr, QGroupBox ): attr.setStyleSheet( "QGroupBox { font-weight: bold; } " ) # initialize click handlers def make_click_handler( func, *args ): #pylint: disable=missing-docstring # FUDGE! Python looks up variables passed in to a lambda when it is *invoked*, so we need # this intermediate function to create lambda's with their arguments at *creation time*. return lambda: func( *args ) for key,vals in SERVER_SETTINGS.items(): key2 = key.replace( "-", "_" ) btn = getattr( self, "select_{}_button".format( key2 ), None ) if btn: ctrl = self._get_control( key ) if vals["type"] == "dir": func = make_click_handler( self._on_select_dir, ctrl, vals["name"] ) elif vals["type"] == "file": func = make_click_handler( self._on_select_file, ctrl, vals["name"], vals["fspec"] ) else: assert False btn.clicked.connect( func ) self.ok_button.clicked.connect( self.on_ok ) self.cancel_button.clicked.connect( self.on_cancel ) # initialize handlers self.chapter_h_notes_dir.textChanged.connect( self.on_chapter_h_notes_dir_changed ) # load the current server settings for key in SERVER_SETTINGS: val = app_settings.value( "ServerSettings/"+key ) or "" ctrl = self._get_control( key ) ctrl.setText( str(val).strip() ) def _on_select_dir( self, ctrl, name ): """Ask the user to select a directory.""" dname = QFileDialog.getExistingDirectory( self, "Select {}".format( name ), ctrl.text(), QFileDialog.ShowDirsOnly ) if dname: ctrl.setText( dname ) def _on_select_file( self, ctrl, name, fspec ): """Ask the user to select a file.""" assert isinstance( fspec, list ) fspec = fspec[:] fspec.append( "All files ({})".format( "*.*" if os.name == "nt" else "*" ) ) fname = QFileDialog.getOpenFileName( self, "Select {}".format( name ), ctrl.text(), ";;".join( fspec ) )[0] if fname: ctrl.setText( fname ) def on_ok( self ): """Accept the new server settings.""" # save a copy of the current settings prev_settings = { key: app_settings.value( "ServerSettings/"+key, "" ) for key in SERVER_SETTINGS } # unload the dialog # NOTE: Typing an unknown path into QFileDialog.getExistingDirectory() causes that directory # to be created!?!? It doesn't really matter, since the user could have also manually typed # an unknown path into an edit box, so we need to validate everything anyway. new_settings = {} for key, vals in SERVER_SETTINGS.items(): ctrl = self._get_control( key ) func = getattr( self, "_unload_"+vals["type"] ) args, kwargs = [ vals["name"] ], {} for k in ("allow_on_path","allow_urls"): if k in vals: kwargs[ k ] = vals[ k ] val = func( ctrl, *args, **kwargs ) if val is None: # nb: something failed validation, an error message has already been shown return new_settings[ key ] = val # install the new settings for key in SERVER_SETTINGS: app_settings.setValue( "ServerSettings/"+key, new_settings[key] ) try: install_server_settings( False ) except Exception as ex: #pylint: disable=broad-except logging.error( traceback.format_exc() ) MainWindow.showErrorMsg( "Couldn't install the server settings:\n\n{}".format( ex ) ) # rollback the changes for key,val in prev_settings.items(): app_settings.setValue( "ServerSettings/"+key, val ) try: install_server_settings( False ) except Exception as ex2: #pylint: disable=broad-except logging.error( traceback.format_exc() ) MainWindow.showErrorMsg( "Couldn't rollback the server settings:\n\n{}".format( ex2 ) ) return self.close() # check if any key settings were changed KEY_SETTINGS = [ "vassal-dir", "vasl-mod", "vasl-extns-dir", "chapter-h-notes-dir" ] changed = [ key for key in KEY_SETTINGS if app_settings.value( "ServerSettings/"+key, "" ) != prev_settings[key] ] if len(changed) == 1: MainWindow.showInfoMsg( "The {} was changed - you should restart the program.".format( SERVER_SETTINGS[ changed[0] ][ "name" ] ) ) elif len(changed) > 1: MainWindow.showInfoMsg( "Some key settings were changed - you should restart the program." ) def on_cancel( self ): """Cancel the dialog.""" self.close() def _update_ui( self ): """Update the UI.""" rc = self.chapter_h_notes_dir.text().strip() != "" self.chapter_h_image_scaling_label.setEnabled( rc ) self.chapter_h_image_scaling_label2.setEnabled( rc ) self.chapter_h_image_scaling.setEnabled( rc ) def on_chapter_h_notes_dir_changed( self, val ): #pylint: disable=unused-argument """Called when the Chapter H notes directory is changed.""" self._update_ui() @staticmethod def _unload_dir( ctrl, name, allow_urls=False ): """Unload and validate a directory path.""" dname = ctrl.text().strip() if allow_urls and dname.startswith( ("http://","https://") ): return dname if dname and not os.path.isdir( dname ): MainWindow.showErrorMsg( "Can't find the {}:\n {}".format( name, dname ) ) ctrl.setFocus() return None return dname @staticmethod def _unload_file( ctrl, name, allow_on_path=False ): """Unload and validate a file path.""" fname = ctrl.text().strip() def is_valid( fname ): #pylint: disable=missing-docstring if not os.path.isabs(fname) and allow_on_path: return shutil.which( fname ) is not None return os.path.isfile( fname ) if fname and not is_valid(fname): if not os.path.isabs(fname) and allow_on_path: MainWindow.showErrorMsg( "Can't find the {} on the PATH:\n {}".format( name, fname ) ) else: MainWindow.showErrorMsg( "Can't find the {}:\n {}".format( name, fname ) ) ctrl.setFocus() return None return fname @staticmethod def _unload_int( ctrl, name ): """Unload and validate an integer value.""" val = ctrl.text().strip() if val and not val.isdigit(): MainWindow.showErrorMsg( "{} must be a numeric value.".format( name ) ) ctrl.setFocus() return None return val def _get_control( self, key ): """Return the UI control for the specified server setting.""" return getattr( self, key.replace("-","_") ) # --------------------------------------------------------------------- def install_server_settings( is_startup ): """Install the server settings.""" # install the server settings from vasl_templates.webapp import app for key in SERVER_SETTINGS: key2 = key.replace( "-", "_" ).upper() app.config[ key2 ] = app_settings.value( "ServerSettings/"+key ) # initialize if is_startup: msg_store = None # nb: we let the web page show startup messages else: msg_store = MsgStore() # load the VASL module fname = app_settings.value( "ServerSettings/vasl-mod" ) set_vasl_mod( fname, msg_store ) # check the VASSAL version VassalShim.check_vassal_version( msg_store ) # show any messages if msg_store: show_msg_store( msg_store )