791 lines
28 KiB
Python
791 lines
28 KiB
Python
"""
|
|
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()
|