#from visual import * __all__ = visual._fix_symbols( globals() ) + [ 'controls','button','toggle','slider','menu'] # 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. # Sliders modified by Lenore Horner 2009 to permit labelling with slider value (after colorsliders.py from the examples) 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() self.x = x self.y = y self.width = width self.height = height self.range = range self.title = title if title is None: title = 'Controls' if foreground is None: foreground = color.white if background is None: background = color.black self.foreground = foreground self.background = background self.display = display(title=title, x=x, y=y, range=range, width=width, height=height, fov=0.4, foreground=foreground, background=background, userzoom=0, userspin=0) self.display.lights=[distant_light(direction=(0,0,1),color=color.white)] 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: 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=(2*width,2*width,2*width), color=scolor) self.label = label(display=disp, pos=self.indicator.pos+vector(0,0,2.*width+1), text="%0.2f" % self.__value, opacity=0, box=0, line=0) 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.label.pos = self.indicator.pos self.label.text="%0.2f" % 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 = 'Menu' self.__value = 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 getvalue(self): return self.__value value = property(getvalue, None) # establishes special getattr/setattr handling 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+vector(0,0,self.thick), 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: self.__value = self.items[self.nitem-1][0] action = self.items[self.nitem-1][1] if action: 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 call the setdir 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)