Verbesserung Tag-Auswahl im Klassifizierungsdialog

This commit is contained in:
2026-09-08 20:52:47 +02:00
parent 30faf00ba2
commit 4cd8a5622e
4 changed files with 141 additions and 32 deletions

View File

@@ -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.

View File

@@ -5,7 +5,7 @@
xmlns:xlink="http://www.w3.org/1999/xlink">
<identifier value="de.mmorath.paperless"/>
<version value="1.1.1"/>
<version value="1.2.0"/>
<display-name>
<name lang="de">Paperless-Connector</name>

View File

@@ -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()

View File

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