From 5c6a16d5dacb902ca2228471df33e867ba1ba966 Mon Sep 17 00:00:00 2001 From: ChelSlava Date: Mon, 30 Mar 2026 10:18:03 +0300 Subject: [PATCH 1/3] Add periodic auto-save feature Closes #1376 --- src/robotide/preferences/saving.py | 4 ++++ src/robotide/preferences/settings.cfg | 4 ++-- src/robotide/ui/mainframe.py | 32 +++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/robotide/preferences/saving.py b/src/robotide/preferences/saving.py index 58cc8a9a4..25c38361d 100644 --- a/src/robotide/preferences/saving.py +++ b/src/robotide/preferences/saving.py @@ -68,6 +68,10 @@ def _create_editors(settings): IntegerChoiceEditor(settings, 'txt number of spaces', _('Separating spaces'), [str(i) for i in range(2, 11)], _('Number of spaces between cells when saving in txt format') + ), + IntegerChoiceEditor(settings, 'auto save interval', _('Auto save interval (minutes)'), + ['0', '1', '2', '3', '5', '10', '15', '20', '30'], + _('Automatically save all files after specified minutes (0 = disabled)') ) ] diff --git a/src/robotide/preferences/settings.cfg b/src/robotide/preferences/settings.cfg index 3bb97fa6d..0b5cbeb12 100644 --- a/src/robotide/preferences/settings.cfg +++ b/src/robotide/preferences/settings.cfg @@ -28,6 +28,8 @@ reformat = False doc language = None # the style of the tabs in notebook pages, Edit, Text, Run. Values from 0 to 5. notebook theme = 0 +# auto save interval: Automatically save all files after specified minutes (0 = disabled) +auto save interval = 0 [General] font size = 11 @@ -71,8 +73,6 @@ tc_kw_name = '#1A5FB4' variable = '#008080' background = '#F6F5F4' enable auto suggestions = True -enable visible spaces = True -enable visible newlines = True [Grid] font size = 10 diff --git a/src/robotide/ui/mainframe.py b/src/robotide/ui/mainframe.py index fca0d2070..edaaec702 100644 --- a/src/robotide/ui/mainframe.py +++ b/src/robotide/ui/mainframe.py @@ -167,7 +167,9 @@ def __init__(self, application, controller): self.color_foreground = self.general_settings.get('foreground', '#5E5C64') if self.general_settings else '#5E5C64' self.font_face = self.general_settings.get('font face', '') if self.general_settings else '' self.font_size = self.general_settings.get('font size', 11) if self.general_settings else 11 - self.ui_language = self.general_settings.get('ui language', 'English') if self.general_settings else 'English' + self.ui_language = self.general_settings.get('ui language', 'English') if self.general_settings else 'English' + self._auto_save_interval = application.settings.get('auto save interval', 0) + self._auto_save_timer = None self.main_menu = None self._init_ui() self.SetIcon(wx.Icon(self._image_provider.RIDE_ICON)) @@ -187,6 +189,7 @@ def __init__(self, application, controller): self.Bind(aui.EVT_AUI_PANE_DOCKING, self.OnFloatDock) self.Bind(aui.EVT_AUI_PANE_DOCKED, self.OnFloatDock) self._subscribe_messages() + self._start_auto_save_timer() wx.CallAfter(self.actions.register_tools) # DEBUG # DEBUG wx.CallAfter(self.OnSettingsChanged, self.general_settings) @@ -197,7 +200,8 @@ def _subscribe_messages(self): (self._set_label, RideTreeSelection), (self._show_validation_error, RideInputValidationError), (self._show_modification_prevented_error, RideModificationPrevented), - (self.on_ui_language_changed, RideSettingsChanged) + (self.on_ui_language_changed, RideSettingsChanged), + (self._on_auto_save_settings_changed, RideSettingsChanged) ]: PUBLISHER.subscribe(listener, topic) @@ -364,6 +368,30 @@ def get_selected_datafile(self): def get_selected_datafile_controller(self): return self.tree.get_selected_datafile_controller() + def _start_auto_save_timer(self): + """Start the auto-save timer if interval is set.""" + if self._auto_save_timer: + self._auto_save_timer.Stop() + self._auto_save_timer = None + if self._auto_save_interval > 0: + self._auto_save_timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self._on_auto_save, self._auto_save_timer) + self._auto_save_timer.Start(self._auto_save_interval * 60 * 1000) # minutes to milliseconds + + def _on_auto_save(self, event): + """Auto-save all files when timer fires.""" + _ = event + if self.controller and self.controller.is_dirty(): + RideBeforeSaving().publish() + self.save_all() + self.SetStatusText(_('Auto-saved all files')) + + def _on_auto_save_settings_changed(self, message): + """Update auto-save timer when settings change.""" + if message.keys and 'auto save interval' in message.keys: + self._auto_save_interval = self._application.settings.get('auto save interval', 0) + self._start_auto_save_timer() + def on_close(self, event): from ..preferences import RideSettings if self._allowed_to_exit(): From b4736c16a77e0af64997b1d02287339fcfcd9148 Mon Sep 17 00:00:00 2001 From: ChelSlava Date: Wed, 8 Apr 2026 08:02:43 +0300 Subject: [PATCH 2/3] Rename event variable for clarity in auto-save --- src/robotide/ui/mainframe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotide/ui/mainframe.py b/src/robotide/ui/mainframe.py index edaaec702..c9e410b20 100644 --- a/src/robotide/ui/mainframe.py +++ b/src/robotide/ui/mainframe.py @@ -380,7 +380,7 @@ def _start_auto_save_timer(self): def _on_auto_save(self, event): """Auto-save all files when timer fires.""" - _ = event + __ = event if self.controller and self.controller.is_dirty(): RideBeforeSaving().publish() self.save_all() From e44f0fd5abefeb3b800812783789b5ff96cb6a7a Mon Sep 17 00:00:00 2001 From: Vyacheslav Date: Sun, 12 Apr 2026 03:41:04 +0300 Subject: [PATCH 3/3] Prevent stale auto-save scheduling after periodic saves The PR review for #3031 requested that the periodic auto-save timer be reset after a successful auto-save cycle. This change follows the reviewer suggestion by scheduling _start_auto_save_timer with wx.CallAfter, while preserving the double-underscore event placeholder so translations remain intact. A focused regression test now verifies that the restart is scheduled after save completion. Constraint: Keep the fix scoped to the open PR #3031 review feedback Rejected: Restart the timer inline inside the EVT_TIMER callback | reviewer proposed deferred restart via wx.CallAfter Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep __ = event in _on_auto_save so the _() translation helper is not shadowed Tested: Targeted pytest for test_on_auto_save_restarts_timer_after_save; py_compile on touched files Not-tested: Full GUI-heavy utest/ui/test_mainframe.py module in this environment (wx access violation outside the targeted test) --- src/robotide/ui/mainframe.py | 3 ++- utest/ui/test_mainframe.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/robotide/ui/mainframe.py b/src/robotide/ui/mainframe.py index c9e410b20..c348668e9 100644 --- a/src/robotide/ui/mainframe.py +++ b/src/robotide/ui/mainframe.py @@ -380,11 +380,12 @@ def _start_auto_save_timer(self): def _on_auto_save(self, event): """Auto-save all files when timer fires.""" - __ = event + __ = event if self.controller and self.controller.is_dirty(): RideBeforeSaving().publish() self.save_all() self.SetStatusText(_('Auto-saved all files')) + wx.CallAfter(self._start_auto_save_timer) def _on_auto_save_settings_changed(self, message): """Update auto-save timer when settings change.""" diff --git a/utest/ui/test_mainframe.py b/utest/ui/test_mainframe.py index 4d71a6ef4..c80eb4bad 100644 --- a/utest/ui/test_mainframe.py +++ b/utest/ui/test_mainframe.py @@ -171,6 +171,44 @@ def my_call(*argument, **options): for _ in range(16): # Hoping to cover all 4 cases start_external_app(__file__) + def test_on_auto_save_restarts_timer_after_save(self): + calls = [] + + class DirtyController: + @staticmethod + def is_dirty(): + return True + + class FakeBeforeSaving: + def publish(self): + calls.append("publish") + + def restart_timer(): + calls.append("restart") + + def fake_call_after(callback, *args, **kwargs): + calls.append(("callafter", callback, args, kwargs)) + + self.frame.controller = DirtyController() + with MonkeyPatch().context() as m: + m.setattr(mainframe, 'RideBeforeSaving', FakeBeforeSaving) + m.setattr(wx, 'CallAfter', fake_call_after) + m.setattr(self.frame, 'save_all', lambda: calls.append("save_all")) + m.setattr(self.frame, 'SetStatusText', lambda text: calls.append(("status", text))) + m.setattr(self.frame, '_start_auto_save_timer', restart_timer) + + self.frame._on_auto_save(object()) + + self.assertEqual( + calls, + [ + "publish", + "save_all", + ("status", "Auto-saved all files"), + ("callafter", restart_timer, (), {}), + ], + ) + if __name__ == '__main__': unittest.main()