pyaccesskit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyaccesskit/AGENT_GUIDE.md +455 -0
- pyaccesskit/__init__.py +167 -0
- pyaccesskit/__main__.py +6 -0
- pyaccesskit/_backends/__init__.py +0 -0
- pyaccesskit/_backends/access/__init__.py +1 -0
- pyaccesskit/_backends/access/design.py +415 -0
- pyaccesskit/_backends/dao/__init__.py +1 -0
- pyaccesskit/_backends/dao/profile.py +40 -0
- pyaccesskit/_backends/dao/schema.py +805 -0
- pyaccesskit/_backends/dao/typemap.py +390 -0
- pyaccesskit/_backends/fake/__init__.py +3 -0
- pyaccesskit/_backends/fake/backend.py +680 -0
- pyaccesskit/_backends/protocols.py +339 -0
- pyaccesskit/_com/__init__.py +1 -0
- pyaccesskit/_com/constants.py +394 -0
- pyaccesskit/_com/dispatch.py +50 -0
- pyaccesskit/_com/errors.py +184 -0
- pyaccesskit/_com/gateway.py +199 -0
- pyaccesskit/_com/raw.py +164 -0
- pyaccesskit/_com/runtime.py +39 -0
- pyaccesskit/_com/variants.py +72 -0
- pyaccesskit/_engines/__init__.py +48 -0
- pyaccesskit/_engines/access.py +300 -0
- pyaccesskit/_engines/inproc.py +148 -0
- pyaccesskit/_engines/probe.py +231 -0
- pyaccesskit/_ledger.py +158 -0
- pyaccesskit/_ops/__init__.py +0 -0
- pyaccesskit/_ops/design.py +127 -0
- pyaccesskit/_ops/schema.py +471 -0
- pyaccesskit/_session/__init__.py +1 -0
- pyaccesskit/_session/protocols.py +78 -0
- pyaccesskit/_session/session.py +354 -0
- pyaccesskit/_text/__init__.py +0 -0
- pyaccesskit/_text/codec.py +114 -0
- pyaccesskit/_version.py +3 -0
- pyaccesskit/_win/__init__.py +1 -0
- pyaccesskit/_win/access_process.py +348 -0
- pyaccesskit/_win/console.py +56 -0
- pyaccesskit/_win/inspector.py +53 -0
- pyaccesskit/_win/job.py +65 -0
- pyaccesskit/_win/processes.py +159 -0
- pyaccesskit/_win/watchdog.py +253 -0
- pyaccesskit/cli/__init__.py +10 -0
- pyaccesskit/cli/_output.py +101 -0
- pyaccesskit/cli/agent.py +99 -0
- pyaccesskit/cli/app.py +54 -0
- pyaccesskit/cli/cleanup.py +56 -0
- pyaccesskit/cli/doctor.py +101 -0
- pyaccesskit/cli/inspection.py +223 -0
- pyaccesskit/database.py +296 -0
- pyaccesskit/diagnostics.py +319 -0
- pyaccesskit/enums.py +258 -0
- pyaccesskit/errors.py +407 -0
- pyaccesskit/forms/__init__.py +45 -0
- pyaccesskit/forms/builder.py +295 -0
- pyaccesskit/forms/collection.py +117 -0
- pyaccesskit/forms/controls.py +157 -0
- pyaccesskit/forms/layout.py +300 -0
- pyaccesskit/forms/spec.py +169 -0
- pyaccesskit/forms/vba.py +138 -0
- pyaccesskit/maintenance.py +32 -0
- pyaccesskit/modules.py +101 -0
- pyaccesskit/objects.py +81 -0
- pyaccesskit/options.py +40 -0
- pyaccesskit/properties.py +74 -0
- pyaccesskit/py.typed +0 -0
- pyaccesskit/queries.py +190 -0
- pyaccesskit/relationships.py +143 -0
- pyaccesskit/schema/__init__.py +73 -0
- pyaccesskit/schema/_base.py +55 -0
- pyaccesskit/schema/_reserved_words.py +55 -0
- pyaccesskit/schema/columns.py +609 -0
- pyaccesskit/schema/compat.py +57 -0
- pyaccesskit/schema/expressions.py +162 -0
- pyaccesskit/schema/indexes.py +114 -0
- pyaccesskit/schema/names.py +122 -0
- pyaccesskit/schema/queries.py +192 -0
- pyaccesskit/schema/relationships.py +132 -0
- pyaccesskit/schema/tables.py +178 -0
- pyaccesskit/tables.py +333 -0
- pyaccesskit/units.py +301 -0
- pyaccesskit-0.1.0.dist-info/METADATA +201 -0
- pyaccesskit-0.1.0.dist-info/RECORD +86 -0
- pyaccesskit-0.1.0.dist-info/WHEEL +4 -0
- pyaccesskit-0.1.0.dist-info/entry_points.txt +2 -0
- pyaccesskit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
"""``Access.Application`` implementation of :class:`~pyaccesskit._backends.protocols.DesignBackend`.
|
|
3
|
+
|
|
4
|
+
Form building follows the flow verified in spike S6:
|
|
5
|
+
|
|
6
|
+
``CreateForm`` (an unsaved, auto-named form in Design view) → header/footer via
|
|
7
|
+
``RunCommand(acCmdFormHdrFtr)`` when needed → properties and section heights (``Section`` is an *indexed*
|
|
8
|
+
property, so it goes through the exact-argument gateway) → ``CreateControl`` for every control and attached
|
|
9
|
+
label → event properties + form module → ``DoCmd.Close(acSaveYes)`` → atomic swap with ``DoCmd.Rename``.
|
|
10
|
+
|
|
11
|
+
If anything fails before the save, the unsaved form is closed with ``acSaveNo`` and nothing is left behind.
|
|
12
|
+
When replacing, the old form is renamed to a hidden ``~pak_bak_…`` name first and restored on failure.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import tempfile
|
|
19
|
+
import uuid
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import pywintypes
|
|
25
|
+
|
|
26
|
+
from pyaccesskit._backends.protocols import ControlInfo
|
|
27
|
+
from pyaccesskit._com import constants as c
|
|
28
|
+
from pyaccesskit._com.gateway import Com, call, get, put
|
|
29
|
+
from pyaccesskit.enums import ControlKind, FormView, ObjectKind, RowSourceType, ScrollBars, Section
|
|
30
|
+
from pyaccesskit.errors import SpecError
|
|
31
|
+
from pyaccesskit.forms.controls import (
|
|
32
|
+
ButtonSpec,
|
|
33
|
+
CheckBoxSpec,
|
|
34
|
+
ComboBoxSpec,
|
|
35
|
+
LabelSpec,
|
|
36
|
+
TextBoxSpec,
|
|
37
|
+
)
|
|
38
|
+
from pyaccesskit.forms.layout import ResolvedForm
|
|
39
|
+
from pyaccesskit.units import Length
|
|
40
|
+
|
|
41
|
+
__all__ = ["AccessDesignBackend"]
|
|
42
|
+
|
|
43
|
+
AC_CMD_FORM_HDR_FTR = 36
|
|
44
|
+
_AC_TYPES = {
|
|
45
|
+
ObjectKind.FORM: c.AcObjectType.acForm,
|
|
46
|
+
ObjectKind.REPORT: c.AcObjectType.acReport,
|
|
47
|
+
ObjectKind.MACRO: c.AcObjectType.acMacro,
|
|
48
|
+
ObjectKind.MODULE: c.AcObjectType.acModule,
|
|
49
|
+
ObjectKind.QUERY: c.AcObjectType.acQuery,
|
|
50
|
+
}
|
|
51
|
+
_ALL_COLLECTIONS = {
|
|
52
|
+
ObjectKind.FORM: ("CurrentProject", "AllForms"),
|
|
53
|
+
ObjectKind.REPORT: ("CurrentProject", "AllReports"),
|
|
54
|
+
ObjectKind.MACRO: ("CurrentProject", "AllMacros"),
|
|
55
|
+
ObjectKind.MODULE: ("CurrentProject", "AllModules"),
|
|
56
|
+
ObjectKind.QUERY: ("CurrentData", "AllQueries"),
|
|
57
|
+
}
|
|
58
|
+
_CONTROL_TYPES = {
|
|
59
|
+
ControlKind.LABEL: c.AcControlType.acLabel,
|
|
60
|
+
ControlKind.TEXTBOX: c.AcControlType.acTextBox,
|
|
61
|
+
ControlKind.CHECKBOX: c.AcControlType.acCheckBox,
|
|
62
|
+
ControlKind.COMBOBOX: c.AcControlType.acComboBox,
|
|
63
|
+
ControlKind.BUTTON: c.AcControlType.acCommandButton,
|
|
64
|
+
}
|
|
65
|
+
_CONTROL_KINDS = {
|
|
66
|
+
int(c.AcControlType.acLabel): ControlKind.LABEL,
|
|
67
|
+
int(c.AcControlType.acTextBox): ControlKind.TEXTBOX,
|
|
68
|
+
int(c.AcControlType.acCheckBox): ControlKind.CHECKBOX,
|
|
69
|
+
int(c.AcControlType.acComboBox): ControlKind.COMBOBOX,
|
|
70
|
+
int(c.AcControlType.acListBox): ControlKind.LISTBOX,
|
|
71
|
+
int(c.AcControlType.acCommandButton): ControlKind.BUTTON,
|
|
72
|
+
int(c.AcControlType.acOptionGroup): ControlKind.OPTION_GROUP,
|
|
73
|
+
int(c.AcControlType.acOptionButton): ControlKind.OPTION_BUTTON,
|
|
74
|
+
int(c.AcControlType.acToggleButton): ControlKind.TOGGLE_BUTTON,
|
|
75
|
+
int(c.AcControlType.acSubform): ControlKind.SUBFORM,
|
|
76
|
+
int(c.AcControlType.acImage): ControlKind.IMAGE,
|
|
77
|
+
int(c.AcControlType.acLine): ControlKind.LINE,
|
|
78
|
+
int(c.AcControlType.acRectangle): ControlKind.RECTANGLE,
|
|
79
|
+
int(c.AcControlType.acTabCtl): ControlKind.TAB_CONTROL,
|
|
80
|
+
int(c.AcControlType.acPage): ControlKind.PAGE,
|
|
81
|
+
int(c.AcControlType.acAttachment): ControlKind.ATTACHMENT,
|
|
82
|
+
}
|
|
83
|
+
_SECTIONS = {Section.DETAIL: 0, Section.HEADER: 1, Section.FOOTER: 2}
|
|
84
|
+
_SECTION_OF = {value: key for key, value in _SECTIONS.items()}
|
|
85
|
+
_DEFAULT_VIEWS = {
|
|
86
|
+
FormView.SINGLE: 0,
|
|
87
|
+
FormView.CONTINUOUS: 1,
|
|
88
|
+
FormView.DATASHEET: 2,
|
|
89
|
+
FormView.SPLIT: 5,
|
|
90
|
+
}
|
|
91
|
+
_SCROLL_BARS = {
|
|
92
|
+
ScrollBars.NEITHER: 0,
|
|
93
|
+
ScrollBars.HORIZONTAL: 1,
|
|
94
|
+
ScrollBars.VERTICAL: 2,
|
|
95
|
+
ScrollBars.BOTH: 3,
|
|
96
|
+
}
|
|
97
|
+
_ROW_SOURCE_TYPES = {
|
|
98
|
+
RowSourceType.TABLE_QUERY: "Table/Query",
|
|
99
|
+
RowSourceType.VALUE_LIST: "Value List",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _optional(obj: Any, name: str) -> Any:
|
|
104
|
+
"""Read a property that not every control has (``None`` when it is missing)."""
|
|
105
|
+
try:
|
|
106
|
+
return get(obj, name)
|
|
107
|
+
except pywintypes.com_error:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AccessDesignBackend:
|
|
112
|
+
"""Design operations through a live ``Access.Application`` with the database open."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, *, com: Callable[[], Com], app: Callable[[], Any]) -> None:
|
|
115
|
+
self._com_getter = com
|
|
116
|
+
self._app_getter = app
|
|
117
|
+
self._temp: Path | None = None
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def _com(self) -> Com:
|
|
121
|
+
return self._com_getter()
|
|
122
|
+
|
|
123
|
+
def _app(self) -> Any:
|
|
124
|
+
return self._app_getter()
|
|
125
|
+
|
|
126
|
+
def _docmd(self) -> Any:
|
|
127
|
+
return get(self._app(), "DoCmd")
|
|
128
|
+
|
|
129
|
+
def _temp_file(self, suffix: str) -> Path:
|
|
130
|
+
if self._temp is None:
|
|
131
|
+
self._temp = Path(tempfile.mkdtemp(prefix="pyaccesskit-"))
|
|
132
|
+
return self._temp / f"{uuid.uuid4().hex}{suffix}"
|
|
133
|
+
|
|
134
|
+
def cleanup(self) -> None:
|
|
135
|
+
"""Delete the private temp folder used for text import/export."""
|
|
136
|
+
if self._temp is not None:
|
|
137
|
+
for path in self._temp.glob("*"):
|
|
138
|
+
with contextlib.suppress(OSError):
|
|
139
|
+
path.unlink()
|
|
140
|
+
with contextlib.suppress(OSError):
|
|
141
|
+
self._temp.rmdir()
|
|
142
|
+
self._temp = None
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------------------------ objects
|
|
145
|
+
def list_objects(self, kind: ObjectKind) -> list[str]:
|
|
146
|
+
owner, collection_name = _ALL_COLLECTIONS[kind]
|
|
147
|
+
with self._com.op(f"list {kind.value}s"):
|
|
148
|
+
collection = get(get(self._app(), owner), collection_name)
|
|
149
|
+
return [
|
|
150
|
+
str(get(get(collection, "Item", i), "Name"))
|
|
151
|
+
for i in range(int(get(collection, "Count")))
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
def delete_object(self, kind: ObjectKind, name: str) -> None:
|
|
155
|
+
with self._com.op(f"delete {kind.value} {name!r}", kind=kind, name=name):
|
|
156
|
+
call(self._docmd(), "DeleteObject", int(_AC_TYPES[kind]), name)
|
|
157
|
+
|
|
158
|
+
def rename_object(self, kind: ObjectKind, old: str, new: str) -> None:
|
|
159
|
+
with self._com.op(f"rename {kind.value} {old!r} to {new!r}", kind=kind, name=old):
|
|
160
|
+
call(self._docmd(), "Rename", new, int(_AC_TYPES[kind]), old)
|
|
161
|
+
|
|
162
|
+
def export_text(self, kind: ObjectKind, name: str) -> bytes:
|
|
163
|
+
path = self._temp_file(".txt")
|
|
164
|
+
try:
|
|
165
|
+
with self._com.op(f"export {kind.value} {name!r} as text", kind=kind, name=name):
|
|
166
|
+
call(self._app(), "SaveAsText", int(_AC_TYPES[kind]), name, str(path))
|
|
167
|
+
return path.read_bytes()
|
|
168
|
+
finally:
|
|
169
|
+
with contextlib.suppress(OSError):
|
|
170
|
+
path.unlink()
|
|
171
|
+
|
|
172
|
+
def import_text(self, kind: ObjectKind, name: str, data: bytes) -> None:
|
|
173
|
+
path = self._temp_file(".txt")
|
|
174
|
+
path.write_bytes(data)
|
|
175
|
+
try:
|
|
176
|
+
with self._com.op(f"import {kind.value} {name!r} from text", kind=kind, name=name):
|
|
177
|
+
call(self._app(), "LoadFromText", int(_AC_TYPES[kind]), name, str(path))
|
|
178
|
+
finally:
|
|
179
|
+
with contextlib.suppress(OSError):
|
|
180
|
+
path.unlink()
|
|
181
|
+
|
|
182
|
+
# -------------------------------------------------------------------------------------- forms
|
|
183
|
+
def build_form(self, form: ResolvedForm, *, replace: bool) -> None:
|
|
184
|
+
spec = form.spec
|
|
185
|
+
app = self._app()
|
|
186
|
+
docmd = self._docmd()
|
|
187
|
+
with self._com.op(f"build form {spec.name!r}", kind=ObjectKind.FORM, name=spec.name):
|
|
188
|
+
frm = call(app, "CreateForm")
|
|
189
|
+
auto = str(get(frm, "Name"))
|
|
190
|
+
saved = False
|
|
191
|
+
try:
|
|
192
|
+
self._populate(app, frm, auto, form)
|
|
193
|
+
del frm
|
|
194
|
+
call(docmd, "Close", int(c.AcObjectType.acForm), auto, int(c.AcCloseSave.acSaveYes))
|
|
195
|
+
saved = True
|
|
196
|
+
finally:
|
|
197
|
+
if not saved:
|
|
198
|
+
with contextlib.suppress(pywintypes.com_error):
|
|
199
|
+
call(
|
|
200
|
+
docmd,
|
|
201
|
+
"Close",
|
|
202
|
+
int(c.AcObjectType.acForm),
|
|
203
|
+
auto,
|
|
204
|
+
int(c.AcCloseSave.acSaveNo),
|
|
205
|
+
)
|
|
206
|
+
self._install(auto, spec.name, replace=replace)
|
|
207
|
+
|
|
208
|
+
def _populate(self, app: Any, frm: Any, auto: str, form: ResolvedForm) -> None:
|
|
209
|
+
spec = form.spec
|
|
210
|
+
if form.has_header:
|
|
211
|
+
call(get(app, "DoCmd"), "RunCommand", AC_CMD_FORM_HDR_FTR)
|
|
212
|
+
if spec.record_source is not None:
|
|
213
|
+
put(frm, "RecordSource", spec.record_source)
|
|
214
|
+
if spec.caption is not None:
|
|
215
|
+
put(frm, "Caption", spec.caption)
|
|
216
|
+
for prop, value in (
|
|
217
|
+
("DefaultView", _DEFAULT_VIEWS[spec.default_view]),
|
|
218
|
+
("AllowAdditions", spec.allow_additions),
|
|
219
|
+
("AllowEdits", spec.allow_edits),
|
|
220
|
+
("AllowDeletions", spec.allow_deletions),
|
|
221
|
+
("DataEntry", spec.data_entry),
|
|
222
|
+
("NavigationButtons", spec.navigation_buttons),
|
|
223
|
+
("RecordSelectors", spec.record_selectors),
|
|
224
|
+
("DividingLines", spec.dividing_lines),
|
|
225
|
+
("ScrollBars", _SCROLL_BARS[spec.scroll_bars]),
|
|
226
|
+
("AutoCenter", spec.auto_center),
|
|
227
|
+
("PopUp", spec.pop_up),
|
|
228
|
+
("Modal", spec.modal),
|
|
229
|
+
("Width", form.width.twips),
|
|
230
|
+
):
|
|
231
|
+
put(frm, prop, value)
|
|
232
|
+
put(get(frm, "Section", _SECTIONS[Section.DETAIL]), "Height", form.detail_height.twips)
|
|
233
|
+
if form.has_header:
|
|
234
|
+
put(
|
|
235
|
+
get(frm, "Section", _SECTIONS[Section.HEADER]),
|
|
236
|
+
"Height",
|
|
237
|
+
(form.header_height or Length(0)).twips,
|
|
238
|
+
)
|
|
239
|
+
put(
|
|
240
|
+
get(frm, "Section", _SECTIONS[Section.FOOTER]),
|
|
241
|
+
"Height",
|
|
242
|
+
(form.footer_height or Length(0)).twips,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
created: dict[str, Any] = {}
|
|
246
|
+
for resolved in form.controls:
|
|
247
|
+
control = resolved.spec
|
|
248
|
+
kind = control.control_kind
|
|
249
|
+
if kind not in _CONTROL_TYPES:
|
|
250
|
+
raise SpecError(f"control kind {kind.value!r} is not supported by the form builder")
|
|
251
|
+
rect = resolved.rect
|
|
252
|
+
column = control.bound_field or ""
|
|
253
|
+
ctl = call(
|
|
254
|
+
app,
|
|
255
|
+
"CreateControl",
|
|
256
|
+
auto,
|
|
257
|
+
int(_CONTROL_TYPES[kind]),
|
|
258
|
+
_SECTIONS[resolved.section],
|
|
259
|
+
"",
|
|
260
|
+
column,
|
|
261
|
+
rect.left.twips,
|
|
262
|
+
rect.top.twips,
|
|
263
|
+
rect.width.twips,
|
|
264
|
+
rect.height.twips,
|
|
265
|
+
)
|
|
266
|
+
put(ctl, "Name", resolved.name)
|
|
267
|
+
self._configure_control(ctl, control)
|
|
268
|
+
created[resolved.name] = ctl
|
|
269
|
+
label = resolved.label
|
|
270
|
+
if label is not None:
|
|
271
|
+
# Tabular layouts put labels in the header: those are free-standing, not attached.
|
|
272
|
+
parent = resolved.name if label.section is resolved.section else ""
|
|
273
|
+
lbl = call(
|
|
274
|
+
app,
|
|
275
|
+
"CreateControl",
|
|
276
|
+
auto,
|
|
277
|
+
int(c.AcControlType.acLabel),
|
|
278
|
+
_SECTIONS[label.section],
|
|
279
|
+
parent,
|
|
280
|
+
"",
|
|
281
|
+
label.rect.left.twips,
|
|
282
|
+
label.rect.top.twips,
|
|
283
|
+
label.rect.width.twips,
|
|
284
|
+
label.rect.height.twips,
|
|
285
|
+
)
|
|
286
|
+
put(lbl, "Name", label.name)
|
|
287
|
+
put(lbl, "Caption", label.caption)
|
|
288
|
+
|
|
289
|
+
for binding in form.events:
|
|
290
|
+
target = frm if binding.object_name == "Form" else created[binding.object_name]
|
|
291
|
+
put(target, binding.property_name, "[Event Procedure]")
|
|
292
|
+
if form.module_text is not None:
|
|
293
|
+
put(frm, "HasModule", True)
|
|
294
|
+
module = get(frm, "Module")
|
|
295
|
+
lines = int(get(module, "CountOfLines"))
|
|
296
|
+
if lines:
|
|
297
|
+
call(module, "DeleteLines", 1, lines)
|
|
298
|
+
call(module, "AddFromString", form.module_text)
|
|
299
|
+
for prop, value in spec.properties.items():
|
|
300
|
+
put(get(get(frm, "Properties"), "Item", prop), "Value", value)
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _configure_control(ctl: Any, control: Any) -> None:
|
|
304
|
+
put(ctl, "Visible", control.visible)
|
|
305
|
+
if isinstance(control, (LabelSpec, ButtonSpec)):
|
|
306
|
+
put(ctl, "Caption", control.caption)
|
|
307
|
+
elif isinstance(control, (TextBoxSpec, CheckBoxSpec, ComboBoxSpec)):
|
|
308
|
+
put(ctl, "Enabled", control.enabled)
|
|
309
|
+
put(ctl, "Locked", control.locked)
|
|
310
|
+
if isinstance(control, TextBoxSpec):
|
|
311
|
+
if control.control_source is not None:
|
|
312
|
+
put(ctl, "ControlSource", control.control_source)
|
|
313
|
+
if control.format is not None:
|
|
314
|
+
put(ctl, "Format", control.format)
|
|
315
|
+
if isinstance(control, ComboBoxSpec):
|
|
316
|
+
put(ctl, "RowSourceType", _ROW_SOURCE_TYPES[control.row_source_type])
|
|
317
|
+
put(ctl, "RowSource", control.row_source)
|
|
318
|
+
put(ctl, "BoundColumn", control.bound_column)
|
|
319
|
+
put(ctl, "ColumnCount", control.column_count)
|
|
320
|
+
put(ctl, "LimitToList", control.limit_to_list)
|
|
321
|
+
if control.column_widths is not None:
|
|
322
|
+
put(
|
|
323
|
+
ctl,
|
|
324
|
+
"ColumnWidths",
|
|
325
|
+
";".join(str(width.twips) for width in control.column_widths),
|
|
326
|
+
)
|
|
327
|
+
for prop, value in control.properties.items():
|
|
328
|
+
put(get(get(ctl, "Properties"), "Item", prop), "Value", value)
|
|
329
|
+
|
|
330
|
+
def _install(self, auto: str, name: str, *, replace: bool) -> None:
|
|
331
|
+
form_type = int(c.AcObjectType.acForm)
|
|
332
|
+
with self._com.op(f"install form {name!r}", kind=ObjectKind.FORM, name=name):
|
|
333
|
+
docmd = self._docmd()
|
|
334
|
+
backup: str | None = None
|
|
335
|
+
try:
|
|
336
|
+
if replace:
|
|
337
|
+
backup = f"~pak_bak_{uuid.uuid4().hex[:12]}"
|
|
338
|
+
call(docmd, "Rename", backup, form_type, name)
|
|
339
|
+
call(docmd, "Rename", name, form_type, auto)
|
|
340
|
+
except BaseException:
|
|
341
|
+
with contextlib.suppress(pywintypes.com_error):
|
|
342
|
+
call(docmd, "DeleteObject", form_type, auto)
|
|
343
|
+
if backup is not None:
|
|
344
|
+
with contextlib.suppress(pywintypes.com_error):
|
|
345
|
+
call(docmd, "Rename", name, form_type, backup)
|
|
346
|
+
raise
|
|
347
|
+
if backup is not None:
|
|
348
|
+
call(docmd, "DeleteObject", form_type, backup)
|
|
349
|
+
|
|
350
|
+
def _open_form(self, name: str, view: int) -> Any:
|
|
351
|
+
call(
|
|
352
|
+
self._docmd(),
|
|
353
|
+
"OpenForm",
|
|
354
|
+
name,
|
|
355
|
+
view,
|
|
356
|
+
"",
|
|
357
|
+
"",
|
|
358
|
+
int(c.AcFormOpenDataMode.acFormPropertySettings),
|
|
359
|
+
int(c.AcWindowMode.acHidden),
|
|
360
|
+
)
|
|
361
|
+
return get(get(self._app(), "Forms"), "Item", name)
|
|
362
|
+
|
|
363
|
+
def _close_form(self, name: str) -> None:
|
|
364
|
+
with contextlib.suppress(pywintypes.com_error):
|
|
365
|
+
call(
|
|
366
|
+
self._docmd(),
|
|
367
|
+
"Close",
|
|
368
|
+
int(c.AcObjectType.acForm),
|
|
369
|
+
name,
|
|
370
|
+
int(c.AcCloseSave.acSaveNo),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def form_controls(self, name: str) -> list[ControlInfo]:
|
|
374
|
+
with self._com.op(f"read controls of form {name!r}", kind=ObjectKind.FORM, name=name):
|
|
375
|
+
frm = self._open_form(name, int(c.AcFormView.acDesign))
|
|
376
|
+
try:
|
|
377
|
+
controls = get(frm, "Controls")
|
|
378
|
+
infos: list[ControlInfo] = []
|
|
379
|
+
for index in range(int(get(controls, "Count"))):
|
|
380
|
+
ctl = get(controls, "Item", index)
|
|
381
|
+
parent_obj = _optional(ctl, "Parent")
|
|
382
|
+
parent = str(get(parent_obj, "Name")) if parent_obj is not None else None
|
|
383
|
+
if parent is not None and parent.casefold() == name.casefold():
|
|
384
|
+
parent = None
|
|
385
|
+
control_source = _optional(ctl, "ControlSource")
|
|
386
|
+
caption = _optional(ctl, "Caption")
|
|
387
|
+
infos.append(
|
|
388
|
+
ControlInfo(
|
|
389
|
+
name=str(get(ctl, "Name")),
|
|
390
|
+
kind=_CONTROL_KINDS.get(
|
|
391
|
+
int(get(ctl, "ControlType")), ControlKind.OTHER
|
|
392
|
+
),
|
|
393
|
+
section=_SECTION_OF.get(int(get(ctl, "Section"))),
|
|
394
|
+
left=Length(int(get(ctl, "Left"))),
|
|
395
|
+
top=Length(int(get(ctl, "Top"))),
|
|
396
|
+
width=Length(int(get(ctl, "Width"))),
|
|
397
|
+
height=Length(int(get(ctl, "Height"))),
|
|
398
|
+
control_source=str(control_source) if control_source else None,
|
|
399
|
+
caption=str(caption) if caption is not None else None,
|
|
400
|
+
parent=parent,
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
return infos
|
|
404
|
+
finally:
|
|
405
|
+
del frm
|
|
406
|
+
self._close_form(name)
|
|
407
|
+
|
|
408
|
+
def check_form_opens(self, name: str) -> None:
|
|
409
|
+
with self._com.op(f"open form {name!r}", kind=ObjectKind.FORM, name=name):
|
|
410
|
+
frm = self._open_form(name, int(c.AcFormView.acNormal))
|
|
411
|
+
try:
|
|
412
|
+
get(frm, "CurrentRecord")
|
|
413
|
+
finally:
|
|
414
|
+
del frm
|
|
415
|
+
self._close_form(name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DAO adapter (COM). Imported lazily by the engines."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# pyright: basic
|
|
2
|
+
"""The "native defaults" profile: database properties Access writes to new databases but DAO does not.
|
|
3
|
+
|
|
4
|
+
Recorded in spike S3 (ADR 0001) by comparing ``NewCurrentDatabase`` with DAO ``CreateDatabase``. Applying
|
|
5
|
+
them to DAO-created files makes them behave exactly like Access-created ones (e.g. tabbed documents
|
|
6
|
+
instead of legacy overlapping windows). ``AccessVersion`` is omitted: Access maintains it itself.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pyaccesskit._backends.dao.schema import _prop, _set_prop
|
|
14
|
+
from pyaccesskit._backends.dao.typemap import DB_BOOLEAN, DB_BYTE, DB_LONG
|
|
15
|
+
|
|
16
|
+
__all__ = ["NATIVE_DEFAULTS", "apply_native_defaults"]
|
|
17
|
+
|
|
18
|
+
NATIVE_DEFAULTS: tuple[tuple[str, int, Any], ...] = (
|
|
19
|
+
("ANSI Query Mode", DB_LONG, 0),
|
|
20
|
+
("CheckTruncatedNumFields", DB_LONG, 1),
|
|
21
|
+
("Clear Cache on Close", DB_LONG, 0),
|
|
22
|
+
("Default Zoom Level", DB_LONG, 100),
|
|
23
|
+
("NavPane Category", DB_LONG, 0),
|
|
24
|
+
("Never Cache", DB_LONG, 0),
|
|
25
|
+
("Option to enable Monaco SQL Editor", DB_LONG, 1),
|
|
26
|
+
("Picture Property Storage Format", DB_LONG, 0),
|
|
27
|
+
("Show Navigation Pane Search Bar", DB_LONG, 1),
|
|
28
|
+
("ShowDocumentTabs", DB_BOOLEAN, True),
|
|
29
|
+
("Themed Form Controls", DB_LONG, 1),
|
|
30
|
+
("Use Microsoft Access 2007 compatible cache", DB_LONG, 0),
|
|
31
|
+
("UseMDIMode", DB_BYTE, 0),
|
|
32
|
+
("WebDesignMode", DB_BYTE, 0),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def apply_native_defaults(db: Any) -> None:
|
|
37
|
+
"""Create any missing native-default property on a DAO ``Database`` (existing values are kept)."""
|
|
38
|
+
for name, dao_type, value in NATIVE_DEFAULTS:
|
|
39
|
+
if _prop(db, name) is None:
|
|
40
|
+
_set_prop(db, name, dao_type, value)
|