Automatically insert/update labels in a VASSAL save file.

master
Pacman Ghost 6 years ago
parent 035aa4922a
commit a933aa4541
  1. 1
      .gitignore
  2. 3
      .pylintrc
  3. 1
      _freeze.py
  4. 9
      conftest.py
  5. 2
      setup.py
  6. 83
      vasl_templates/file_dialog.py
  7. 6
      vasl_templates/main.py
  8. 26
      vasl_templates/main_window.py
  9. 84
      vasl_templates/server_settings.py
  10. 329
      vasl_templates/ui/server_settings.ui
  11. 96
      vasl_templates/web_channel.py
  12. 1
      vasl_templates/webapp/__init__.py
  13. 2
      vasl_templates/webapp/config/constants.py
  14. 8
      vasl_templates/webapp/config/site.cfg.example
  15. 2
      vasl_templates/webapp/data/default-template-pack/atmm.j2
  16. 2
      vasl_templates/webapp/data/default-template-pack/baz.j2
  17. 2
      vasl_templates/webapp/data/default-template-pack/extras/blank-space.j2
  18. 2
      vasl_templates/webapp/data/default-template-pack/extras/hip-guns.j2
  19. 2
      vasl_templates/webapp/data/default-template-pack/extras/kgs/grenade-bundles.j2
  20. 2
      vasl_templates/webapp/data/default-template-pack/extras/kgs/molotov-cocktails.j2
  21. 2
      vasl_templates/webapp/data/default-template-pack/extras/pf-count.j2
  22. 2
      vasl_templates/webapp/data/default-template-pack/extras/turn-track-shading.j2
  23. 2
      vasl_templates/webapp/data/default-template-pack/mol-p.j2
  24. 2
      vasl_templates/webapp/data/default-template-pack/mol.j2
  25. 2
      vasl_templates/webapp/data/default-template-pack/ob_note.j2
  26. 2
      vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
  27. 2
      vasl_templates/webapp/data/default-template-pack/ob_setup.j2
  28. 2
      vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
  29. 2
      vasl_templates/webapp/data/default-template-pack/pf.j2
  30. 2
      vasl_templates/webapp/data/default-template-pack/piat.j2
  31. 2
      vasl_templates/webapp/data/default-template-pack/players.j2
  32. 2
      vasl_templates/webapp/data/default-template-pack/psk.j2
  33. 2
      vasl_templates/webapp/data/default-template-pack/scenario.j2
  34. 2
      vasl_templates/webapp/data/default-template-pack/scenario_note.j2
  35. 2
      vasl_templates/webapp/data/default-template-pack/ssr.j2
  36. 2
      vasl_templates/webapp/data/default-template-pack/victory_conditions.j2
  37. 4
      vasl_templates/webapp/file_server/vasl_mod.py
  38. 5
      vasl_templates/webapp/static/css/main.css
  39. 9
      vasl_templates/webapp/static/css/vassal.css
  40. 18
      vasl_templates/webapp/static/extras.js
  41. 10
      vasl_templates/webapp/static/help/index.html
  42. 17
      vasl_templates/webapp/static/main.js
  43. 38
      vasl_templates/webapp/static/simple_notes.js
  44. 154
      vasl_templates/webapp/static/snippets.js
  45. 2
      vasl_templates/webapp/static/utils.js
  46. 291
      vasl_templates/webapp/static/vassal.js
  47. 6
      vasl_templates/webapp/templates/index.html
  48. 1
      vasl_templates/webapp/templates/testing.html
  49. 9
      vasl_templates/webapp/templates/vassal.html
  50. 57
      vasl_templates/webapp/tests/fixtures/dump-vsav/labels.txt
  51. BIN
      vasl_templates/webapp/tests/fixtures/dump-vsav/labels.vsav
  52. BIN
      vasl_templates/webapp/tests/fixtures/update-vsav/empty.vsav
  53. 1
      vasl_templates/webapp/tests/fixtures/update-vsav/full.json
  54. BIN
      vasl_templates/webapp/tests/fixtures/update-vsav/full.vsav
  55. 1
      vasl_templates/webapp/tests/fixtures/update-vsav/hill621-legacy.json
  56. BIN
      vasl_templates/webapp/tests/fixtures/update-vsav/hill621-legacy.vsav
  57. BIN
      vasl_templates/webapp/tests/fixtures/update-vsav/latw-legacy.vsav
  58. BIN
      vasl_templates/webapp/tests/fixtures/update-vsav/latw.vsav
  59. 9
      vasl_templates/webapp/tests/test_capabilities.py
  60. 4
      vasl_templates/webapp/tests/test_counters.py
  61. 21
      vasl_templates/webapp/tests/test_ob.py
  62. 21
      vasl_templates/webapp/tests/test_scenario_persistence.py
  63. 62
      vasl_templates/webapp/tests/test_snippets.py
  64. 574
      vasl_templates/webapp/tests/test_vassal.py
  65. 22
      vasl_templates/webapp/tests/utils.py
  66. 117
      vasl_templates/webapp/utils.py
  67. 349
      vasl_templates/webapp/vassal.py
  68. 3
      vassal-shim/.gitignore
  69. 27
      vassal-shim/Makefile
  70. 208
      vassal-shim/data/boardNames.txt
  71. BIN
      vassal-shim/release/vassal-shim.jar
  72. 51
      vassal-shim/src/vassal_shim/GamePieceLabelFields.java
  73. 107
      vassal-shim/src/vassal_shim/LabelArea.java
  74. 62
      vassal-shim/src/vassal_shim/Main.java
  75. 17
      vassal-shim/src/vassal_shim/ModuleManagerMenuManager.java
  76. 20
      vassal-shim/src/vassal_shim/ReportNode.java
  77. 57
      vassal-shim/src/vassal_shim/Snippet.java
  78. 45
      vassal-shim/src/vassal_shim/Utils.java
  79. 884
      vassal-shim/src/vassal_shim/VassalShim.java

1
.gitignore vendored

@ -1,4 +1,5 @@
_work_/
_releases_/
.venv*
*.pyc

@ -139,7 +139,8 @@ disable=print-statement,
invalid-name,
wrong-import-position,
global-statement,
too-few-public-methods
too-few-public-methods,
duplicate-code, # can't get it to shut up about @pytest.mark.skipif's :-/
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

@ -73,6 +73,7 @@ args = [
"--onefile",
"--name", target_name,
]
args.extend( [ "--add-data", "vassal-shim/release/vassal-shim.jar" + os.pathsep + "vasl_templates/webapp" ] )
# NOTE: We also need to include the config/ and data/ subdirectories, but we would like to
# make them available to the user, so we include them ourself in the final release archive.
def map_dir( src, dest ): #pylint: disable=missing-docstring

@ -43,12 +43,21 @@ def pytest_addoption( parser ):
"--short-tests", action="store_true", dest="short_tests", default=False,
help="Run a shorter version of the test suite."
)
# NOTE: Some tests require the VASL module file(s). We don't want to put these into source control,
# so we provide this option to allow the caller to specify where they live.
parser.addoption(
"--vasl-mods", action="store", dest="vasl_mods", default=None,
help="Directory containing the VASL .vmod file(s)."
)
# NOTE: Some tests require VASSAL to be installed. This option allows the caller to specify
# where it is (multiple installations can be placed in sub-directories).
parser.addoption(
"--vassal", action="store", dest="vassal", default=None,
help="Directory containing VASSAL installation(s)."
)
# NOTE: It's not good to have the code run differently to how it will normally,
# but using the clipboard to retrieve snippets causes more trouble than it's worth :-/
# since any kind of clipboard activity while the tests are running could cause them to fail

@ -22,13 +22,13 @@ setup(
"PyQT5==5.10.0",
"pyyaml==3.13",
"pillow==5.3.0",
"selenium==3.12.0",
"click==6.7",
],
extras_require = {
"dev": [
"pytest==3.6.0",
"tabulate==0.8.2",
"selenium==3.12.0",
"lxml==4.2.4",
"pylint==1.9.2",
"pytest-pylint==0.9.0",

@ -0,0 +1,83 @@
""" Manage loading and saving files. """
import os
from PyQt5.QtWidgets import QFileDialog
# ---------------------------------------------------------------------
# NOTE: While loading/saving files works fine when handled by the embedded browser,
# we can't get the full path of the file loaded (because of browser security).
# This means that we can't do things like default to saving a scenario to the same file
# it was loaded from, or retrying a failed save. This is such a lousy UX,
# we handle load/save operations ourself, where we can manage things like this.
class FileDialog:
"""Manage loading and saving files."""
def __init__( self, parent, object_name, default_extn, filters, default_fname ):
self.parent = parent
self.object_name = object_name
self.default_extn = default_extn
self.filters = filters
self.curr_fname = default_fname
def load_file( self, binary ):
"""Load a file."""
# ask the user which file to load
fname, _ = QFileDialog.getOpenFileName(
self.parent, "Load {}".format( self.object_name ),
self.curr_fname,
self.filters
)
if not fname:
return None
# load the file
try:
with open( fname, "rb" ) as fp:
data = fp.read()
except Exception as ex: #pylint: disable=broad-except
self.parent.showErrorMsg( "Can't load the {}:\n\n{}".format( self.object_name, ex ) )
return None
if not binary:
data = data.decode( "utf-8" )
self.curr_fname = fname
return data
def save_file( self, data ):
"""Save data to a file."""
# initialize
if isinstance( data, str ):
data = data.encode( "utf-8" )
while True: # nb: keep trying until the save succeeds or the user cancels the operation
# ask the user where to save the file
fname, _ = QFileDialog.getSaveFileName(
self.parent, "Save {}".format( self.object_name),
self.curr_fname,
self.filters
)
if not fname:
return False
# check the file extension
extn = os.path.splitext( fname )[1]
if not extn:
fname += self.default_extn
elif fname.endswith( "." ):
fname = fname[:-1]
# save the file
try:
with open( fname, "wb", ) as fp:
fp.write( data )
except Exception as ex: #pylint: disable=broad-except
self.parent.showErrorMsg( "Can't save the {}:\n\n{}".format( self.object_name, ex ) )
continue
self.curr_fname = fname
return True

@ -15,6 +15,8 @@ from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore
import click
from vasl_templates.webapp.utils import SimpleError
# FUDGE! This needs to be created before showing any UI elements e.g. an error message box.
qt_app = QApplication( sys.argv )
@ -86,7 +88,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
# configure the default scenario
if default_scenario:
if not os.path.isfile( default_scenario ):
raise RuntimeError( "Can't find the default scenario file." )
raise SimpleError( "Can't find the default scenario file." )
webapp_main.default_scenario = default_scenario
# configure remote debugging
@ -134,7 +136,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
except: #pylint: disable=bare-except
resp = None
if resp:
raise RuntimeError( "The application is already running." )
raise SimpleError( "The application is already running." )
# start the webapp server
def webapp_thread():

@ -5,15 +5,16 @@ import os
import re
import json
import io
import base64
import logging
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, QMargins, pyqtSlot
from PyQt5.QtCore import Qt, QUrl, QMargins, pyqtSlot, QVariant
from vasl_templates.webapp.config.constants import APP_NAME
from vasl_templates.webapp.config.constants import APP_NAME, IS_FROZEN
from vasl_templates.main import app_settings
from vasl_templates.web_channel import WebChannelHandler
from vasl_templates.utils import log_exceptions
@ -55,7 +56,7 @@ class MainWindow( QWidget ):
# initialize the main window
self.setWindowTitle( APP_NAME )
if getattr( sys, "frozen", False ):
if IS_FROZEN:
dname = sys._MEIPASS #pylint: disable=no-member,protected-access
else:
dname = os.path.join( os.path.split(__file__)[0], "webapp" )
@ -249,6 +250,25 @@ class MainWindow( QWidget ):
"""Called when the user wants to save a scenario."""
return self._web_channel_handler.save_scenario( data )
@pyqtSlot( result=QVariant )
@log_exceptions( caption="SLOT EXCEPTION" )
def load_vsav( self ):
"""Called when the user wants to update a VASL scenario."""
fname, data = self._web_channel_handler.load_vsav()
if data is None:
return None
return QVariant( {
"filename": fname,
"data": base64.b64encode( data ).decode( "utf-8" )
} )
@pyqtSlot( str, str, result=bool )
@log_exceptions( caption="SLOT EXCEPTION" )
def save_updated_vsav( self, fname, data ):
"""Called when a VASL scenario has been updated and is ready to be saved."""
data = base64.b64decode( data )
return self._web_channel_handler.save_updated_vsav( fname, data )
@pyqtSlot( str )
@log_exceptions( caption="SLOT EXCEPTION" )
def on_user_settings_change( self, user_settings ): #pylint: disable=no-self-use

@ -9,6 +9,7 @@ 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.vassal import SUPPORTED_VASSAL_VERSIONS_DISPLAY
from vasl_templates.webapp.file_server.vasl_mod import VaslMod, SUPPORTED_VASL_MOD_VERSIONS_DISPLAY
from vasl_templates.webapp.files import install_vasl_mod
@ -26,39 +27,96 @@ class ServerSettingsDialog( QDialog ):
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" ) )
)
for btn in ["vassal_dir","vasl_mod","boards_dir","java","webdriver"]:
getattr( self, "select_{}_button".format(btn) ).setIcon(
QIcon( os.path.join( base_dir, "resources/file_browser.png" ) )
)
self.setMinimumSize( self.size() )
# initialize handlers
self.select_vassal_dir_button.clicked.connect( self.on_select_vassal_dir )
self.select_vasl_mod_button.clicked.connect( self.on_select_vasl_mod )
self.select_boards_dir_button.clicked.connect( self.on_select_boards_dir )
self.select_java_button.clicked.connect( self.on_select_java )
self.select_webdriver_button.clicked.connect( self.on_select_webdriver )
self.ok_button.clicked.connect( self.on_ok )
self.cancel_button.clicked.connect( self.on_cancel )
# load the current server settings
self.vassal_dir.setText( app_settings.value( "ServerSettings/vassal-dir" ) )
self.vassal_dir.setToolTip(
"Supported versions: {}".format( SUPPORTED_VASSAL_VERSIONS_DISPLAY )
)
self.vasl_mod.setText( app_settings.value( "ServerSettings/vasl-mod" ) )
self.vasl_mod.setToolTip(
"Supported versions: {}".format( SUPPORTED_VASL_MOD_VERSIONS_DISPLAY )
)
self.boards_dir.setText( app_settings.value( "ServerSettings/boards-dir" ) )
self.java_path.setText( app_settings.value( "ServerSettings/java-path" ) )
self.webdriver_path.setText( app_settings.value( "ServerSettings/webdriver-path" ) )
self.webdriver_path.setToolTip( "Configure either geckodriver or chromedriver here." )
def on_select_vassal_dir( self ):
"""Let the user locate the VASSAL installation directory."""
dname = QFileDialog.getExistingDirectory(
self, "Select VASSAL installation directory",
self.vassal_dir.text(),
QFileDialog.ShowDirsOnly
)
if dname:
self.vassal_dir.setText( dname )
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 (*.*)"
self.vasl_mod.text(),
"VASL module files (*.vmod);;All files (*.*)"
)[0]
if fname:
self.vasl_mod.setText( fname )
def on_select_boards_dir( self ):
"""Let the user locate the VASL boards directory."""
dname = QFileDialog.getExistingDirectory(
self, "Select VASL boards directory",
self.boards_dir.text(),
QFileDialog.ShowDirsOnly
)
if dname:
self.boards_dir.setText( dname )
def on_select_java( self ):
"""Let the user locate the Java executable."""
fname = QFileDialog.getOpenFileName(
self, "Select Java executable",
self.java_path.text(),
_make_exe_filter_string()
)[0]
if fname:
self.java_path.setText( fname )
def on_select_webdriver( self ):
"""Let the user locate the webdriver executable."""
fname = QFileDialog.getOpenFileName(
self, "Select webdriver",
self.webdriver_path.text(),
_make_exe_filter_string()
)[0]
if fname:
self.webdriver_path.setText( fname )
def on_ok( self ):
"""Accept the new server settings."""
# save the new settings
app_settings.setValue( "ServerSettings/vassal-dir", self.vassal_dir.text() )
fname = self.vasl_mod.text().strip()
vasl_mod_changed = fname != app_settings.value( "ServerSettings/vasl-mod" )
app_settings.setValue( "ServerSettings/vasl-mod", fname )
app_settings.setValue( "ServerSettings/boards-dir", self.boards_dir.text() )
app_settings.setValue( "ServerSettings/java-path", self.java_path.text() )
app_settings.setValue( "ServerSettings/webdriver-path", self.webdriver_path.text() )
# install the new settings
# NOTE: We should really do this before saving the new settings, but that's more trouble
@ -80,11 +138,27 @@ class ServerSettingsDialog( QDialog ):
"""Cancel the dialog."""
self.close()
def _make_exe_filter_string():
"""Make a file filter string for executables."""
buf = []
if os.name == "nt":
buf.append( "Executable files (*.exe)" )
buf.append( "All files (*.*)" )
return ";;".join( buf )
# ---------------------------------------------------------------------
def install_server_settings():
"""Install the server settings."""
# install the server settings
from vasl_templates.webapp import app as app
app.config["VASSAL_DIR"] = app_settings.value( "ServerSettings/vassal-dir" )
app.config["VASL_MOD"] = app_settings.value( "ServerSettings/vasl-mod" )
app.config["BOARDS_DIR"] = app_settings.value( "ServerSettings/boards-dir" )
app.config["JAVA_PATH"] = app_settings.value( "ServerSettings/java-path" )
app.config["WEBDRIVER_PATH"] = app_settings.value( "ServerSettings/webdriver-path" )
# load the VASL module
fname = app_settings.value( "ServerSettings/vasl-mod" )
if fname:

@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>500</width>
<height>90</height>
<height>199</height>
</rect>
</property>
<property name="windowTitle">
@ -21,100 +21,227 @@
</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>
<layout class="QFormLayout" name="formLayout">
<property name="verticalSpacing">
<number>2</number>
</property>
</spacer>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>VA&amp;SSAL installation:</string>
</property>
<property name="buddy">
<cstring>vassal_dir</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="QLineEdit" name="vassal_dir"/>
</item>
<item>
<widget class="QPushButton" name="select_vassal_dir_button">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<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 row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="spacing">
<number>2</number>
</property>
<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>22</width>
<height>22</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>VASL &amp;boards:</string>
</property>
<property name="buddy">
<cstring>boards_dir</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="QLineEdit" name="boards_dir"/>
</item>
<item>
<widget class="QPushButton" name="select_boards_dir_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>&amp;Java:</string>
</property>
<property name="buddy">
<cstring>java_path</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="QLineEdit" name="java_path"/>
</item>
<item>
<widget class="QPushButton" name="select_java_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Web driver:</string>
</property>
<property name="buddy">
<cstring>webdriver_path</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="QLineEdit" name="webdriver_path"/>
</item>
<item>
<widget class="QPushButton" name="select_webdriver_button">
<property name="minimumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
@ -199,6 +326,20 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>vassal_dir</tabstop>
<tabstop>select_vassal_dir_button</tabstop>
<tabstop>vasl_mod</tabstop>
<tabstop>select_vasl_mod_button</tabstop>
<tabstop>boards_dir</tabstop>
<tabstop>select_boards_dir_button</tabstop>
<tabstop>java_path</tabstop>
<tabstop>select_java_button</tabstop>
<tabstop>webdriver_path</tabstop>
<tabstop>select_webdriver_button</tabstop>
<tabstop>ok_button</tabstop>
<tabstop>cancel_button</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

@ -2,87 +2,57 @@
import os
from PyQt5.QtWidgets import QFileDialog
from vasl_templates.webapp.config.constants import APP_NAME
from vasl_templates.file_dialog import FileDialog
# ---------------------------------------------------------------------
class WebChannelHandler:
"""Handle web channel requests."""
_FILE_FILTERS = "Scenario files (*.json);;All files (*)"
def __init__( self, window ):
# initialize
self._window = window
# NOTE: While loading/saving scenarios works fine when handled by the embedded browser,
# we can't get the full path of the file saved loaded (because of browser security).
# This means that we can't e.g. default saving a scenario to the same file it was loaded from.
# This is such a lousy UX, we handle load/save operations ourself, where we can manage this.
self._curr_scenario_fname = None
def __init__( self, parent ):
self.parent = parent
self.scenario_file_dialog = FileDialog(
self.parent,
"scenario", ".json",
"Scenario files (*.json);;All files (*)",
"scenario.json"
)
self.updated_vsav_file_dialog = FileDialog(
self.parent,
"VASL scenario", ".vsav",
"VASL scenario files (*.vsav);;All files (*)",
"scenario.vsav"
)
def on_new_scenario( self ):
"""Called when the scenario is reset."""
self._curr_scenario_fname = None
self.scenario_file_dialog.curr_fname = None
def load_scenario( self ):
"""Called when the user wants to load a scenario."""
# ask the user which file to load
fname, _ = QFileDialog.getOpenFileName(
self._window, "Load scenario",
os.path.split(self._curr_scenario_fname)[0] if self._curr_scenario_fname else None,
WebChannelHandler._FILE_FILTERS
)
if not fname:
return None
# load the scenario
try:
with open( fname, "r", encoding="utf-8" ) as fp:
data = fp.read()
except Exception as ex: #pylint: disable=broad-except
self._window.showErrorMsg( "Can't load the scenario:\n\n{}".format( ex ) )
return None
self._curr_scenario_fname = fname
return data
return self.scenario_file_dialog.load_file( False )
def save_scenario( self, data ):
"""Called when the user wants to save a scenario."""
# ask the user where to save the scenario
fname, _ = QFileDialog.getSaveFileName(
self._window, "Save scenario",
self._curr_scenario_fname,
WebChannelHandler._FILE_FILTERS
)
if not fname:
return False
# check the file extension
extn = os.path.splitext( fname )[1]
if not extn:
fname += ".json"
elif fname.endswith( "." ):
fname = fname[:-1]
# save the file
try:
with open( fname, "w", encoding="utf-8" ) as fp:
fp.write( data )
except Exception as ex: #pylint: disable=broad-except
self._window.showErrorMsg( "Can't save the scenario:\n\n{}".format( ex ) )
return False
self._curr_scenario_fname = fname
return True
return self.scenario_file_dialog.save_file( data )
def on_scenario_name_change( self, val ):
"""Update the main window title to show the scenario name."""
self._window.setWindowTitle(
self.parent.setWindowTitle(
"{} - {}".format( APP_NAME, val ) if val else APP_NAME
)
def load_vsav( self ):
"""Called when the user wants to load a VASL scenario to update."""
data = self.updated_vsav_file_dialog.load_file( True )
if data is None:
return None, None
fname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[1]
return fname, data
def save_updated_vsav( self, fname, data ):
"""Called when a VASL scenario has been updated and is ready to be saved."""
dname = os.path.split( self.updated_vsav_file_dialog.curr_fname )[0]
self.updated_vsav_file_dialog.curr_fname = os.path.join( dname, fname )
return self.updated_vsav_file_dialog.save_file( data )

@ -59,6 +59,7 @@ import vasl_templates.webapp.main #pylint: disable=cyclic-import
import vasl_templates.webapp.vo #pylint: disable=cyclic-import
import vasl_templates.webapp.snippets #pylint: disable=cyclic-import
import vasl_templates.webapp.files #pylint: disable=cyclic-import
import vasl_templates.webapp.vassal #pylint: disable=cyclic-import
# ---------------------------------------------------------------------

@ -8,7 +8,9 @@ APP_VERSION = "v0.5" # nb: also update setup.py
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
if getattr( sys, "frozen", False ):
IS_FROZEN = True
BASE_DIR = os.path.split( sys.executable )[0]
else:
IS_FROZEN = False
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )
DATA_DIR = os.path.join( BASE_DIR, "data" )

@ -1,4 +1,10 @@
[Site Config]
; Enable VASL counter images in the UI by configuring a VASL .vmod file here.
VASL_MOD = ...
VASL_MOD = ...configure the VASL module (e.g. vasl-6.4.3.vmod)...
; Configure VASSAL to be able to automatically update labels in a VASL scenario.
VASSAL_DIR = ...configure the VASSAL installation directory...
BOARDS_DIR = ...configure the VASL boards directory...
WEBDRIVER_PATH = ...configure either geckodriver or chromedriver here...
; JAVA_PATH = ...configure the Java executable here (optional, must be in the PATH otherwise)...

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name Blank space -->
<!-- vasl-templates:description Generates a white label that can be used to cover up and hide things in your scenario. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name Hidden Guns -->
<!-- vasl-templates:description HIP Guns for Solo Play, taken from <a href="http://vftt.co.uk/vfttpdfs.asp"><i>View From The Trenches</i></a>, Issue 34/35. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name KGS Grenade Bundles -->
<!-- vasl-templates:description Data chart for Grenade Bundles in <i>Kampfgruppe Scherer</i>. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name KGS Molotov Cocktails -->
<!-- vasl-templates:description Data chart for Molotov Cocktails in <i>Kampfgruppe Scherer</i>. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name PF count -->
<!-- vasl-templates:description Add the snippet as the label of a Panzerfaust counter, then press <i>Ctrl-L</i> when you need to update the number of remaining shots. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<!-- vasl-templates:name Turn Track shading -->
<!-- vasl-templates:description Generates a shaded square that you can place behind the Turn Track to indicate an LV Hindrance e.g. because of dusk/dawn. -->

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -1,4 +1,4 @@
<html>
<html> <!-- vasl-templates:id {{SNIPPET_ID}} -->
<head>
<meta charset="utf-8">

@ -11,8 +11,8 @@ _logger = logging.getLogger( "vasl_mod" )
from vasl_templates.webapp.file_server.utils import get_vo_gpids, get_effective_gpid
SUPPORTED_VASL_MOD_VERSIONS = [ "6.3.3", "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.3.3, 6.4.0-6.4.3"
SUPPORTED_VASL_MOD_VERSIONS = [ "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.4.0-6.4.3"
# ---------------------------------------------------------------------

@ -14,7 +14,7 @@ label { height: 1.25em ; margin-top: -3px ; }
#menu { position: absolute ; top: 15px ; right: 8px ; z-index: 1 ; }
#menu input[type='image'] { height: 30px ; }
.PopMenu-Item { width: 11em ; }
.PopMenu-Item { width: 12em ; }
.PopMenu-Item a { padding: 5px 10px 5px 10px ; }
.PopMenu-Icon { display: none ; }
@ -26,7 +26,8 @@ label { height: 1.25em ; margin-top: -3px ; }
.select2-dropdown { color: #444 ; }
.snippet-control button.generate { height: 26px ; padding: 2px 10px 2px 5px ; }
.snippet-control button.generate { height: 26px ; padding: 2px 10px 2px 5px ; color: #000 ; }
.snippet-control button.generate.inactive { color: #aaa ; }
.snippet-control button.generate img { height: 20px ; margin-right: 5px ; vertical-align: middle ; }
.snippet-control .ui-selectmenu-button { padding: 2px 10px ; }
.snippet-control-menu-item { font-size: 75% ; font-style: italic ; }

@ -0,0 +1,9 @@
.ui-dialog.update-vsav .ui-dialog-titlebar { display: none ; }
#update-vsav { display: flex ; align-items: center ; }
#update-vsav img { margin-right: 1em ; }
#vassal-shim-error textarea { width: 100% ; height: 15em ; min-height: 5em ; resize: none ; padding: 2px ; font-family: monospace ; font-size: 80% ; }
.ui-dialog.vassal-shim-error .ui-dialog-titlebar { background: #f5af41 ; }
.ui-dialog.vassal-shim-error .ui-dialog-content { display: flex ; flex-direction: column ; }
.ui-dialog.vassal-shim-error .ui-dialog-content textarea { flex-grow: 1 ; }
.ui-dialog.vassal-shim-error .ui-dialog-buttonpane { border: none ; margin-top: 0 !important ; padding-top: 0 !important ; }

@ -147,20 +147,30 @@ function _parse_extra_template( template_id, template )
function fixup_template_parameters( template )
{
// identify any non-standard template parameters
var matches = [] ;
var regex = /\{\{([A-Z0-9_]+?):.*?\}\}/g ;
var matches = [] ;
var match ;
while( (match = regex.exec( template )) !== null )
matches.push( [ regex.lastIndex-match[0].length, match[0].length, match[1] ] ) ;
// fix them up
var i ;
if ( matches.length > 0 ) {
for ( var i=matches.length-1 ; i >= 0 ; --i )
for ( i=matches.length-1 ; i >= 0 ; --i )
template = template.substr(0,matches[i][0]) + "{{"+matches[i][2]+"}}" + template.substr(matches[i][0]+matches[i][1]) ;
}
// remove comments
template = template.replace( /<!-- vasl-templates:.*? -->\n*/g, "" ) ;
// remove all our special comments, except for the snippet ID
regex = /<!-- vasl-templates:(.*?) .*? -->\n*/g ;
matches = [] ;
while( (match = regex.exec( template )) !== null ) {
if ( match[1] !== "id" )
matches.push( [ regex.lastIndex-match[0].length, match[0].length ] ) ;
}
if ( matches.length > 0 ) {
for ( i=matches.length-1 ; i >= 0 ; --i )
template = template.substr(0,matches[i][0]) + template.substr(matches[i][0]+matches[i][1]) ;
}
return template ;
}

@ -296,6 +296,16 @@ pytest --webdriver chrome --headless
<p> <small><em>NOTE: Internet Explorer is also supported as a WebDriver, but due to differences in the way it works, some tests are currently failing for this.</em></small>
<h2> Compiling the VASSAL shim </h2>
<p> The program uses VASSAL to update VASL scenarios (<tt>.vsav</tt> files), and since this is written in Java, a helper program has been written in Java to do this.
<p> To compile the program, go to the <tt>$/vassal-shim</tt> directory and type:
<div class="code">
make all VASSAL_DIR=...
</div>
where <tt>VASSAL_DIR</tt> points to VASSAL's <tt>lib/</tt> directory (the program needs <tt>Vengine.jar</tt>).
<p> Since this program doesn't change very often, the resulting artifact (<tt>vassal-shim.jar</tt>) is checked into source control, so that it can be used without needing to install a JDK and compiling it first.
<h2> Code lint'ing </h2>
<p> Python code is checked using <a href="https://pylint.readthedocs.io/en/latest/"><tt>pylint</tt></a> (installed during the <tt>pip install</tt> above), which should be run from the root directory of the repo.

@ -9,7 +9,7 @@ gVaslPieceInfo = {} ;
gWebChannelHandler = null ;
gEmSize = null ;
var _NATIONALITY_SPECIFIC_BUTTONS = {
var NATIONALITY_SPECIFIC_BUTTONS = {
"russian": [ "mol", "mol-p" ],
"german": [ "pf", "psk", "atmm" ],
"american": [ "baz" ],
@ -39,6 +39,7 @@ $(document).ready( function () {
new_scenario: { label: "New scenario", action: function() { on_new_scenario() ; } },
load_scenario: { label: "Load scenario", action: on_load_scenario },
save_scenario: { label: "Save scenario", action: on_save_scenario },
update_vsav: { label: "Update VASL scenario", action: on_update_vsav },
separator: { type: "separator" },
template_pack: { label: "Load template pack", action: on_template_pack },
separator2: { type: "separator" },
@ -63,10 +64,10 @@ $(document).ready( function () {
} ) ;
}
} ) ;
// add a handler for when the "load scenario" file has been selected
// add handlers
$("#load-scenario").change( on_load_scenario_file_selected ) ;
// add a handler for when the "load template pack" file has been selected
$("#load-template-pack").change( on_template_pack_file_selected ) ;
$("#load-vsav").change( on_load_vsav_file_selected ) ;
// all done - we can show the menu now
$("#menu").show() ;
@ -421,10 +422,10 @@ function update_page_load_status( id )
// check if the vehicle/ordnance listings have finished loading
if ( gPageLoadStatus.indexOf( "vehicle-listings" ) === -1 && gPageLoadStatus.indexOf( "ordnance-listings" ) === -1 ) {
// NOTE: If the default scanerio contains any vehicles or ordnance, it will look up the V/O listings,
// NOTE: If the default scenario contains any vehicles or ordnance, it will look up the V/O listings,
// so we need to wait until those have arrived. Note that while the default scenario will normally
// be empty, having stuff in it is very useful during development.
do_on_new_scenario() ;
do_on_new_scenario( false ) ;
}
// check if the page has finished loading
@ -555,9 +556,9 @@ function on_player_change( player_no )
var player_nat = update_ob_tab_header( player_no ) ;
// show/hide the nationality-specific buttons
for ( var nat in _NATIONALITY_SPECIFIC_BUTTONS ) {
for ( var i=0 ; i < _NATIONALITY_SPECIFIC_BUTTONS[nat].length ; ++i ) {
var button_id = _NATIONALITY_SPECIFIC_BUTTONS[nat][i] ;
for ( var nat in NATIONALITY_SPECIFIC_BUTTONS ) {
for ( var i=0 ; i < NATIONALITY_SPECIFIC_BUTTONS[nat].length ; ++i ) {
var button_id = NATIONALITY_SPECIFIC_BUTTONS[nat][i] ;
var $elem = $( "#panel-ob_notes_" + player_no + " div.snippet-control[data-id='" + button_id + "']" ) ;
$elem.css( "display", nat == player_nat ? "inline-block" : "none" ) ;
}

@ -88,6 +88,13 @@ function _do_edit_simple_note( $sortable2, $entry, default_width )
// create a new note
if ( caption !== "" ) {
data = { caption: caption, width: width } ;
if ( note_type === "scenario_notes" || note_type === "ob_setups" || note_type === "ob_notes" ) {
var usedIds = {} ;
$sortable2.find( "li" ).each( function() {
usedIds[ $(this).data("sortable2-data").id ] = true ;
} ) ;
data.id = auto_assign_id( usedIds ) ;
}
_do_add_simple_note( $sortable2, data ) ;
}
}
@ -132,23 +139,32 @@ function _make_simple_note( note_type, caption )