1#    Copyright (C) 2005 Jeremy S. Sanders
2#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License along
15#    with this program; if not, write to the Free Software Foundation, Inc.,
16#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17###############################################################################
18
19"""Module for holding setting values.
20
21e.g.
22
23s = Int('foo', 5)
24s.get()
25s.set(42)
26s.fromUIText('42')
27"""
28
29from __future__ import division
30import re
31import sys
32
33import numpy as N
34
35from ..compat import cbasestr, cstr, crepr
36from .. import qtall as qt
37from . import controls
38from .settingdb import settingdb, uilocale, ui_floattostring, ui_stringtofloat
39from .reference import ReferenceBase, Reference
40
41from .. import utils
42from .. import datasets
43
44class OnModified(qt.QObject):
45    """onmodified is emitted from an object contained in each setting."""
46    onModified = qt.pyqtSignal()
47
48class Setting(object):
49    """A class to store a value with a particular type."""
50
51    # differentiate widgets, settings and setting
52    nodetype = 'setting'
53
54    typename = 'setting'
55
56    # various items in class hierarchy
57    iswidget = False
58    issetting = True
59    issettings = False
60
61    def __init__(self, name, value, descr='', usertext='',
62                 formatting=False, hidden=False):
63        """Initialise the values.
64
65        name: setting name
66        value: default value and initial value
67        descr:  description of the setting
68        usertext: name of setting for user
69        formatting: whether setting applies to formatting
70        hidden: hide widget from user
71        """
72        self.readonly = False
73        self.parent = None
74        self.name = name
75        self.descr = descr
76        self.usertext = usertext
77        self.formatting = formatting
78        self.hidden = hidden
79        self.default = value
80        self.onmodified = OnModified()
81        self._val = self._ref = None
82
83        # calls the set function for the val property
84        self.val = value
85
86    def _copyHelper(self, before, after, optional):
87        """Help copy an object.
88
89        before are arguments before val
90        after are arguments after val
91        optinal as optional arguments
92        """
93
94        val = self._ref if self._ref else self._val
95
96        args = (self.name,) + before + (val,) + after
97
98        opt = optional.copy()
99        opt['descr'] = self.descr
100        opt['usertext'] = self.usertext
101        opt['formatting'] = self.formatting
102        opt['hidden'] = self.hidden
103
104        obj = self.__class__(*args, **opt)
105        obj.readonly = self.readonly
106        obj.default = self.default
107        return obj
108
109    def copy(self):
110        """Make a setting which has its values copied from this one.
111
112        This needs to be overridden if the constructor changes
113        """
114        return self._copyHelper((), (), {})
115
116    def get(self):
117        """Get the value."""
118
119        if self._ref:
120            return self._ref.resolve(self).get()
121        else:
122            return self._val
123
124    def set(self, v):
125        """Set the value."""
126
127        if isinstance(v, ReferenceBase):
128            self._val = None
129            self._ref = v
130        else:
131            self._val = self.normalize(v)
132            self._ref = None
133
134        self.onmodified.onModified.emit()
135
136    val = property(
137        get, set, None,
138        'Get or modify the value of the setting')
139
140    def isReference(self):
141        """Is this a setting a reference to another object."""
142        return bool(self._ref)
143
144    def getReference(self):
145        """Return the reference object. Raise ValueError if not a reference"""
146        if self._ref:
147            return self._ref
148        else:
149            raise ValueError("Setting is not a reference")
150
151    def getStylesheetLink(self):
152        """Get text that this setting should default to linked to the
153        stylesheet."""
154        path = []
155        obj = self
156        while not obj.parent.iswidget:
157            path.insert(0, obj.name)
158            obj = obj.parent
159        path = ['', 'StyleSheet', obj.parent.typename] + path
160        return '/'.join(path)
161
162    def linkToStylesheet(self):
163        """Make this setting link to stylesheet setting, if possible."""
164        self.set( Reference(self.getStylesheetLink()) )
165
166    @property
167    def path(self):
168        """Return full path of setting."""
169        path = []
170        obj = self
171        while obj is not None:
172            # logic easier to understand here
173            # do not add settings name for settings of widget
174            if not obj.iswidget and obj.parent.iswidget:
175                pass
176            else:
177                if obj.name == '/':
178                    path.insert(0, '')
179                else:
180                    path.insert(0, obj.name)
181            obj = obj.parent
182        return '/'.join(path)
183
184    def toUIText(self):
185        """Convert the type to text to show in UI."""
186        return ""
187
188    def fromUIText(self, text):
189        """Convert text from UI into type for setting.
190
191        Raises utils.InvalidType if cannot convert."""
192        return None
193
194    def saveText(self, saveall, rootname = ''):
195        """Return text to restore the value of this setting."""
196
197        if (saveall or not self.isDefault()) and not self.readonly:
198            if self._ref:
199                return "SetToReference('%s%s', %s)\n" % (
200                    rootname, self.name, crepr(self._ref.value))
201            else:
202                return "Set('%s%s', %s)\n" % (
203                    rootname, self.name, utils.rrepr(self.val))
204        else:
205            return ''
206
207    def setOnModified(self, fn):
208        """Set the function to be called on modification (passing True)."""
209        self.onmodified.onModified.connect(fn)
210
211        if self._ref:
212            # tell references to notify us if they are modified
213            self._ref.setOnModified(self, fn)
214
215    def removeOnModified(self, fn):
216        """Remove the function from the list of function to be called."""
217        self.onmodified.onModified.disconnect(fn)
218
219    def newDefault(self, value):
220        """Update the default and the value."""
221        self.default = value
222        self.val = value
223
224    def isDefault(self):
225        """Is the current value a default?
226        This also returns true if it is linked to the appropriate stylesheet
227        """
228
229        if self._ref and isinstance(self.default, ReferenceBase):
230            return self._ref.value == self.default.value
231        else:
232            return self._val == self.default
233
234    def isDefaultLink(self):
235        """Is this a link to the default stylesheet value."""
236        return self._ref and self._ref.value == self.getStylesheetLink()
237
238    def setSilent(self, val):
239        """Set the setting, without propagating modified flags.
240
241        This shouldn't often be used as it defeats the automatic updation.
242        Used for temporary modifications."""
243
244        self._ref = None
245        self._val = self.normalize(val)
246
247    def normalize(self, val):
248        """Convert external value to normalized form for storing
249
250        Raises a utils.InvalidType if this is not possible."""
251        return val
252
253    def makeControl(self, *args):
254        """Make a qt control for editing the setting.
255
256        The control emits settingValueChanged() when the setting has
257        changed value."""
258
259        return None
260
261    def getDocument(self):
262        """Return document."""
263        p = self.parent
264        while p:
265            if p.iswidget:
266                return p.document
267            p = p.parent
268        return None
269
270    def getWidget(self):
271        """Return associated widget."""
272        w = self.parent
273        while not w.iswidget:
274            w = w.parent
275        return w
276
277    def safeEvalHelper(self, text):
278        """Evaluate an expression, catching naughtiness."""
279        try:
280            comp = self.getDocument().evaluate.compileCheckedExpression(
281                text)
282            if comp is None:
283                raise utils.InvalidType
284            return float( eval(comp, self.getDocument().evaluate.context) )
285        except:
286            raise utils.InvalidType
287
288# forward setting to another setting
289class SettingBackwardCompat(Setting):
290    """Forward setting requests to another setting.
291
292    This is used for backward-compatibility.
293    """
294
295    typename = 'backward-compat'
296
297    def __init__(self, name, newrelpath, val, translatefn=None,
298                 **args):
299        """Point this setting to another.
300        newrelpath is a path relative to this setting's parent
301        """
302
303        self.translatefn = translatefn
304        args['hidden'] = True
305        Setting.__init__(self, name, val, **args)
306        self.relpath = newrelpath
307
308    def getForward(self):
309        """Get setting this setting forwards to."""
310        doc = self.getDocument()
311        return doc.resolveSettingPath(self.parent, self.relpath)
312
313    def normalize(self, val):
314        if self.parent is not None:
315            return self.getForward().normalize(val)
316
317    def toUIText(self):
318        return self.getForward().toUIText()
319
320    def fromUIText(self, val):
321        return self.getForward().fromUIText(val)
322
323    def set(self, val):
324        if self.parent is not None and not isinstance(val, ReferenceBase):
325            if self.translatefn:
326                val = self.translatefn(val)
327            self.getForward().set(val)
328
329    def isDefault(self):
330        return self.getForward().isDefault()
331
332    def get(self):
333        return self.getForward().get()
334
335    def copy(self):
336        return self._copyHelper(
337            (self.relpath,), (), {'translatefn': self.translatefn})
338
339    def makeControl(self, *args):
340        return None
341
342    def saveText(self, saveall, rootname = ''):
343        return ''
344
345    def linkToStylesheet(self):
346        """Do nothing for backward compatibility settings."""
347        pass
348
349# Store strings
350class Str(Setting):
351    """String setting."""
352
353    typename = 'str'
354
355    def normalize(self, val):
356        if isinstance(val, cbasestr):
357            return val
358        raise utils.InvalidType
359
360    def toUIText(self):
361        return self.val
362
363    def fromUIText(self, text):
364        return text
365
366    def makeControl(self, *args):
367        return controls.String(self, *args)
368
369class Notes(Str):
370    """String for making notes."""
371
372    typename = 'str-notes'
373
374    def makeControl(self, *args):
375        return controls.Notes(self, *args)
376
377# Store bools
378class Bool(Setting):
379    """Bool setting."""
380
381    typename = 'bool'
382
383    def normalize(self, val):
384        if type(val) in (bool, int):
385            return bool(val)
386        raise utils.InvalidType
387
388    def toUIText(self):
389        return 'True' if self.val else 'False'
390
391    def fromUIText(self, text):
392        t = text.strip().lower()
393        if t in ('true', '1', 't', 'y', 'yes'):
394            return True
395        elif t in ('false', '0', 'f', 'n', 'no'):
396            return False
397        else:
398            raise utils.InvalidType
399
400    def makeControl(self, *args):
401        return controls.Bool(self, *args)
402
403# Storing integers
404class Int(Setting):
405    """Integer settings."""
406
407    typename = 'int'
408
409    def __init__(self, name, value, minval=-1000000, maxval=1000000,
410                 **args):
411        """Initialise the values.
412
413        minval is minimum possible value of setting
414        maxval is maximum possible value of setting
415        """
416
417        self.minval = minval
418        self.maxval = maxval
419        Setting.__init__(self, name, value, **args)
420
421    def copy(self):
422        """Make a setting which has its values copied from this one.
423
424        This needs to be overridden if the constructor changes
425        """
426        return self._copyHelper((), (), {'minval': self.minval,
427                                         'maxval': self.maxval})
428
429    def normalize(self, val):
430        if isinstance(val, int):
431            if val >= self.minval and val <= self.maxval:
432                return val
433            else:
434                raise utils.InvalidType('Out of range allowed')
435        raise utils.InvalidType
436
437    def toUIText(self):
438        return uilocale.toString(self.val)
439
440    def fromUIText(self, text):
441        i, ok = uilocale.toLongLong(text)
442        if not ok:
443            raise ValueError
444
445        if i >= self.minval and i <= self.maxval:
446            return i
447        else:
448            raise utils.InvalidType('Out of range allowed')
449
450    def makeControl(self, *args):
451        return controls.Int(self, *args)
452
453def _finiteRangeFloat(f, minval=-1e300, maxval=1e300):
454    """Return a finite float in range or raise exception otherwise."""
455    f = float(f)
456    if not N.isfinite(f):
457        raise utils.InvalidType('Finite values only allowed')
458    if f < minval or f > maxval:
459        raise utils.InvalidType('Out of range allowed')
460    return f
461
462# for storing floats
463class Float(Setting):
464    """Float settings."""
465
466    typename = 'float'
467
468    def __init__(self, name, value, minval=-1e200, maxval=1e200,
469                 **args):
470        """Initialise the values.
471
472        minval is minimum possible value of setting
473        maxval is maximum possible value of setting
474        """
475
476        self.minval = minval
477        self.maxval = maxval
478        Setting.__init__(self, name, value, **args)
479
480    def copy(self):
481        """Make a setting which has its values copied from this one.
482
483        This needs to be overridden if the constructor changes
484        """
485        return self._copyHelper((), (), {'minval': self.minval,
486                                         'maxval': self.maxval})
487
488    def normalize(self, val):
489        if isinstance(val, int) or isinstance(val, float):
490            return _finiteRangeFloat(
491                val, minval=self.minval, maxval=self.maxval)
492        raise utils.InvalidType
493
494    def toUIText(self):
495        return ui_floattostring(self.val)
496
497    def fromUIText(self, text):
498        try:
499            f = ui_stringtofloat(text)
500        except ValueError:
501            # try to evaluate
502            f = self.safeEvalHelper(text)
503        return self.normalize(f)
504
505    def makeControl(self, *args):
506        return controls.Edit(self, *args)
507
508class FloatOrAuto(Float):
509    """Save a float or text auto."""
510
511    typename = 'float-or-auto'
512
513    def normalize(self, val):
514        if type(val) in (int, float):
515            return _finiteRangeFloat(val, minval=self.minval, maxval=self.maxval)
516        elif isinstance(val, cbasestr) and val.strip().lower() == 'auto':
517            return 'Auto'
518        else:
519            raise utils.InvalidType
520
521    def toUIText(self):
522        if isinstance(self.val, cbasestr) and self.val.lower() == 'auto':
523            return 'Auto'
524        else:
525            return ui_floattostring(self.val)
526
527    def fromUIText(self, text):
528        if text.strip().lower() == 'auto':
529            return 'Auto'
530        else:
531            return Float.fromUIText(self, text)
532
533    def makeControl(self, *args):
534        return controls.Choice(self, True, ['Auto'], *args)
535
536class FloatSlider(Float):
537    """A float with a slider control."""
538
539    typename = 'float-slider'
540
541    def __init__(self, name, value, step=10, tick=50, scale=1, **args):
542        """Step is the size to step by."""
543        Float.__init__(self, name, value, **args)
544        self.step = step
545        self.tick = tick
546        self.scale = scale
547
548    def copy(self):
549        return self._copyHelper((), (), {
550            'minval': self.minval,
551            'maxval': self.maxval,
552            'step': self.step,
553            'tick': self.tick,
554            'scale': self.scale,
555        })
556
557    def makeControl(self, *args):
558        return controls.FloatSlider(self, *args)
559
560class IntOrAuto(Setting):
561    """Save an int or text auto."""
562
563    typename = 'int-or-auto'
564
565    def normalize(self, val):
566        if isinstance(val, int):
567            return val
568        elif isinstance(val, cbasestr) and val.strip().lower() == 'auto':
569            return 'Auto'
570        else:
571            raise utils.InvalidType
572
573    def toUIText(self):
574        if isinstance(self.val, cbasestr) and self.val.lower() == 'auto':
575            return 'Auto'
576        else:
577            return uilocale.toString(self.val)
578
579    def fromUIText(self, text):
580        if text.strip().lower() == 'auto':
581            return 'Auto'
582        else:
583            i, ok = uilocale.toLongLong(text)
584            if not ok:
585                raise utils.InvalidType
586            return i
587
588    def makeControl(self, *args):
589        return controls.Choice(self, True, ['Auto'], *args)
590
591# these are functions used by the distance setting below.
592# they don't work as class methods
593
594def _distPhys(match, painter, mult):
595    """Convert a physical unit measure in multiples of points."""
596    return painter.pixperpt * mult * float(match.group(1))
597
598def _idistval(val, unit):
599    """Convert value to text, dropping zeros and . points on right."""
600    return ("%.3f" % val).rstrip('0').rstrip('.') + unit
601
602def _distInvPhys(pixdist, painter, mult, unit):
603    """Convert number of pixels into physical distance."""
604    return _idistval(pixdist / (mult*painter.pixperpt), unit)
605
606def _distPerc(match, painter):
607    """Convert from a percentage of maxdim."""
608    return painter.maxdim * 0.01 * float(match.group(1))
609
610def _distInvPerc(pixdist, painter):
611    """Convert pixel distance into percentage."""
612    return _idistval(pixdist * 100. / painter.maxdim, '%')
613
614def _distFrac(match, painter):
615    """Convert from a fraction a/b of maxdim."""
616    try:
617        return painter.maxdim * float(match.group(1))/float(match.group(4))
618    except ZeroDivisionError:
619        return 0.
620
621def _distRatio(match, painter):
622    """Convert from a simple 0.xx ratio of maxdim."""
623
624    # if it's greater than 1 then assume it's a point measurement
625    if float(match.group(1)) > 1.:
626        return _distPhys(match, painter, 1)
627
628    return painter.maxdim * float(match.group(1))
629
630# regular expression to match distances
631distre_expr = r'''^
632 [ ]*                                # optional whitespace
633
634 (\.?[0-9]+|[0-9]+\.[0-9]*)          # a floating point number
635
636 [ ]*                                # whitespace
637
638 (cm|pt|mm|inch|in|"|%||             # ( unit, no unit,
639  (?P<slash>/) )                     # or / )
640
641 (?(slash)[ ]*                       # if it was a slash, match any whitespace
642  (\.?[0-9]+|[0-9]+\.[0-9]*))        # and match following fp number
643
644 [ ]*                                # optional whitespace
645$'''
646
647class Distance(Setting):
648    """A veusz distance measure, e.g. 1pt or 3%."""
649
650    typename = 'distance'
651
652    # match a distance
653    distre = re.compile(distre_expr, re.VERBOSE)
654
655    # functions to convert from unit values to points
656    unit_func = {
657        'cm': lambda match, painter:
658            _distPhys(match, painter, 720/25.4),
659        'pt': lambda match, painter:
660            _distPhys(match, painter, 1.),
661        'mm': lambda match, painter:
662            _distPhys(match, painter, 72/25.4),
663        'in': lambda match, painter:
664            _distPhys(match, painter, 72.),
665        'inch': lambda match, painter:
666            _distPhys(match, painter, 72.),
667        '"': lambda match, painter:
668            _distPhys(match, painter, 72.),
669        '%': _distPerc,
670        '/': _distFrac,
671        '': _distRatio
672        }
673
674    # inverse functions for converting points to units
675    inv_unit_func = {
676        'cm': lambda match, painter:
677            _distInvPhys(match, painter, 720/25.4, 'cm'),
678        'pt': lambda match, painter:
679            _distInvPhys(match, painter, 1., 'pt'),
680        'mm': lambda match, painter:
681            _distInvPhys(match, painter, 72/25.4, 'mm'),
682        'in': lambda match, painter:
683            _distInvPhys(match, painter, 72., 'in'),
684        'inch': lambda match, painter:
685            _distInvPhys(match, painter, 72., 'in'),
686        '"': lambda match, painter:
687            _distInvPhys(match, painter, 72., 'in'),
688        '%': _distInvPerc,
689        '/': _distInvPerc,
690        '': _distInvPerc
691        }
692
693    @classmethod
694    def isDist(kls, dist):
695        """Is the text a valid distance measure?"""
696
697        return kls.distre.match(dist) is not None
698
699    def normalize(self, val):
700        if self.distre.match(val) is not None:
701            return val
702        else:
703            raise utils.InvalidType
704
705    def toUIText(self):
706        # convert decimal point to display locale
707        return self.val.replace('.', uilocale.decimalPoint())
708
709    def fromUIText(self, text):
710        # convert decimal point from display locale
711        text = text.replace(uilocale.decimalPoint(), '.')
712
713        if self.isDist(text):
714            return text
715        else:
716            raise utils.InvalidType
717
718    def makeControl(self, *args):
719        return controls.Distance(self, *args)
720
721    @classmethod
722    def convertDistance(kls, painter, dist):
723        '''Convert a distance to plotter units.
724
725        dist: eg 0.1 (fraction), 10% (percentage), 1/10 (fraction),
726                 10pt, 1cm, 20mm, 1inch, 1in, 1" (size)
727        painter: painter to get metrics to convert physical sizes
728        '''
729
730        # match distance against expression
731        m = kls.distre.match(dist)
732        if m is not None:
733            # lookup function to call to do conversion
734            func = kls.unit_func[m.group(2)]
735            return func(m, painter)
736
737        # none of the regexps match
738        raise ValueError( "Cannot convert distance in form '%s'" %
739                          dist )
740
741    def convert(self, painter):
742        """Convert this setting's distance as above"""
743        return self.convertDistance(painter, self.val)
744
745    def convertPts(self, painter):
746        """Get the distance in points."""
747        return self.convert(painter) / painter.pixperpt
748
749    def convertInverse(self, distpix, painter):
750        """Convert distance in pixels into units of this distance.
751        """
752
753        m = self.distre.match(self.val)
754        if m is not None:
755            # if it matches convert back
756            inversefn = self.inv_unit_func[m.group(2)]
757        else:
758            # otherwise force unit
759            inversefn = self.inv_unit_func['cm']
760
761        # do inverse mapping
762        return inversefn(distpix, painter)
763
764class DistancePt(Distance):
765    """For a distance in points."""
766
767    def makeControl(self, *args):
768        return controls.DistancePt(self, *args)
769
770class DistancePhysical(Distance):
771    """For physical distances (no fractional)."""
772
773    def isDist(self, val):
774        m = self.distre.match(val)
775        if m:
776            # disallow non-physical distances
777            if m.group(2) not in ('/', '', '%'):
778                return True
779        return False
780
781    def makeControl(self, *args):
782        return controls.Distance(self, *args, physical=True)
783
784class DistanceOrAuto(Distance):
785    """A distance or the value Auto"""
786
787    typename = 'distance-or-auto'
788
789    distre = re.compile( distre_expr + r'|^Auto$', re.VERBOSE )
790
791    def isAuto(self):
792        return self.val == 'Auto'
793
794    def makeControl(self, *args):
795        return controls.Distance(self, allowauto=True, *args)
796
797class Choice(Setting):
798    """One out of a list of strings."""
799
800    # maybe should be implemented as a dict to speed up checks
801
802    typename = 'choice'
803
804    def __init__(self, name, vallist, val, descriptions=None,
805                 uilist=None, **args):
806        """Setting val must be in vallist.
807        descriptions is an optional addon to put a tooltip on each item
808        in the control.
809        uilist is a tuple/list of text to show to the user, instead of vallist
810        """
811
812        assert type(vallist) in (list, tuple)
813
814        self.vallist = vallist
815        self.descriptions = descriptions
816        self.uilist = uilist
817
818        Setting.__init__(self, name, val, **args)
819
820    def copy(self):
821        """Make a copy of the setting."""
822        return self._copyHelper(
823            (self.vallist,), (), {
824                'descriptions': self.descriptions, 'uilist': self.uilist})
825
826    def normalize(self, val):
827        if val in self.vallist:
828            return val
829        else:
830            raise utils.InvalidType
831
832    def toUIText(self):
833        return self.val
834
835    def fromUIText(self, text):
836        if text in self.vallist:
837            return text
838        else:
839            raise utils.InvalidType
840
841    def makeControl(self, *args):
842        return controls.Choice(self, False, self.vallist,
843                               descriptions=self.descriptions,
844                               uilist=self.uilist, *args)
845
846class ChoiceOrMore(Setting):
847    """One out of a list of strings, or anything else."""
848
849    # maybe should be implemented as a dict to speed up checks
850
851    typename = 'choice-or-more'
852
853    def __init__(self, name, vallist, val, descriptions=None, **args):
854        """Setting has val must be in vallist.
855        descriptions is an optional addon to put a tooltip on each item
856        in the control
857        """
858
859        self.vallist = vallist
860        self.descriptions = descriptions
861        Setting.__init__(self, name, val, **args)
862
863    def copy(self):
864        """Make a copy of the setting."""
865        return self._copyHelper(
866            (self.vallist,), (), {
867                'descriptions': self.descriptions})
868
869    def normalize(self, val):
870        return val
871
872    def toUIText(self):
873        return self.val
874
875    def fromUIText(self, text):
876        return text
877
878    def makeControl(self, *args):
879        argsv = {'descriptions': self.descriptions}
880        return controls.Choice(self, True, self.vallist, *args, **argsv)
881
882class FloatChoice(ChoiceOrMore):
883    """A numeric value, which can also be chosen from the list of values."""
884
885    typename = 'float-choice'
886
887    def normalize(self, val):
888        if isinstance(val, int) or isinstance(val, float):
889            return _finiteRangeFloat(val)
890        raise utils.InvalidType
891
892    def toUIText(self):
893        return ui_floattostring(self.val)
894
895    def fromUIText(self, text):
896        try:
897            f = ui_stringtofloat(text)
898        except ValueError:
899            # try to evaluate
900            f = self.safeEvalHelper(text)
901        return self.normalize(f)
902
903    def makeControl(self, *args):
904        argsv = {'descriptions': self.descriptions}
905        strings = [ui_floattostring(x) for x in self.vallist]
906        return controls.Choice(self, True, strings, *args, **argsv)
907
908class FloatDict(Setting):
909    """A dictionary, taking floats as values."""
910
911    typename = 'float-dict'
912
913    def normalize(self, val):
914        if type(val) != dict:
915            raise utils.InvalidType
916
917        for v in val.values():
918            if type(v) not in (float, int):
919                raise utils.InvalidType
920
921        # return copy
922        return dict(val)
923
924    def toUIText(self):
925        text = ['%s = %s' % (k, ui_floattostring(self.val[k]))
926                for k in sorted(self.val)]
927        return '\n'.join(text)
928
929    def fromUIText(self, text):
930        """Do conversion from list of a=X\n values."""
931
932        out = {}
933        # break up into lines
934        for l in text.split('\n'):
935            l = l.strip()
936            if len(l) == 0:
937                continue
938
939            # break up using =
940            p = l.strip().split('=')
941
942            if len(p) != 2:
943                raise utils.InvalidType
944
945            try:
946                v = ui_stringtofloat(p[1])
947            except ValueError:
948                raise utils.InvalidType
949
950            out[ p[0].strip() ] = v
951        return out
952
953    def makeControl(self, *args):
954        return controls.MultiLine(self, *args)
955
956class FloatList(Setting):
957    """A list of float values."""
958
959    typename = 'float-list'
960
961    def normalize(self, val):
962        if type(val) not in (list, tuple):
963            raise utils.InvalidType
964
965        # horribly slow test for invalid entries
966        out = []
967        for i in val:
968            if type(i) not in (float, int):
969                raise utils.InvalidType
970            else:
971                out.append( float(i) )
972        return out
973
974    def toUIText(self):
975        """Make a string a, b, c."""
976        # can't use the comma for splitting if used as a decimal point
977
978        join = ', '
979        if uilocale.decimalPoint() == ',':
980            join = '; '
981        return join.join( [ui_floattostring(x) for x in self.val] )
982
983    def fromUIText(self, text):
984        """Convert from a, b, c or a b c."""
985
986        # don't use commas if it is the decimal separator
987        splitre = r'[\t\n, ]+'
988        if uilocale.decimalPoint() == ',':
989            splitre = r'[\t\n; ]+'
990
991        out = []
992        for x in re.split(splitre, text.strip()):
993            if x:
994                try:
995                    out.append( ui_stringtofloat(x) )
996                except ValueError:
997                    out.append( self.safeEvalHelper(x) )
998        return out
999
1000    def makeControl(self, *args):
1001        return controls.String(self, *args)
1002
1003class WidgetPath(Str):
1004    """A setting holding a path to a widget. This is checked for validity."""
1005
1006    typename = 'widget-path'
1007
1008    def __init__(self, name, val, relativetoparent=True,
1009                 allowedwidgets = None,
1010                 **args):
1011        """Initialise the setting.
1012
1013        The widget is located relative to
1014        parent if relativetoparent is True, otherwise this widget.
1015
1016        If allowedwidgets is not None, only those widgets types in the list are
1017        allowed by this setting.
1018        """
1019
1020        Str.__init__(self, name, val, **args)
1021        self.relativetoparent = relativetoparent
1022        self.allowedwidgets = allowedwidgets
1023
1024    def copy(self):
1025        """Make a copy of the setting."""
1026        return self._copyHelper((), (),
1027                                {'relativetoparent': self.relativetoparent,
1028                                 'allowedwidgets': self.allowedwidgets})
1029
1030    def getReferredWidget(self, val = None):
1031        """Get the widget referred to. We double-check here to make sure
1032        it's the one.
1033
1034        Returns None if setting is blank
1035        utils.InvalidType is raised if there's a problem
1036        """
1037
1038        # this is a bit of a hack, so we don't have to pass a value
1039        # for the setting (which we need to from normalize)
1040        if val is None:
1041            val = self.val
1042
1043        if val == '':
1044            return None
1045
1046        # find the widget associated with this setting
1047        widget = self
1048        while not widget.iswidget:
1049            widget = widget.parent
1050
1051        # usually makes sense to give paths relative to a parent of a widget
1052        if self.relativetoparent:
1053            widget = widget.parent
1054
1055        # resolve the text to a widget
1056        try:
1057            widget = widget.document.resolveWidgetPath(widget, val)
1058        except ValueError:
1059            raise utils.InvalidType
1060
1061        # check the widget against the list of allowed types if given
1062        if self.allowedwidgets is not None:
1063            allowed = False
1064            for c in self.allowedwidgets:
1065                if isinstance(widget, c):
1066                    allowed = True
1067            if not allowed:
1068                raise utils.InvalidType
1069
1070        return widget
1071
1072class Dataset(Str):
1073    """A setting to choose from the possible datasets."""
1074
1075    typename = 'dataset'
1076
1077    def __init__(self, name, val, dimensions=1, datatype='numeric',
1078                 **args):
1079        """
1080        dimensions is the number of dimensions the dataset needs
1081        """
1082
1083        self.dimensions = dimensions
1084        self.datatype = datatype
1085        Setting.__init__(self, name, val, **args)
1086
1087    def copy(self):
1088        """Make a setting which has its values copied from this one."""
1089        return self._copyHelper((), (),
1090                                {'dimensions': self.dimensions,
1091                                 'datatype': self.datatype})
1092
1093    def makeControl(self, *args):
1094        """Allow user to choose between the datasets."""
1095        return controls.Dataset(self, self.getDocument(), self.dimensions,
1096                                self.datatype, *args)
1097
1098    def getData(self, doc):
1099        """Return a list of datasets entered."""
1100        d = doc.data.get(self.val)
1101        if ( d is not None and
1102             d.datatype == self.datatype and
1103             (d.dimensions == self.dimensions or self.dimensions == 'all') ):
1104                 return d
1105
1106class Strings(Setting):
1107    """A multiple set of strings."""
1108
1109    typename = 'str-multi'
1110
1111    def normalize(self, val):
1112        """Takes a tuple/list of strings:
1113        ('ds1','ds2'...)
1114        """
1115
1116        if isinstance(val, cbasestr):
1117            return (val, )
1118
1119        if type(val) not in (list, tuple):
1120            raise utils.InvalidType
1121
1122        # check each entry in the list is appropriate
1123        for ds in val:
1124            if not isinstance(ds, cbasestr):
1125                raise utils.InvalidType
1126
1127        return tuple(val)
1128
1129    def makeControl(self, *args):
1130        """Allow user to choose between the datasets."""
1131        return controls.Strings(self, self.getDocument(), *args)
1132
1133class Datasets(Setting):
1134    """A setting to choose one or more of the possible datasets."""
1135
1136    typename = 'dataset-multi'
1137
1138    def __init__(self, name, val, dimensions=1, datatype='numeric',
1139                 **args):
1140        """
1141        dimensions is the number of dimensions the dataset needs
1142        """
1143
1144        Setting.__init__(self, name, val, **args)
1145        self.dimensions = dimensions
1146        self.datatype = datatype
1147
1148    def normalize(self, val):
1149        """Takes a tuple/list of strings:
1150        ('ds1','ds2'...)
1151        """
1152
1153        if isinstance(val, cbasestr):
1154            return (val, )
1155
1156        if type(val) not in (list, tuple):
1157            raise utils.InvalidType
1158
1159        # check each entry in the list is appropriate
1160        for ds in val:
1161            if not isinstance(ds, cbasestr):
1162                raise utils.InvalidType
1163
1164        return tuple(val)
1165
1166    def copy(self):
1167        """Make a setting which has its values copied from this one."""
1168        return self._copyHelper((), (),
1169                                {'dimensions': self.dimensions,
1170                                 'datatype': self.datatype})
1171
1172    def makeControl(self, *args):
1173        """Allow user to choose between the datasets."""
1174        return controls.Datasets(self, self.getDocument(), self.dimensions,
1175                                 self.datatype, *args)
1176
1177    def getData(self, doc):
1178        """Return a list of datasets entered."""
1179        out = []
1180        for name in self.val:
1181            d = doc.data.get(name)
1182            if ( d is not None and
1183                 d.datatype == self.datatype and
1184                 d.dimensions == self.dimensions ):
1185                out.append(d)
1186        return out
1187
1188class DatasetExtended(Dataset):
1189    """Choose a dataset, give an expression or specify a list of float
1190    values."""
1191
1192    typename = 'dataset-extended'
1193
1194    def normalize(self, val):
1195        """Check is a string (dataset name or expression) or a list of
1196        floats (numbers).
1197        """
1198
1199        if isinstance(val, cbasestr):
1200            return val
1201        elif self.dimensions == 1:
1202            # list of numbers only allowed for 1d datasets
1203            if isinstance(val, float) or isinstance(val, int):
1204                return [val]
1205            else:
1206                try:
1207                    return [float(x) for x in val]
1208                except (TypeError, ValueError):
1209                    pass
1210        raise utils.InvalidType
1211
1212    def toUIText(self):
1213        if isinstance(self.val, cbasestr):
1214            return self.val
1215        else:
1216            # join based on , or ; depending on decimal point
1217            join = ', '
1218            if uilocale.decimalPoint() == ',':
1219                join = '; '
1220            return join.join( [ ui_floattostring(x)
1221                                for x in self.val ] )
1222
1223    def fromUIText(self, text):
1224        """Convert from text."""
1225
1226        text = text.strip()
1227
1228        if self.dimensions > 1:
1229            return text
1230
1231        # split based on , or ; depending on decimal point
1232        splitre = r'[\t\n, ]+'
1233        if uilocale.decimalPoint() == ',':
1234            splitre = r'[\t\n; ]+'
1235
1236        out = []
1237        for x in re.split(splitre, text):
1238            if x:
1239                try:
1240                    out.append( ui_stringtofloat(x) )
1241                except ValueError:
1242                    # fail conversion, so exit with text
1243                    return text
1244        return out
1245
1246    def getFloatArray(self, doc):
1247        """Get a numpy of values or None."""
1248        if isinstance(self.val, cbasestr):
1249            ds = doc.evaluate.evalDatasetExpression(
1250                self.val, datatype=self.datatype, dimensions=self.dimensions)
1251            if ds:
1252                # get numpy array of values
1253                return N.array(ds.data)
1254        else:
1255            # list of values
1256            return N.array(self.val)
1257        return None
1258
1259    def isDataset(self, doc):
1260        """Is this setting a dataset?"""
1261        return (isinstance(self.val, cbasestr) and
1262                doc.data.get(self.val))
1263
1264    def isEmpty(self):
1265        """Is this unset?"""
1266        return self.val == [] or self.val == ''
1267
1268    def getData(self, doc):
1269        """Return veusz dataset"""
1270        if isinstance(self.val, cbasestr):
1271            return doc.evaluate.evalDatasetExpression(
1272                self.val, datatype=self.datatype, dimensions=self.dimensions)
1273        else:
1274            return datasets.valsToDataset(
1275                self.val, self.datatype, self.dimensions)
1276
1277class DatasetOrStr(Dataset):
1278    """Choose a dataset or enter a string.
1279
1280    Non string datasets are converted to string arrays using this.
1281    """
1282
1283    typename = 'dataset-or-str'
1284
1285    def __init__(self, name, val, **args):
1286        Dataset.__init__(self, name, val, datatype='text', **args)
1287
1288    def getData(self, doc, checknull=False):
1289        """Return either a list of strings, a single item list.
1290        If checknull then None is returned if blank
1291        """
1292        if doc:
1293            ds = doc.data.get(self.val)
1294            if ds and ds.dimensions == 1:
1295                return doc.formatValsWithDatatypeToText(
1296                    ds.data, ds.displaytype)
1297        if checknull and not self.val:
1298            return None
1299        else:
1300            return [cstr(self.val)]
1301
1302    def makeControl(self, *args):
1303        return controls.DatasetOrString(self, self.getDocument(), *args)
1304
1305    def copy(self):
1306        """Make a setting which has its values copied from this one."""
1307        return self._copyHelper((), (), {})
1308
1309class Color(ChoiceOrMore):
1310    """A color setting."""
1311
1312    typename = 'color'
1313
1314    def __init__(self, name, value, **args):
1315        """Initialise the color setting with the given name, default
1316        and description."""
1317        ChoiceOrMore.__init__(self, name, [], value, **args)
1318
1319    def copy(self):
1320        """Make a copy of the setting."""
1321        return self._copyHelper((), (), {})
1322
1323    def color(self, painter, dataindex=0):
1324        """Return QColor from color.
1325
1326        painter is a Veusz Painter
1327        dataindex is index for automatically getting colors for subdatasets.
1328        """
1329
1330        if self.val.lower() == 'auto':
1331            # lookup widget
1332            w = self.parent
1333            while w is not None and not w.iswidget:
1334                w = w.parent
1335            if w is None:
1336                return qt.QColor()
1337            # get automatic color
1338            return painter.docColor(w.autoColor(painter, dataindex=dataindex))
1339        else:
1340            return painter.docColor(self.val)
1341
1342    def makeControl(self, *args):
1343        return controls.Color(self, *args)
1344
1345class FillStyle(Choice):
1346    """A setting for the different fill styles provided by Qt."""
1347
1348    typename = 'fill-style'
1349
1350    _fillstyles = [
1351        'solid', 'horizontal', 'vertical', 'cross',
1352        'forward diagonals', 'backward diagonals',
1353        'diagonal cross',
1354        '94% dense', '88% dense', '63% dense', '50% dense',
1355        '37% dense', '12% dense', '6% dense'
1356    ]
1357
1358    _fillcnvt = {
1359        'solid': qt.Qt.SolidPattern,
1360        'horizontal': qt.Qt.HorPattern,
1361        'vertical': qt.Qt.VerPattern,
1362        'cross': qt.Qt.CrossPattern,
1363        'forward diagonals': qt.Qt.FDiagPattern,
1364        'backward diagonals': qt.Qt.BDiagPattern,
1365        'diagonal cross': qt.Qt.DiagCrossPattern,
1366        '94% dense': qt.Qt.Dense1Pattern,
1367        '88% dense': qt.Qt.Dense2Pattern,
1368        '63% dense': qt.Qt.Dense3Pattern,
1369        '50% dense': qt.Qt.Dense4Pattern,
1370        '37% dense': qt.Qt.Dense5Pattern,
1371        '12% dense': qt.Qt.Dense6Pattern,
1372        '6% dense': qt.Qt.Dense7Pattern,
1373    }
1374
1375    controls.FillStyle._fills = _fillstyles
1376    controls.FillStyle._fillcnvt = _fillcnvt
1377
1378    def __init__(self, name, value, **args):
1379        Choice.__init__(self, name, self._fillstyles, value, **args)
1380
1381    def copy(self):
1382        """Make a copy of the setting."""
1383        return self._copyHelper((), (), {})
1384
1385    def qtStyle(self):
1386        """Return Qt ID of fill."""
1387        return self._fillcnvt[self.val]
1388
1389    def makeControl(self, *args):
1390        return controls.FillStyle(self, *args)
1391
1392class LineStyle(Choice):
1393    """A setting choosing a particular line style."""
1394
1395    typename = 'line-style'
1396
1397    # list of allowed line styles
1398    _linestyles = [
1399        'solid', 'dashed', 'dotted',
1400        'dash-dot', 'dash-dot-dot', 'dotted-fine',
1401        'dashed-fine', 'dash-dot-fine',
1402        'dot1', 'dot2', 'dot3', 'dot4',
1403        'dash1', 'dash2', 'dash3', 'dash4', 'dash5',
1404        'dashdot1', 'dashdot2', 'dashdot3'
1405    ]
1406
1407    # convert from line styles to Qt constants and a custom pattern (if any)
1408    _linecnvt = {
1409        'solid': (qt.Qt.SolidLine, None),
1410        'dashed': (qt.Qt.DashLine, None),
1411        'dotted': (qt.Qt.DotLine, None),
1412        'dash-dot': (qt.Qt.DashDotLine, None),
1413        'dash-dot-dot': (qt.Qt.DashDotDotLine, None),
1414        'dotted-fine': (qt.Qt.CustomDashLine, [2, 4]),
1415        'dashed-fine': (qt.Qt.CustomDashLine, [8, 4]),
1416        'dash-dot-fine': (qt.Qt.CustomDashLine, [8, 4, 2, 4]),
1417        'dot1': (qt.Qt.CustomDashLine, [0.1, 2]),
1418        'dot2': (qt.Qt.CustomDashLine, [0.1, 4]),
1419        'dot3': (qt.Qt.CustomDashLine, [0.1, 6]),
1420        'dot4': (qt.Qt.CustomDashLine, [0.1, 8]),
1421        'dash1': (qt.Qt.CustomDashLine, [4, 4]),
1422        'dash2': (qt.Qt.CustomDashLine, [4, 8]),
1423        'dash3': (qt.Qt.CustomDashLine, [8, 8]),
1424        'dash4': (qt.Qt.CustomDashLine, [16, 8]),
1425        'dash5': (qt.Qt.CustomDashLine, [16, 16]),
1426        'dashdot1': (qt.Qt.CustomDashLine, [0.1, 4, 4, 4]),
1427        'dashdot2': (qt.Qt.CustomDashLine, [0.1, 4, 8, 4]),
1428        'dashdot3': (qt.Qt.CustomDashLine, [0.1, 2, 4, 2]),
1429    }
1430
1431    controls.LineStyle._lines = _linestyles
1432
1433    def __init__(self, name, default, **args):
1434        Choice.__init__(self, name, self._linestyles, default, **args)
1435
1436    def copy(self):
1437        """Make a copy of the setting."""
1438        return self._copyHelper((), (), {})
1439
1440    def qtStyle(self):
1441        """Get Qt ID of chosen line style."""
1442        return self._linecnvt[self.val]
1443
1444    def makeControl(self, *args):
1445        return controls.LineStyle(self, *args)
1446
1447class Axis(Str):
1448    """A setting to hold the name of an axis.
1449
1450    direction is 'horizontal', 'vertical' or 'both'
1451    """
1452
1453    typename = 'axis'
1454
1455    def __init__(self, name, val, direction, **args):
1456        """Initialise using the document, so we can get the axes later.
1457
1458        direction is horizontal or vertical to specify the type of axis to
1459        show
1460        """
1461
1462        Setting.__init__(self, name, val, **args)
1463        self.direction = direction
1464
1465    def copy(self):
1466        """Make a copy of the setting."""
1467        return self._copyHelper((), (self.direction,), {})
1468
1469    def makeControl(self, *args):
1470        """Allows user to choose an axis or enter a name."""
1471        return controls.Axis(self, self.getDocument(), self.direction, *args)
1472
1473class WidgetChoice(Str):
1474    """Hold the name of a child widget."""
1475
1476    typename = 'widget-choice'
1477
1478    def __init__(self, name, val, widgettypes={}, **args):
1479        """Choose widgets from (named) type given."""
1480        Setting.__init__(self, name, val, **args)
1481        self.widgettypes = widgettypes
1482
1483    def copy(self):
1484        """Make a copy of the setting."""
1485        return self._copyHelper((), (),
1486                                {'widgettypes': self.widgettypes})
1487
1488    def buildWidgetList(self, level, widget, outdict):
1489        """A recursive helper to build up a list of possible widgets.
1490
1491        This iterates over widget's children, and adds widgets as tuples
1492        to outdict using outdict[name] = (widget, level)
1493
1494        Lower level images of the same name outweigh other images further down
1495        the tree
1496        """
1497
1498        for child in widget.children:
1499            if child.typename in self.widgettypes:
1500                if (child.name not in outdict) or (outdict[child.name][1]>level):
1501                    outdict[child.name] = (child, level)
1502            else:
1503                self.buildWidgetList(level+1, child, outdict)
1504
1505    def getWidgetList(self):
1506        """Return a dict of valid widget names and the corresponding objects."""
1507
1508        # find widget which contains setting
1509        widget = self.parent
1510        while not widget.iswidget and widget is not None:
1511            widget = widget.parent
1512
1513        # get widget's parent
1514        if widget is not None:
1515            widget = widget.parent
1516
1517        # get list of widgets from recursive find
1518        widgets = {}
1519        if widget is not None:
1520            self.buildWidgetList(0, widget, widgets)
1521
1522        # turn (object, level) pairs into object
1523        outdict = {}
1524        for name, val in widgets.items():
1525            outdict[name] = val[0]
1526
1527        return outdict
1528
1529    def findWidget(self):
1530        """Find the image corresponding to this setting.
1531
1532        Returns Image object if succeeds or None if fails
1533        """
1534
1535        widgets = self.getWidgetList()
1536        try:
1537            return widgets[self.get()]
1538        except KeyError:
1539            return None
1540
1541    def makeControl(self, *args):
1542        """Allows user to choose an image widget or enter a name."""
1543        return controls.WidgetChoice(self, self.getDocument(), *args)
1544
1545class Marker(Choice):
1546    """Choose a marker type from one allowable."""
1547
1548    typename = 'marker'
1549
1550    def __init__(self, name, value, **args):
1551        Choice.__init__(self, name, utils.MarkerCodes, value, **args)
1552
1553    def copy(self):
1554        """Make a copy of the setting."""
1555        return self._copyHelper((), (), {})
1556
1557    def makeControl(self, *args):
1558        return controls.Marker(self, *args)
1559
1560class Arrow(Choice):
1561    """Choose an arrow type from one allowable."""
1562
1563    typename = 'arrow'
1564
1565    def __init__(self, name, value, **args):
1566        Choice.__init__(self, name, utils.ArrowCodes, value, **args)
1567
1568    def copy(self):
1569        """Make a copy of the setting."""
1570        return self._copyHelper((), (), {})
1571
1572    def makeControl(self, *args):
1573        return controls.Arrow(self, *args)
1574
1575class LineSet(Setting):
1576    """A setting which corresponds to a set of lines.
1577    """
1578
1579    typename='line-multi'
1580
1581    def normalize(self, val):
1582        """Takes a tuple/list of tuples:
1583        [('dotted', '1pt', 'color', <trans>, False), ...]
1584
1585        These are style, width, color, and hide or
1586        style, widget, color, transparency, hide
1587        """
1588
1589        if type(val) not in (list, tuple):
1590            raise utils.InvalidType
1591
1592        # check each entry in the list is appropriate
1593        for line in val:
1594            try:
1595                style, width, color, hide = line
1596            except ValueError:
1597                raise utils.InvalidType
1598
1599            if ( not isinstance(color, cbasestr) or
1600                 not Distance.isDist(width) or
1601                 style not in LineStyle._linestyles or
1602                 type(hide) not in (int, bool) ):
1603                raise utils.InvalidType
1604
1605        return val
1606
1607    def makeControl(self, *args):
1608        """Make specialised lineset control."""
1609        return controls.LineSet(self, *args)
1610
1611    def makePen(self, painter, row):
1612        """Make a pen for the painter using row.
1613
1614        If row is outside of range, then cycle
1615        """
1616
1617        if len(self.val) == 0:
1618            return qt.QPen(qt.Qt.NoPen)
1619        else:
1620            row = row % len(self.val)
1621            v = self.val[row]
1622            style, width, color, hide = v
1623            width = Distance.convertDistance(painter, width)
1624            style, dashpattern = LineStyle._linecnvt[style]
1625            col = painter.docColor(color)
1626            pen = qt.QPen(col, width, style)
1627
1628            if dashpattern:
1629                pen.setDashPattern(dashpattern)
1630
1631            if hide:
1632                pen.setStyle(qt.Qt.NoPen)
1633            return pen
1634
1635class FillSet(Setting):
1636    """A setting which corresponds to a set of fills.
1637
1638    This setting keeps an internal array of LineSettings.
1639    """
1640
1641    typename = 'fill-multi'
1642
1643    def normalize(self, val):
1644        """Takes a tuple/list of tuples:
1645        [('solid', 'color', False), ...]
1646
1647        These are color, fill style, and hide or
1648        color, fill style, and hide
1649
1650        (style, color, hide,
1651        [optional transparency, linewidth,
1652         linestyle, spacing, backcolor, backtrans, backhide]])
1653
1654        """
1655
1656        if type(val) not in (list, tuple):
1657            raise utils.InvalidType
1658
1659        # check each entry in the list is appropriate
1660        for fill in val:
1661            try:
1662                style, color, hide = fill[:3]
1663            except ValueError:
1664                raise utils.InvalidType
1665
1666            if ( not isinstance(color, cbasestr) or
1667                 style not in utils.extfillstyles or
1668                 type(hide) not in (int, bool) or
1669                 len(fill) not in (3, 10) ):
1670                raise utils.InvalidType
1671
1672        return val
1673
1674    def makeControl(self, *args):
1675        """Make specialised lineset control."""
1676        return controls.FillSet(self, *args)
1677
1678    def returnBrushExtended(self, row):
1679        """Return BrushExtended for the row."""
1680        from . import collections
1681        s = collections.BrushExtended('tempbrush')
1682        s.parent = self
1683
1684        if len(self.val) == 0:
1685            s.hide = True
1686        else:
1687            v = self.val[row % len(self.val)]
1688            s.style = v[0]
1689            s.color = v[1]
1690            s.hide = v[2]
1691            if len(v) == 10:
1692                (s.transparency, s.linewidth, s.linestyle,
1693                 s.patternspacing, s.backcolor,
1694                 s.backtransparency, s.backhide) = v[3:]
1695        return s
1696
1697class Filename(Str):
1698    """Represents a filename setting."""
1699
1700    typename = 'filename'
1701
1702    def makeControl(self, *args):
1703        return controls.Filename(self, 'file', *args)
1704
1705    def normalize(self, val):
1706        if sys.platform == 'win32':
1707            val = val.replace('\\', '/')
1708        return val
1709
1710class ImageFilename(Filename):
1711    """Represents an image filename setting."""
1712
1713    typename = 'filename-image'
1714
1715    def makeControl(self, *args):
1716        return controls.Filename(self, 'image', *args)
1717
1718class FontFamily(Str):
1719    """Represents a font family."""
1720
1721    typename = 'font-family'
1722
1723    def makeControl(self, *args):
1724        """Make a special font combobox."""
1725        return controls.FontFamily(self, *args)
1726
1727class ErrorStyle(Choice):
1728    """Error bar style.
1729    The allowed values are below in _errorstyles.
1730    """
1731
1732    typename = 'errorbar-style'
1733
1734    _errorstyles = (
1735        'none',
1736        'bar', 'barends', 'box', 'diamond', 'curve',
1737        'barbox', 'bardiamond', 'barcurve',
1738        'boxfill', 'diamondfill', 'curvefill',
1739        'fillvert', 'fillhorz',
1740        'linevert', 'linehorz',
1741        'linevertbar', 'linehorzbar',
1742        'barhi', 'barlo',
1743        'barendshi', 'barendslo',
1744        'linehorzlo', 'linehorzhi', 'linevertlo', 'lineverthi',
1745        )
1746
1747    controls.ErrorStyle._errorstyles  = _errorstyles
1748
1749    def __init__(self, name, value, **args):
1750        Choice.__init__(self, name, self._errorstyles, value, **args)
1751
1752    def copy(self):
1753        """Make a copy of the setting."""
1754        return self._copyHelper((), (), {})
1755
1756    def makeControl(self, *args):
1757        return controls.ErrorStyle(self, *args)
1758
1759class AlignHorz(Choice):
1760    """Alignment horizontally."""
1761
1762    typename = 'align-horz'
1763
1764    def __init__(self, name, value, **args):
1765        Choice.__init__(self, name, ['left', 'centre', 'right'], value, **args)
1766    def copy(self):
1767        """Make a copy of the setting."""
1768        return self._copyHelper((), (), {})
1769
1770class AlignVert(Choice):
1771    """Alignment vertically."""
1772
1773    typename = 'align-vert'
1774
1775    def __init__(self, name, value, **args):
1776        Choice.__init__(self, name, ['top', 'centre', 'bottom'], value, **args)
1777    def copy(self):
1778        """Make a copy of the setting."""
1779        return self._copyHelper((), (), {})
1780
1781class AlignHorzWManual(Choice):
1782    """Alignment horizontally."""
1783
1784    typename = 'align-horz-+manual'
1785
1786    def __init__(self, name, value, **args):
1787        Choice.__init__(self, name, ['left', 'centre', 'right', 'manual'],
1788                        value, **args)
1789    def copy(self):
1790        """Make a copy of the setting."""
1791        return self._copyHelper((), (), {})
1792
1793class AlignVertWManual(Choice):
1794    """Alignment vertically."""
1795
1796    typename = 'align-vert-+manual'
1797
1798    def __init__(self, name, value, **args):
1799        Choice.__init__(self, name, ['top', 'centre', 'bottom', 'manual'],
1800                        value, **args)
1801    def copy(self):
1802        """Make a copy of the setting."""
1803        return self._copyHelper((), (), {})
1804
1805# Bool which shows/hides other settings
1806class BoolSwitch(Bool):
1807    """Bool switching setting."""
1808
1809    def __init__(self, name, value, settingsfalse=[], settingstrue=[],
1810                 **args):
1811        """Enables/disables a set of settings if True or False
1812        settingsfalse and settingstrue are lists of names of settings
1813        which are hidden/shown to user
1814        """
1815
1816        self.sfalse = settingsfalse
1817        self.strue = settingstrue
1818        Bool.__init__(self, name, value, **args)
1819
1820    def makeControl(self, *args):
1821        return controls.BoolSwitch(self, *args)
1822
1823    def copy(self):
1824        return self._copyHelper((), (), {'settingsfalse': self.sfalse,
1825                                         'settingstrue': self.strue})
1826
1827class ChoiceSwitch(Choice):
1828    """Show or hide other settings based on the choice given here."""
1829
1830    def __init__(self, name, vallist, value,
1831                 showfn=lambda x: ((),()),
1832                 **args):
1833        """Enables/disables a set of settings depending on showfn(val)
1834
1835        showfn(val) returns (show, hide), where show and hide are
1836        lists of settings to show or hide
1837        """
1838
1839        self.showfn = showfn
1840        Choice.__init__(self, name, vallist, value, **args)
1841
1842    def makeControl(self, *args):
1843        return controls.ChoiceSwitch(self, False, self.vallist, *args)
1844
1845    def copy(self):
1846        return self._copyHelper(
1847            (self.vallist,), (),
1848            {'showfn': self.showfn})
1849
1850class FillStyleExtended(ChoiceSwitch):
1851    """A setting for the different fill styles provided by Qt."""
1852
1853    typename = 'fill-style-ext'
1854
1855    @staticmethod
1856    def _showsetns(val):
1857        """Get list of settings to show or hide"""
1858        hatchsetns = (
1859            'linewidth', 'linestyle', 'patternspacing',
1860            'backcolor', 'backtransparency', 'backhide')
1861
1862        if val == 'solid' or val.find('dense') >= 0:
1863            return ((), hatchsetns)
1864        else:
1865            return (hatchsetns, ())
1866
1867    def __init__(self, name, value, **args):
1868        ChoiceSwitch.__init__(
1869            self, name, utils.extfillstyles, value,
1870            showfn=self._showsetns,
1871            **args)
1872
1873    def copy(self):
1874        """Make a copy of the setting."""
1875        return self._copyHelper((), (), {})
1876
1877    def makeControl(self, *args):
1878        return controls.FillStyleExtended(self, *args)
1879
1880class RotateInterval(Choice):
1881    '''Rotate a label with intervals given.'''
1882
1883    def __init__(self, name, val, **args):
1884        Choice.__init__(self, name,
1885                        ('-180', '-135', '-90', '-45',
1886                         '0', '45', '90', '135', '180'),
1887                        val, **args)
1888
1889    def normalize(self, val):
1890        """Store rotate angle."""
1891        # backward compatibility with rotate option
1892        # False: angle 0
1893        # True:  angle 90
1894        if val == False:
1895            val = '0'
1896        elif val == True:
1897            val = '90'
1898        return Choice.normalize(self, val)
1899
1900    def copy(self):
1901        """Make a copy of the setting."""
1902        return self._copyHelper((), (), {})
1903
1904class Colormap(Str):
1905    """A setting to set the color map used in an image.
1906    This is based on a Str rather than Choice as the list might
1907    change later.
1908    """
1909
1910    def makeControl(self, *args):
1911        return controls.Colormap(self, self.getDocument(), *args)
1912
1913class AxisBound(FloatOrAuto):
1914    """Axis bound - either numeric, Auto or date."""
1915
1916    typename = 'axis-bound'
1917
1918    def makeControl(self, *args):
1919        return controls.AxisBound(self, *args)
1920
1921    def toUIText(self):
1922        """Convert to text, taking into account mode of Axis.
1923        Displays datetimes in date format if used
1924        """
1925
1926        try:
1927            mode = self.parent.mode
1928        except AttributeError:
1929            mode = None
1930
1931        v = self.val
1932        if ( not isinstance(v, cbasestr) and v is not None and
1933             mode == 'datetime' ):
1934            return utils.dateFloatToString(v)
1935
1936        return FloatOrAuto.toUIText(self)
1937
1938    def fromUIText(self, txt):
1939        """Convert from text, allowing datetimes."""
1940
1941        v = utils.dateStringToDate(txt)
1942        if N.isfinite(v):
1943            return v
1944        else:
1945            return FloatOrAuto.fromUIText(self, txt)
1946