#!/usr/bin/env python
################################################################################
#
#       This file is part of Gato (Graph Algorithm Toolbox) 
#       You can find more information at 
#       http://gato.sf.net
#
#	file:   EditObjectAttributesDialog.py
#	author: Alexander Schliep (alexander@schliep.org)
#
#       Copyright (C) 1998-2015, Alexander Schliep, Winfried Hochstaettler and 
#       Copyright 1998-2001 ZAIK/ZPR, Universitaet zu Koeln
#                                   
#       Contact: alexander@schliep.org, winfried.hochstaettler@fernuni-hagen.de             
#
#       Information: http://gato.sf.net
#
#       This library is free software; you can redistribute it and/or
#       modify it under the terms of the GNU Library General Public
#       License as published by the Free Software Foundation; either
#       version 2 of the License, or (at your option) any later version.
#
#       This library is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#       Library General Public License for more details.
#
#       You should have received a copy of the GNU Library General Public
#       License along with this library; if not, write to the Free
#       Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
#
#       This file is version $Revision: 670 $ 
#                       from $Date: 2015-01-13 16:04:11 -0500 (Tue, 13 Jan 2015) $
#             last change by $Author: schliep $.
#
################################################################################
from Tkinter import *
from ScrolledText import *
import tkSimpleDialog 
import tkMessageBox
from tkColorChooser import askcolor
import copy
import sys
import os
import types


def typed_assign(var, val):
    result = type(var)(val)
    result.__dict__ = copy.copy(var.__dict__)
    return result
    
    
    
    #-------------------------------------------------------------------------------
class TkBoolEntry:
    """Tk entry field for selecting one Bool"""

    def __init__(self, master, default=False):
        
        self.frame = Frame(master, relief=FLAT)
        self.status = IntVar()
        self.status.set(default)
        useDefaultButton = Checkbutton(self.frame, variable=self.status, command=self.toggle)
        useDefaultButton.grid(row=0, column=0, padx=4, pady=3, sticky=W)
        
    def tkWidget(self):
        return self.frame
        
    def get(self):
        return bool(self.status)
        
    def set(self, value):
        self.status = value

    def toggle(self):
        self.status = not self.status


class TkStringEntry:
    """Tk entry field for editing strings"""
    
    def __init__(self, master, width):
        self.entryWidget = Entry(master, width=width, exportselection=FALSE)
        
    def tkWidget(self):
        return self.entryWidget
        
    def get(self):
        return self.entryWidget.get()
        
    def set(self, value):
        self.entryWidget.delete(0,END)
        self.entryWidget.insert(0,"%s" % value)
        
    def select(self):    
        self.entryWidget.selection_range(0,"end")
        self.entryWidget.focus_set()
        
        
class TkIntEntry(TkStringEntry):
    """Tk entry field for editing one integer"""
    
    def get(self):
        return int(self.entryWidget.get())
        
        
class TkFloatEntry(TkStringEntry):
    """Tk entry field for editing one float"""
    
    def get(self):
        return float(self.entryWidget.get())
        
        
class TkDefaultMixin:
    """Mixin for TkStringEntry, TkIntEntry, TkFloatEntry, ... to deal with
       values which have an externally defined default value. Combination
       of 'use default' checkbox and corresponding entry field """
    
    def __init__(self, master, useDefault, defaultValue, defaultText=None):
        if defaultText is None:
            defaultText="Use default ("+str(defaultValue)+")"
        self.frame = Frame(master, relief=FLAT)
        self.useDefault = IntVar()
        self.useDefault.set(useDefault)
        self.defaultValue = defaultValue
        useDefaultButton = Checkbutton(self.frame, text=defaultText,
                                       variable=self.useDefault,
                                       command=self.toggleDefault)
        useDefaultButton.grid(row=0, column=0, padx=4, pady=3, sticky=W)
        
    def finish(self):
        self.entryWidget.grid(row=0, column=1, padx=4, pady=3, sticky=W)
        self.switchDefault(self.useDefault.get())   
        
    def UseDefault(self):
        return self.useDefault.get()
        
    def switchDefault(self, value):
        if value == 0:
            self.entryWidget['state'] = NORMAL
            self.entryWidget.delete(0,END)
            self.set(self.defaultValue)
        else:
            self.entryWidget.delete(0,END)
            self.entryWidget['state'] = DISABLED
            
    def toggleDefault(self):
        self.switchDefault(self.useDefault.get())            
        
        
class TkDefaultStringEntry(TkStringEntry, TkDefaultMixin):

    def __init__(self, master, width, useDefault, defaultValue):
        TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
        TkStringEntry.__init__(self, self.frame, width)
        self.finish()        
        
    def tkWidget(self): # To avoid ambiguity
        return self.frame
        
        
class TkDefaultIntEntry(TkIntEntry, TkDefaultMixin):

    def __init__(self, master, width, useDefault, defaultValue):
        TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
        TkIntEntry.__init__(self, self.frame, width)
        self.finish()        
        
    def tkWidget(self): # To avoid ambiguity
        return self.frame
        
    def get(self):
        if self.UseDefault():
            return self.defaultValue
        else:
            return TkIntEntry.get(self)
            
class TkDefaultFloatEntry(TkFloatEntry, TkDefaultMixin):

    def __init__(self, master, width, useDefault, defaultValue):
        TkDefaultMixin.__init__(self, master, useDefault, defaultValue)
        TkFloatEntry.__init__(self, self.frame, width)
        self.finish()        
        
    def tkWidget(self): # To avoid ambiguity
        return self.frame
        
    def get(self):
        if self.UseDefault():
            return self.defaultValue
        else:
            return TkFloatEntry.get(self)
            
            
            
class TkPopupSelector:
    def __init__(self, master, value2pop, pop2value, width):
    
        self.value2pop = value2pop
        self.pop2value = pop2value
        self.popupvalue = StringVar()
        self.popupvalue.set(self.pop2value.keys()[0]) # XXX first value as default 
        
        # XXX Uuughhh
        keys = self.value2pop.keys()
        keys.sort()
        pops = map(lambda x: value2pop[x], keys)
        #log.debug("pops = %s" % pops)
        args = (master, self.popupvalue) + tuple(pops)
        
        self.tkwidget = apply(OptionMenu, args)
        self.tkwidget.config(height=1, width=width)
        
    def tkWidget(self):
        return self.tkwidget
        
    def get(self):
        return self.pop2value[self.popupvalue.get()]
        
    def set(self, value):
        try:
            self.popupvalue.set(self.value2pop[value])
        except:
            self.popupvalue.set(self.pop2value.keys()[0]) # XXX first value as default       
            
    def select(self):    
        # Cant choose invalid value with popup
        pass
        
class TkStringPopupSelector:
    def __init__(self, master, strings):
    
        self.strings = strings
        self.popupvalue = StringVar()
        if len(self.strings) > 0:
            self.popupvalue.set(self.strings[0]) # XXX first value as default 
            
        width = max(map(len, self.strings))
        args = (master, self.popupvalue) + tuple(self.strings)
        self.tkwidget = apply(OptionMenu, args)
        self.tkwidget.config(height=1, width=width)
        
    def tkWidget(self):
        return self.tkwidget
        
    def get(self):
        return self.popupvalue.get()
        
    def set(self, value):
        try:
            self.popupvalue.set(value)
        except:
            self.popupvalue.set(self.strings[0]) # XXX first value as default       
            
    def select(self):    
        # Cant choose invalid value with popup
        pass
        
        
class TkColorSelector:
    def __init__(self, master, color='black'):
        #self.tkwidget = Button(master, width=8, command=self.editColor)
        self.tkwidget = Frame(master, height=18, width=60, relief=RIDGE, borderwidth=1)
        self.tkwidget.bind("<ButtonRelease-1>", self.editColor)
        self.set(color)
        
    def editColor(self, event):
        color = askcolor(self.color)[1]
        if color is not None:
            self.set(color)
            
    def tkWidget(self):
        return self.tkwidget
        
    def get(self):
        return self.color
        
    def set(self, value):
        self.color = value
        self.tkwidget.config(bg=self.color)
        
    def select(self):    
        # Cant choose invalid value with popup
        pass
        
        
        
class EditObjectAttributesDialog(tkSimpleDialog.Dialog):
    """ Creates an editable (pseudo-)inspector for a selected set of
        attributes of a given object
    
         - master : tk master widget
         - object : the object, whose attributes we want to edit
         - attr_names : a list of attr_names or a dictionary of attr_names and displayed Text
    
        By making use of Python 2.2's capability of subclassing built-in
        types such as ints, information about editing etc. is conveyed.
        An attr must have:
         - validate(value) method [return 1, if value is a valid new value for attr]
    
        The class of an attr can have the following mix-ins:
         - Popubable 
         - WithDefault 
    """
    
    def __init__(self, master, object, attr_names):
        self.object = object
        self.attr_names = attr_names
        self.edit = {}
        tkSimpleDialog.Dialog.__init__(self, master, "Edit: %s" % self.object.desc)
        
        
    def editWidget(self, master, object, attr_name):
        """ Create a widget capable of editing attr and insert attr's current value"""
        
        attr = object.__dict__[attr_name]
        attr_type = type(attr)
        widget = None
        default = isinstance(attr, WithDefault) # has a WithDefault mixin
        
        if isinstance(attr, Popupable):            
            widget = TkPopupSelector(master, attr.val2pop, attr.pop2val, attr.width)
            
        elif isinstance(attr, ValidatingBool):
            widget = TkBoolEntry(master)

        elif isinstance(attr, str):
        
            if default:
                widget = TkDefaultStringEntry(master, max(32, len(attr)), attr.useDefault, attr)
            else:
                widget = TkStringEntry(master, max(32, len(attr)))
                
        elif isinstance(attr, int):
        
            if default:
                widget = TkDefaultIntEntry(master, 6, attr.useDefault, attr)
            else:
                widget = TkIntEntry(master, 6)
                
        elif isinstance(attr, float):
        
            if default:
                widget = TkDefaultFloatEntry(master, 8, attr.useDefault, attr)
            else:
                widget = TkFloatEntry(master, 8)
                
        widget.set(attr)
        return widget
        
        
    def body(self, master):
        self.resizable(0,0)	
        
        # Header Zeile
        label = Label(master, text="Name", anchor=E)
        label.grid(row=0, column=0, padx=4, pady=3, sticky=E)
        label = Label(master, text="Value", anchor=W)
        label.grid(row=0, column=1, padx=4, pady=3, sticky=W)
        
        cur_row = 1

        if isinstance(self.attr_names, list):
            for attr in self.attr_names:
                label = Label(master, text="%s" % attr, anchor=E)
                label.grid(row=cur_row, column=0, padx=4, pady=3, sticky=E)

                self.edit[attr] = self.editWidget(master, self.object, attr)
                if self.edit[attr] != None:
                    self.edit[attr].tkWidget().grid(row=cur_row, column=1, padx=2, pady=1, sticky=W)

                cur_row = cur_row + 1
        else:
            for attr in self.attr_names.keys():
                label = Label(master, text="%s" % self.attr_names[attr], anchor=E)
                label.grid(row=cur_row, column=0, padx=4, pady=3, sticky=E)

                self.edit[attr] = self.editWidget(master, self.object, attr)
                if self.edit[attr] != None:
                    self.edit[attr].tkWidget().grid(row=cur_row, column=1, padx=2, pady=1, sticky=W)

                cur_row = cur_row + 1

    def validate(self):
        for attr_name in self.edit.keys():
            try:
            
                # In python 2.2 we can subclass attributes and add a validate method
                # to attributes
            
                value = self.edit[attr_name].get()
                
                if self.object.__dict__[attr_name].validate(value) == 0:
                    raise ValueError
                    
            except ValueError:
                msg = "Please enter a valid value for %s" % attr_name
                tkMessageBox.showwarning("Invalid Value", msg, parent=self)
                self.edit[attr_name].select()
                return 0
                
                # Everything is valid => set values
        for attr_name in self.edit.keys():            
            self.object.__dict__[attr_name] = typed_assign(self.object.__dict__[attr_name], self.edit[attr_name].get())
            
            if isinstance(self.object.__dict__[attr_name], WithDefault):
                self.object.__dict__[attr_name].useDefault = self.edit[attr_name].useDefault.get()
                
                
        return 1
        
        
        #-------------------------------------------------------------------------------
class WithDefault:
    """Mix-in for variables which have a default value"""
    
    def setDefault(self, useDefault, defaultValue):
        self.useDefault = useDefault
        self.defaultValue = defaultValue
        
    def validate(self, value):
    ##        if self.useDefault:
    ##            return 1
    ##        else:
    ##            return 1 # XXX How can I call a method of the class I am mixed too
        return 1
        
        
class Popupable:
    """Mix-in for variables which can be edited via a pop-up menu
       - val2pop : dict mapping value to string for pop up menu
       - pop2val: dict mapping pop up menu string to value
       - width: maximal string length in pop up
    """
    def setPopup(self, val2pop, pop2val = None, width = None):
        self.val2pop = val2pop
        self.pop2val = None
        self.width = None
        
        if pop2val == None:
            self.pop2val = {} # Private copy
            self.width = 0
            
            for val in val2pop.keys():
                pop = val2pop[val]
                self.width = max(len(pop), self.width)
                self.pop2val[pop] = val
        else:
            self.pop2val = pop2val
            self.width = width
            
    def validate(self, value):
        return 1
        
        ##class PopupableStr(str):
        ##    """Class for variables which can be edited via a pop-up menu
        ##       - values: array of values 
        ##       - width: maximal string length in pop up
        ##    """
        ##    def setPopup(self, values, width = None):
        
        ##        self.values = values
        ##        self.width = width
        
        ##        if width == None:
        ##            self.width = 0
        
        ##            for s in values:
        ##                self.width = max(len(s), self.width)
        
        ##    def validate(self, value):
        ##        return 1
        
        
class AlwaysValidate:
    """Mix-in for variables which always are valid"""
    def validate(self, value):
        return 1
        
        #-------------------------------------------------------------------------------
class ValidatingBool(int):
    """Editable replacement for bool"""
    def validate(self, value):
        if value is True or value is False:
            return 1

class ValidatingInt(int, AlwaysValidate):
    """Editable replacement for ints"""
    pass
    
class  ValidatingFloat(float, AlwaysValidate):
    """Editable replacement for floats"""
    pass
    
class  ValidatingString(str, AlwaysValidate):
    """Editable replacement for strings"""
    pass
    
class PopupableInt(int, Popupable):
    """A replacement for ints editable via a pop-up"""
    pass
    
class Probability(float):
    """An editable float taking values from [0,1]"""
    def validate(self, value):
        if 0.0 <= value and value <= 1.0:
            return 1
        else:
            return 0
            
class DefaultedInt(int, WithDefault):
    """An editable int with a default value"""    
    pass
    
class DefaultedFloat(float, WithDefault):
    """An editable float with a default value"""    
    pass
    
class DefaultedString(str, WithDefault):
    """An editable strinf with a default value"""    
    pass
    
    
    #======================================================================
    #
    # Demo:
    #
class TkTestFrame(Frame):

    def __init__(self, parent=None):
        Frame.__init__(self,parent)
        Pack.config(self)
        self.createWidgets()
        
        self.desc = ValidatingString("The TkTestFrame")
        self.x = DefaultedInt(1)
        self.x.setDefault(1, 122)
        self.y = ValidatingFloat(2.33)
        self.choose = PopupableInt(3)
        self.pop2val = {"aaa":1, "xxx":2, "sss":3}
        self.val2pop = {1:"aaa", 2:"xxx", 3:"sss"}
        self.choose.setPopup(self.val2pop, self.pop2val, 5)
        
    def createWidgets(self):
        self.QUIT = Button(self, text='QUIT', foreground='red', 
                           command=self.quit)
        self.QUIT.pack(side=LEFT)
        self.About = Button(self, text='Preferences', foreground='red', 
                           command=self.About)
        self.About.pack(side=LEFT)
        
        
    def About(self):
        aboutBox = EditObjectAttributesDialog(self.master, self, ['desc', 'x', 'y', 'choose'])
        del self.pop2val["aaa"]
        del self.val2pop[1]
        aboutBox = EditObjectAttributesDialog(self.master, self, ['desc', 'x', 'y', 'choose'])
        
if __name__ == '__main__':
    app = TkTestFrame()
    app.mainloop()
    
    
