import qt

GOOD=qt.QValidator.Acceptable
INCOMPLETE=qt.QValidator.Intermediate
FORBIDDEN=qt.QValidator.Invalid

badcolor=qt.QColor(255,190,190)

class MyLineEdit(qt.QLineEdit):
    """QLineEdit with 2 extra slots to change the appearance when a
       'bad' value is entered."""
    def __init__(self,*arg,**kw):
        qt.QLineEdit.__init__(self,*arg,**kw)
        self.pal=qt.QPalette(self.palette())
        self.pal.setColor(qt.QColorGroup.Base,badcolor)
        
    def set_invalid(self):
        #print "DBG> Set invalid",self
        self.setPalette(self.pal)

    def set_valid(self):
        #print "DBG> Set valid",self
        self.unsetPalette()

class MyComboBox(qt.QComboBox):
    """QComboBox with 2 extra slots to change the appearance when a
       'bad' value is entered."""
    def __init__(self,*arg,**kw):
        qt.QComboBox.__init__(self,*arg,**kw)
        self.pal=qt.QPalette(self.palette())
        self.pal.setColor(qt.QColorGroup.Base,badcolor)
        
    def set_invalid(self):
        self.setPalette(self.pal)

    def set_valid(self):
        self.unsetPalette()

class MySpinBox(qt.QSpinBox):
    """QSpinBox with 2 extra slots to change the appearance when a
       'bad' value is entered. Also uses an external 'incrfunc' for
       incrementing and decrementing the value."""
    def __init__(self,incrfunc,*arg,**kw):
        qt.QSpinBox.__init__(self,*arg,**kw)
        self.setWrapping(1) # Abuse: make sure buttons are always black.
        self.pal=qt.QPalette(self.palette())
        self.pal.setColor(qt.QColorGroup.Base,badcolor)
        self.incrfunc=incrfunc
        
    def set_invalid(self):
        self.setPalette(self.pal)

    def set_valid(self):
        self.unsetPalette()

    def stepUp(self):
        self.editor().setText(self.incrfunc(str(self.text()),1))

    def stepDown(self):
        self.editor().setText(self.incrfunc(str(self.text()),-1))

    def mapValueToText(self,value):
        # Make sure the text is not changed behind our backs.
        return self.editor().text()
    
class tracevar(qt.QObject):
    def __init__(self,*arg,**kw):
        qt.QObject.__init__(self,*arg,**kw)
        self.value=None
        self.wl=[]
        
    def get(self):
        return self.value

    def set(self,v):
        v=str(v)
        if v!=self.value:
            self.ValueChanged(v)
            self.emit(qt.PYSIGNAL('valueChanged(const QString &)'),(v,))
        self.value=v

    def bind(self,w):
        self.wl.append(w)

    def GoodValue(self):
        for w in self.wl[:]:
            try:
                w.set_valid()
            except RuntimeError:
                print "DBG> %s disappeared",w
                self.wl.remove(w)

    def BadValue(self):
        for w in self.wl[:]:
            try:
                w.set_invalid()
            except RuntimeError:
                print "DBG> %s disappeared",w
                self.wl.remove(w)

    def ValueChanged(self,val):
        for w in self.wl[:]:
            try:
                w.setText(val)
            except RuntimeError:
                print "DBG> %s disappeared",w
                self.wl.remove(w)
        
    def Widget(self,parent):
        w=MyLineEdit(parent,'var')
        self.bind(w)
        qt.QObject.connect(w,qt.SIGNAL('textChanged(const QString &)'),self.set)
        w.setText(self.get())
        return w

class combotracevar(tracevar):
    def __init__(self,list,*arg,**kw):
        tracevar.__init__(self,*arg,**kw)
        self.list=list

    def set(self,v):
        v=str(v)
        if v!=self.value:
            try:
                i=self.list.index(v)
            except ValueError:
                pass
            else:
                self.ValueChanged(i)
                self.emit(qt.PYSIGNAL('valueChanged(int)'),(i,))
            self.value=v

    def ValueChanged(self,val):
        for w in self.wl[:]:
            try:
                w.setCurrentItem(val)
            except RunTimeError:
                self.wl.remove(w)

    def Widget(self,parent):
        w=MyComboBox(len(self.list)>20,parent,"combo box")
        for s in self.list:
            w.insertItem(s)
        self.bind(w)
        qt.QObject.connect(w,qt.SIGNAL('textChanged(const QString &)'),self.set)
        try:
            i=self.list.index(self.get())
        except ValueError:
            pass
        else:
            w.setCurrentItem(i)
        return w

class spintracevar(tracevar):
    def __init__(self,incrfunc,*arg,**kw):
        tracevar.__init__(self,*arg,**kw)
        self.incrfunc=incrfunc

    def set(self,v):
        v=str(v)
        if v!=self.value:
            self.ValueChanged(v)
            self.emit(qt.PYSIGNAL('valueChanged(const QString &)'),(v,))
            self.value=v

    def Widget(self,parent):
        w=MySpinBox(self.incrfunc,parent,"spin box")
        self.bind(w)
        qt.QObject.connect(w.editor(),qt.SIGNAL('textChanged(const QString &)'),self.set)
        qt.QObject.connect(self,qt.PYSIGNAL('valueChanged(const QString &)'),w.editor(),qt.SLOT('setText(const QString &)'))
        w.editor().setText(self.get())
        return w

class myvalidator(qt.QValidator):
    def __init__(self,func):
        qt.QValidator.__init__(self,None,'test')
        self.func=func
        
    def validate(self,value,position):
        return (self.func(str(value)),position)

    def fixup(self,s):
        pass

class myvariable:
    """A 'variable' abstract superclass. Misses the essential
       (get|set)representation methods."""
    xbutrow=2
    xbutrowspan=1
    
    def __init__(self,label="",extrabuttons=()):
        self.label=label
        self.extrabuttons=extrabuttons

    def get(self):
	"""Get the internal value of the variable"""
	return self.fromrepresentation(self.getrepresentation())

    def set(self,newvalue):
	"""Set the value of the variable."""
        self.setrepresentation(self.torepresentation(newvalue))

    def fromrepresentation(self,v):
	"""Convert the "displayable" version to the "internal" version of the
           variable"""
	return v

    def torepresentation(self,v):
	"""Convert the "internal" version to the "displayable" version of the
           variable"""
	return v

    def Valid(self):
	"""Check whether the variable has a valid value"""
	return self.validate(self.getrepresentation())==GOOD

    def validate(self,value):
	"""Check whether "value" is a valid value for the variable.

	   Return value:
              variables.GOOD = OK
              variables.FORBIDDEN = Not OK
              variables.INCOMPLETE = Not OK, but allowable in a Widget
                                     (could go somewhere...)
        """
	return GOOD

    def Widget(self,master=None):
        w=self._Widget(master)
        if self.extrabuttons:
            f=qt.QHBox(w,'xtrabuttons')
            for tup in self.extrabuttons:
                b=qt.QPushButton(f,tup[0])
                b.setText(tup[0])
                b.connect(b,qt.SIGNAL('clicked()'),tup[1])
        return w
    
class qtvariable(myvariable):
    """Variable that is stored in a tracevar for display in a GUI.
       The type of variable is only determined by the subclass."""
    def setrepresentation(self,value):
	"""Set the external representation of the variable to 'value'
           (already in external representation)"""
	#print "set",self.label,"to",value
	if value!=self.tracevar.get():
	    self.tracevar.set(value)

    def getrepresentation(self):
	"""Get the external representation of the variable"""
	return self.tracevar.get()
    
    def _Widget(self,master=None):
	"""Create a widget to edit the variable, and return the created widget"""
        x=qt.QHBox(master,'variable')
        qt.QLabel(self.label,x,'label')
        w=self.tracevar.Widget(x)
        if hasattr(w,'setValidator'):
            w.setValidator(self.iv)
	return x

    def trace(self,command):
        return self.tracevar.connect(self.tracevar,qt.PYSIGNAL('valueChanged(const QString &)'),command)
    
class stringvariable(qtvariable):
    """Variable for which the internal representation is a string."""
    def __init__(self,re=None,width=10,**kw):
	"""Constructor
           optional parameters:
              re    = None   : regular expression that should be met.
        """
        qtvariable.__init__(self,**kw)
        self.iv=myvalidator(self.validateandchangecolor)
	self.tracevar=self._tracevar()
        self.re=re
        self.width=width

    def _tracevar(self):
        return tracevar()

    def validateandchangecolor(self,s):
        v=self.validate(s)
        if v==GOOD:
            self.tracevar.GoodValue()
        else:
            self.tracevar.BadValue()
        return v
    
    def validate(self,s):
        if not self.re:
            return GOOD
        if self.re.match(s):
            return GOOD
        else:
            return INCOMPLETE

    def setCommand(self,cmd):
        qt.QObject.connect(self.tracevar,qt.PYSIGNAL('valueChanged(const QString &)'),cmd)
        
class realvariable(stringvariable):
    """Variable for which the internal representation is a real, and the external 
       representation is the decimal string representing the real."""
    def __init__(self,lowerlimit=None,upperlimit=None,format=None,width=14,**kw):
	"""Constructor optional parameters:
              lowerlimit = None : lowest allowable value for the real.
	      upperlimit = None : highest allowable value for the real.
              label = None      : label for the widget.
        """
        kw['width']=14
        stringvariable.__init__(self,**kw)
	self.lowerlimit=lowerlimit
	self.upperlimit=upperlimit
        self.format=format
	self.setexceptions()

    def setexceptions(self):
	"""Normally an empty string, or just a '.' or '-' character would not be 
           valid 'real' values. But they might be needed to type a real into the 
           field of a Widget. Therefore these situations are coded here as special
           exceptions."""
	if self.lowerlimit>=0:
	    #If the lower limit is bigger than 0, we do never need to type a "-".
	    self.exceptions=['','.']
	else:
	    self.exceptions=['','.','-']

    def fromrepresentation(self,v):
	"""Converting from the external string representation to the internal real value"""
	try:
	    return float(v)
	except (TypeError,ValueError):
            pass
        try:
            return float(eval(v))
        except:
            return None

    def torepresentation(self,v):
	"""Converting from the internal real value to the external string representation"""
        if self.format is None:
            s=str(v)
            if s[-2:]==".0":
                s=s[:-2]
            return s
        else:
            return self.format%v
        
    def validate(self,v):
	"""Check whether 'v' as contents of the entry widget is allowed."""
        if type(v)!=type(''):
            vi=v
        else:
	    vi=self.fromrepresentation(v)
	if vi is None:
	    # If it is not a string representation of a real, something is fishy...
	    if v in self.exceptions:
	        # On the way to become a proper real value
		return INCOMPLETE
            else:
                # An expression ending in an operator
                if type(v)==type('') and len(v)>1:
                    vi=self.fromrepresentation(v[:-1])
                    if v[-1] in ['+','-','*','/'] and vi!=None:
                        return INCOMPLETE
                # Completely wrong (like containing letters)
                return FORBIDDEN
	if self.lowerlimit!=None and vi<0 and self.lowerlimit>=0:
	    # Below 0 and lower limit is above. Contains invalid '-' sign.
	    return FORBIDDEN
	if self.lowerlimit!=None and vi<self.lowerlimit:
	    # Too low!
	    return INCOMPLETE
	if self.upperlimit!=None and vi>self.upperlimit:
	    # Too high!
	    return INCOMPLETE
	# Completely valid
	return GOOD

    def _Widget(self,master):
	x=stringvariable._Widget(self,master)
	return x

class limitedstringvariable(stringvariable):
    """String variable with a limited list of possibilities"""
    possibilities=[]
    def validate(self,v):
	if v in self.possibilities:
	    return GOOD
	else:
	    return INCOMPLETE

class xcolorstringvariable(stringvariable):
    def validate(self,v):
        if qt.QColor(v).isValid():
            return GOOD
        else:
            return INCOMPLETE

class optionstringvariable(limitedstringvariable):
    presentpossibilities=None
    def list(self):
        if self.presentpossibilities:
            return self.presentpossibilities
        else:
            return self.possibilities
        
    def _tracevar(self):
        return combotracevar(self.list())

class systemvariable(optionstringvariable):
    """Crystallographic special"""
    possibilities=['triclinic','monoclinic','orthorhombic','hexagonal','trigonal','trigonalh','tetragonal','cubic']

class anglevariable(realvariable):
    """Internal representation is a real representing an angle in radians, external 
       representation is the string containing the decimal representation of the angle
       in degrees. Default format is 4 digits after comma."""
    def __init__(self,**kw):
        if not kw.has_key('format'):
            kw['format']="%.4f"
        realvariable.__init__(self,**kw)
        
    def fromrepresentation(self,v):
	try:
	    return realvariable.fromrepresentation(self,v)*3.14159265358979323846/180.0
        except (TypeError,ValueError):
	    return None

    def torepresentation(self,v):
	if v is None:
	    return None
	return realvariable.torepresentation(self,v*180.0/3.14159265358979323846)
    
class integervariable(realvariable):
    """Internal representation is an integer, external representation
       is a string containing the decimal representation of the integer"""

    def setexceptions(self):
	"""Same as for real, but no decimal point allowed"""
	if self.lowerlimit!=None and self.lowerlimit>=0:
	    self.exceptions=['']
	else:
	    self.exceptions=['','-']

    def fromrepresentation(self,v):
	"""Same as for real, but convert to integer rather than to real"""
	if v=="-":
	    return None
        try:
	    return int(v)
	except (TypeError,ValueError):
	    return None

    def torepresentation(self,v):
        return "%d"%v
        
class countermixin:
    log=0
    def _tracevar(self):
        return spintracevar(self._Increment)

    def _Increment(self,text,factor):
	v=self.fromrepresentation(text)
        if self.log:
            if v==0:
                try:
                    ntext=text[:-1]+'1'
                    nv=self.fromrepresentation(ntext)
                except ValueError:
                    nv=1.0
            else:
                nv=v*(float(self.increment)**factor)
                i=1
                while self.torepresentation(nv)==text and i<5:
                    # Make sure it changes....
                    nv=nv*(float(self.increment)**factor)
                    i=i+1
        else:
            nv=v+factor*self.increment
	if self.validate(self.torepresentation(nv))==GOOD:
	    return self.torepresentation(nv)
	else:
	    return text
    
class counterintegervariable(countermixin,integervariable):
    """Special integer variable that can be incremented and decremented using
       arrows in the widget"""
    def __init__(self,increment=1,**kw):
        self.increment=increment
        integervariable.__init__(self,**kw)
    
class counterrealvariable(countermixin,realvariable):
    """Special real variable that can be incremented and decremented using
       arrows in the widget"""
    def __init__(self,increment=1,**kw):
        self.increment=increment
        realvariable.__init__(self,**kw)

class counteranglevariable(countermixin,anglevariable):
    """Special angle variable that can be incremented and decremented using
       arrows in the widget"""
    def __init__(self,increment=3.1415926535/180.0,**kw):
        self.increment=increment
        anglevariable.__init__(self,**kw)

class logcounterrealvariable(counterrealvariable):
    log=1
    def __init__(self,increment=1.41421356237,**kw):
        kw['increment']=increment
        counterrealvariable.__init__(self,**kw)
        
class logcounteranglevariable(counteranglevariable):
    log=1
    def __init__(self,increment=1.41421356237,**kw):
        kw['increment']=increment
        counteranglevariable.__init__(self,**kw)

############## Test.

if __name__=="__main__":
    import sys
    app=qt.QApplication(sys.argv)
    qt.QObject.connect(app,qt.SIGNAL('lastWindowClosed()'),app,qt.SLOT('quit()'))
    root=qt.QVBox(None,'root')
    if 0:
        b1=stringvariable(label="Type a string")
        b1.set("no202")
    if 0:
        b1=systemvariable(label="What system")
        b1.set("triclinic")
    if 1:
        b1=realvariable(label="A number between 3 and 7.5 please",lowerlimit=3.0,upperlimit=7.5)
        b1.set(3)
    if 0:
        b1=logcounterrealvariable(label="A number between 1 and 100 please",lowerlimit=1,upperlimit=100,format="%.2f")
        b1.set(3)
    w1=b1.Widget(root)
    w2=b1.Widget(root)
    # This should be tested also without the close, then
    # verify whether both widgets change in unison!
    if 1:
        w1.close(1)
    root.show()
    app.exec_loop()
