Added a "server settings" dialog to the desktop app.

master
Pacman Ghost 6 years ago
parent a90b2a6d4a
commit 9c9771a1b4
  1. 36
      vasl_templates/main.py
  2. 71
      vasl_templates/main_window.py
  3. BIN
      vasl_templates/resources/file_browser.png
  4. 91
      vasl_templates/server_settings.py
  5. 204
      vasl_templates/ui/server_settings.ui
  6. 7
      vasl_templates/webapp/files.py
  7. 2
      vasl_templates/webapp/templates/user-settings-dialog.html

@ -9,16 +9,18 @@ import traceback
import logging import logging
import urllib.request import urllib.request
import PyQt5.QtWebEngineWidgets
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, QSettings, QDir from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore import PyQt5.QtCore
import click import click
from vasl_templates.main_window import MainWindow
from vasl_templates.webapp import app as webapp from vasl_templates.webapp import app as webapp
from vasl_templates.webapp import load_debug_config from vasl_templates.webapp import load_debug_config
from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets
app_settings = None
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
_QT_LOGGING_LEVELS = { _QT_LOGGING_LEVELS = {
@ -44,7 +46,7 @@ def qtMessageHandler( msg_type, context, msg ):# pylint: disable=unused-argument
@click.option( "--default-scenario", help="Default scenario settings." ) @click.option( "--default-scenario", help="Default scenario settings." )
@click.option( "--remote-debugging", help="Chrome DevTools port number." ) @click.option( "--remote-debugging", help="Chrome DevTools port number." )
@click.option( "--debug", help="Debug config file." ) @click.option( "--debug", help="Debug config file." )
def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: disable=too-many-locals def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: disable=too-many-locals,too-many-branches
"""Main entry point for the application.""" """Main entry point for the application."""
# configure the default template pack # configure the default template pack
@ -70,10 +72,12 @@ def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: d
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging
# load the application settings # load the application settings
fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf" app_settings_fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
if not os.path.isfile( fname ) : if not os.path.isfile( app_settings_fname ) :
fname = os.path.join( QDir.homePath(), fname ) app_settings_fname = os.path.join( QDir.homePath(), app_settings_fname )
settings = QSettings( fname, QSettings.IniFormat ) # FUDGE! Declaring app_settings as global here doesn't work on Windows (?!), we have to do this weird import :-/
import vasl_templates.main #pylint: disable=import-self
vasl_templates.main.app_settings = QSettings( app_settings_fname, QSettings.IniFormat )
# install the debug config file # install the debug config file
if debug: if debug:
@ -82,6 +86,22 @@ def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: d
# connect PyQt's logging to Python logging # connect PyQt's logging to Python logging
PyQt5.QtCore.qInstallMessageHandler( qtMessageHandler ) PyQt5.QtCore.qInstallMessageHandler( qtMessageHandler )
# FUDGE! We need to do this before showing any UI elements e.g. an error message box.
app = QApplication( sys.argv )
# install the server settings
try:
from vasl_templates.server_settings import install_server_settings #pylint: disable=cyclic-import
install_server_settings()
except Exception as ex: #pylint: disable=broad-except
from vasl_templates.main_window import MainWindow #pylint: disable=cyclic-import
MainWindow.showErrorMsg(
"Couldn't install the server settings:\n {}\n\n"
"Please correct them in the \"Server settings\" dialog, or in the config file:\n {}".format(
ex, app_settings_fname
)
)
# disable the Flask "do not use in a production environment" warning # disable the Flask "do not use in a production environment" warning
import flask.cli import flask.cli
flask.cli.show_server_banner = lambda *args: None flask.cli.show_server_banner = lambda *args: None
@ -125,9 +145,9 @@ def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: d
disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" ) disable_browser = webapp.config.get( "DISABLE_WEBENGINEVIEW" )
# run the application # run the application
app = QApplication( sys.argv )
url = "http://localhost:{}".format( port ) url = "http://localhost:{}".format( port )
main_window = MainWindow( settings, url, disable_browser ) from vasl_templates.main_window import MainWindow #pylint: disable=cyclic-import
main_window = MainWindow( url, disable_browser )
main_window.show() main_window.show()
ret_code = app.exec_() ret_code = app.exec_()

@ -6,13 +6,14 @@ import json
import io import io
import logging import logging
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QMessageBox from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMenuBar, QAction, QLabel, QMessageBox
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage
from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtGui import QDesktopServices, QIcon from PyQt5.QtGui import QDesktopServices, QIcon
from PyQt5.QtCore import Qt, QUrl, pyqtSlot from PyQt5.QtCore import Qt, QUrl, pyqtSlot
from vasl_templates.webapp.config.constants import APP_NAME from vasl_templates.webapp.config.constants import APP_NAME
from vasl_templates.main import app_settings
from vasl_templates.web_channel import WebChannelHandler from vasl_templates.web_channel import WebChannelHandler
from vasl_templates.utils import log_exceptions from vasl_templates.utils import log_exceptions
@ -42,11 +43,12 @@ class AppWebPage( QWebEnginePage ):
class MainWindow( QWidget ): class MainWindow( QWidget ):
"""Main application window.""" """Main application window."""
def __init__( self, settings, url, disable_browser ): instance = None
def __init__( self, url, disable_browser ):
# initialize # initialize
super().__init__() super().__init__()
self.settings = settings
self._view = None self._view = None
self._is_closing = False self._is_closing = False
@ -56,12 +58,23 @@ class MainWindow( QWidget ):
os.path.join( os.path.split(__file__)[0], "webapp/static/images/app.ico" ) os.path.join( os.path.split(__file__)[0], "webapp/static/images/app.ico" )
) ) ) )
# create the menu
menu_bar = QMenuBar( self )
file_menu = menu_bar.addMenu( "&File" )
def add_action( caption, handler ):
"""Add a menu action."""
action = QAction( caption, self )
action.triggered.connect( handler )
file_menu.addAction( action )
add_action( "&Settings", self.on_settings )
add_action( "E&xit", self.on_exit )
# set the window geometry # set the window geometry
if disable_browser: if disable_browser:
self.setFixedSize( 300, 100 ) self.setFixedSize( 300, 100 )
else: else:
# restore it from the previous session # restore it from the previous session
val = self.settings.value( "MainWindow/geometry" ) val = app_settings.value( "MainWindow/geometry" )
if val : if val :
self.restoreGeometry( val ) self.restoreGeometry( val )
else : else :
@ -69,12 +82,13 @@ class MainWindow( QWidget ):
self.setMinimumSize( 800, 500 ) self.setMinimumSize( 800, 500 )
# initialize the layout # initialize the layout
layout = QVBoxLayout( self )
layout.addWidget( menu_bar )
# FUDGE! We offer the option to disable the QWebEngineView since getting it to run # FUDGE! We offer the option to disable the QWebEngineView since getting it to run
# under Windows (especially older versions) is unreliable (since it uses OpenGL). # under Windows (especially older versions) is unreliable (since it uses OpenGL).
# By disabling it, the program will at least start (in particular, the webapp server), # By disabling it, the program will at least start (in particular, the webapp server),
# and non-technical users can then open an external browser and connect to the webapp # and non-technical users can then open an external browser and connect to the webapp
# that way. Sigh... # that way. Sigh...
layout = QVBoxLayout( self )
if not disable_browser: if not disable_browser:
# initialize the web view # initialize the web view
@ -115,6 +129,10 @@ class MainWindow( QWidget ):
label.setOpenExternalLinks( True ) label.setOpenExternalLinks( True )
layout.addWidget( label ) layout.addWidget( label )
# register the instance
assert MainWindow.instance is None
MainWindow.instance = self
def closeEvent( self, evt ) : def closeEvent( self, evt ) :
"""Handle requests to close the window (i.e. exit the application).""" """Handle requests to close the window (i.e. exit the application)."""
@ -125,7 +143,7 @@ class MainWindow( QWidget ):
def close_window(): def close_window():
"""Close the main window.""" """Close the main window."""
if self._view: if self._view:
self.settings.setValue( "MainWindow/geometry" , self.saveGeometry() ) app_settings.setValue( "MainWindow/geometry", self.saveGeometry() )
self.close() self.close()
# check if the scenario is dirty # check if the scenario is dirty
@ -137,7 +155,7 @@ class MainWindow( QWidget ):
close_window() close_window()
return return
# yup - ask the user to confirm the close # yup - ask the user to confirm the close
rc = self.ask( rc = MainWindow.ask(
"This scenario has been changed\n\nDo you want to close the program, and lose your changes?", "This scenario has been changed\n\nDo you want to close the program, and lose your changes?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes | QMessageBox.No,
QMessageBox.No QMessageBox.No
@ -149,17 +167,30 @@ class MainWindow( QWidget ):
self._view.page().runJavaScript( "is_scenario_dirty()", callback ) self._view.page().runJavaScript( "is_scenario_dirty()", callback )
evt.ignore() # nb: we wait until the Javascript finishes to process the event evt.ignore() # nb: we wait until the Javascript finishes to process the event
def showInfoMsg( self, msg ): @staticmethod
def showInfoMsg( msg ):
"""Show an informational message.""" """Show an informational message."""
QMessageBox.information( self , APP_NAME , msg ) QMessageBox.information( MainWindow.instance, APP_NAME, msg )
def showErrorMsg( self, msg ): @staticmethod
def showErrorMsg( msg ):
"""Show an error message.""" """Show an error message."""
QMessageBox.warning( self , APP_NAME , msg ) QMessageBox.warning( MainWindow.instance, APP_NAME, msg )
def ask( self, msg , buttons , default ) : @staticmethod
def ask( msg, buttons, default ) :
"""Ask the user a question.""" """Ask the user a question."""
return QMessageBox.question( self , APP_NAME , msg , buttons , default ) return QMessageBox.question( MainWindow.instance, APP_NAME, msg, buttons, default )
def on_exit( self ):
"""Menu action handler."""
self.close()
def on_settings( self ):
"""Menu action handler."""
from vasl_templates.server_settings import ServerSettingsDialog #pylint: disable=cyclic-import
dlg = ServerSettingsDialog( self )
dlg.exec_()
@pyqtSlot() @pyqtSlot()
@log_exceptions( caption="SLOT EXCEPTION" ) @log_exceptions( caption="SLOT EXCEPTION" )
@ -171,16 +202,16 @@ class MainWindow( QWidget ):
# load and install the user settings # load and install the user settings
buf = io.StringIO() buf = io.StringIO()
buf.write( "{" ) buf.write( "{" )
for key in self.settings.allKeys(): for key in app_settings.allKeys():
if key.startswith( "UserSettings/" ): if key.startswith( "UserSettings/" ):
buf.write( '"{}": {},'.format( key[13:], self.settings.value(key) ) ) buf.write( '"{}": {},'.format( key[13:], app_settings.value(key) ) )
buf.write( '"_dummy_": null }' ) buf.write( '"_dummy_": null }' )
buf = buf.getvalue() buf = buf.getvalue()
user_settings = {} user_settings = {}
try: try:
user_settings = json.loads( buf ) user_settings = json.loads( buf )
except Exception as ex: #pylint: disable=broad-except except Exception as ex: #pylint: disable=broad-except
self.showErrorMsg( "Couldn't load the user settings:\n\n{}".format( ex ) ) MainWindow.showErrorMsg( "Couldn't load the user settings:\n\n{}".format( ex ) )
logging.error( "Couldn't load the user settings: %s", ex ) logging.error( "Couldn't load the user settings: %s", ex )
logging.error( buf ) logging.error( buf )
return return
@ -209,16 +240,16 @@ class MainWindow( QWidget ):
@pyqtSlot( str ) @pyqtSlot( str )
@log_exceptions( caption="SLOT EXCEPTION" ) @log_exceptions( caption="SLOT EXCEPTION" )
def on_user_settings_change( self, user_settings ): def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use
"""Called when the user changes the user settings.""" """Called when the user changes the user settings."""
# delete all existing keys # delete all existing keys
for key in self.settings.allKeys(): for key in app_settings.allKeys():
if key.startswith( "UserSettings/" ): if key.startswith( "UserSettings/" ):
self.settings.remove( key ) app_settings.remove( key )
# save the new user settings # save the new user settings
user_settings = json.loads( user_settings ) user_settings = json.loads( user_settings )
for key,val in user_settings.items(): for key,val in user_settings.items():
self.settings.setValue( "UserSettings/{}".format(key) , val ) app_settings.setValue( "UserSettings/{}".format(key), val )
@pyqtSlot( str ) @pyqtSlot( str )
@log_exceptions( caption="SLOT EXCEPTION" ) @log_exceptions( caption="SLOT EXCEPTION" )

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -0,0 +1,91 @@
"""Implement the "server settings" dialog."""
import os
from PyQt5 import uic
from PyQt5.QtWidgets import QDialog, QFileDialog
from PyQt5.QtGui import QIcon
from vasl_templates.main import app_settings
from vasl_templates.main_window import MainWindow
from vasl_templates.webapp.config.constants import DATA_DIR
from vasl_templates.webapp.file_server.vasl_mod import VaslMod
from vasl_templates.webapp.files import install_vasl_mod
# ---------------------------------------------------------------------
class ServerSettingsDialog( QDialog ):
"""Let the user manage 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.select_vasl_mod_button.setIcon(
QIcon( os.path.join( base_dir, "resources/file_browser.png" ) )
)
self.setMinimumSize( self.size() )
# initialize handlers
self.select_vasl_mod_button.clicked.connect( self.on_select_vasl_mod )
self.ok_button.clicked.connect( self.on_ok )
self.cancel_button.clicked.connect( self.on_cancel )
# load the current server settings
self.vasl_mod.setText( app_settings.value( "ServerSettings/vasl-mod" ) )
def on_select_vasl_mod( self ):
"""Let the user select a VASL module."""
fname = QFileDialog.getOpenFileName(
self, "Select VASL module",
app_settings.value( "ServerSettings/vasl-mod" ),
"VASL module files (*.vmod)|All files (*.*)"
)[0]
if fname:
self.vasl_mod.setText( fname )
def on_ok( self ):
"""Accept the new server settings."""
# save the new settings
fname = self.vasl_mod.text().strip()
vasl_mod_changed = fname != app_settings.value( "ServerSettings/vasl-mod" )
app_settings.setValue( "ServerSettings/vasl-mod", fname )
# install the new settings
# NOTE: We should really do this before saving the new settings, but that's more trouble
# than it's worth at this stage... :-/
try:
install_server_settings()
except Exception as ex: #pylint: disable=broad-except
MainWindow.showErrorMsg( "Couldn't install the server settings:\n\n{}".format( ex ) )
return
self.close()
# check if the VASL module was changed
if vasl_mod_changed:
# NOTE: It would be nice not to require a restart, but calling QWebEngineProfile.clearHttpCache() doesn't
# seem to, ya know, clear the cache, nor does setting the cache type to NoCache seem to do anything :-/
MainWindow.showInfoMsg( "The VASL module was changed - you should restart the program." )
def on_cancel( self ):
"""Cancel the dialog."""
self.close()
# ---------------------------------------------------------------------
def install_server_settings():
"""Install the server settings."""
# load the VASL module
fname = app_settings.value( "ServerSettings/vasl-mod" )
if fname:
vasl_mod = VaslMod( fname, DATA_DIR )
else:
vasl_mod = None
install_vasl_mod( vasl_mod )

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>90</height>
</rect>
</property>
<property name="windowTitle">
<string>Server settings</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;VASL module:</string>
</property>
<property name="buddy">
<cstring>vasl_mod</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="vasl_mod"/>
</item>
<item>
<widget class="QPushButton" name="select_vasl_mod_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
<zorder>vasl_mod</zorder>
<zorder>label</zorder>
<zorder>select_vasl_mod_button</zorder>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>309</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="ok_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>OK</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

@ -15,6 +15,13 @@ if app.config.get( "VASL_MOD" ):
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
def install_vasl_mod( new_vasl_mod ):
"""Install a new VASL module."""
global vasl_mod
vasl_mod = new_vasl_mod
# ---------------------------------------------------------------------
@app.route( "/counter/<gpid>/<side>/<int:index>" ) @app.route( "/counter/<gpid>/<side>/<int:index>" )
@app.route( "/counter/<gpid>/<side>", defaults={"index":0} ) @app.route( "/counter/<gpid>/<side>", defaults={"index":0} )
def get_counter_image( gpid, side, index ): def get_counter_image( gpid, side, index ):

@ -1,4 +1,4 @@
<div id="user-settings" style="display:none;"> <div id="user-settings" style="display:none;">
<input type="checkbox" name="include-vasl-images-in-snippets">&nbsp;Include VASL images in snippets <input type="checkbox" name="include-vasl-images-in-snippets">&nbsp;Include VASL images in snippets
<div class="note include-vasl-images-in-snippets-hint" style="margin-left:20px;">This program must be running before you load the VASL scenario.</div> <div class="note include-vasl-images-in-snippets-hint" style="margin-left:20px;">This program must be running before you load the scenario in VASL.</div>
</div> </div>

Loading…
Cancel
Save