diff --git a/README.md b/README.md
index 01bba34..9aa7b17 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Paperless-Connector für LibreOffice
+# Paperless-ngx für LibreOffice
Erweiterung, die Dokumente direkt aus einem Paperless-ngx-Archiv öffnet,
Bearbeitungen als **neue Version desselben Dokuments** ablegt, die
@@ -241,7 +241,7 @@ selbstgebautem Multipart-Upload.
```bash
./build.sh
-unopkg add --force paperless-libreoffice-1.1.0.oxt
+unopkg add --force paperless-libreoffice-1.2.0.oxt
```
> **Zur Versionsabhängigkeit:** `OpenOffice.org-minimal-version` in
@@ -443,7 +443,7 @@ weiterreicht.
## Stand und Grenzen
-Version 1.1.0. Das Gerüst ist vollständig und die Client-Logik getestet,
+Version 1.2.0. Das Gerüst ist vollständig und die Client-Logik getestet,
aber die Erweiterung ist an einer echten Instanz noch nicht über längere
Zeit erprobt.
@@ -460,6 +460,3 @@ Der letzte Punkt ist der relevanteste für den Mehrbenutzerbetrieb.
Paperless kennt keine Sperren; wer zeitgleich arbeitet, erzeugt zwei
Versionen statt eines Konflikts. Bei einem kleinen Kreis ist das
verschmerzbar.
-
-Die Software wird ohne jegliche Gewähr oder Haftungsübernahme bereitgestellt.
-Die Nutzung erfolgt auf eigene Gefahr.
diff --git a/description.xml b/description.xml
index 66ac83d..240295f 100644
--- a/description.xml
+++ b/description.xml
@@ -5,7 +5,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink">
-
+
Paperless-Connector
diff --git a/pythonpath/paperless_dialogs.py b/pythonpath/paperless_dialogs.py
index 196fa9c..5f6d8e7 100644
--- a/pythonpath/paperless_dialogs.py
+++ b/pythonpath/paperless_dialogs.py
@@ -13,10 +13,13 @@ from com.sun.star.awt.MessageBoxButtons import BUTTONS_OK, BUTTONS_OK_CANCEL
from com.sun.star.awt.MessageBoxType import (ERRORBOX, INFOBOX, QUERYBOX,
WARNINGBOX)
+from paperless_i18n import t
+
# ---------------------------------------------------------------- Meldungen
def message(ctx, frame, text, title="Paperless", kind=INFOBOX):
+ text, title = t(text, ctx), t(title, ctx)
parent = frame.getContainerWindow() if frame else None
toolkit = ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.awt.Toolkit", ctx)
@@ -30,6 +33,7 @@ def error(ctx, frame, text, title="Paperless — Fehler"):
def confirm(ctx, frame, text, title="Paperless"):
+ text, title = t(text, ctx), t(title, ctx)
parent = frame.getContainerWindow() if frame else None
toolkit = ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.awt.Toolkit", ctx)
@@ -57,6 +61,7 @@ def choice(ctx, frame, title, text, options, width=360):
import unohelper
from com.sun.star.awt import XActionListener
+ text = t(text, ctx)
zeilen = text.count("\n") + 1
th = max(30, zeilen * 11)
h = th + 46
@@ -156,6 +161,7 @@ class DialogBuilder:
"""Dünne Hülle um UnoControlDialogModel."""
def __init__(self, ctx, title, width, height):
+ title = t(title, ctx)
self.ctx = ctx
smgr = ctx.ServiceManager
self.model = smgr.createInstanceWithContext(
@@ -183,26 +189,32 @@ class DialogBuilder:
return ctrl
def label(self, name, x, y, w, h, text):
- return self.add("FixedText", name, x, y, w, h, Label=text)
+ return self.add("FixedText", name, x, y, w, h,
+ Label=t(text, self.ctx))
def edit(self, name, x, y, w, h, text="", **kw):
return self.add("Edit", name, x, y, w, h, Text=text, **kw)
def button(self, name, x, y, w, h, label, action=0, default=False):
return self.add("Button", name, x, y, w, h,
- Label=label, PushButtonType=action,
+ Label=t(label, self.ctx), PushButtonType=action,
DefaultButton=default)
def listbox(self, name, x, y, w, h, items=(), dropdown=False):
+ # Einträge fester Auswahllisten sind Oberflächentexte. Inhalte aus
+ # Paperless werden nicht über diesen Weg gesetzt, sondern später
+ # mit addItems() - die bleiben also unangetastet.
return self.add("ListBox", name, x, y, w, h,
- StringItemList=tuple(items), Dropdown=dropdown)
+ StringItemList=tuple(t(i, self.ctx) for i in items),
+ Dropdown=dropdown)
def combo(self, name, x, y, w, h, items=(), text=""):
return self.add("ComboBox", name, x, y, w, h,
StringItemList=tuple(items), Dropdown=True, Text=text)
def check(self, name, x, y, w, h, label, state=0):
- return self.add("CheckBox", name, x, y, w, h, Label=label, State=state)
+ return self.add("CheckBox", name, x, y, w, h,
+ Label=t(label, self.ctx), State=state)
def execute(self, frame=None):
self.dialog.setModel(self.model)
@@ -240,6 +252,9 @@ def settings_dialog(ctx, frame, settings):
d.check("tls", 80, 72, 172, 12, "TLS-Zertifikat prüfen",
1 if settings.get("verify_tls", True) else 0)
+ d.label("l2c", 80, 86, 172, 20,
+ "Abschalten nur für Testinstanzen mit selbst ausgestelltem "
+ "Zertifikat. Ohne Prüfung ist die Verbindung angreifbar.")
d.label("l3", 8, 92, 70, 12, "Versionslabel")
d.edit("label", 80, 90, 172, 14, settings.get("version_label", ""))
@@ -657,9 +672,19 @@ def classify_dialog(ctx, frame, doc, tags, correspondents, doctypes):
"""
Klassifizierung bearbeiten. Liefert ein PATCH-payload-dict oder None.
- Tags werden als Mehrfachauswahl gefuehrt, Korrespondent und Dokumenttyp
- als Auswahlfeld mit Freitext - so lassen sich neue Werte anlegen.
+ Die Tagauswahl arbeitet mit einem Suchfeld und Kontrollkästchen statt
+ mit der Mehrfachauswahl der Liste. Der Grund ist praktischer Natur:
+ Bei dreissig Tags ist Strg-Klicken mühsam, und ein versehentlicher
+ Klick ohne Strg löscht die ganze bisherige Auswahl. Mit
+ Kontrollkästchen kann das nicht passieren.
+
+ Gewählte Tags stehen oben, abgetrennt vom Rest. Sie bleiben dort auch
+ dann sichtbar, wenn der Filter sie ausschliessen würde - sonst
+ verliert man beim Suchen den Überblick, was schon angehakt ist.
"""
+ import unohelper
+ from com.sun.star.awt import XItemListener, XTextListener
+
def names(rows):
return [r["name"] for r in rows]
@@ -669,7 +694,7 @@ def classify_dialog(ctx, frame, doc, tags, correspondents, doctypes):
return r["name"]
return ""
- d = DialogBuilder(ctx, "Klassifizierung bearbeiten", 300, 250)
+ d = DialogBuilder(ctx, "Klassifizierung bearbeiten", 300, 276)
d.label("l1", 8, 10, 60, 12, "Titel")
d.edit("title", 70, 8, 222, 14, doc.get("title") or "")
@@ -687,25 +712,107 @@ def classify_dialog(ctx, frame, doc, tags, correspondents, doctypes):
name_of(doctypes, doc.get("document_type")))
d.label("l5", 8, 90, 60, 12, "Tags")
- lb = d.listbox("tags", 70, 88, 222, 120)
- lb.MultiSelection = True
- d.label("l5b", 70, 210, 222, 10,
- "Mehrfachauswahl mit Strg oder Umschalt")
+ d.edit("filter", 70, 88, 222, 14, "")
+ d.label("l5c", 70, 104, 222, 10, "Tippen zum Filtern")
- d.button("ok", 178, 226, 52, 16, "Speichern", 1, True)
- d.button("cancel", 236, 226, 56, 16, "Abbrechen", 2)
+ mono(d.listbox("tags", 70, 116, 222, 108))
+ d.label("l5b", 70, 226, 222, 20,
+ "Anklicken schaltet um. Gewählte stehen oben.")
+
+ d.button("ok", 178, 252, 52, 16, "Speichern", 1, True)
+ d.button("cancel", 236, 252, 56, 16, "Abbrechen", 2)
d.dialog.setModel(d.model)
smgr = ctx.ServiceManager
toolkit = smgr.createInstanceWithContext("com.sun.star.awt.Toolkit", ctx)
d.dialog.createPeer(toolkit, frame.getContainerWindow() if frame else None)
- tag_ctrl = d.get("tags")
- tag_ctrl.addItems(tuple(names(tags)), 0)
- have = set(doc.get("tags") or [])
- for i, t in enumerate(tags):
- if t["id"] in have:
- tag_ctrl.selectItemPos(i, True)
+ lb = d.get("tags")
+ gewaehlt = set(doc.get("tags") or [])
+
+ # Zuordnung von Listenposition zu Tag. Sie ändert sich mit jedem
+ # Filterlauf, deshalb wird sie beim Zeichnen neu aufgebaut.
+ zustand = {"zeilen": [], "stumm": False}
+
+ TRENNER = None # Platzhalter für die Trennzeile
+
+ def sichtbare():
+ muster = d.get("filter").getText().strip().lower()
+ oben, unten = [], []
+ for t in tags:
+ if t["id"] in gewaehlt:
+ oben.append(t)
+ elif not muster or muster in t["name"].lower():
+ unten.append(t)
+ return oben, unten
+
+ def zeichnen():
+ """Liste neu aufbauen. Setzt stumm, damit das Umschalten nicht
+ durch die eigene Änderung erneut ausgelöst wird."""
+ zustand["stumm"] = True
+ try:
+ lb.removeItems(0, lb.getItemCount())
+ oben, unten = sichtbare()
+ zeilen, eintraege = [], []
+
+ for t in oben:
+ zeilen.append(t)
+ eintraege.append("[x] " + t["name"])
+ if oben and unten:
+ zeilen.append(TRENNER)
+ eintraege.append(" " + "-" * 30)
+ for t in unten:
+ zeilen.append(t)
+ eintraege.append("[ ] " + t["name"])
+
+ if not eintraege:
+ zeilen.append(TRENNER)
+ eintraege.append(" (keine Übereinstimmung)")
+
+ zustand["zeilen"] = zeilen
+ lb.addItems(tuple(eintraege), 0)
+ finally:
+ zustand["stumm"] = False
+
+ def umschalten():
+ if zustand["stumm"]:
+ return
+ pos = lb.getSelectedItemPos()
+ zeilen = zustand["zeilen"]
+ if not (0 <= pos < len(zeilen)):
+ return
+ t = zeilen[pos]
+ if t is TRENNER:
+ return
+ if t["id"] in gewaehlt:
+ gewaehlt.discard(t["id"])
+ else:
+ gewaehlt.add(t["id"])
+ zeichnen()
+
+ class Klick(unohelper.Base, XItemListener):
+ def itemStateChanged(self, _ev):
+ umschalten()
+
+ def disposing(self, _ev):
+ pass
+
+ class Tippen(unohelper.Base, XTextListener):
+ def textChanged(self, _ev):
+ if not zustand["stumm"]:
+ zeichnen()
+
+ def disposing(self, _ev):
+ pass
+
+ # Die Behandler in einer Liste halten. Als Wegwerfobjekte übergeben
+ # könnte der Speicherbereiniger sie einsammeln, bevor der Dialog
+ # fertig ist - die Ereignisse liefen dann ins Leere.
+ behandler = [Klick(), Tippen()]
+ lb.addItemListener(behandler[0])
+ d.get("filter").addTextListener(behandler[1])
+
+ zeichnen()
if d.dialog.execute() != 1:
d.dispose()
@@ -716,8 +823,9 @@ def classify_dialog(ctx, frame, doc, tags, correspondents, doctypes):
if created:
payload["created"] = created
- selected = d.get("tags").getSelectedItemsPos()
- payload["tags"] = [tags[i]["id"] for i in selected if i < len(tags)]
+ # Aus der Menge, nicht aus der Liste: Die Anzeige kann gefiltert sein,
+ # die Auswahl gilt aber vollständig.
+ payload["tags"] = [t["id"] for t in tags if t["id"] in gewaehlt]
payload["_correspondent_name"] = d.get("corr").getText().strip()
payload["_document_type_name"] = d.get("dtype").getText().strip()
diff --git a/pythonpath/paperless_i18n.py b/pythonpath/paperless_i18n.py
index ec7d4b9..b4ef895 100644
--- a/pythonpath/paperless_i18n.py
+++ b/pythonpath/paperless_i18n.py
@@ -179,8 +179,10 @@ EN = {
# -- Klassifizierung
"Klassifizierung bearbeiten": "Edit classification",
"Format JJJJ-MM-TT": "Format YYYY-MM-DD",
- "Mehrfachauswahl mit Strg oder Umschalt":
- "Multiple selection with Ctrl or Shift",
+ "Tippen zum Filtern": "Type to filter",
+ "Anklicken schaltet um. Gewählte stehen oben.":
+ "Click to toggle. Selected tags are listed first.",
+ " (keine Übereinstimmung)": " (no match)",
"Klassifizierung gespeichert.": "Classification saved.",
"Stammdaten konnten nicht geladen werden:\n%s":
"Master data could not be loaded:\n%s",
@@ -335,8 +337,10 @@ FR = {
"Klassifizierung bearbeiten": "Modifier le classement",
"Format JJJJ-MM-TT": "Format AAAA-MM-JJ",
- "Mehrfachauswahl mit Strg oder Umschalt":
- "Sélection multiple avec Ctrl ou Maj",
+ "Tippen zum Filtern": "Saisir pour filtrer",
+ "Anklicken schaltet um. Gewählte stehen oben.":
+ "Cliquer pour basculer. Les étiquettes choisies sont en haut.",
+ " (keine Übereinstimmung)": " (aucune correspondance)",
"Klassifizierung gespeichert.": "Classement enregistré.",
"Stammdaten konnten nicht geladen werden:\n%s":
"Les données de référence n'ont pas pu être chargées :\n%s",