Repository navigation
Expand file tree
/
Copy pathmodule_config_dialog.py
More file actions
267 lines (211 loc) · 9.27 KB
/
Copy pathmodule_config_dialog.py
File metadata and controls
267 lines (211 loc) · 9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Modeless module config dialogs generated from module.yaml config_dialog specs."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import unohelper
from com.sun.star.awt import XActionListener, XTopWindowListener
if TYPE_CHECKING:
from com.sun.star.awt import ActionEvent
from com.sun.star.lang import EventObject
from plugin.chatbot.dialogs import TabListener, get_optional, translate_dialog
from plugin.framework.uno_context import get_extension_url
log = logging.getLogger(__name__)
_active_dialogs: dict[str, Any] = {}
def _tab_buttons_in_order(dlg: Any) -> list[Any]:
"""btn_tab_* controls in dialog-model order (the order the XDL generator writes)."""
from plugin.chatbot.dialogs import _dialog_model_element_names
buttons: list[Any] = []
for name in _dialog_model_element_names(dlg):
if not str(name).startswith("btn_tab_"):
continue
btn = get_optional(dlg, str(name))
if btn is not None:
buttons.append(btn)
return buttons
def get_module_config_dialog_id(module_name: str) -> str | None:
from plugin.chatbot.settings_fields import find_module_manifest
manifest = find_module_manifest(module_name)
if not manifest:
return None
cfg_dialog = manifest.get("config_dialog") or {}
dialog_id = str(cfg_dialog.get("id") or "").strip()
return dialog_id or None
def get_module_config_field_specs(ctx: Any, module_name: str) -> list[dict[str, Any]]:
"""Field specs for a standalone module config dialog (flat control ids)."""
from plugin.chatbot.settings_fields import build_module_field_specs
return build_module_field_specs(module_name, ctx=ctx, control_ids="flat")
def apply_module_config_result(ctx: Any, module_name: str, result: dict[str, Any]) -> None:
"""Persist standalone module dialog values to writeragent.json."""
from plugin.chatbot.settings_fields import apply_field_specs_result
field_specs = get_module_config_field_specs(ctx, module_name)
apply_field_specs_result(ctx, result, field_specs)
class ModuleConfigDialog:
"""Modeless settings dialog for one MODULES entry with config_dialog metadata."""
_ctx: Any
_module_name: str
_closed: bool
def __init__(self, ctx: Any, module_name: str) -> None:
self._ctx = ctx
self._module_name = module_name
self._dlg: Any | None = None
self._closed = False
self._top_listener: Any | None = None
@classmethod
def show(cls, ctx: Any, module_name: str) -> None:
existing = _active_dialogs.get(module_name)
if existing is not None:
try:
existing.close()
except Exception:
log.debug("Failed to close prior module config dialog", exc_info=True)
dialog = cls(ctx, module_name)
_active_dialogs[module_name] = dialog
dialog._open()
def close(self) -> None:
if self._closed:
return
self._closed = True
_active_dialogs.pop(self._module_name, None)
dlg = self._dlg
self._dlg = None
if dlg is None:
return
try:
dlg.setVisible(False)
except Exception:
log.exception("Failed to hide module config dialog")
try:
dlg.dispose()
except Exception:
log.exception("Failed to dispose module config dialog")
def _open(self) -> None:
ctx = self._ctx
dialog_id = get_module_config_dialog_id(self._module_name)
if not dialog_id:
log.error("No config_dialog.id for module %s", self._module_name)
return
try:
smgr = ctx.getServiceManager()
base_url = get_extension_url(ctx)
dp = smgr.createInstanceWithContext("com.sun.star.awt.DialogProvider", ctx)
dlg = dp.createDialog(base_url + "/Dialogs/%s.xdl" % dialog_id)
except Exception:
log.exception("Failed to load module config dialog %s", dialog_id)
return
self._dlg = dlg
translate_dialog(dlg)
self._setup_tabs()
self._wire_buttons()
self._populate_fields(get_module_config_field_specs(ctx, self._module_name))
owner = self
class _TopWindowListener(unohelper.Base, XTopWindowListener):
def windowClosing(self, e: EventObject) -> None:
owner.close()
def windowClosed(self, e: EventObject) -> None:
pass
def windowOpened(self, e: EventObject) -> None:
pass
def windowMinimized(self, e: EventObject) -> None:
pass
def windowNormalized(self, e: EventObject) -> None:
pass
def windowActivated(self, e: EventObject) -> None:
pass
def windowDeactivated(self, e: EventObject) -> None:
pass
def disposing(self, Source: EventObject) -> None:
pass
self._top_listener = _TopWindowListener()
dlg.addTopWindowListener(self._top_listener)
dlg.setVisible(True)
def _setup_tabs(self) -> None:
assert self._dlg is not None
# Step numbers follow control order, which is the page order the
# generator writes. A new page: in module.yaml then gets a listener
# without a second hardcoded map.
for page_num, btn in enumerate(_tab_buttons_in_order(self._dlg), start=1):
btn.addActionListener(TabListener(self._dlg, page_num))
def _wire_buttons(self) -> None:
assert self._dlg is not None
owner = self
class _ApplyListener(unohelper.Base, XActionListener):
def actionPerformed(self, rEvent: ActionEvent) -> None:
owner._apply(close=False)
def disposing(self, Source: EventObject) -> None:
pass
class _OkListener(unohelper.Base, XActionListener):
def actionPerformed(self, rEvent: ActionEvent) -> None:
owner._apply(close=True)
def disposing(self, Source: EventObject) -> None:
pass
class _CloseListener(unohelper.Base, XActionListener):
def actionPerformed(self, rEvent: ActionEvent) -> None:
owner.close()
def disposing(self, Source: EventObject) -> None:
pass
apply_btn = get_optional(self._dlg, "btn_apply")
if apply_btn:
apply_btn.addActionListener(_ApplyListener())
ok_btn = get_optional(self._dlg, "btn_ok")
if ok_btn:
ok_btn.addActionListener(_OkListener())
close_btn = get_optional(self._dlg, "btn_close")
if close_btn:
close_btn.addActionListener(_CloseListener())
def _populate_fields(self, field_specs: list[dict[str, Any]]) -> None:
from plugin.chatbot.settings_fields import populate_settings_control
assert self._dlg is not None
for field in field_specs:
ctrl = get_optional(self._dlg, field["name"])
if ctrl is None:
log.warning(
"Module config dialog %s missing control %r",
self._module_name,
field["name"],
)
continue
populate_settings_control(ctrl, field)
def _extract_result(self) -> dict[str, Any]:
from plugin.chatbot.settings_fields import read_settings_control
assert self._dlg is not None
result: dict[str, Any] = {}
for field in get_module_config_field_specs(self._ctx, self._module_name):
name = field["name"]
ctrl = get_optional(self._dlg, name)
if ctrl is None:
continue
value = read_settings_control(ctrl, field)
if value is None:
continue
# Helper returns bool. This dialog used to persist "true"/"false"
# strings (LibrePy does the same wrap). WriterAgent Settings keeps
# the bool.
if isinstance(value, bool):
result[name] = "true" if value else "false"
else:
result[name] = value
return result
def _apply(self, *, close: bool) -> None:
from plugin.chatbot.dialogs import msgbox
from plugin.framework.i18n import _
try:
result = self._extract_result()
apply_module_config_result(self._ctx, self._module_name, result)
except Exception as exc:
# What was wrong: a failed save still called close(), so OK
# dismissed the dialog after the values were not stored.
log.exception("Failed to apply module config for %s", self._module_name)
msgbox(self._ctx, _("Invalid Setting"), str(exc))
return
if close:
self.close()
def show_module_config_dialog(ctx: Any, module_name: str) -> None:
"""Open the modeless standalone config dialog for *module_name*."""
ModuleConfigDialog.show(ctx, module_name)
def show_vision_settings_dialog(ctx: Any) -> None:
"""Open Vision / OCR settings (vision module config_dialog)."""
show_module_config_dialog(ctx, "vision")