widgets.py 26.6 KB
Newer Older
Paul McCarthy's avatar
Paul McCarthy committed
1
2
#!/usr/bin/env python
#
3
# widgets.py - Generate wx GUI widgets for props.PropertyBase objects.
Paul McCarthy's avatar
Paul McCarthy committed
4
5
6
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
7
8
9
10
"""This module provides functions to generate Generate :mod:`wx` GUI widgets
which allow the user to edit the properties of a :class:`.HasProperties`
instance.

Paul McCarthy's avatar
Paul McCarthy committed
11
12
13
14
Most of the functions in this module are not intended to be called directly -
they are used by the :mod:`.build` module. However, a few functions defined
here are made available at the :mod:`props` namespace level, and are intended
to be called by application code:
15
16
17


 .. autosummary::
18
19
    :nosignatures:

20
21
22
23
24
25
26
27
28
29
    makeWidget
    makeListWidgets
    makeSyncWidget
    bindWidget
    unbindWidget


The other functions defined in this module are used by the :mod:`.build`
module, which generates a GUI from a view specification. The following
functions are available:
30
31
32


 .. autosummary::
33
34
    :nosignatures:

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    _FilePath
    _String
    _Real
    _Int
    _Percentage
    _Colour
    _ColourMap
    _LinkBox


Widgets for some other property types are implemented in separate modules,
purely to keep module file sizes down:


 .. autosummary::
50
51
    :nosignatures:

52
53
54
55
56
57
58
59
    ~props.widgets_list._List
    ~props.widgets_bounds._Bounds
    ~props.widgets_point._Point
    ~props.widgets_choice._Choice
    ~props.widgets_boolean._Boolean
    ~props.widgets_number._Number


60
61
 .. warning:: The :mod:`.widgets_list` module has not been looked at
              in a while, and is probably broken.
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
While all of these functions have a standardised signature, some of them
(e.g. the ``_Colour`` function) accept extra arguments which provide some
level of customisation. You can provide these arguments indirectly in the
:class:`.ViewItem` specification for a specific property. For example::


    import props

    class MyObj(props.HasProperties):
        myColour     = props.Colour()
        myBoolean    = props.Boolean()

    view = props.VGroup((

        # Give the colour button a size of 32 * 32
        props.Widget('myColour',  size=(32, 32)),

        # Use a toggle button for the boolean property,
        # using 'boolean.png' as the button icon
        props.Widget('myBoolean', icon='boolean.png') ))

    myobj = MyObj()

    dlg = props.buildDialog(None, myobj, view=view)

    dlg.ShowModal()
Paul McCarthy's avatar
Paul McCarthy committed
90

91
"""
Paul McCarthy's avatar
Paul McCarthy committed
92

93
94
import logging

Paul McCarthy's avatar
Paul McCarthy committed
95
96
97
98
99
import sys

import os
import os.path as op

100
from collections import Iterable
Paul McCarthy's avatar
Paul McCarthy committed
101

102
103
import six

104
import wx
105
106
107

try:                from wx.combo import BitmapComboBox
except ImportError: from wx.adv   import BitmapComboBox
108

109
import fsleyes_widgets.colourbutton as colourbtn
110

111
112

log = logging.getLogger(__name__)
113
114


115
116
117
118
119
def _propBind(hasProps,
              propObj,
              propVal,
              guiObj,
              evType,
120
              widgetGet=None,
121
122
              widgetSet=None,
              widgetDestroy=None):
123
    """Binds a :class:`.PropertyValue` instance to a widget.
124
    
125
126
    Sets up event callback functions such that, on a change to the given
    property value, the value displayed by the given GUI widget will be
127
128
129
130
    updated. Similarly, whenever a GUI event of the specified type (or types -
    you may pass in a list of event types) occurs, the property value will be
    set to the value controlled by the GUI widget.

131
    :param hasProps:      The owning :class:`.HasProperties` instance.
132
    
133
    :param propObj:       The :class:`.PropertyBase` property type.
134
    
135
    :param propVal:       The :class:`.PropertyValue` to be bound.
136
    
137
    :param guiObj:        The :mod:`wx` GUI widget
138
    
139
140
    :param evType:        The event type (or list of event types) which should 
                          be listened for on the ``guiObj``.
141
    
142
143
    :param widgetGet:     Function which returns the current widget value. If
                          ``None``, the ``guiObj.GetValue`` method is used.
144
 
145
146
147
148
149
150
    :param widgetSet:     Function which sets the current widget value. If
                          ``None``, the ``guiObj.SetValue`` method is used.

    :param widgetDestroy: Function which is called if/when the widget is
                          destroyed. Must accept one argument - the
                          :class:`wx.Event` object.
151
    """
152
153
154

    if not isinstance(evType, Iterable): evType = [evType]

155
156
    listenerName    = 'WidgetBind_{}'   .format(id(guiObj))
    listenerAttName = 'WidgetBindAtt_{}'.format(id(guiObj))
157

158
159
160
161
162
163
164
165
    if widgetGet is None:
        widgetGet = guiObj.GetValue
    if widgetSet is None:

        handleNone = True
        widgetSet  = guiObj.SetValue
    else:
        handleNone = False
166
167
168
169

    log.debug('Binding PropertyValue ({}.{} [{}]) to widget {} ({})'.format(
        hasProps.__class__.__name__,
        propVal._name,
170
        id(propVal),
171
172
        guiObj.__class__.__name__, id(guiObj)))

173
    def _guiUpdate(*a):
174
175
176
177
        """
        Called whenever the property value is changed.
        Sets the GUI widget value to that of the property.
        """
178
        value = propVal.get()
179

180
        if widgetGet() == value: return
181
182

        # most wx widgets complain if you try to set their value to None
183
        if handleNone and (value is None): value = ''
184
185
186
187
188
189
190
191
192
193

        log.debug('Updating Widget {} ({}) from {}.{} ({}): {}'.format(
            guiObj.__class__.__name__,
            id(guiObj),
            hasProps.__class__.__name__,
            propVal._name,
            id(hasProps),
            value))
        
        widgetSet(value)
194
195
        
    def _propUpdate(*a):
196
197
198
199
        """
        Called when the value controlled by the GUI widget
        is changed. Updates the property value.
        """
200

201
        value = widgetGet()
202

203
        if propVal.get() == value: return
204

205
206
207
208
209
210
211
212
        log.debug('Updating {}.{} ({}) from widget  {} ({}): {}'.format(
            hasProps.__class__.__name__,
            propVal._name,
            id(hasProps),
            guiObj.__class__.__name__,
            id(guiObj),
            value)) 

213
        propVal.disableListener(listenerName)
214
        propVal.set(value)
215
216
217
218
219
220
221
222
223
224
225
226
227

        # Re-enable the property listener
        # bound to this widget only if the
        # widget has not been destroyed.
        # 
        # This is to prevent a (somewhat
        # harmless) scenario whereby setting
        # the property value results in the
        # deletion of the widget to which it
        # is bound.
        if guiObj:
            propVal.enableListener(listenerName)
        
228

229
230
    def _attUpdate(ctx, att, *a):
        val = propVal.getAttribute(att)
231
232
233
        if att == 'enabled': 
            guiObj.Enable(val)

234
    _guiUpdate(propVal.get())
235
    _attUpdate(hasProps, 'enabled')
236

237
    # set up the callback functions
238
    for ev in evType: guiObj.Bind(ev, _propUpdate)
239
240
    propVal.addListener(         listenerName,    _guiUpdate, weak=False)
    propVal.addAttributeListener(listenerAttName, _attUpdate, weak=False)
241

242
    def onDestroy(ev):
243
        ev.Skip()
244
245
246
247
 
        if ev.GetEventObject() is not guiObj:
            return
        
248
249
250
251
252
253
254
        log.debug('Widget {} ({}) destroyed (removing '
                  'listener {} from {}.{})'.format(
                      guiObj.__class__.__name__,
                      id(guiObj),
                      listenerName,
                      hasProps.__class__.__name__,
                      propVal._name))
255
        propVal.removeListener(         listenerName)
256
        propVal.removeAttributeListener(listenerAttName)
257
258
259

        if widgetDestroy is not None:
            widgetDestroy(ev)
260
261

    guiObj.Bind(wx.EVT_WINDOW_DESTROY, onDestroy)
262

263

264
265
266
267
268
269
270
def _propUnbind(hasProps, propObj, propVal, guiObj, evType):
    """Removes any event binding which has been previously configured via the
    :func:`_propBind` function, between the given :class:`.PropertyValue`
    instance, and the given :mod:`wx` widget.
    """
    if not isinstance(evType, Iterable): evType = [evType]

271
272
    listenerName    = 'WidgetBind_{}'   .format(id(guiObj))
    listenerAttName = 'WidgetBindAtt_{}'.format(id(guiObj)) 
273
274

    propVal.removeListener(         listenerName)
275
    propVal.removeAttributeListener(listenerAttName)
276
277
278
279

    for ev in evType: guiObj.Unbind(ev)


280
def _setupValidation(widget, hasProps, propObj, propVal):
281
    """Configures input validation for the given widget, which is assumed to be
282
    bound to the given ``propVal`` (a :class:`.PropertyValue` object).
283
284
285
286
287

    Any changes to the property value are validated and, if the new value is
    invalid, the widget background colour is changed to a light red, so that
    the user is aware of the invalid-ness.

288
    This function is only used for a few different property types, namely
289
290
291
      - :class:`.String`
      - :class:`.FilePath`
      - :class:`.Number`
292

293
294
    :param widget:   The :mod:`wx` GUI widget.
    
295
    :param hasProps: The owning :class:`.HasProperties` instance.
296
    
297
    :param propObj:  The :class:`.PropertyBase` property type.
298
    
299
    :param propVal:  The :class:`.PropertyValue` instance.
Paul McCarthy's avatar
Paul McCarthy committed
300
301
302
    """

    invalidBGColour = '#ff9999'
303
    validBGColour   = widget.GetBackgroundColour()
Paul McCarthy's avatar
Paul McCarthy committed
304

305
    def _changeBGOnValidate(value, valid, *a):
Paul McCarthy's avatar
Paul McCarthy committed
306
307
308
309
310
311
        """
        Called whenever the property value changes. Checks
        to see if the new value is valid and changes the
        widget background colour according to the validity
        of the new value.
        """
312
313
314
315
316
        
        if valid: newBGColour = validBGColour
        else:     newBGColour = invalidBGColour
        
        widget.SetBackgroundColour(newBGColour)
317
        widget.Refresh()
Paul McCarthy's avatar
Paul McCarthy committed
318

319
320
321
322
323
    # We add a callback listener to the PropertyValue object,
    # rather than to the PropertyBase, as one property may be
    # associated with multiple variables, and we don't want
    # the widgets associated with those other variables to
    # change background.
324
    lName = 'widgets_py_ChangeBG_{}'.format(id(widget))
325
    propVal.addListener(lName, _changeBGOnValidate, weak=False)
326
327
328

    # And ensure that the listener is
    # removed when the widget is destroyed
329
330
331
332
333
    def onDestroy(ev):
        propVal.removeListener(lName)
        ev.Skip()
    
    widget.Bind(wx.EVT_WINDOW_DESTROY, onDestroy)
Paul McCarthy's avatar
Paul McCarthy committed
334
335
336

    # Validate the initial property value,
    # so the background is appropriately set
337
    _changeBGOnValidate(None, propVal.isValid(), None)
Paul McCarthy's avatar
Paul McCarthy committed
338
339
    

340
341
342
def _String(parent, hasProps, propObj, propVal, **kwargs):
    """Creates and returns a :class:`wx.TextCtrl` object, allowing the user to
    edit the given ``propVal`` (managed by a :class:`.String` instance).
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
    :param parent:   The :mod:`wx` parent object.
    
    :param hasProps: The owning :class:`.HasProperties` instance.

    :param propObj:  The :class:`.PropertyBase` instance (assumed to be a
                     :class:`.String`).
    
    :param propVal:  The :class:`.PropertyValue` instance.

    :param kwargs:   Type-specific options.
    """

    widget = wx.TextCtrl(parent)

    # Under linux/GTK, TextCtrl widgets
    # absorb mouse wheel events,  so I'm
    # adding a custom handler to prevent this.
    if wx.Platform == '__WXGTK__':
        def wheel(ev):
            widget.GetParent().GetEventHandler().ProcessEvent(ev)
        widget.Bind(wx.EVT_MOUSEWHEEL, wheel)
    
    # Use a DC object to calculate a decent
    # minimum size for the widget
    dc       = wx.ClientDC(widget)
    textSize = dc.GetTextExtent('w' * 17)
    widgSize = widget.GetBestSize().Get()

    widget.SetMinSize((max(textSize[0], widgSize[0]),
                       max(textSize[1], widgSize[1])))
 
    _propBind(hasProps, propObj, propVal, widget, wx.EVT_TEXT)
    _setupValidation(widget, hasProps, propObj, propVal)
    
    return widget
379
380


381
def _FilePath(parent, hasProps, propObj, propVal, **kwargs):
382
    """Creates and returns a panel containing a :class:`wx.TextCtrl` and a
383
384
385
386
387
    :class:`wx.Button`.

    The button, when clicked, opens a file dialog allowing the user to choose
    a file/directory to open, or a location to save (this depends upon how the
    ``propObj`` [a :class:`.FilePath` instance] object was configured).
388
389

    See the :func:`_String` documentation for details on the parameters.
Paul McCarthy's avatar
Paul McCarthy committed
390
391
    """

392
393
394
395
396
397
398
399
400
401
    # The _lastFilePathDir variable is used to
    # retain the most recently visited directory
    # in file dialogs. New file dialogs are
    # initialised to display this directory.

    # This is currently a global setting, but it
    # may be more appropriate to make it a per-widget
    # setting.  Easily done, just make this a dict,
    # with the widget (or property name) as the key.
    lastFilePathDir = getattr(_FilePath, 'lastFilePathDir', os.getcwd())
Paul McCarthy's avatar
Paul McCarthy committed
402

403
404
    value = propVal.get()
    if value is None: value = ''
Paul McCarthy's avatar
Paul McCarthy committed
405

406
407
    panel   = wx.Panel(parent)
    textbox = wx.TextCtrl(panel)
408
409
410
411
412
413
414
415
416
    button  = wx.Button(panel, label='Choose')

    sizer = wx.BoxSizer(wx.HORIZONTAL)
    sizer.Add(textbox, flag=wx.EXPAND, proportion=1)
    sizer.Add(button,  flag=wx.EXPAND)

    panel.SetSizer(sizer)
    panel.SetAutoLayout(1)
    sizer.Fit(panel)
417
418
419

    exists = propObj.getConstraint(hasProps, 'exists')
    isFile = propObj.getConstraint(hasProps, 'isFile')
420
    
421
    def _choosePath(ev):
Paul McCarthy's avatar
Paul McCarthy committed
422
423
        global _lastFilePathDir

424
        if exists and isFile:
425
426
            dlg = wx.FileDialog(parent,
                                message='Choose file',
427
                                defaultDir=lastFilePathDir,
428
429
430
                                defaultFile=value,
                                style=wx.FD_OPEN)
            
431
        elif exists and (not isFile):
432
433
            dlg = wx.DirDialog(parent,
                               message='Choose directory',
434
                               defaultPath=lastFilePathDir) 
Paul McCarthy's avatar
Paul McCarthy committed
435
436

        else:
437
438
            dlg = wx.FileDialog(parent,
                                message='Save file',
439
                                defaultDir=lastFilePathDir,
440
441
442
                                defaultFile=value,
                                style=wx.FD_SAVE)

Paul McCarthy's avatar
Paul McCarthy committed
443

444
445
446
        dlg.ShowModal()
        path = dlg.GetPath()
        
Paul McCarthy's avatar
Paul McCarthy committed
447
        if path != '' and path is not None:
448
            _FilePath.lastFilePathDir = op.dirname(path)
449
450
451
452
            propVal.set(path)
            
    _setupValidation(textbox, hasProps, propObj, propVal)
    _propBind(hasProps, propObj, propVal, textbox, wx.EVT_TEXT)
Paul McCarthy's avatar
Paul McCarthy committed
453

454
    button.Bind(wx.EVT_BUTTON, _choosePath)
Paul McCarthy's avatar
Paul McCarthy committed
455
    
456
    return panel
Paul McCarthy's avatar
Paul McCarthy committed
457
458


459
def _Real(parent, hasProps, propObj, propVal, **kwargs):
460
    """Creates and returns a widget allowing the user to edit the given
461
462
    :class:`.Real` property value. See the :func:`.widgets_number._Number`
    function for more details.
463
464
465

    See the :func:`_String` documentation for details on the parameters.
    """
466
    from props.widgets_number import _Number
467
    return _Number(parent, hasProps, propObj, propVal, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
468
469


470
def _Int(parent, hasProps, propObj, propVal, **kwargs):
471
    """Creates and returns a widget allowing the user to edit the given
472
473
    :class:`.Int` property value. See the :func:`.widgets_number._Number`
    function for more details.
474
475

    See the :func:`_String` documentation for details on the parameters.
476
477
    """
    from props.widgets_number import _Number
478
    return _Number(parent, hasProps, propObj, propVal, **kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
479
480


481
def _Percentage(parent, hasProps, propObj, propVal, **kwargs):
482
    """Creates and returns a widget allowing the user to edit the given
483
484
    :class:`.Percentage` property value. See the
    :func:`.widgets_number._Number` function for more details.
485
486
487

    See the :func:`_String` documentation for details on the parameters.
    """ 
Paul McCarthy's avatar
Paul McCarthy committed
488
    # TODO Add '%' signs to Scale labels.
489
    from props.widgets_number import _Number
490
    return _Number(parent, hasProps, propObj, propVal, **kwargs) 
Paul McCarthy's avatar
Paul McCarthy committed
491
492
        

493
def _Colour(parent, hasProps, propObj, propVal, size=(16, 16), **kwargs):
494
495
496
497
    """Creates and returns a :class:`.ColourButton` widget, allowing
    the user to modify the given :class:`.Colour` property value.

    :arg size: Desired size, in pixels, of the ``ColourButton``.
498
    """
499
500

    colourButton = colourbtn.ColourButton(parent, size=size)
501

502
    def widgetGet():
503
504

        vals = colourButton.GetValue()
505
506
        return [v / 255.0 for v in vals]
    
507
    def widgetSet(vals):
508
        colour = [int(v * 255.0) for v in vals]
509
        colourButton.SetValue(colour)
510
511
512
513

    _propBind(hasProps,
              propObj,
              propVal,
514
515
              colourButton,
              colourbtn.EVT_COLOUR_BUTTON_EVENT,
516
517
              widgetGet,
              widgetSet)
518

519
    return colourButton
520
521


522
def _makeColourMapBitmap(cmap):
523
524
525
    """Used by the :func:`_ColourMap` function.

    Makes a little bitmap image from a :class:`~matplotlib.colors.Colormap`
526
    instance.
527
528
    """

529
530
    import numpy as np

531
    width, height = 75, 15
532

533
534
    # create a single colour  for each horizontal pixel
    colours = cmap(np.linspace(0.0, 1.0, width))
535

536
537
    # discard alpha values
    colours = colours[:, :3]
538

539
540
    # repeat each horizontal pixel (height) times
    colours = np.tile(colours, (height, 1, 1))
541

542
543
544
    # scale to [0,255] and cast to uint8
    colours = colours * 255
    colours = np.array(colours, dtype=np.uint8)
545

546
547
    # make a wx Bitmap from the colour data
    colours = colours.ravel(order='C')
Paul McCarthy's avatar
Paul McCarthy committed
548
549
550

    if six.PY2: bitmap = wx.BitmapFromBuffer( width, height, colours)
    else:       bitmap = wx.Bitmap.FromBuffer(width, height, colours)
551
    return bitmap
552
553


554
def _ColourMap(parent, hasProps, propObj, propVal, labels=None, **kwargs):
555
556
    """Creates and returns a combobox, allowing the user to change the value
    of the given :class:`.ColourMap` property value.
557

558
559
560
561
562
563
564
565
    :arg labels: A dictionary containing ``{name : label}`` mappings,
                 defining a display name/label for each colour map. If
                 not provided, the colour map ``name`` attribute is used
                 as the display name.

                 Can alternately be a function which accepts a colour map
                 identifier name, and returns its display name.

566
    See also the :func:`_makeColourMapBitmap` function.
567
568
    """

569
570
    import matplotlib.cm as mplcm

571
572
573
574
575
    # These are used by the inner-functions defined
    # below, and are dynamically updated when the
    # list of available colour maps change. I'm 
    # storing each of them in a list, so the inner
    # functions will have access to updated versions.
Paul McCarthy's avatar
Paul McCarthy committed
576
577
    cmapKeys = [list(propObj.getColourMaps(hasProps))]
    cmapObjs = [list(map(mplcm.get_cmap, cmapKeys[0]))]
578

579
    # create the combobox
580
    cbox = BitmapComboBox(parent, style=wx.CB_READONLY | wx.CB_DROPDOWN)
581

582
583
584
585
586
587
588
589
    # OwnerDrawnComboBoxes seem to absorb mouse
    # events and, under OSX/cocoa at least, this
    # causes the currently selected item to
    # change. I don't want this.
    def wheel(ev):
        parent.GetEventHandler().ProcessEvent(ev)
    cbox.Bind(wx.EVT_MOUSEWHEEL, wheel)
    
590
    def widgetGet():
591
592
593
594
        sel = cbox.GetSelection()
        if sel == -1:
            sel = 0
        return cmapObjs[0][sel]
595
596

    def widgetSet(value):
597
598
599
600
        if value is not None:
            cbox.SetSelection(cmapObjs[0].index(value))
        else:
            cbox.SetSelection(0)
601

602
603
604
    # Called when the list of available 
    # colour maps changes - updates the 
    # options displayed in the combobox 
605
    def cmapsChanged(*a):
606

607
        selected    = cbox.GetSelection()
Paul McCarthy's avatar
Paul McCarthy committed
608
609
        cmapKeys[0] = list(propObj.getColourMaps(hasProps))
        cmapObjs[0] = list(map(mplcm.get_cmap, cmapKeys[0]))
610
611

        cbox.Clear()
612

613
614
615
616
617
618
619
        # Store the width of the biggest bitmap, 
        # and the width of the biggest label.
        # the BitmapComboBox doesn't size itself
        # properly on all platforms, so we'll
        # do it manually, dammit
        maxBmpWidth = 0
        maxLblWidth = 0
620
        dc          = wx.ClientDC(cbox)
621

622
623
        # Make a little bitmap for every colour
        # map, and add it to the combobox
624
625
        for cmap in cmapObjs[0]:

626
627
628
629
630
631
632
633
634
635
636
            # Labels can either be None
            if labels is None:
                name = cmap.name

            # Or a function
            elif hasattr(labels, '__call__'):
                name = labels(cmap.name)

            # Or a dictionary
            else:
                name = labels.get(cmap.name, cmap.name)
637
            
638
639
640
            bitmap = _makeColourMapBitmap(cmap)
            cbox.Append(name, bitmap)

641
642
            # use the DC to get the label size
            lblWidth = dc.GetTextExtent(name)[0]
643
            bmpWidth = bitmap.GetWidth()
644

645
646
647
            if bmpWidth > maxBmpWidth: maxBmpWidth = bmpWidth
            if lblWidth > maxLblWidth: maxLblWidth = lblWidth

648
649
650
651
        # Explicitly set the minimum size from
        # the maximum bitmap/label sizes, with 
        # some extra to account for the drop
        # down button
652
653
        cbox.InvalidateBestSize()
        bestHeight = cbox.GetBestSize().GetHeight()
654
        cbox.SetMinSize((maxBmpWidth + maxLblWidth + 40, bestHeight))
655

656
        cbox.SetSelection(selected)
657
        cbox.Refresh()
658
659

    # Initialise the combobox options
660
    cmapsChanged()
661

662
663
664
665
666
667
668
669
    # Make sure the combobox options are updated
    # when the property options change
    lName = 'ColourMap_ComboBox_{}'.format(id(cbox))
    propVal.addAttributeListener(lName, cmapsChanged, weak=False)

    def onDestroy(ev):
        propVal.removeAttributeListener(lName)
        
670
    # Bind the combobox to the property
671
672
673
674
    _propBind(hasProps,
              propObj,
              propVal,
              cbox,
675
676
677
678
              evType=wx.EVT_COMBOBOX,
              widgetGet=widgetGet,
              widgetSet=widgetSet,
              widgetDestroy=onDestroy)
679

680
    # Set the initial combobox selection
681
    currentVal = propVal.get()
682
    if currentVal is None: currentVal = 0
683
    else:                  currentVal = cmapObjs[0].index(currentVal)
684
685
686

    cbox.SetSelection(currentVal)
 
687
688
689
    return cbox


690
def _LinkBox(parent, hasProps, propObj, propVal, **kwargs):
691
692
693
694
695
696
697
    """Creates a 'link' button which toggles synchronisation
    between the property on the given ``hasProps`` instance,
    and its parent.
    """
    propName = propObj.getLabel(hasProps)
    value    = hasProps.isSyncedToParent(propName)
    linkBox  = wx.ToggleButton(parent,
698
                               label=six.u('\u21cb'),
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
                               style=wx.BU_EXACTFIT)
    linkBox.SetValue(value)

    if (hasProps.getParent() is None)                   or \
       (not hasProps.canBeSyncedToParent(    propName)) or \
       (not hasProps.canBeUnsyncedFromParent(propName)):
        linkBox.Enable(False)
        
    else:

        # Update the binding state when the linkbox is modified
        def onLinkBox(ev):
            value = linkBox.GetValue()
            if value: hasProps.syncToParent(    propName)
            else:     hasProps.unsyncFromParent(propName)

        # And update the linkbox when the binding state is modified
        def onSyncProp(*a):
            linkBox.SetValue(hasProps.isSyncedToParent(propName))

        def onDestroy(ev):
            ev.Skip()
            hasProps.removeSyncChangeListener(propName, lName)

        lName = 'widget_LinkBox_{}_{}'.format(propName, linkBox)
        
        linkBox.Bind(wx.EVT_TOGGLEBUTTON,   onLinkBox)
        linkBox.Bind(wx.EVT_WINDOW_DESTROY, onDestroy)
727
        hasProps.addSyncChangeListener(propName, lName, onSyncProp, weak=False)
728
729
730
731

    return linkBox    


732
def makeSyncWidget(parent, hasProps, propName, **kwargs):
733
734
735
736
737
738
739
740
741
742
    """Creates a button which controls synchronisation of the specified
    property on the given ``hasProps`` instance, with the corresponding
    property on its parent.

    See the :func:`makeWidget` function for a description of the
    arguments.
    """
    propObj = hasProps.getProp(propName)
    propVal = propObj.getPropVal(hasProps)

743
    return _LinkBox(parent, hasProps, propObj, propVal, **kwargs)
744
745


746
def makeWidget(parent, hasProps, propName, **kwargs):
747
748
749
750
    """Given ``hasProps`` (a :class:`.HasProperties` instance), ``propName``
    (the name of a property of ``hasProps``), and ``parent``, a GUI object,
    creates and returns a widget, or a panel containing widgets, which may
    be used to edit the property value.
751
752
753
754

    :param parent:       A :mod:`wx` object to be used as the parent for the
                         generated widget(s).
    
755
    :param hasProps:     A :class:`.HasProperties` instance.
756
    
757
758
    :param str propName: Name of the :class:`.PropertyBase` property to
                         generate a widget for.
759
760

    :param kwargs:       Type specific arguments.
Paul McCarthy's avatar
Paul McCarthy committed
761
762
    """

763
    propObj = hasProps.getProp(propName)
764
    propVal = propObj.getPropVal(hasProps)
Paul McCarthy's avatar
Paul McCarthy committed
765

766
    if propObj is None:
Paul McCarthy's avatar
Paul McCarthy committed
767
        raise ValueError('Could not find property {}.{}'.format(
768
            hasProps.__class__.__name__, propName))
Paul McCarthy's avatar
Paul McCarthy committed
769
770
771

    makeFunc = getattr(
        sys.modules[__name__],
772
        '_{}'.format(propObj.__class__.__name__), None)
Paul McCarthy's avatar
Paul McCarthy committed
773
774
775

    if makeFunc is None:
        raise ValueError(
776
            'Unknown property type: {}'.format(propObj.__class__.__name__))
Paul McCarthy's avatar
Paul McCarthy committed
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
    return makeFunc(parent, hasProps, propObj, propVal, **kwargs)


def makeListWidgets(parent, hasProps, propName, **kwargs):
    """Creates a widget for every value in the given list property.
    """

    propObj     = hasProps.getProp(propName)._listType
    propValList = getattr(hasProps, propName).getPropertyValueList()

    makeFunc = getattr(
        sys.modules[__name__],
        '_{}'.format(propObj.__class__.__name__), None)

    if makeFunc is None:
        raise ValueError(
            'Unknown property type: {}'.format(propObj.__class__.__name__))

    widgets = []

    for propVal in propValList:
        widgets.append(makeFunc(parent, hasProps, propObj, propVal, **kwargs))

    return widgets
802
803
804
805
806
807
808
809


def bindWidget(widget,
               hasProps,
               propName,
               evTypes,
               widgetGet=None,
               widgetSet=None):
810
811
    """Binds the given widget to the specified property. See the
    :func:`_propBind` method for details of the arguments.
812
813
814
815
816
817
818
    """

    propObj = hasProps.getProp(   propName)
    propVal = hasProps.getPropVal(propName)

    _propBind(
        hasProps, propObj, propVal, widget, evTypes, widgetGet, widgetSet)
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


def bindListWidgets(widgets,
                    hasProps,
                    propName,
                    evTypes,
                    widgetSets=None,
                    widgetGets=None):
    """Binds the given sequence of widgets to each of the values in the
    specified list property.
    """
    
    if widgetSets is None: widgetSets = [None] * len(widgets)
    if widgetGets is None: widgetGets = [None] * len(widgets)

    propObj     = hasProps.getProp( propName)
    propValList = getattr(hasProps, propName).getPropertyValueList()

    for propVal, widget, wGet, wSet in zip(
            propValList, widgets, widgetGets, widgetSets):
        
        _propBind(hasProps,
                  propObj,
                  propVal,
                  widget,
                  evTypes,
                  wGet,
                  wSet)
847
848
849
850
851
852
853
854
855
856
857


def unbindWidget(widget, hasProps, propName, evTypes):
    """Unbinds the given widget from the specified property, assumed to have
    been previously bound via the :func:`bindWidget` function.
    """

    propObj = hasProps.getProp(   propName)
    propVal = hasProps.getPropVal(propName) 

    _propUnbind(hasProps, propObj, propVal, widget, evTypes)