Initial commit
This commit is contained in:
790
pythonpath/paperless_dialogs.py
Normal file
790
pythonpath/paperless_dialogs.py
Normal file
@@ -0,0 +1,790 @@
|
||||
"""
|
||||
paperless_dialogs.py - Dialoge, programmatisch mit UNO aufgebaut.
|
||||
|
||||
Bewusst ohne XDL-Dateien: Die muessten mitverpackt, über die
|
||||
Erweiterungs-URL geladen und je Sprache gepflegt werden. Programmatisch
|
||||
gebaute Dialoge sind ausfuehrlicher im Code, dafuer ohne Paketierungsaufwand
|
||||
und ohne Ladepfad-Fallstricke.
|
||||
"""
|
||||
|
||||
import uno
|
||||
from com.sun.star.awt import Rectangle
|
||||
from com.sun.star.awt.MessageBoxButtons import BUTTONS_OK, BUTTONS_OK_CANCEL
|
||||
from com.sun.star.awt.MessageBoxType import (ERRORBOX, INFOBOX, QUERYBOX,
|
||||
WARNINGBOX)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Meldungen
|
||||
|
||||
def message(ctx, frame, text, title="Paperless", kind=INFOBOX):
|
||||
parent = frame.getContainerWindow() if frame else None
|
||||
toolkit = ctx.ServiceManager.createInstanceWithContext(
|
||||
"com.sun.star.awt.Toolkit", ctx)
|
||||
box = toolkit.createMessageBox(parent, kind, BUTTONS_OK, title, text)
|
||||
box.execute()
|
||||
box.dispose()
|
||||
|
||||
|
||||
def error(ctx, frame, text, title="Paperless — Fehler"):
|
||||
message(ctx, frame, text, title, ERRORBOX)
|
||||
|
||||
|
||||
def confirm(ctx, frame, text, title="Paperless"):
|
||||
parent = frame.getContainerWindow() if frame else None
|
||||
toolkit = ctx.ServiceManager.createInstanceWithContext(
|
||||
"com.sun.star.awt.Toolkit", ctx)
|
||||
box = toolkit.createMessageBox(parent, QUERYBOX, BUTTONS_OK_CANCEL,
|
||||
title, text)
|
||||
result = box.execute()
|
||||
box.dispose()
|
||||
return result == 1 # 1 = OK
|
||||
|
||||
|
||||
def choice(ctx, frame, title, text, options, width=360):
|
||||
"""
|
||||
Rückfrage mit mehr als zwei Möglichkeiten.
|
||||
|
||||
Die eingebaute MessageBox kennt nur OK und Abbrechen. Und
|
||||
PushButtonType taugt hier nicht: Es kennt ausschließlich Standard, OK,
|
||||
Abbrechen und Hilfe - bei drei Schaltflächen wäre die dritte „Hilfe“
|
||||
und würde den Dialog nicht schließen.
|
||||
|
||||
Deshalb gewöhnliche Schaltflächen mit Ereignisbehandler, die das
|
||||
Ergebnis vermerken und endExecute() aufrufen.
|
||||
|
||||
Liefert den Index der gewählten Schaltfläche oder None bei Abbruch.
|
||||
"""
|
||||
import unohelper
|
||||
from com.sun.star.awt import XActionListener
|
||||
|
||||
zeilen = text.count("\n") + 1
|
||||
th = max(30, zeilen * 11)
|
||||
h = th + 46
|
||||
d = DialogBuilder(ctx, title, width, h)
|
||||
d.label("t", 10, 10, width - 20, th, text)
|
||||
|
||||
bw = max(60, min(120, (width - 20) // len(options) - 6))
|
||||
x = width - 10 - len(options) * (bw + 6) + 6
|
||||
for i, label_text in enumerate(options):
|
||||
# PushButtonType 0 = Standard: schliesst den Dialog NICHT von selbst.
|
||||
d.button("b%d" % i, x, h - 26, bw, 16, label_text, 0,
|
||||
i == len(options) - 1)
|
||||
x += bw + 6
|
||||
|
||||
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)
|
||||
|
||||
ergebnis = {"index": None}
|
||||
|
||||
class Klick(unohelper.Base, XActionListener):
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
|
||||
def actionPerformed(self, _ev):
|
||||
ergebnis["index"] = self.index
|
||||
d.dialog.endExecute()
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
# Die Behandler müssen am Leben bleiben, solange der Dialog läuft.
|
||||
behandler = []
|
||||
for i in range(len(options)):
|
||||
k = Klick(i)
|
||||
behandler.append(k)
|
||||
d.get("b%d" % i).addActionListener(k)
|
||||
|
||||
d.dialog.execute()
|
||||
d.dispose()
|
||||
return ergebnis["index"]
|
||||
|
||||
|
||||
# ------------------------------------------------------- Spaltenlayout
|
||||
|
||||
# Listenfelder verwenden standardmäßig eine Proportionalschrift. Mit
|
||||
# Leerzeichen aufgefüllte Spalten sind darin NICHT ausgerichtet - die
|
||||
# Überschrift steht dann woanders als der Inhalt. Deshalb wird für die
|
||||
# Listen und ihre Überschriften eine dicht laufende Schrift gesetzt.
|
||||
#
|
||||
# Die Angabe ist eine Ersatzkette: LibreOffice nimmt den ersten
|
||||
# verfügbaren Namen.
|
||||
MONO = "Liberation Mono;DejaVu Sans Mono;Noto Sans Mono;Courier New;monospace"
|
||||
|
||||
SEP = "│" # senkrechter Strich als dezenter Spaltentrenner
|
||||
|
||||
|
||||
def mono(ctrl):
|
||||
"""
|
||||
Dicht laufende Schrift auf ein Steuerelement legen.
|
||||
|
||||
Die Schriftgröße wird bewusst NICHT gesetzt - so erbt das Element die
|
||||
Größe des Dialogs und damit die Systemvorgabe. Ein fester Wert wäre
|
||||
auf hochauflösenden Anzeigen zu klein.
|
||||
"""
|
||||
try:
|
||||
ctrl.FontName = MONO
|
||||
except Exception:
|
||||
pass
|
||||
return ctrl
|
||||
|
||||
|
||||
def columns(values, widths):
|
||||
"""
|
||||
Werte auf feste Zeichenbreiten bringen und mit Strichen trennen.
|
||||
|
||||
Negative Breite bedeutet rechtsbündig, 0 bedeutet Rest der Zeile.
|
||||
Nur sinnvoll zusammen mit mono().
|
||||
"""
|
||||
out = []
|
||||
for value, width in zip(values, widths):
|
||||
text = "" if value is None else str(value)
|
||||
if width == 0:
|
||||
out.append(text)
|
||||
elif width < 0:
|
||||
w = -width
|
||||
out.append(text[:w].rjust(w))
|
||||
else:
|
||||
out.append(text[:width].ljust(width))
|
||||
return (" %s " % SEP).join(out)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Baukasten
|
||||
|
||||
class DialogBuilder:
|
||||
"""Dünne Hülle um UnoControlDialogModel."""
|
||||
|
||||
def __init__(self, ctx, title, width, height):
|
||||
self.ctx = ctx
|
||||
smgr = ctx.ServiceManager
|
||||
self.model = smgr.createInstanceWithContext(
|
||||
"com.sun.star.awt.UnoControlDialogModel", ctx)
|
||||
self.model.PositionX = 0
|
||||
self.model.PositionY = 0
|
||||
self.model.Width = width
|
||||
self.model.Height = height
|
||||
self.model.Title = title
|
||||
self.dialog = smgr.createInstanceWithContext(
|
||||
"com.sun.star.awt.UnoControlDialog", ctx)
|
||||
self._smgr = smgr
|
||||
|
||||
def add(self, kind, name, x, y, w, h, **props):
|
||||
ctrl = self.model.createInstance(
|
||||
"com.sun.star.awt.UnoControl%sModel" % kind)
|
||||
ctrl.PositionX, ctrl.PositionY = x, y
|
||||
ctrl.Width, ctrl.Height = w, h
|
||||
for key, value in props.items():
|
||||
try:
|
||||
setattr(ctrl, key, value)
|
||||
except Exception:
|
||||
pass
|
||||
self.model.insertByName(name, ctrl)
|
||||
return ctrl
|
||||
|
||||
def label(self, name, x, y, w, h, text):
|
||||
return self.add("FixedText", name, x, y, w, h, Label=text)
|
||||
|
||||
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,
|
||||
DefaultButton=default)
|
||||
|
||||
def listbox(self, name, x, y, w, h, items=(), dropdown=False):
|
||||
return self.add("ListBox", name, x, y, w, h,
|
||||
StringItemList=tuple(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)
|
||||
|
||||
def execute(self, frame=None):
|
||||
self.dialog.setModel(self.model)
|
||||
toolkit = self._smgr.createInstanceWithContext(
|
||||
"com.sun.star.awt.Toolkit", self.ctx)
|
||||
parent = frame.getContainerWindow() if frame else None
|
||||
self.dialog.createPeer(toolkit, parent)
|
||||
result = self.dialog.execute()
|
||||
return result
|
||||
|
||||
def get(self, name):
|
||||
return self.dialog.getControl(name)
|
||||
|
||||
def dispose(self):
|
||||
try:
|
||||
self.dialog.dispose()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Einstellungen
|
||||
|
||||
def settings_dialog(ctx, frame, settings):
|
||||
"""Verbindungsdaten. Liefert das geänderte dict oder None."""
|
||||
d = DialogBuilder(ctx, "Paperless — Verbindung", 260, 172)
|
||||
d.label("l1", 8, 10, 70, 12, "Basisadresse")
|
||||
d.edit("base", 80, 8, 172, 14, settings.get("base_url", ""))
|
||||
d.label("l1b", 80, 24, 172, 10, "z. B. https://dms.example.org")
|
||||
|
||||
d.label("l2", 8, 42, 70, 12, "API-Token")
|
||||
d.edit("token", 80, 40, 172, 14, settings.get("token", ""),
|
||||
EchoChar=ord("*"))
|
||||
d.label("l2b", 80, 56, 172, 10,
|
||||
"Im Benutzerprofil von Paperless erzeugen")
|
||||
|
||||
d.check("tls", 80, 72, 172, 12, "TLS-Zertifikat prüfen",
|
||||
1 if settings.get("verify_tls", True) else 0)
|
||||
|
||||
d.label("l3", 8, 92, 70, 12, "Versionslabel")
|
||||
d.edit("label", 80, 90, 172, 14, settings.get("version_label", ""))
|
||||
d.label("l3b", 80, 106, 172, 10, "{datum} wird ersetzt")
|
||||
|
||||
d.label("l4", 8, 112, 70, 12, "User-Agent")
|
||||
d.edit("ua", 80, 110, 172, 14, settings.get("user_agent", ""))
|
||||
d.label("l4b", 80, 126, 172, 10,
|
||||
"leer = Vorgabe. Nur ändern, wenn ein Proxy blockt.")
|
||||
|
||||
d.button("ok", 140, 146, 52, 16, "Speichern", 1, True)
|
||||
d.button("cancel", 198, 146, 52, 16, "Abbrechen", 2)
|
||||
|
||||
if d.execute(frame) != 1:
|
||||
d.dispose()
|
||||
return None
|
||||
out = dict(settings)
|
||||
out["base_url"] = d.get("base").getText().strip()
|
||||
out["token"] = d.get("token").getText().strip()
|
||||
out["verify_tls"] = bool(d.get("tls").getState())
|
||||
out["version_label"] = d.get("label").getText().strip()
|
||||
out["user_agent"] = d.get("ua").getText().strip()
|
||||
d.dispose()
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Dokumentsuche
|
||||
|
||||
def open_dialog(ctx, frame, client):
|
||||
"""
|
||||
Ein Dialog von oben nach unten: Suche, Trefferliste, Versionsliste.
|
||||
|
||||
Die Trefferliste zeigt je Dokument genau einen Eintrag - das
|
||||
Wurzeldokument. Wird darin etwas ausgewählt, füllt sich darunter die
|
||||
Versionsliste mit Datum und Dateiformat je Fassung.
|
||||
|
||||
Liefert (dokument, version, nur_lesen) oder None.
|
||||
"""
|
||||
import paperless_formats as fmt
|
||||
|
||||
W, H = 420, 322
|
||||
d = DialogBuilder(ctx, "Dokument aus Paperless öffnen", W, H)
|
||||
|
||||
# Spaltenbreiten in Zeichen. Negativ = rechtsbündig, 0 = Rest der Zeile.
|
||||
COL_RES = (-7, 10, 0) # ASN, Erstellt, Titel
|
||||
COL_VER = (-3, 10, 14, 0) # Nr., Erstellt, Format, Bezeichnung
|
||||
|
||||
# Listenfelder rücken ihren Text um wenige Einheiten ein. Die
|
||||
# Überschrift bekommt denselben Versatz, sonst stehen die Spalten
|
||||
# gegeneinander verschoben.
|
||||
HX = 11
|
||||
|
||||
# 1. Suche
|
||||
d.label("l1", 8, 10, 40, 12, "Suche")
|
||||
d.edit("q", 50, 8, W - 122, 14, "")
|
||||
d.button("go", W - 66, 8, 58, 14, "Suchen", 0, True)
|
||||
d.label("hint1", 8, 26, W - 16, 10,
|
||||
"Leer lassen für die zuletzt hinzugefügten Dokumente. "
|
||||
"Archiv-Seriennummer funktioniert ebenfalls.")
|
||||
|
||||
# 2. Treffer - Bezeichnung, darunter Spaltenüberschrift, darunter Liste
|
||||
d.label("l2", 8, 42, 200, 11, "Dokumente")
|
||||
mono(d.label("head1", HX, 55, W - HX - 8, 11,
|
||||
columns(("ASN", "Erstellt", "Titel"), COL_RES)))
|
||||
mono(d.listbox("results", 8, 68, W - 16, 104))
|
||||
|
||||
# 3. Versionen
|
||||
d.label("l3", 8, 180, 200, 11, "Versionen")
|
||||
mono(d.label("head2", HX, 193, W - HX - 8, 11,
|
||||
" " + columns(("Nr", "Erstellt", "Format", "Bezeichnung"),
|
||||
COL_VER)))
|
||||
mono(d.listbox("versions", 8, 206, W - 16, 66))
|
||||
|
||||
d.label("hint2", 8, 278, W - 16, 20, "")
|
||||
d.label("l4", 8, 301, 46, 11, "Öffnen als")
|
||||
d.listbox("mode", 58, 299, 130, 14,
|
||||
["Bearbeitbar", "Schreibgeschützt"], dropdown=True)
|
||||
d.button("ok", W - 122, 298, 54, 16, "Öffnen", 1)
|
||||
d.button("cancel", W - 62, 298, 54, 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)
|
||||
|
||||
lb_res = d.get("results")
|
||||
lb_ver = d.get("versions")
|
||||
state = {"rows": [], "versions": [], "doc": None, "cache": {}, "last": -1}
|
||||
|
||||
# ---------------------------------------------------------- Trefferliste
|
||||
|
||||
def refresh(query):
|
||||
try:
|
||||
rows = client.search(query)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
d.get("hint2").setText("Fehler bei der Suche: %s" % str(exc)[:70])
|
||||
return
|
||||
state["rows"] = rows
|
||||
state["last"] = -1
|
||||
lb_res.removeItems(0, lb_res.getItemCount())
|
||||
lb_ver.removeItems(0, lb_ver.getItemCount())
|
||||
entries = []
|
||||
for r in rows:
|
||||
asn = r.get("archive_serial_number")
|
||||
entries.append(columns((
|
||||
str(asn) if asn is not None else "—",
|
||||
(r.get("created") or "")[:10],
|
||||
(r.get("title") or "")[:90]), COL_RES))
|
||||
if entries:
|
||||
lb_res.addItems(tuple(entries), 0)
|
||||
lb_res.selectItemPos(0, True)
|
||||
load_versions()
|
||||
else:
|
||||
d.get("hint2").setText("Keine Treffer.")
|
||||
|
||||
# --------------------------------------------------------- Versionsliste
|
||||
|
||||
def load_versions():
|
||||
"""
|
||||
Versionen des gewählten Dokuments nachladen.
|
||||
|
||||
Die Formatangabe steht nicht in der eingebetteten versions-Liste,
|
||||
deshalb je Version ein Abruf. Ergebnisse werden zwischengespeichert,
|
||||
damit das Blättern in der Trefferliste nicht bei jedem Tastendruck
|
||||
erneut abfragt.
|
||||
"""
|
||||
pos = lb_res.getSelectedItemPos()
|
||||
if pos == state["last"] or not (0 <= pos < len(state["rows"])):
|
||||
return
|
||||
state["last"] = pos
|
||||
row = state["rows"][pos]
|
||||
doc_id = row["id"]
|
||||
|
||||
lb_ver.removeItems(0, lb_ver.getItemCount())
|
||||
d.get("hint2").setText("Versionen werden geladen …")
|
||||
|
||||
if doc_id in state["cache"]:
|
||||
full, versions = state["cache"][doc_id]
|
||||
else:
|
||||
try:
|
||||
full = client.document(doc_id)
|
||||
versions = client.version_details(full)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
d.get("hint2").setText("Fehler: %s" % str(exc)[:70])
|
||||
return
|
||||
state["cache"][doc_id] = (full, versions)
|
||||
|
||||
state["doc"], state["versions"] = full, versions
|
||||
entries = []
|
||||
for v in versions:
|
||||
editable = fmt.is_editable(v.get("filename"), v.get("mime_type"))
|
||||
entries.append((" " if editable else "• ") + columns((
|
||||
v.get("no", "?"),
|
||||
(v.get("added") or "")[:10],
|
||||
fmt.label(v.get("filename"), v.get("mime_type")),
|
||||
(v.get("version_label") or "")[:40]), COL_VER))
|
||||
if entries:
|
||||
lb_ver.addItems(tuple(entries), 0)
|
||||
# Vorgabe: die aktuellste Version. Die Liste kommt absteigend,
|
||||
# also Position 0.
|
||||
lb_ver.selectItemPos(0, True)
|
||||
update_hint()
|
||||
|
||||
def update_hint():
|
||||
"""
|
||||
Hinweis und Vorbelegung der Öffnungsart.
|
||||
|
||||
Die Vorbelegung ist ein Vorschlag, keine Vorschrift - die
|
||||
Formaterkennung kann danebenliegen, und dann soll der Anwender
|
||||
trotzdem bearbeitbar öffnen können.
|
||||
"""
|
||||
pos = lb_ver.getSelectedItemPos()
|
||||
vers = state["versions"]
|
||||
if not (0 <= pos < len(vers)):
|
||||
d.get("hint2").setText("")
|
||||
return
|
||||
v = vers[pos]
|
||||
fn, mt = v.get("filename"), v.get("mime_type")
|
||||
if fmt.is_editable(fn, mt):
|
||||
d.get("mode").selectItemPos(0, True)
|
||||
d.get("hint2").setText(
|
||||
"Bearbeitbar. Änderungen können als neue Version abgelegt "
|
||||
"werden.")
|
||||
elif not fmt.is_known(fn, mt):
|
||||
# Unbekanntes Format: kein Grund, das Bearbeiten zu verbieten.
|
||||
d.get("mode").selectItemPos(0, True)
|
||||
d.get("hint2").setText(
|
||||
"Format nicht erkannt: %s\n"
|
||||
"Bearbeitbar öffnen ist möglich — beim Zurückschreiben "
|
||||
"wird ODF verwendet." % fmt.detail(fn, mt))
|
||||
else:
|
||||
d.get("mode").selectItemPos(1, True)
|
||||
d.get("hint2").setText(
|
||||
"%s ist in LibreOffice nicht sinnvoll bearbeitbar.\n"
|
||||
"Schreibgeschützt empfohlen — bearbeitbar ist trotzdem "
|
||||
"wählbar." % fmt.label(fn, mt))
|
||||
|
||||
# ------------------------------------------------------------- Ereignisse
|
||||
|
||||
import unohelper
|
||||
from com.sun.star.awt import XActionListener, XItemListener
|
||||
|
||||
class GoListener(unohelper.Base, XActionListener):
|
||||
def actionPerformed(self, _ev):
|
||||
refresh(d.get("q").getText().strip())
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
class ResultListener(unohelper.Base, XItemListener):
|
||||
def itemStateChanged(self, _ev):
|
||||
load_versions()
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
class VersionListener(unohelper.Base, XItemListener):
|
||||
def itemStateChanged(self, _ev):
|
||||
update_hint()
|
||||
|
||||
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 = [GoListener(), ResultListener(), VersionListener()]
|
||||
d.get("go").addActionListener(behandler[0])
|
||||
lb_res.addItemListener(behandler[1])
|
||||
lb_ver.addItemListener(behandler[2])
|
||||
|
||||
refresh("")
|
||||
|
||||
if d.dialog.execute() != 1:
|
||||
d.dispose()
|
||||
return None
|
||||
|
||||
vpos = lb_ver.getSelectedItemPos()
|
||||
# Die Wahl des Anwenders wird NICHT überschrieben. Wer ein als nicht
|
||||
# bearbeitbar eingestuftes Format trotzdem bearbeitbar öffnen will,
|
||||
# darf das - die Erkennung kann danebenliegen.
|
||||
readonly = d.get("mode").getSelectedItemPos() == 1
|
||||
doc, versions = state["doc"], state["versions"]
|
||||
d.dispose()
|
||||
|
||||
if doc is None or not (0 <= vpos < len(versions)):
|
||||
return None
|
||||
return doc, versions[vpos], readonly
|
||||
|
||||
|
||||
# --------------------------------------------------- Ablegeziel wählen
|
||||
|
||||
def store_target_dialog(ctx, frame, client, source):
|
||||
"""
|
||||
Wohin soll die geöffnete Datei abgelegt werden?
|
||||
|
||||
Drei Fälle, die sich in der Absicht unterscheiden:
|
||||
|
||||
* ein NEUES Dokument mit eigener Versionskette
|
||||
* eine neue Version an einem ANDEREN, bereits vorhandenen Dokument
|
||||
|
||||
Der zweite Fall ist der, den Dokumentenverwaltungen gern vergessen:
|
||||
Die Rechnung kommt als PDF, die bearbeitbare Fassung entsteht später.
|
||||
Beides gehört zusammen, aber das PDF war zuerst da.
|
||||
|
||||
Liefert ein dict oder None.
|
||||
"""
|
||||
import paperless_formats as fmt
|
||||
|
||||
W, H = 430, 300
|
||||
COL = (-7, 10, 0)
|
||||
HX = 11
|
||||
title = (source.get("title") or "")
|
||||
|
||||
d = DialogBuilder(ctx, "In Paperless ablegen", W, H)
|
||||
|
||||
d.label("l0", 8, 10, 34, 12, "Ziel")
|
||||
d.listbox("mode", 46, 8, 250, 14,
|
||||
["Neues Dokument anlegen",
|
||||
"An bestehendes Dokument anhängen"], dropdown=True)
|
||||
|
||||
# --- Fall 1: neues Dokument
|
||||
d.label("n1", 8, 36, 46, 12, "Titel")
|
||||
d.edit("title", 58, 34, W - 66, 14, (title + " (Kopie)")[:120])
|
||||
d.check("meta", 58, 54, W - 66, 12,
|
||||
"Klassifizierung des geöffneten Dokuments übernehmen", 1)
|
||||
|
||||
# --- Fall 2: an bestehendes Dokument anhängen
|
||||
d.label("e1", 8, 78, 46, 12, "Suche")
|
||||
d.edit("q", 58, 76, W - 130, 14, "")
|
||||
d.button("go", W - 66, 76, 58, 14, "Suchen", 0)
|
||||
mono(d.label("head", HX, 96, W - HX - 8, 11,
|
||||
columns(("ASN", "Erstellt", "Titel"), COL)))
|
||||
mono(d.listbox("results", 8, 109, W - 16, 96))
|
||||
d.label("e2", 8, 209, W - 16, 20, "")
|
||||
|
||||
d.check("switch", 8, 236, W - 16, 12,
|
||||
"Danach mit dem Zieldokument weiterarbeiten", 1)
|
||||
d.label("l9", 8, 250, W - 16, 20,
|
||||
"Das geöffnete Fenster wird dem Ziel zugeordnet. Weitere "
|
||||
"Versionen landen dann dort.")
|
||||
|
||||
d.button("ok", W - 122, 276, 54, 16, "Ablegen", 1, True)
|
||||
d.button("cancel", W - 62, 276, 54, 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)
|
||||
|
||||
lb_res = d.get("results")
|
||||
state = {"rows": []}
|
||||
|
||||
def set_mode():
|
||||
neu_ = d.get("mode").getSelectedItemPos() == 0
|
||||
for name in ("n1", "title", "meta"):
|
||||
d.get(name).setEnable(neu_)
|
||||
for name in ("e1", "q", "go", "head", "results"):
|
||||
d.get(name).setEnable(not neu_)
|
||||
if neu_:
|
||||
d.get("e2").setText("")
|
||||
else:
|
||||
update_target_hint()
|
||||
|
||||
def refresh(query):
|
||||
try:
|
||||
rows = client.search(query)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
d.get("e2").setText("Fehler bei der Suche: %s" % str(exc)[:70])
|
||||
return
|
||||
state["rows"] = rows
|
||||
lb_res.removeItems(0, lb_res.getItemCount())
|
||||
entries = []
|
||||
for r in rows:
|
||||
asn = r.get("archive_serial_number")
|
||||
entries.append(columns((
|
||||
str(asn) if asn is not None else "—",
|
||||
(r.get("created") or "")[:10],
|
||||
(r.get("title") or "")[:90]), COL))
|
||||
if entries:
|
||||
lb_res.addItems(tuple(entries), 0)
|
||||
lb_res.selectItemPos(0, True)
|
||||
update_target_hint()
|
||||
|
||||
def update_target_hint():
|
||||
pos = lb_res.getSelectedItemPos()
|
||||
rows = state["rows"]
|
||||
if not (0 <= pos < len(rows)):
|
||||
d.get("e2").setText("Kein Zieldokument gewählt.")
|
||||
return
|
||||
r = rows[pos]
|
||||
d.get("e2").setText(
|
||||
"Die geöffnete Datei wird als neue Version an „%s“ angehängt.\n"
|
||||
"Die bisherige Fassung bleibt als Version erhalten."
|
||||
% (r.get("title") or "")[:60])
|
||||
|
||||
import unohelper
|
||||
from com.sun.star.awt import XActionListener, XItemListener
|
||||
|
||||
class Go(unohelper.Base, XActionListener):
|
||||
def actionPerformed(self, _ev):
|
||||
refresh(d.get("q").getText().strip())
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
class ModeChanged(unohelper.Base, XItemListener):
|
||||
def itemStateChanged(self, _ev):
|
||||
set_mode()
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
class ResultChanged(unohelper.Base, XItemListener):
|
||||
def itemStateChanged(self, _ev):
|
||||
update_target_hint()
|
||||
|
||||
def disposing(self, _ev):
|
||||
pass
|
||||
|
||||
behandler = [Go(), ModeChanged(), ResultChanged()]
|
||||
d.get("go").addActionListener(behandler[0])
|
||||
d.get("mode").addItemListener(behandler[1])
|
||||
lb_res.addItemListener(behandler[2])
|
||||
|
||||
d.get("mode").selectItemPos(0, True)
|
||||
set_mode()
|
||||
|
||||
if d.dialog.execute() != 1:
|
||||
d.dispose()
|
||||
return None
|
||||
|
||||
neu_ = d.get("mode").getSelectedItemPos() == 0
|
||||
out = {"switch": bool(d.get("switch").getState())}
|
||||
if neu_:
|
||||
out["mode"] = "new"
|
||||
out["title"] = d.get("title").getText().strip()
|
||||
out["take_meta"] = bool(d.get("meta").getState())
|
||||
d.dispose()
|
||||
return out if out["title"] else None
|
||||
|
||||
pos = lb_res.getSelectedItemPos()
|
||||
rows = state["rows"]
|
||||
d.dispose()
|
||||
if not (0 <= pos < len(rows)):
|
||||
return None
|
||||
out["mode"] = "existing"
|
||||
out["target"] = rows[pos]
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------- Klassifizierung
|
||||
|
||||
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.
|
||||
"""
|
||||
def names(rows):
|
||||
return [r["name"] for r in rows]
|
||||
|
||||
def name_of(rows, ident):
|
||||
for r in rows:
|
||||
if r["id"] == ident:
|
||||
return r["name"]
|
||||
return ""
|
||||
|
||||
d = DialogBuilder(ctx, "Klassifizierung bearbeiten", 300, 250)
|
||||
|
||||
d.label("l1", 8, 10, 60, 12, "Titel")
|
||||
d.edit("title", 70, 8, 222, 14, doc.get("title") or "")
|
||||
|
||||
d.label("l2", 8, 30, 60, 12, "Erstellt")
|
||||
d.edit("created", 70, 28, 90, 14, (doc.get("created") or "")[:10])
|
||||
d.label("l2b", 164, 30, 128, 10, "Format JJJJ-MM-TT")
|
||||
|
||||
d.label("l3", 8, 50, 60, 12, "Korrespondent")
|
||||
d.combo("corr", 70, 48, 222, 14, names(correspondents),
|
||||
name_of(correspondents, doc.get("correspondent")))
|
||||
|
||||
d.label("l4", 8, 70, 60, 12, "Dokumenttyp")
|
||||
d.combo("dtype", 70, 68, 222, 14, names(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.button("ok", 178, 226, 52, 16, "Speichern", 1, True)
|
||||
d.button("cancel", 236, 226, 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)
|
||||
|
||||
if d.dialog.execute() != 1:
|
||||
d.dispose()
|
||||
return None
|
||||
|
||||
payload = {"title": d.get("title").getText().strip()}
|
||||
created = d.get("created").getText().strip()
|
||||
if created:
|
||||
payload["created"] = created
|
||||
|
||||
selected = d.get("tags").getSelectedItemsPos()
|
||||
payload["tags"] = [tags[i]["id"] for i in selected if i < len(tags)]
|
||||
|
||||
payload["_correspondent_name"] = d.get("corr").getText().strip()
|
||||
payload["_document_type_name"] = d.get("dtype").getText().strip()
|
||||
d.dispose()
|
||||
return payload
|
||||
|
||||
|
||||
# -------------------------------------------------------------- Freigabe
|
||||
|
||||
def share_dialog(ctx, frame, default_days=7, default_tag="Sharelink",
|
||||
known_tags=()):
|
||||
"""
|
||||
Laufzeit, Dateivariante und optionaler Tag.
|
||||
|
||||
Der Tag macht geteilte Dokumente später auffindbar - Paperless zeigt
|
||||
zwar die eigenen Freigaben an, aber ein Tag lässt sich in gespeicherten
|
||||
Ansichten und Regeln verwenden.
|
||||
|
||||
Liefert (tage, variante, tagname_oder_None) oder None.
|
||||
"""
|
||||
d = DialogBuilder(ctx, "Dokument teilen", 300, 168)
|
||||
|
||||
d.label("l1", 8, 12, 90, 12, "Gültigkeit in Tagen")
|
||||
d.edit("days", 104, 10, 50, 14, str(default_days))
|
||||
d.label("l1b", 104, 26, 188, 10, "0 = unbegrenzt gültig")
|
||||
|
||||
d.label("l2", 8, 44, 90, 12, "Dateivariante")
|
||||
d.listbox("ver", 104, 42, 188, 14,
|
||||
["Original", "Archivfassung (PDF)"], dropdown=True)
|
||||
|
||||
d.check("tag", 8, 70, 284, 12,
|
||||
"Dokument mit einem Tag kennzeichnen", 1)
|
||||
d.combo("tagname", 104, 86, 188, 14, known_tags, default_tag)
|
||||
d.label("l3b", 104, 102, 188, 20,
|
||||
"Macht geteilte Dokumente auffindbar. Ist der Tag noch nicht "
|
||||
"vorhanden, wird er nach Rückfrage angelegt.")
|
||||
|
||||
d.button("ok", 178, 144, 54, 16, "Erzeugen", 1, True)
|
||||
d.button("cancel", 236, 144, 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)
|
||||
d.get("ver").selectItemPos(0, True)
|
||||
|
||||
if d.dialog.execute() != 1:
|
||||
d.dispose()
|
||||
return None
|
||||
try:
|
||||
days = int(d.get("days").getText().strip() or "0")
|
||||
except ValueError:
|
||||
days = default_days
|
||||
variant = "original" if d.get("ver").getSelectedItemPos() == 0 else "archive"
|
||||
tag = d.get("tagname").getText().strip() if d.get("tag").getState() else None
|
||||
d.dispose()
|
||||
return days, variant, (tag or None)
|
||||
|
||||
|
||||
def result_dialog(ctx, frame, title, text, url=None):
|
||||
"""Ergebnis anzeigen, Link zum Herauskopieren in einem Textfeld."""
|
||||
height = 120 if url else 90
|
||||
d = DialogBuilder(ctx, title, 320, height)
|
||||
d.label("l1", 8, 10, 304, 24, text)
|
||||
if url:
|
||||
d.label("l2", 8, 42, 304, 10, "Link (zum Kopieren markieren):")
|
||||
d.edit("url", 8, 54, 304, 14, url, ReadOnly=True)
|
||||
d.button("ok", 258, height - 24, 54, 16, "Schließen", 1, True)
|
||||
d.execute(frame)
|
||||
d.dispose()
|
||||
Reference in New Issue
Block a user