forked from wxGlade/wxGlade
-
Notifications
You must be signed in to change notification settings - Fork 0
/
edit_base.py
945 lines (832 loc) · 39.9 KB
/
edit_base.py
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
import wx
import new_properties as np
import common, misc, compat, clipboard, config
MANAGED_PROPERTIES = ["span", "proportion", "border", "flag"]
if config.debugging:
class _UniqueList(list):
def append(self, obj):
if obj in self and obj is not None:
raise AssertionError("Element already in list")
list.append(self, obj)
def insert(self, index, obj):
if obj in self and obj is not None:
raise AssertionError("Element already in list")
list.insert(self, index, obj)
def __setitem__(self, index, obj):
if obj in self and obj is not None:
if obj is self[index]: return
raise AssertionError("Element already in list")
list.__setitem__(self, index, obj)
else:
_UniqueList = list
class EditBase(np.PropertyOwner):
IS_TOPLEVEL = IS_SLOT = IS_SIZER = IS_WINDOW = IS_ROOT = IS_TOPLEVEL_WINDOW = IS_CONTAINER = False
IS_CLASS = None # dynamically set during code generation if a class is generated for this item
# usually this one is fixed, but EditPanel/EditToplevelPanel will overwrite it depending on the "scrollable" property
WX_CLASS = None # needs to be defined in every derived class; e.g. "wxFrame", "wxBoxSizer", "TopLevelPanel"
WX_CLASSES = None # used if WX_CLASS can be changed dynamically
WXG_BASE = None # usually None, but if defined, it will be written to the wxg file instead of the editor class name
IS_NAMED = True # default, only False for Spacer
#CHILDREN = 1 # 0 or a fixed number or None for e.g. a sizer with a variable number of children; -1 for 0 or 1
ATT_CHILDREN = None
TREE_ICON = None # defaults to editor class name
def __init__(self, name, parent, index):
assert self.WX_CLASS
np.PropertyOwner.__init__(self)
self.widget = None # this is the reference to the actual wxWindow widget, created when required
self.item = None # the TreeCtrl item
# initialise instance properties
self.name = np.NameProperty(name)
# initialise structure
self.parent = parent
if self.CHILDREN is None:
# variable number of children
self.children = _UniqueList([])
elif self.CHILDREN:
# fixed number of children
self.children = _UniqueList([None]*self.CHILDREN)
else:
# no children
self.children = None
if isinstance(index, str):
setattr(self.parent, index, self)
self.attribute_name = index
else:
self.parent.add_item(self, index)
# the toplevel parent keeps track of the names ( see next two methods ...contained_name() )
if self.IS_TOPLEVEL:
# either derived from edit_windows.TopLevelBase or a toplevel Menu/ToolBar where IS_TOPLEVEL is set True
self.names = set([self.name])
self._NUMBERS = {} # for finding new names
elif self.IS_NAMED:
self.toplevel_parent.track_contained_name( new_name=name )
# manage names of contained elements ###############################################################################
# actually, this might be too strict if contained elements are their own classes
def get_next_contained_name(self, fmt, exclude=None):
# get a name that is not yet used for one of the children inside
# will only be used for IS_TOPLEVEL==True; currently only by TopLevelBase; in future maybe for toolbars as well
number = self._NUMBERS.get(fmt, 1)
while True:
name = fmt % number
if not name in self.names and (not exclude or not name in exclude):
self._NUMBERS[fmt] = number
return name
number += 1
def track_contained_name(self, old_name=None, new_name=None):
# only for named elements (IS_NAMED==True)
# to remove: new_name=None
# to add: old_name=None
# EditDialog also uses this to track names for "affirmative" and "escape" properties
#print("track_contained_name", old_name,new_name, self.names)
if old_name is not None:
try:
self.names.remove( old_name )
except KeyError:
pass
if new_name is not None: self.names.add( new_name )
# tree navigation (parent and children) ############################################################################
@property
def toplevel_parent(self):
# go up to parent until that's parent IS_ROOT
item = self
parent = item.parent
while not parent.IS_ROOT:
item = parent
parent = item.parent
return item
@property
def parent_window(self):
# go up to parent until it is no sizer
item = self.parent
while True:
if item.IS_WINDOW: return item
item = item.parent
if item is None: return None
@property
def toplevel_parent_window(self):
# go up to parent until IS_TOPLEVEL is True
item = self.parent
while True:
if item.IS_TOPLEVEL: return item
item = item.parent
#@property
#def class_object(self):
## used for code generation: the object for which a class code is generated
#item = self
#parent = item.parent
#while parent is not None:
#if not item.IS_SIZER and item.IS_CLASS: return item
#item = parent
#parent = item.parent
#return None
@property
def parent_class_object(self):
# same as before, but start with parent
# used only for XRC code generation, so IS_CLASS is set
item = self.parent
parent = item.parent
while parent is not None:
if not item.IS_SIZER and item.IS_CLASS: return item
item = parent
parent = item.parent
return None
@property
def sizer(self):
# return the containing sizer or None
if self.parent.IS_SIZER: return self.parent
return None
@property
def top_sizer(self):
# return the top containing sizer or None
if not self.parent.IS_SIZER:
if self.IS_SIZER: return self
return None
item = self.parent
while item.parent.IS_SIZER:
item = item.parent
return item
def get_all_children(self):
# this always returns a copy, as it might be used by recursive_remove
ret = []
if self.ATT_CHILDREN:
for att in self.ATT_CHILDREN or []:
child = getattr(self, att)
if child is not None: ret.append(child)
ret.extend(self.children or [])
return ret
def _get_child(self, index):
if index is None and self.children:
return self.children[0]
if isinstance(index, str) and index in self.ATT_CHILDREN:
return getattr(self, index)
if self.children and index<=len(self.children):
return self.children[index]
return self
def _get_child_pos(self, child):
# calculate pos including named attributes; this is the position in the tree view
pos = 0
if self.ATT_CHILDREN:
for att in self.ATT_CHILDREN or []:
c = getattr(self, att)
if c is not None:
if child is c: return pos
pos += 1
return pos + self.children.index(child)
def find_children(self, name=None, wx_class=None):
ret = []
for child in self.get_all_children():
if not child: continue
ret += child.find_children(name, wx_class)
if name is not None and child.name!=name: continue
if wx_class is not None and child.WX_CLASS!=wx_class: continue
ret.append(child)
return ret
####################################################################################################################
@property
def index(self):
# index or attribute name
attribute_name = getattr(self, "attribute_name", None)
if attribute_name is not None: return attribute_name
return self.parent.children.index(self)
def add_item(self, child, index=None):
if index is None:
# happens during loading or pasting
if self.CHILDREN is None:
# variable number of children
self.children.append(child)
return
# fixed number of children; fill first free position (a None or a Slot)
assert self.CHILDREN
if None in self.children:
index = self.children.index(None)
elif self.CHILDREN in (-1,1):
index = 0
if len(self.children)<=index:
self.children += [None]*(index - len(self.children) + 1)
if self.children[index] is not None:
self.children[index].recursive_remove(0, keep_slot=True)
self.children[index] = child
def insert_item(self, child, index):
# for now only for child=None as placeholder; used by notebook
self.children.insert(index, child)
def remove_item(self, child, level, keep_slot=False):
"Removes child from self and adjust pos of following items"
if not child: return
if child in self.children:
index = self.children.index(child)
if keep_slot:
self.children[index] = None
else:
del self.children[index]
return
if hasattr(child, "attribute_name"):
setattr(self, child.attribute_name, None)
return
raise ValueError("Internal error")
def has_ancestor(self, editor):
"Returns True if editor is parent or parents parent ..."
parent = self.parent
if parent is None: return False
while True:
if editor is parent: return True
if parent.parent is None: return False
parent = parent.parent
def get_path(self):
"""returns a list of widget names, from the toplevel to the selected one
Example: ['frame_1', 'sizer_1', 'panel_1', 'sizer_2', 'button_1']
if button_1 is the currently selected widget"""
ret = []
w = self
while w:
if w.IS_SLOT:
ret.append("SLOT %d"%w.index)
else:
ret.append(w.name)
w = w.parent
ret.reverse()
return "/".join(ret)
# property handling ################################################################################################
@staticmethod
def MOVE_PROPERTY(PROPERTIES, move_property, after_property):
"move a property to another position, right behind after_property"
PROPERTIES.remove( move_property )
PROPERTIES.insert( PROPERTIES.index(after_property)+1, move_property )
def _properties_changed(self, modified, actions):
if modified and "name" in modified and self.properties["name"].previous_value is not None:
if config.debugging or config.testing:
assert self.IS_NAMED
old_name = self.properties["name"].previous_value or None
self.toplevel_parent.track_contained_name(old_name, self.name)
if modified and ("name" in modified or "label" in modified or "title" in modified):
actions.add("label")
def properties_changed(self, modified):
actions = np.PropertyOwner.properties_changed(self, modified)
if common.app_tree is not None and ("label" in actions or "image" in actions):
common.app_tree.refresh(self, refresh_label=("label" in actions), refresh_image=("image" in actions))
return actions
# widget creation and destruction ##################################################################################
def create(self):
# entry point to create widget including all children
self.recursive_create_widgets(level=0)
self.layout()
def recursive_create_widgets(self, level):
self.create_widget()
self.finish_widget_creation(level)
for child in self.get_all_children():
child.recursive_create_widgets(level+1)
self.child_widgets_created(level) # if level==0, only one of the child widgets was created
self.parent.child_widget_created(self, level)
def layout(self):
# called once after all widgets incl. children were created or e.g. layout property modified
# before 2020-08-10 the ClipboardXmlWidgetBuilder.endElement() had code with
# SafeYield, layout, Refresh, GetTopLevelParent().SendSizeEvent()
self.widget.Layout()
if self.IS_TOPLEVEL: return
if self.IS_WINDOW: self.widget.SendSizeEvent()
parent_window = self.parent_window
parent_window.widget.SendSizeEvent()
if ( hasattr(self.widget, "SendSizeEvent") and not self.IS_SIZER and
(wx.Platform == '__WXGTK__' or not self.parent.IS_SIZER) ):
# following is required for
# - gtk when e.g. pasting a button or label with non-standard font; not for Windows or Mac Os
# - adding e.g. a grid or text control to a frame (without a sizer); not for panel
wx.SafeYield()
compat.wxWindow_SendSizeEventToParent(self.widget)
# following is required when e.g. adding a slot or widget to a sizer on a panel in a sizer
compat.wxWindow_SendSizeEventToParent(self.parent_window.widget)
# actual widget creation
def create_widget(self):
"Initializes self.widget and shows it"
raise NotImplementedError
def finish_widget_creation(self, level):
"Binds the popup menu handler and connects some event handlers to self.widgets; set tooltip string"
if not self.widget: return
self.widget.Bind(wx.EVT_RIGHT_DOWN, self.popup_menu)
if self.WX_CLASS in ("wxStatusBar",): return
compat.SetToolTip(self.widget, self._get_tooltip_string())
# callbacks when children were created
def child_widget_created(self, child, level):
# called after child's children were also created; implemented for notebook, splitter, sizers
pass
def child_widgets_created(self, level):
# implemented for notebook, splitter, sizers?
pass
# actual widget destruction, called from recursive_remove
def destroy_widget(self, level, later=True):
# just destroy the widget; all bookkeeping / data structure update is done in recursive_remove
# level is 0 for toplevel or when the user just deletes this one
if not self.widget: return
if later:
compat.DestroyLater(self.widget)
else:
self.widget.Destroy()
self.widget = None
def destroying_child_widget(self, child, index):
# called before a child widget is destroyed; e.g. used by splitter to unsplit
# child has been removed from self.children already,
# except when a widget is re-created or a slot is just set to overlapped
# index can be an index or an attribute name
pass
def destroyed_child_widget(self):
pass
####################################################################################################################
def recursive_remove(self, level, keep_slot=False):
"recursively remove children and then self from parent; delete widget; remove from tree and do bookkeeping"
# this is not a GUI entry point, see remove() for this!
index = self.index
# recursively remove children
if self.children:
for child in self.get_all_children():
if child is None: continue # this might happen during loading when a widget type is not supported
child.recursive_remove(level+1)
self.parent.remove_item(self, level, keep_slot)
if level==0 and self.widget:
self.parent.destroying_child_widget(self, index)
self.destroy_widget(level)
self.parent.destroyed_child_widget()
# remove from Tree (rebuild_tree to be called separately)
if misc.focused_widget is self: misc.focused_widget = None
if common.app_tree is not None:
common.app_tree.remove(self) # remove mutual reference from widget to/from Tree item
# bookkeeping
if not self.IS_TOPLEVEL and self.IS_NAMED and self.name:
self.toplevel_parent.track_contained_name( self.name )
def remove(self, focus=True, user=True):
# entry point from GUI or script
if user:
common.history.widget_removing(self)
common.root.saved = False # update the status of the app
self.recursive_remove(level=0)
misc.rebuild_tree(self.parent, recursive=False, focus=focus)
if user: common.history.widget_removed(None)
# XML generation ###################################################################################################
def get_editor_name(self):
# the panel classes will return something else here, depending on self.scrollable
return self.WXG_BASE or self.__class__.__name__
def write(self, output, tabs):
"Writes the xml code for the widget to the given output file"
# write object tag, including class, name, base
classname = self.get_editor_name()
# to disable custom class code generation (for panels...)
outer_tabs = u' ' * tabs
instance_class = ''
if "class" in self.properties:
klass = self.get_prop_value("class", default=self.WX_CLASS)
if self.check_prop_truth("instance_class"):
instance_class = " " + common.format_xml_attrs(instance_class=self.instance_class)
else:
klass = self.get_prop_value("instance_class", default=self.WX_CLASS)
output.append(u'%s<object %s %s %s%s>\n' % ( outer_tabs,
common.format_xml_attrs(**{'class': klass}),
common.format_xml_attrs(name=self.name),
common.format_xml_attrs(base=classname),
instance_class) )
if config.debugging and getattr(self, "_restore_properties", None):
raise ValueError("properties not restored")
self.restore_properties()
# write properties, but without name and class
# XXX be 100% compatible to 0.7.2, where option is written into the object; remove later
properties = self.get_properties(without=set(MANAGED_PROPERTIES))
for prop in properties:
prop.write(output, tabs+1)
if self.IS_SIZER:
for child in self.children or []:
if not child.IS_SLOT:
inner_xml = []
for name in MANAGED_PROPERTIES:
name = child.properties[name]
if name is not None:
name.write(inner_xml, tabs+2)
child.write(inner_xml, tabs+2)
stmt = common.format_xml_tag( u'object', inner_xml, tabs+1,
is_xml=True, **{'class': 'sizeritem'} )
output.extend(stmt)
else:
child.write(output, tabs+1)
elif self.children is not None or self.ATT_CHILDREN is not None:
for child in self.get_all_children():
assert not config.debugging or child is not None
child.write(output, tabs+1)
output.append(u'%s</object>\n' % outer_tabs)
# XML loading and slot handling ####################################################################################
def _add_slots(self, max_index=None):
"replace None with Slot"
for index, child in enumerate(self.children):
if child is None:
if max_index is not None and index>max_index: continue
Slot(self, index)
def _add_slot(self):
# used when loading or pasting an (optional) slot to a panel
self.children.append(None)
self._add_slots() # replace the None with a slot
def on_load(self, child=None):
"called from XML parser, right after the widget is loaded; children have been loaded already"
# when a child has been pasted in, it's also called, with argument child
if self.CHILDREN != 0:
self._add_slots()
def free_slot(self, index, force_layout=True):
"Replaces the element at index with an empty slot"
# called from ManagedBase context menu when removing an item
slot = self._free_slot(index, force_layout)
misc.rebuild_tree(slot)
def _free_slot(self, index, force_layout=True):
#with self.toplevel_parent.frozen(): # this does not work on mac os: when deleting a panel notebook page, it will remain black
slot = Slot(self, index)
if self.widget: slot.create() # create the actual SizerSlot as wx.Window with hatched background
return slot
# for tree and help display ########################################################################################
def _get_tree_label(self):
# get a label for node
s = self.name
if self.WX_CLASS=="CustomWidget":
s += ' (%s)' % self.instance_class
elif self.check_prop("class"):
s += ' (%s)' % self.klass
if getattr(self, "has_title", None):
# include title
s += ': "%s"'%self.title
elif self.check_prop("stockitem"):
s = "%s: %s"%(s, self.stockitem)
elif self.check_prop("label"):
# include label of control
label = self.label
label = label.replace("\n","\\n").replace("\t","\\t")
if '"' in label:
if len(label)>36:
s += ": '%s..."%(label[:30])
else:
s += ": '%s'"%label
else:
if len(label)>24:
s += ': "%s...'%(label[:30])
else:
s += ': "%s"'%label
elif getattr(self, "has_title", None):
# include title
s += ': "%s"'%self.title
elif self.parent.WX_CLASS=="wxNotebook":
# notebook pages: include page title: "[title] name"
notebook = self.parent
if self in notebook.children:
title = notebook.tabs[notebook.children.index(self)][0]
s = '[%s] %s'%(title, s)
return s
def _get_tree_image(self):
"Get an image name for tree display"
return self.TREE_ICON or self.__class__.__name__
def _label_editable(self):
# returns True if the label can be edited in the Tree ctrl
if not "name" in self.properties: return False
if not "label" in self.properties: return True
label = self.label
# no editing in case of special characters
if "\n" in label or "\t" in label or "'" in label or '"' in label: return False
if len(label)>24: return False
return True
def _get_tooltip_string(self):
# get tooltip string: first (optional) part from parent, second from ourself
# used as tooltip for the widget in the Design window and also for the status bar of the Palette
tooltip = []
if self.parent.children and self in self.parent.children:
tooltip.append( self.parent._get_parent_tooltip(self.index) )
tooltip.append( self._get_tooltip() )
return "\n".join(s for s in tooltip if s)
def _get_parent_tooltip(self, index):
# called for a child; e.g. Splitter pane may return "Left spliter pane:" for pos=0
return None
def _get_tooltip(self):
return None
class Slot(EditBase):
"A window to represent an empty slot, e.g. single slot of a Frame or a page of a Notebook"
PROPERTIES = ["Slot", "info"]
IS_TOPLEVEL = IS_SIZER = IS_WINDOW = False
IS_SLOT = True
IS_NAMED = False
CHILDREN = 0
def __init__(self, parent, index, label=None):
# XXX unify with EditBase.__init__ ?
assert isinstance(index, int)
assert not parent.IS_SLOT
np.PropertyOwner.__init__(self)
self.klass = self.classname = "slot"
self.label = label
# initialise instance properties
self.widget = None # Reference to the widget resembling the slot (a wx.Window)
self.name = "SLOT"
self.overlapped = False # for spanning in GridBagSizer
self.item = None
# initialise structure
self.parent = parent
self.children = None
self.parent.add_item(self, index)
# display some help
self.info = np.DisplayProperty(self._get_tooltip())
# the following are just set to use the same Add call as with widgets
self.proportion = 1
self.span = (1,1)
self.flag = wx.EXPAND
self.border = 0
def update_view(self, selected):
# we can ignore selected here, as the repainting only takes place later
if self.widget:
self.widget.Refresh()
def create_widget(self):
if self.overlapped and self.parent._IS_GRIDBAG: return
style = wx.FULL_REPAINT_ON_RESIZE
if self.parent.CHILDREN in (-1, 1): # e.g. Panel in a Frame
size = self.parent.widget.GetClientSize()
else:
size = (20, 20)
self.widget = wx.Window(self.parent_window.widget, -1, size=size, style=style)
self.widget.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
#self.widget.SetAutoLayout(True)
self.widget.Bind(wx.EVT_PAINT, self.on_paint)
self.widget.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
self.widget.Bind(wx.EVT_RIGHT_DOWN, self.popup_menu)
self.widget.Bind(wx.EVT_LEFT_DOWN, self.on_drop_widget)
self.widget.Bind(wx.EVT_MIDDLE_DOWN, misc.exec_after(self.on_select_and_paste))
self.widget.Bind(wx.EVT_ENTER_WINDOW, self.on_enter)
self.widget.Bind(wx.EVT_LEAVE_WINDOW, self.on_leave)
#self.widget.Bind(wx.EVT_CHAR_HOOK, misc.on_key_down_event) # catch cursor keys XXX still required?
def recursive_create_widgets(self, level):
if self.overlapped: return
self.create_widget()
self.finish_widget_creation(level)
self.parent.child_widget_created(self, level)
def is_visible(self):
return False
def on_enter(self, event):
# hack. definitely. but...
misc.currently_under_mouse = self.widget
# set cursor
if common.adding_widget:
if self.check_drop_compatibility()[0]:
self.widget.SetCursor(wx.CROSS_CURSOR)
else:
self.widget.SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY))
else:
self.widget.SetCursor(wx.STANDARD_CURSOR)
event.Skip()
def on_leave(self, event):
# currently_under_mouse is used to restore the normal cursor, if the
# user cancelled the addition of a widget and the cursor is over this slot
misc.currently_under_mouse = None
event.Skip()
def on_paint(self, event):
"Handle paint request and draw hatched lines onto the window"
#if not self.sizer: return # in deletion
dc = wx.PaintDC(self.widget)
self._draw_background(dc)
def on_erase_background(self, event):
dc = event.GetDC()
if not dc:
dc = wx.ClientDC(self)
rect = self.widget.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
self._draw_background(dc, clear=False)
def _draw_background(self, dc, clear=True):
"draw the hatches on device context dc (red if selected)"
size = self.widget.GetSize()
small = size[0]<10 or size[1]<10
focused = misc.focused_widget is self
if clear:
if small and focused:
dc.SetBackground(wx.Brush(wx.BLUE))
else:
dc.SetBackground(wx.Brush(wx.LIGHT_GREY))
dc.Clear()
if small and focused:
color = wx.WHITE
elif small or not focused:
color = wx.BLACK
else:
color = wx.BLUE
if focused:
hatch = compat.BRUSHSTYLE_CROSSDIAG_HATCH
elif not self.parent.IS_SIZER:
hatch = compat.BRUSHSTYLE_FDIAGONAL_HATCH
else:
if not "cols" in self.parent.PROPERTIES: # horizontal/vertical sizer or grid sizer?
pos = self.index
else:
pos = sum( self.sizer._get_row_col(self.index) )
hatch = compat.BRUSHSTYLE_FDIAGONAL_HATCH if pos%2 else compat.BRUSHSTYLE_BDIAGONAL_HATCH
brush = wx.Brush(color, hatch)
# draw hatched lines in foreground
dc.SetBrush(brush)
size = self.widget.GetClientSize()
dc.DrawRectangle(0, 0, size.width, size.height)
# context menu #####################################################################################################
def popup_menu(self, event, pos=None):
event_widget = event.GetEventObject()
menu = self._create_popup_menu(widget=event_widget)
if pos is None:
# convert relative event position to relative widget position
event_pos = event.GetPosition()
screen_pos = event_widget.ClientToScreen(event_pos)
pos = event_widget.ScreenToClient(screen_pos)
event_widget.PopupMenu(menu, pos)
menu.Destroy()
def _create_popup_menu(self, widget):
# menu title
if self.parent.IS_SIZER and "cols" in self.parent.properties:
rows, cols = self.parent._get_actual_rows_cols()
# calculate row and col of our slot
row,col = self.parent._get_row_col(self.index)
menu = wx.Menu(_("Slot %d/%d"%(row+1,col+1)))
elif self.parent.CHILDREN not in (-1,1):
menu = wx.Menu(_("Slot %d"%self.index))
else:
menu = wx.Menu(_("Slot"))
# edit: paste
i = misc.append_menu_item(menu, -1, _('Paste\tCtrl+V'), wx.ART_PASTE)
misc.bind_menu_item_after(widget, i, clipboard.paste, self)
if not clipboard.check("widget","sizer"): i.Enable(False)
menu.AppendSeparator()
# slot actions
if self.parent.IS_SIZER or self.parent.CHILDREN==-1:
if not "cols" in self.parent.properties:
i = misc.append_menu_item(menu, -1, _('Remove Slot\tDel'), wx.ART_DELETE)
misc.bind_menu_item_after(widget, i, self.remove)
if self.parent.IS_SIZER and len(self.parent.children)<=1: i.Enable(False)
else:
# if inside a grid sizer: allow removal of empty rows/cols
# check whether all slots in same row/col are empty
row_is_empty = col_is_empty = True
for index,child in enumerate(self.parent.children):
child_row, child_col = self.parent._get_row_col(index)
if child_row==row and not child.IS_SLOT:
row_is_empty = False
if child_col==col and not child.IS_SLOT:
col_is_empty = False
# allow removal of empty row
i = misc.append_menu_item(menu, -1, _('Remove Row %d'%(row+1)) )
misc.bind_menu_item_after(widget, i, self.parent.remove_row, row)
if not row_is_empty or rows<=1: i.Enable(False)
# allow removal of empty col
i = misc.append_menu_item(menu, -1, _('Remove Column %d'%(col+1)) )
misc.bind_menu_item_after(widget, i, self.parent.remove_col, col)
if not col_is_empty or cols<=1: i.Enable(False)
menu.AppendSeparator()
if hasattr(self.parent, "_add_parent_popup_menu_items"):
self.parent._add_parent_popup_menu_items(menu, self, widget)
p = self.toplevel_parent_window # misc.get_toplevel_widget(self.sizer)
#if p is not None and p.preview_is_visible():
if p.preview_is_visible():
item = _('Close preview (%s)\tF5') % p.name
else:
item = _('Preview (%s)\tF5') % p.name
i = misc.append_menu_item( menu, -1, item )
#misc.bind_menu_item_after(widget, i, self.preview_parent)
misc.bind_menu_item_after(widget, i, p.preview)
return menu
####################################################################################################################
def remove(self, user=True):
# entry point from GUI
i = self.index
EditBase.remove(self, focus=False, user=user)
if i >= len(self.parent.children): i = len(self.parent.children)-1
# set focused widget
if i>=0:
misc.set_focused_widget( self.parent.children[i] )
else:
misc.set_focused_widget( self.parent )
def on_drop_widget(self, event, reset=None):
"""replaces self with a widget. This method is called to add every non-toplevel
widget or sizer, and in turn calls the appropriate builder function
(found in the 'common.widgets' dict)."""
if not common.adding_widget: # widget focused/selected
misc.set_focused_widget(self)
if self.widget:
self.widget.Refresh()
self.widget.SetFocus()
return
if not self.check_drop_compatibility()[0]:
return
if self.widget:
self.widget.SetCursor(wx.NullCursor)
common.adding_window = event and event.GetEventObject().GetTopLevelParent() or None
common.history.widget_adding(self)
# call the appropriate builder
new_widget = common.widgets[common.widget_to_add](self.parent, self.index)
if new_widget is None: return
misc.rebuild_tree(new_widget)
if reset is False: return
if event is None or not misc.event_modifier_copy(event):
common.adding_widget = common.adding_sizer = False
common.widget_to_add = None
if event is not None and new_widget.widget:
# set focus; required mainly on macOS to receive keys
widget = None
if new_widget.IS_WINDOW:
widget = new_widget.widget
elif new_widget.IS_SIZER:
widget = new_widget.toplevel_parent_window.widget
if hasattr(widget, "SetFocus"):
widget.SetFocus()
common.history.widget_added(new_widget)
def check_drop_compatibility(self):
if common.adding_sizer and self.parent.IS_CONTAINER:
return (False, "No sizer can be added here")
if self.parent.WX_CLASS in ("wxPanel", "wxScrolledWindow", ):
# no containers like splitter or notebook in a panel
if common.widget_to_add in ("EditSplitterWindow", "EditNotebook"):
return (False, "No container can be added here")
return (True,None)
# clipboard handling ###############################################################################################
def check_compatibility(self, widget, typename=None):
"check whether widget can be pasted here"
if self.parent.CHILDREN == -1:
# single or no child: no sizer but a panel or frame
return self.parent.check_compatibility(widget, typename)
if typename is not None:
if typename=="sizer" and self.parent.CHILDREN != 1:
return (False, "No sizer can be pasted here")
if typename=="window":
return (False, "No toplevel object can be pasted here.")
return (True,None)
if widget.IS_TOPLEVEL:
return (False, "No toplevel object can be pasted here.")
if self.parent.IS_CONTAINER and widget.IS_SIZER:
# e.g. a sizer dropped on a splitter window slot; instead, a panel would be required
return (False, "No sizer can be pasted here")
return (True,None)
def clipboard_paste(self, clipboard_data):
"Insert a widget from the clipboard to the current destination"
if self.parent.CHILDREN==-1:
# e.g. Panel has special treatment
return self.parent.clipboard_paste(clipboard_data)
return clipboard._paste(self.parent, self.index, clipboard_data)
def on_select_and_paste(self, *args):
"Middle-click event handler: selects the slot and, if the clipboard is not empty, pastes its content here"
misc.focused_widget = self
self.widget.SetFocus()
clipboard.paste(self)
####################################################################################################################
def destroy_widget(self, level):
if self.widget is None: return
if misc.currently_under_mouse is self.widget:
misc.currently_under_mouse = None
self.widget.Hide()
if wx.VERSION_STRING!="2.8.12.0":
# unbind events to prevent new created (and queued) events
self.widget.Bind(wx.EVT_PAINT, None)
self.widget.Bind(wx.EVT_RIGHT_DOWN, None)
self.widget.Bind(wx.EVT_LEFT_DOWN, None)
self.widget.Bind(wx.EVT_MIDDLE_DOWN, None)
self.widget.Bind(wx.EVT_ENTER_WINDOW, None)
self.widget.Bind(wx.EVT_LEAVE_WINDOW, None)
self.widget.Bind(wx.EVT_KEY_DOWN, None)
compat.DestroyLater(self.widget)
self.widget = None
if misc.focused_widget is self:
misc.set_focused_widget(None)
def write(self, output, tabs):
if self.parent.CHILDREN==-1:
output.extend( common.format_xml_tag( u'object', '', tabs, **{'class': 'slot'}) )
# for tree and help display ########################################################################################
def _get_tree_label(self):
if self.label: return str(self.label)
if self.parent.CHILDREN in (1,-1): return "SLOT"
index = self.index
if hasattr(self.parent, "_get_slot_label"):
return self.parent._get_slot_label(index)
if self.parent.IS_SIZER and "cols" in self.parent.properties:
# grid sizer: display row/col
rows, cols = self.parent._get_actual_rows_cols()
row = index // cols + 1 # 1 based at the moment
col = index % cols + 1
return "SLOT %d/%d"%(row, col)
return "SLOT %d"%(index)
def _get_tree_image(self):
"Get an image name for tree display"
if self.parent.WX_CLASS=="wxSplitterWindow":
return 'EditSplitterSlot-%s'%self.parent._get_label(self.index) # 'Left', 'Right', 'Top', 'Bottom'
if not self.parent.IS_SIZER: return "EditSlot"
name = "EditSizerSlot"
if "rows" in self.parent.properties:
rows, cols = self.parent._get_actual_rows_cols()
row, col = self.parent._get_row_col(self.index, cols)
posh = posv = 1 # default
if row==0: posv = 0
elif row+1==rows: posv = 2
if col==0: posh = 0
elif col+1==cols: posh = 2
name = "EditGridSizerSlot-%s%s"%(posh, posv)
if self.overlapped: name += "-Disabled"
elif "orient" in self.parent.properties:
sizer_orient = self.parent.orient
if sizer_orient is not None:
if sizer_orient==wx.VERTICAL:
name = "EditVerticalSizerSlot"
elif sizer_orient==wx.HORIZONTAL:
name = "EditHorizontalSizerSlot"
return name
def _get_tooltip(self):
if self.parent.WX_CLASS in ("wxPanel", "wxScrolledWindow"):
return "Add a sizer here."
if self.parent.WX_CLASS in ("wxFrame",):
return "Add a control or container or sizer here, e.g. a panel, a panel plus sizer, a notebook or a sizer."
if self.parent.WX_CLASS in ("wxDialog",):
return "Add a sizer or a control here."
return "Add a control or container here, e.g. a panel, a panel plus sizer or a notebook."