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",