python-msilib 0.4.2__cp314-cp314-win32.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.
msilib/__init__.py ADDED
@@ -0,0 +1,778 @@
1
+ """Read and write Microsoft Installer files."""
2
+
3
+ # Copyright (C) 2005 Martin v. Löwis
4
+ # Licensed to PSF under a Contributor Agreement.
5
+ import contextlib
6
+ import fnmatch
7
+ import os
8
+ import platform
9
+ import re
10
+ import string
11
+ from tempfile import mkstemp
12
+
13
+ from msilib._msi import (
14
+ MSICOLINFO_NAMES,
15
+ MSICOLINFO_TYPES,
16
+ MSIDBOPEN_CREATE,
17
+ MSIDBOPEN_CREATEDIRECT,
18
+ MSIDBOPEN_DIRECT,
19
+ MSIDBOPEN_PATCHFILE,
20
+ MSIDBOPEN_READONLY,
21
+ MSIDBOPEN_TRANSACT,
22
+ MSIMODIFY_ASSIGN,
23
+ MSIMODIFY_DELETE,
24
+ MSIMODIFY_INSERT,
25
+ MSIMODIFY_INSERT_TEMPORARY,
26
+ MSIMODIFY_MERGE,
27
+ MSIMODIFY_REFRESH,
28
+ MSIMODIFY_REPLACE,
29
+ MSIMODIFY_SEEK,
30
+ MSIMODIFY_UPDATE,
31
+ MSIMODIFY_VALIDATE,
32
+ MSIMODIFY_VALIDATE_DELETE,
33
+ MSIMODIFY_VALIDATE_FIELD,
34
+ MSIMODIFY_VALIDATE_NEW,
35
+ PID_APPNAME,
36
+ PID_AUTHOR,
37
+ PID_CHARCOUNT,
38
+ PID_CODEPAGE,
39
+ PID_COMMENTS,
40
+ PID_CREATE_DTM,
41
+ PID_KEYWORDS,
42
+ PID_LASTAUTHOR,
43
+ PID_LASTPRINTED,
44
+ PID_LASTSAVE_DTM,
45
+ PID_PAGECOUNT,
46
+ PID_REVNUMBER,
47
+ PID_SECURITY,
48
+ PID_SUBJECT,
49
+ PID_TEMPLATE,
50
+ PID_TITLE,
51
+ PID_WORDCOUNT,
52
+ CreateRecord,
53
+ FCICreate,
54
+ MSIError,
55
+ OpenDatabase,
56
+ UuidCreate,
57
+ )
58
+
59
+ __version__ = "0.4.2"
60
+
61
+ __all__ = [
62
+ "CAB",
63
+ "MSICOLINFO_NAMES",
64
+ "MSICOLINFO_TYPES",
65
+ "MSIDBOPEN_CREATE",
66
+ "MSIDBOPEN_CREATEDIRECT",
67
+ "MSIDBOPEN_DIRECT",
68
+ "MSIDBOPEN_PATCHFILE",
69
+ "MSIDBOPEN_READONLY",
70
+ "MSIDBOPEN_TRANSACT",
71
+ "MSIMODIFY_ASSIGN",
72
+ "MSIMODIFY_DELETE",
73
+ "MSIMODIFY_INSERT",
74
+ "MSIMODIFY_INSERT_TEMPORARY",
75
+ "MSIMODIFY_MERGE",
76
+ "MSIMODIFY_REFRESH",
77
+ "MSIMODIFY_REPLACE",
78
+ "MSIMODIFY_SEEK",
79
+ "MSIMODIFY_UPDATE",
80
+ "MSIMODIFY_VALIDATE",
81
+ "MSIMODIFY_VALIDATE_DELETE",
82
+ "MSIMODIFY_VALIDATE_FIELD",
83
+ "MSIMODIFY_VALIDATE_NEW",
84
+ "PID_APPNAME",
85
+ "PID_AUTHOR",
86
+ "PID_CHARCOUNT",
87
+ "PID_CODEPAGE",
88
+ "PID_COMMENTS",
89
+ "PID_CREATE_DTM",
90
+ "PID_KEYWORDS",
91
+ "PID_LASTAUTHOR",
92
+ "PID_LASTPRINTED",
93
+ "PID_LASTSAVE_DTM",
94
+ "PID_PAGECOUNT",
95
+ "PID_REVNUMBER",
96
+ "PID_SECURITY",
97
+ "PID_SUBJECT",
98
+ "PID_TEMPLATE",
99
+ "PID_TITLE",
100
+ "PID_WORDCOUNT",
101
+ "Binary",
102
+ "Control",
103
+ "CreateRecord",
104
+ "Dialog",
105
+ "Directory",
106
+ "FCICreate",
107
+ "Feature",
108
+ "MSIError",
109
+ "OpenDatabase",
110
+ "RadioButtonGroup",
111
+ "Table",
112
+ "UuidCreate",
113
+ "add_data",
114
+ "add_stream",
115
+ "add_tables",
116
+ "change_sequence",
117
+ "datasizemask",
118
+ "gen_uuid",
119
+ "init_database",
120
+ "knownbits",
121
+ "make_id",
122
+ "type_binary",
123
+ "type_key",
124
+ "type_localizable",
125
+ "type_long",
126
+ "type_nullable",
127
+ "type_short",
128
+ "type_string",
129
+ "type_valid",
130
+ "typemask",
131
+ ]
132
+
133
+
134
+ # Should work in windows, but also in mingw, cygwin, ...
135
+ AMD64 = platform.machine() in ("x64", "x86_64", "AMD64")
136
+ ARM64 = platform.machine() in ("aarch64", "arm64", "ARM64")
137
+
138
+ # Keep msilib.Win64 around to preserve backwards compatibility.
139
+ Win64 = AMD64
140
+
141
+ # Partially taken from Wine
142
+ datasizemask = 0x00FF
143
+ type_valid = 0x0100
144
+ type_localizable = 0x0200
145
+
146
+ typemask = 0x0C00
147
+ type_long = 0x0000
148
+ type_short = 0x0400
149
+ type_string = 0x0C00
150
+ type_binary = 0x0800
151
+
152
+ type_nullable = 0x1000
153
+ type_key = 0x2000
154
+ # XXX temporary, localizable?
155
+ knownbits = (
156
+ datasizemask
157
+ | type_valid
158
+ | type_localizable
159
+ | typemask
160
+ | type_nullable
161
+ | type_key
162
+ )
163
+
164
+
165
+ class Table:
166
+ def __init__(self, name) -> None:
167
+ self.name = name
168
+ self.fields = []
169
+
170
+ def add_field(self, index, name, type) -> None:
171
+ self.fields.append((index, name, type))
172
+
173
+ def sql(self) -> str:
174
+ fields = []
175
+ keys = []
176
+ self.fields.sort()
177
+ fields = [None] * len(self.fields)
178
+ for index, name, ftype in self.fields:
179
+ idx = index - 1
180
+ unk = ftype & ~knownbits
181
+ if unk:
182
+ print(f"{self.name}.{name} unknown bits {unk:x}")
183
+ size = ftype & datasizemask
184
+ dtype = ftype & typemask
185
+ if dtype == type_string:
186
+ tname = f"CHAR({size})" if size else "CHAR"
187
+ elif dtype == type_short:
188
+ assert size == 2
189
+ tname = "SHORT"
190
+ elif dtype == type_long:
191
+ assert size == 4
192
+ tname = "LONG"
193
+ elif dtype == type_binary:
194
+ assert size == 0
195
+ tname = "OBJECT"
196
+ else:
197
+ tname = "unknown"
198
+ print(f"{self.name}.{name} unknown integer type {size}")
199
+ flags = "" if ftype & type_nullable else " NOT NULL"
200
+ if ftype & type_localizable:
201
+ flags += " LOCALIZABLE"
202
+ fields[idx] = f"`{name}` {tname}{flags}"
203
+ if ftype & type_key:
204
+ keys.append(f"`{name}`")
205
+ fields = ", ".join(fields)
206
+ keys = ", ".join(keys)
207
+ return f"CREATE TABLE {self.name} ({fields} PRIMARY KEY {keys})"
208
+
209
+ def create(self, db) -> None:
210
+ v = db.OpenView(self.sql())
211
+ v.Execute(None)
212
+ v.Close()
213
+
214
+
215
+ class _Unspecified:
216
+ pass
217
+
218
+
219
+ def change_sequence(
220
+ seq, action, seqno=_Unspecified, cond=_Unspecified
221
+ ) -> None:
222
+ """Change the sequence number of an action in a sequence list."""
223
+ for i in range(len(seq)):
224
+ if seq[i][0] == action:
225
+ if cond is _Unspecified:
226
+ cond = seq[i][1]
227
+ if seqno is _Unspecified:
228
+ seqno = seq[i][2]
229
+ seq[i] = (action, cond, seqno)
230
+ return
231
+ msg = "Action not found in sequence"
232
+ raise ValueError(msg)
233
+
234
+
235
+ def add_data(db, table, values) -> None:
236
+ v = db.OpenView(f"SELECT * FROM `{table}`")
237
+ count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
238
+ r = CreateRecord(count)
239
+ for value in values:
240
+ assert len(value) == count, value
241
+ for i in range(count):
242
+ field = value[i]
243
+ if isinstance(field, int):
244
+ r.SetInteger(i + 1, field)
245
+ elif isinstance(field, str):
246
+ r.SetString(i + 1, field)
247
+ elif field is None:
248
+ pass
249
+ elif isinstance(field, Binary):
250
+ r.SetStream(i + 1, field.name)
251
+ else:
252
+ msg = f"Unsupported type {field.__class__.__name__}"
253
+ raise TypeError(msg)
254
+ try:
255
+ v.Modify(MSIMODIFY_INSERT, r)
256
+ except Exception: # noqa: BLE001
257
+ msg = f"Could not insert {values!r} into {table}"
258
+ raise MSIError(msg) from None
259
+
260
+ r.ClearData()
261
+ v.Close()
262
+
263
+
264
+ def add_stream(db, name, path) -> None:
265
+ v = db.OpenView(f"INSERT INTO _Streams (Name, Data) VALUES ('{name}', ?)")
266
+ r = CreateRecord(1)
267
+ r.SetStream(1, path)
268
+ v.Execute(r)
269
+ v.Close()
270
+
271
+
272
+ def init_database(
273
+ name, schema, ProductName, ProductCode, ProductVersion, Manufacturer
274
+ ):
275
+ with contextlib.suppress(OSError):
276
+ os.unlink(name)
277
+ ProductCode = ProductCode.upper()
278
+ # Create the database
279
+ db = OpenDatabase(name, MSIDBOPEN_CREATE)
280
+ # Create the tables
281
+ for t in schema.tables:
282
+ t.create(db)
283
+ # Fill the validation table
284
+ add_data(db, "_Validation", schema._Validation_records) # noqa: SLF001
285
+ # Initialize the summary information, allowing at most 20 properties
286
+ si = db.GetSummaryInformation(20)
287
+ si.SetProperty(PID_TITLE, "Installation Database")
288
+ si.SetProperty(PID_SUBJECT, ProductName)
289
+ si.SetProperty(PID_AUTHOR, Manufacturer)
290
+ # https://learn.microsoft.com/en-us/windows/win32/msi/template-summary
291
+ if AMD64:
292
+ si.SetProperty(PID_TEMPLATE, "x64;1033")
293
+ elif ARM64:
294
+ si.SetProperty(PID_TEMPLATE, "Arm64;1033")
295
+ else:
296
+ si.SetProperty(PID_TEMPLATE, "Intel;1033")
297
+ si.SetProperty(PID_REVNUMBER, gen_uuid())
298
+ # https://learn.microsoft.com/en-us/windows/win32/msi/word-count-summary
299
+ # 2 = long file names, compressed, original media
300
+ si.SetProperty(PID_WORDCOUNT, 2)
301
+ # https://learn.microsoft.com/en-us/windows/win32/msi/page-count-summary
302
+ # https://learn.microsoft.com/en-us/windows/win32/msi/using-64-bit-windows-installer-packages
303
+ if ARM64:
304
+ si.SetProperty(PID_PAGECOUNT, 500) # minimum of Windows Installer 5.0
305
+ else:
306
+ si.SetProperty(PID_PAGECOUNT, 200) # minimum of Windows Installer 2.0
307
+ si.SetProperty(PID_APPNAME, "Python MSI Library")
308
+ # XXX more properties
309
+ si.Persist()
310
+ add_data(
311
+ db,
312
+ "Property",
313
+ [
314
+ ("ProductName", ProductName),
315
+ ("ProductCode", ProductCode),
316
+ ("ProductVersion", ProductVersion),
317
+ ("Manufacturer", Manufacturer),
318
+ ("ProductLanguage", "1033"),
319
+ ],
320
+ )
321
+ db.Commit()
322
+ return db
323
+
324
+
325
+ def add_tables(db, module) -> None:
326
+ for table in module.tables:
327
+ add_data(db, table, getattr(module, table))
328
+
329
+
330
+ def make_id(value: str) -> str:
331
+ identifier_chars = string.ascii_letters + string.digits + "._"
332
+ value = "".join([c if c in identifier_chars else "_" for c in value])
333
+ if value[0] in (string.digits + "."):
334
+ value = "_" + value
335
+ assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", value), "FILE" + value
336
+ return value
337
+
338
+
339
+ def gen_uuid() -> str:
340
+ return "{" + UuidCreate().upper() + "}"
341
+
342
+
343
+ class CAB:
344
+ def __init__(self, name) -> None:
345
+ self.name = name
346
+ self.files = []
347
+ self.filenames = set()
348
+ self.index = 0
349
+
350
+ def gen_id(self, file) -> str:
351
+ logical = _logical = make_id(file)
352
+ pos = 1
353
+ while logical in self.filenames:
354
+ logical = f"{_logical}.{pos}"
355
+ pos += 1
356
+ self.filenames.add(logical)
357
+ return logical
358
+
359
+ def append(self, full, file, logical) -> tuple[int, str]:
360
+ if os.path.isdir(full):
361
+ return None
362
+ if not logical:
363
+ logical = self.gen_id(file)
364
+ self.index += 1
365
+ self.files.append((full, logical))
366
+ return self.index, logical
367
+
368
+ def commit(self, db) -> None:
369
+ fd, filename = mkstemp()
370
+ os.close(fd)
371
+ FCICreate(filename, self.files)
372
+ add_data(
373
+ db, "Media", [(1, self.index, None, "#" + self.name, None, None)]
374
+ )
375
+ add_stream(db, self.name, filename)
376
+ os.unlink(filename)
377
+ db.Commit()
378
+
379
+
380
+ _directories = set()
381
+
382
+
383
+ class Directory:
384
+ def __init__(
385
+ self,
386
+ db,
387
+ cab,
388
+ basedir,
389
+ physical,
390
+ _logical,
391
+ default,
392
+ componentflags=None,
393
+ ) -> None:
394
+ """Create a new directory in the Directory table. There is a current
395
+ component at each point in time for the directory, which is either
396
+ explicitly created through start_component, or implicitly when files
397
+ are added for the first time. Files are added into the current
398
+ component, and into the cab file. To create a directory, a base
399
+ directory object needs to be specified (can be None), the path to the
400
+ physical directory, and a logical directory name. Default specifies the
401
+ DefaultDir slot in the directory table. componentflags specifies the
402
+ default flags that new components get.
403
+ """
404
+ index = 1
405
+ _logical = make_id(_logical)
406
+ logical = _logical
407
+ while logical in _directories:
408
+ logical = f"{_logical}{index}"
409
+ index += 1
410
+ _directories.add(logical)
411
+ self.db = db
412
+ self.cab = cab
413
+ self.basedir = basedir
414
+ self.physical = physical
415
+ self.logical = logical
416
+ self.component = None
417
+ self.short_names = set()
418
+ self.ids = set()
419
+ self.keyfiles = {}
420
+ self.componentflags = componentflags
421
+ if basedir:
422
+ self.absolute = os.path.join(basedir.absolute, physical)
423
+ blogical = basedir.logical
424
+ else:
425
+ self.absolute = physical
426
+ blogical = None
427
+ add_data(db, "Directory", [(logical, blogical, default)])
428
+
429
+ def start_component(
430
+ self, component=None, feature=None, flags=None, keyfile=None, uuid=None
431
+ ) -> None:
432
+ """Add an entry to the Component table, and make this component the
433
+ current for this directory. If no component name is given, the
434
+ directory name is used. If no feature is given, the current feature is
435
+ used. If no flags are given, the directory's default flags are used. If
436
+ no keyfile is given, the KeyPath is left null in the Component table.
437
+ """
438
+ if flags is None:
439
+ flags = self.componentflags
440
+ uuid = gen_uuid() if uuid is None else uuid.upper()
441
+ if component is None:
442
+ component = self.logical
443
+ self.component = component
444
+ # https://learn.microsoft.com/en-us/windows/win32/msi/component-table
445
+ if AMD64 or ARM64:
446
+ flags |= 256 # msidbComponentAttributes64bit
447
+ if keyfile:
448
+ keyid = self.cab.gen_id(keyfile)
449
+ self.keyfiles[keyfile] = keyid
450
+ else:
451
+ keyid = None
452
+ add_data(
453
+ self.db,
454
+ "Component",
455
+ [(component, uuid, self.logical, flags, None, keyid)],
456
+ )
457
+ if feature is None:
458
+ feature = current_feature
459
+ add_data(self.db, "FeatureComponents", [(feature.id, component)])
460
+
461
+ def make_short(self, file: str) -> str:
462
+ oldfile = file
463
+ file = file.replace("+", "_")
464
+ file = "".join(c for c in file if c not in r' "/\[]:;=,')
465
+ parts = file.split(".")
466
+ if len(parts) > 1:
467
+ prefix = "".join(parts[:-1]).upper()
468
+ suffix = parts[-1].upper()
469
+ if not prefix:
470
+ prefix = suffix
471
+ suffix = None
472
+ else:
473
+ prefix = file.upper()
474
+ suffix = None
475
+ if (
476
+ len(parts) < 3
477
+ and len(prefix) <= 8
478
+ and file == oldfile
479
+ and (not suffix or len(suffix) <= 3)
480
+ ):
481
+ file = f"{prefix}.{suffix}" if suffix else prefix
482
+ else:
483
+ file = None
484
+ if file is None or file in self.short_names:
485
+ prefix = prefix[:6]
486
+ if suffix:
487
+ suffix = suffix[:3]
488
+ pos = 1
489
+ while 1:
490
+ if suffix:
491
+ file = f"{prefix}~{pos}.{suffix}"
492
+ else:
493
+ file = f"{prefix}~{pos}"
494
+ if file not in self.short_names:
495
+ break
496
+ pos += 1
497
+ assert pos < 10000
498
+ if pos in (10, 100, 1000):
499
+ prefix = prefix[:-1]
500
+ self.short_names.add(file)
501
+ # restrictions on short names
502
+ assert not re.search(r'[\?|><:/*"+,;=\[\]]', file)
503
+ return file
504
+
505
+ def add_file(
506
+ self,
507
+ file: str,
508
+ src: str | None = None,
509
+ version: str | None = None,
510
+ language: str | None = None,
511
+ ) -> str:
512
+ """Add a file to the current component of the directory, starting a new
513
+ one if there is no current component. By default, the file name in the
514
+ source and the file table will be identical. If the src file is
515
+ specified, it is interpreted relative to the current directory.
516
+ Optionally, a version and a language can be specified for the entry in
517
+ the File table.
518
+ """
519
+ if not self.component:
520
+ self.start_component(self.logical, current_feature, 0)
521
+ if not src:
522
+ # Allow relative paths for file if src is not specified
523
+ src = file
524
+ file = os.path.basename(file)
525
+ absolute = os.path.join(self.absolute, src)
526
+ # restrictions on long names
527
+ assert not re.search(r'[\?|><:/*]"', file)
528
+ logical = self.keyfiles.get(file, None)
529
+ sequence, logical = self.cab.append(absolute, file, logical)
530
+ assert logical not in self.ids
531
+ self.ids.add(logical)
532
+ short = self.make_short(file)
533
+ full = f"{short}|{file}"
534
+ filesize = os.stat(absolute).st_size
535
+ # constants.msidbFileAttributesVital
536
+ # Compressed omitted, since it is the database default
537
+ # could add r/o, system, hidden
538
+ attributes = 512
539
+ add_data(
540
+ self.db,
541
+ "File",
542
+ [
543
+ (
544
+ logical,
545
+ self.component,
546
+ full,
547
+ filesize,
548
+ version,
549
+ language,
550
+ attributes,
551
+ sequence,
552
+ )
553
+ ],
554
+ )
555
+ # if not version:
556
+ # # Add hash if the file is not versioned
557
+ # filehash = FileHash(absolute, 0)
558
+ # add_data(self.db, "MsiFileHash",
559
+ # [(logical, 0, filehash.IntegerData(1),
560
+ # filehash.IntegerData(2), filehash.IntegerData(3),
561
+ # filehash.IntegerData(4))])
562
+ # Automatically remove .pyc files on uninstall (2)
563
+ # XXX: adding so many RemoveFile entries makes installer unbelievably
564
+ # slow. So instead, we have to use wildcard remove entries
565
+ if file.endswith(".py"):
566
+ add_data(
567
+ self.db,
568
+ "RemoveFile",
569
+ [
570
+ (
571
+ logical + "c",
572
+ self.component,
573
+ f"{short}C|{file}c",
574
+ self.logical,
575
+ 2,
576
+ ),
577
+ (
578
+ logical + "o",
579
+ self.component,
580
+ f"{short}O|{file}o",
581
+ self.logical,
582
+ 2,
583
+ ),
584
+ ],
585
+ )
586
+ return logical
587
+
588
+ def glob(self, pattern: str, exclude=None) -> list[str]:
589
+ """Add a list of files to the current component as specified in the
590
+ glob pattern. Individual files can be excluded in the exclude list.
591
+ """
592
+ try:
593
+ files = os.listdir(self.absolute)
594
+ except OSError:
595
+ return []
596
+ if pattern[:1] != ".":
597
+ files = (f for f in files if f[0] != ".")
598
+ files = fnmatch.filter(files, pattern)
599
+ for f in files:
600
+ if exclude and f in exclude:
601
+ continue
602
+ self.add_file(f)
603
+ return files
604
+
605
+ def remove_pyc(self) -> None:
606
+ """Remove .pyc files on uninstall."""
607
+ add_data(
608
+ self.db,
609
+ "RemoveFile",
610
+ [(self.component + "c", self.component, "*.pyc", self.logical, 2)],
611
+ )
612
+
613
+
614
+ class Binary:
615
+ def __init__(self, fname) -> None:
616
+ self.name = fname
617
+
618
+ def __repr__(self) -> str:
619
+ return f'msilib.Binary(os.path.join(dirname,"{self.name}"))'
620
+
621
+
622
+ class Feature:
623
+ def __init__(
624
+ self,
625
+ db,
626
+ id,
627
+ title,
628
+ desc,
629
+ display,
630
+ level=1,
631
+ parent=None,
632
+ directory=None,
633
+ attributes=0,
634
+ ) -> None:
635
+ self.id = id
636
+ if parent:
637
+ parent = parent.id
638
+ add_data(
639
+ db,
640
+ "Feature",
641
+ [(id, parent, title, desc, display, level, directory, attributes)],
642
+ )
643
+
644
+ def set_current(self) -> None:
645
+ global current_feature
646
+ current_feature = self
647
+
648
+
649
+ class Control:
650
+ def __init__(self, dlg, name) -> None:
651
+ self.dlg = dlg
652
+ self.name = name
653
+
654
+ def event(self, event, argument, condition="1", ordering=None) -> None:
655
+ add_data(
656
+ self.dlg.db,
657
+ "ControlEvent",
658
+ [(self.dlg.name, self.name, event, argument, condition, ordering)],
659
+ )
660
+
661
+ def mapping(self, event, attribute) -> None:
662
+ add_data(
663
+ self.dlg.db,
664
+ "EventMapping",
665
+ [(self.dlg.name, self.name, event, attribute)],
666
+ )
667
+
668
+ def condition(self, action, condition) -> None:
669
+ add_data(
670
+ self.dlg.db,
671
+ "ControlCondition",
672
+ [(self.dlg.name, self.name, action, condition)],
673
+ )
674
+
675
+
676
+ class RadioButtonGroup(Control):
677
+ def __init__(self, dlg, name, property) -> None:
678
+ self.dlg = dlg
679
+ self.name = name
680
+ self.property = property
681
+ self.index = 1
682
+
683
+ def add(self, name, x, y, w, h, text, value=None) -> None:
684
+ if value is None:
685
+ value = name
686
+ add_data(
687
+ self.dlg.db,
688
+ "RadioButton",
689
+ [(self.property, self.index, value, x, y, w, h, text, None)],
690
+ )
691
+ self.index += 1
692
+
693
+
694
+ class Dialog:
695
+ def __init__(
696
+ self, db, name, x, y, w, h, attr, title, first, default, cancel
697
+ ) -> None:
698
+ self.db = db
699
+ self.name = name
700
+ self.x, self.y, self.w, self.h = x, y, w, h
701
+ add_data(
702
+ db,
703
+ "Dialog",
704
+ [(name, x, y, w, h, attr, title, first, default, cancel)],
705
+ )
706
+
707
+ def control(
708
+ self, name, type, x, y, w, h, attr, prop, text, next, help
709
+ ) -> Control:
710
+ add_data(
711
+ self.db,
712
+ "Control",
713
+ [
714
+ (
715
+ self.name,
716
+ name,
717
+ type,
718
+ x,
719
+ y,
720
+ w,
721
+ h,
722
+ attr,
723
+ prop,
724
+ text,
725
+ next,
726
+ help,
727
+ )
728
+ ],
729
+ )
730
+ return Control(self, name)
731
+
732
+ def text(self, name, x, y, w, h, attr, text) -> Control:
733
+ return self.control(
734
+ name, "Text", x, y, w, h, attr, None, text, None, None
735
+ )
736
+
737
+ def bitmap(self, name, x, y, w, h, text) -> Control:
738
+ return self.control(
739
+ name, "Bitmap", x, y, w, h, 1, None, text, None, None
740
+ )
741
+
742
+ def line(self, name, x, y, w, h) -> Control:
743
+ return self.control(
744
+ name, "Line", x, y, w, h, 1, None, None, None, None
745
+ )
746
+
747
+ def pushbutton(self, name, x, y, w, h, attr, text, next) -> Control:
748
+ return self.control(
749
+ name, "PushButton", x, y, w, h, attr, None, text, next, None
750
+ )
751
+
752
+ def radiogroup(self, name, x, y, w, h, attr, prop, text, next) -> Control:
753
+ add_data(
754
+ self.db,
755
+ "Control",
756
+ [
757
+ (
758
+ self.name,
759
+ name,
760
+ "RadioButtonGroup",
761
+ x,
762
+ y,
763
+ w,
764
+ h,
765
+ attr,
766
+ prop,
767
+ text,
768
+ next,
769
+ None,
770
+ )
771
+ ],
772
+ )
773
+ return RadioButtonGroup(self, name, prop)
774
+
775
+ def checkbox(self, name, x, y, w, h, attr, prop, text, next) -> Control:
776
+ return self.control(
777
+ name, "CheckBox", x, y, w, h, attr, prop, text, next, None
778
+ )