built auto save extension for LibreOffice
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
.antigravitycli/
|
||||
.pytest_cache/
|
||||
dist/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="Jobs" oor:package="org.openoffice.Office">
|
||||
<node oor:name="Jobs">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace">
|
||||
<prop oor:name="Service">
|
||||
<value>org.libreoffice.extensions.autosave.AutoSaveJob</value>
|
||||
</prop>
|
||||
</node>
|
||||
</node>
|
||||
<node oor:name="Events">
|
||||
<node oor:name="OnLoad" oor:op="fuse">
|
||||
<node oor:name="JobList">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace"/>
|
||||
</node>
|
||||
</node>
|
||||
<node oor:name="OnNew" oor:op="fuse">
|
||||
<node oor:name="JobList">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</oor:component-data>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest xmlns:manifest="http://openoffice.org/2001/manifest">
|
||||
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python" manifest:full-path="autosave.py"/>
|
||||
<manifest:file-entry manifest:media-type="application/vnd.sun.star.configuration-data" manifest:full-path="Jobs.xcu"/>
|
||||
</manifest:manifest>
|
||||
Binary file not shown.
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
from threading import Timer
|
||||
from datetime import datetime
|
||||
import uno
|
||||
import unohelper
|
||||
from com.sun.star.util import XModifyListener
|
||||
from com.sun.star.task import XJob, XJobExecutor
|
||||
|
||||
LOG_FILE = "C:\\Users\\jasonross\\workspace\\LibreOfficeAutoSave\\autosave_debug.log"
|
||||
|
||||
def log_debug(msg):
|
||||
try:
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log_debug("autosave.py module is being loaded by LibreOffice!")
|
||||
|
||||
class AutoSaveListener(unohelper.Base, XModifyListener):
|
||||
def __init__(self, doc, delay_seconds=1.0):
|
||||
self.doc = doc
|
||||
self.delay = delay_seconds
|
||||
self.timer = None
|
||||
log_debug(f"AutoSaveListener initialized for document: {doc}")
|
||||
|
||||
def modified(self, event):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
self.timer = Timer(self.delay, self.execute_safe_save)
|
||||
self.timer.start()
|
||||
|
||||
def execute_safe_save(self):
|
||||
try:
|
||||
log_debug("Timer expired, attempting save...")
|
||||
if self.doc.supportsService("com.sun.star.document.OfficeDocument"):
|
||||
if self.doc.hasLocation() and not self.doc.isReadonly():
|
||||
file_url = self.doc.getURL()
|
||||
log_debug(f"File has location: {file_url}")
|
||||
|
||||
if file_url.startswith("file:///"):
|
||||
system_path = uno.fileUrlToSystemPath(file_url)
|
||||
bak_path = os.path.join(os.path.dirname(system_path), f".{os.path.basename(system_path)}.bak")
|
||||
try:
|
||||
if os.path.exists(system_path):
|
||||
shutil.copy2(system_path, bak_path)
|
||||
log_debug(f"Created backup at {bak_path}")
|
||||
except Exception as e:
|
||||
log_debug(f"Backup failed: {e}")
|
||||
|
||||
self.doc.store()
|
||||
log_debug("Document successfully saved.")
|
||||
else:
|
||||
log_debug("Document is readonly or has no location (not saved yet).")
|
||||
except Exception as e:
|
||||
log_debug(f"Auto-save exception: {e}\n{traceback.format_exc()}")
|
||||
|
||||
def disposing(self, event):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
log_debug("Listener disposed.")
|
||||
|
||||
g_listeners = {}
|
||||
|
||||
class AutoSaveJob(unohelper.Base, XJob, XJobExecutor):
|
||||
def __init__(self, ctx=None):
|
||||
self.ctx = ctx
|
||||
log_debug("AutoSaveJob instantiated!")
|
||||
|
||||
def execute(self, arguments):
|
||||
log_debug(f"AutoSaveJob execute() called with args: {arguments}")
|
||||
self._attach_listener()
|
||||
return ()
|
||||
|
||||
def trigger(self, args):
|
||||
log_debug(f"AutoSaveJob trigger() called with args: {args}")
|
||||
self._attach_listener()
|
||||
|
||||
def _attach_listener(self):
|
||||
try:
|
||||
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:
|
||||
log_debug("Listener already attached to this document.")
|
||||
return
|
||||
listener = AutoSaveListener(doc, delay_seconds=1.0)
|
||||
doc.addModifyListener(listener)
|
||||
g_listeners[doc] = listener
|
||||
log_debug("Successfully attached listener to current document.")
|
||||
else:
|
||||
log_debug("No active document found.")
|
||||
except Exception as e:
|
||||
log_debug(f"Error in AutoSaveJob attach: {e}\n{traceback.format_exc()}")
|
||||
|
||||
g_ImplementationHelper = unohelper.ImplementationHelper()
|
||||
g_ImplementationHelper.addImplementation(
|
||||
AutoSaveJob,
|
||||
"org.libreoffice.extensions.autosave.AutoSaveJob",
|
||||
("com.sun.star.task.Job",),
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:d="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<identifier value="org.libreoffice.extensions.autosave"/>
|
||||
<version value="1.0.0"/>
|
||||
<display-name>
|
||||
<name lang="en-US">AutoSave</name>
|
||||
</display-name>
|
||||
</description>
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
|
||||
def build_oxt():
|
||||
build_dir = "build"
|
||||
dist_dir = "dist"
|
||||
oxt_name = "AutoSave.oxt"
|
||||
|
||||
# Clean previous builds
|
||||
if os.path.exists(build_dir):
|
||||
shutil.rmtree(build_dir)
|
||||
if not os.path.exists(dist_dir):
|
||||
os.makedirs(dist_dir)
|
||||
|
||||
# Copy source files to build directory
|
||||
shutil.copytree("src", build_dir)
|
||||
|
||||
# Create the .oxt file (which is a zip archive)
|
||||
oxt_path = os.path.join(dist_dir, oxt_name)
|
||||
with zipfile.ZipFile(oxt_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, _, files in os.walk(build_dir):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
# The archive root should be the contents of the 'build' dir
|
||||
arcname = os.path.relpath(file_path, build_dir).replace("\\", "/")
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
print(f"Successfully built extension: {oxt_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_oxt()
|
||||
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$LO_PATH = "C:\Program Files\LibreOffice\program"
|
||||
$UNOPKG = Join-Path $LO_PATH "unopkg.com"
|
||||
$SOFFICE = Join-Path $LO_PATH "soffice.exe"
|
||||
$EXT_ID = "org.libreoffice.extensions.autosave"
|
||||
|
||||
Write-Host "1. Building the extension..." -ForegroundColor Cyan
|
||||
python build_oxt.py
|
||||
|
||||
Write-Host "2. Killing open LibreOffice instances..." -ForegroundColor Cyan
|
||||
# Kill both the launcher and the main binary, silently ignoring if they aren't running
|
||||
Stop-Process -Name "soffice" -Force -ErrorAction SilentlyContinue
|
||||
Stop-Process -Name "soffice.bin" -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Give the OS a moment to fully release file locks on the UNO packages
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Write-Host "3. Uninstalling existing extension..." -ForegroundColor Cyan
|
||||
# We catch and ignore errors here because 'unopkg remove' will throw an error if the extension isn't currently installed
|
||||
try {
|
||||
& $UNOPKG remove $EXT_ID *>&1 | Out-Null
|
||||
} catch {
|
||||
# Ignore "package not found" errors
|
||||
}
|
||||
|
||||
Write-Host "4. Installing the new extension..." -ForegroundColor Cyan
|
||||
& $UNOPKG add -f dist\AutoSave.oxt
|
||||
|
||||
Write-Host "5. Starting LibreOffice Writer..." -ForegroundColor Cyan
|
||||
Start-Process -FilePath $SOFFICE -ArgumentList "--writer"
|
||||
|
||||
Write-Host "Deployment complete! Writer is launching in the background." -ForegroundColor Green
|
||||
Generated
+273
@@ -0,0 +1,273 @@
|
||||
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "26.5.1"
|
||||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893"},
|
||||
{file = "black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90"},
|
||||
{file = "black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4"},
|
||||
{file = "black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef"},
|
||||
{file = "black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22"},
|
||||
{file = "black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c"},
|
||||
{file = "black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7"},
|
||||
{file = "black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59"},
|
||||
{file = "black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3"},
|
||||
{file = "black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe"},
|
||||
{file = "black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8"},
|
||||
{file = "black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217"},
|
||||
{file = "black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d"},
|
||||
{file = "black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264"},
|
||||
{file = "black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418"},
|
||||
{file = "black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3"},
|
||||
{file = "black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0"},
|
||||
{file = "black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294"},
|
||||
{file = "black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a"},
|
||||
{file = "black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52"},
|
||||
{file = "black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168"},
|
||||
{file = "black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3"},
|
||||
{file = "black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18"},
|
||||
{file = "black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50"},
|
||||
{file = "black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae"},
|
||||
{file = "black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2"},
|
||||
{file = "black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=1.0.0"
|
||||
platformdirs = ">=2"
|
||||
pytokens = ">=0.4.0,<0.5.0"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.10)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.1"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2"},
|
||||
{file = "click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "8.0.1"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.10.0"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"},
|
||||
{file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
|
||||
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.1.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"},
|
||||
{file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
hyperscan = ["hyperscan (>=0.7)"]
|
||||
optional = ["typing-extensions (>=4)"]
|
||||
re2 = ["google-re2 (>=1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.10.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"},
|
||||
{file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
||||
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["coverage", "pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
||||
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
|
||||
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = ">=1.0.1"
|
||||
packaging = ">=22"
|
||||
pluggy = ">=1.5,<2"
|
||||
pygments = ">=2.7.2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytokens"
|
||||
version = "0.4.1"
|
||||
description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"},
|
||||
{file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"},
|
||||
{file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"},
|
||||
{file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"},
|
||||
{file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"},
|
||||
{file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"},
|
||||
{file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"},
|
||||
{file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"},
|
||||
{file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"},
|
||||
{file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"},
|
||||
{file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"},
|
||||
{file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"},
|
||||
{file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"},
|
||||
{file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"},
|
||||
{file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"},
|
||||
{file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"},
|
||||
{file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"},
|
||||
{file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"},
|
||||
{file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"},
|
||||
{file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"},
|
||||
{file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"},
|
||||
{file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"},
|
||||
{file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"},
|
||||
{file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"},
|
||||
{file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"},
|
||||
{file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"},
|
||||
{file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"},
|
||||
{file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"},
|
||||
{file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"},
|
||||
{file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"},
|
||||
{file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"},
|
||||
{file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"},
|
||||
{file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.14"
|
||||
content-hash = "677487ac55f2b53800a1f496f6b30a79dcec9f89aae9c147f61e95914fd22dd9"
|
||||
@@ -0,0 +1,22 @@
|
||||
[project]
|
||||
name = "libreoffice-autosave"
|
||||
version = "0.1.0"
|
||||
description = "LibreOffice AutoSave Extension"
|
||||
authors = [
|
||||
{name = "Jason Ross",email = "jason.ross841@gmail.com"}
|
||||
]
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest (>=9.0.3,<10.0.0)",
|
||||
"black (>=26.5.1,<27.0.0)",
|
||||
"isort (>=8.0.1,<9.0.0)"
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="Jobs" oor:package="org.openoffice.Office">
|
||||
<node oor:name="Jobs">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace">
|
||||
<prop oor:name="Service">
|
||||
<value>org.libreoffice.extensions.autosave.AutoSaveJob</value>
|
||||
</prop>
|
||||
</node>
|
||||
</node>
|
||||
<node oor:name="Events">
|
||||
<node oor:name="OnLoad" oor:op="fuse">
|
||||
<node oor:name="JobList">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace"/>
|
||||
</node>
|
||||
</node>
|
||||
<node oor:name="OnNew" oor:op="fuse">
|
||||
<node oor:name="JobList">
|
||||
<node oor:name="org.libreoffice.extensions.autosave.AutoSaveJob" oor:op="replace"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</oor:component-data>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest xmlns:manifest="http://openoffice.org/2001/manifest">
|
||||
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python" manifest:full-path="autosave.py"/>
|
||||
<manifest:file-entry manifest:media-type="application/vnd.sun.star.configuration-data" manifest:full-path="Jobs.xcu"/>
|
||||
</manifest:manifest>
|
||||
Binary file not shown.
+107
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
from threading import Timer
|
||||
from datetime import datetime
|
||||
import uno
|
||||
import unohelper
|
||||
from com.sun.star.util import XModifyListener
|
||||
from com.sun.star.task import XJob, XJobExecutor
|
||||
|
||||
LOG_FILE = "C:\\Users\\jasonross\\workspace\\LibreOfficeAutoSave\\autosave_debug.log"
|
||||
|
||||
def log_debug(msg):
|
||||
try:
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(f"[{datetime.now().isoformat()}] {msg}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
log_debug("autosave.py module is being loaded by LibreOffice!")
|
||||
|
||||
class AutoSaveListener(unohelper.Base, XModifyListener):
|
||||
def __init__(self, doc, delay_seconds=1.0):
|
||||
self.doc = doc
|
||||
self.delay = delay_seconds
|
||||
self.timer = None
|
||||
log_debug(f"AutoSaveListener initialized for document: {doc}")
|
||||
|
||||
def modified(self, event):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
self.timer = Timer(self.delay, self.execute_safe_save)
|
||||
self.timer.start()
|
||||
|
||||
def execute_safe_save(self):
|
||||
try:
|
||||
log_debug("Timer expired, attempting save...")
|
||||
if self.doc.supportsService("com.sun.star.document.OfficeDocument"):
|
||||
if self.doc.hasLocation() and not self.doc.isReadonly():
|
||||
file_url = self.doc.getURL()
|
||||
log_debug(f"File has location: {file_url}")
|
||||
|
||||
if file_url.startswith("file:///"):
|
||||
system_path = uno.fileUrlToSystemPath(file_url)
|
||||
bak_path = os.path.join(os.path.dirname(system_path), f".{os.path.basename(system_path)}.bak")
|
||||
try:
|
||||
if os.path.exists(system_path):
|
||||
shutil.copy2(system_path, bak_path)
|
||||
log_debug(f"Created backup at {bak_path}")
|
||||
except Exception as e:
|
||||
log_debug(f"Backup failed: {e}")
|
||||
|
||||
self.doc.store()
|
||||
log_debug("Document successfully saved.")
|
||||
else:
|
||||
log_debug("Document is readonly or has no location (not saved yet).")
|
||||
except Exception as e:
|
||||
log_debug(f"Auto-save exception: {e}\n{traceback.format_exc()}")
|
||||
|
||||
def disposing(self, event):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
log_debug("Listener disposed.")
|
||||
|
||||
g_listeners = {}
|
||||
|
||||
class AutoSaveJob(unohelper.Base, XJob, XJobExecutor):
|
||||
def __init__(self, ctx=None):
|
||||
self.ctx = ctx
|
||||
log_debug("AutoSaveJob instantiated!")
|
||||
|
||||
def execute(self, arguments):
|
||||
log_debug(f"AutoSaveJob execute() called with args: {arguments}")
|
||||
self._attach_listener()
|
||||
return ()
|
||||
|
||||
def trigger(self, args):
|
||||
log_debug(f"AutoSaveJob trigger() called with args: {args}")
|
||||
self._attach_listener()
|
||||
|
||||
def _attach_listener(self):
|
||||
try:
|
||||
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:
|
||||
log_debug("Listener already attached to this document.")
|
||||
return
|
||||
listener = AutoSaveListener(doc, delay_seconds=1.0)
|
||||
doc.addModifyListener(listener)
|
||||
g_listeners[doc] = listener
|
||||
log_debug("Successfully attached listener to current document.")
|
||||
else:
|
||||
log_debug("No active document found.")
|
||||
except Exception as e:
|
||||
log_debug(f"Error in AutoSaveJob attach: {e}\n{traceback.format_exc()}")
|
||||
|
||||
g_ImplementationHelper = unohelper.ImplementationHelper()
|
||||
g_ImplementationHelper.addImplementation(
|
||||
AutoSaveJob,
|
||||
"org.libreoffice.extensions.autosave.AutoSaveJob",
|
||||
("com.sun.star.task.Job",),
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<description xmlns="http://openoffice.org/extensions/description/2006" xmlns:d="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<identifier value="org.libreoffice.extensions.autosave"/>
|
||||
<version value="1.0.0"/>
|
||||
<display-name>
|
||||
<name lang="en-US">AutoSave</name>
|
||||
</display-name>
|
||||
</description>
|
||||
Binary file not shown.
@@ -0,0 +1,112 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Add the project root to sys.path so we can import src
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Mock the uno and unohelper modules before importing our module
|
||||
sys.modules['uno'] = MagicMock()
|
||||
|
||||
class MockBase: pass
|
||||
mock_unohelper = MagicMock()
|
||||
mock_unohelper.Base = MockBase
|
||||
sys.modules['unohelper'] = mock_unohelper
|
||||
|
||||
class MockListener: pass
|
||||
class MockXJob: pass
|
||||
class MockXJobExecutor: pass
|
||||
|
||||
mock_util = MagicMock()
|
||||
mock_util.XModifyListener = MockListener
|
||||
sys.modules['com.sun.star.util'] = mock_util
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.XJob = MockXJob
|
||||
mock_task.XJobExecutor = MockXJobExecutor
|
||||
sys.modules['com.sun.star.task'] = mock_task
|
||||
|
||||
# Now we can safely import our module
|
||||
import src.autosave as autosave
|
||||
from src.autosave import AutoSaveListener, AutoSaveJob
|
||||
|
||||
def setup_function():
|
||||
# Clear the global listeners dictionary before each test
|
||||
autosave.g_listeners.clear()
|
||||
|
||||
def test_debounce_timer():
|
||||
mock_doc = MagicMock()
|
||||
listener = AutoSaveListener(mock_doc, delay_seconds=0.1)
|
||||
|
||||
# Simulate multiple rapid modifications
|
||||
listener.modified(None)
|
||||
first_timer = listener.timer
|
||||
|
||||
listener.modified(None)
|
||||
second_timer = listener.timer
|
||||
|
||||
# The first timer should be cancelled, the second should be active
|
||||
assert first_timer is not second_timer
|
||||
|
||||
# Ensure execute_safe_save has not executed synchronously
|
||||
mock_doc.store.assert_not_called()
|
||||
|
||||
@patch('src.autosave.os.path.exists')
|
||||
@patch('src.autosave.shutil.copy2')
|
||||
def test_execute_safe_save_success(mock_copy2, mock_exists):
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.supportsService.return_value = True
|
||||
mock_doc.hasLocation.return_value = True
|
||||
mock_doc.isReadonly.return_value = False
|
||||
mock_doc.getURL.return_value = "file:///dummy/path/test.odt"
|
||||
|
||||
# Mock uno.fileUrlToSystemPath to just return a string
|
||||
autosave.uno.fileUrlToSystemPath.return_value = "/dummy/path/test.odt"
|
||||
mock_exists.return_value = True
|
||||
|
||||
listener = AutoSaveListener(mock_doc, delay_seconds=0.1)
|
||||
listener.execute_safe_save()
|
||||
|
||||
# Assert copy2 was called to create the backup
|
||||
mock_copy2.assert_called_once()
|
||||
# Assert store() was called to save the document
|
||||
mock_doc.store.assert_called_once()
|
||||
|
||||
def test_execute_safe_save_no_location():
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.supportsService.return_value = True
|
||||
mock_doc.hasLocation.return_value = False # Document not saved yet
|
||||
|
||||
listener = AutoSaveListener(mock_doc)
|
||||
listener.execute_safe_save()
|
||||
|
||||
# store() should NOT be called
|
||||
mock_doc.store.assert_not_called()
|
||||
|
||||
def test_autosave_job_execution():
|
||||
job = AutoSaveJob()
|
||||
|
||||
# Mock context and document
|
||||
mock_ctx = MagicMock()
|
||||
autosave.uno.getComponentContext.return_value = mock_ctx
|
||||
|
||||
mock_smgr = MagicMock()
|
||||
mock_ctx.ServiceManager = mock_smgr
|
||||
|
||||
mock_desktop = MagicMock()
|
||||
mock_smgr.createInstanceWithContext.return_value = mock_desktop
|
||||
|
||||
mock_doc = MagicMock()
|
||||
mock_desktop.getCurrentComponent.return_value = mock_doc
|
||||
|
||||
# Execute the job
|
||||
job.execute(())
|
||||
|
||||
# The listener should be created and attached
|
||||
assert mock_doc in autosave.g_listeners
|
||||
mock_doc.addModifyListener.assert_called_once()
|
||||
|
||||
# Re-executing should not attach a duplicate listener
|
||||
job.execute(())
|
||||
assert mock_doc.addModifyListener.call_count == 1
|
||||
Reference in New Issue
Block a user