from visual import *

# Bruce Sherwood, March 2002
# Import this module to create buttons, toggle switches, sliders, and pull-down menus.
# See test routine at end of this module for an example of how to use controls.

lastcontrols = None # the most recently created controls window
gray = (0.7, 0.7, 0.7)
darkgray = (0.5, 0.5, 0.5)

class controls: # make a special window for buttons, sliders, and pull-down menus
    def __init__(self, x=0, y=0, width=300, height=320, range=100,
                 title=None, foreground=None, background=None):
        global lastcontrols
        lastcontrols = self
        currentdisplay = display.get_selected()
        if title is None:
            title = 'Controls'
        if foreground is None:
            foreground = color.white
        if background is None:
            background = color.black
        self.display = display(title=title, x=x, y=y, range=range,
                               width=width, height=height, fov=0.5,
                               foreground=foreground, background=background,
                               userzoom=0, userspin=0,
                               lights=[vector(0,2,1)])
        self.focus = None
        self.lastpos = None
        self.controllist = []
        currentdisplay.select()

    def addcontrol(self, control):
        self.controllist.append(control)

    def interact(self):
        if self.display.mouse.events: 
            m = self.display.mouse.getevent()
            if m.press == 'left' and m.pick:
                picked = m.pick
                if self.focus: # have been moving over menu with mouse up
                    picked = self.focus
                for control in self.controllist:
                    if control.active is picked:
                        self.focus = control
                        control.highlight(m.pos)
            elif m.release == 'left':
                focus = self.focus
                self.focus = None # menu may reset self.focus for "sticky" menu
                if focus:
                    focus.unhighlight(m.pos)
        elif self.focus: # if dragging a control
            pos = self.display.mouse.pos
            if pos != self.lastpos:
                self.focus.update(pos)
                self.lastpos = pos

class ctrl(object): # common aspects of buttons, sliders, and menus
    # Note: ctrl is a subclass of "object" in order to be a new-type class which
    # permits use of the new "property" feature exploited by buttons and sliders.
    def __init__(self, args):
        if args.has_key('controls'):
            self.controls = args['controls']
        elif lastcontrols is None:
            self.controls = controls()
        else:
            self.controls = lastcontrols
        self.controls.addcontrol(self)
        self.pos = vector(0,0)
        self.action = None
        if args.has_key('pos'):
            self.pos = vector(args['pos'])
        if args.has_key('value'):
            self.value = args['value']
        if args.has_key('action'):
            self.action = args['action']
            
    def highlight(self, pos):
        pass
        
    def unhighlight(self, pos):
        pass

    def update(self, pos):
        pass
        
    def execute(self):
        if self.action:
            apply(self.action)

class button(ctrl):
    def __init__(self, **args):
        self.type = 'button'
        self.value = 0
        ctrl.__init__(self, args)
        width = height = 40
        bcolor = gray
        edge = darkgray
        self.__text = ''
        if args.has_key('width'):
            width = args['width']
        if args.has_key('height'):
            height = args['height']
        if args.has_key('text'):
            self.__text = args['text']
        if args.has_key('color'):
            bcolor = args['color']
        disp = self.controls.display
        framewidth = width/10.
        self.thick = 2.*framewidth
        self.box1 = box(display=disp, pos=self.pos+vector(0,height/2.-framewidth/2.,0),
                       size=(width,framewidth,self.thick), color=edge)
        self.box2 = box(display=disp, pos=self.pos+vector(-width/2.+framewidth/2.,0,0),
                       size=(framewidth,height,self.thick), color=edge)
        self.box3 = box(display=disp, pos=self.pos+vector(width/2.-framewidth/2.,0,0),
                       size=(framewidth,height,self.thick), color=edge)
        self.box4 = box(display=disp, pos=self.pos+vector(0,-height/2.+framewidth/2.,0),
                       size=(width,framewidth,self.thick), color=edge)
        self.button = box(display=disp, pos=self.pos+vector(0,0,self.thick/2.+1.),
                       size=(width-2.*framewidth,height-2.*framewidth,self.thick), color=bcolor)
        self.label = label(display=disp, pos=self.button.pos, color=color.black,
                           text=self.__text, line=0, box=0, opacity=0)
        self.active = self.button

    def gettext(self):
        return self.label.text

    def settext(self, text):
        self.label.text = text

    text = property(gettext, settext) # establishes special getattr/setattr handling

    def highlight(self, pos):
        self.button.pos.z -= self.thick
        self.label.pos.z -= self.thick
        self.value = 1
        
    def unhighlight(self, pos):
        self.button.pos.z += self.thick
        self.label.pos.z += self.thick
        self.value = 0
        self.execute()

class toggle(ctrl):
    def __init__(self, **args):
        self.type = 'toggle'
        self.__value = 0
        ctrl.__init__(self, args)
        width = height = 20
        self.angle = pi/6. # max rotation of toggle
        bcolor = gray
        edge = darkgray
        self.__text0 = ''
        self.__text1 = ''
        if args.has_key('width'):
            width = args['width']
        if args.has_key('height'):
            height = args['height']
        if args.has_key('text0'):
            self.__text0 = args['text0']
        if args.has_key('text1'):
            self.__text1 = args['text1']
        if args.has_key('color'):
            bcolor = args['color']
        if args.has_key('value'):
            self.__value = args['value']
        diskthick = width/4.
        diskradius = height/2.
        ballradius = 0.6*diskradius
        self.rodlength = 1.2*diskradius+ballradius
        disp = self.controls.display
        self.frame = frame(display=disp, pos=self.pos, axis=(1,0,0))
        self.back = box(display=disp, frame=self.frame, pos=(0,0,0),
                        size=(width,height,0.3*diskradius), color=darkgray)
        self.disk1 = cylinder(display=disp, frame=self.frame, pos=(-diskthick,0,0),
                              axis=(-diskthick,0), radius=diskradius, color=gray)
        self.disk2 = cylinder(display=disp, frame=self.frame, pos=(diskthick,0,0),
                              axis=(diskthick,0), radius=diskradius, color=gray)
        self.rod = cylinder(display=disp, frame=self.frame, pos=(0,0,0),
                              axis=(0,0,self.rodlength), radius=width/8., color=gray)
        self.ball = sphere(display=disp, frame=self.frame, pos=(0,0,self.rodlength),
                              radius=ballradius, color=gray)
        self.label0 = label(display=disp, frame=self.frame, pos=(0,-1.0*height), text=self.__text0,
                           line=0, box=0, opacity=0)
        self.label1 = label(display=disp, frame=self.frame, pos=(0,1.0*height), text=self.__text1,
                           line=0, box=0, opacity=0)
        self.settoggle(self.__value)
        self.active = self.ball

    def settoggle(self, val):
        self.__value = val
        if val == 1:
            newpos = self.rodlength*vector(0,sin(self.angle), cos(self.angle))
        else:
            newpos = self.rodlength*vector(0,-sin(self.angle), cos(self.angle))
        self.rod.axis = newpos
        self.ball.pos = newpos
             
    def getvalue(self):
        return self.__value

    def setvalue(self, val):
        self.settoggle(val)
        self.__value = val

    value = property(getvalue, setvalue) # establishes special getattr/setattr handling

    def gettext0(self):
        return self.label0.text

    def settext0(self, text):
        self.label0.text = text

    text0 = property(gettext0, settext0) # establishes special getattr/setattr handling

    def gettext1(self):
        return self.label1.text

    def settext1(self, text):
        self.label1.text = text

    text1 = property(gettext1, settext1) # establishes special getattr/setattr handling
        
    def unhighlight(self, pos):
        if self.controls.display.mouse.pick is self.active:
            self.__value = not(self.__value)
            self.settoggle(self.__value)
            self.execute()

class slider(ctrl):
    def __init__(self, **args):
        self.type = 'slider'
        self.__value = 0
        ctrl.__init__(self, args)
        self.length = 100.
        width = 10.
        shaftcolor = darkgray
        scolor = gray
        self.min = 0.
        self.max = 100.
        self.axis = vector(1,0,0)
        if args.has_key('axis'):
            self.axis = vector(args['axis'])
            self.length = mag(self.axis)
            self.axis = norm(self.axis)
        if args.has_key('length'):
            self.length = args['length']
        if args.has_key('width'):
            width = args['width']
        if args.has_key('min'):
            self.min = args['min']
            if self.__value == 0:
                self.__value = self.min
        if args.has_key('max'):
            self.max = args['max']
        if args.has_key('color'):
            scolor = args['color']
        disp = self.controls.display
        self.shaft = box(display=disp, 
                       pos=self.pos+self.axis*self.length/2., axis=self.axis,
                       size=(self.length,0.5*width,0.5*width), color=shaftcolor)
        self.indicator = box(display=disp,
                       pos=self.pos+self.axis*self.__value*self.length/(self.max-self.min),
                       axis=self.axis,
                       size=(width,width,width), color=scolor)
        self.active = self.indicator

    def getvalue(self):
        return self.__value

    def setvalue(self, val):
        self.update(self.pos+self.axis*val*self.length/(self.max-self.min))
        self.__value = val

    value = property(getvalue, setvalue) # establishes special getattr/setattr handling

    def update(self, pos):
        val = dot((pos-self.pos),self.axis)*(self.max-self.min)/self.length
        if val < self.min:
            val = self.min
        elif val > self.max:
            val = self.max
        if val != self.__value:
            self.indicator.pos = self.pos+self.axis*val*self.length/(self.max-self.min)
            self.__value = val
            self.execute()

class menu(ctrl):
    def __init__(self, **args):
        self.type = 'menu'
        ctrl.__init__(self, args)
        self.items = []
        self.width = self.height = 40
        self.text = None
        self.color = gray
        self.nitem = 0
        self.open = 0 # true if menu display open in the window
        self.action = 1 # dummy placeholder; what is driven is menu.execute()
        if args.has_key('width'):
            self.width = args['width']
        if args.has_key('height'):
            self.height = args['height']
        if args.has_key('text'):
            self.text = args['text']
        if args.has_key('color'):
            self.color = args['color']
        self.thick = 0.2*self.width
        disp = self.controls.display
        self.active = box(display=disp, pos=self.pos+vector(0,0,self.thick),
                       size=(self.width,self.height,self.thick), color=self.color)
        self.label = label(display=disp, pos=self.active.pos, color=color.black,
                           text=self.text, line=0, box=0, opacity=0)

    def inmenu(self, pos): # return item number (0-N) where mouse is, or -1
        # note that item is 0 if mouse is in menu title
        if self.pos.x-self.width/2. < pos.x < self.pos.x+self.width/2.:
            nitem = int((self.pos.y+self.height/2.-pos.y)/self.height)
            if 0 <= nitem <= len(self.items):
                return(nitem)
            else:
                return(-1)
        return(-1)
        
    def highlight(self, pos): # mouse down: open the menu, displaying the menu items
        self.nitem = self.inmenu(pos)
        if self.open: # "sticky" menu already open
            if self.nitem > 0:
                self.update(pos)
            else:
                self.unhighlight(pos)
                self.open = 0
            return
        pos = self.pos-vector(0,self.height,0)
        self.boxes = []
        self.highlightedbox = None
        disp = self.controls.display
        for item in self.items:
            self.boxes.append( (box(display=disp, pos=pos+vector(0,0,self.thick),
                       size=(self.width,self.height,self.thick), color=self.color),
                       label(display=disp, pos=pos, color=color.black,
                       text=item[0], line=0, box=0, opacity=0)) )
            pos = pos-vector(0,self.height,0)

    def unhighlight(self, pos): # mouse up: close the menu; selected item will be executed
        self.nitem = self.inmenu(pos)
        if self.nitem == 0 and not self.open: # don't close if mouse up in menu title
            self.controls.focus = self # restore menu to be in focus
            self.open = 1
            return
        for box in self.boxes:
            box[0].visible = 0
            box[1].visible = 0
        self.boxes = []
        self.open = 0
        self.execute()
            
    def update(self, pos): # highlight an individual item during drag
        self.nitem = self.inmenu(pos)
        if self.nitem > 0:
            if self.highlightedbox is not None:
                self.highlightedbox.color = gray
            if self.items[self.nitem-1][1]: # if there is an associated action
                self.highlightedbox = self.boxes[self.nitem-1][0]
                self.highlightedbox.color = darkgray
        else:
            if self.highlightedbox is not None:
                self.highlightedbox.color = gray
            self.highlightedbox = None

    def execute(self):
        if self.nitem > 0:
            action = self.items[self.nitem-1][1]
            if action:
                apply(action)
        

if __name__ == '__main__': # for testing the module

# Create "call-back" routines, routines that are called by the interact
# machinery when certain mouse events happen:

    def setdir(direction): # called on button up events
        cube.dir = direction

    def togglecubecolor(): # called on toggle switch flips
        if t1.value:
            cube.color = color.cyan
        else:
            cube.color = color.red

    def cubecolor(value): # called on a menu choice
        cube.color = value
        if cube.color == color.red:
            t1.value = 0 # make toggle switch setting consistent with menu choice
        else:
            t1.value = 1
        
    def setrate(obj): # called on slider drag events
        cuberate(obj.value) # value is min-max slider position
        if obj is s1:
            s2.value = s1.value # demonstrate coupling of the two sliders
        else:
            s1.value = s2.value

    def cuberate(value):
        cube.dtheta = 2*value*pi/1e4

    w = 350
    display(x=w, y=0, width=w, height=w, range=1.5, forward=-vector(0,1,1), newzoom=1)
    cube = box(color=color.red)

# In establishing the controls window, range=60 means what it usually means:
# (0,0) is in the center of the window, and (60,60) is the lower right corner.
# If range is not specified, the default is 100.
    c = controls(x=0, y=0, width=w, height=w, range=60)

# Buttons have a "text" attribute (the button label) which can be read and set.
# Toggles have "text0" and "text1" attributes which can be read and set.
# Toggles and sliders have a "value" attribute (0/1, or location of indicator) which can be read and set.

# The pos attribute for buttons, toggles, and menus is the center of the control (like "box").
# The pos attribute for sliders is at one end, and axis points to the other end (like "cylinder").

# By default a control is created in the most recently created "controls" window, but you
# can change this by specifying "controls=..." when creating a button, toggle, slider, or menu.

# The Python construct "lambda: setdir(-1)" below passes the location of the setdir function
# to the interact machinery, which uses "apply" to call the function when an action
# is to be taken. This scheme ensures that the execution of the function takes place
# in the appropriate namespace context in the case of importing the controls module.

    bl = button(pos=(-30,30), height=30, width=40, text='Left', action=lambda: setdir(-1))
    br = button(pos=(30,30), height=30, width=40, text='Right', action=lambda: setdir(1))
    s1 = slider(pos=(-15,-40), width=7, length=70, axis=(1,0.7,0), action=lambda: setrate(s1))
    s2 = slider(pos=(-30,-50), width=7, length=50, axis=(0,1,0), action=lambda: setrate(s2))
    t1 = toggle(pos=(40,-30), width=10, height=10, text0='Red', text1='Cyan', action=lambda: togglecubecolor())
    m1 = menu(pos=(0,0,0), height=7, width=25, text='Options')

# After creating the menu heading, add menu items:
    m1.items.append(('Left', lambda: setdir(-1))) # specify menu item title and action to perform
    m1.items.append(('Right', lambda: setdir(1)))
    m1.items.append(('---------',None)) # a dummy separator
    m1.items.append(('Red', lambda: cubecolor(color.red)))
    m1.items.append(('Cyan', lambda: cubecolor(color.cyan)))

    s1.value = 70 # update the slider
    setrate(s1) # set the rotation rate of the cube
    setdir(-1) # set the rotation direction of the cube

    while 1:
        rate(100)
        c.interact() # check for events, drive actions; must be executed repeatedly in a loop
        cube.rotate(axis=(0,1,0), angle=cube.dir*cube.dtheta)
       
