/
ui.py
1439 lines (1091 loc) · 44.6 KB
/
ui.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
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
# -*- coding: iso-8859-1 -*-
# $Id$
# Snowberry: Extensible Launcher for the Doomsday Engine
#
# Copyright (C) 2004, 2005
# Jaakko Keränen <jaakko.keranen@iki.fi>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not: http://www.opensource.org/
## @file ui.py User Interface
##
## This module contains classes and functions for managing the
## application's user interface. The basic concept is that the
## interface has been divided into a number of areas. The top-level
## areas (title, profiles, command, help) are created when the main
## window is constructed, and cannot be changed at runtime. The
## contents of each area can be modified at runtime.
##
## Many of the areas are managed by plugins. This means the plugin
## creates all the widgets inside the area.
##
## Each area can be divided into further sub-areas, which are treated
## the same way as the top-level areas. The Area class can represent
## either a top-level area or a sub-area.
import sys, os, wx, string
import wx.wizard as wiz
import host, events, widgets, language
import profiles as pr
import settings as st
import logger
# An array of UI areas.
uiAreas = {}
mainPanel = None
def getArea(id):
"""Returns the UI area with the specified ID.
@return An Area object.
"""
return uiAreas[id]
def _newArea(area):
"""A private helper function for adding a new area to the list of
identifiable areas. Sub-areas are not identifiable.
@param area An Area object.
"""
global uiAreas
uiAreas[area.getId()] = area
def selectTab(id):
"""Select a tab in the tab switcher.
@param id Identifier of the tab.
"""
#mainPanel.selectTabPanel(uiAreas[id].getPanel())
mainPanel.getTabs().selectTab(id)
def createTab(id):
"""Create a new tab in the tab switcher.
@param id The identifier of the new tab. The label of the tab
will be the translation of this identifier.
@return An Area object that represents the new tab.
"""
#panel = mainPanel.createTabPanel(id)
#tabs = mainPanel.getTabs()
# The label is determined by the translation of the identifier.
#tabs.AddPage(panel, language.translate(id))
# Create the Area and set the default layout parameters.
#area = Area(id, panel, Area.ALIGN_VERTICAL, border=3)
#area.setExpanding(True)
#area.setWeight(1)
#_newArea(area)
area = mainPanel.getTabs().addTab(id)
area.setBorder(3)
# Register this area so it can be found with getArea().
_newArea(area)
return area
def chooseFile(prompt, default, mustExist, fileTypes, defExt=None):
"""Show a file chooser dialog.
@param prompt Identifier of the dialog title string.
@param default The default selection.
@param mustExist The selected file must exist.
@param fileTypes An array of tuples (name, extension).
@return The full pathname of the selected file.
"""
# Compile the string of file types.
types = []
for ident, ext in fileTypes:
types.append(language.translate(ident))
types.append('*.' + ext.lower() + ';*.' + ext.upper())
# Always append the "all files" filter.
types.append(language.translate('file-type-anything'))
types.append('*.*')
if not defExt:
if len(fileTypes) > 0:
# By default use the first file type.
defExt = fileTypes[0][1]
else:
defExt = ''
if mustExist:
flags = wx.FILE_MUST_EXIST
else:
flags = 0
selection = wx.FileSelector(language.translate(prompt),
os.path.dirname(default),
os.path.basename(default),
defExt,
string.join(types, '|'),
flags)
return selection
def chooseFolder(prompt, default):
"""Show a directory chooser dialog.
@param prompt Identifier of the dialog title string.
@param default The default path.
@return The selected path.
"""
return wx.DirSelector(language.translate(prompt), default)
class Area (widgets.Widget):
"""Each Area instance governs the contents of a wxPanel widget.
Together with the classes in widgets.py, the Area class
encapsulates all management of the user interface. Modules that
use ui.py or widgets.py need not access wxWidgets directly at all.
The Area class implements the widgets.Widget interface from
widgets.py. getWxWidget() and clear() are the important
Widget-inherited methods that Area uses.
Area alignment constants:
- ALIGN_HORIZONTAL: Widgets are placed next to each other
inside the area.
- ALIGN_VERTICAL: New widgets are placed below the existing
widgets.
"""
# Default areas.
TITLE = 'main-title'
PROFILES = 'main-profiles'
COMMAND = 'main-command'
TABS = 'main-tabs'
HELP = 'main-help'
# Layout alignments.
ALIGN_HORIZONTAL = 0
ALIGN_VERTICAL = 1
# wxWidget ID counters.
WIDGET_ID = 100
def __init__(self, id, panel, alignment=0, border=0, parentArea=None):
"""The constructor initializes the Area instance so that it
can be used immediately afterwards.
@param panel The wxWidget panel inside which the new Area will
be.
@param alignment Alignment for widgets inside the area. This
can be either ALIGN_VERTICAL or ALIGN_HORIZONTAL.
@param border Width of the border around the new Area.
@param parentArea Area instance that will act as the parent of
the new area. If this is None, it is assumed that the new
Area is directly inside the main panel of the dialog.
"""
self.id = id
self.panel = panel
self.border = border
# Widget layout parameters.
# Weight for sizer when adding widgets.
self.weight = 1
self.expanding = True
# Create a Sizer that maintains the proper layout of widgets
# inside the Area.
if alignment == Area.ALIGN_HORIZONTAL:
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
else:
self.sizer = wx.BoxSizer(wx.VERTICAL)
# Top-level areas have a wx.Panel, with which the sizer needs
# to be associated. Subareas use the parent's area.
if not parentArea:
self.panel.SetSizer(self.sizer)
self.parentArea = None
else:
self.parentArea = parentArea
# The contents of the area will be added inside this sizer.
self.containerSizer = self.sizer
# The arrays that contain information about the contents of
# the Area.
self.widgets = []
def getId(self):
return self.id
def getPanel(self):
return self.panel
def getWxWidget(self):
"""An Area instance can be used like a Widget."""
return self.sizer
def __getNewId(self):
"""Generate a new widget ID number. These are given to
wxWidgets so the events they send can be handled.
"""
id = Area.WIDGET_ID
Area.WIDGET_ID += 1
return id
def setWeight(self, weight):
"""Set the sizer weight that will be used for the next widget
that is created."""
self.weight = weight
def setBorder(self, border):
"""Set the width of empty space around widgets.
@param border Number of pixels for new borders.
"""
self.border = border
def setBackgroundColor(self, red, green, blue):
self.panel.SetBackgroundColour(wx.Colour(red, green, blue))
self.panel.SetBackgroundStyle(wx.BG_STYLE_COLOUR)
def setExpanding(self, expanding):
self.expanding = expanding
def updateLayout(self, redoLayout=True):
"""Recalculate the layout of all the widgets inside the area.
This must be called when widgets are created or destroyed at
runtime. The layout of all areas is updated automatically
right before the main window is displayed during startup. If
a plugin modifies the layout of its widgets during event
handling, it must call updateLayout() manually.
"""
# Tell the main sizer of our panel's current minimum size.
m = self.sizer.GetMinSize()
topSizer = self.panel.GetContainingSizer()
if topSizer:
topSizer.SetItemMinSize(self.panel, m)
# Redo the layout.
if redoLayout:
self.sizer.Layout()
def addSpacer(self):
"""Adds empty space into the area. The size of the empty
space is determined by the current weight.
"""
## @todo There must be a better way to do this.
self.createArea('')
def __addWidget(self, widget):
"""Insert a new widget into the area.
@param wxWidget Widget object to add to the area.
"""
# The Area keeps track of its contents.
self.widgets.append(widget)
# Use the low-level helper to do the adding.
self._addWxWidget(widget.getWxWidget())
def _getLayoutFlags(self):
"""Determines the appropriate layout flags for child widgets."""
flags = 0
if self.expanding: flags |= wx.EXPAND
if self.border > 0:
if len(self.widgets) > 1:
flags |= wx.ALL
else:
flags |= wx.NORTH | wx.SOUTH | wx.WEST | wx.EAST
flags |= wx.ALIGN_CENTER_VERTICAL
flags |= wx.ALIGN_CENTER
return flags
def _addWxWidget(self, wxWidget):
"""Insert a wxWidget into the area's sizer. This protected
low-level helper method takes care of selecting the correct
layout parameters for the widget.
@param wxWidget A wxWidget object.
"""
# Insert the widget into the Sizer with appropriate layout
# parameters.
self.containerSizer.Add(wxWidget, self.weight,
self._getLayoutFlags(), self.border)
def destroyWidget(self, widget):
"""Destroy a particular widget inside the area.
@param widget The widget to destroy.
"""
if widget in self.widgets:
widget.clear()
widget.destroy()
self.widgets.remove(widget)
def destroy(self):
"""Destroy the area and its contents."""
self.clear()
if not self.parentArea:
# Standalone areas are never added to a sizer.
return
# Detach and destroy the sizer of this area.
self.parentArea.getWxWidget().Detach(self.getWxWidget())
self.getWxWidget().Destroy()
def clear(self):
"""Remove all the widgets inside the area."""
# Remove each widget individually.
for widget in self.widgets:
# Tell the widget it is about to the removed. Subareas
# will process their contents recursively.
widget.clear()
# The widget must be explicitly destroyed.
widget.destroy()
# No widgets remain.
self.widgets = []
def enable(self, doEnable):
"""Enabling an area means enabling all the widgets in it."""
for widget in self.widgets:
widget.enable(doEnable)
def createArea(self, alignment=1, border=0, boxedWithTitle=None):
"""Create a sub-area that is a part of the parent area and can
contain widgets.
@param alignment Widget alignment inside the sub-area. Can be
either ALIGN_VERTICAL or ALIGN_HORIZONTAL.
@param border Border width for the sub-area (pixels).
@param boxedWithTitle If you want to enclose the area inside a
box frame, give the identifier of the box title here. The
actual label text is the translation of the identifier.
@return An Area object that represents the contents of the
area.
"""
# Subareas don't have identifiers.
if boxedWithTitle:
subArea = BoxedArea(boxedWithTitle, self, self.panel,
alignment, border)
else:
subArea = Area('', self.panel, alignment, border, self)
self.__addWidget(subArea)
return subArea
def createMultiArea(self):
multi = MultiArea('', self)
self.__addWidget(multi)
return multi
def createButton(self, name, style=widgets.Button.STYLE_NORMAL):
"""Create a button widget inside the area.
@param name Identifier of the button. The text label that
appears in the button will be the translation of this
identifier.
@return A widgets.Button object.
"""
w = widgets.Button(self.panel, self.__getNewId(), name, style)
self.__addWidget(w)
return w
def createLine(self):
"""Create a static horizontal divider line."""
widget = widgets.Line(self.panel)
self.__addWidget(widget)
return widget
def createImage(self, imageFile):
"""Create an image widget inside the area.
@param imageFile Base name of the image file.
"""
widget = widgets.Image(self.panel, imageFile)
self.__addWidget(widget)
return widget
def createText(self, name='', suffix=''):
"""Create a text label widget inside the area.
@param name Identifier of the text label widget. The actual
text that appears in the widget is the translation of this
identifier.
@return A widgets.Text object.
"""
widget = widgets.Text(self.panel, -1, name, suffix)
self.__addWidget(widget)
return widget
def createTextField(self, name=''):
"""Create a text field widget inside the area.
@param name Identifier of the text field widget.
@return A widgets.TextField object.
"""
widget = widgets.TextField(self.panel, -1, name)
self.__addWidget(widget)
return widget
def createFormattedText(self):
"""Create a formatted text label widget inside the area.
Initially the widget contains no text.
@return A widgets.FormattedText object.
"""
widget = widgets.FormattedText(self.panel, -1)
self.__addWidget(widget)
return widget
def createList(self, name, style=widgets.List.STYLE_SINGLE):
"""Create a list widget inside the area. Initially the list
is empty.
@param name Identifier of the list widget. Events sent by the
widget are formed based on this identifier. For example,
selection of an item causes the notification
'(name)-selected'.
@return A widgets.List object.
"""
widget = widgets.List(self.panel, self.__getNewId(), name, style)
self.__addWidget(widget)
return widget
def createFormattedList(self, name):
"""Create a list widget that displays HTML-formatted items.
Initially the list is empty.
@param name Identifier of the list widget. This is used when
the widget sends notifications.
@param A widgets.FormattedList widget.
"""
widget = widgets.FormattedList(self.panel, self.__getNewId(), name)
self.__addWidget(widget)
return widget
def createDropList(self, name):
"""Create a drop-down list widget inside the area. Initially
the list is empty.
@param name Identifier of the drop-down list widget. Events
sent by the widget are formed based on this identifier.
For example, selection of an item causes the notification
'(name)-selected'.
@return A widgets.DropList object.
"""
widget = widgets.DropList(self.panel, self.__getNewId(), name)
self.__addWidget(widget)
return widget
def createTree(self):
"""Create a tree widget inside the area. Initially the tree
is empty. The widgets.Tree class provides methods for
populating the tree with items.
@return A widgets.Tree object.
@see widgets.Tree
"""
widget = widgets.Tree(self.panel, self.__getNewId())
self.__addWidget(widget)
return widget
def createTabArea(self, name, style=widgets.TabArea.STYLE_ICON):
"""Create a TabArea widget inside the area. The widget is
composed of an icon list and a notebook-like tabbing area.
@param name The identifier of the new TabArea widget. This
will be used as the base name of events sent by the widget.
@return A widgets.TabArea widget.
"""
if style == widgets.TabArea.STYLE_FORMATTED:
# Create a subarea and a separate MultiArea in there.
sub = self.createArea(alignment=Area.ALIGN_HORIZONTAL,
border=0)
# Create the formatted list (with extended functionality).
widget = widgets.FormattedTabArea(self.panel, self.__getNewId(),
name)
sub.setWeight(2)
sub.__addWidget(widget)
# Create the multiarea and pair it up.
sub.setWeight(7)
multi = sub.createMultiArea()
widget.setMultiArea(multi)
else:
widget = widgets.TabArea(self.panel, self.__getNewId(),
name, style)
self.__addWidget(widget)
return widget
def createCheckBox(self, name, isChecked):
"""Create a check box widget inside the area.
@param name Identifier of the widget. The text label of the
widget is the translation of this identifier.
@param isChecked The initial state of the widget.
@return A widgets.CheckBox obejct.
"""
widget = widgets.CheckBox(self.panel, self.__getNewId(),
name, isChecked)
self.__addWidget(widget)
return widget
def createRadioButton(self, name, isChecked, isFirst=False):
widget = widgets.RadioButton(self.panel, self.__getNewId(), name,
isChecked, isFirst)
self.__addWidget(widget)
return widget
def createNumberField(self, name):
"""Create an integer editing widget inside the area.
Initially the editing field is empty.
@param name Identifier of the widget. The text label of the
widget is the translation of this identifier. Notification
events sent by the widget are based on this identifier.
@return A widgets.NumberField object.
"""
widget = widgets.NumberField(self.panel, self.__getNewId(), name)
self.__addWidget(widget)
return widget
def createSlider(self, name):
widget = widgets.Slider(self.panel, self.__getNewId(), name)
self.__addWidget(widget)
return widget
def createSetting(self, setting):
"""Create one or more widgets that can be used to edit the
value of the specified setting. Automatically creates a
subarea that contains all the widgets of the setting.
@param setting A Setting object.
@return The area which contains all the widgets of the
setting.
"""
# Create a subarea for all the widgets of the setting.
area = self.createArea(alignment = Area.ALIGN_HORIZONTAL)
area.setExpanding(False)
if setting.getType() == 'toggle':
# Toggle settings have just a checkbox.
#area.createCheckBox(setting.getId(), False)
area.setWeight(5)
area.createText(setting.getId())
area.setWeight(2)
drop = area.createDropList(setting.getId())
# Add all the possible choices into the list.
drop.addItem('default')
drop.addItem('yes')
drop.addItem('no')
# By default, select the default choice. This will get
# updated shortly, though.
drop.selectItem('default')
elif setting.getType() == 'range':
# Create a label and the integer edit field.
area.setWeight(5)
area.createText(setting.getId())
area.setWeight(2)
nf = area.createNumberField(setting.getId())
nf.setRange(setting.getMinimum(), setting.getMaximum())
elif setting.getType() == 'slider':
# Create a label and a slider.
area.setWeight(2)
area.createText(setting.getId())
area.setWeight(2)
slider = area.createSlider(setting.getId())
slider.setRange(setting.getMinimum(),
setting.getMaximum(),
setting.getStep())
elif setting.getType() == 'choice':
# Create a label and a drop-down list.
area.setWeight(2)
area.createText(setting.getId())
area.setWeight(2)
drop = area.createDropList(setting.getId())
# Insert the choices into the list.
drop.addItem('default')
for a in setting.getAlternatives():
drop.addItem(a)
# Sort if necessary.
if setting.isSorted():
drop.sortItems()
# The selection will be updated soon afterwards.
drop.selectItem('default')
elif setting.getType() == 'text':
# Create a text field.
area.setWeight(1)
area.createText(setting.getId())
area.setWeight(2)
text = area.createTextField(setting.getId())
elif setting.getType() == 'file':
# Create a text field and a button.
area.setWeight(1)
area.createText(setting.getId())
area.setWeight(2)
text = area.createTextField(setting.getId())
area.setWeight(0)
browseButton = area.createButton('browse-button',
style=widgets.Button.STYLE_MINI)
def browseAction():
# Open a file browser for selecting the value for a file
# setting.
settingId = setting.getId()
value = pr.getActive().getValue(settingId)
if not value:
currentValue = ''
else:
currentValue = value.getValue()
selection = chooseFile(settingId + '-selection-title',
currentValue,
setting.hasToExist(),
setting.getAllowedTypes())
if len(selection) > 0:
pr.getActive().setValue(settingId, selection)
browseButton.addReaction(browseAction)
# If the file must exist, don't accept nonexistent files.
if setting.hasToExist():
text.setValidator(lambda fileName: os.path.exists(fileName))
return area
class BoxedArea (Area):
"""A BoxedArea is an Area with a frame around it."""
def __init__(self, boxTitle, parentArea, panel, alignment=0, border=0):
"""Construct a new BoxedArea. Instances of BoxedArea are
always sub-areas.
@param boxTitle Identifier of the box title.
@param parentArea The Area object inside which the BoxedArea
will be added.
@param panel The wxPanel into which all the widgets in the
BoxedArea will be placed.
@param alignment Alignment of widgets inside the area.
@param border Border around widgets.
"""
Area.__init__(self, '', panel, alignment, border, parentArea)
self.label = boxTitle
# Create a wxStaticBox and place it inside the area.
self.box = wx.StaticBox(panel, -1, language.translate(boxTitle))
self.containerSizer = wx.StaticBoxSizer(self.box,
self.sizer.GetOrientation())
# Insert the static box sizer into the area's sizer. All the
# widgets of the area will go inside the container sizer,
# which is the static box sizer.
self.sizer.Add(self.containerSizer, self.weight,
self._getLayoutFlags(), self.border)
def destroy(self):
"""Destroy the area and its contents."""
# First destroy the contents of the area.
self.clear()
self.sizer.Detach(self.containerSizer)
## @todo GTK crashes here if Destroy is called?
#self.containerSizer.Destroy()
self.box.Destroy()
# Proceed with the normal Area destruction.
Area.destroy(self)
def retranslate(self):
"""Update the title of the box."""
pass
class MultiArea (Area):
"""A MultiArea contains multiple subareas. Only one of them will
be visible at a time. All the subareas are visible in the same
space."""
def __init__(self, id, parentArea):
Area.__init__(self, id, parentArea.panel, parentArea=parentArea)
self.currentPage = None
self.pages = {}
events.addNotifyListener(self.onNotify)
def destroy(self):
"""Destroy all the pages."""
events.removeNotifyListener(self.onNotify)
# Hide (detach) all the pages.
self.showPage(None)
for area in self.pages.values():
area.destroy()
Area.destroy()
def getPages(self):
"""Return a list of all the pages of the area.
@return An array of identifier strings.
"""
return self.pages.keys()
def createPage(self, identifier, align=Area.ALIGN_VERTICAL, border=3):
"""Create a subpage in the multiarea. This does not make the
page visible, however.
@param identifier Identifier of the new page.
"""
# Create a new panel for the page.
panel = wx.Panel(self.panel, -1)
if host.isWindows():
panel.SetBackgroundColour(widgets.tabBgColour)
panel.SetBackgroundStyle(wx.BG_STYLE_SYSTEM)
panel.Hide()
# Create a new Area for the page.
area = Area('', panel, alignment=align, border=border)
area.setExpanding(True)
area.setWeight(1)
self.pages[identifier] = area
return area
def removePage(self, identifier):
"""Remove a page from the multiarea.
@param identifier Page to remove.
"""
try:
area = self.pages[identifier]
# Detach (if shown) and destroy the page area.
sizer = area.panel.GetContainingSizer()
if sizer:
sizer.Detach(area.panel)
area.destroy()
# Remove the page from the dictionary.
del self.pages[identifier]
except KeyError:
# Ignore unknown identifiers.
pass
def showPage(self, identifier):
"""Show a specific page. The shown page is added to this
area's containing sizer.
@param identifier Identifier of the page to show. If None,
all pages are hidden.
"""
changed = False
for pageId in self.pages.keys():
area = self.pages[pageId]
sizer = area.panel.GetContainingSizer()
if pageId == identifier:
# Show this page.
if not sizer:
self.containerSizer.Add(area.panel, 1,
wx.EXPAND | wx.ALL, 3)
area.panel.Show()
changed = True
else:
# Hide this page, if not already hidden.
if sizer:
sizer.Detach(area.panel)
area.panel.Hide()
changed = True
if changed:
self.updateLayout()
def onNotify(self, event):
if event.hasId('preparing-windows'):
self.__updateMinSize()
Area.updateLayout(self)
def __updateMinSize(self):
"""Calculate min size based on all pages."""
minWidth = 0
minHeight = 0
for area in self.pages.values():
minSize = area.panel.GetBestSize()
minWidth = max(minWidth, minSize[0])
minHeight = max(minHeight, minSize[1])
self.sizer.SetMinSize((minWidth + 6, minHeight + 6))
self.sizer.Layout()
def refresh(self):
"""Redraw the contents of the multiarea."""
self.panel.Refresh()
class AreaDialog (wx.Dialog):
"""AreaDialog implements a wxDialog that has an Area inside it."""
def __init__(self, parent, wxId, id, size, align=Area.ALIGN_VERTICAL):
wx.Dialog.__init__(self, parent, wxId, language.translate(id),
style=(wx.RESIZE_BORDER | wx.CLOSE_BOX |
wx.CAPTION))
self.dialogId = id
self.widgetMap = {}
if size:
self.SetMinSize(size)
# All the commands that will cause the dialog to be closed.
self.dialogEndCommands = ['ok', 'cancel', 'yes', 'no']
# Create an area inside the dialog.
self.area = Area('', self, alignment=align, border=0)
def getArea(self):
"""Return the Area which contains the dialog's widgets.
@return Area object.
"""
return self.area
def addEndCommand(self, command):
"""Add a new command that will close the dialog.
@param command A command identifier.
"""
self.dialogEndCommands.append(command)
def center(self):
"""Position the dialog in the center of the screen."""
self.Centre()
def run(self):
"""Show the dialog as modal. Won't return until the dialog
has been closed.
@return The command that closed the dialog.
"""
self.area.getWxWidget().Fit(self)
# We'll listen to the common dialog commands (ok, cancel)
# ourselves.
events.addCommandListener(self.handleCommand)
# The command that closes the dialog. We'll assume the dialog
# gets closed.
self.closingCommand = 'cancel'
# Won't return until the dialog closes.
self.ShowModal()
# Stop listening.
events.removeCommandListener(self.handleCommand)
return self.closingCommand
def close(self, closingCommand):
"""Close a modal dialog box that is currently open.
@param closingCommand The command that is reported as being
the one that caused the dialog to close.
"""
self.closingCommand = closingCommand
self.EndModal(0)
def handleCommand(self, event):
"""The dialog listens to commands while it is modal.
@param event A events.Command object.
"""
if event.getId() in self.dialogEndCommands:
self.close(event.getId())
def identifyWidget(self, identifier, widget):
"""Tell the dialog of a widget inside dialog.
@param identifier Identifier of the widget.
@param widget The widget object.
"""
self.widgetMap[identifier] = widget
def enableWidget(self, identifier, doEnable=True):
try:
self.widgetMap[identifier].enable(doEnable)
except:
pass
def disableWidget(self, identifier):
self.enableWidget(identifier, False)
class WizardDialog (wiz.Wizard):
"""A wizard dialog. Automatically manages the Next and Prev
buttons, and opening and closing of the dialog. The pages of the
dialog must be created separately, using WizardPages."""
def __init__(self, title, imageFileName):