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 urllib.request
import PyQt5.QtWebEngineWidgets
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore
import click
from vasl_templates.main_window import MainWindow
from vasl_templates.webapp import app as webapp
from vasl_templates.webapp import load_debug_config
from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets
app_settings = None
# ---------------------------------------------------------------------
_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( "--remote-debugging", help="Chrome DevTools port number." )
@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."""
# 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
# load the application settings
fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
if not os.path.isfile( fname ) :
fname = os.path.join( QDir.homePath(), fname )
settings = QSettings( fname, QSettings.IniFormat )
app_settings_fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
if not os.path.isfile( app_settings_fname ) :
app_settings_fname = os.path.join( QDir.homePath(), app_settings_fname )
# 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
if debug:
@ -82,6 +86,22 @@ def main( template_pack, default_scenario, remote_debugging, debug ): #pylint: d
# connect PyQt's logging to Python logging
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
import flask.cli
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" )
# run the application
app = QApplication( sys.argv )
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()
ret_code = app.exec_()

@ -6,13 +6,14 @@ import json
import io
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.QtWebChannel import QWebChannel
from PyQt5.QtGui import QDesktopServices, QIcon
from PyQt5.QtCore import Qt, QUrl, pyqtSlot
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.utils import log_exceptions
@ -42,11 +43,12 @@ class AppWebPage( QWebEnginePage ):
class MainWindow( QWidget ):
"""Main application window."""
def __init__( self, settings, url, disable_browser ):
instance = None
def __init__( self, url, disable_browser ):
# initialize
super().__init__()
self.settings = settings
self._view = None
self._is_closing = False
@ -56,12 +58,23 @@ class MainWindow( QWidget ):
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
if disable_browser:
self.setFixedSize( 300, 100 )
else:
# restore it from the previous session
val = self.settings.value( "MainWindow/geometry" )
val = app_settings.value( "MainWindow/geometry" )
if val :
self.restoreGeometry( val )
else :
@ -69,12 +82,13 @@ class MainWindow( QWidget ):
self.setMinimumSize( 800, 500 )
# initialize the layout
layout = QVBoxLayout( self )
layout.addWidget( menu_bar )
# 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).
# 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
# that way. Sigh...
layout = QVBoxLayout( self )
if not disable_browser:
# initialize the web view
@ -115,6 +129,10 @@ class MainWindow( QWidget ):
label.setOpenExternalLinks( True )
layout.addWidget( label )
# register the instance
assert MainWindow.instance is None
MainWindow.instance = self
def closeEvent( self, evt ) :
"""Handle requests to close the window (i.e. exit the application)."""
@ -125,7 +143,7 @@ class MainWindow( QWidget ):
def close_window():
"""Close the main window."""
if self._view:
self.settings.setValue( "MainWindow/geometry" , self.saveGeometry() )
app_settings.setValue( "MainWindow/geometry", self.saveGeometry() )
self.close()
# check if the scenario is dirty
@ -137,7 +155,7 @@ class MainWindow( QWidget ):
close_window()
return
# 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?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
@ -149,17 +167,30 @@ class MainWindow( QWidget ):
self._view.page().runJavaScript( "is_scenario_dirty()", callback )
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."""
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."""
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."""
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()
@log_exceptions( caption="SLOT EXCEPTION" )
@ -171,16 +202,16 @@ class MainWindow( QWidget ):
# load and install the user settings
buf = io.StringIO()
buf.write( "{" )
for key in self.settings.allKeys():
for key in app_settings.allKeys():
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 = buf.getvalue()
user_settings = {}
try:
user_settings = json.loads( buf )
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( buf )
return
@ -209,16 +240,16 @@ class MainWindow( QWidget ):
@pyqtSlot( str )
@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."""
# delete all existing keys
for key in self.settings.allKeys():
for key in app_settings.allKeys():
if key.startswith( "UserSettings/" ):
self.settings.remove( key )
app_settings.remove( key )
# save the new user settings
user_settings = json.loads( user_settings )
for key,val in user_settings.items():
self.settings.setValue( "UserSettings/{}".format(key) , val )
app_settings.setValue( "UserSettings/{}".format(key), val )
@pyqtSlot( str )
@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>", defaults={"index":0} )
def get_counter_image( gpid, side, index ):

@ -1,4 +1,4 @@
<div id="user-settings" style="display:none;">
<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>

Loading…
Cancel
Save