-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathyaml2code.py
More file actions
4262 lines (3790 loc) · 215 KB
/
Copy pathyaml2code.py
File metadata and controls
4262 lines (3790 loc) · 215 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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
YAML to C++ Group Generator
Generates C++ Group module files from YAML form definitions.
"""
import sys
import subprocess
import argparse
import re
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
import datetime
from dataclasses import dataclass
def ensure_yaml():
try:
import yaml
return yaml
except ImportError:
print("pyyaml not found, attempting to install...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyyaml", "--break-system-packages"])
import yaml
return yaml
except Exception as e:
print(f"Failed to install pyyaml: {e}")
sys.exit(1)
yaml = ensure_yaml()
# A C++ numeric literal, optionally signed, optionally hex, optionally carrying an
# integer/float suffix (-1L, 0x10u, 3.0f, 123ULL, .5, 5.). Recognized so it can be
# emitted verbatim instead of being run through int()/float() (which chokes on the
# suffix) or quoted as a string.
_CPP_NUMERIC_LITERAL_RE = re.compile(r'^[+-]?(?:0[xX][0-9a-fA-F]+|\d+\.\d*|\.\d+|\d+)[uUlLfF]*$')
# A bare C++ identifier (wxNOT_FOUND, nullptr, MY_CONSTANT) -- as opposed to a
# quoted piece of text -- recognized so named constants can be emitted verbatim
# instead of being quoted as a string literal.
_CPP_IDENTIFIER_RE = re.compile(r'^[A-Za-z_]\w*$')
class CppGenerator:
"""
Generates C++23 module (.ixx) files from YAML form definitions: wxWidgets
Group/Page/WizardPage modules for `groups:`/`pages:`/`wizardpages:` sections.
See generate_from_yaml() for the single-parse entry point.
`tables:` sections are no longer generated into C++ at all -- db::TableLoader
(Libs/Core/src/Table.cpp, hand-written) parses the same `tables:`/`relationships:`
YAML directly at runtime to CREATE TABLE the schema and CREATE VIEW a
"<table>_detail" joined view per table with relationships. Reads/writes go through
the generic db::RowSet (Libs/Core/src/RowSet.ixx) -- no per-table generated struct.
"""
debugging = False
quiet: bool = False
sizer_info = False
target_type: str = "groups"
target_class: str = "Group"
app_target: str = "pass_the_name_of_your_app_target_to_yaml2ui"
now: str = datetime.datetime.now().date().isoformat() + " " + datetime.datetime.now().time().strftime("%H:%M:%S")
next_PageType: int = 1000
export_var: str = "GFX_EXPORT"
impl_dir: Optional[Path] = None
@dataclass(frozen=True)
class SizerProperties:
position: Optional[Tuple[int, int]]
span: Optional[Tuple[int, int]]
rows: int
cols: int
kind: str = "flexgrid"
proportion: int = 0
growable_rows: List[int] = None
growable_cols: List[int] = None
col_width: int = 0
row_height: int = 0
hgap: int = 0
vgap: int = 0
flag: int = 0 # e.g. wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL
border: int = 0
min_size: Optional[Tuple[int, int]] = None
size: Optional[Tuple[int, int]] = None
def __init__(self):
self.control_value_mapping = {
'Activity': 'hs::NullValue',
'BitmapToggleButton': 'bool',
'Button': 'std::string',
'CheckBox': 'bool',
'Choice': 'ID::Type',
'Combo': 'ID::Type',
'ComplexComboBox': 'ID::Type',
'DatePicker': 'wxDateTime',
'ELBox': 'WhoCared',
'Gauge': 'int',
'GridCtrl': 'dunno',
'Group': 'std::string',
'InfoBar': 'hs::NullType',
'IntTextCtrl': 'int',
'MarkupText': 'std::string',
'MaskedEdit': 'std::string',
'NotesCtrl': 'std::string',
'RadioBox': 'int',
'RadioButton': 'bool',
'ScrollBar': 'int',
'SearchBar': 'std::string',
'SearchToolBar': 'std::string',
'Slider': 'int',
'SpinCtrl': 'int',
'SpinCtrlDouble': 'double',
'StaticBox': 'std::string',
'StaticLine': 'hs::NullValue',
'StaticText': 'std::string',
'TextCtrl': 'std::string',
'ToggleButton': 'bool',
'TreeCtrl': 'hs::NullValue',
}
self.control_default_mapping = {
'Activity': 'hs::NullValue::Null',
'BitmapToggleButton': 'false',
'Button': '""',
'CheckBox': 'false',
'Choice': 'ID::Null',
'Combo': 'ID::Null',
'ComplexComboBox': 'ID::Null',
'DatePicker': 'nulldatetime',
'ELBox': 'WhoCared',
'Gauge': '0',
'GridCtrl': 'dunno',
'Group': '""',
'InfoBar': 'Null',
'IntTextCtrl': '0',
'MarkupText': '""',
'MaskedEdit': '""',
'NotesCtrl': '""',
'RadioBox': '0',
'RadioButton': 'false',
'ScrollBar': '0',
'SearchBar': '""',
'SearchToolBar': '""',
'Slider': '0',
'SpinCtrl': '0',
'SpinCtrlDouble': '0',
'StaticBox': '""',
'StaticLine': 'hs::NullValue::Null',
'StaticText': '""',
'TextCtrl': '""',
'ToggleButton': 'false',
'TreeCtrl': 'hs::NullValue::Null',
}
self.control_contains_value_mapping = {
'Activity': False,
'BitmapButton': False,
'BitmapToggleButton': False,
'Button': False,
'CheckBox': True,
'Choice': True,
'Combo': True,
'ComplexComboBox': True,
'DateCtrl': True,
'DatePicker': True,
'ELBox': True,
'Gauge': True,
'GridCtrl': True,
'Group': False,
'InfoBar': False,
'IntTextCtrl': True,
'MarkupText': True,
'MaskedEdit': True,
'NotesCtrl': True,
'OutlineText': False,
'Page': False,
'RadioBox': True,
'RadioButton': True,
'ScrollBar': True,
'SearchBar': False,
'SearchToolBar': False,
'Slider': True,
'SpinCtrl': True,
'SpinCtrlDouble': True,
'StaticBox': False,
'StaticLine': False,
'StaticText': True,
'TextCtrl': True,
'ToggleButton': False,
'TreeCtrl': False,
}
# Controls that hold a set of rows rather than one scalar value. The table/field
# binding machinery (initFromField/where()/commit() on Ctrl) is built around a
# single-row scalar and doesn't apply to these - see collect_refresh_targets().
self.multi_row_control_classes = {
'ListCtrl',
'ELBox',
}
# self.control_to_module = {
# 'Activity': 'Activity',
# 'Button': 'Button',
# 'ToggleButton': 'Button',
# 'BitmapToggleButton': 'Button',
# 'CheckBox': 'CheckBox',
# 'Choice': 'Choice',
# 'IntChoice': 'Choice',
# 'ComboBox': 'Combo',
# 'IntComboBox': 'Combo',
# 'ComplexComboBox': 'Ctrl.ComplexComboBox',
# 'DatePicker': 'Date',
# 'DateCtrl': 'Date',
# 'ELBox': 'EditableListBox',
# 'Gauge': 'Gauge',
# 'GridCtrl': 'Grid',
# 'AuiInfoBar': 'InfoBar.Aui',
# 'InfoBar': 'InfoBar',
# 'MarkupText': 'Markup',
# 'MaskedEdit': 'MaskedEdit',
# 'OutlineText': 'OutlineText',
# 'RadioButton': 'RadioButton',
# 'RadioBox': 'RadioButton',
# 'ScrollBar': 'ScrollBar',
# 'SearchBar': 'Search.Bar',
# 'SearchToolBar': 'SearchToolBar',
# 'Slider': 'Slider',
# 'SpinCtrl': 'Spin',
# 'SpinCtrlDouble': 'Spin',
# 'StaticBox': 'StaticBox',
# 'StaticLine': 'StaticLine',
# 'StaticText': 'StaticText',
# 'TextCtrl': 'TextCtrl',
# 'Toolbar': 'Toolbar',
# 'TreeCtrl': 'Tree',
# 'UserBar': 'User.Bar'
# }
self.validator_class_mapping = {
'CapsValidator': 'CapsValidator',
'GenericValidator': 'GenericValidator',
'ComboLikeValidator': 'ComboLikeValidator'
}
# Validator to module mapping
self.validator_to_module = {
'CapsValidator': 'TextCtrl',
'CapsValidatorBase': 'GenericValidator',
'ComboLikeCapsValidator': 'GenericValidator',
'ComboLikeValidator': 'GenericValidator',
'ComplexComboBoxValidator': 'Ctrl.ComplexComboBox',
'CurrencyValidator': 'TextCtrl',
'DateValidator': 'Date',
'DomainValidator': 'GenericValidator',
'ELBoxValidator': 'EditableListBox',
'EmailValidator': 'GenericValidator',
'GenericValidator': 'GenericValidator',
'MaskValidator': 'MaskedEdit',
'PhoneValidator': 'MaskedEdit',
'TextFilterValidator': 'TextCtrl'
}
# Size to wxSize mapping
self.size_mapping = {
'sizeCtrlButton': 'sizeCtrlButton',
'sizeCtrlCheckBox': 'sizeCtrlCheckBox',
'sizeCtrlELB': 'sizeCtrlELB',
'sizeCtrlLarge': 'sizeCtrlLarge',
'sizeCtrlMedium': 'sizeCtrlMedium',
'sizeCtrlMediumLarge': 'sizeCtrlMediumLarge',
'sizeCtrlSmall': 'sizeCtrlSmall',
'sizeCtrlSpin': 'sizeCtrlSpin',
'sizeNotes': 'sizeNotes',
'sizeLabel': 'sizeLabel',
'sizeLabelLarge': 'sizeLabelLarge',
'sizeLabelMedium': 'sizeLabelMedium',
'sizeLabelSmall': 'sizeLabelSmall',
'sizeGroup': 'wxDefaultSize',
'sizePage': 'wxDefaultSize',
'sizeWizardPage': 'wxDefaultSize'
}
self.event_mapping = {
'EVT_BUTTON': 'wxEVT_BUTTON',
'EVT_TOGGLEBUTTON': 'wxEVT_TOGGLEBUTTON',
'EVT_CHECKBOX': 'wxEVT_CHECKBOX',
'EVT_CHOICE': 'wxEVT_CHOICE',
'EVT_COMBOBOX_CLOSEUP': 'wxEVT_COMBOBOX_CLOSEUP',
'EVT_COMBOBOX_DROPDOWN': 'wxEVT_COMBOBOX_DROPDOWN',
'EVT_COMBOBOX': 'wxEVT_COMBOBOX',
'EVT_DATE_CHANGED': 'wxEVT_DATE_CHANGED',
'EVT_LISTBOX': 'wxEVT_LISTBOX',
'EVT_LISTBOX_DCLICK': 'wxEVT_LISTBOX_DCLICK',
'EVT_LIST_BEGIN_LABEL_EDIT': 'wxEVT_LIST_BEGIN_LABEL_EDIT',
'EVT_LIST_BEGIN_RDRAG': 'wxEVT_LIST_BEGIN_RDRAG',
'EVT_LIST_CACHE_HINT': 'wxEVT_LIST_CACHE_HINT',
'EVT_LIST_COL_BEGIN_DRAG': 'wxEVT_LIST_COL_BEGIN_DRAG',
'EVT_LIST_COL_CLICK': 'wxEVT_LIST_COL_CLICK',
'EVT_LIST_COL_DRAGGING': 'wxEVT_LIST_COL_DRAGGING',
'EVT_LIST_COL_END_DRAG': 'wxEVT_LIST_COL_END_DRAG',
'EVT_LIST_COL_RIGHT_CLICK': 'wxEVT_LIST_COL_RIGHT_CLICK',
'EVT_LIST_DELETE_ALL_ITEMS': 'wxEVT_LIST_DELETE_ALL_ITEMS',
'EVT_LIST_DELETE_ITEM': 'wxEVT_LIST_DELETE_ITEM',
'EVT_LIST_END_LABEL_EDIT': 'wxEVT_LIST_END_LABEL_EDIT',
'EVT_LIST_INSERT_ITEM': 'wxEVT_LIST_INSERT_ITEM',
'EVT_LIST_ITEM_ACTIVATED': 'wxEVT_LIST_ITEM_ACTIVATED',
'EVT_LIST_ITEM_CHECKED': 'wxEVT_LIST_ITEM_CHECKED',
'EVT_LIST_ITEM_DESELECTED': 'wxEVT_LIST_ITEM_DESELECTED',
'EVT_LIST_ITEM_FOCUSED': 'wxEVT_LIST_ITEM_FOCUSED',
'EVT_LIST_ITEM_MIDDLE_CLICK': 'wxEVT_LIST_ITEM_MIDDLE_CLICK',
'EVT_LIST_ITEM_RIGHT_CLICK': 'wxEVT_LIST_ITEM_RIGHT_CLICK',
'EVT_LIST_ITEM_SELECTED': 'wxEVT_LIST_ITEM_SELECTED',
'EVT_LIST_ITEM_UNCHECKED': 'wxEVT_LIST_ITEM_UNCHECKED',
'EVT_LIST_KEY_DOWN': 'wxEVT_LIST_KEY_DOWN',
'EVT_RADIOBOX': 'wxEVT_RADIOBOX',
'EVT_RADIOBUTTON': 'wxEVT_RADIOBUTTON',
'EVT_SCROLL_TOP': 'wxEVT_SCROLL_TOP',
'EVT_SCROLL_BOTTOM': 'wxEVT_SCROLL_BOTTOM',
'EVT_SCROLL_LINEUP': 'wxEVT_SCROLL_LINEUP',
'EVT_SCROLL_LINEDOWN': 'wxEVT_SCROLL_LINEDOWN',
'EVT_SCROLL_PAGEUP': 'wxEVT_SCROLL_PAGEUP',
'EVT_SCROLL_PAGEDOWN': 'wxEVT_SCROLL_PAGEDOWN',
'EVT_SCROLL_THUMBTRACK': 'wxEVT_SCROLL_THUMBTRACK',
'EVT_SCROLL_THUMBRELEASE': 'wxEVT_SCROLL_THUMBRELEASE',
'EVT_SCROLL_CHANGED': 'wxEVT_SCROLL_CHANGED',
'EVT_SLIDER': 'wxEVT_SLIDER',
'EVT_SPIN': 'wxEVT_SPIN',
'EVT_SPINCTRL': 'wxEVT_SPINCTRL',
'EVT_SPINCTRLDOUBLE': 'wxEVT_SPINCTRLDOUBLE',
'EVT_TEXT': 'wxEVT_TEXT',
'EVT_TEXT_ENTER': 'wxEVT_TEXT_ENTER',
'EVT_TEXT_URL': 'wxEVT_TEXT_URL',
'EVT_TEXT_MAXLEN': 'wxEVT_TEXT_MAXLEN',
'EVT_TREE_BEGIN_DRAG': 'wxEVT_TREE_BEGIN_DRAG',
'EVT_TREE_BEGIN_LABEL_EDIT': 'wxEVT_TREE_BEGIN_LABEL_EDIT',
'EVT_TREE_BEGIN_RDRAG': 'wxEVT_TREE_BEGIN_RDRAG',
'EVT_TREE_DELETE_ITEM': 'wxEVT_TREE_DELETE_ITEM',
'EVT_TREE_END_DRAG': 'wxEVT_TREE_END_DRAG',
'EVT_TREE_END_LABEL_EDIT': 'wxEVT_TREE_END_LABEL_EDIT',
'EVT_TREE_GET_INFO': 'wxEVT_TREE_GET_INFO',
'EVT_TREE_ITEM_GETTOOLTIP': 'wxEVT_TREE_ITEM_GETTOOLTIP',
'EVT_TREE_ITEM_ACTIVATED': 'wxEVT_TREE_ITEM_ACTIVATED',
'EVT_TREE_ITEM_COLLAPSED': 'wxEVT_TREE_ITEM_COLLAPSED',
'EVT_TREE_ITEM_COLLAPSING': 'wxEVT_TREE_ITEM_COLLAPSING',
'EVT_TREE_ITEM_EXPANDED': 'wxEVT_TREE_ITEM_EXPANDED',
'EVT_TREE_ITEM_EXPANDING': 'wxEVT_TREE_ITEM_EXPANDING',
'EVT_TREE_ITEM_MENU': 'wxEVT_TREE_ITEM_MENU',
'EVT_TREE_ITEM_MIDDLE_CLICK': 'wxEVT_TREE_ITEM_MIDDLE_CLICK',
'EVT_TREE_ITEM_RIGHT_CLICK': 'wxEVT_TREE_ITEM_RIGHT_CLICK',
'EVT_TREE_KEY_DOWN': 'wxEVT_TREE_KEY_DOWN',
'EVT_TREE_SEL_CHANGED': 'wxEVT_TREE_SEL_CHANGED',
'EVT_TREE_SEL_CHANGING': 'wxEVT_TREE_SEL_CHANGING',
'EVT_TREE_SET_INFO': 'wxEVT_TREE_SET_INFO',
'EVT_TREE_STATE_IMAGE_CLICK': 'wxEVT_TREE_STATE_IMAGE_CLICK',
'EVT_SET_FOCUS': 'wxEVT_SET_FOCUS',
'EVT_KILL_FOCUS': 'wxEVT_KILL_FOCUS',
'EVT_MENU': 'wxEVT_MENU',
'EVT_UPDATE_UI': 'wxEVT_UPDATE_UI',
'EVT_TOOL': 'wxEVT_TOOL',
'EVT_TOOL_RCLICKED': 'wxEVT_TOOL_RCLICKED',
'EVT_SIZE': 'wxEVT_SIZE',
'EVT_MOVE': 'wxEVT_MOVE',
'EVT_PAINT': 'wxEVT_PAINT',
'EVT_IDLE': 'wxEVT_IDLE',
'EVT_TIMER': 'wxEVT_TIMER',
'EVT_KEY_DOWN': 'wxEVT_KEY_DOWN',
'EVT_KEY_UP': 'wxEVT_KEY_UP',
'EVT_CHAR': 'wxEVT_CHAR',
'EVT_CHAR_HOOK': 'wxEVT_CHAR_HOOK',
'EVT_LEFT_DOWN': 'wxEVT_LEFT_DOWN',
'EVT_LEFT_UP': 'wxEVT_LEFT_UP',
'EVT_LEFT_DCLICK': 'wxEVT_LEFT_DCLICK',
'EVT_MIDDLE_DOWN': 'wxEVT_MIDDLE_DOWN',
'EVT_MIDDLE_UP': 'wxEVT_MIDDLE_UP',
'EVT_MIDDLE_DCLICK': 'wxEVT_MIDDLE_DCLICK',
'EVT_RIGHT_DOWN': 'wxEVT_RIGHT_DOWN',
'EVT_RIGHT_UP': 'wxEVT_RIGHT_UP',
'EVT_RIGHT_DCLICK': 'wxEVT_RIGHT_DCLICK',
'EVT_MOTION': 'wxEVT_MOTION',
'EVT_ENTER_WINDOW': 'wxEVT_ENTER_WINDOW',
'EVT_LEAVE_WINDOW': 'wxEVT_LEAVE_WINDOW',
'EVT_MOUSEWHEEL': 'wxEVT_MOUSEWHEEL',
}
def be_quiet(self, _quiet: bool) -> None:
self.quiet = bool(_quiet)
def show_sizer_info(self, _show: bool) -> None:
self.sizer_info = bool(_show)
def _dbg(self, msg: str) -> None:
"""Verbose trace output, gated on the per-file 'debugging: true' YAML key
(see generate_from_yaml). Always to stderr so it never pollutes generated
code piped to stdout in single-file mode."""
if self.debugging:
print(f"[DEBUG] {msg}", file=sys.stderr)
def target(self, _targets: str) -> None:
t = _targets.lower()
if t == "groups" or t == "group":
self.target_class = "Group"
self.target_type = "groups"
elif t == "pages" or t == "page":
self.target_class = "Page"
self.target_type = "pages"
elif t == "wizardpages" or t == "wizardpage":
self.target_class = "WizardPage"
self.target_type = "wizardpages"
elif t == "wizard":
self.target_class = "Wizard"
self.target_type = "wizard"
elif t == "book":
self.target_class = "Book"
self.target_type = "book"
else:
raise ValueError(f"Unknown target '{_targets}'")
def generate_ui_module(self, target_name: str, class_def: Dict[str, Any], yaml_file: Path, top_verbatim: str,
output_dir: Optional[Path] = None) -> str:
"""Generate the complete C++ group/page/wizardpage module file (list-based schema)."""
self._dbg(f"generate_ui_module: '{target_name}' -> {self.target_class} "
f"(top-level keys: {list(class_def.keys()) if isinstance(class_def, dict) else class_def})")
allow = self._allowed_sets()
self._warn_unknown_keys(class_def, allow["class_def"], f"control class_def '{target_name}'", yaml_file)
variables_block = self.extract_variables_block(class_def, yaml_file)
if variables_block:
self._dbg(f"'{target_name}': variables block: {list(variables_block.keys())}")
else:
self._dbg(f"'{target_name}': no 'variables' block")
code: List[str] = []
code.append('module;')
code.append('//')
# Use YAML file modification time for deterministic headers (prevents needless rebuilds)
try:
_mt = datetime.datetime.fromtimestamp(yaml_file.stat().st_mtime)
_mts = _mt.isoformat(sep=' ', timespec='seconds')
except Exception:
_mts = 'unknown'
code.append(f'// Auto-generated from')
code.append(f'// {yaml_file} (mtime: {_mts})')
code.append('')
code.append('// Make any changes there. This file will be overwritten.')
code.append('')
code.append('#include "Core/Core.h"')
code.append('#include "Core/CoreData.h"')
code.append('#include "Core/Util.h"')
code.append('')
# wx/wx.h omitted intentionally: including it in every generated module's
# global fragment multiplies its SLoc entries by the number of BMIs, exhausting
# Clang's 2.1 GB source-location budget. wx types (wxWindowIDRef, etc.) are
# reachable via `import SplitterPage;` in the module body, so they don't need
# to be re-declared here. See commit 8c24e32 for the Windows precedent.
code.append('#include "Gfx/gfx_export.h"')
code.append('#include "Gfx/WidgetsFwd.h"')
code.append('')
code.append(f'#include "{self.app_target}/Sizes.h"')
code.append('')
code.append('#include <unordered_set>')
for directive in self.collect_variable_includes(variables_block):
code.append(f'#include {directive}')
code.append('')
layout_key = target_name
#
# if ':' not in target_name:
# layout_category = "GeneratorSource"
# layout_key = target_name
# else:
# layout_category, layout_key = target_name.split(':', 1)
# target_name = layout_key
pascal_name = self.to_pascal_case(target_name)
class_name = f"{pascal_name}{self.target_class}"
# Elements are a list in the new schema
elements = class_def.get('elements', [])
if not isinstance(elements, list):
self._dbg(f"'{target_name}': 'elements' is a {type(elements).__name__}, not a list - treating as empty")
elements = []
else:
self._dbg(f"'{target_name}': {len(elements)} element section(s)")
cpp_class = class_def.get("class_name") or self.to_pascal_case(target_name) + self.target_class
# Required imports
required_imports = self.get_required_imports(elements, yaml_file)
for mod in self.collect_variable_modules(variables_block):
if mod not in required_imports:
required_imports.append(mod)
# A book-container item (book: ... container: true) declares child pages to
# populate its nested Book with, via the same 'pages' key the book: category's
# container:false 'populate' flavour uses. Not a widget/element - just gather
# each child's module for import here; the actual new-Page-call lines are
# emitted further down, right after this class's own control-grid layout loads.
book_children = class_def.get('pages')
if isinstance(book_children, list):
self._dbg(f"'{target_name}': {len(book_children)} book child page(s) declared under 'pages'")
for child in book_children:
if isinstance(child, dict):
mod = child.get('module')
if isinstance(mod, str) and mod.strip() and mod.strip() not in required_imports:
required_imports.append(mod.strip())
if book_children and "Book" not in required_imports:
required_imports.append("Book")
else:
self._dbg(f"'{target_name}': no book child 'pages' declared")
# RecordSet-refresh scaffolding (recordset: block) — pages and groups only
recordset = self.extract_recordset(target_name, class_def, yaml_file) \
if self.target_type in ("pages", "groups") else None
if recordset and "DB.RowSet" not in required_imports:
required_imports.append("DB.RowSet")
self._dbg(f"'{target_name}': recordset = {recordset}" if recordset
else f"'{target_name}': no 'recordset:' block (or not applicable to {self.target_type})")
module_name = self.extract_module(self.to_pascal_case(target_name), class_def, cpp_class, yaml_file)
module_list = self.extract_needed_modules(self.to_pascal_case(target_name), class_def, cpp_class, yaml_file)
export_module = self.extract_export_module(self.to_pascal_case(target_name), class_def, cpp_class, yaml_file)
if not module_name is None and not module_name == export_module:
required_imports.append(module_name)
elif not module_list is None:
required_imports.extend(module_list)
# Determine base class (Page/Group/WizardPage). A page with a recordset: block and no
# explicit base_class (or an explicit "Page") gets RecordSetPage instead -- it owns the
# m_rs/reloadTable()/refreshFromCurrent()/onSetActive() machinery this generator used to
# emit per-page, closing the same "accident of omission" gap a settings-style page author
# forgetting to change base_class: by hand would otherwise reopen. SplitterPage already
# derives from RecordSetPage, so an explicit "SplitterPage" is left unchanged.
_, top_base_class = self.extract_control_class(target_name, class_def, yaml_file)
implies_record_set_page = (self.target_type == "pages" and recordset is not None
and top_base_class == "Page")
if implies_record_set_page:
top_base_class = "RecordSetPage"
if "RecordSetPage" not in required_imports:
required_imports.append("RecordSetPage")
if "Page" in required_imports:
required_imports.remove("Page")
true_imports = []
for module in required_imports:
if not module == export_module:
true_imports.append(module)
else:
print(f'export_module {export_module} cannot be imported: {target_name} {yaml_file}')
self._dbg(f"'{target_name}': import '{module}' DROPPED (same as this module's own export_module)")
self._dbg(f"'{target_name}': resolved imports: {true_imports}")
imports_formatted = '\n'.join(f"import {module};" for module in true_imports)
code.append(f'export module {export_module};')
code.append('')
code.append(f'{imports_formatted}')
code.append('')
code.append('export namespace PageType {')
code.append(f"const Type {cpp_class}({self.next_PageType});")
code.append('}')
code.append('')
ns = class_def.get("namespace", "wx")
layout_class_name = class_def.get("layout", self.to_pascal_case(target_name) + self.target_class)
if ':' not in layout_class_name:
layout_category = "GeneratorSource"
else:
layout_category, layout_class_name = layout_class_name.split(':', 1)
code.append(f"namespace {ns} {{")
code.append("")
self.next_PageType += 1
# Placement 1: top-level verbatim (inside namespace, before class)
if isinstance(top_verbatim, str) and top_verbatim.strip():
for line in top_verbatim.rstrip().splitlines():
code.append(f"{line}")
# alt_data_source: emit each control's generated DBSource policy struct (namespace
# scope, non-exported) before the class that names it as a template argument. Backed
# by the generic db::Row (DB.RowSet) -- value_field is assumed integer-typed (id/FK),
# matching every real usage (a lookup table's id, populating an ID::Type-valued control).
for var, tag, alt_ds, data_type in self.collect_alt_data_sources(elements, yaml_file):
struct_name = f"{tag}DBSource"
value_get = f'r.get<int>("{alt_ds["value_field"]}")'
value_expr = f"ID::Type({value_get})" if data_type == "ID::Type" else value_get
code.append(f"struct {struct_name} {{")
code.append(f' static auto table() -> std::string {{ return "{alt_ds["table"]}"; }}')
code.append(f' static auto displayText(const db::Row &r) -> std::string {{ return r.get<std::string>("{alt_ds["display_field"]}"); }}')
code.append(f" static auto value(const db::Row &r) -> {data_type} {{ return {value_expr}; }}")
code.append(f" static constexpr auto includeBlank() -> bool {{ return {'true' if alt_ds['include_blank'] else 'false'}; }}")
code.append(f' static auto blankText() -> std::string {{ return "{alt_ds["blank_text"]}"; }}')
# textField()/locked() are only required by ELBoxDBSourceFor (ELBox's row-write-back
# concept, Gfx/src/ctrls/ELBox.ixx) -- harmless additions for Choice/Combo/ListBox,
# which only require DBSourceFor and never reference them.
code.append(f' static constexpr auto textField() -> std::string_view {{ return "{alt_ds["display_field"]}"; }}')
code.append(f' static auto locked(const db::Row &r) -> bool {{ return r.get<hs_bool>("bLocked").get(); }}')
code.append("};")
code.append("")
code.append(f"export class {self.export_var} {cpp_class} : public {top_base_class} {{")
code.append(" std::filesystem::path layoutPath;")
code.append(" std::string layoutKey;")
# The generated ctor parameter is always named 'args' (see ctor signature
# emission below), regardless of whether this page/group declares its own
# class_args.args_in factory. Children that don't supply their own
# 'args:' block must receive that same parameter unaltered, not a
# hardcoded nullanymap — so this is set unconditionally rather than only when
# a factory is built.
parent_args_var_for_children: Optional[str] = "args"
page_extract_inside_entries: List[Tuple[str, str, bool, str, str, Any]] = []
# Class-level (class_args:) args map and extract_inside entries at top of ctor
page_args_factory: Optional[str] = None
page_args_var: Optional[str] = None
merge_helper_name: Optional[str] = None
has_class_args = False
packed_args_in = self._emit_page_scope_args(target_name, class_def, yaml_file)
if packed_args_in is not None:
emplace_lines, page_args_var, page_extract_inside_entries = packed_args_in
if emplace_lines:
has_class_args = True
# Clang 21 previously crashed (infinite recursion in getTypeInfoImpl) on
# a static anymap variable brace-aggregate-initialized with std::any
# values inside a C++ module — class-level inline or function-local,
# didn't matter, as long as the whole map came from one initializer_list
# expression. Building the map with sequential .emplace() calls instead
# (one std::any construction per statement, never inside a brace-init
# list) avoids that expression shape entirely. The same reasoning rules
# out a static anymap *member* for class_args' own defaults -- it's
# regenerated by this same factory function, called fresh at every merge
# site below (merge() drains its source, so reusing one instance across
# calls would silently empty it out after the first construction).
page_args_factory = f"{page_args_var}Default"
code.append(f" static auto {page_args_factory}() -> anymap {{")
code.append(" anymap m;")
code.extend(emplace_lines)
code.append(" return m;")
code.append(" }")
# param calls in the body use 'args' (the ctor parameter); the
# factory function above only supplies the ctor's default argument.
# merge() returns void, so it can't sit inline as the anymap argument to
# the base-class constructor call below -- this helper mutates the
# caller's own 'args' local in place (by reference) and hands back a
# reference to it, so later body code (extract_inside's param() calls)
# sees the filled-in class defaults too.
merge_helper_name = f"{page_args_var}Merged"
code.append(f" static auto {merge_helper_name}(anymap &a) -> anymap & {{")
code.append(f" a.merge({page_args_factory}());")
code.append(" return a;")
code.append(" }")
# Impl dir/stub path determined early: both the on_set_active/on_kill_active
# overrides and 'functions:' entries may need to be stubbed out here.
if self.impl_dir is not None:
impl_dir = self.impl_dir
elif output_dir is not None:
impl_dir = output_dir / "impl"
else:
impl_dir = yaml_file.parent / "impl"
stub_path = impl_dir / f"{cpp_class}_impl.cpp"
kill_declared, on_kill_active = self.extract_group_method_body('on_kill_active', target_name, class_def, yaml_file)
set_declared, on_set_active = self.extract_group_method_body('on_set_active', target_name, class_def, yaml_file)
event_declared, on_event = self.extract_group_method_body('on_event', target_name, class_def, yaml_file)
self._dbg(f"'{target_name}': on_kill_active declared={kill_declared} (has body={on_kill_active is not None}), "
f"on_set_active declared={set_declared} (has body={on_set_active is not None}), "
f"on_event declared={event_declared} (has body={on_event is not None})")
# refreshFromCurrent() always hands off to refreshEx() so hand-written
# tweaks to freshly-loaded field values have a stable, never-overwritten home.
refresh_ex_declared = recordset is not None
if kill_declared or set_declared or event_declared or refresh_ex_declared:
code.append("")
code.append("protected:")
code.append(" // OnKillActive/SetActive/onEvent overrides")
if kill_declared:
note = "" if on_kill_active is not None else f" // Implemented in {stub_path}"
code.append(f" auto onKillActive(bool autoDisable) -> void override;{note}")
if set_declared:
note = "" if on_set_active is not None else f" // Implemented in {stub_path}"
code.append(f" auto onSetActive(bool autoEnable) -> void override;{note}")
if event_declared:
note = "" if on_event is not None else f" // Implemented in {stub_path}"
code.append(f" auto onEvent(sig::RecordSetEvent event) -> void override;{note}")
if refresh_ex_declared:
# Pages: overrides RecordSetPage::refreshEx() (virtual, empty default).
# Groups: no common base owns a RowSet, so this is a plain (non-overriding)
# member function refreshFromCurrent(rec) itself calls directly.
refresh_ex_override = " override" if self.target_type == "pages" else ""
code.append(f" auto refreshEx(const db::Row *rec) -> void{refresh_ex_override}; // Implemented in {stub_path}")
# Declarations
control_decls = self.generate_control_declarations(elements, yaml_file)
self._dbg(f"'{target_name}': {len(control_decls)} member declaration line(s) generated"
if control_decls else f"'{target_name}': NO control declarations generated from 'elements'")
code.append("")
code.append('\n'.join(control_decls) if control_decls else ' // No elements defined')
# m_rs/m_moveHandle and the Move*-subscribe/suspend/unsubscribe ctor/dtor boilerplate
# that used to be generated here for every recordset page now live on RecordSetPage
# (Gfx/src/interface/book/RecordSetPage.ixx/.cpp) -- nothing to emit.
# Custom member variables (variables: block) — grouped under explicit access
# specifiers so placement here is independent of whatever access level the
# preceding declarations left the class in.
variable_access_groups = {'public': [], 'protected': [], 'private': []}
for var_name, var_def in variables_block.items():
variable_access_groups[var_def['access']].append(
self.format_variable_declaration(var_name, var_def, yaml_file))
def format_variable_access_block(access_name: str, decls: List[str]) -> str:
if not decls:
return ""
return f"\n{access_name}:\n" + '\n'.join(decls)
variables_public_block = format_variable_access_block('public', variable_access_groups['public'])
variables_protected_block = format_variable_access_block('protected', variable_access_groups['protected'])
variables_private_block = format_variable_access_block('private', variable_access_groups['private'])
if variables_public_block: code.append(variables_public_block)
if variables_protected_block: code.append(variables_protected_block)
if variables_private_block: code.append(variables_private_block)
# Functions (group/page level) inside class
functions_all = self._validate_functions(class_def.get('functions'))
access_groups = {'public': [], 'protected': [], 'private': []}
for fname, fdef in functions_all.items():
args = fdef['args']
ret = fdef['return']
body = fdef['body']
const_suffix = " const" if fdef['const'] else ""
static_prefix = "static " if fdef['static'] else ""
override_suffix = " override" if fdef['override'] else ""
noexcept_suffix = self._format_noexcept(fdef.get('noexcept', False))
if body is None:
fn_text = (
f" {static_prefix}auto {fname} ({args})"
f"{const_suffix}{noexcept_suffix} -> {ret}{override_suffix};"
f" // Implemented in {stub_path}"
)
else:
body = body.replace('\r\n', '\n').replace('\r', '\n')
body_lines = body.split('\n')
indented_body = '\n'.join(f" {line}" if line else "" for line in body_lines)
fn_text = (
f" {static_prefix}auto {fname} ({args}){const_suffix}{noexcept_suffix} -> {ret}{override_suffix} {{\n"
f"{indented_body}"
f" }}"
)
access_groups[fdef['access']].append(fn_text)
# Generated refresh scaffolding. Groups still get a full refreshFromCurrent(rec) (no
# common base to inherit one from -- Group doesn't own a RowSet). Pages now inherit a
# concrete refreshFromCurrent()/reloadTable() from RecordSetPage (null-check, refreshEx(),
# transferTheseToWindow(), the DB requery) -- only emit a bindRecordFields() override
# when this page actually has something page-specific to forward (direct bound controls
# and/or nested groups); a page with neither (the common case -- everything lives inside
# a Group) gets no override at all, relying on RecordSetPage's empty default. Records are
# always db::Row -- there's no per-table generated struct, so every field read goes
# through get<T>(name), always wrapped in optional<> so a NULL column never throws
# (wx::initFromField already handles the optional-empty case by leaving the control
# untouched).
if recordset:
bound_controls, group_members = self.collect_refresh_targets(elements, yaml_file)
if self.target_type == "pages":
if bound_controls or group_members:
bf: List[str] = [" auto bindRecordFields (const db::Row *rec) -> void override {"]
for var, fld, cpp_type in bound_controls:
bf.append(f' wx::initFromField({var}, rec->get<std::optional<{cpp_type}>>("{fld}"));')
bf.append(f' {var}->where("id = " + std::to_string(rec->get<int>("id")));')
for var in group_members:
bf.append(f" if constexpr (requires {{ {var}->refreshFromCurrent(rec); }})")
bf.append(f" {var}->refreshFromCurrent(rec);")
bf.append(" }")
access_groups['public'].append('\n'.join(bf))
if recordset.get('allow_add') is False:
av: List[str] = [" auto addValidationResult() -> db::RequestResult override {"]
av.append(' return db::RequestResult::veto("Adding a record is not permitted here.");')
av.append(" }")
access_groups['public'].append('\n'.join(av))
else: # groups: unchanged -- Group owns no RowSet of its own to inherit this from.
rfc: List[str] = []
rfc.append( " auto refreshFromCurrent (const db::Row *rec) -> void {")
rfc.append( " if (!rec)")
rfc.append( " return;")
for var, fld, cpp_type in bound_controls:
rfc.append(f' wx::initFromField({var}, rec->get<std::optional<{cpp_type}>>("{fld}"));')
# Retarget the control's commit() UPDATE at the current record
rfc.append(f' {var}->where("id = " + std::to_string(rec->get<int>("id")));')
for var in group_members:
# Guarded: a nested group without its own recordset: is skipped instead of
# breaking the build.
rfc.append(f" if constexpr (requires {{ {var}->refreshFromCurrent(rec); }})")
rfc.append(f" {var}->refreshFromCurrent(rec);")
rfc.append(" refreshEx(rec);")
# initFromField()/pushToCtrl() above only paint the raw ValueT (e.g. cents
# as a plain int) onto the native control; validators (e.g. CurrencyValidator's
# cents -> "$123.45" formatting) only run via transferToWindow(), so re-run it
# here or freshly-displayed records show unformatted raw values until the user
# starts editing (which is the only other place transferToWindow() is invoked).
rfc.append(" ICtrl::transferTheseToWindow(controlMap());")
rfc.append(" }")
access_groups['public'].append('\n'.join(rfc))
def format_access_block(access_name: str, fns: List[str]) -> str:
if not fns:
return ""
return f"\n{access_name}:\n" + '\n'.join(fns)
public_access_block = format_access_block('public', access_groups['public'])
protected_access_block = format_access_block('protected', access_groups['protected'])
private_access_block = format_access_block('private', access_groups['private'])
if public_access_block: code.append(public_access_block)
if protected_access_block: code.append(protected_access_block)
if private_access_block: code.append(private_access_block)
code.append("")
code.append("public:")
# RecordSetPage's own dtor unsubscribes m_moveHandle/etc. now -- every generated page
# (recordset or not) can default this.
code.append(f" ~{cpp_class}() override = default;")
code.append("")
# Constructor signature and base ctor call.
# The ctor parameter itself (named 'args') is what param() reads from in the
# body (to stay non-circular) and what gets forwarded unaltered to children;
# the default *value* for that parameter comes from the emplace-based factory
# function when args_in triplets were declared, else the empty nullanymap.
# When this class declares class_args, 'args' must be a by-value parameter
# (not const anymap&) so {merge_helper_name}(args) -- which mutates it via
# unordered_map::merge -- can be called on it; classes without class_args keep
# the original const-ref parameter unchanged.
default_args_expr = f"{page_args_factory}()" if page_args_factory else "nullanymap"
args_param_type = "anymap " if has_class_args else "const anymap &"
value_default = "PageType::Null" if top_base_class == "Page" else "std::string{}"
pad1: str = " " * len(f" explicit {cpp_class} ( ")
if self.target_type == "pages":
code.append(f" explicit {cpp_class} ( Book *book, ")
code.append(f"{pad1}wxWindowIDRef id, ")
code.append(f"{pad1}const std::string& name,")
code.append(f"{pad1}PageType::Type type = PageType::{cpp_class},")
code.append(f"{pad1}int imageIndex = -1,")
code.append(f"{pad1}{args_param_type}args = {default_args_expr})")
args_expr = f"{merge_helper_name}(args)" if has_class_args else "args"
# RecordSetPage/SplitterPage both take (..., table, orderBy, imageIndex, args, ...) --
# table/orderBy are mandatory positional parameters on both (no default value), so
# this branch must always supply them as literals once top_base_class is either one,
# even when recordset: omits 'table:' -- that's a *table-less coordinator page* (see
# RecordSetPage.ixx's class comment), not "generate the plain-Page ctor call" below;
# passing "" for both leaves reloadTable() a permanent no-op and refreshFromCurrent()
# always calling refreshEx() instead of gating on a current record.
if recordset is not None and top_base_class in ("RecordSetPage", "SplitterPage"):
tbl_lit = recordset.get('table') or ''
ob_lit = recordset.get('order_by') or ''
code.append(f' : {top_base_class} (book, id, name, type, "{tbl_lit}", "{ob_lit}", imageIndex, {args_expr}) {{')
else:
code.append(f" : {top_base_class} (book, id, name, type, imageIndex, {args_expr}) {{")
else:
code.append(f" explicit {cpp_class} ( UICreateFlags cflags, ")
code.append(f"{pad1}std::string name, ")
code.append(f"{pad1}wxWindow *pParent, ")
code.append(f"{pad1}value_t value = {value_default},")
code.append(f"{pad1}{args_param_type}args = {default_args_expr},")
code.append(f"{pad1}long style = 0)")
if has_class_args:
code.append(f" : {top_base_class} (cflags, name, pParent, value, {merge_helper_name}(args), style) {{")
else:
code.append(f" : {top_base_class} (cflags, name, pParent, value, args, style) {{")
# class_args: feed the Interface-level creationArgs() storage too (fresh
# factory call, independent of the merge above) so post-construction reads of
# creationArgs() also see the filled-in class defaults. Interface:: is
# explicitly qualified because Group also inherits mergeWithCreationArgs from
# Ctrl (via StaticBox), making an unqualified call ambiguous there.
if has_class_args:
code.append(f" this->Interface::mergeWithCreationArgs({page_args_factory}());")
# class_args.extract_inside at ctor top
if page_extract_inside_entries:
for var_name, ty, no_auto, map_name, entry_name, default in page_extract_inside_entries:
lit = self._resolve_default_literal(default, ty, yaml_file,
f"class_args.extract_inside.'{entry_name}'")
prefix = "" if no_auto else "auto "
code.append(f' {prefix}{var_name} = param({map_name}, "{entry_name}", {lit});')
# code.append("")
# Layout boilerplate
code.append(
f' layoutPath = Util::getInstance().resourceName(UIType::{layout_category}, "{layout_class_name}", false, nullptr);')
code.append(
f' ASSERT_MSG(!layoutPath.empty(), "Couldn\'t find layout resource \'{layout_class_name}\'");')
code.append(f' layoutKey = "{layout_key}";')
code.append("")
# # Page-level sizer properties. Needed before any placement calls.
# if self.sizer_info:
# # Get sizer information
# sizer_def = class_def.get('sizer')
# if sizer_def:
# sizer_properties: CppGenerator.SizerProperties = self.extract_sizer(sizer_def)
# code.append(f' /*')
# code.append(f' * Sizer information for {self.target_class}:')
# code.append(f' *')
# code.append(f' * border : {sizer_properties.border}')
# code.append(f' * col_width : {sizer_properties.col_width}')
# code.append(f' * cols : {sizer_properties.cols}')
# code.append(f' * flag : {sizer_properties.flag}')
# code.append(f' * growable_cols : {sizer_properties.growable_cols}')
# code.append(f' * growable_rows : {sizer_properties.growable_rows}')
# code.append(f' * hgap : {sizer_properties.hgap}')
# code.append(f' * kind : {sizer_properties.kind}')
# code.append(f' * min_size : {sizer_properties.min_size}')
# code.append(f' * position : {sizer_properties.position}')
# code.append(f' * proportion : {sizer_properties.proportion}')
# code.append(f' * row_height : {sizer_properties.row_height}')
# code.append(f' * rows : {sizer_properties.rows}')
# code.append(f' * size : {sizer_properties.size}')
# code.append(f' * span : {sizer_properties.span}')
# code.append(f' * vgap : {sizer_properties.vgap}')
# code.append(f' */')
# code.append(f'')
# Creation code for list-based elements
creation_code, target_parent = self.generate_control_creation(target_name, elements, layout_class_name,
yaml_file,
parent_args_var_for_children)
self._dbg(f"'{target_name}': generate_control_creation -> {len(creation_code)} line(s), "
f"target_parent='{target_parent}'")
if creation_code:
code.append(f' auto targetParent = {target_parent};')
code.append('')
code.append('\n'.join(creation_code))
else:
self._dbg(f"'{target_name}': NO control creation code produced - class body will have an empty ctor")
code.append(' // No control creation code\n')
# RecordSetPage's own ctor subscribes m_moveHandle (refresh on Move*, suspended until
# onSetActive() resumes it) -- nothing to emit here anymore.
# Placement: finally (before loadLayout). Spliced in here, rather than at the
# end of the ctor, so a finally block's effects (state/widgets it sets up) are
# already in place by the time loadLayout's resolution pass runs, instead of
# only existing after layout has already completed.
finally_block = self._extract_finally_begin(class_def)
if isinstance(finally_block, str) and finally_block.strip():
for line in finally_block.rstrip().splitlines():
code.append(f" {line}")
code.append(
' VERIFY_MSG(this->loadLayout(layoutPath, layoutKey), "Error loading layout resource " + layoutPath.string());')
if self.target_type == 'wizardpages':
code.append(" GetPageSizer().Add(&grid(), 1, wxALL | wxGROW);")
elif self.target_type == 'pages':
if isinstance(book_children, list) and book_children:
if not isinstance(top_base_class, str) or "PageContainer" not in top_base_class:
print(f"Warning: '{target_name}' declares 'pages' (book children) but base_class "
f"'{top_base_class}' does not derive from PageContainer; book() won't exist "
f"{yaml_file}", file=sys.stderr)
code.append(" load();")
code.extend(self._generate_book_child_calls(target_name, book_children, "this->book()", yaml_file,
parent_args_var="args"))
# Placement: sizer fit/freeze (end of ctor) - after loadLayout and any book
# children, so controls added by either are accounted for in the fit instead
# of being tacked onto an already-sized page.
if self.target_type == 'wizardpages':
code.append(' SetSizerAndFit(&GetPageSizer(), true);')
elif self.target_type == 'pages':
code.append(' if (getForm())')
code.append(' getForm()->SetSizerAndFit(&grid(), true);')
code.append(" }")
code.append("};")
if on_kill_active is not None or on_set_active is not None or on_event is not None:
code.append("")
if on_kill_active is not None: