diff --git a/vasl_templates/main_window.py b/vasl_templates/main_window.py
index 6343e58..f1dfe01 100644
--- a/vasl_templates/main_window.py
+++ b/vasl_templates/main_window.py
@@ -2,6 +2,8 @@
import os
import re
+import json
+import io
import logging
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QMessageBox
@@ -161,7 +163,35 @@ class MainWindow( QWidget ):
@pyqtSlot()
@log_exceptions( caption="SLOT EXCEPTION" )
- def on_new_scenario( self):
+ def on_app_loaded( self ):
+ """Called when the application has finished loading.
+
+ NOTE: This handler might be called multiple times.
+ """
+ # load and install the user settings
+ buf = io.StringIO()
+ buf.write( "{" )
+ for key in self.settings.allKeys():
+ if key.startswith( "UserSettings/" ):
+ buf.write( '"{}": {},'.format( key[13:], self.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 ) )
+ logging.error( "Couldn't load the user settings: %s", ex )
+ logging.error( buf )
+ return
+ del user_settings["_dummy_"]
+ self._view.page().runJavaScript(
+ "install_user_settings('{}')".format( json.dumps( user_settings ) )
+ )
+
+ @pyqtSlot()
+ @log_exceptions( caption="SLOT EXCEPTION" )
+ def on_new_scenario( self ):
"""Called when the user wants to load a scenario."""
self._web_channel_handler.on_new_scenario()
@@ -177,6 +207,19 @@ class MainWindow( QWidget ):
"""Called when the user wants to save a scenario."""
return self._web_channel_handler.save_scenario( data )
+ @pyqtSlot( str )
+ @log_exceptions( caption="SLOT EXCEPTION" )
+ def on_user_settings_change( self, user_settings ):
+ """Called when the user changes the user settings."""
+ # delete all existing keys
+ for key in self.settings.allKeys():
+ if key.startswith( "UserSettings/" ):
+ self.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 )
+
@pyqtSlot( str )
@log_exceptions( caption="SLOT EXCEPTION" )
def on_scenario_name_change( self, val ):
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2 b/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
index 2cd094d..6fd5fde 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
@@ -3,7 +3,7 @@
@@ -23,8 +23,9 @@ sup { font-size: 75% ; }
{%for ord in OB_ORDNANCE%}
-
+ |
{{ord.name}}
+ {%if ord.image%} {%endif%}
{%if ord.notes%}
{{ord.note_number}}, {{ord.notes | join(", ")}}
diff --git a/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2 b/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
index 3252726..2f9922c 100644
--- a/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
+++ b/vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
@@ -3,7 +3,7 @@
@@ -23,8 +23,9 @@ sup { font-size: 75% ; }
{%for veh in OB_VEHICLES%}
-
+ |
{{veh.name}}
+ {%if veh.image%} {%endif%}
{%if veh.notes%}
{{veh.note_number}}, {{veh.notes | join(", ")}}
diff --git a/vasl_templates/webapp/static/css/main.css b/vasl_templates/webapp/static/css/main.css
index f8c4342..8b87c19 100644
--- a/vasl_templates/webapp/static/css/main.css
+++ b/vasl_templates/webapp/static/css/main.css
@@ -137,6 +137,10 @@ button.edit-template img { height: 18px ; vertical-align: middle ; margin-right:
.ui-dialog.select-vo-image .ui-dialog-content { padding: 0 ; height: 100% !important ; overflow: hidden ; }
.ui-dialog.select-vo-image .vo-images img { margin: 5px ; padding: 10px ; border: 1px dotted #ddd ; }
+.ui-dialog.user-settings .ui-dialog-titlebar { background: #80d0ff ; }
+.ui-dialog.user-settings .ui-dialog-buttonpane { border: none ; padding: 0 ; font-size: 75% ; }
+.ui-dialog.user-settings .note { font-size: 80% ; font-style: italic ; color: #666 ; }
+
.growl-title { display: none ; }
.growl .pre { font-family: monospace ; }
.growl div.pre { margin: 0 0 1em 1em ; font-size: 80% ; }
diff --git a/vasl_templates/webapp/static/js-cookie/js.cookie.js b/vasl_templates/webapp/static/js-cookie/js.cookie.js
new file mode 100644
index 0000000..6d0965a
--- /dev/null
+++ b/vasl_templates/webapp/static/js-cookie/js.cookie.js
@@ -0,0 +1,163 @@
+/*!
+ * JavaScript Cookie v2.2.0
+ * https://github.com/js-cookie/js-cookie
+ *
+ * Copyright 2006, 2015 Klaus Hartl & Fagner Brack
+ * Released under the MIT license
+ */
+;(function (factory) {
+ var registeredInModuleLoader;
+ if (typeof define === 'function' && define.amd) {
+ define(factory);
+ registeredInModuleLoader = true;
+ }
+ if (typeof exports === 'object') {
+ module.exports = factory();
+ registeredInModuleLoader = true;
+ }
+ if (!registeredInModuleLoader) {
+ var OldCookies = window.Cookies;
+ var api = window.Cookies = factory();
+ api.noConflict = function () {
+ window.Cookies = OldCookies;
+ return api;
+ };
+ }
+}(function () {
+ function extend () {
+ var i = 0;
+ var result = {};
+ for (; i < arguments.length; i++) {
+ var attributes = arguments[ i ];
+ for (var key in attributes) {
+ result[key] = attributes[key];
+ }
+ }
+ return result;
+ }
+
+ function decode (s) {
+ return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
+ }
+
+ function init (converter) {
+ function api() {}
+
+ function set (key, value, attributes) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ attributes = extend({
+ path: '/'
+ }, api.defaults, attributes);
+
+ if (typeof attributes.expires === 'number') {
+ attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5);
+ }
+
+ // We're using "expires" because "max-age" is not supported by IE
+ attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';
+
+ try {
+ var result = JSON.stringify(value);
+ if (/^[\{\[]/.test(result)) {
+ value = result;
+ }
+ } catch (e) {}
+
+ value = converter.write ?
+ converter.write(value, key) :
+ encodeURIComponent(String(value))
+ .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
+
+ key = encodeURIComponent(String(key))
+ .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)
+ .replace(/[\(\)]/g, escape);
+
+ var stringifiedAttributes = '';
+ for (var attributeName in attributes) {
+ if (!attributes[attributeName]) {
+ continue;
+ }
+ stringifiedAttributes += '; ' + attributeName;
+ if (attributes[attributeName] === true) {
+ continue;
+ }
+
+ // Considers RFC 6265 section 5.2:
+ // ...
+ // 3. If the remaining unparsed-attributes contains a %x3B (";")
+ // character:
+ // Consume the characters of the unparsed-attributes up to,
+ // not including, the first %x3B (";") character.
+ // ...
+ stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
+ }
+
+ return (document.cookie = key + '=' + value + stringifiedAttributes);
+ }
+
+ function get (key, json) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ var jar = {};
+ // To prevent the for loop in the first place assign an empty array
+ // in case there are no cookies at all.
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+ var i = 0;
+
+ for (; i < cookies.length; i++) {
+ var parts = cookies[i].split('=');
+ var cookie = parts.slice(1).join('=');
+
+ if (!json && cookie.charAt(0) === '"') {
+ cookie = cookie.slice(1, -1);
+ }
+
+ try {
+ var name = decode(parts[0]);
+ cookie = (converter.read || converter)(cookie, name) ||
+ decode(cookie);
+
+ if (json) {
+ try {
+ cookie = JSON.parse(cookie);
+ } catch (e) {}
+ }
+
+ jar[name] = cookie;
+
+ if (key === name) {
+ break;
+ }
+ } catch (e) {}
+ }
+
+ return key ? jar[key] : jar;
+ }
+
+ api.set = set;
+ api.get = function (key) {
+ return get(key, false /* read as raw */);
+ };
+ api.getJSON = function (key) {
+ return get(key, true /* read as json */);
+ };
+ api.remove = function (key, attributes) {
+ set(key, '', extend(attributes, {
+ expires: -1
+ }));
+ };
+
+ api.defaults = {};
+
+ api.withConverter = init;
+
+ return api;
+ }
+
+ return init(function () {});
+}));
diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js
index ec7c5ed..698d945 100644
--- a/vasl_templates/webapp/static/main.js
+++ b/vasl_templates/webapp/static/main.js
@@ -1,3 +1,5 @@
+APP_URL_BASE = window.location.origin ;
+
gTemplatePack = {} ;
gDefaultNationalities = {} ;
gValidTemplateIds = [] ;
@@ -24,6 +26,9 @@ $(document).ready( function () {
// connect to the web channel
new QWebChannel( qt.webChannelTransport, function(channel) {
gWebChannelHandler = channel.objects.handler ;
+ // FUDGE! If the page finishes loading before the web channel is ready,
+ // the desktop won't get this notification. To be sure, we issue it again...
+ gWebChannelHandler.on_app_loaded() ;
} ) ;
} ) ;
}
@@ -37,6 +42,7 @@ $(document).ready( function () {
separator: { type: "separator" },
template_pack: { label: "Load template pack", action: on_template_pack },
separator2: { type: "separator" },
+ user_settings: { label: "Settings", action: user_settings },
show_help: { label: "Help", action: show_help },
} ) ;
// nb: we only show the popmenu on left click (not the normal right-click)
@@ -402,6 +408,9 @@ function update_page_load_status( id )
$("#watermark").fadeIn( 5*1000 ) ;
// notify the test suite
$("body").append( $(" ") ) ;
+ // notify the PyQT desktop application
+ if ( gWebChannelHandler )
+ gWebChannelHandler.on_app_loaded() ;
}
}
diff --git a/vasl_templates/webapp/static/snippets.js b/vasl_templates/webapp/static/snippets.js
index d0949af..432aed4 100644
--- a/vasl_templates/webapp/static/snippets.js
+++ b/vasl_templates/webapp/static/snippets.js
@@ -220,6 +220,11 @@ function unload_snippet_params( params, check_date_capabilities )
note_number: vo_entry.note_number,
notes: vo_entry.notes
} ;
+ if ( gUserSettings["include-vasl-images-in-snippets"] ) {
+ var url = get_vo_image_url( vo_entry, vo_image_id ) ;
+ if ( url )
+ obj.image = APP_URL_BASE + url ;
+ }
if ( vo_entry.no_radio )
obj.no_radio = vo_entry.no_radio ;
if ( vo_entry.no_if ) {
diff --git a/vasl_templates/webapp/static/user_settings.js b/vasl_templates/webapp/static/user_settings.js
new file mode 100644
index 0000000..9e68763
--- /dev/null
+++ b/vasl_templates/webapp/static/user_settings.js
@@ -0,0 +1,82 @@
+gUserSettings = Cookies.getJSON( "user-settings" ) || {} ;
+
+USER_SETTINGS = {
+ "include-vasl-images-in-snippets": "checkbox",
+} ;
+
+// --------------------------------------------------------------------
+
+function user_settings()
+{
+ function load_settings() {
+ // load each user setting
+ for ( var name in USER_SETTINGS ) {
+ var $elem = $( ".ui-dialog.user-settings [name='" + name + "']" ) ;
+ var func = handlers[ "load_" + USER_SETTINGS[name] ] ;
+ func( $elem, gUserSettings[name] ) ;
+ }
+ }
+
+ function unload_settings() {
+ // unload each user setting
+ var settings = {} ;
+ for ( var name in USER_SETTINGS ) {
+ var $elem = $( ".ui-dialog.user-settings [name='" + name + "']" ) ;
+ func = handlers[ "unload_" + USER_SETTINGS[name] ] ;
+ settings[name] = func( $elem ) ;
+ }
+ return settings ;
+ }
+
+ var handlers = {
+ load_checkbox: function( $elem, val ) { $elem.prop( "checked", val?true:false ) ; },
+ unload_checkbox: function( $elem ) { return $elem.prop( "checked" ) ; },
+ } ;
+
+ function update_ui() {
+ // update the UI
+ var $dlg = $( ".ui-dialog.user-settings" ) ;
+ var is_checked = $dlg.find( "input[name='include-vasl-images-in-snippets']" ).prop( "checked" ) ;
+ $dlg.find( ".include-vasl-images-in-snippets-hint" ).css(
+ "color", is_checked ? "#444" : "#aaa"
+ ) ;
+ }
+
+ // show the "user settings" dialog
+ $( "#user-settings" ).dialog( {
+ title: "User settings",
+ dialogClass: "user-settings",
+ modal: true,
+ width: 450,
+ height: 150,
+ resizable: false,
+ create: function() {
+ init_dialog( $(this), "OK", false ) ;
+ $(this).find( "input[name='include-vasl-images-in-snippets']" ).change( update_ui ) ;
+ },
+ open: function() {
+ // load the current user settings
+ load_settings( $(this) ) ;
+ update_ui() ;
+ },
+ buttons: {
+ OK: function() {
+ // unload and install the new user settings
+ var settings = unload_settings() ;
+ gUserSettings = settings ;
+ Cookies.set( "user-settings", settings, { expires: 999 } ) ;
+ if ( gWebChannelHandler )
+ gWebChannelHandler.on_user_settings_change( JSON.stringify( settings ) ) ;
+ $(this).dialog( "close" ) ;
+ },
+ Cancel: function() { $(this).dialog( "close" ) ; },
+ },
+ } ) ;
+}
+
+// --------------------------------------------------------------------
+
+function install_user_settings( user_settings ) // nb: this is called by the PyQT desktop application
+{
+ gUserSettings = JSON.parse( user_settings ) ;
+}
diff --git a/vasl_templates/webapp/static/vo.js b/vasl_templates/webapp/static/vo.js
index 845823b..e4d5866 100644
--- a/vasl_templates/webapp/static/vo.js
+++ b/vasl_templates/webapp/static/vo.js
@@ -32,7 +32,7 @@ function add_vo( vo_type, player_no )
if ( is_small_vasl_piece( vo_entry ) )
div_class += " small-piece" ;
var buf2 = [ " ",
- " ",
+ " ",
" ",
vo_entry.name,
vo_entry.type ? " ("+vo_entry.type+")" : "",
@@ -131,7 +131,7 @@ function do_add_vo( vo_type, player_no, vo_entry, vo_image_id )
fixed_height = "2.5em" ;
}
div_tag += ">" ;
- var url = _get_vo_image_url( vo_entry, vo_image_id ) ;
+ var url = get_vo_image_url( vo_entry, vo_image_id, true ) ;
$sortable2.sortable2( "add", {
content: $( div_tag + " " + vo_entry.name + " " ),
data: { caption: vo_entry.name, vo_entry: vo_entry, vo_image_id: vo_image_id, fixed_height: fixed_height },
@@ -214,7 +214,7 @@ function on_select_vo_image( $btn ) {
for ( var i=0 ; i < vo_images.length ; ++i ) {
var $elem = $( " " )
.bind( "load", on_image_loaded )
- .attr( "src", _get_vo_image_url( null, vo_images[i] ) ) ;
+ .attr( "src", get_vo_image_url( null, vo_images[i], true ) ) ;
$images.append( $elem ) ;
}
@@ -240,7 +240,7 @@ function on_select_vo_image( $btn ) {
// handle image selection
$images.children( "img" ).click( function() {
vo_image_id = vo_images[ $(this).data("index") ] ;
- $img.attr( "src", _get_vo_image_url(vo_image_id) ) ;
+ $img.attr( "src", get_vo_image_url(vo_image_id,true) ) ;
$img.data( "vo-image-id", vo_image_id ) ;
$dlg.dialog( "close" ) ;
// nb: if the user selected an image, we take that to mean they also want to add that vehicle/ordnance
@@ -262,7 +262,7 @@ function on_select_vo_image( $btn ) {
} ) ;
}
-function _get_vo_image_url( vo_entry, vo_image_id )
+function get_vo_image_url( vo_entry, vo_image_id, allow_missing_image )
{
if ( vo_image_id )
return "/counter/" + vo_image_id[0] + "/front/" + vo_image_id[1] ;
@@ -273,7 +273,7 @@ function _get_vo_image_url( vo_entry, vo_image_id )
if ( vo_entry.gpid )
return "/counter/" + vo_entry.gpid + "/front" ;
}
- return gImagesBaseUrl + "/missing-image.png" ;
+ return allow_missing_image ? gImagesBaseUrl + "/missing-image.png" : null ;
}
function is_small_vasl_piece( vo_entry )
diff --git a/vasl_templates/webapp/templates/index.html b/vasl_templates/webapp/templates/index.html
index 3b467d7..868bc57 100644
--- a/vasl_templates/webapp/templates/index.html
+++ b/vasl_templates/webapp/templates/index.html
@@ -52,6 +52,8 @@
{%include "select-vo-dialog.html"%}
{%include "select-vo-image-dialog.html"%}
+{%include "user-settings-dialog.html"%}
+
{%include "ask-dialog.html"%}
| |