diff --git a/build/Addons.xcu b/build/Addons.xcu
new file mode 100644
index 0000000..280e7c4
--- /dev/null
+++ b/build/Addons.xcu
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ AutoSave
+
+
+
+
+ Enable debug logging
+
+
+ org.libreoffice.extensions.autosave.autosavejob:ToggleDebugLogging
+
+
+ _self
+
+
+
+
+
+
+
diff --git a/build/META-INF/manifest.xml b/build/META-INF/manifest.xml
index 81cf6bf..9012644 100644
--- a/build/META-INF/manifest.xml
+++ b/build/META-INF/manifest.xml
@@ -2,4 +2,6 @@
+
+
diff --git a/build/ProtocolHandler.xcu b/build/ProtocolHandler.xcu
new file mode 100644
index 0000000..a4bdbd0
--- /dev/null
+++ b/build/ProtocolHandler.xcu
@@ -0,0 +1,10 @@
+
+
+
+
+
+ org.libreoffice.extensions.autosave.autosavejob:*
+
+
+
+
diff --git a/build/autosave.py b/build/autosave.py
index 187e68e..2169549 100644
--- a/build/autosave.py
+++ b/build/autosave.py
@@ -1,16 +1,49 @@
import os
import shutil
import traceback
+import json
from threading import Timer
from datetime import datetime
import uno
import unohelper
-from com.sun.star.util import XModifyListener
+from com.sun.star.util import XModifyListener, URL
from com.sun.star.task import XJob, XJobExecutor
+from com.sun.star.frame import XDispatchProvider, XDispatch, FeatureStateEvent
+from com.sun.star.lang import XInitialization
+from com.sun.star.awt import XCallback
+from pathlib import Path
-LOG_FILE = "C:\\Users\\jasonross\\workspace\\LibreOfficeAutoSave\\autosave_debug.log"
+LOG_FILE = Path.home() / "libreoffice_autosave_debug.log"
+CONFIG_FILE = Path.home() / "libreoffice_autosave_config.json"
+
+def is_debug_logging_enabled():
+ try:
+ if CONFIG_FILE.exists():
+ with open(CONFIG_FILE, "r") as f:
+ config = json.load(f)
+ return config.get("debug_logging", False)
+ except Exception:
+ pass
+ return False
+
+def set_debug_logging_enabled(enabled):
+ try:
+ config = {}
+ if CONFIG_FILE.exists():
+ with open(CONFIG_FILE, "r") as f:
+ try:
+ config = json.load(f)
+ except Exception:
+ pass
+ config["debug_logging"] = enabled
+ with open(CONFIG_FILE, "w") as f:
+ json.dump(config, f)
+ except Exception:
+ pass
def log_debug(msg):
+ if not is_debug_logging_enabled():
+ return
try:
with open(LOG_FILE, "a") as f:
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
@@ -19,6 +52,38 @@ def log_debug(msg):
log_debug("autosave.py module is being loaded by LibreOffice!")
+g_status_listeners = []
+g_listeners = {}
+
+def notify_all_listeners():
+ enabled = is_debug_logging_enabled()
+ url = URL()
+ url.Complete = "org.libreoffice.extensions.autosave.autosavejob:ToggleDebugLogging"
+ for listener in list(g_status_listeners):
+ try:
+ event = FeatureStateEvent()
+ event.FeatureURL = url
+ event.Source = None
+ event.IsEnabled = True
+ event.Requery = False
+ event.State = enabled
+ listener.statusChanged(event)
+ except Exception:
+ try:
+ g_status_listeners.remove(listener)
+ except ValueError:
+ pass
+
+class SafeSaveCallback(unohelper.Base, XCallback):
+ def __init__(self, listener):
+ self.listener = listener
+
+ def notify(self, data):
+ try:
+ self.listener._do_actual_save()
+ except Exception as e:
+ log_debug(f"Exception in SafeSaveCallback.notify: {e}\n{traceback.format_exc()}")
+
class AutoSaveListener(unohelper.Base, XModifyListener):
def __init__(self, doc, delay_seconds=1.0):
self.doc = doc
@@ -34,6 +99,27 @@ class AutoSaveListener(unohelper.Base, XModifyListener):
self.timer.start()
def execute_safe_save(self):
+ try:
+ log_debug("Timer expired, scheduling save on main thread via AsyncCallback...")
+ ctx = uno.getComponentContext()
+ smgr = ctx.ServiceManager
+ async_cb = smgr.createInstanceWithContext("com.sun.star.awt.AsyncCallback", ctx)
+ if async_cb:
+ callback = SafeSaveCallback(self)
+ async_cb.addCallback(callback, None)
+ log_debug("Save scheduled successfully.")
+ else:
+ log_debug("Failed to create AsyncCallback service, falling back to direct save.")
+ self._do_actual_save()
+ except Exception as e:
+ log_debug(f"Error scheduling safe save: {e}\n{traceback.format_exc()}")
+ # Fallback
+ try:
+ self._do_actual_save()
+ except Exception as e2:
+ log_debug(f"Fallback save failed: {e2}")
+
+ def _do_actual_save(self):
try:
log_debug("Timer expired, attempting save...")
if self.doc.supportsService("com.sun.star.document.OfficeDocument"):
@@ -61,30 +147,97 @@ class AutoSaveListener(unohelper.Base, XModifyListener):
def disposing(self, event):
if self.timer:
self.timer.cancel()
+
+ # Remove from g_listeners dictionary to prevent leaks
+ doc = getattr(event, "Source", self.doc)
+ try:
+ if doc in g_listeners:
+ del g_listeners[doc]
+ log_debug("Successfully removed document from g_listeners on dispose.")
+ elif self.doc in g_listeners:
+ del g_listeners[self.doc]
+ log_debug("Successfully removed self.doc from g_listeners on dispose.")
+ except Exception as e:
+ log_debug(f"Error removing document from g_listeners: {e}")
+
log_debug("Listener disposed.")
-g_listeners = {}
-
-class AutoSaveJob(unohelper.Base, XJob, XJobExecutor):
+class AutoSaveJob(unohelper.Base, XJob, XJobExecutor, XDispatchProvider, XDispatch, XInitialization):
def __init__(self, ctx=None):
self.ctx = ctx
+ self.frame = None
log_debug("AutoSaveJob instantiated!")
+ # XInitialization
+ def initialize(self, args):
+ log_debug(f"AutoSaveJob initialize() called with args: {args}")
+ if len(args) > 0:
+ self.frame = args[0]
+
+ # XJob & XJobExecutor
def execute(self, arguments):
log_debug(f"AutoSaveJob execute() called with args: {arguments}")
- self._attach_listener()
+ self._attach_listener(arguments)
return ()
def trigger(self, args):
log_debug(f"AutoSaveJob trigger() called with args: {args}")
self._attach_listener()
- def _attach_listener(self):
+ # XDispatchProvider
+ def queryDispatch(self, url, targetFrameName, searchFlags):
+ log_debug(f"queryDispatch called for URL: {url.Complete}")
+ if url.Complete.startswith("org.libreoffice.extensions.autosave.autosavejob:"):
+ return self
+ return None
+
+ def queryDispatches(self, requests):
+ return tuple(self.queryDispatch(r.URL, r.FrameName, r.SearchFlags) for r in requests)
+
+ # XDispatch
+ def dispatch(self, url, arguments):
+ log_debug(f"dispatch called for URL: {url.Complete}")
+ if url.Complete.endswith("ToggleDebugLogging"):
+ current = is_debug_logging_enabled()
+ set_debug_logging_enabled(not current)
+ log_debug(f"Toggled debug logging to: {not current}")
+ notify_all_listeners()
+
+ def addStatusListener(self, listener, url):
+ log_debug(f"addStatusListener called for URL: {url.Complete}")
+ if url.Complete.endswith("ToggleDebugLogging"):
+ if listener not in g_status_listeners:
+ g_status_listeners.append(listener)
+ try:
+ event = FeatureStateEvent()
+ event.FeatureURL = url
+ event.Source = self
+ event.IsEnabled = True
+ event.Requery = False
+ event.State = is_debug_logging_enabled()
+ listener.statusChanged(event)
+ except Exception as e:
+ log_debug(f"Error in addStatusListener initial notify: {e}")
+
+ def removeStatusListener(self, listener, url):
+ log_debug(f"removeStatusListener called for URL: {url.Complete}")
+ if listener in g_status_listeners:
+ g_status_listeners.remove(listener)
+
+ def _attach_listener(self, arguments=None):
try:
- ctx = uno.getComponentContext()
- smgr = ctx.ServiceManager
- desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
- doc = desktop.getCurrentComponent()
+ doc = None
+ if arguments:
+ for arg in arguments:
+ if hasattr(arg, "Name") and arg.Name == "Model":
+ doc = arg.Value
+ break
+
+ if not doc:
+ ctx = uno.getComponentContext()
+ smgr = ctx.ServiceManager
+ desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
+ doc = desktop.getCurrentComponent()
if doc:
if doc in g_listeners:
@@ -103,5 +256,5 @@ g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(
AutoSaveJob,
"org.libreoffice.extensions.autosave.AutoSaveJob",
- ("com.sun.star.task.Job",),
+ ("com.sun.star.task.Job", "com.sun.star.frame.ProtocolHandler"),
)
diff --git a/src/Addons.xcu b/src/Addons.xcu
new file mode 100644
index 0000000..280e7c4
--- /dev/null
+++ b/src/Addons.xcu
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ AutoSave
+
+
+
+
+ Enable debug logging
+
+
+ org.libreoffice.extensions.autosave.autosavejob:ToggleDebugLogging
+
+
+ _self
+
+
+
+
+
+
+
diff --git a/src/META-INF/manifest.xml b/src/META-INF/manifest.xml
index 81cf6bf..9012644 100644
--- a/src/META-INF/manifest.xml
+++ b/src/META-INF/manifest.xml
@@ -2,4 +2,6 @@
+
+
diff --git a/src/ProtocolHandler.xcu b/src/ProtocolHandler.xcu
new file mode 100644
index 0000000..a4bdbd0
--- /dev/null
+++ b/src/ProtocolHandler.xcu
@@ -0,0 +1,10 @@
+
+
+
+
+
+ org.libreoffice.extensions.autosave.autosavejob:*
+
+
+
+
diff --git a/src/autosave.py b/src/autosave.py
index 187e68e..2169549 100644
--- a/src/autosave.py
+++ b/src/autosave.py
@@ -1,16 +1,49 @@
import os
import shutil
import traceback
+import json
from threading import Timer
from datetime import datetime
import uno
import unohelper
-from com.sun.star.util import XModifyListener
+from com.sun.star.util import XModifyListener, URL
from com.sun.star.task import XJob, XJobExecutor
+from com.sun.star.frame import XDispatchProvider, XDispatch, FeatureStateEvent
+from com.sun.star.lang import XInitialization
+from com.sun.star.awt import XCallback
+from pathlib import Path
-LOG_FILE = "C:\\Users\\jasonross\\workspace\\LibreOfficeAutoSave\\autosave_debug.log"
+LOG_FILE = Path.home() / "libreoffice_autosave_debug.log"
+CONFIG_FILE = Path.home() / "libreoffice_autosave_config.json"
+
+def is_debug_logging_enabled():
+ try:
+ if CONFIG_FILE.exists():
+ with open(CONFIG_FILE, "r") as f:
+ config = json.load(f)
+ return config.get("debug_logging", False)
+ except Exception:
+ pass
+ return False
+
+def set_debug_logging_enabled(enabled):
+ try:
+ config = {}
+ if CONFIG_FILE.exists():
+ with open(CONFIG_FILE, "r") as f:
+ try:
+ config = json.load(f)
+ except Exception:
+ pass
+ config["debug_logging"] = enabled
+ with open(CONFIG_FILE, "w") as f:
+ json.dump(config, f)
+ except Exception:
+ pass
def log_debug(msg):
+ if not is_debug_logging_enabled():
+ return
try:
with open(LOG_FILE, "a") as f:
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
@@ -19,6 +52,38 @@ def log_debug(msg):
log_debug("autosave.py module is being loaded by LibreOffice!")
+g_status_listeners = []
+g_listeners = {}
+
+def notify_all_listeners():
+ enabled = is_debug_logging_enabled()
+ url = URL()
+ url.Complete = "org.libreoffice.extensions.autosave.autosavejob:ToggleDebugLogging"
+ for listener in list(g_status_listeners):
+ try:
+ event = FeatureStateEvent()
+ event.FeatureURL = url
+ event.Source = None
+ event.IsEnabled = True
+ event.Requery = False
+ event.State = enabled
+ listener.statusChanged(event)
+ except Exception:
+ try:
+ g_status_listeners.remove(listener)
+ except ValueError:
+ pass
+
+class SafeSaveCallback(unohelper.Base, XCallback):
+ def __init__(self, listener):
+ self.listener = listener
+
+ def notify(self, data):
+ try:
+ self.listener._do_actual_save()
+ except Exception as e:
+ log_debug(f"Exception in SafeSaveCallback.notify: {e}\n{traceback.format_exc()}")
+
class AutoSaveListener(unohelper.Base, XModifyListener):
def __init__(self, doc, delay_seconds=1.0):
self.doc = doc
@@ -34,6 +99,27 @@ class AutoSaveListener(unohelper.Base, XModifyListener):
self.timer.start()
def execute_safe_save(self):
+ try:
+ log_debug("Timer expired, scheduling save on main thread via AsyncCallback...")
+ ctx = uno.getComponentContext()
+ smgr = ctx.ServiceManager
+ async_cb = smgr.createInstanceWithContext("com.sun.star.awt.AsyncCallback", ctx)
+ if async_cb:
+ callback = SafeSaveCallback(self)
+ async_cb.addCallback(callback, None)
+ log_debug("Save scheduled successfully.")
+ else:
+ log_debug("Failed to create AsyncCallback service, falling back to direct save.")
+ self._do_actual_save()
+ except Exception as e:
+ log_debug(f"Error scheduling safe save: {e}\n{traceback.format_exc()}")
+ # Fallback
+ try:
+ self._do_actual_save()
+ except Exception as e2:
+ log_debug(f"Fallback save failed: {e2}")
+
+ def _do_actual_save(self):
try:
log_debug("Timer expired, attempting save...")
if self.doc.supportsService("com.sun.star.document.OfficeDocument"):
@@ -61,30 +147,97 @@ class AutoSaveListener(unohelper.Base, XModifyListener):
def disposing(self, event):
if self.timer:
self.timer.cancel()
+
+ # Remove from g_listeners dictionary to prevent leaks
+ doc = getattr(event, "Source", self.doc)
+ try:
+ if doc in g_listeners:
+ del g_listeners[doc]
+ log_debug("Successfully removed document from g_listeners on dispose.")
+ elif self.doc in g_listeners:
+ del g_listeners[self.doc]
+ log_debug("Successfully removed self.doc from g_listeners on dispose.")
+ except Exception as e:
+ log_debug(f"Error removing document from g_listeners: {e}")
+
log_debug("Listener disposed.")
-g_listeners = {}
-
-class AutoSaveJob(unohelper.Base, XJob, XJobExecutor):
+class AutoSaveJob(unohelper.Base, XJob, XJobExecutor, XDispatchProvider, XDispatch, XInitialization):
def __init__(self, ctx=None):
self.ctx = ctx
+ self.frame = None
log_debug("AutoSaveJob instantiated!")
+ # XInitialization
+ def initialize(self, args):
+ log_debug(f"AutoSaveJob initialize() called with args: {args}")
+ if len(args) > 0:
+ self.frame = args[0]
+
+ # XJob & XJobExecutor
def execute(self, arguments):
log_debug(f"AutoSaveJob execute() called with args: {arguments}")
- self._attach_listener()
+ self._attach_listener(arguments)
return ()
def trigger(self, args):
log_debug(f"AutoSaveJob trigger() called with args: {args}")
self._attach_listener()
- def _attach_listener(self):
+ # XDispatchProvider
+ def queryDispatch(self, url, targetFrameName, searchFlags):
+ log_debug(f"queryDispatch called for URL: {url.Complete}")
+ if url.Complete.startswith("org.libreoffice.extensions.autosave.autosavejob:"):
+ return self
+ return None
+
+ def queryDispatches(self, requests):
+ return tuple(self.queryDispatch(r.URL, r.FrameName, r.SearchFlags) for r in requests)
+
+ # XDispatch
+ def dispatch(self, url, arguments):
+ log_debug(f"dispatch called for URL: {url.Complete}")
+ if url.Complete.endswith("ToggleDebugLogging"):
+ current = is_debug_logging_enabled()
+ set_debug_logging_enabled(not current)
+ log_debug(f"Toggled debug logging to: {not current}")
+ notify_all_listeners()
+
+ def addStatusListener(self, listener, url):
+ log_debug(f"addStatusListener called for URL: {url.Complete}")
+ if url.Complete.endswith("ToggleDebugLogging"):
+ if listener not in g_status_listeners:
+ g_status_listeners.append(listener)
+ try:
+ event = FeatureStateEvent()
+ event.FeatureURL = url
+ event.Source = self
+ event.IsEnabled = True
+ event.Requery = False
+ event.State = is_debug_logging_enabled()
+ listener.statusChanged(event)
+ except Exception as e:
+ log_debug(f"Error in addStatusListener initial notify: {e}")
+
+ def removeStatusListener(self, listener, url):
+ log_debug(f"removeStatusListener called for URL: {url.Complete}")
+ if listener in g_status_listeners:
+ g_status_listeners.remove(listener)
+
+ def _attach_listener(self, arguments=None):
try:
- ctx = uno.getComponentContext()
- smgr = ctx.ServiceManager
- desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
- doc = desktop.getCurrentComponent()
+ doc = None
+ if arguments:
+ for arg in arguments:
+ if hasattr(arg, "Name") and arg.Name == "Model":
+ doc = arg.Value
+ break
+
+ if not doc:
+ ctx = uno.getComponentContext()
+ smgr = ctx.ServiceManager
+ desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
+ doc = desktop.getCurrentComponent()
if doc:
if doc in g_listeners:
@@ -103,5 +256,5 @@ g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(
AutoSaveJob,
"org.libreoffice.extensions.autosave.AutoSaveJob",
- ("com.sun.star.task.Job",),
+ ("com.sun.star.task.Job", "com.sun.star.frame.ProtocolHandler"),
)
diff --git a/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc
index 9429f11..9f5db72 100644
Binary files a/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc and b/tests/__pycache__/test_autosave.cpython-314-pytest-9.0.3.pyc differ
diff --git a/tests/test_autosave.py b/tests/test_autosave.py
index 91121ae..1c838c9 100644
--- a/tests/test_autosave.py
+++ b/tests/test_autosave.py
@@ -17,9 +17,16 @@ sys.modules['unohelper'] = mock_unohelper
class MockListener: pass
class MockXJob: pass
class MockXJobExecutor: pass
+class MockXDispatchProvider: pass
+class MockXDispatch: pass
+class MockXInitialization: pass
+class MockFeatureStateEvent: pass
+class MockURL: pass
+class MockXCallback: pass
mock_util = MagicMock()
mock_util.XModifyListener = MockListener
+mock_util.URL = MockURL
sys.modules['com.sun.star.util'] = mock_util
mock_task = MagicMock()
@@ -27,6 +34,20 @@ mock_task.XJob = MockXJob
mock_task.XJobExecutor = MockXJobExecutor
sys.modules['com.sun.star.task'] = mock_task
+mock_frame = MagicMock()
+mock_frame.XDispatchProvider = MockXDispatchProvider
+mock_frame.XDispatch = MockXDispatch
+mock_frame.FeatureStateEvent = MockFeatureStateEvent
+sys.modules['com.sun.star.frame'] = mock_frame
+
+mock_lang = MagicMock()
+mock_lang.XInitialization = MockXInitialization
+sys.modules['com.sun.star.lang'] = mock_lang
+
+mock_awt = MagicMock()
+mock_awt.XCallback = MockXCallback
+sys.modules['com.sun.star.awt'] = mock_awt
+
# Now we can safely import our module
import src.autosave as autosave
from src.autosave import AutoSaveListener, AutoSaveJob
@@ -61,6 +82,15 @@ def test_execute_safe_save_success(mock_copy2, mock_exists):
mock_doc.isReadonly.return_value = False
mock_doc.getURL.return_value = "file:///dummy/path/test.odt"
+ # Mock context and smgr for AsyncCallback
+ mock_ctx = MagicMock()
+ autosave.uno.getComponentContext.return_value = mock_ctx
+ mock_smgr = MagicMock()
+ mock_ctx.ServiceManager = mock_smgr
+ mock_async_cb = MagicMock()
+ mock_async_cb.addCallback.side_effect = lambda cb, data: cb.notify(data)
+ mock_smgr.createInstanceWithContext.return_value = mock_async_cb
+
# Mock uno.fileUrlToSystemPath to just return a string
autosave.uno.fileUrlToSystemPath.return_value = "/dummy/path/test.odt"
mock_exists.return_value = True
@@ -78,6 +108,15 @@ def test_execute_safe_save_no_location():
mock_doc.supportsService.return_value = True
mock_doc.hasLocation.return_value = False # Document not saved yet
+ # Mock context and smgr for AsyncCallback
+ mock_ctx = MagicMock()
+ autosave.uno.getComponentContext.return_value = mock_ctx
+ mock_smgr = MagicMock()
+ mock_ctx.ServiceManager = mock_smgr
+ mock_async_cb = MagicMock()
+ mock_async_cb.addCallback.side_effect = lambda cb, data: cb.notify(data)
+ mock_smgr.createInstanceWithContext.return_value = mock_async_cb
+
listener = AutoSaveListener(mock_doc)
listener.execute_safe_save()
@@ -110,3 +149,121 @@ def test_autosave_job_execution():
# Re-executing should not attach a duplicate listener
job.execute(())
assert mock_doc.addModifyListener.call_count == 1
+
+def test_autosave_job_execution_with_model():
+ job = AutoSaveJob()
+
+ # Create a mock NamedValue for the Model argument
+ mock_arg = MagicMock()
+ mock_arg.Name = "Model"
+ mock_doc = MagicMock()
+ mock_arg.Value = mock_doc
+
+ # Execute the job with the Model argument
+ job.execute((mock_arg,))
+
+ # The listener should be created and attached to the passed mock_doc
+ assert mock_doc in autosave.g_listeners
+ mock_doc.addModifyListener.assert_called_once()
+
+def test_disposing_cleanup():
+ mock_doc = MagicMock()
+ listener = AutoSaveListener(mock_doc)
+
+ # Put listener in g_listeners
+ autosave.g_listeners[mock_doc] = listener
+ assert mock_doc in autosave.g_listeners
+
+ # Trigger disposing
+ mock_event = MagicMock()
+ mock_event.Source = mock_doc
+ listener.disposing(mock_event)
+
+ # The document should be removed from g_listeners
+ assert mock_doc not in autosave.g_listeners
+
+def test_debug_logging_configuration(tmp_path):
+ # Temporarily redirect CONFIG_FILE to a temp file
+ config_file = tmp_path / "autosave_config.json"
+ with patch('src.autosave.CONFIG_FILE', config_file):
+ # Default should be False
+ assert not autosave.is_debug_logging_enabled()
+
+ # Enabling should set it to True
+ autosave.set_debug_logging_enabled(True)
+ assert autosave.is_debug_logging_enabled()
+
+ # Disabling should set it to False
+ autosave.set_debug_logging_enabled(False)
+ assert not autosave.is_debug_logging_enabled()
+
+def test_log_debug_only_when_enabled(tmp_path):
+ config_file = tmp_path / "autosave_config.json"
+ log_file = tmp_path / "autosave_debug.log"
+
+ with patch('src.autosave.CONFIG_FILE', config_file), patch('src.autosave.LOG_FILE', log_file):
+ # By default (disabled), log_debug should not write
+ autosave.log_debug("test message 1")
+ assert not log_file.exists()
+
+ # When enabled, log_debug should write
+ autosave.set_debug_logging_enabled(True)
+ autosave.log_debug("test message 2")
+ assert log_file.exists()
+ with open(log_file, "r") as f:
+ content = f.read()
+ assert "test message 2" in content
+
+def test_protocol_handler_dispatch(tmp_path):
+ config_file = tmp_path / "autosave_config.json"
+ job = AutoSaveJob()
+
+ with patch('src.autosave.CONFIG_FILE', config_file):
+ # Set to False initially
+ autosave.set_debug_logging_enabled(False)
+
+ # Query dispatch
+ mock_url = MagicMock()
+ mock_url.Complete = "org.libreoffice.extensions.autosave.autosavejob:ToggleDebugLogging"
+ dispatcher = job.queryDispatch(mock_url, "_self", 0)
+ assert dispatcher is job
+
+ # Query dispatch with invalid protocol
+ invalid_url = MagicMock()
+ invalid_url.Complete = "invalid.protocol:ToggleDebugLogging"
+ assert job.queryDispatch(invalid_url, "_self", 0) is None
+
+ # Query dispatches
+ req = MagicMock()
+ req.URL = mock_url
+ req.FrameName = "_self"
+ req.SearchFlags = 0
+ dispatchers = job.queryDispatches((req,))
+ assert dispatchers == (job,)
+
+ # Test dispatch
+ job.dispatch(mock_url, ())
+ assert autosave.is_debug_logging_enabled()
+
+ # Test status listener
+ mock_listener = MagicMock()
+ job.addStatusListener(mock_listener, mock_url)
+ assert mock_listener in autosave.g_status_listeners
+
+ # Verifying statusChanged was called with initial state (True)
+ mock_listener.statusChanged.assert_called_once()
+ event = mock_listener.statusChanged.call_args[0][0]
+ assert event.State is True
+
+ # Dispatching again should toggle to False and notify
+ mock_listener.statusChanged.reset_mock()
+ job.dispatch(mock_url, ())
+ assert not autosave.is_debug_logging_enabled()
+ mock_listener.statusChanged.assert_called_once()
+ event = mock_listener.statusChanged.call_args[0][0]
+ assert event.State is False
+
+ # Remove listener
+ job.removeStatusListener(mock_listener, mock_url)
+ assert mock_listener not in autosave.g_status_listeners
+