diff --git a/vasl_templates/main_window.py b/vasl_templates/main_window.py index 2e6ca6c..03f8dde 100644 --- a/vasl_templates/main_window.py +++ b/vasl_templates/main_window.py @@ -297,11 +297,11 @@ class MainWindow( QWidget ): for key,val in user_settings.items(): app_settings.setValue( "UserSettings/{}".format(key), val ) - @pyqtSlot( str ) + @pyqtSlot( str, bool ) @catch_exceptions( caption="SLOT EXCEPTION" ) - def on_scenario_details_change( self, val ): - """Update the main window title to show the scenario details.""" - self._web_channel_handler.on_scenario_details_change( val ) + def on_update_scenario_status( self, caption, is_dirty ): + """Update the UI to show the scenario's status.""" + self._web_channel_handler.on_update_scenario_status( caption, is_dirty ) @pyqtSlot( str ) @catch_exceptions( caption="SLOT EXCEPTION" ) diff --git a/vasl_templates/web_channel.py b/vasl_templates/web_channel.py index 432749f..cc8c396 100644 --- a/vasl_templates/web_channel.py +++ b/vasl_templates/web_channel.py @@ -53,11 +53,14 @@ class WebChannelHandler: return None return self.scenario_file_dialog.curr_fname - def on_scenario_details_change( self, val ): + def on_update_scenario_status( self, caption, is_dirty ): """Update the main window title to show the scenario details.""" - self.parent.setWindowTitle( - "{} - {}".format( APP_NAME, val ) if val else APP_NAME - ) + title = APP_NAME + if caption: + title += " - {}".format( caption ) + if is_dirty: + title += " (*)" + self.parent.setWindowTitle( title ) def on_snippet_image( self, img_data ): #pylint: disable=no-self-use """Called when a snippet image has been generated.""" diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js index c95942f..64cb4c7 100644 --- a/vasl_templates/webapp/static/main.js +++ b/vasl_templates/webapp/static/main.js @@ -335,12 +335,12 @@ $(document).ready( function () { .button( {} ) ; // watch for changes to the scenario details - $("input[name='SCENARIO_NAME']").on( "input propertychange paste", function() { - on_scenario_details_change() ; - } ) ; - $("input[name='SCENARIO_ID']").on( "input propertychange paste", function() { - on_scenario_details_change() ; - } ) ; + $("input[name='SCENARIO_NAME']").on( "input propertychange paste", update_scenario_status ) ; + $("input[name='SCENARIO_ID']").on( "input propertychange paste", update_scenario_status ) ; + // NOTE: The following is to add/remove the "scenario modified" indicator. It's pretty inefficent + // to do this using a timer, but we would otherwise have to attach a "on change" event handler + // to every single input field, simple note, etc., which would be far more complicated and error-prone. + setInterval( update_scenario_status, 1*1000 ) ; // adjust the layout on resize $(window).resize( function() { diff --git a/vasl_templates/webapp/static/snippets.js b/vasl_templates/webapp/static/snippets.js index ee80060..b45e66b 100644 --- a/vasl_templates/webapp/static/snippets.js +++ b/vasl_templates/webapp/static/snippets.js @@ -1485,8 +1485,8 @@ function do_load_scenario_data( params ) // update the UI $("#tabs").tabs( "option", "active", 0 ) ; - on_scenario_details_change() ; on_scenario_date_change() ; + update_scenario_status() ; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1716,10 +1716,10 @@ function reset_scenario() // -------------------------------------------------------------------- -function is_scenario_dirty() +function is_scenario_dirty( force ) { // nb: confirming operations is insanely annoying during development :-/ - if ( getUrlParam( "disable-dirty-scenario-check" ) ) + if ( !force && getUrlParam( "disable-dirty-scenario-check" ) ) return false ; // check if the scenario has been changed since it was loaded, or last saved @@ -1731,7 +1731,7 @@ function is_scenario_dirty() last_saved_scenario[key] = gLastSavedScenario[key] ; } var params = unload_params_for_save( false ) ; - return (JSON.stringify(params) != JSON.stringify(last_saved_scenario) ) ; + return JSON.stringify( params ) != JSON.stringify( last_saved_scenario ) ; } // -------------------------------------------------------------------- @@ -1980,9 +1980,9 @@ function _update_vo_sortable2_entries() // -------------------------------------------------------------------- -function on_scenario_details_change() +function update_scenario_status() { - // update the document title to include the scenario details + // get the scenario details var scenario_name = $("input[name='SCENARIO_NAME']").val().trim() ; var scenario_id = $("input[name='SCENARIO_ID']").val().trim() ; var caption = "" ; @@ -1992,13 +1992,19 @@ function on_scenario_details_change() caption = scenario_name ; else if ( scenario_id ) caption = scenario_id ; - document.title = gAppName ; + + // update the window title + var title = gAppName ; if ( caption ) - document.title += " - " + caption ; + title += " - " + caption ; + var is_dirty = is_scenario_dirty( true ) ; + if ( is_dirty ) + title += " (*)" ; + document.title = title ; // notify the PyQt wrapper application if ( gWebChannelHandler ) - gWebChannelHandler.on_scenario_details_change( caption ) ; + gWebChannelHandler.on_update_scenario_status( caption, is_dirty ) ; } function on_scenario_theater_change() diff --git a/vasl_templates/webapp/tests/test_dirty_scenario_checks.py b/vasl_templates/webapp/tests/test_dirty_scenario_checks.py index e90623d..f1c0819 100644 --- a/vasl_templates/webapp/tests/test_dirty_scenario_checks.py +++ b/vasl_templates/webapp/tests/test_dirty_scenario_checks.py @@ -103,12 +103,29 @@ def test_dirty_scenario_checks( webapp, webdriver ): else: assert False + def check_is_dirty( expected ): + """Check if the scenario is being flagged as dirty.""" + if expected: + func = lambda: webdriver.title.endswith( " (*)" ) + else: + func = lambda: not webdriver.title.endswith( " (*)" ) + # NOTE: There is a race condition here if things are not working properly. Since the window title + # is updated on a timer, if we're expecting it to be (say) not modified, but the UI thinks that + # it is modified, we could check the window title here, see that the scenario is being flagged + # as not modified and continue on. The timer then fires, updates the UI to flag the scenario + # as modified, and we will have missed the error. + # To fix this, we force the scenario status to be updated. + webdriver.execute_script( "update_scenario_status()" ) + wait_for( 2, func ) + def do_test( tab_id, param ): """Test checking for a dirty scenario.""" # change the specified field + check_is_dirty( False ) select_tab( tab_id ) state = change_field( param ) + check_is_dirty( True ) # make sure we get asked to confirm a "new scenario" operation select_menu_option( "new_scenario" ) @@ -120,9 +137,11 @@ def test_dirty_scenario_checks( webapp, webdriver ): click_dialog_button( "Cancel" ) select_tab( tab_id ) check_field( param, state ) + check_is_dirty( True ) # revert the change revert_field( param, state ) + check_is_dirty( False ) # we should now be able to reset the scenario without a confirmation _ = set_stored_msg_marker( "_last-info_" ) @@ -132,6 +151,7 @@ def test_dirty_scenario_checks( webapp, webdriver ): # change the field again select_tab( tab_id ) state = change_field( param ) + check_is_dirty( True ) # make sure we get asked to confirm a "load scenario" operation select_menu_option( "load_scenario" ) @@ -143,9 +163,11 @@ def test_dirty_scenario_checks( webapp, webdriver ): click_dialog_button( "Cancel" ) select_tab( tab_id ) check_field( param, state ) + check_is_dirty( True ) # revert the change revert_field( param, state ) + check_is_dirty( False ) # we should be able to load a scenario without a confirmation # NOTE: We don't do this, since it will cause the OPEN FILE dialog to come up :-/ diff --git a/vasl_templates/webapp/tests/test_scenario_persistence.py b/vasl_templates/webapp/tests/test_scenario_persistence.py index 502b0b2..8435c91 100644 --- a/vasl_templates/webapp/tests/test_scenario_persistence.py +++ b/vasl_templates/webapp/tests/test_scenario_persistence.py @@ -110,7 +110,7 @@ def test_scenario_persistence( webapp, webdriver ): #pylint: disable=too-many-st }, } load_scenario_params( SCENARIO_PARAMS ) - check_window_title( "my test scenario (xyz123)" ) + check_window_title( "my test scenario (xyz123) (*)" ) check_ob_tabs( "russian", "german" ) assert_scenario_params_complete( SCENARIO_PARAMS, True )