Allow VASL counter images to be included in V/O snippets.

master
Pacman Ghost 6 years ago
parent 6dee1f6e36
commit ec7e9f00b3
  1. 45
      vasl_templates/main_window.py
  2. 5
      vasl_templates/webapp/data/default-template-pack/ob_ordnance.j2
  3. 5
      vasl_templates/webapp/data/default-template-pack/ob_vehicles.j2
  4. 4
      vasl_templates/webapp/static/css/main.css
  5. 163
      vasl_templates/webapp/static/js-cookie/js.cookie.js
  6. 9
      vasl_templates/webapp/static/main.js
  7. 5
      vasl_templates/webapp/static/snippets.js
  8. 82
      vasl_templates/webapp/static/user_settings.js
  9. 12
      vasl_templates/webapp/static/vo.js
  10. 4
      vasl_templates/webapp/templates/index.html
  11. 4
      vasl_templates/webapp/templates/user-settings-dialog.html
  12. 56
      vasl_templates/webapp/tests/test_user_settings.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 ):

@ -3,7 +3,7 @@
<head>
<style>
td { margin: 0 ; padding: 0 ; }
.note { font-size: 90% ; font-style: italic ; color: #808080 ; }
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; }
sup { font-size: 75% ; }
</style>
</head>
@ -23,8 +23,9 @@ sup { font-size: 75% ; }
{%for ord in OB_ORDNANCE%}
<tr style="border-bottom:1px dotted #e0e0e0;">
<td valign="top" style="padding:2px 5px;">
<td valign="top" style="padding:2px 5px 5px;">
<b> {{ord.name}} </b>
{%if ord.image%} <br> <img src="{{ord.image}}"> {%endif%}
<div class="note">
{%if ord.notes%}
{{ord.note_number}}, {{ord.notes | join(", ")}}

@ -3,7 +3,7 @@
<head>
<style>
td { margin: 0 ; padding: 0 ; }
.note { font-size: 90% ; font-style: italic ; color: #808080 ; }
.note { margin-top: 2px ; font-size: 90% ; font-style: italic ; color: #808080 ; }
sup { font-size: 75% ; }
</style>
</head>
@ -23,8 +23,9 @@ sup { font-size: 75% ; }
{%for veh in OB_VEHICLES%}
<tr style="border-bottom:1px dotted #e0e0e0;">
<td valign="top" style="padding:2px 5px;">
<td valign="top" style="padding:2px 5px 5px;">
<b> {{veh.name}} </b>
{%if veh.image%} <br> <img src="{{veh.image}}"> {%endif%}
<div class="note">
{%if veh.notes%}
{{veh.note_number}}, {{veh.notes | join(", ")}}

@ -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% ; }

@ -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 () {});
}));

@ -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( $("<div id='_page-loaded_'></div>") ) ;
// notify the PyQT desktop application
if ( gWebChannelHandler )
gWebChannelHandler.on_app_loaded() ;
}
}

@ -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 ) {

@ -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 ) ;
}

@ -32,7 +32,7 @@ function add_vo( vo_type, player_no )
if ( is_small_vasl_piece( vo_entry ) )
div_class += " small-piece" ;
var buf2 = [ "<div class='" + div_class + "' data-index='" + opt.id + "'>",
"<img src='" + _get_vo_image_url(vo_entry,null) + "' class='vasl-image'>",
"<img src='" + get_vo_image_url(vo_entry,null,true) + "' class='vasl-image'>",
"<div class='content'><div>",
vo_entry.name,
vo_entry.type ? "&nbsp;<span class='vo-type'>("+vo_entry.type+")</span>" : "",
@ -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 + "<img src='"+url+"'>" + vo_entry.name + "</div>" ),
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 = $( "<img data-index='" + i + "'>" )
.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 )

@ -52,6 +52,8 @@
{%include "select-vo-dialog.html"%}
{%include "select-vo-image-dialog.html"%}
{%include "user-settings-dialog.html"%}
{%include "ask-dialog.html"%}
</body>
@ -67,6 +69,7 @@
<script src="{{url_for('static',filename='download/download.min.js')}}"></script>
<script src="{{url_for('static',filename='jszip/jszip.min.js')}}"></script>
<script src="{{url_for('static',filename='select2/select2.min.js')}}"></script>
<script src="{{url_for('static',filename='js-cookie/js.cookie.js')}}"></script>
<script>
gAppName = "{{APP_NAME}}" ;
@ -85,6 +88,7 @@ gHelpUrl = "{{url_for('show_help')}}" ;
<script src="{{url_for('static',filename='simple_notes.js')}}"></script>
<script src="{{url_for('static',filename='vo.js')}}"></script>
<script src="{{url_for('static',filename='sortable.js')}}"></script>
<script src="{{url_for('static',filename='user_settings.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script>
{%include "testing.html"%}

@ -0,0 +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>

@ -0,0 +1,56 @@
""" Test the user settings. """
import json
from vasl_templates.webapp.tests.utils import \
init_webapp, find_child, _get_clipboard, \
wait_for, select_menu_option, click_dialog_button
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo
from vasl_templates.webapp.config.constants import DATA_DIR as REAL_DATA_DIR
# ---------------------------------------------------------------------
def test_include_vasl_images_in_snippets( webapp, webdriver, monkeypatch ):
"""Test the user settings."""
# initialize
monkeypatch.setitem( webapp.config, "DATA_DIR", REAL_DATA_DIR )
init_webapp( webapp, webdriver )
# add a vehicle
add_vo( webdriver, "vehicles", 1, "PzKpfw IB (Tt)" )
# enable "show VASL images in snippets"
select_menu_option( "user_settings" )
elem = find_child( ".ui-dialog.user-settings input[name='include-vasl-images-in-snippets']" )
assert not elem.is_selected()
elem.click()
click_dialog_button( "OK" )
_check_cookies( webdriver, "include-vasl-images-in-snippets", True )
# make sure that it took effect
snippet_btn = find_child( "button[data-id='ob_vehicles_1']" )
snippet_btn.click()
wait_for( 2, lambda: "/counter/2524/front" in _get_clipboard() )
# disable "show VASL images in snippets"
select_menu_option( "user_settings" )
elem = find_child( ".ui-dialog.user-settings input[name='include-vasl-images-in-snippets']" )
assert elem.is_selected()
elem.click()
click_dialog_button( "OK" )
_check_cookies( webdriver, "include-vasl-images-in-snippets", False )
# make sure that it took effect
snippet_btn.click()
wait_for( 2, lambda: "/counter/2524/front" not in _get_clipboard() )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def _check_cookies( webdriver, name, expected ):
"""Check that a user setting was stored in the cookies correctly."""
cookies = [ c for c in webdriver.get_cookies() if c["name"] == "user-settings" ]
assert len(cookies) == 1
val = cookies[0]["value"].replace( "%22", '"' )
user_settings = json.loads( val )
assert user_settings[name] == expected
Loading…
Cancel
Save