class checkbox_pref(InstanceMacro): #e rename -- Checkbox(...), with various kinds of args to say what state it uses in different ways? #e make it one of several prefs controls for other types of pref and control #e generalize to all named state -- e.g. see also LocalVariable_StateRef -- permit passing in the stateref? #e get dflt label from stateref?? # note: this was split out of kluge_dragtool_state_checkbox_expr 061214, # extended here into a def (later renamed checkbox_pref_OLDER), then a class prefs_key = Arg(str) label = Arg(Anything) # string or Widget2D dflt = ArgOrOption(bool, False) sbar_text = Option(str, '') use_label = If(call_Expr(lambda label: type(label) == type(""), label), TextRect(label), label) ## was TextRect(label,1,20) use_sbar_text = or_Expr( sbar_text, If(call_Expr(lambda label: type(label) == type(""), label), label, "")) stateref = Instance(PrefsKey_StateRef(prefs_key, dflt)) # note: without Instance here, next line stateref.value says (correctly): ## AssertionError: compute method asked for on non-Instance <PrefsKey_StateRef#47221(a)> var = stateref.value checkbox = If( var, checkbox_image('mac_checkbox_on.png'), checkbox_image('mac_checkbox_off.png'), ) _value = DisplayListChunk( Highlightable( SimpleRow(CenterY(checkbox), CenterY(use_label)), # align = CenterY is nim ## on_press = Set(debug_evals_of_Expr(stateref.value), not_Expr(var) ), #070119 debug_evals_of_Expr - worked on_press=_self.on_press, # the following works too, but I wanted to intercept it to add some py code [070305]: ## Set( stateref.value, not_Expr(var) ), sbar_text=use_sbar_text)) # note: using DisplayListChunk in _value works & is faster [070103] #070124 comment: the order DisplayListChunk( Highlightable( )) presumably means that the selobj # (which draws the highlightable's delegate) doesn't include the displist; doesn't matter much; # that CenterY(use_label) inside might be ok, or might be a bug which is made up for by the +0.5 I'm adding to drawfont2 # in testdraw.py today -- not sure. incr_drawable = Instance( Boxed(CenterY(checkbox), pixelgap=0, bordercolor=gray, borderwidth=2)) # I tried orange as a warning color -- means the checkbox reflects an intention but not yet the reality. # But it was annoyingly too visible. So I'll try gray. # If all colorboxeds are unpopular, then try an image that has a little spray of lines coming from the center, instead. def on_press(self): self.stateref.value = not self.stateref.value # was, in the expr: Set( stateref.value, not_Expr(var) ) ###e revise this code to use self.draw_incrementally once that's refiled into Highlightable ###e def func(self=self): self.incr_drawable.draw() ## self.draw() # includes the label - probably a waste but who cares self.env.glpane.swapBuffers() # update display [needed] ran_already_flag, funcres = self.run_OpenGL_in_local_coords( func) # this method runs in the Highlightable made in _value assert ran_already_flag return pass # end of class checkbox_pref
class TestIterator_wrong_to_compare(InstanceMacro): """ variant of that which shows one Instance twice """ thing = Arg(Widget) # the only difference w1 = Instance(thing) # these Instances will be idempotent w2 = Instance(thing) _value = SimpleColumn( w1, w2) # show one instance twice pass
class TestIterator(InstanceMacro): """ simple iterator which makes two distinct instances of the same arg """ #e for debug, we should make args to pass to this which show their ipaths as text! thing = ArgExpr(Widget) w1 = Instance(thing) w2 = Instance(thing) # two distinct instances _value = SimpleColumn( w1, w2) pass
class NativeImage( DelegatingInstanceOrExpr ): #070304 [works (imperfectly? see comments there) in testexpr_11u6] """ Show an image in its native size and aspect ratio -- that is, one image pixel == one texture pixel == one screen pixel, when the local coordsys is the standard viewing coordsys. Note: callers are advised to enclose this inside Highlightable, at least until we add local glnames to fix the bug of all bareMotion over non-Highlightable drawing causing redraws. """ # args filename = Arg(str, "x") #e better type, eg Filename? ###BUG: ought to take all options and pass them on to Image [070403 comment] # formulae [non-public in spite of the names] im1_expr = Image(filename, use_mipmaps=True, ideal_width=-1, ideal_height=-1) # customize Image to use native texture size (but without controlling the aspect ratio for display) ###e consider also including the options that IconImage does, or options passed by caller # (but passing general opts (like python **kws syntax can do) is nim in the exprs language) im1 = Instance( im1_expr) # grab an actual image so we can find out its native size th = im1._texture_holder # gets its _texture_holder object imops = th._image # get its nEImageOps object # appearance delegate = im1_expr( size=Center(Rect(imops.orig_width * PIXELS, imops.orig_height * PIXELS))) # (implem note: this customizes im1_expr options even though its args (filename) are already supplied. # That used to give a warning but apparently doesn't now. If it does again someday, # it's easy to fix here -- just give filename in each instantation rather than in im1_expr.) pass
class _height_dragger(DelegatingInstanceOrExpr): # args #e coordsystem? for now let x/y be the way it spreads, z be the height height_ref = Arg(StateRef, doc="stateref to a height variable") # appearance/behavior #e should draw some "walls" too, and maybe limit the height # note: the following sets up a cyclic inter-Instance ref ( drag_handler -> delegate -> drag_handler); # it works since the actual refs are symbolic (getattr_Expr(_self, 'attr'), not evalled except when used). drag_handler = Instance( xxx_drag_behavior(_self._delegate, height_ref, Ray(ORIGIN, DX))) ### DX for initial test, then DZ # note: passing _self._delegate (not _self.delegate) fixed my "undiagnosed bug". delegate = DraggableObject( Image( "blueflake.png" ), ###e needs an option to be visible from both sides (default True, probably) ## constrain_delta = _self.constrain_delta, ###IMPLEM constrain_delta in DraggableObject ## delta_stateref = height_ref ###IMPLEM delta_stateref [done] (and rename it); and change this to make height_ref a number not vector; ##e and maybe include the constraint in that transform, since it seems implicit in it anyway. ####HOW in semantics? # ... = PropertyRef(DZ * height_ref.value, Set( height_ref.value, project_onto_unit_vector( _something, DZ )) # ... = TransformStateRef( height_ref, ... ) # funcs? or formula in terms of 1 or 2 vars? with a chance at solving it?? ##e _kluge_drag_handler=drag_handler) ## def constrain_delta(self, delta): ## not yet - called ## return project_onto_unit_vector( delta, DZ) pass
class _height_dragger_3(DelegatingInstanceOrExpr): # args height_ref = Arg(StateRef, doc = "stateref to a height variable") direction = Arg(Vector) sbar_text = Option(str, "_height_dragger_3") range = Option(tuple_Expr, None, doc = "range limit of height") # appearance/behavior #e should draw some "walls" too, and maybe limit the height drag_handler = Instance( DragBehavior_AlongLine( _self._delegate, height_ref, ## Ray(ORIGIN, DX) # works ## Ray(ORIGIN, DZ) # works, but only if you trackball it (as expected)... ## Ray(ORIGIN, direction) # fails -- Ray is an ordinary class, not an expr! ###FIX call_Expr(Ray, ORIGIN, direction), # this workaround fixes it for now. # (in prior commit it didn't fix it, but only because of a typo in the testexpr defs # in tests.py, which meant I passed DZ when I thought I passed DX.) range = range )) ### NOTE: drag_handler is also being used to compute the translation from the height, even between drags. delegate = Overlay( Highlightable( Translate( Image("blueflake.png"), ###e needs an option to be visible from both sides (default True, probably) drag_handler._translation ###k ok?? only if that thing hangs around even in between drags, i guess! #e #k not sure if this code-commoning is good, but it's tempting. hmm. ), sbar_text = sbar_text, behavior = drag_handler ), Translate(Rect(2), direction * -0.01), Line(ORIGIN, ORIGIN + direction * height_ref.value, white) ) pass
class test_polyline_drag(State_preMixin, ExampleCommand): # class constants needed by mode API for example commands commandName = 'test_polyline_drag-commandName' default_mode_status_text = "test_polyline_drag" featurename = "Prototype: Example Polyline Drag Command" ## PM_class = test_polyline_drag_PM # tracked state rubberBand_enabled = State( bool, False, doc="whether we're rubberbanding a new segment now") # TODO: ### grab a polyline object from an example file in exprs ### (the one that has a closed boolean and can draw in 3d) ### have a list of them, we're rubberbanding last segment of last one... lastSegmentStart = ORIGIN + 6 * DY # stub lastSegmentEnd = State( Point, doc="endpoint of last segment; only meaningful when rubberBand_enabled" ) # note: these might be redundant with some points in a polyline object; # should they be aliases into one? rubberBandDrawable = If_expr( rubberBand_enabled, Line(lastSegmentStart, lastSegmentEnd, red, thickness=4), NullDrawable) whatWeDraw = Instance(rubberBandDrawable) # init methods def __init__(self, glpane): # not sure why this method is needed, see comment in test_connectWithState.__init__ super(test_polyline_drag, self).__init__(glpane) ExampleCommand.__init__(self, glpane) return def Draw(self): super(test_polyline_drag, self).Draw() #k self.whatWeDraw.draw() return def leftDown(self, event): print "leftDown" # TODO: # get the point (mouseray in plane) # make a real point there (or mark it as real, if we have a point object for it already) # (so it has a blue dot of its own) # update self.rubberBand_enabled # STUB: self.rubberBand_enabled = True self.lastSegmentEnd = self.lastSegmentStart + 3 * DX ExampleCommand.leftDown(self, event) ### will this prevent this error in mouse release: ## AttributeError: 'test_polyline_drag' object has no attribute 'LMB_press_event' ## [GLPane.py:1805] [selectAtomsMode.py:899] return pass
class SimpleColumn(Widget2D): #061115 #e or use InstanceMacro using Overlay & Translate? Could work, but slower and not needed... might help in CL with fancier gaps. ## a0 = Arg(Widget2D) # note: this probably doesn't preclude caller from passing None, and even if it does, nothing enforces that yet; # if caller does pass None (to this or to Overlay), best thing is probably to replace this with Pass = Rect(0,0,white) # and use it as a drawable, but have special case to use no gap under it -- or the equiv, as a simple change # to our btop formula so it's 0 if not a0 -- which is already done, so maybe there's no need to worry about a0 = None. # Maybe it should be an optional arg like the others. [061115] a0 = Arg(Widget2D, None) # even the first arg can be missing, as when applying it to a list of no elts [061205] a1 = Arg(Widget2D, None) a2 = Arg(Widget2D, None) a3 = Arg(Widget2D, None) a4 = Arg(Widget2D, None) a5 = Arg(Widget2D, None) # use ArgList here when that works a6 = Arg(Widget2D, None) a7 = Arg(Widget2D, None) a8 = Arg(Widget2D, None) a9 = Arg(Widget2D, None) a10 = Arg(Widget2D, None) a11 = Arg(Widget2D, None) # the 12th arg is 1 too many toomany = Instance(TextRect("too many args to SimpleColumn")) #070212 added Instance to fix mysterious bug manifesting as this debug output: ## getattr_debugprint: <lexenv_ipath_Expr... <TextRect#2331(a)>> has no 'bleft' args = list_Expr(a0,a1,a2,a3,a4,a5, a6,a7,a8,a9,a10, # could say or_Expr(a0, Spacer(0)) but here is not where it matters and_Expr(a11, toomany) ) ## gap = Option(Width, 3 * PIXELS) pixelgap = Option(float, 3) # 070104 int -> float gap = pixelgap * PIXELS print_lbox = Option(bool, False) #061127 for debugging; should be harmless; never tested (target bug got diagnosed in another way) drawables = call_Expr(lambda args: filter(None, args) , args) ## empty = not drawables ###e BUG: needs more Expr support, I bet; as it is, likely to silently be a constant False; not used internally empty = not_Expr(drawables) bleft = call_Expr(lambda drawables: max([arg.bleft for arg in drawables] + [0]) , drawables) # 070211 arg.bleft -> getattr_debugprint(arg, 'bleft') bright = call_Expr(lambda drawables: max([arg.bright for arg in drawables] + [0]) , drawables) height = call_Expr(lambda drawables, gap: sum([arg.height for arg in drawables]) + gap * max(len(drawables)-1,0) , drawables, gap) ## btop = a0 and a0.btop or 0 # bugfix -- use _Expr forms instead; i think this form silently turned into a0.btop [061205] btop = or_Expr( and_Expr( a0, a0.btop), 0) bbottom = height - btop def draw(self): if self.print_lbox: print "print_lbox: %r lbox attrs are %r" % (self, (self.bleft, self.bright, self.bbottom, self.btop)) glPushMatrix() prior = None for a in self.drawables: if prior: # move from prior to a dy = prior.bbottom + self.gap + a.btop glTranslatef(0,-dy,0) # positive is up, but Column progresses down prior = a self.drawkid(a) ## a.draw() glPopMatrix() return pass # end of class SimpleColumn or SimpleColumn_OLD
class TestIterator_alsoobsnow_nevertried(InstanceMacro): """ simple iterator which makes two instances of the same arg """ #e for debug, we should make args to pass to this which show their ipaths as text! thing = Arg(Maker(Widget)) # Maker? ExprFor? ProducerOf? Producer? Expr? w1 = Instance(thing) #k is number of evals correct in principle? internal uses of Instance assumed the expr was literal, were they wrong? # analyzing the code: this calls _i_inst with (an arg that evaluated to) getattr_Expr(_self, 'thing'), # and to instantiate that, it evals it, returning (I think) whatever self.thing is, which should be an instance of the arg. # This may make no sense but it's predicted that self.w1 and self.w2 should be instances, and the same one, of the arg. # that's wrong [thing's formula instantiates once too much, Instance(thing) once too little since I meant, I guess, I(*thing)] # , but it's not what I seem to be seeing, which is w1.width below running on either a non-instance # or something with a non-instance inside it. The non-instance is Translate, w1 should be a Boxed, so maybe that's consistent # if there's an error in Boxed. So I'm adding sanity checks to zome of: Boxed, Overlay, InstanceMacro, Translate. ###DOIT ## print "w1 before ExprsMeta = %r" % (w1,) ### w2 = Instance(thing) # kluge since we don't have Row yet: _value = Overlay( w1, Translate(w2, V_expr(w1.width + 4 * PIXELS, 0,0))) pass
class whatever(DelegatingInstanceOrExpr): ###e rename # simulates the env that demo_ui will provide (stub version, evolving to be more like it) ui_and_world = Instance(World()) ##### ###e following needs to permit cmd_DrawOnSurface to vary # (at least let it also be cmd_MakeRect; use a menu of options? or use one ActionButton per command, since more like a toolbar?) # -- but with Instance inside the variation, I think -- # ie it should be a map from the desired cmd expr to the cmd instance -- or, make a new one each time, so it's a cmdrun... # maybe see how demo_ui/toolbar was planning to do it... ###e toolbar = SimpleColumn( ActionButton( _self.do_cmd_DrawOnSurface, "button: cmd_DrawOnSurface" ), #e make the text from the command #e the running one should look pressed ActionButton(_self.do_cmd_MakeRect, "button: cmd_MakeRect"), ) ###e show it where? above PM for now? current_cmdrun = State( Anything, None ) # what is actually in here? an Instance of a "command run", [btw is None needed??] # or of a command obj that handles multiple runs (ie a command_runner?)... up to it to not get messed up if that happens # (and for now, unfortunately, not remaking it is probably a significant speed optim) # (so should we let an outer command handler have the PM and get reused, but an inner CommandRun get remade? why bother?) def do_cmd_DrawOnSurface( self ): #e which name is better: do_cmd or set_cmd? depends on type of command! self._do_cmd(cmd_DrawOnSurface) def do_cmd_MakeRect(self): self._do_cmd(cmd_MakeRect) def _do_cmd(self, cmd): "set self.current_cmdrun, etc..." # do we make a new one if button pressed when one already running?? yes for now. self.current_cmdrun = self.Instance( cmd(world=self.ui_and_world), id(cmd)) #e cache that expr? index ok? why cache instance? #old cmt: #e args? world? new object? does its super handle some? Command vs CommandRun? pm = current_cmdrun.property_manager corner_stuff = SimpleColumn(toolbar, pm) delegate = Overlay( current_cmdrun, DrawInCorner(corner_stuff, corner=PM_CORNER), ) def _init_instance(self): super(whatever, self)._init_instance() self.do_cmd_MakeRect( ) # or at least set some command, preferably a "null" or "default" one # note that this resets the current tool state on reload -- not really desirable; # how was demo_ui planning to handle that? ###k pass
class PM_Command( DelegatingInstanceOrExpr ): #e review name, etc ##e delegating?? ###k hmm, is it a command, or a CommandRun? can those be same class? "superclass for commands with their own property managers" # default ways to get the PM property_manager_groups = () # typically overridden by a subclass property_manager_message = _self.__class__.__name__ #e improve, use _C_ if needed # typically overridden by a subclass property_manager = Instance( PM_from_groups(_self.property_manager_groups, message=_self.property_manager_message)) # find the world -- maybe this belongs in a yet higher Command superclass? ###e world = Option(World) pass
class SimpleRow(Widget2D): # copy of SimpleColumn, but bbottom <-> bright, btop <-> bleft, width <- height, and 0,-dy -> dx,0, basically a0 = Arg(Widget2D, None) a1 = Arg(Widget2D, None) a2 = Arg(Widget2D, None) a3 = Arg(Widget2D, None) a4 = Arg(Widget2D, None) a5 = Arg(Widget2D, None) # use ArgList here when that works a6 = Arg(Widget2D, None) a7 = Arg(Widget2D, None) a8 = Arg(Widget2D, None) a9 = Arg(Widget2D, None) a10 = Arg(Widget2D, None) a11 = Arg(Widget2D, None) toomany = Instance(TextRect("too many args to SimpleRow")) args = list_Expr(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, and_Expr(a11, toomany)) pixelgap = Option(int, 3) gap = pixelgap * PIXELS drawables = call_Expr(lambda args: filter(None, args), args) empty = not_Expr(drawables) btop = call_Expr( lambda drawables: max([arg.btop for arg in drawables] + [0]), drawables) bbottom = call_Expr( lambda drawables: max([arg.bbottom for arg in drawables] + [0]), drawables) width = call_Expr( lambda drawables, gap: sum([arg.width for arg in drawables]) + gap * max(len(drawables) - 1, 0), drawables, gap) bleft = or_Expr(and_Expr(a0, a0.bleft), 0) bright = width - bleft def draw(self): glPushMatrix() prior = None for a in self.drawables: if prior: # move from prior to a dx = prior.bright + self.gap + a.bleft glTranslatef(dx, 0, 0) # positive is right, and Row progresses right prior = a self.drawkid(a) ## a.draw() glPopMatrix() return pass # end of class SimpleRow
class cmd_MakeRect(PM_Command): """#doc """ # name and description cmd_name = "MakeRect" cmd_desc = "make a screen-aligned rectangle" # in abs coords, with choosable fill color # property manager property_manager_groups = list_Expr(make_Rect_PG(_self)) property_manager_message = """ Drag out a purple Rect. """ # appearance of world while we're active, including code for click/drag event handlers for objects or bg background = Instance( _cmd_MakeRect_BG(world=_self.world) ) #k might not need to be split out, once bugs are fixed delegate = Overlay( _self.world, background, ####e SHOULD NOT BE NEEDED, but doesn't work anyway BackgroundObject(background), ) pass # end of class cmd_MakeRect
class DraggablyBoxed( Boxed ): # 070316; works 070317 [testexpr_36] before ww,hh State or resizable, and again (_36b) after them # inherit args, options, formulae from Boxed thing = _self.thing ###k WONT WORK unless we kluge ExprsMeta to remove this assignment from the namespace -- which we did. ###e not sure this is best syntax though. attr = _super.attr implies it'd work inside larger formulae, but it can't; # attr = Boxed.attr might be ok, whether it can work is not reviewed; it too might imply what _super does, falsely I think. extra1 = _self.extra1 borderthickness = _self.borderthickness rectframe = _self.rectframe # a pure expr # new options resizable = Option(bool, False, doc="whether to make it resizable at lower right") # works 070317 10pm (testexpr_36b) except for a few ###BUGS [updated info 070318 7pm]: # + [fixed] the wrong corner resizes (top right) (logic bug) # + [fixed] resizer doesn't move (understood -- wrong expr for its posn; commented below) # - negative sizes allowed (missing feature - limit the drag - need new DragBehavior feature) # - no clipping to interior of rectframe (missing feature - draw something clipped) # - perspective view ought to work, but entirely ###UNTESTED. # also, cosmetic bugs: # - resizer doesn't follow mouse in rotated coordsys, even in ortho view (though it's still useable). # (This is not surprising -- we're using the wrong kind of DragBehavior as a simple kluge.) # - the resizer is ugly, in shape & color. clipped = Option( bool, False, doc="###doc") #070322 new feature ### make True default after testing? # state # WARNING: due to ipath persistence, if you revise dflt_expr you apparently need to restart ne1 to see the change. ## ww = State(Width, thing.width + 2 * extra1) # replaces non-state formula in superclass -- seems to work ## hh = State(Width, thing.height + 2 * extra1) ## # now we just need a way to get a stateref to, effectively, the 3-tuple (ww,hh,set-value-discarder) ... instead, use whj: whj = State(Vector, V_expr(thing.width + 2 * extra1, -thing.height - 2 * extra1, 0)) #e not sure this is sound in rotated coordsys translation = State(Vector, ORIGIN) # override super formulae ww = whj[0] # seems to work hh = neg_Expr( whj[1] ) # negative is needed since drag down (negative Y direction) needs to increase height # (guess: neg_Expr wouldn't be needed if we used an appropriate new DragBehavior in resizer, # rather than our current klugy use of SimpleDragBehavior) # appearance rectframe_h = Instance( Highlightable( ## rectframe(bordercolor=green),####### cust is just to see if it works -- it doesn't, i guess i sort of know why ##bug: __call__ of <getattr_Expr#8243: (S._self, <constant_Expr#8242: 'rectframe'>)> with: () {'bordercolor': (0.0, 1.0, 0.0)} ##AssertionError: getattr exprs are not callable TopLeft(rectframe), #e different colored hover-highlighted version?? for now, just use sbar_text to know you're there. sbar_text= "draggable box frame", # this disappears on press -- is that intended? ###k behavior=SimpleDragBehavior( # arg1: the highlightable _self.rectframe_h, # arg2: a write-capable reference to _self.translation ## fails - evalled at compile time, not an expr: LvalueFromObjAndAttr( _self, 'translation'), ###BUG: why didn't anything complain when that bug caused the state value to be an add_Expr, not a number-array? call_Expr(LvalueFromObjAndAttr, _self, 'translation'), #e alternate forms for this that we might want to make work: # - getattr_StateRef(_self, 'translation') # simple def of the above # - StateRef_for( _self.translation ) # turns any lvalue into a stateref! Name is not good enough, though. ))) resizer = Instance( Highlightable( Center(Rect(extra1, extra1)), #e also try BottomRight highlighted=Center(Rect(extra1, extra1, white)), pressed=_my.highlighted, sbar_text="resize the box frame", behavior=SimpleDragBehavior( _self.resizer, call_Expr(LvalueFromObjAndAttr, _self, 'whj')))) ###BUG: in Boxed, rectframe comes first, so lbox attrs are delegated to it. We should do that too -- # but right now we draw it later in order to obscure the thing if they overlap. With clipping we won't need that -- # but without clipping we will. If the latter still matters, we need a version of Overlay with delegation != drawing order, # or, to delegate appearance and layout to different instances ourselves. (Or just to define new formulae for lbox -- easiest.) #e drawme = Instance( Overlay( If( clipped, Clipped( thing, planes=[ call_Expr(clip_to_right_of_x0, -thing.bleft - extra1 + ww - borderthickness), # note: the (- borderthickness) term makes the clipping reach exactly # to the inner rectframe edge. Without it, clipping would reach to the outer edge, # which for 3d objects inside the frame can cause them to obscure it. # (Other interesting values are (- extra1) and (- borderthickness/2.0), # but they both look worse, IMHO.) call_Expr(clip_below_y0, thing.btop + extra1 - hh + borderthickness) ]), thing, ), Translate(rectframe_h, V_expr(-thing.bleft - extra1, thing.btop + extra1)), If( resizable, ## Translate( resizer, V_expr( thing.bright + extra1, - thing.bbottom - extra1)) ###WRONG - this posn is fixed by thing's lbox dims, not affected by ww, hh; # will the fix be clearer if we use a TopLeft alignment expr? # It'd be hard to use it while maintaining thing's origin for use by external alignment -- # but maybe there's no point in doing that. Translate( resizer, V_expr(-thing.bleft - extra1 + ww, thing.btop + extra1 - hh))))) _value = Translate( drawme, ## DisplayListChunk( drawme), ###k this DisplayListChunk might break the Highlightable in rectframe_h ##### translation) pass # end of class DraggablyBoxed
class EditNanotube_EditCommand(State_preMixin, EditCommand): """ Command to edit a NanotubeSegment (nanotube). """ # class constants GraphicsMode_class = EditNanotube_GraphicsMode PM_class = EditNanotube_PropertyManager commandName = 'EDIT_NANOTUBE' featurename = "Edit Nanotube" from utilities.constants import CL_SUBCOMMAND command_level = CL_SUBCOMMAND command_parent = 'BUILD_NANOTUBE' command_should_resume_prevMode = True command_has_its_own_PM = True flyoutToolbar = None call_makeMenus_for_each_event = True handlePoint1 = State( Point, ORIGIN) handlePoint2 = State( Point, ORIGIN) #The minimum 'stopper'length used for resize handles #@see: self._update_resizeHandle_stopper_length for details. _resizeHandle_stopper_length = State(Width, -100000) rotationHandleBasePoint1 = State( Point, ORIGIN) rotationHandleBasePoint2 = State( Point, ORIGIN) #See self._update_resizeHandle_radius where this gets changed. #also see EditNanotube_ResizeHandle to see how its implemented. handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) #@TODO: modify the 'State params for rotation_distance rotation_distance1 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) rotation_distance2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) leftHandle = Instance( EditNanotube_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth'), origin = handlePoint1, fixedEndOfStructure = handlePoint2, direction = norm_Expr(handlePoint1 - handlePoint2), sphereRadius = handleSphereRadius1, range = (_resizeHandle_stopper_length, 10000) )) rightHandle = Instance( EditNanotube_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth2'), origin = handlePoint2, fixedEndOfStructure = handlePoint1, direction = norm_Expr(handlePoint2 - handlePoint1), sphereRadius = handleSphereRadius2, range = (_resizeHandle_stopper_length, 10000) )) rotationHandle1 = Instance( RotationHandle( command = _self, rotationDistanceRef = call_Expr( ObjAttr_StateRef, _self, 'rotation_distance1'), center = handlePoint1, axis = norm_Expr(handlePoint1 - handlePoint2), origin = rotationHandleBasePoint1, radiusVector = norm_Expr(rotationHandleBasePoint1 - handlePoint1) )) rotationHandle2 = Instance( RotationHandle( command = _self, rotationDistanceRef = call_Expr( ObjAttr_StateRef, _self, 'rotation_distance2'), center = handlePoint2, axis = norm_Expr(handlePoint2 - handlePoint1), origin = rotationHandleBasePoint2, radiusVector = norm_Expr(rotationHandleBasePoint2 - handlePoint2) )) def __init__(self, commandSequencer): """ Constructor for InsertDna_EditCommand """ glpane = commandSequencer.assy.glpane State_preMixin.__init__(self, glpane) EditCommand.__init__(self, commandSequencer) #Graphics handles for editing the structure . self.handles = [] self.grabbedHandle = None #Initialize DEBUG preference pref_nt_segment_resize_by_recreating_nanotube() return def editStructure(self, struct = None): EditCommand.editStructure(self, struct) if self.hasValidStructure(): #TODO 2008-03-25: better to get all parameters from self.struct and #set it in propMgr? This will mostly work except that reverse is #not true. i.e. we can not specify same set of params for #self.struct.setProps ...because endPoint1 and endPoint2 are derived. #by the structure when needed. self.propMgr.setParameters(self.struct.getProps()) #Store the previous parameters. Important to set it after you #set nanotube attrs in the propMgr. #self.previousParams is used in self._previewStructure and #self._finalizeStructure to check if self.struct changed. self.previousParams = self._gatherParameters() self._updateHandleList() self.updateHandlePositions() return def hasValidStructure(self): """ Tells the caller if this edit command has a valid structure. Overrides EditCommand.hasValidStructure() """ #(By Bruce 2008-02-13) isValid = EditCommand.hasValidStructure(self) if not isValid: return isValid # would like to check here whether it's empty of axis chunks; # instead, this will do for now (probably too slow, though): p1, p2 = self.struct.nanotube.getEndPoints() return (p1 is not None) def _getStructureType(self): """ Subclasses override this method to define their own structure type. Returns the type of the structure this editCommand supports. This is used in isinstance test. @see: EditCommand._getStructureType() (overridden here) """ return self.win.assy.NanotubeSegment def _updateHandleList(self): """ Updates the list of handles (self.handles) @see: self.editStructure @see: EditNanotube_GraphicsMode._drawHandles() """ # note: if handlePoint1 and/or handlePoint2 can change more often than this # runs, we'll need to rerun the two assignments above whenever they # change and before the handle is drawn. An easy way would be to rerun # these assignments in the draw method of our GM. [bruce 080128] self.handles = [] # guess, but seems like a good idea [bruce 080128] self.handles.append(self.leftHandle) self.handles.append(self.rightHandle) if DEBUG_ROTATION_HANDLES: self.handles.append(self.rotationHandle1) self.handles.append(self.rotationHandle2) return def updateHandlePositions(self): """ Update handle positions and also update the resize handle radii and their 'stopper' lengths. @see: self._update_resizeHandle_radius() @see: self._update_resizeHandle_stopper_length() @see: EditNanotube_GraphicsMode._drawHandles() """ self.handlePoint1 = None # Needed! self.handlePoint2 = None #TODO: Call this method less often by implementing model_changed #see bug 2729 for a planned optimization self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE self._update_resizeHandle_radius() handlePoint1, handlePoint2 = self.struct.nanotube.getEndPoints() if 0: # Debug prints print "updateHandlePositions(): handlePoint1=", handlePoint1 print "updateHandlePositions(): handlePoint2=", handlePoint2 if handlePoint1 is not None and handlePoint2 is not None: # (that condition is bugfix for deleted axis segment, bruce 080213) self.handlePoint1, self.handlePoint2 = handlePoint1, handlePoint2 #Update the 'stopper' length where the resize handle being dragged #should stop. See self._update_resizeHandle_stopper_length() #for more details self._update_resizeHandle_stopper_length() if DEBUG_ROTATION_HANDLES: self.rotation_distance1 = CYLINDER_WIDTH_DEFAULT_VALUE self.rotation_distance2 = CYLINDER_WIDTH_DEFAULT_VALUE #Following computes the base points for rotation handles. #to be revised -- Ninad 2008-02-13 unitVectorAlongAxis = norm(self.handlePoint1 - self.handlePoint2) v = cross(self.glpane.lineOfSight, unitVectorAlongAxis) self.rotationHandleBasePoint1 = self.handlePoint1 + norm(v) * 4.0 self.rotationHandleBasePoint2 = self.handlePoint2 + norm(v) * 4.0 return def _update_resizeHandle_radius(self): """ Finds out the sphere radius to use for the resize handles, based on atom /chunk or glpane display (whichever decides the display of the end atoms. The default value is 1.2. @see: self.updateHandlePositions() """ self.handleSphereRadius1 = HANDLE_RADIUS_DEFAULT_VALUE self.handleSphereRadius2 = HANDLE_RADIUS_DEFAULT_VALUE return def _update_resizeHandle_stopper_length(self): """ Update the limiting length at which the resize handle being dragged should 'stop' without proceeding further in the drag direction. The segment resize handle stops when you are dragging it towards the other resizeend and the distance between the two ends reaches two duplexes. The self._resizeHandle_stopper_length computed in this method is used as a lower limit of the 'range' option provided in declaration of resize handle objects (see class definition for the details) @see: self.updateHandlePositions() """ total_length = vlen(self.handlePoint1 - self.handlePoint2) nanotubeRise = self.struct.nanotube.getRise() self._resizeHandle_stopper_length = - total_length + nanotubeRise return def _gatherParameters(self): """ Return the parameters from the property manager UI. @return: The endpoints of the nanotube. @rtype: tuple (endPoint1, endPoint2). """ return self.propMgr.getParameters() def _createStructure(self): """ Returns the current NanotubeSegment being edited with a new nanotube chunk. @return : Nanotube segment that include the new nanotube chunk. @rtype: L{NanotubeSegment} """ try: # Create a new nanotube chunk using new params. n, m, type, endings, endPoint1, endPoint2 = self._gatherParameters() from cnt.model.NanotubeParameters import NanotubeParameters self.nanotube = NanotubeParameters() nanotube = self.nanotube nanotube.setChirality(n, m) nanotube.setType(type) nanotube.setEndings(endings) nanotube.setEndPoints(endPoint1, endPoint2) position = V(0.0, 0.0, 0.0) ntChunk = nanotube.build(self.struct.name, self.win.assy, position) nanotube.computeEndPointsFromChunk(ntChunk) # Needed. self.struct.addchild(ntChunk) #WARNING 2008-03-05: #When we actually permit modifying a nanotube without recreating it, #then the following properties must be set in self._modifyStructure #as well. Needs more thought. props =(nanotube.getChirality(), nanotube.getType(), nanotube.getEndings(), nanotube.getEndPoints()) self.struct.setProps(props) return self.struct except (PluginBug, UserError): self.struct.kill() raise PluginBug("Internal error while trying to recreate a NanotubeSegment.") return None def _modifyStructure(self, params): """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old nanotube and replaces it with a new one using self._createStructure. """ if not pref_nt_segment_resize_by_recreating_nanotube(): self._modifyStructure_NEW_SEGMENT_RESIZE(params) return # To modify the structure, we need to: # 1. Delete the current nanotube chunk inside the NanotubeSegment group. # 2. Create a new new nanotube chunk using nanotube.build() # 3. Add the newly generated nanotube to this NanotubeSegment. # 4. Update all the nanotube parameters. # Note: Steps 2-4 are done in self._createStructure() assert self.struct self.struct.members[0].kill() # Delete the current nanotube chunk. self.previousParams = params self.struct = self._createStructure() return def _modifyStructure_NEW_SEGMENT_RESIZE(self, params): #@ NOT FIXED """ This resizes without recreating whole nanotube Overrides EditCommand._modifystructure. @attention: is not implemented. """ #@TODO: - rename this method from _modifyStructure_NEW_SEGMENT_RESIZE #to self._modifyStructure, after more testing #This method is used for debug prefence: #'Nanotube Segment: resize without recreating whole nanotube' #see also self.modifyStructure_NEW_SEGMENT_RESIZE assert self.struct from utilities.debug import print_compact_stack print_compact_stack("_modifyStructure_NEW_SEGMENT_RESIZE() not fixed!" ) print "Params =", params self.nanotube = params #@ length_diff = self._determine_how_to_change_length() #@ if length_diff == 0: print_compact_stack("BUG: length_diff is always ZERO." ) return elif length_diff > 0: print "Nanotube longer by ", length_diff, ", angstroms." else: print "Nanotube shorter by ", length_diff, ", angstroms." return if numberOfBasePairsToAddOrRemove != 0: #@@@@ Not reached. resizeEnd_final_position = self._get_resizeEnd_final_position( ladderEndAxisAtom, abs(numberOfBasePairsToAddOrRemove), nanotubeRise ) self.nanotube.modify(self.struct, length_diff, ladderEndAxisAtom.posn(), resizeEnd_final_position) #Find new end points of structure parameters after modification #and set these values in the propMgr. new_end1 , new_end2 = self.struct.nanotube.getEndPoints() #@ params_to_set_in_propMgr = (new_end1, new_end2) #TODO: Need to set these params in the PM #and then self.previousParams = params_to_set_in_propMgr self.previousParams = params return def _get_resizeEnd_final_position(self, ladderEndAxisAtom, numberOfBases, nanotubeRise): final_position = None if self.grabbedHandle: final_position = self.grabbedHandle.currentPosition else: other_axisEndAtom = self.struct.getOtherAxisEndAtom(ladderEndAxisAtom) axis_vector = ladderEndAxisAtom.posn() - other_axisEndAtom.posn() segment_length_to_add = 0 #@ final_position = ladderEndAxisAtom.posn() + norm(axis_vector)*segment_length_to_add return final_position def getStructureName(self): """ Returns the name string of self.struct if there is a valid structure. Otherwise returns None. This information is used by the name edit field of this command's PM when we call self.propMgr.show() @see: EditNanotube_PropertyManager.show() @see: self.setStructureName """ if self.hasValidStructure(): return self.struct.name return None def setStructureName(self, name): """ Sets the name of self.struct to param <name> (if there is a valid structure. The PM of this command callss this method while closing itself @param name: name of the structure to be set. @type name: string @see: EditNanotube_PropertyManager.close() @see: self.getStructureName() """ #@BUG: We call this method in self.propMgr.close(). But propMgr.close() #is called even when the command is 'cancelled'. That means the #structure will get changed even when user hits cancel button or #exits the command by clicking on empty space. #This should really be done in self._finalizeStructure but that #method doesn't get called when you click on empty space to exit #the command. See EditNanotube_GraphicsMode.leftUp for a detailed #comment. if self.hasValidStructure(): self.struct.name = name return def getCursorText(self): """ This is used as a callback method in NanotubeLine mode @see: NanotubeLineMode.setParams, NanotubeLineMode_GM.Draw """ if self.grabbedHandle is None: return text = '' textColor = env.prefs[cursorTextColor_prefs_key] if not env.prefs[editNanotubeEditCommand_showCursorTextCheckBox_prefs_key]: return text, textColor currentPosition = self.grabbedHandle.currentPosition fixedEndOfStructure = self.grabbedHandle.fixedEndOfStructure nanotubeLength = vlen( currentPosition - fixedEndOfStructure ) nanotubeLengthString = self._getCursorText_length(nanotubeLength) text = nanotubeLengthString #@TODO: The following updates the PM as the cursor moves. #Need to rename this method so that you that it also does more things #than just to return a textString -- Ninad 2007-12-20 self.propMgr.ntLengthLineEdit.setText(nanotubeLengthString) return text, textColor def _getCursorText_length(self, nanotubeLength): """ Returns a string that gives the length of the Nanotube for the cursor text """ nanotubeLengthString = '' if env.prefs[editNanotubeEditCommand_cursorTextCheckBox_length_prefs_key]: lengthUnitString = 'A' #change the unit of length to nanometers if the length is > 10A #fixes part of bug 2856 if nanotubeLength > 10.0: lengthUnitString = 'nm' nanotubeLength = nanotubeLength * 0.1 nanotubeLengthString = "%5.3f%s"%(nanotubeLength, lengthUnitString) return nanotubeLengthString def modifyStructure(self): """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{EditNanotube_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} As of 2008-02-01 it recreates the structure @see: a note in self._createStructure() about use of ntSegment.setProps """ if not pref_nt_segment_resize_by_recreating_nanotube(): self.modifyStructure_NEW_SEGMENT_RESIZE() return if self.grabbedHandle is None: return self.propMgr.endPoint1 = self.grabbedHandle.fixedEndOfStructure self.propMgr.endPoint2 = self.grabbedHandle.currentPosition #@length = vlen(self.propMgr.endPoint1 - self.propMgr.endPoint2 ) #@ self.preview_or_finalize_structure(previewing = True) self.updateHandlePositions() self.glpane.gl_update() return def modifyStructure_NEW_SEGMENT_RESIZE(self): #@ NOT FIXED """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{EditNanotube_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} As of 2008-02-01 it recreates the structure @see: a note in self._createStructure() about use of ntSegment.setProps """ #TODO: need to cleanup this and may be use use something like #self.previousParams = params in the end -- 2008-03-24 (midnight) #@TODO: - rename this method from modifyStructure_NEW_SEGMENT_RESIZE #to self.modifyStructure, after more testing #This method is used for debug prefence: #'Nanotube Segment: resize without recreating whole duplex' #see also self._modifyStructure_NEW_SEGMENT_RESIZE if self.grabbedHandle is None: return self.propMgr.endPoint1 = self.grabbedHandle.fixedEndOfStructure self.propMgr.endPoint2 = self.grabbedHandle.currentPosition DEBUG_DO_EVERYTHING_INSIDE_MODIFYSTRUCTURE_METHOD = False if DEBUG_DO_EVERYTHING_INSIDE_MODIFYSTRUCTURE_METHOD: # TO DO: this entire block of code. --Mark 2008-04-03 print_compact_stack("modifyStructure_NEW_SEGMENT_RESIZE(): NOT FIXED") length = vlen(self.grabbedHandle.fixedEndOfStructure - \ self.grabbedHandle.currentPosition ) endAtom1, endAtom2 = self.struct.getAxisEndAtoms() #@ for atm in (endAtom1, endAtom2): if not same_vals(self.grabbedHandle.fixedEndOfStructure, atm.posn()): ladderEndAxisAtom = atm break endPoint1, endPoint2 = self.struct.nanotube.getEndPoints() old_dulex_length = vlen(endPoint1 - endPoint2) nanotubeRise = self.struct.getProps() #@ params_to_set_in_propMgr = ( self.grabbedHandle.origin, self.grabbedHandle.currentPosition, ) ##self._modifyStructure(params) ############################################ self.nanotube = NanotubeParameters() #@ Creates 5x5 CNT. Missing PM params. length_diff = self._determine_how_to_change_length() ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() #@ #@ Nanotube class needs modify() method. self.nanotube.modify(self.struct, length_diff, ladderEndAxisAtom.posn(), self.grabbedHandle.currentPosition) #TODO: Important note: How does NE1 know that structure is modified? #Because number of base pairs parameter in the PropMgr changes as you #drag the handle . This is done in self.getCursorText() ... not the #right place to do it. OR that method needs to be renamed to reflect #this as suggested in that method -- Ninad 2008-03-25 self.preview_or_finalize_structure(previewing = True) ##self.previousParams = params_to_set_in_propMgr self.glpane.gl_update() return def get_axisEndAtom_at_resize_end(self): ladderEndAxisAtom = None if self.grabbedHandle is not None: ladderEndAxisAtom = self.struct.getAxisEndAtomAtPosition(self.grabbedHandle.origin) else: endAtom1, endAtom2 = self.struct.getAxisEndAtoms() ladderEndAxisAtom = endAtom2 return ladderEndAxisAtom def _determine_how_to_change_length(self): #@ NEEDS WORK """ Returns the difference in length between the original nanotube and the modified nanotube, where: 0 = no change in length > 0 = lengthen < 0 = trim """ nanotubeRise = self.struct.nanotube.getRise() endPoint1, endPoint2 = self.struct.nanotube.getEndPoints() #@ original_nanotube_length = vlen(endPoint1 - endPoint2) new_nanotube_length = vlen(endPoint1 - endPoint2) #@ return new_nanotube_length - original_nanotube_length #@ ALWAYS RETURNS ZERO def makeMenus(self): """ Create context menu for this command. """ if not hasattr(self, 'graphicsMode'): return selobj = self.glpane.selobj if selobj is None: return self.Menu_spec = [] highlightedChunk = None if isinstance(selobj, Chunk): highlightedChunk = selobj if isinstance(selobj, Atom): highlightedChunk = selobj.molecule elif isinstance(selobj, Bond): chunk1 = selobj.atom1.molecule chunk2 = selobj.atom2.molecule if chunk1 is chunk2 and chunk1 is not None: highlightedChunk = chunk1 if highlightedChunk is None: return highlightedChunk.make_glpane_cmenu_items(self.Menu_spec, self) return
class _cmd_MakeRect_BG(Highlightable): """Background event bindings for dragging out new Rects. (Not involved with selecting/moving/editing/resizing rects after they're made, even if that happens immediately to a rect this code makes from a single drag.) """ # args [needed for sharing state with something] world = Option(World) #e something to share state with a control panel in the PM - unless we just use prefs for that # The drag event handlers modify the following state and/or derived objects (some documented with their defs): # self.rubber_rect = None or a newly made Rect we're dragging out right now # (implemented as a rubberband-object drawable -- NOT a model object -- since it has formulae to our state, not its own state) # # nim: self.newnode = None or a Model Object version of the Rect we're dragging out now, or last dragged out... # in general this may be redundant with "the selection", if dragging out a rect selects it, or shift-dragging it adds it to sel... # note, which of these are change-tracked? the ones used in drawing. that means all of them i guess. startpoint = State(Point, None, doc="mousedown position; one corner of the rect" ) #e (or its center, for variants of this cmd) curpoint = State( Point, None, doc="last mouse position used for the other corner of the rect") making_a_rect_now = State( bool, False, doc="whether we're making a rect right now, using the current drag") # formulae whj = curpoint - startpoint # contains dims of current rect; only valid while we're making one, or after it's done before next press w = whj[ 0] ###k what if negative?? evidently we make a neg-width Rect and nothing complains and it draws properly... ###REVIEW h = whj[1] # ditto color = purple # for now -- will be a pref or PM control for the color to use for new rects rubber_rect = Instance( If(making_a_rect_now, Translate(Rect(w, h, color), startpoint))) # this Instance is needed, at least by Highlightable # appearance -- note, for superclass Highlightable, this is plain, not delegate, and must be an Instance. # This means it's not good to subclass Highlightable rather than delegating to it. (Especially in example code!) ###FIX ## delegate = _self.rubber_rect plain = _self.rubber_rect # code for event handlers during the drag. def on_press(self): self.startpoint = self.current_event_mousepoint() self.making_a_rect_now = False #k probably not needed # don't make until we drag! self.curpoint = None #k probably not needed, but might help to catch bugs where whj is used when it shouldn't be #e change cursor, someday; sooner, change state to name the desired cursor, and display that cursorname in the PM return def on_drag(self): self.curpoint = self.current_event_mousepoint(plane=self.startpoint) self.making_a_rect_now = True return def on_release(self): ##e decide whether to really make one... here, assume we do, as long as we started one: if self.making_a_rect_now: node_expr = DraggableObject( StatefulRect(self.rubber_rect) ) #e StatefulMovableRect? StatefulSnapshot(self.rubber_rect)?? ###k can we just pass the rubber rect and assume StatefulRect can grab its state from it by taking a snapshot?? ###WRONG for DraggableObject to be here, I think -- it's how we display it -- not sure, at least movability might be here... ### the posn is in the rubber_rect since it has Translate... this seems potentially bad/klugy tho... # older cmts, related: ... also its state needs to include abs posn... # maybe this means, split DraggableObject into event binding part (for this UI) and Movable part (for abs posn state). #k self.newnode = self.world.make_and_add(node_expr, type="Rect") ###BUG (understood): type = "Rect" is not affecting sbar_text in DraggableObject. Need to add it in StatefulRect itself. self.newnode.motion = self.startpoint ###KLUGE self.making_a_rect_now = False # hide the rubber rect now that the real one is there else: print "fyi: click with no drag did nothing" ##e remove after debug return pass # end of class _cmd_MakeRect_BG
class DraggableHandle_AlongCircle(DelegatingInstanceOrExpr): """ A kind of draggable handle which can be dragged along a line to change the value of a single floating point parameter (representing position along the line, using a scale and origin determined by our arguments). ### TODO: add epydoc parameters to this docstring? not sure that will work for a class! Maybe we need to synthesize those (from the doc options below) into a fake __init__ method for its sake?? """ # == args and options # options for appearance of handle appearance = Option( Drawable, Center(Rect(0.2, 0.2, white)), doc = "appearance, when not highlighted") appearance_highlighted = Option( Drawable, appearance, doc = "appearance, when highlighted") sbar_text = Option(str, "draggable handle along circle", doc = "statusbar text on mouseover") # state variable controlled by dragging rotationDistanceRef = Option(StateRef, doc = "stateref to a dragged disctance variable") # action options, for Highlightable to do after the ones that come from # DragBehavior_AlongLine [new feature of this and Highlightable, 080129] on_press = Option(Action) on_drag = Option(Action) on_release = Option(Action) on_release_in = Option(Action, on_release) on_release_out = Option(Action, on_release) on_doubleclick = Option(Action) #origin of the handle itself origin = Option( Point, ORIGIN) # Center of the circle whose perimeter serves as a path along which to # drag center = Option( Point, ORIGIN) #Axis of the circle. axis = Option( Vector, DX, doc = "vector giving direction and scale") #radius Vector radiusVector = Option( Vector, DX, doc = "vector giving direction and scale") #angle range for the rotation range_for_rotation = Option(tuple_Expr, None, doc = "range limit of angle (tuple)") # == internal instances and formulae (modified from test_statearray_3.py) _drag_handler = Instance( DragBehavior_AlongCircle( _self._delegate, rotationDistanceRef, origin, center, axis, radiusVector, range_for_rotation = range_for_rotation)) #QUESTION: Should the 'RotateTranslate' transform from exprs.transforms be #used here? -- Ninad 2008-02-13 delegate = \ Highlightable( Translate( appearance, _drag_handler._rotation #k ok?? only if that thing hangs around even in between drags, i guess! #e #k not sure if this code-commoning is good, but it's tempting. hmm. ), highlighted = Translate( appearance_highlighted, _drag_handler._rotation ), sbar_text = sbar_text, behavior = _drag_handler, on_press = _self.on_press, on_drag = _self.on_drag, # (note: no need to pass on_release) on_release_in = _self.on_release_in, on_release_out = _self.on_release_out, on_doubleclick = _self.on_doubleclick, ) pass # end of class
class test_connectWithState(State_preMixin, ExampleCommand): # class constants needed by mode API for example commands commandName = 'test_connectWithState-commandName' default_mode_status_text = "test_connectWithState" featurename = "Prototype: Test connectWithState" PM_class = test_connectWithState_PM # tracked state -- this initializes specially defined instance variables # which will track all their uses and changes so that connectWithState # works for them: cylinderVertical = State(bool, False) cylinderWidth = State(float, CYLINDER_WIDTH_DEFAULT_VALUE) # TODO: soon this will be the only use of this constant, so it can be inlined cylinderColor = State('color-stub', pink) # type should be Color (nim), but type is not yet used # note: you can add _e_debug = True to one or more of these State definitions # to see debug prints about some accesses to this state. GraphicsMode_class = _test_connectWithState_GM # init methods def __init__(self, glpane): # I don't know why this method is needed. ##### REVIEW (super semantics), FIX or clean up super(test_connectWithState, self).__init__(glpane) # State_preMixin.__init__ ExampleCommand.__init__(self, glpane) # (especially this part) return ## def __init__(self, glpane): ## super(test_connectWithState, self).__init__(glpane) #### # that only calls some mode's init method, #### # so (for now) call this separately: #### IorE_guest_mixin.__init__(self, glpane) ## return # exprs-based formulae (and some compute methods) direction = If_expr( cylinderVertical, DY, DX ) def _C_width_direction(self): """ compute self.width_direction """ # Note: to do this with a formula expr instead # would require cross_Expr to be defined, # and glpane.lineOfSight to be tracked. return cross( self.direction, self.env.glpane.lineOfSight ) width_direction = _self.width_direction # so it can be used in formulae below # stub for handle test code [070912] widthHandleEnabled = True # stub ## widthHandle = Instance(Rect()) # stub h_offset = 0.5 + 0.2 # get it from handle? nah (not good if that changes with time); just make it fit. # or we could decide that handles ought to have useful fixed bounding boxes... ## widthHandle = Instance(Translate(Center(Rect(0.5)), ## width_direction * (cylinderWidth / 2.0 + h_offset) )) #stub widthHandle = Instance( DraggableHandle_AlongLine( appearance = Center(Rect(0.5, 0.5, white)), ### REVIEW: # Can't we just replace the following with something based on the formula for the position, # width_direction * (cylinderWidth / 2.0 + h_offset) # ? # As it is, I have to manually solve that formula for origin and direction to pass in, # i.e. rewrite it as # position = origin + direction * cylinderWidth ## height_ref = cylinderWidth, ###WRONG ## height_ref = ObjAttr_StateRef( _self, 'cylinderWidth'), ## ## AssertionError: ObjAttr_StateRef fallback is nim -- needed for S._self height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth'), # guess at workaround; #e we need a more principled way! ### REVIEW: efficient enough? (guess: overhead only happens once, so yes) # could we say instead something like: height_ref = Variable(cylinderWidth) ?? Or VariableRef? Or StateRef_to ? origin = width_direction * h_offset, # note: also includes cylinder center, but that's hardcoded at ORIGIN direction = width_direction / 2.0, sbar_text = "cylinder width", ### TODO: make it a formula, include printed value of width? range = (0.1, 10), ### TODO: DraggableHandle_AlongLine should take values from the stateref if this option is not provided; # meanwhile, we ought to pass a consistent value! )) # Note: the Instance is required; but I'm not sure if it would be # if we were using a fuller exprs superclass or init code. [bruce 070912] def cmd_Bigger(self): self.cylinderWidth += 0.5 set_cylinder_height( cylinder_height() + 0.5) # TODO: enforce maxima return def cmd_Smaller(self): self.cylinderWidth -= 0.5 set_cylinder_height( cylinder_height() - 0.5) # enforce minima (###BUG: not the same ones as declared in the PM) ### REVISE: min & max should be declared in State macro and (optionally) enforced by it if self.cylinderWidth < 0.1: self.cylinderWidth = 0.1 if cylinder_height() < 0.1: set_cylinder_height(0.1) return pass
class NanotubeSegment_EditCommand(State_preMixin, EditCommand): """ Command to edit a NanotubeSegment object. To edit a segment, first enter BuildNanotube_EditCommand (accessed using Build > Nanotube) then, select an existing NanotubeSegment within the NanotubeGroup you are editing. When you select the NanotubeSegment, it enters NanotubeSegment_Editcommand and shows the property manager with its widgets showing the properties of selected segment. """ cmd = 'Nanotube Segment' sponsor_keyword = 'Nanotube' prefix = 'NanotubeSegment' # used for gensym cmdname = "NANOTUBE_SEGMENT" commandName = 'NANOTUBE_SEGMENT' featurename = 'Edit Nanotube Segment' command_should_resume_prevMode = True command_has_its_own_gui = True command_can_be_suspended = False create_name_from_prefix = True call_makeMenus_for_each_event = True #Graphics Mode GraphicsMode_class = NanotubeSegment_GraphicsMode #This is set to BuildDna_EditCommand.flyoutToolbar (as of 2008-01-14, #it only uses flyoutToolbar = None _parentNanotubeGroup = None handlePoint1 = State( Point, ORIGIN) handlePoint2 = State( Point, ORIGIN) #The minimum 'stopper'length used for resize handles #@see: self._update_resizeHandle_stopper_length for details. _resizeHandle_stopper_length = State(Width, -100000) rotationHandleBasePoint1 = State( Point, ORIGIN) rotationHandleBasePoint2 = State( Point, ORIGIN) #See self._update_resizeHandle_radius where this gets changed. #also see NanotubeSegment_ResizeHandle to see how its implemented. handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) #@TODO: modify the 'State params for rotation_distance rotation_distance1 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) rotation_distance2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) leftHandle = Instance( NanotubeSegment_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth'), origin = handlePoint1, fixedEndOfStructure = handlePoint2, direction = norm_Expr(handlePoint1 - handlePoint2), sphereRadius = handleSphereRadius1, range = (_resizeHandle_stopper_length, 10000) )) rightHandle = Instance( NanotubeSegment_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth2'), origin = handlePoint2, fixedEndOfStructure = handlePoint1, direction = norm_Expr(handlePoint2 - handlePoint1), sphereRadius = handleSphereRadius2, range = (_resizeHandle_stopper_length, 10000) )) rotationHandle1 = Instance( RotationHandle( command = _self, rotationDistanceRef = call_Expr( ObjAttr_StateRef, _self, 'rotation_distance1'), center = handlePoint1, axis = norm_Expr(handlePoint1 - handlePoint2), origin = rotationHandleBasePoint1, radiusVector = norm_Expr(rotationHandleBasePoint1 - handlePoint1) )) rotationHandle2 = Instance( RotationHandle( command = _self, rotationDistanceRef = call_Expr( ObjAttr_StateRef, _self, 'rotation_distance2'), center = handlePoint2, axis = norm_Expr(handlePoint2 - handlePoint1), origin = rotationHandleBasePoint2, radiusVector = norm_Expr(rotationHandleBasePoint2 - handlePoint2) )) def __init__(self, commandSequencer, struct = None): """ Constructor for DnaDuplex_EditCommand """ glpane = commandSequencer State_preMixin.__init__(self, glpane) EditCommand.__init__(self, commandSequencer) self.struct = struct #Graphics handles for editing the structure . self.handles = [] self.grabbedHandle = None #Initialize DEBUG preference pref_nt_segment_resize_by_recreating_nanotube() return def init_gui(self): """ Initialize gui. """ #Note that NanotubeSegment_EditCommand only act as an edit command for an #existing structure. The call to self.propMgr.show() is done only during #the call to self.editStructure ..i .e. only after self.struct is #updated. This is done because of the following reason: # - self.init_gui is called immediately after entering the command. # - self.init_gui in turn, initialized propMgr object and may also # show the property manager. The self.propMgr.show routine calls # an update widget method just before the show. This update method # updates the widgets based on the parameters from the existing # structure of the command (self.editCommand.struct) # Although, it checks whether this structure exists, the editCommand # could still have a self.struct attr from a previous run. (Note that # EditCommand API was written before the command sequencer API and # it has some loose ends like this. ) -- Ninad 2008-01-22 self.create_and_or_show_PM_if_wanted(showPropMgr = False) return def editStructure(self, struct = None): EditCommand.editStructure(self, struct) if self.hasValidStructure(): #When the structure (segment) is finalized (after the modifications) #it will be added to the original NanotubeGroup to which it belonged #before we began editing (modifying) it. self._parentNanotubeGroup = self.struct.getNanotubeGroup() #Set the endpoints #@ DOES THIS DO ANYTHING? I don't think so. --Mark 2008-04-01 #@endPoint1, endPoint2 = self.struct.nanotube.getEndPoints() #@params_for_propMgr = (endPoint1, endPoint2) #TODO 2008-03-25: better to get all parameters from self.struct and #set it in propMgr? This will mostly work except that reverse is #not true. i.e. we can not specify same set of params for #self.struct.setProps ...because endPoint1 and endPoint2 are derived. #by the structure when needed. Commenting out following line of code self.propMgr.setParameters(self.struct.getProps()) #Store the previous parameters. Important to set it after you #set nanotube attrs in the propMgr. #self.previousParams is used in self._previewStructure and #self._finalizeStructure to check if self.struct changed. self.previousParams = self._gatherParameters() self._updateHandleList() self.updateHandlePositions() return def keep_empty_group(self, group): """ Returns True if the empty group should not be automatically deleted. otherwise returns False. The default implementation always returns False. Subclasses should override this method if it needs to keep the empty group for some reasons. Note that this method will only get called when a group has a class constant autdelete_when_empty set to True. (and as of 2008-03-06, it is proposed that dna_updater calls this method when needed. @see: Command.keep_empty_group() which is overridden here. @see: BreakStrands_Command.keep_empty_group @see: Group.autodelete_when_empty.. a class constant used by the dna_updater (the dna updater then decides whether to call this method to see which empty groups need to be deleted) """ bool_keep = EditCommand.keep_empty_group(self, group) if not bool_keep: if self.hasValidStructure(): if group is self.struct: bool_keep = True elif group is self.struct.parent_node_of_class(self.assy.NanotubeGroup): bool_keep = True #If this command doesn't have a valid structure, as a fall back, #lets instruct it to keep ALL the NanotubeGroup objects even when empty #Reason? ..see explanation in BreakStrands_Command.keep_empty_group elif isinstance(group, self.assy.NanotubeGroup): bool_keep = True return bool_keep def hasValidStructure(self): """ Tells the caller if this edit command has a valid structure. Overrides EditCommand.hasValidStructure() """ #(By Bruce 2008-02-13) isValid = EditCommand.hasValidStructure(self) if not isValid: return isValid # would like to check here whether it's empty of axis chunks; # instead, this will do for now (probably too slow, though): p1, p2 = self.struct.nanotube.getEndPoints() return (p1 is not None) def _getStructureType(self): """ Subclasses override this method to define their own structure type. Returns the type of the structure this editCommand supports. This is used in isinstance test. @see: EditCommand._getStructureType() (overridden here) """ return self.win.assy.NanotubeSegment def _updateHandleList(self): """ Updates the list of handles (self.handles) @see: self.editStructure @see: NanotubeSegment_GraphicsMode._drawHandles() """ # note: if handlePoint1 and/or handlePoint2 can change more often than this # runs, we'll need to rerun the two assignments above whenever they # change and before the handle is drawn. An easy way would be to rerun # these assignments in the draw method of our GM. [bruce 080128] self.handles = [] # guess, but seems like a good idea [bruce 080128] self.handles.append(self.leftHandle) self.handles.append(self.rightHandle) if DEBUG_ROTATION_HANDLES: self.handles.append(self.rotationHandle1) self.handles.append(self.rotationHandle2) return def updateHandlePositions(self): """ Update handle positions and also update the resize handle radii and their 'stopper' lengths. @see: self._update_resizeHandle_radius() @see: self._update_resizeHandle_stopper_length() @see: NanotubeSegment_GraphicsMode._drawHandles() """ self.handlePoint1 = None # Needed! self.handlePoint2 = None #TODO: Call this method less often by implementing model_changed #see bug 2729 for a planned optimization self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE self._update_resizeHandle_radius() handlePoint1, handlePoint2 = self.struct.nanotube.getEndPoints() if 0: # Debug prints print "updateHandlePositions(): handlePoint1=", handlePoint1 print "updateHandlePositions(): handlePoint2=", handlePoint2 if handlePoint1 is not None and handlePoint2 is not None: # (that condition is bugfix for deleted axis segment, bruce 080213) self.handlePoint1, self.handlePoint2 = handlePoint1, handlePoint2 #Update the 'stopper' length where the resize handle being dragged #should stop. See self._update_resizeHandle_stopper_length() #for more details self._update_resizeHandle_stopper_length() if DEBUG_ROTATION_HANDLES: self.rotation_distance1 = CYLINDER_WIDTH_DEFAULT_VALUE self.rotation_distance2 = CYLINDER_WIDTH_DEFAULT_VALUE #Following computes the base points for rotation handles. #to be revised -- Ninad 2008-02-13 unitVectorAlongAxis = norm(self.handlePoint1 - self.handlePoint2) v = cross(self.glpane.lineOfSight, unitVectorAlongAxis) self.rotationHandleBasePoint1 = self.handlePoint1 + norm(v) * 4.0 self.rotationHandleBasePoint2 = self.handlePoint2 + norm(v) * 4.0 return def _update_resizeHandle_radius(self): """ Finds out the sphere radius to use for the resize handles, based on atom /chunk or glpane display (whichever decides the display of the end atoms. The default value is 1.2. @see: self.updateHandlePositions() """ self.handleSphereRadius1 = HANDLE_RADIUS_DEFAULT_VALUE self.handleSphereRadius2 = HANDLE_RADIUS_DEFAULT_VALUE return def _update_resizeHandle_stopper_length(self): """ Update the limiting length at which the resize handle being dragged should 'stop' without proceeding further in the drag direction. The segment resize handle stops when you are dragging it towards the other resizeend and the distance between the two ends reaches two duplexes. The self._resizeHandle_stopper_length computed in this method is used as a lower limit of the 'range' option provided in declaration of resize handle objects (see class definition for the details) @see: self.updateHandlePositions() """ total_length = vlen(self.handlePoint1 - self.handlePoint2) nanotubeRise = self.struct.nanotube.getRise() self._resizeHandle_stopper_length = - total_length + nanotubeRise return def _createPropMgrObject(self): """ Creates a property manager object (that defines UI things) for this editCommand. """ assert not self.propMgr propMgr = self.win.createNanotubeSegmentPropMgr_if_needed(self) return propMgr def _gatherParameters(self): """ Return the parameters from the property manager UI. @return: The endpoints of the nanotube. @rtype: tuple (endPoint1, endPoint2). """ return self.propMgr.getParameters() def _createStructure(self): """ Creates and returns the structure (in this case a L{NanotubeSegment} object. @return : Nanotube segment that include the nanotube chunk. @rtype: L{NanotubeSegment} """ # self.name needed for done message if self.create_name_from_prefix: # create a new name name = self.name = gensym(self.prefix, self.win.assy) # (in _build_struct) self._gensym_data_for_reusing_name = (self.prefix, name) else: # use externally created name self._gensym_data_for_reusing_name = None # (can't reuse name in this case -- not sure what prefix it was # made with) name = self.name # Create the model tree group node. # Make sure that the 'topnode' of this part is a Group (under which the # DNa group will be placed), if the topnode is not a group, make it a # a 'Group' (applicable to Clipboard parts).See part.py # --Part.ensure_toplevel_group method. This is an important line # and it fixes bug 2585 self.win.assy.part.ensure_toplevel_group() ntSegment = NanotubeSegment(self.name, self.win.assy, self.win.assy.part.topnode, editCommand = self ) try: # Make the NanotubeSegment. n, m, type, endings, endPoint1, endPoint2 = self._gatherParameters() from cnt.model.Nanotube import Nanotube self.nanotube = Nanotube() nanotube = self.nanotube nanotube.setChirality(n, m) nanotube.setType(type) nanotube.setEndings(endings) nanotube.setEndPoints(endPoint1, endPoint2) position = V(0.0, 0.0, 0.0) ntChunk = nanotube.build(self.name, self.win.assy, position) nanotube.computeEndPointsFromChunk(ntChunk) ntSegment.addchild(ntChunk) #set some properties such as nanotubeRise #This information will be stored on the NanotubeSegment object so that #it can be retrieved while editing this object. #Should these props be assigned to the NanotubeSegment in #Nanotube.build() itself? This needs to be answered while modifying #build() method to fit in the dna data model. --Ninad 2008-03-05 #WARNING 2008-03-05: Since self._modifyStructure calls #self._createStructure() #If in the near future, we actually permit modifying a #structure (such as dna) without actually recreating the whole #structure, then the following properties must be set in #self._modifyStructure as well. Needs more thought. props =(nanotube.getChirality(), nanotube.getType(), nanotube.getEndings(), nanotube.getEndPoints()) ntSegment.setProps(props) return ntSegment except (PluginBug, UserError): # Why do we need UserError here? Mark 2007-08-28 ntSegment.kill() raise PluginBug("Internal error while trying to create a NanotubeSegment.") return def _modifyStructure(self, params): """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old structure and creates a new one using self._createStructure. This was needed for the structures like this (Dna, Nanotube etc) . . See more comments in the method. """ if not pref_nt_segment_resize_by_recreating_nanotube(): self._modifyStructure_NEW_SEGMENT_RESIZE(params) return assert self.struct # parameters have changed, update existing structure self._revertNumber() # self.name needed for done message if self.create_name_from_prefix: # create a new name name = self.name = gensym(self.prefix, self.win.assy) # (in _build_struct) self._gensym_data_for_reusing_name = (self.prefix, name) else: # use externally created name self._gensym_data_for_reusing_name = None # (can't reuse name in this case -- not sure what prefix it was # made with) name = self.name #@NOTE: Unlike editcommands such as Plane_EditCommand, this #editCommand actually removes the structure and creates a new one #when its modified. -- Ninad 2007-10-24 self._removeStructure() self.previousParams = params self.struct = self._createStructure() # Now append the new structure in self._segmentList (this list of # segments will be provided to the previous command # (BuildDna_EditCommand) # TODO: Should self._createStructure does the job of appending the # structure to the list of segments? This fixes bug 2599 # (see also BuildDna_PropertyManager.Ok if self._parentNanotubeGroup is not None: #Should this be an assertion? (assert self._parentNanotubeGroup is not #None. For now lets just print a warning if parentNanotubeGroup is None self._parentNanotubeGroup.addSegment(self.struct) return def _modifyStructure_NEW_SEGMENT_RESIZE(self, params): #@ NOT FIXED """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old structure and creates a new one using self._createStructure. This was needed for the structures like this (Dna, Nanotube etc) . . See more comments in the method. @attention: is not implemented. """ #@TODO: - rename this method from _modifyStructure_NEW_SEGMENT_RESIZE #to self._modifyStructure, after more testing #This method is used for debug prefence: #'Nanotube Segment: resize without recreating whole duplex' #see also self.modifyStructure_NEW_SEGMENT_RESIZE assert self.struct from utilities.debug import print_compact_stack print_compact_stack("_modifyStructure_NEW_SEGMENT_RESIZE() not fixed!" ) print "Params =", params self.nanotube = params #@ length_diff = self._determine_how_to_change_length() #@ if length_diff == 0: print_compact_stack("BUG: length_diff is always ZERO." ) return elif length_diff > 0: print "Nanotube longer by ", length_diff, ", angstroms." else: print "Nanotube shorter by ", length_diff, ", angstroms." return if numberOfBasePairsToAddOrRemove != 0: #@@@@ Not reached. resizeEnd_final_position = self._get_resizeEnd_final_position( ladderEndAxisAtom, abs(numberOfBasePairsToAddOrRemove), nanotubeRise ) self.nanotube.modify(self.struct, length_diff, ladderEndAxisAtom.posn(), resizeEnd_final_position) #Find new end points of structure parameters after modification #and set these values in the propMgr. new_end1 , new_end2 = self.struct.nanotube.getEndPoints() #@ params_to_set_in_propMgr = (new_end1, new_end2) #TODO: Need to set these params in the PM #and then self.previousParams = params_to_set_in_propMgr self.previousParams = params return def _get_resizeEnd_final_position(self, ladderEndAxisAtom, numberOfBases, nanotubeRise): final_position = None if self.grabbedHandle: final_position = self.grabbedHandle.currentPosition else: other_axisEndAtom = self.struct.getOtherAxisEndAtom(ladderEndAxisAtom) axis_vector = ladderEndAxisAtom.posn() - other_axisEndAtom.posn() segment_length_to_add = 0 #@ final_position = ladderEndAxisAtom.posn() + norm(axis_vector)*segment_length_to_add return final_position def getStructureName(self): """ Returns the name string of self.struct if there is a valid structure. Otherwise returns None. This information is used by the name edit field of this command's PM when we call self.propMgr.show() @see: NanotubeSegment_PropertyManager.show() @see: self.setStructureName """ if self.hasValidStructure(): return self.struct.name return None def setStructureName(self, name): """ Sets the name of self.struct to param <name> (if there is a valid structure. The PM of this command callss this method while closing itself @param name: name of the structure to be set. @type name: string @see: NanotubeSegment_PropertyManager.close() @see: self.getStructureName() """ #@BUG: We call this method in self.propMgr.close(). But propMgr.close() #is called even when the command is 'cancelled'. That means the #structure will get changed even when user hits cancel button or #exits the command by clicking on empty space. #This should really be done in self._finalizeStructure but that #method doesn't get called when you click on empty space to exit #the command. See NanotubeSegment_GraphicsMode.leftUp for a detailed #comment. if self.hasValidStructure(): self.struct.name = name return def getCursorText(self): """ This is used as a callback method in NanotubeLine mode @see: NanotubeLineMode.setParams, NanotubeLineMode_GM.Draw """ if self.grabbedHandle is None: return if not env.prefs[nanotubeSegmentEditCommand_showCursorTextCheckBox_prefs_key]: return '', black text = "" textColor = black currentPosition = self.grabbedHandle.currentPosition fixedEndOfStructure = self.grabbedHandle.fixedEndOfStructure nanotubeLength = vlen( currentPosition - fixedEndOfStructure ) nanotubeLengthString = self._getCursorText_length(nanotubeLength) text = nanotubeLengthString #@TODO: The following updates the PM as the cursor moves. #Need to rename this method so that you that it also does more things #than just to return a textString -- Ninad 2007-12-20 self.propMgr.ntLengthLineEdit.setText(nanotubeLengthString) return text, textColor def _getCursorText_length(self, nanotubeLength): """ Returns a string that gives the length of the Nanotube for the cursor text """ nanotubeLengthString = '' if env.prefs[nanotubeSegmentEditCommand_cursorTextCheckBox_length_prefs_key]: lengthUnitString = 'A' #change the unit of length to nanometers if the length is > 10A #fixes part of bug 2856 if nanotubeLength > 10.0: lengthUnitString = 'nm' nanotubeLength = nanotubeLength * 0.1 nanotubeLengthString = "%5.3f%s"%(nanotubeLength, lengthUnitString) return nanotubeLengthString def modifyStructure(self): """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{NanotubeSegment_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} As of 2008-02-01 it recreates the structure @see: a note in self._createStructure() about use of ntSegment.setProps """ if not pref_nt_segment_resize_by_recreating_nanotube(): self.modifyStructure_NEW_SEGMENT_RESIZE() return if self.grabbedHandle is None: return self.propMgr.endPoint1 = self.grabbedHandle.fixedEndOfStructure self.propMgr.endPoint2 = self.grabbedHandle.currentPosition #@length = vlen(self.propMgr.endPoint1 - self.propMgr.endPoint2 ) #@ self.preview_or_finalize_structure(previewing = True) self.updateHandlePositions() self.glpane.gl_update() return def modifyStructure_NEW_SEGMENT_RESIZE(self): #@ NOT FIXED """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{NanotubeSegment_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} As of 2008-02-01 it recreates the structure @see: a note in self._createStructure() about use of ntSegment.setProps """ #TODO: need to cleanup this and may be use use something like #self.previousParams = params in the end -- 2008-03-24 (midnight) #@TODO: - rename this method from modifyStructure_NEW_SEGMENT_RESIZE #to self.modifyStructure, after more testing #This method is used for debug prefence: #'Nanotube Segment: resize without recreating whole duplex' #see also self._modifyStructure_NEW_SEGMENT_RESIZE if self.grabbedHandle is None: return self.propMgr.endPoint1 = self.grabbedHandle.fixedEndOfStructure self.propMgr.endPoint2 = self.grabbedHandle.currentPosition DEBUG_DO_EVERYTHING_INSIDE_MODIFYSTRUCTURE_METHOD = False if DEBUG_DO_EVERYTHING_INSIDE_MODIFYSTRUCTURE_METHOD: # TO DO: this entire block of code. --Mark 2008-04-03 print_compact_stack("modifyStructure_NEW_SEGMENT_RESIZE(): NOT FIXED") length = vlen(self.grabbedHandle.fixedEndOfStructure - \ self.grabbedHandle.currentPosition ) endAtom1, endAtom2 = self.struct.getAxisEndAtoms() #@ for atm in (endAtom1, endAtom2): if not same_vals(self.grabbedHandle.fixedEndOfStructure, atm.posn()): ladderEndAxisAtom = atm break endPoint1, endPoint2 = self.struct.nanotube.getEndPoints() old_dulex_length = vlen(endPoint1 - endPoint2) nanotubeRise = self.struct.getProps() #@ params_to_set_in_propMgr = ( self.grabbedHandle.origin, self.grabbedHandle.currentPosition, ) ##self._modifyStructure(params) ############################################ self.nanotube = Nanotube() #@ Creates 5x5 CNT. Miisng PM params. length_diff = self._determine_how_to_change_length() ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() #@ #@ Nanotube class needs modify() method. self.nanotube.modify(self.struct, length_diff, ladderEndAxisAtom.posn(), self.grabbedHandle.currentPosition) #TODO: Important note: How does NE1 know that structure is modified? #Because number of base pairs parameter in the PropMgr changes as you #drag the handle . This is done in self.getCursorText() ... not the #right place to do it. OR that method needs to be renamed to reflect #this as suggested in that method -- Ninad 2008-03-25 self.preview_or_finalize_structure(previewing = True) ##self.previousParams = params_to_set_in_propMgr self.glpane.gl_update() return def get_axisEndAtom_at_resize_end(self): ladderEndAxisAtom = None if self.grabbedHandle is not None: ladderEndAxisAtom = self.struct.getAxisEndAtomAtPosition(self.grabbedHandle.origin) else: endAtom1, endAtom2 = self.struct.getAxisEndAtoms() ladderEndAxisAtom = endAtom2 return ladderEndAxisAtom def _determine_how_to_change_length(self): #@ NEEDS WORK """ Returns the difference in length between the original nanotube and the modified nanotube, where: 0 = no change in length > 0 = lengthen < 0 = trim """ nanotubeRise = self.struct.nanotube.getRise() endPoint1, endPoint2 = self.struct.nanotube.getEndPoints() #@ original_nanotube_length = vlen(endPoint1 - endPoint2) new_nanotube_length = vlen(endPoint1 - endPoint2) #@ return new_nanotube_length - original_nanotube_length #@ ALWAYS RETURNS ZERO def makeMenus(self): """ Create context menu for this command. (Build Dna mode) """ if not hasattr(self, 'graphicsMode'): return selobj = self.glpane.selobj if selobj is None: return self.Menu_spec = [] highlightedChunk = None if isinstance(selobj, Chunk): highlightedChunk = selobj if isinstance(selobj, Atom): highlightedChunk = selobj.molecule elif isinstance(selobj, Bond): chunk1 = selobj.atom1.molecule chunk2 = selobj.atom2.molecule if chunk1 is chunk2 and chunk1 is not None: highlightedChunk = chunk1 if highlightedChunk is None: return if self.hasValidStructure(): nanotubeGroup = self.struct.parent_node_of_class(self.assy.NanotubeGroup) if nanotubeGroup is None: return #following should be self.struct.getNanotubeGroup or self.struct.getNanotubeGroup #need to formalize method name and then make change. if not nanotubeGroup is highlightedChunk.parent_node_of_class(self.assy.NanotubeGroup): item = ("Edit unavailable: Member of a different NanotubeGroup", noop, 'disabled') self.Menu_spec.append(item) return highlightedChunk.make_glpane_context_menu_items(self.Menu_spec, command = self) return
class DraggableHandle_AlongLine(DelegatingInstanceOrExpr): ### TODO: all options might need renaming! replace "height" everywhere. """ A kind of draggable handle which can be dragged along a line to change the value of a single floating point parameter (representing position along the line, using a scale and origin determined by our arguments). ### TODO: add epydoc parameters to this docstring? not sure that will work for a class! Maybe we need to synthesize those (from the doc options below) into a fake __init__ method for its sake?? """ # == args and options # options for appearance of handle appearance = Option( Drawable, Center(Rect(0.2, 0.2, white)), doc = "our appearance, when not highlighted") ###e take other args of Highlightable? appearance_highlighted = Option( Drawable, appearance, doc = "our appearance, when highlighted") sbar_text = Option(str, "draggable handle", doc = "our statusbar text on mouseover") # state variable controlled by dragging height_ref = Option(StateRef, doc = "stateref to a height variable") # TODO: rename, redoc range = Option(tuple_Expr, None, doc = "range limit of height (tuple)") ### MAYBE: RENAME to avoid conflict with python range in this code ### TODO: take values from the stateref if this option is not provided # action options, for Highlightable to do after the ones that come from # DragBehavior_AlongLine [new feature of this and Highlightable, 080129] on_press = Option(Action) on_drag = Option(Action) on_release = Option(Action) on_release_in = Option(Action, on_release) on_release_out = Option(Action, on_release) on_doubleclick = Option(Action) # line along which to drag it, and how to interpret the state as a position along that line (origin and scale) origin = Option( Point, ORIGIN) direction = Option( Vector, DX, doc = "vector giving direction and scale") # providing a default value is mainly just for testing #If this is false, the 'highlightable' object i.e. this handle #won't be drawn. The delegate (that defines a Highlightable) #We define an If_expr to check whether to draw the highlightable object. #[by Ninad] # [this should probably be revised, with hasValidParamsForDrawing replaced # with an overridable compute method, for robustness -- current implem # is suspected of causing tracebacks from insufficient update of this # state. Need to review whether methodname needs to be hasValidParamsForDrawing # to conform with any API. -- bruce 080409 comment] should_draw = State(bool, True) # == internal instances and formulae (modified from test_statearray_3.py) _drag_handler = Instance( DragBehavior_AlongLine( _self._delegate, height_ref, call_Expr(Ray, origin, direction), # note: we need call_Expr since Ray is an ordinary class, not an expr! ###FIX range = range )) # note: _drag_handler is also being used to compute the translation from the height, even between drags. #@see: DnaStrand_ResizeHandle.hasValidParamsForDrawing #@see: definition of State attr should_draw delegate = \ If_expr( _self.should_draw, Highlightable( Translate( appearance, _drag_handler._translation #k ok?? only if that thing hangs around even in between drags, i guess! #e #k not sure if this code-commoning is good, but it's tempting. hmm. ), highlighted = Translate( appearance_highlighted, _drag_handler._translation ), sbar_text = sbar_text, behavior = _drag_handler, on_press = _self.on_press, on_drag = _self.on_drag, # (note: no need to pass on_release) on_release_in = _self.on_release_in, on_release_out = _self.on_release_out, on_doubleclick = _self.on_doubleclick ) #end of Highlightable ) #end of If_expr def hasValidParamsForDrawing(self): """ Overridden in subclasses. Default implementation returns True if this object (the highlightable) can be drawn without any known issues @see: DnaStrand_ResizeHandle.hasValidParamsForDrawing for more notes. """ self.should_draw = True return self.should_draw pass # end of class
class MultipleDnaSegmentResize_EditCommand(DnaSegment_EditCommand): #Graphics Mode GraphicsMode_class = MultipleDnaSegmentResize_GraphicsMode #Property Manager PM_class = MultipleDnaSegmentResize_PropertyManager cmdname = 'MULTIPLE_DNA_SEGMENT_RESIZE' # REVIEW: needed? correct? commandName = 'MULTIPLE_DNA_SEGMENT_RESIZE' featurename = "Edit Multiple Dna Segments" from utilities.constants import CL_SUBCOMMAND command_level = CL_SUBCOMMAND command_parent = 'BUILD_DNA' #This command operates on (resizes) multiple DnaSegments at once. #It does that by looping through the DnaSegments and resizing them #individually. self.currentStruct is set to the 'current' DnaSegment #in that loop and then it is used in the resizing code. #@see: DnaSegmentList class #@see: self.modifyStructure() currentStruct = None handlePoint1 = State(Point, ORIGIN) handlePoint2 = State(Point, ORIGIN) #The minimum 'stopper'length used for resize handles #@see: self._update_resizeHandle_stopper_length for details. _resizeHandle_stopper_length = State(Width, -100000) rotationHandleBasePoint1 = State(Point, ORIGIN) rotationHandleBasePoint2 = State(Point, ORIGIN) #See self._update_resizeHandle_radius where this gets changed. #also see DnaSegment_ResizeHandle to see how its implemented. handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) #@TODO: modify the 'State params for rotation_distance rotation_distance1 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) rotation_distance2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) leftHandle = Instance( DnaSegment_ResizeHandle( command=_self, height_ref=call_Expr(ObjAttr_StateRef, _self, 'cylinderWidth'), origin=handlePoint1, fixedEndOfStructure=handlePoint2, direction=norm_Expr(handlePoint1 - handlePoint2), sphereRadius=handleSphereRadius1, range=(_resizeHandle_stopper_length, 10000))) rightHandle = Instance( DnaSegment_ResizeHandle( command=_self, height_ref=call_Expr(ObjAttr_StateRef, _self, 'cylinderWidth2'), origin=handlePoint2, fixedEndOfStructure=handlePoint1, direction=norm_Expr(handlePoint2 - handlePoint1), sphereRadius=handleSphereRadius2, range=(_resizeHandle_stopper_length, 10000))) def _update_previousParams_in_model_changed(self): """ Overrides superclass method. Does nothing in this class. @see: self.model_changed() which calls this method. """ pass def isAddSegmentsToolActive(self): """ Returns True if the Add segments tool in the PM, that allows adding dna segments from the resize segment list, is active. @see: MultipleDnaSegmentResize_GraphicsMode.chunkLeftUp() @see: MultipleDnaSegmentResize_GraphicsMode.end_selection_from_GLPane() @see: self.isRemoveSegmentsToolActive() @see: self.addSegmentToResizeSegmentList() """ if self.propMgr is None: return False return self.propMgr.isAddSegmentsToolActive() def isRemoveSegmentsToolActive(self): """ Returns True if the Remove Segments tool in the PM, that allows removing dna segments from the resize segment list, is active. @see: MultipleDnaSegmentResize_GraphicsMode.chunkLeftUp() @see: MultipleDnaSegmentResize_GraphicsMode.end_selection_from_GLPane() @see: self.isAddSegmentsToolActive() @see: self.removeSegmentFromResizeSegmentList() """ if self.propMgr is None: return False return self.propMgr.isRemoveSegmentsToolActive() def addSegmentToResizeSegmentList(self, segment): """ Adds the given segment to the resize segment list @param segment: The DnaSegment to be added to the resize segment list. Also does other things such as updating resize handles etc. @type sement: B{DnaSegment} @see: self.isAddSegmentsToolActive() """ assert isinstance(segment, self.win.assy.DnaSegment) self.struct.addToStructList(segment) def removeSegmentFromResizeSegmentList(self, segment): """ Removes the given segment from the resize segment list @param segment: The DnaSegment to be removed from the resize segment list. Also does other things such as updating resize handles etc. @type sement: B{DnaSegment} @see: self.isRemoveSegmentsToolActive() """ assert isinstance(segment, self.win.assy.DnaSegment) self.struct.removeFromStructList(segment) def setResizeStructList(self, structList): """ Replaces the list of segments to be resized with the given segmentList. Calls the related method of self.struct. @param structList: New list of segments to be resized. @type structList: list @see: DnaSegmentList.setResizeStructList() """ if self.hasValidStructure(): self.struct.setStructList(structList) self.updateHandlePositions() def getResizeSegmentList(self): """ Returns a list of segments to be resized at once. @see: DnaSegmentList.getStructList() """ if self.hasValidStructure(): return self.struct.getStructList() return () def updateResizeSegmentList(self): """ Update the list of resize segments (maintained by self.struct) """ if self.hasValidStructure(): self.struct.updateStructList() def editStructure(self, struct=None): """ Overrides superclass method. It first creates a B{DnaSegmentList} object using the provided list (struct argument) The DnaSegmentList object acts as self.struct. @param struct: The caller of this method should provide a list of containing all the structurs that need to be edited (resized) at once. Caller also needs to make sure that this list contains objects of class DnaSegment. @type struct: list @see: B{DnaSegmentList} """ if isinstance(struct, list): dnaSegmentListObject = DnaSegmentList(self, structList=struct) _superclass.editStructure(self, struct=dnaSegmentListObject) def _updatePropMgrParams(self): """ Subclasses may override this method. Update some property manager parameters with the parameters of self.struct (which is being edited) @see: self.editStructure() @TODO: For multiple Dna segment resizing, this method does nothing as of 2008-05-14. Need to be revised. """ #Commented out code as of 2008-05-14 =========== #endPoint1, endPoint2 = self.struct.getAxisEndPoints() #params_for_propMgr = (None, #endPoint1, #endPoint2) ##TODO 2008-03-25: better to get all parameters from self.struct and ##set it in propMgr? This will mostly work except that reverse is ##not true. i.e. we can not specify same set of params for ##self.struct.setProps ...because endPoint1 and endPoint2 are derived. ##by the structure when needed. Commenting out following line of code ##UPDATE 2008-05-06 Fixes a bug due to which the parameters in propMGr ##of DnaSegment_EditCommand are not same as the original structure ##(e.g. bases per turn and duplexrise) ###self.propMgr.setParameters(params_for_propMgr) pass def _update_resizeHandle_radius(self): """ Finds out the sphere radius to use for the resize handles, based on atom /chunk or glpane display (whichever decides the display of the end atoms. The default value is 1.2. @see: self.updateHandlePositions() @see: B{Atom.drawing_radius()} @TODO: Refactor this. It does essentially same thing as the superclass method. The problem is due to the use of the global constant HANDLE_RADIUS_DEFAULT_VALUE which needs to be different (bigger) in this class. So better to make it a class constant (need similar changes in several commands. To be done in a refactoring project. """ atm1, atm2 = self.struct.getAxisEndAtoms() if atm1 is not None: self.handleSphereRadius1 = max(1.005 * atm1.drawing_radius(), 1.005 * HANDLE_RADIUS_DEFAULT_VALUE) if atm2 is not None: self.handleSphereRadius2 = max(1.005 * atm2.drawing_radius(), 1.005 * HANDLE_RADIUS_DEFAULT_VALUE) def _modifyStructure(self, params): """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old structure and creates a new one using self._createStructure. This was needed for the structures like this (Dna, Nanotube etc) . . See more comments in the method. """ if self.currentStruct is None: print_compact_stack("bug: self.currentStruct doesn't exist"\ "exiting self._modifyStructure") return assert isinstance(self.currentStruct, self.win.assy.DnaSegment) self.dna = B_Dna_PAM3_Generator() duplexRise = self.currentStruct.getDuplexRise() basesPerTurn = self.currentStruct.getBasesPerTurn() numberOfBasePairsToAddOrRemove = self._determine_numberOfBasePairs_to_change( ) ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() if ladderEndAxisAtom is None: print_compact_stack("bug: unable to resize the DnaSegment"\ "%r, end axis ato not found"%self.currentStruct) return if numberOfBasePairsToAddOrRemove != 0: resizeEnd_final_position = self._get_resizeEnd_final_position( ladderEndAxisAtom, abs(numberOfBasePairsToAddOrRemove), duplexRise) self.dna.modify(self.currentStruct, ladderEndAxisAtom, numberOfBasePairsToAddOrRemove, basesPerTurn, duplexRise, ladderEndAxisAtom.posn(), resizeEnd_final_position) self.previousParams = params return def modifyStructure(self): """ Called when a resize handle is dragged to change the length of one or more DnaSegments (to be resized together) Called upon leftUp . To acheive this resizing, it loops through the DnaSegments to be resized and calls self._modifyStructure() for individual DnaSegments. Note that Client should call this public method and should never call the private method self._modifyStructure. @see: B{DnaSegment_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self._modifyStructure} @see: B{DnaSegmentList.updateAverageEndPoints()} """ if self.grabbedHandle is None: return for segment in self.getResizeSegmentList(): #TODO 2008-05-14: DnaSegmentList class relies on self.currentStruct. #See if there is a better way. Ok for now self.currentStruct = segment #self._modifyStructure needs 'param' argument. As of 2008-05-14 #it is not supported/needed for multiple dna segment resizing. #So just pass an empty tuple. params = () self._modifyStructure(params) #Reset the self.currentStruct after use self.currentStruct = None #Update the average end points of the self.struct so that resize handles #are placed at proper positions. self.struct.updateAverageEndPoints() self.win.assy.changed() self.win.win_update() def _get_resizeEnd_final_position(self, ladderEndAxisAtom, numberOfBases, duplexRise): """ Returns the final position of the resize end. Note that we can not use the grabbedHandle's currentPosition as the final position because resize handle is placed at an 'average' position. So we must compute the correct vector using the 'self.currentStruct' """ final_position = None other_axisEndAtom = self.struct.getOtherAxisEndAtom(ladderEndAxisAtom) if other_axisEndAtom is None: return None axis_vector = ladderEndAxisAtom.posn() - other_axisEndAtom.posn() segment_length_to_add = getDuplexLength('B-DNA', numberOfBases, duplexRise=duplexRise) signFactor = +1 ##if self.grabbedHandle is not None: ##vec = self.grabbedHandle.currentPosition - \ ## self.grabbedHandle.fixedEndOfStructure ##if dot(vec, axis_vector) < 0: ##signFactor = -1 ##else: ##signFactor = +1 axis_vector = axis_vector * signFactor final_position = ladderEndAxisAtom.posn() + \ norm(axis_vector)*segment_length_to_add return final_position def hasResizableStructure(self): """ """ if len(self.struct.getStructList()) == 0: why_not = 'Segment list is empty' isResizable = False return isResizable, why_not return _superclass.hasResizableStructure(self) def _getStructureType(self): """ Returns the type of the structure this command supports. This is used in isinstance test. @see: EditCommand._getStructureType() """ return DnaSegmentList def getDnaRibbonParams(self): """ Overrides superclass method. @TODO:[2008-05-14] This could SLOW things up! Its called multiple times from the graphics mode (for all the DnaSegments to be resized at once). There is no apparent solution to this, but we could try optimizing the following code. Also needs some refactoring. @see: MultipleDnaSegmentResize_GraphicsMode._drawHandles() @see: self._determine_numberOfBasePairs_to_change() """ #Optimization: First test a few basic things to see if the dna ribbon #should be drawn, before doing more computations -- Ninad 2008-05-14 params_when_adding_bases = None params_when_removing_bases = None if self.grabbedHandle is None: return None, None if self.grabbedHandle.origin is None: return None, None ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() if ladderEndAxisAtom is None: return None, None numberOfBasePairsToAddOrRemove = self._determine_numberOfBasePairs_to_change( ) if numberOfBasePairsToAddOrRemove == 0: return None, None direction_of_drag = norm(self.grabbedHandle.currentPosition - \ self.grabbedHandle.origin) #@TODO: BUG: DO NOT use self._get_resizeEnd_final_position to compute #the resizeEnd_final_position. It computes the segment length to add #using the number of base pairs to add ... and it uses #getDuplexLength method which in turn rounds off the length as an integer #multiple of duplexRise. Although this is what we want in self._modify() #the drawDnaRibbon draws the base pair only if step size is > 3.18! #so no basepair is drawn at exact val of 3.18. This is BUG, harder to #fix. for drawing dna ribbon, lets justcompute the resize end final #position using the following code. -- Ninad 2008-05-14 other_axisEndAtom = self.struct.getOtherAxisEndAtom(ladderEndAxisAtom) if other_axisEndAtom is None: return None, None axis_vector = ladderEndAxisAtom.posn() - other_axisEndAtom.posn() currentPosition = self.grabbedHandle.currentPosition changedLength = vlen(currentPosition - self.grabbedHandle.origin) #NOTE: (TODO) we call self._determine_numberOfBasePairs_to_change() at the #beginning of this method which checks various things such as #total distance moved by the handle etc to determine whether to draw #the ribbon. So those checks are not done here. If that call is removed #then we need to do those checks. if dot(self.grabbedHandle.direction, direction_of_drag) < 0: signFactor = -1.0 else: signFactor = 1.0 resizeEnd_final_position = ladderEndAxisAtom.posn() + \ norm(axis_vector)*changedLength*signFactor #If the segment is being shortened (determined by checking the #direction of drag) , no need to draw the rubberband line. if dot(self.grabbedHandle.direction, direction_of_drag) < 0: params_when_adding_bases = None params_when_removing_bases = (resizeEnd_final_position) return params_when_adding_bases, \ params_when_removing_bases basesPerTurn = self.struct.getBasesPerTurn() duplexRise = self.struct.getDuplexRise() ladder = ladderEndAxisAtom.molecule.ladder endBaseAtomList = ladder.get_endBaseAtoms_containing_atom( ladderEndAxisAtom) ribbon1_start_point = None ribbon2_start_point = None ribbon1_direction = None ribbon2_direction = None ribbon1Color = applegreen ribbon2Color = applegreen if endBaseAtomList and len(endBaseAtomList) > 2: strand_atom1 = endBaseAtomList[0] strand_atom2 = endBaseAtomList[2] if strand_atom1: ribbon1_start_point = strand_atom1.posn() for bond_direction, neighbor in strand_atom1.bond_directions_to_neighbors( ): if neighbor and neighbor.is_singlet(): ribbon1_direction = bond_direction break ribbon1Color = strand_atom1.molecule.color if not ribbon1Color: ribbon1Color = strand_atom1.element.color if strand_atom2: ribbon2_start_point = strand_atom2.posn() for bond_direction, neighbor in strand_atom2.bond_directions_to_neighbors( ): if neighbor and neighbor.is_singlet(): ribbon2_direction = bond_direction break ribbon2Color = strand_atom2.molecule.color if not ribbon2Color: ribbon2Color = strand_atom2.element.color params_when_adding_bases = (ladderEndAxisAtom.posn(), resizeEnd_final_position, basesPerTurn, duplexRise, ribbon1_start_point, ribbon2_start_point, ribbon1_direction, ribbon2_direction, ribbon1Color, ribbon2Color) params_when_removing_bases = None return params_when_adding_bases, params_when_removing_bases def _determine_numberOfBasePairs_to_change(self): """ Overrides superclass method @TODO: This is significantly different (and perhaps better) than the superclass method. See how to incorporate changes in this method in superclass. @see: self.getDnaRibbonParams() """ currentPosition = self.grabbedHandle.currentPosition fixedEndOfStructure = self.grabbedHandle.fixedEndOfStructure duplexRise = self.struct.getDuplexRise() changedLength = vlen(currentPosition - self.grabbedHandle.origin) direction_of_drag = norm(self.grabbedHandle.currentPosition - \ self.grabbedHandle.origin) #Even when the direction of drag is negative (i.e. the basepairs being #removed), make sure not to remove base pairs for very small movement #of the grabbed handle if changedLength < 0.2 * duplexRise: return 0 #This check quickly determines if the grabbed handle moved by a distance #more than the duplexRise and avoids further computations #This condition is applicable only when the direction of drag is #positive..i.e. bases bing added to the segment. if changedLength < duplexRise and \ dot(self.grabbedHandle.direction, direction_of_drag) > 0: return 0 #If the segment is being shortened (determined by checking the #direction of drag) numberOfBasesToAddOrRemove = \ getNumberOfBasePairsFromDuplexLength( 'B-DNA', changedLength, duplexRise = duplexRise) if dot(self.grabbedHandle.direction, direction_of_drag) < 0: numberOfBasesToAddOrRemove = -numberOfBasesToAddOrRemove if numberOfBasesToAddOrRemove > 0: #dna.modify will remove the first base pair it creates #(that basepair will only be used for proper alignment of the #duplex with the existing structure) So we need to compensate for #this basepair by adding 1 to the new number of base pairs. #UPDATE 2008-05-14: The following commented out code #i.e. "##numberOfBasesToAddOrRemove += 1" is not required in this #class , because the way we compute the number of base pairs to #be added is different than than how its done at the moment in the #superclass. In this method, we compute bases to be added from #the resize end and that computation INCLUDES the resize end. #so the number that it returns is already one more than the actual #bases to be added. so commenting out the following line # -- Ninad 2008-05-14 ##numberOfBasesToAddOrRemove += 1 ##print "*** numberOfBasesToAddOrRemove = ", numberOfBasesToAddOrRemove ##print "**** changedLength =", changedLength pass ###UPDATE 2008-06-26: for some reason, when the number of base pairs ###to be added (i.e. value of numberOfBasesToAddOrRemove) is 1 more ###than the actual number of base pairs to be added. So subtract 1 ###from this number. Cause not debugged. -- Ninad ##if numberOfBasesToAddOrRemove > 1: ##numberOfBasesToAddOrRemove -= 1 #UPDATE 2008-08-20: Note that DnaSegment_EditCommand.getCursorText() #does the job of removing the extra basepair from numberOfBasesToAddOrRemove #It(subtracting 1 basePair) is not done here as #self._modifyStructure() needs it without the subtraction. This is #prone to bugs and need to be cleaned up. -- Ninad return numberOfBasesToAddOrRemove
class DragBehavior_AlongCircle(DragBehavior): """ A drag behavior which moves the original hitpoint along a line, storing only its 1d-position-offset along the line's direction [noting that the hitpoint is not necessarily equal to the moved object's origin] [#doc better] """ highlightable = Arg(Anything) rotation_parameter_ref = Arg(StateRef, doc = "where anything proportional to angle is stored") #translation_parameter_ref = Arg(StateRef, #doc = "where translation is stored") origin = Arg(StateRef, doc = "handle base point") center = Arg(StateRef, doc = "center of the circle") axis = Arg(StateRef, doc = "circle axis") radiusVector = Arg(StateRef, doc = "radius of circle") locked_parameter = Option(tuple_Expr, None, doc = "locked parameter") range_for_rotation = Option(tuple_Expr, None, doc = "range limit of angle of rotation") range_for_translation = Option(tuple_Expr, None, doc = "range limit of translation") # provides transient state for saving a fixed coordsys to use throughout #a drag saved_coordsys = Instance( SavedCoordsys() ) # helper methods (these probably belong in a superclass): def current_event_mousepoint(self, *args, **kws): return self.saved_coordsys.current_event_mousepoint(*args, **kws) def current_event_mouseray(self): p0 = self.current_event_mousepoint(depth = 0.0) # nearest depth ###k p1 = self.current_event_mousepoint(depth = 1.0) # farthest depth ###k return Ray(p0, p1 - p0) # specific methods def _C__rotation(self): """ Compute self._rotation from the externally stored rotation paramater """ k = self.rotation_parameter_ref.value ##rotation = k*self.radiusVector #REVISE THIS from geometry.VQT import norm return self.origin#self.radiusVector + k*norm(self.radiusVector) def on_press(self): self.saved_coordsys.copy_from( self.highlightable) self.startpoint = self.current_event_mousepoint() #Formula needs to be revised. ##self.offset = self.startpoint - (ORIGIN + self._rotation) ##self.circularPath = self.constrain_to_circle + self.offset def on_drag(self): mouseray = self.current_event_mouseray() ##k = self.circularPath.closest_pt_params_to_ray(mouseray) k = None if k is not None: # store k, after range-limiting if self.range_for_rotation is not None: low, high = self.range_for_rotation if low is not None and k < low: k = low if high is not None and k > high: k = high self.rotation_parameter_ref.value = k ##e by analogy with DraggableObject, should we perhaps save this ##side effect until the end? return def on_release(self): pass
class DnaSegment_EditCommand(State_preMixin, EditCommand): """ Command to edit a DnaSegment object. To edit a segment, first enter BuildDna_EditCommand (accessed using Build> Dna) then, select an axis chunk of an existing DnaSegment within the DnaGroup you are editing. When you select the axis chunk, it enters DnaSegment_Editcommand and shows the property manager with its widgets showing the properties of selected segment. """ cmd = 'Dna Segment' sponsor_keyword = 'DNA' prefix = 'Segment ' # used for gensym cmdname = "DNA_SEGMENT" commandName = 'DNA_SEGMENT' featurename = 'Edit Dna Segment' command_should_resume_prevMode = True command_has_its_own_gui = True command_can_be_suspended = False # Generators for DNA, nanotubes and graphene have their MT name # generated (in GeneratorBaseClass) from the prefix. create_name_from_prefix = True call_makeMenus_for_each_event = True #Graphics Mode GraphicsMode_class = DnaSegment_GraphicsMode #This is set to BuildDna_EditCommand.flyoutToolbar (as of 2008-01-14, #it only uses flyoutToolbar = None handlePoint1 = State(Point, ORIGIN) handlePoint2 = State(Point, ORIGIN) #The minimum 'stopper'length used for resize handles #@see: self._update_resizeHandle_stopper_length for details. _resizeHandle_stopper_length = State(Width, -100000) rotationHandleBasePoint1 = State(Point, ORIGIN) rotationHandleBasePoint2 = State(Point, ORIGIN) #See self._update_resizeHandle_radius where this gets changed. #also see DnaSegment_ResizeHandle to see how its implemented. handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) #@TODO: modify the 'State params for rotation_distance rotation_distance1 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) rotation_distance2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) duplexRise = getDuplexRise('B-DNA') leftHandle = Instance( DnaSegment_ResizeHandle( command=_self, height_ref=call_Expr(ObjAttr_StateRef, _self, 'cylinderWidth'), origin=handlePoint1, fixedEndOfStructure=handlePoint2, direction=norm_Expr(handlePoint1 - handlePoint2), sphereRadius=handleSphereRadius1, discRadius=env.prefs[dnaSegmentResizeHandle_discRadius_prefs_key], discThickness=env. prefs[dnaSegmentResizeHandle_discThickness_prefs_key], range=(_resizeHandle_stopper_length, 10000))) rightHandle = Instance( DnaSegment_ResizeHandle( command=_self, height_ref=call_Expr(ObjAttr_StateRef, _self, 'cylinderWidth2'), origin=handlePoint2, fixedEndOfStructure=handlePoint1, direction=norm_Expr(handlePoint2 - handlePoint1), sphereRadius=handleSphereRadius2, discRadius=env.prefs[dnaSegmentResizeHandle_discRadius_prefs_key], discThickness=env. prefs[dnaSegmentResizeHandle_discThickness_prefs_key], range=(_resizeHandle_stopper_length, 10000))) rotationHandle1 = Instance( RotationHandle(command=_self, rotationDistanceRef=call_Expr(ObjAttr_StateRef, _self, 'rotation_distance1'), center=handlePoint1, axis=norm_Expr(handlePoint1 - handlePoint2), origin=rotationHandleBasePoint1, radiusVector=norm_Expr(rotationHandleBasePoint1 - handlePoint1))) rotationHandle2 = Instance( RotationHandle(command=_self, rotationDistanceRef=call_Expr(ObjAttr_StateRef, _self, 'rotation_distance2'), center=handlePoint2, axis=norm_Expr(handlePoint2 - handlePoint1), origin=rotationHandleBasePoint2, radiusVector=norm_Expr(rotationHandleBasePoint2 - handlePoint2))) def __init__(self, commandSequencer, struct=None): """ Constructor for DnaDuplex_EditCommand """ glpane = commandSequencer State_preMixin.__init__(self, glpane) EditCommand.__init__(self, commandSequencer) self.struct = struct #Graphics handles for editing the structure . self.handles = [] self.grabbedHandle = None def init_gui(self): """ Initialize gui. """ #Note that DnaSegment_EditCommand only act as an edit command for an #existing structure. The call to self.propMgr.show() is done only during #the call to self.editStructure ..i .e. only after self.struct is #updated. This is done because of the following reason: # - self.init_gui is called immediately after entering the command. # - self.init_gui in turn, initialized propMgr object and may also # show the property manager. The self.propMgr.show routine calls # an update widget method just before the show. This update method # updates the widgets based on the parameters from the existing # structure of the command (self.editCommand.struct) # Although, it checks whether this structure exists, the editCommand # could still have a self.struct attr from a previous run. (Note that # EditCommand API was written before the command sequencer API and # it has some loose ends like this. ) -- Ninad 2008-01-22 self.create_and_or_show_PM_if_wanted(showPropMgr=False) def model_changed(self): #This MAY HAVE BUG. WHEN -- #debug pref 'call model_changed only when needed' is ON #See related bug 2729 for details. #The following code that updates te handle positions and the strand #sequence fixes bugs like 2745 and updating the handle positions #updating handle positions in model_changed instead of in #self.graphicsMode._draw_handles() is also a minor optimization #This can be further optimized by debug pref #'call model_changed only when needed' but its NOT done because of an # issue menitoned in bug 2729 - Ninad 2008-04-07 EditCommand.model_changed(self) #This also calls the #propMgr.model_changed if self.grabbedHandle is not None: return #For Rattlesnake, PAM5 segment resizing is not supported. #@see: self.hasResizableStructure() if self.hasValidStructure(): isStructResizable, why_not = self.hasResizableStructure() if not isStructResizable: self.handles = [] return elif len(self.handles) == 0: self._updateHandleList() self.updateHandlePositions() self._update_previousParams_in_model_changed() def _update_previousParams_in_model_changed(self): #The following fixes bug 2802. The bug comment has details of what #it does. Copying some portion of it below-- #We have fixed similar problem for strand resizing, by updating the #self.previousParams attr in model_changed method (and also updating #the numberOfBasePairs spinbox in the PM. But here, user can even #change the number of basepairs from the PM. When he does that, #the model_changed is called and it resets the number of basepairs #spinbox value with the ones currently on the structure! Thereby #making it impossible to upate structure using spinbox. To fix this #we introduce a new parameter in propMgr.getParameters() which #reports the actual number of bases on the structure. #-- Ninad 2008-04-12 if self.previousParams is not None: new_numberOfBasePairs = self.struct.getNumberOfBasePairs() if new_numberOfBasePairs != self.previousParams[0]: self.propMgr.numberOfBasePairsSpinBox.setValue( new_numberOfBasePairs) self.previousParams = self.propMgr.getParameters() def editStructure(self, struct=None): EditCommand.editStructure(self, struct) if self.hasValidStructure(): self._updatePropMgrParams() #Store the previous parameters. Important to set it after you #set duplexRise and basesPerTurn attrs in the propMgr. #self.previousParams is used in self._previewStructure and #self._finalizeStructure to check if self.struct changed. self.previousParams = self._gatherParameters() #For Rattlesnake, we do not support resizing of PAM5 model. #So don't append the exprs handles to the handle list (and thus #don't draw those handles. See self.model_changed() isStructResizable, why_not = self.hasResizableStructure() if not isStructResizable: self.handles = [] else: self._updateHandleList() self.updateHandlePositions() def _updatePropMgrParams(self): """ Subclasses may override this method. Update some property manager parameters with the parameters of self.struct (which is being edited) @see: self.editStructure() """ #Format in which params need to be provided to the Property manager #(i.e. in propMgr.setParameters(): #numberOfBasePairs, #dnaForm, #dnaModel, #basesPerTurn, #duplexRise, #endPoint1, #endPoint2 basesPerTurn, duplexRise = self.struct.getProps() endPoint1, endPoint2 = self.struct.getAxisEndPoints() numberOfBasePairs = self.struct.getNumberOfBasePairs() color = self.struct.getColor() params_for_propMgr = (numberOfBasePairs, None, None, basesPerTurn, duplexRise, endPoint1, endPoint2, color) #TODO 2008-03-25: better to get all parameters from self.struct and #set it in propMgr? This will mostly work except that reverse is #not true. i.e. we can not specify same set of params for #self.struct.setProps ...because endPoint1 and endPoint2 are derived. #by the structure when needed. Commenting out following line of code #UPDATE 2008-05-06 Fixes a bug due to which the parameters in propMGr #of DnaSegment_EditCommand are not same as the original structure #(e.g. bases per turn and duplexrise) self.propMgr.setParameters(params_for_propMgr) def keep_empty_group(self, group): """ Returns True if the empty group should not be automatically deleted. otherwise returns False. The default implementation always returns False. Subclasses should override this method if it needs to keep the empty group for some reasons. Note that this method will only get called when a group has a class constant autdelete_when_empty set to True. (and as of 2008-03-06, it is proposed that dna_updater calls this method when needed. @see: Command.keep_empty_group() which is overridden here. @see: BreakStrands_Command.keep_empty_group @see: Group.autodelete_when_empty.. a class constant used by the dna_updater (the dna updater then decides whether to call this method to see which empty groups need to be deleted) """ bool_keep = EditCommand.keep_empty_group(self, group) if not bool_keep: if self.hasValidStructure(): if group is self.struct: bool_keep = True elif group is self.struct.parent_node_of_class( self.assy.DnaGroup): bool_keep = True #If this command doesn't have a valid structure, as a fall back, #lets instruct it to keep ALL the DnaGroup objects even when empty #Reason? ..see explanation in BreakStrands_Command.keep_empty_group elif isinstance(group, self.assy.DnaGroup): bool_keep = True return bool_keep def hasResizableStructure(self): """ For Rattlesnake release, we dont support segment resizing for PAM5 models. If the structure is not resizable, the handles won't be drawn @see:self.model_changed() @see:DnaSegment_PropertyManager.model_changed() @see: self.editStructure() @see: DnaSegment.is_PAM3_DnaSegment() """ #Note: This method fixes bugs similar to bug 2812 but the changes #didn't made it to Rattlesnake rc2 -- Ninad 2008-04-16 isResizable = True why_not = '' if not self.hasValidStructure(): isResizable = False why_not = 'It is invalid.' return isResizable, why_not isResizable = self.struct.is_PAM3_DnaSegment() if not isResizable: why_not = 'It needs to be converted to PAM3 model' return isResizable, why_not endAtom1, endAtom2 = self.struct.getAxisEndAtoms() if endAtom1 is None or endAtom2 is None: isResizable = False why_not = "Unable to determine one or both end atoms of the segment" return isResizable, why_not if endAtom1 is endAtom2: isResizable = False why_not = "Resizing a segment with single atom is unsupported" return isResizable, why_not return isResizable, why_not def hasValidStructure(self): """ Tells the caller if this edit command has a valid structure. Overrides EditCommand.hasValidStructure() """ #(By Bruce 2008-02-13) isValid = EditCommand.hasValidStructure(self) if not isValid: return isValid # would like to check here whether it's empty of axis chunks; # instead, this will do for now (probably too slow, though): p1, p2 = self.struct.getAxisEndPoints() return (p1 is not None) def _getStructureType(self): """ Subclasses override this method to define their own structure type. Returns the type of the structure this editCommand supports. This is used in isinstance test. @see: EditCommand._getStructureType() (overridden here) """ return self.win.assy.DnaSegment def _updateHandleList(self): """ Updates the list of handles (self.handles) @see: self.editStructure @see: DnaSegment_GraphicsMode._drawHandles() """ # note: if handlePoint1 and/or handlePoint2 can change more often than this # runs, we'll need to rerun the two assignments above whenever they # change and before the handle is drawn. An easy way would be to rerun # these assignments in the draw method of our GM. [bruce 080128] self.handles = [] # guess, but seems like a good idea [bruce 080128] self.handles.append(self.leftHandle) self.handles.append(self.rightHandle) if DEBUG_ROTATION_HANDLES: self.handles.append(self.rotationHandle1) self.handles.append(self.rotationHandle2) def updateHandlePositions(self): """ Update handle positions and also update the resize handle radii and their 'stopper' lengths. @see: self._update_resizeHandle_radius() @see: self._update_resizeHandle_stopper_length() @see: DnaSegment_GraphicsMode._drawHandles() """ if len(self.handles) == 0: #No handles are appended to self.handles list. #@See self.model_changed() and self._updateHandleList() return #TODO: Call this method less often by implementing model_changed #see bug 2729 for a planned optimization self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE self._update_resizeHandle_radius() handlePoint1, handlePoint2 = self.struct.getAxisEndPoints() if handlePoint1 is not None and handlePoint2 is not None: # (that condition is bugfix for deleted axis segment, bruce 080213) self.handlePoint1, self.handlePoint2 = handlePoint1, handlePoint2 #Update the 'stopper' length where the resize handle being dragged #should stop. See self._update_resizeHandle_stopper_length() #for more details self._update_resizeHandle_stopper_length() if DEBUG_ROTATION_HANDLES: self.rotation_distance1 = CYLINDER_WIDTH_DEFAULT_VALUE self.rotation_distance2 = CYLINDER_WIDTH_DEFAULT_VALUE #Following computes the base points for rotation handles. #to be revised -- Ninad 2008-02-13 unitVectorAlongAxis = norm(self.handlePoint1 - self.handlePoint2) v = cross(self.glpane.lineOfSight, unitVectorAlongAxis) self.rotationHandleBasePoint1 = self.handlePoint1 + norm( v) * 4.0 self.rotationHandleBasePoint2 = self.handlePoint2 + norm( v) * 4.0 def _update_resizeHandle_radius(self): """ Finds out the sphere radius to use for the resize handles, based on atom /chunk or glpane display (whichever decides the display of the end atoms. @see: self.updateHandlePositions() @see: B{Atom.drawing_radius()} """ atm1, atm2 = self.struct.getAxisEndAtoms() if atm1 is not None: self.handleSphereRadius1 = max(1.25 * atm1.drawing_radius(), 1.25 * HANDLE_RADIUS_DEFAULT_VALUE) if atm2 is not None: self.handleSphereRadius2 = max(1.25 * atm2.drawing_radius(), 1.25 * HANDLE_RADIUS_DEFAULT_VALUE) def _update_resizeHandle_stopper_length(self): """ Update the limiting length at which the resize handle being dragged should 'stop' without proceeding further in the drag direction. The segment resize handle stops when you are dragging it towards the other resizeend and the distance between the two ends reaches two duplexes. The self._resizeHandle_stopper_length computed in this method is used as a lower limit of the 'range' option provided in declaration of resize handle objects (see class definition for the details) @see: self.updateHandlePositions() """ total_length = vlen(self.handlePoint1 - self.handlePoint2) duplexRise = self.struct.getDuplexRise() #Length of the duplex for 2 base pairs two_bases_length = getDuplexLength('B-DNA', 2, duplexRise=duplexRise) self._resizeHandle_stopper_length = -total_length + two_bases_length def _createPropMgrObject(self): """ Creates a property manager object (that defines UI things) for this editCommand. """ assert not self.propMgr propMgr = self.win.createDnaSegmentPropMgr_if_needed(self) return propMgr def _gatherParameters(self): """ Return the parameters from the property manager UI. @return: All the parameters (get those from the property manager): - numberOfBases - dnaForm - basesPerTurn - endPoint1 - endPoint2 @rtype: tuple """ return self.propMgr.getParameters() def _createStructure(self): """ Creates and returns the structure (in this case a L{Group} object that contains the DNA strand and axis chunks. @return : group containing that contains the DNA strand and axis chunks. @rtype: L{Group} @note: This needs to return a DNA object once that model is implemented """ params = self._gatherParameters() # No error checking in build_struct, do all your error # checking in gather_parameters number_of_basePairs_from_struct,\ numberOfBases, \ dnaForm, \ dnaModel, \ basesPerTurn, \ duplexRise, \ endPoint1, \ endPoint2, \ color_junk = params #Note: color_junk is not used. Ideally it should do struct.setColor(color) #but the color combobox in the PM directly sets the color of the #structure to the specified one when the current index in the combobx #changes #If user enters the number of basepairs and hits preview i.e. endPoint1 #and endPoint2 are not entered by the user and thus have default value #of V(0, 0, 0), then enter the endPoint1 as V(0, 0, 0) and compute #endPoint2 using the duplex length. #Do not use '==' equality check on vectors! its a bug. Use same_vals # or Veq instead. if Veq(endPoint1, endPoint2) and Veq(endPoint1, V(0, 0, 0)): endPoint2 = endPoint1 + \ self.win.glpane.right*getDuplexLength('B-DNA', numberOfBases) if numberOfBases < 1: msg = redmsg("Cannot preview/insert a DNA duplex with 0 bases.") self.propMgr.updateMessage(msg) self.dna = None # Fixes bug 2530. Mark 2007-09-02 return None if dnaForm == 'B-DNA': if dnaModel == 'PAM3': dna = B_Dna_PAM3() elif dnaModel == 'PAM5': dna = B_Dna_PAM5() else: print "bug: unknown dnaModel type: ", dnaModel else: raise PluginBug("Unsupported DNA Form: " + dnaForm) self.dna = dna # needed for done msg # self.name needed for done message if self.create_name_from_prefix: # create a new name name = self.name = gensym(self.prefix, self.win.assy) # (in _build_struct) self._gensym_data_for_reusing_name = (self.prefix, name) else: # use externally created name self._gensym_data_for_reusing_name = None # (can't reuse name in this case -- not sure what prefix it was # made with) name = self.name # Create the model tree group node. # Make sure that the 'topnode' of this part is a Group (under which the # DNa group will be placed), if the topnode is not a group, make it a # a 'Group' (applicable to Clipboard parts).See part.py # --Part.ensure_toplevel_group method. This is an important line # and it fixes bug 2585 self.win.assy.part.ensure_toplevel_group() dnaSegment = DnaSegment(self.name, self.win.assy, self.win.assy.part.topnode, editCommand=self) try: # Make the DNA duplex. <dnaGroup> will contain three chunks: # - Strand1 # - Strand2 # - Axis dna.make(dnaSegment, numberOfBases, basesPerTurn, duplexRise, endPoint1, endPoint2) #set some properties such as duplexRise and number of bases per turn #This information will be stored on the DnaSegment object so that #it can be retrieved while editing this object. #This works with or without dna_updater. Now the question is #should these props be assigned to the DnaSegment in #dnaDuplex.make() itself ? This needs to be answered while modifying #make() method to fit in the dna data model. --Ninad 2008-03-05 #WARNING 2008-03-05: Since self._modifyStructure calls #self._createStructure() #If in the near future, we actually permit modifying a #structure (such as dna) without actually recreating the whole #structre, then the following properties must be set in #self._modifyStructure as well. Needs more thought. props = (duplexRise, basesPerTurn) dnaSegment.setProps(props) return dnaSegment except (PluginBug, UserError): # Why do we need UserError here? Mark 2007-08-28 dnaSegment.kill() raise PluginBug( "Internal error while trying to create DNA duplex.") def _modifyStructure(self, params): """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old structure and creates a new one using self._createStructure. This was needed for the structures like this (Dna, Nanotube etc) . . See more comments in the method. """ assert self.struct self.dna = B_Dna_PAM3() number_of_basePairs_from_struct,\ numberOfBases, \ dnaForm, \ dnaModel, \ basesPerTurn, \ duplexRise, \ endPoint1, \ endPoint2 , \ color = params #Delete unused parameters. del endPoint1 del endPoint2 del number_of_basePairs_from_struct numberOfBasePairsToAddOrRemove = self._determine_numberOfBasePairs_to_change( ) ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() if numberOfBasePairsToAddOrRemove != 0: resizeEnd_final_position = self._get_resizeEnd_final_position( ladderEndAxisAtom, abs(numberOfBasePairsToAddOrRemove), duplexRise) self.dna.modify(self.struct, ladderEndAxisAtom, numberOfBasePairsToAddOrRemove, basesPerTurn, duplexRise, ladderEndAxisAtom.posn(), resizeEnd_final_position) #Find new end points of structure parameters after modification #and set these values in the propMgr. new_end1, new_end2 = self.struct.getAxisEndPoints() params_to_set_in_propMgr = (numberOfBases, dnaForm, dnaModel, basesPerTurn, duplexRise, new_end1, new_end2, color) #TODO: Need to set these params in the PM #and then self.previousParams = params_to_set_in_propMgr self.previousParams = params return def _get_resizeEnd_final_position(self, ladderEndAxisAtom, numberOfBases, duplexRise): final_position = None if self.grabbedHandle: final_position = self.grabbedHandle.currentPosition else: other_axisEndAtom = self.struct.getOtherAxisEndAtom( ladderEndAxisAtom) axis_vector = ladderEndAxisAtom.posn() - other_axisEndAtom.posn() segment_length_to_add = getDuplexLength('B-DNA', numberOfBases, duplexRise=duplexRise) final_position = ladderEndAxisAtom.posn( ) + norm(axis_vector) * segment_length_to_add return final_position def getStructureName(self): """ Returns the name string of self.struct if there is a valid structure. Otherwise returns None. This information is used by the name edit field of this command's PM when we call self.propMgr.show() @see: DnaSegment_PropertyManager.show() @see: self.setStructureName """ if self.hasValidStructure(): return self.struct.name else: return None def setStructureName(self, name): """ Sets the name of self.struct to param <name> (if there is a valid structure. The PM of this command callss this method while closing itself @param name: name of the structure to be set. @type name: string @see: DnaSegment_PropertyManager.close() @see: self.getStructureName() """ #@BUG: We call this method in self.propMgr.close(). But propMgr.close() #is called even when the command is 'cancelled'. That means the #structure will get changed even when user hits cancel button or #exits the command by clicking on empty space. #This should really be done in self._finalizeStructure but that #method doesn't get called when you click on empty space to exit #the command. See DnaSegment_GraphicsMode.leftUp for a detailed #comment. if self.hasValidStructure(): self.struct.name = name def getCursorText(self): """ This is used as a callback method in DnaLine mode @see: DnaLineMode.setParams, DnaLineMode_GM.Draw """ #@TODO: Refactor this. Similar code exists in #DnaStrand_EditCommand.getCursorText() -- Ninad 2008-04-12 if self.grabbedHandle is None: return currentPosition = self.grabbedHandle.currentPosition fixedEndOfStructure = self.grabbedHandle.fixedEndOfStructure duplexRise = self.struct.getDuplexRise() ############# raw_numberOfBasePairsToAddOrRemove = self._determine_numberOfBasePairs_to_change( ) #Following fixes bugs like 2904 and 2906 #Note that we are using numberOfBasePairsToAddOrRemove in self._modifyStructure() #if self._determine_numberOfBasePairs_to_change() returns the number of basepairs #to add, it returns 1 more than the actual number of basepairs. Because #while creating the dna, it removes the first base pair of the newly created #dna. So, for cursor text and for PM spinbox, we should make adjustments to the #raw_numberOfBasePairsToAddOrRemove so that it reflects the correct value #in the spinbox and in the PM if raw_numberOfBasePairsToAddOrRemove > 1: numberOfBasePairsToAddOrRemove = raw_numberOfBasePairsToAddOrRemove - 1 else: numberOfBasePairsToAddOrRemove = raw_numberOfBasePairsToAddOrRemove ############## current_numberOfBasePairs = self.struct.getNumberOfBasePairs() numberOfBasePairs = current_numberOfBasePairs + numberOfBasePairsToAddOrRemove #@TODO: The following updates the PM as the cursor moves. #Need to rename this method so that you that it also does more things #than just to return a textString -- Ninad 2007-12-20 self.propMgr.numberOfBasePairsSpinBox.setValue(numberOfBasePairs) #Note: for Rattlesnake rc2, the text color is green when bases are added #, red when subtracted black when no change. But this implementation is #changed based on Mark's user experience. The text is now always shown #in black color. -- Ninad 2008-04-17 textColor = black text = "" if not env.prefs[ dnaSegmentEditCommand_showCursorTextCheckBox_prefs_key]: return '', black #@@TODO: refactor. #this duplex length canculation fixes bug 2906 duplexLength = getDuplexLength('B-DNA', numberOfBasePairs, duplexRise=duplexRise) #Cursor text strings -- duplexLengthString = str(round(duplexLength, 3)) numberOfBasePairsString = self._getCursorText_numberOfBasePairs( numberOfBasePairs) duplexLengthString = self._getCursorText_length(duplexLength) changedBasePairsString = self._getCursorText_changedBasePairs( numberOfBasePairs) #Add commas (to be refactored) commaString = ", " text = numberOfBasePairsString if text and changedBasePairsString: text += " " # commaString not needed here. Mark 2008-07-03 text += changedBasePairsString if text and duplexLengthString: text += commaString text += duplexLengthString return (text, textColor) def _getCursorText_numberOfBasePairs(self, numberOfBasePairs): """ Return the cursor textstring that gives information about the number of basepairs if the corresponding prefs_key returns True. """ numberOfBasePairsString = '' if env.prefs[ dnaSegmentEditCommand_cursorTextCheckBox_numberOfBasePairs_prefs_key]: numberOfBasePairsString = "%db" % numberOfBasePairs return numberOfBasePairsString def _getCursorText_length(self, duplexLength): """ """ duplexLengthString = '' if env.prefs[ dnaSegmentEditCommand_cursorTextCheckBox_length_prefs_key]: lengthUnitString = 'A' #change the unit of length to nanometers if the length is > 10A #fixes part of bug 2856 if duplexLength > 10.0: lengthUnitString = 'nm' duplexLength = duplexLength * 0.1 duplexLengthString = "%5.3f%s" % (duplexLength, lengthUnitString) return duplexLengthString def _getCursorText_changedBasePairs(self, numberOfBasePairs): """ """ changedBasePairsString = '' if env.prefs[ dnaSegmentEditCommand_cursorTextCheckBox_changedBasePairs_prefs_key]: original_numberOfBasePairs = self.struct.getNumberOfBasePairs() changed_basePairs = numberOfBasePairs - original_numberOfBasePairs if changed_basePairs > 0: changedBasePairsString = "(" + "+" + str( changed_basePairs) + ")" else: changedBasePairsString = "(" + str(changed_basePairs) + ")" return changedBasePairsString def getDnaRibbonParams(self): """ Returns parameters for drawing the dna ribbon. If the dna rubberband line should NOT be drawn (example when you are removing basepairs from the segment So the caller should check if the method return value is not None. @see: DnaSegment_GraphicsMode._draw_handles() """ if self.grabbedHandle is None: return None if self.grabbedHandle.origin is None: return None direction_of_drag = norm(self.grabbedHandle.currentPosition - \ self.grabbedHandle.origin) #If the segment is being shortened (determined by checking the #direction of drag) , no need to draw the rubberband line. if dot(self.grabbedHandle.direction, direction_of_drag) < 0: return None basesPerTurn = self.struct.getBasesPerTurn() duplexRise = self.struct.getDuplexRise() ladderEndAxisAtom = self.get_axisEndAtom_at_resize_end() ladder = ladderEndAxisAtom.molecule.ladder endBaseAtomList = ladder.get_endBaseAtoms_containing_atom( ladderEndAxisAtom) ribbon1_start_point = None ribbon2_start_point = None ribbon1_direction = None ribbon2_direction = None ribbon1Color = applegreen ribbon2Color = applegreen if endBaseAtomList and len(endBaseAtomList) > 2: strand_atom1 = endBaseAtomList[0] strand_atom2 = endBaseAtomList[2] if strand_atom1: ribbon1_start_point = strand_atom1.posn() for bond_direction, neighbor in strand_atom1.bond_directions_to_neighbors( ): if neighbor and neighbor.is_singlet(): ribbon1_direction = bond_direction break ribbon1Color = strand_atom1.molecule.color if not ribbon1Color: ribbon1Color = strand_atom1.element.color if strand_atom2: ribbon2_start_point = strand_atom2.posn() for bond_direction, neighbor in strand_atom2.bond_directions_to_neighbors( ): if neighbor and neighbor.is_singlet(): ribbon2_direction = bond_direction break ribbon2Color = strand_atom2.molecule.color if not ribbon2Color: ribbon2Color = strand_atom2.element.color return (self.grabbedHandle.origin, self.grabbedHandle.currentPosition, basesPerTurn, duplexRise, ribbon1_start_point, ribbon2_start_point, ribbon1_direction, ribbon2_direction, ribbon1Color, ribbon2Color) def modifyStructure(self): """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{DnaSegment_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} As of 2008-02-01 it recreates the structure @see: a note in self._createStructure() about use of dnaSegment.setProps """ #TODO: need to cleanup this and may be use use something like #self.previousParams = params in the end -- 2008-03-24 (midnight) if self.grabbedHandle is None: return ##self.propMgr.setParameters(params_to_set_in_propMgr) #TODO: Important note: How does NE1 know that structure is modified? #Because number of base pairs parameter in the PropMgr changes as you #drag the handle . This is done in self.getCursorText() ... not the #right place to do it. OR that method needs to be renamed to reflect #this as suggested in that method -- Ninad 2008-03-25 self.preview_or_finalize_structure(previewing=True) ##self.previousParams = params_to_set_in_propMgr self.glpane.gl_update() def get_axisEndAtom_at_resize_end(self): ladderEndAxisAtom = None if self.grabbedHandle is not None: ladderEndAxisAtom = self.struct.getAxisEndAtomAtPosition( self.grabbedHandle.origin) else: endAtom1, endAtom2 = self.struct.getAxisEndAtoms() ladderEndAxisAtom = endAtom2 return ladderEndAxisAtom def _determine_numberOfBasePairs_to_change(self): """ """ duplexRise = self.struct.getDuplexRise() numberOfBasesToAddOrRemove = 0 #Following helps fixing bugs like 2904 and 2906 see also self.getCursorText() #and TODO items in that method. Also note that the grabbed handle case #is similar to the one in MultipleDnaSegmentResize_EditCommand. #needs refactoring and overall cleanup. if self.grabbedHandle is not None: currentPosition = self.grabbedHandle.currentPosition fixedEndOfStructure = self.grabbedHandle.fixedEndOfStructure changedLength = vlen(currentPosition - self.grabbedHandle.origin) direction_of_drag = norm(self.grabbedHandle.currentPosition - \ self.grabbedHandle.origin) #Even when the direction of drag is negative (i.e. the basepairs being #removed), make sure not to remove base pairs for very small movement #of the grabbed handle if changedLength < 0.2 * duplexRise: return 0 #This check quickly determines if the grabbed handle moved by a distance #more than the duplexRise and avoids further computations #This condition is applicable only when the direction of drag is #positive..i.e. bases bing added to the segment. if changedLength < duplexRise and \ dot(self.grabbedHandle.direction, direction_of_drag) > 0: return 0 #If the segment is being shortened (determined by checking the #direction of drag) numberOfBasesToAddOrRemove = \ getNumberOfBasePairsFromDuplexLength( 'B-DNA', changedLength, duplexRise = duplexRise) if dot(self.grabbedHandle.direction, direction_of_drag) < 0: numberOfBasesToAddOrRemove = -numberOfBasesToAddOrRemove if numberOfBasesToAddOrRemove > 0: #dna.modify will remove the first base pair it creates #(that basepair will only be used for proper alignment of the #duplex with the existing structure) So we need to compensate for #this basepair by adding 1 to the new number of base pairs. #UPDATE 2008-05-14: The following commented out code #i.e. "##numberOfBasesToAddOrRemove += 1" is not required in this #class , because the way we compute the number of base pairs to #be added is different than than how its done at the moment in the #superclass. In this method, we compute bases to be added from #the resize end and that computation INCLUDES the resize end. #so the number that it returns is already one more than the actual #bases to be added. so commenting out the following line # -- Ninad 2008-05-14 ##numberOfBasesToAddOrRemove += 1 pass else: #The Property manager will be showing the current number #of base pairs (w. May be we can use that number directly here? #The following is safer to do so lets just recompute the #number of base pairs. (if it turns out to be slow, we will consider #using the already computed calue from the property manager new_numberOfBasePairs = self.propMgr.numberOfBasePairsSpinBox.value( ) endPoint1, endPoint2 = self.struct.getAxisEndPoints() if endPoint1 is None or endPoint2 is None: return 0 original_duplex_length = vlen(endPoint1 - endPoint2) original_numberOfBasePairs = self.struct.getNumberOfBasePairs() numberOfBasesToAddOrRemove = new_numberOfBasePairs - original_numberOfBasePairs if numberOfBasesToAddOrRemove > 0: #dna.modify will remove the first base pair it creates #(that basepair will only be used for proper alignment of the #duplex with the existing structure) So we need to compensate for #this basepair by adding 1 to the new number of base pairs. numberOfBasesToAddOrRemove += 1 return numberOfBasesToAddOrRemove def makeMenus(self): """ Create context menu for this command. (Build Dna mode) """ if not hasattr(self, 'graphicsMode'): return selobj = self.glpane.selobj if selobj is None: return self.Menu_spec = [] highlightedChunk = None if isinstance(selobj, Chunk): highlightedChunk = selobj if isinstance(selobj, Atom): highlightedChunk = selobj.molecule elif isinstance(selobj, Bond): chunk1 = selobj.atom1.molecule chunk2 = selobj.atom2.molecule if chunk1 is chunk2 and chunk1 is not None: highlightedChunk = chunk1 if highlightedChunk is None: return if self.hasValidStructure(): dnaGroup = self.struct.parent_node_of_class(self.assy.DnaGroup) if dnaGroup is None: return #following should be self.struct.getDnaGroup or self.struct.getDnaGroup #need to formalize method name and then make change. if not dnaGroup is highlightedChunk.parent_node_of_class( self.assy.DnaGroup): item = ("Edit unavailable: Member of a different DnaGroup", noop, 'disabled') self.Menu_spec.append(item) return highlightedChunk.make_glpane_context_menu_items(self.Menu_spec, command=self)
class main_ui_layout(DelegatingInstanceOrExpr): #e rename? is it not only the ui, but the entire app? (selection, files, etc) #e merge in the App obj from test.py, and the _recent_tests system in some form? # args (none yet) # internal state - permanent ###e note: when we reload and remake this instance, we'd prefer it if the world state stayed unchanged (as i presume it does) # but if the default_tool instance and toolstack state got remade. The lack of the latter has been confusing me # since changes to ui code aren't working. I think this is a difference between a ui and operations layer (should change) # vs model data layer (should not change even tho the op methods on it can change). So when I can put these things into layers # (not only State, but even Instance or attrs within them) and make those sensitive to reload, that will help. # In the meantime -- if I could kluge Instance and State to take an option to control this # (like index = exprs_globals.reload_counter) # it might help.... #####TRYIT SOMETIME, and BE CAREFUL UNTIL I DO. world = Instance(World()) default_tool = Instance(DefaultToolRun()) # internal state - varying toolstack = State(list_Expr, [ default_tool ]) # always has at least one tool on it; a stack of Instances not exprs # maybe the better term for this is something like command & subcommand current_tool = toolstack[ -1] # last tool on the stack is current; exiting it will pop the stack (Instance not expr) ##e (add a type-assertion (as opposed to type-coercion) primitive, so I can say "this is an Instance" in the code?) # NOTE: this is not strictly speaking a tool, but ONE RUN of a tool. That might be important enough to rename it for, # to ToolRun or maybe ActiveTool or RunningTool or ToolInUse or ToolBeingUsed... # [but note, obj might remain around on history or in Undo stack, even when no longer being used], # since we also have to deal with Tools in the sense of Tool Run Producers, eg toolbuttons. ###e # parts of the appearance registry = find_or_make_global_command_registry( ) ## None ###STUB, will fail -- ## AttributeError: 'NoneType' object has no attribute 'command_for_toolname' toolstack_ref = None ###STUB toolbar = Instance( MainToolbar(registry, ["Features", "Build", "Sketch"], toolstack_ref)) #e arg order? ###e args/opts for what tools to show -- maybe their cmdnames & it loads them from elsewhere #e add row of tool buttons, and flyout toolbar; use ChoiceRow?? the things should probably look pressed... # they might need cmenus (find out what the deal is with the cmenus i see in the ui mockup - related to flyouts? # yes, it's like this: main tools have cmenus with subtools, and if you pick one, main tool and its subtool both look pressed # I'll need new specialized controls.py classes for these; new super Control for all kinds of controls?? (not sure why...) propmgr = SimpleColumn( TextRect( "(property manager)"), #e possibly to become a tab control tab DebugPrintAttrs( current_tool.property_manager ) # must be None if we don't want one visible; otherwise an Instance ###BUG: DebugPrintAttrs shows that it's a spacer -- I guess IorE turns None into one when it instantiates? Make it a false one?? ) mt = SimpleColumn( TextRect( "(model tree)" ), #e possibly to become a tab control tab, but only when we're in the left channel MT_try2(world) #e rename to "Feature Manager" ?? ##e soon, MT should be not on whole world but on model or cur. part, a specific obj in the world ) graphics_area = _self.world ##e ditto for what we show here, except it might not be the exact same object, and it will really be shown in a way # that depends on both the current display style and the current tool (command & subcommand) graphics_area_topright_buttons = current_tool.graphics_area_topright_buttons # overall appearance delegate = Overlay( # stuff in the corners -- note, these don't use the corner constants for standalone tests like PM_CORNER DrawInCorner(corner=UPPER_LEFT)( SimpleColumn( toolbar, #e add tab control SimpleRow( If( current_tool.property_manager, Top(propmgr), None ), #k None?? prob ok now, see demo_MT comment 070302 ###k Top(mt) ), #e actually we'd then put a splitter & glpane-like-thing... #e anything just below the propmgr? )), DrawInCorner(corner=UPPER_RIGHT) ( ##e of graphics area, not entire screen... graphics_area_topright_buttons ### WRONG, these should go under the main toolbar area on the right # (but we don't yet have any 2dwidgets which expand to fill the available space, except DrawInCorner of entire screen) # (this won't matter once the toolbar is done entirely in Qt, so we don't need to correct it for now) ), #e other corners? "... an area (view) on the right side # of the main window for accessing the part library, on-line documentation, etc" # the main graphics area #e [this too ought to go under the toolbar and to the right of the propmgr, but that can wait until they're fully in Qt] graphics_area) pass
class SimpleDragBehavior(DragBehavior): #works circa 070317; revised 070318 """ the simplest kind of DragBehavior -- translate the passed state just like the mouse moves (screen-parallel) [#doc] """ ##e rename to indicate what it does -- translate, 3d, screen-parallel # (no limits, no grid, no constraint -- could add opts for those #e) # note: for now, it probably doesn't matter if we are remade per drag event, or live through many of them -- # we store state during a drag (and left over afterwards) but reset it all when the next one starts -- # ASSUMING we actually get an on_press event for it -- we don't detect the error of not getting that! # OTOH, even if we're newly made per drag, we don't detect or tolerate a missing initial on_press. ###BUG I guess # args: # a stateref to the translation state we should modify, # and something to ask about the drag event (for now, highlightable, but later, a DragEvent object) # (best arg order unclear; it may turn out that the DragEvent to ask is our delegate in the future -- so let it come first) highlightable = Arg(Anything) # for current_event_mousepoint (coordsys) -- will this always be needed? at least a DragEvent will be! translation_ref = Arg( StateRef, doc= "ref to translation state, e.g. call_Expr( LvalueFromObjAndAttr, some_obj, 'translation')" ) # state: saved_coordsys = Instance( SavedCoordsys() ) # provides transient state for saving a fixed coordsys to use throughout a drag def current_event_mousepoint( self, *args, **kws ): #e zap this and inline it, for clarity? or move it into DragBehavior superclass?? return self.saved_coordsys.current_event_mousepoint(*args, **kws) # note: the following methods are heavily modified from the ones in DraggableObject. # [They are the methods of some interface, informal so far, # but I don't know if it's exactly the one I've elsewhere called Draggable.] def on_press(self): self.saved_coordsys.copy_from( self.highlightable ) # needed, since self.highlightable's coordsys changes during the drag! point = self.current_event_mousepoint( ) # the touched point on the visible object (hitpoint) self.oldpoint = self.startpoint = point def on_drag(self): # Note (for any on_drag method -- really about the interface it's in [###e refile]): # we can assume this is a "real drag", # not one which is too short to count (and is therefore treated as a click instead), # since the caller is responsible for not calling on_drag until it decides this is a real drag. oldpoint = self.oldpoint # was saved by prior on_drag or by on_press point = self.current_event_mousepoint(plane=self.startpoint) self.translation_ref.value = self.translation_ref.value + (point - oldpoint) self.oldpoint = point def on_release(self): pass pass # end of class SimpleDragBehavior
class World(InstanceOrExpr ): #070205 revised, public nodelist -> private _nodeset """maintains the set of objects in the model; provides general operations on them """ #k is this 070401 change ok: ModelObject -> InstanceOrExpr? At least it doesn't seem to have caused trouble. ###e Q: Is it ok for this to inherit _CoordsysHolder and have .draw save the coords? Or to own one of those to hold them? # The goal is for some external thing to be able to copy the coords we will draw a newly made thing under. # that's partly misguided given that we might in theory draw things in more than one place # and/or in a different place than in the prior frame. The external thing that wants to do this # is self._newobj.copy_saved_coordsys_from( self.world) in class PalletteWell.on_press as of 070401. # It wants to use the right coordsys to turn a mouseray into that obj's initial position in the World. Hmm. # A: In principle, if we're drawing the world twice (eg in stereo), we'd better be able to tell from a mouseray # used to put something into it, which world-instance we're putting it into! and each of those needs to know # where mouse events would drop things (based on where it was last drawn -- no ambiguity there). # So it's correct for something to save that and reveal it -- it just needs to be a world-drawn-instance # which would somehow show up (as selobj?) when interpreting that mouseray. For now, it's ok to kluge this # by assuming this class is taking the role of world-drawn-instance and being drawn only once. Alternatively, # we could say that the recipient of the mouseray should also be told what background it's over (whether or not it # hit the background or some object drawn over it) (this might be "one of several background objects" for stereo) # so it would know how to find "absolute model coords" in a canonical way. Or a third way is for the World client code # to include an invisible object drawn just to capture the coords. In the multi-world-drawn-instance case, we'd have to combine # this object with our knowledge of which worldinstance was drawn, by storing the drawn coords under the crossproduct # of that and the object... # ... Trying one of these solutions with self._coordsys_holder below. Seems to work & fix the pallette bug. Comment needs cleanup. # [070401] # ###FLAW: logically, this should store exprs needed to remake its instances, merely caching the instances, # but in fact, stores the instances themselves. This will make save/load harder, and means instances survive code-reloading. # Probably it should be fixed before trying to do save/load. [070206 comment] _nodeset = State( Anything, {} ) # self._nodeset is private; it's changetracked but only when entirely replaced (not just modified!) # (###e optim: can that be fixed? we need a way to access the LvalForState and inval it) ###e we'll need to put that State on a different state-layer (i.e. kind of StatePlace) when we start saving/loading this _coordsys_holder = Instance(SavedCoordsys( )) #070401 -- see long comment above, about _CoordsysHolder def _init_instance(self): super(World, self)._init_instance() set_default_attrs(self.untracked_model_state, _index_counter=4000 ) #070213; could State() do this for us instead? #e def _C_number_of_objects(self): """Compute self.number_of_objects, defined as the total number of objects explicitly stored in self, of all types, in all Parts and configurations (but not including objects removed by Undo but present in Undo history). WARNING: Parts, configurations, and Undo are nim for this class as of 070206. Note: probably not very useful, due to that lack of qualification by anything. Perhaps useful to know whether self is entirely empty, or as a ballpark estimate of how much data it might contain. """ return len(self._nodeset) def list_all_objects_of_type(self, model_type): """Return a nominally read-only list of all objects in self of the given model_type (or IorE class, as a special case), in a deterministic order not yet decided on (might be creation order, world-index order, MT order). [Current implem uses creation order as an Instance, as told by _e_serno; this order will become unideal as new kinds of object ops are added, like potential objs or moving them from other worlds (if possible). ##e 070206] ##e needs options for "not just this config", "not just this Part" (and revised implem, once we support configs or Parts) #e optim: extend API to be able to specify ordering -- otherwise it has to be sorted twice (here, and often in caller). """ type_predicate = model_type_predicate( model_type ) # this permits model_type to be an IorE subclass (or its name) ###BAD: not enough kinds of type exprs can be passed, and error checking on model_type (not being garbage) is nim return filter( type_predicate, self._sorted_objects ) ##e optim: filter first (or keep them already type-separated), then sort def _append_node(self, index, node): #070201 new feature ###e SHOULD RENAME [070205] "not sure if this should be private... API revised 070205, ought to rename it more (and #doc it)" ## self._nodeset = dict(self._nodeset) ## ###KLUGE: copy it first, to make sure it gets change-tracked. Inefficient when long! ###BUG in the above: # doesn't work, since the change to it is not tracked, since the old and new values compare equal! (guess) #k note that LvalForState may also not keep a deep enough copy of the old value in doing the compare, # to not be fooled by the direct modification of the dict (in the sense of that mod escaping its notice, even later)! # (i'm not sure -- needs review, e.g. about whether bugs of this kind will go undetected and be too hard to catch) ## self._nodeset[index] = node newset = dict(self._nodeset) newset[index] = node self._nodeset = newset # this ought to compare different and cause changetracking (assuming the index was in fact new) ###e OPTIM IDEA: can we modify it, then set it to itself? No, because LvalForState will compare saved & new value -- # the same mutable object! We need to fix that, but a workaround is to set it to None and then set it to itself again. # That ought to work fine. ####TRYIT but then fix LvalForState so we can tell it we modified its mutable contents. [070228] return def _C__sorted_objects(self): """compute private self._sorted_objects (a list, ordered by something not yet decided, probably creation time; probably should be the same order used by list_all_objects_of_type) """ res = self._nodeset.values() res = sorted_by(res, lambda obj: obj._e_serno) ###KLUGE: use _e_serno in place of .creation_order, not yet defined -- # works now, but wrong in general due to potential objects or moved-from-elsewhere objects. # (Q: Would index-in-world be ordered the same way? A: Yes for now, but not good to require this in the future.) return res def draw(self): # draw all the nodes [#e 070228 ###e in future we're more likely to draw X(node) for X supplied from caller & subset of nodes] # [optim idea 070103 late: have caller put this in a DisplayListChunk; will it actually work? # the hope is, yes for animating rotation, with proper inval when nodelist changes. It ought to work! Try it. It works!] self._coordsys_holder.save_coords_if_safe() #070401 for node in self._sorted_objects: # print "%r is drawing %r at %r" % (self, node, node.pos) # suspicious: all have same pos ... didn't stay true, nevermind self.drawkid(node) ## node.draw() # this assumes the items in the list track their own posns, which might not make perfect sense; # otoh if they didn't we'd probably replace them with container objs for our view of them, which did track their pos; # so it doesn't make much difference in our code. we can always have a type "Vertex for us" to coerce them to # which if necessary adds the pos which only we see -- we'd want this if one Vertex could be in two Worlds at diff posns. # (Which is likely, due to Configuration Management.) if 0 and node is self._sorted_objects[-1]: print "drew last node in list, %r, ipath[0] %r, pos %r" % ( node, node.ipath[0], node.pos) ###e see comment above: "maybe World needs to wrap all it draws with glue to add looks and behavior to it" return def make_and_add( self, expr, type=None ): #070206 added type option -- required for now (temporary); semantics not fully defined """Make a new model object instance from expr, add it to the world at a local index we choose, and return it. This is the main public method for making new model objects. [As a temporary ###KLUGE (070206), the type needs to be passed, for purposes of methods like self.list_all_objects_of_type. This will no longer be needed once we can reliably infer the type (including supertypes) from the made instance of expr. But that probably requires altering delegation to specify which attrs to delegate (based on which interface they're part of), including a new attr for model object type. ###e Even once that's done, the caller may want to pass type-like info, perhaps "tags" -- e.g. one current caller passes type = "dot" which could not be inferred from expr it passes. ###e Callers may also want to pass desired relations, like a parent object, for convenience, or to influence details of how we make and index the new instance. ##e callers will also want to wrap objects with type-nicknames (see code comment for details). [WARNING: the API may be revised to also return the index. Or, we might store that in object, or store a back-dict here.] """ index, node = self._make(expr) # that picks a unique index (using a counter in transient_state); I think that's ok # (but its change/usage tracking needs review ####k) self._append_node(index, node) # revised 070205 if 'model_type_on_object_kluge_070206': ###KLUGE -- store type on object (simulates later inferring it from object) # update 070213: the right thing to do is probably not this, but to wrap the object itself with a type-nickname # (before passing it to us), so not only this world (self), but everything that sees it (MT default label, commands/ops, # selobj checks, etc), can react to its type-nickname. if type is not None: # node._i_model_type is defined on all Instances, and node should be one ## if not hasattr(node, '_i_model_type'): ## node._i_model_type = None # this is how much of a kluge it is -- we're not even sure which classes need this attr if node._i_model_type is not None: assert node._i_model_type == type, "make_and_add type %r inconsistent with existing type %r on %r" % \ (type, node._i_model_type, node) node._i_model_type = type pass return node def _make(self, expr): # moved here from demo_drag 070202 """[private] Allocate a new index, use it as the localmost component of ipath while making [or finding?? I THINK NOT ####k] expr, and return (index, new-expr-instance). Note that it's up to expr whether to actually make use of the suggested ipath in the new instance. The case of one instance stored with different indices in World is not reviewed, and not recommended until it is, but it's up to the caller to worry about. I don't know it's wrong, just never thought about it and assumed it would not happen when analyzing the code. """ index = None #e rename? #e move to some superclass #e worry about lexenv, eg _self in the expr, _this or _my in it... is expr hardcoded or from an arg? #e revise implem in other ways eg eval vs instantiate #e default unique index?? (unique in the saved stateplace, not just here) # (in fact, is reuse of index going to occur from a Command and be a problem? note *we* are acting as command... #e use in other recent commits that inlined it if index is None: # usual case I hope (due to issues mentioned above [maybe here or maybe in demo_drag.py]): allocate one index = self.untracked_model_state._index_counter if 'index should be modified [070203]': # see comments above dated 070203 index = index + 1 self.untracked_model_state._index_counter = index # 070213 changed implem of _index_counter to make sure it's not change/usage-tracked. # (Fyi, I don't know whether or not it was before (using State()), in a way that mattered. # The tracking danger would be that whenever you make any new object, # the old objects see that the index they used is different # and think they too need remaking, or something like that! This needs thinking through # since it probably covers all make-data, not just the index. All make-data is being snapshotted. # For that matter, things used by "commands" are in general different than things used to recompute. # Maybe entire commands need to have tracked usage discarded or kept in a new kind of place. #####FIGURE OUT) # # Note (language design flaw): to set this attr (using current code), # you have to refer to it directly in the stateplace, # rather than using the kind of abbrev that (in Highlightable glname) seems to work for get: # _index_counter = _self.untracked_model_state._index_counter # This could possibly be fixed in C_rule_for_formula (not sure), or by making the abbrev in a fancier manner. # (If you try to set the abbrev, you get # "AssertionError: subclass [C_rule_for_formula] ... should implement set_for_our_cls".) # ###BUG: even for get, the abbreviated form (self._index_counter) is not updated when the spelled out form is!! # See this debug print from when we tried that here: ## print "after set to %r, self._index_counter = %r, self.untracked_model_state._index_counter = %r" % \ ## (index, self._index_counter, self.untracked_model_state._index_counter ) ## # it prints: after set to 4002, self._index_counter = 4001, self.untracked_model_state._index_counter = 4002 # This is weird, since Highlightable seems to succeed using a similar glname abbrev for get. # (Unless it doesn't, and I never noticed?? To check, I just added debug code for that -- so far untriggered.) pass ###e LOGIC ISSUE: should assert the resulting ipath has never been used, # or have a more fundamental mechanism to guarantee that env = self.env # maybe wrong, esp re _self ipath = (index, self.ipath) return index, env.make(expr, ipath) # note: does eval before actual make # == def _C_mt_kids( self): # 070206 experiment related to ModelTreeNodeInterface (sp?) ###e how do we depend on the mt display prefs? note we need to be drawn twice, once in graphics area using .draw # and once in MT using the mt_* methods, but with independent envs provided! Self.env might have room in attr namespace, # but it has a single origin. Besides there might be two indep MT views or graphics views -- each of those also needs # separate envs (of the same kind). But that makes us two instances! I guess we need separate World data obj vs World viewer, # to handle that! I'm not even sure it makes sense to put the viewing methods in the same object... but maybe it does # with this concept of partial instantiation [nim], in which we could instantiate the viewing layer(?) (interface??) twice, # and the data layer just once, letting it (an instance) serve as the expr for instantiating the viewing layer in two places. # (But this makes it clear that the env would need to be split into separate parts, one for each partially instantiable # layer -- hopefully these parts would correspond to interfaces (or sets of them), i.e. an interface's attrs would # have to all be instantiated at the same time, and decls would control which ones were instantiated together in which # partial-instantiation layers.) # # So for now let's pretend self.env can tell us... tho as initial kluge, the global env.prefs (get_pref?) could tell us. # But even sooner, just pretend we don't care and always show all the kids. return self._sorted_objects mt_name = "testmode" #e or maybe something like State(str, "Untitled"), or a stateref # or "Untitled" as it was before 070208 mt_openable = True ## mt_node_id = getattr_Expr( _self, '_e_serno') mt_node_id = getattr_Expr( _self, 'ipath' ) #e optim: should intern the ipath ###e move this to IorE? btw redundant w/ def mt_node_id # 070218 -- by test, using ipath (a bugfix) makes world.open persistent (as hoped/predicted); # probably doesn't affect open-MT newnode slowness (but now that's fixed in different way in demo_MT) # == def _cmd_Clear( self ): #070106 experimental naming convention for a "cmd method" -- a command on this object (requiring no args/opts, by default) if self._nodeset: # avoid gratuitous change-track by only doing it if it does something (see also _cmd_Clear_nontrivial) # NOTE: this cond is probably not needed, since (IIRC) LvalForState only invalidates if a same_vals comparison fails. ###k self._nodeset = {} return # related formulae for that command # (names are related by convention, only used in this file, so far; prototype for wider convention, but not yet well thought through) _cmd_Clear_nontrivial = not_Expr( not_Expr(_nodeset) ) #KLUGE: need a more direct boolean coercion (not that it's really needed at all) # can be used to enable (vs disable) a button or menu item for this command on this object _cmd_Clear_legal = True # whether giving the command to this object from a script would be an error _cmd_Clear_tooltip = "clear all objects" # a command button or menu item could use this as its tooltip ###e is this client-specific?? def _cmd_Make(self): print "world make is nim" ### _cmd_Make_tooltip = "make something [nim]" ###e when we know the source of what to make, it can also tell us this tooltip pass # end of class World
class DragBehavior_AlongLine(DragBehavior ): #070318 (compare to SimpleDragBehavior) """ A drag behavior which moves the original hitpoint along a line, storing only its 1d-position-offset along the line's direction [noting that the hitpoint is not necessarily equal to the moved object's origin] [#doc better] """ # args [#e replace 'height' with 'posn_parameter' or 'posn_param' in these comments & docstrings] highlightable = Arg( Anything ) ###e is there a way we can require that we're passed an Instance rather than making it ourselves? # I suspect that violating that caused the bug in example 2. # (A way that would work: a specialcase check in _init_instance. # But I'd rather have an option on Arg to do that. require_already_instance?? Or ArgInstance? that latter invites confusion # since it's not analogous to ArgExpr.) posn_parameter_ref = Arg(StateRef, doc="where the variable height is stored") constrain_to_line = Arg( Ray, doc="the line/ray on which the height is interpreted as a position") ###e rename: constraint_line? line_to_constrain_to? constrain_to_this_line? # note: the position of the height on this line is typically used as the position of the drawn movable object's origin; # we shouldn't assume the drag startpoint is on the line, since the drawn movable object might be touched at any of its points. ##e rename Ray -> MarkedLine? (a line, with real number markings on it) ParametricLine? (no, suggests they can be distorted/curved) range = Option(tuple_Expr, None, doc="range limit of height") #nim ##e drag event object can be passed to us... as a delegate! [In recent code, it seems like the Highlightable is taking this role.] # (if this delegates or supers to something that knows all about the drag in a lower level way, # then maybe it's not so bad to be getting dragevent info from self rather than a dragevent arg... hmm. # An argument in favor of that: self is storing state related to one drag anyway, so nothing is lost # by assuming some other level of self stores some other level of that state. So maybe we create a standard # DragHandling object, then create a behavior-specific delegate to it, to which *it* delegates on_press etc; # and we also get it to delegate state-modifying calls to some external object -- or maybe that's not needed # if things like this one's stateref are enough. ##k) # # related: Maybe DraggableObject gets split into the MovableObject and the DragBehavior... # same as in SimpleDragBehavior: saved_coordsys & current_event_mousepoint # state: saved_coordsys = Instance( SavedCoordsys() ) # provides transient state for saving a fixed coordsys to use throughout a drag # helper methods (these probably belong in a superclass): def current_event_mousepoint( self, *args, **kws ): #e zap this and inline it, for clarity? or move it into DragBehavior superclass?? return self.saved_coordsys.current_event_mousepoint(*args, **kws) def current_event_mouseray(self): p0 = self.current_event_mousepoint(depth=0.0) # nearest depth ###k p1 = self.current_event_mousepoint(depth=1.0) # farthest depth ###k return Ray( p0, p1 - p0 ) #e passing just p1 should be ok too, but both forms can't work unless p0,p1 are typed objects... # specific methods def _C__translation( self ): ### WARNING: used externally too -- rename to be not private if we decide that's ok ###e """ compute self._translation from the externally stored height """ k = self.posn_parameter_ref.value return self.constrain_to_line.posn_from_params( k) #e review renaming, since we are asking it for a 3-tuple def on_press(self): self.saved_coordsys.copy_from( self.highlightable ) # needed before using current_event_mousepoint or current_event_mouseray # (since self.highlightable's coordsys changes during the drag) self.startpoint = self.current_event_mousepoint( ) # the touched point on the visible object (hitpoint) self.offset = self.startpoint - ( ORIGIN + self._translation ) #k maybe ok for now, but check whether sensible in long run self.line = self.constrain_to_line + self.offset # the line the hitpoint is on (and constrained to, if handle is rigid) # (this is parallel to self.constrain_to_line and intersects the hitpoint) def on_drag(self): # Note: we can assume this is a "real drag" (not one which is too short to count), due to how we are called. mouseray = self.current_event_mouseray() k = self.line.closest_pt_params_to_ray(mouseray) # # BUG: for lines with a lot of Z direction, in perspective view, # this is wrong -- we want the closest point on the screen, # not in space. The current code (closest point in space) # would tend to come up with a point too near the screen, # if the constraint line and the mouseray are diverging away # from each other (in model space) with depth. # # TODO: fix this. Possible fixes include: # - common code, using a projection matrix & its inverse (seems hard) # - separate code for Ortho vs Perspective case (needs eyepoint) # (might be clearest & simplest; it might turn out we'd want other # behavior tweaks which differed in these cases -- for example, # stop dragging and turn the handle red when it gets so far away # in model space (near the "point at infinity" visible on the screen # for an infinite line -- aka the "point of convergence" for a family # of parallel lines) that its motion would be too sensitive, since # the constraint line is too close to perpendicular to the screen) # - save the initial mouseray too, since it gives us enough info # (along with current one) to know the entire projection # (except when they coincide, in which case no motion is needed). # Alg for this is not yet worked out. Q: is an ortho projection # along initial mouseray a good approximation? Probably not, # if we approach the "point at infinity". # # Also we need a refactoring, so that one external object can store # both the geometric info about the constraint, and the state of the # dragpoint along it, accessible as parameter-along-line or point or both, # with either being the underlying state. (Unless nothing less than a # DragBehavior can actually do all those things, in which case, # we need variants depending on whether the point or the parameter # is the underlying state. But more likely, we want a DragState which # knows how to make the underlying state convenient, and a DragBehavior # which knows how to connect that to mouse gestures, so these can vary # separately.) # # [bruce 070913 comment] # if k is not None: # store k, after range-limiting range = self.range # don't use the python builtin of the same name, #in this method! (#e or rename the option?) if range is not None: low, high = range if low is not None and k < low: k = low if high is not None and k > high: k = high self.posn_parameter_ref.value = k ##e by analogy with DraggableObject, should we perhaps save this ##side effect until the end? return def on_release(self): pass pass # end of class DragBehavior_AlongLine
class ActionButton(DelegatingInstanceOrExpr): # 070104 quick prototype "ActionButton(command, text) is something the user can press to run command, which looks like a button." # args/options command = Arg(Action) #e default which prints? text = Arg(str, "<do it>" ) #e default text should be extracted from the command somehow button = Arg( Widget2D, Rect(15. * PIXELS) ) # can it be left out so only text label is used? ideally we'd have text with special border... enabled = Option( bool, True ) # whether the button should look enabled, and whether the command will run when the button is operated # formulae use_label = TextRect(text) ## TextRect(text,1,20)###e revise plain_button = CenterY(button) highlighted_button = Boxed( plain_button, # note: despite the name, this is only shown as the highlighted form when enabled is true bordercolor= blue, # should color adapt to bg? is it a bad idea to put this over bg rather than over button? borderthickness=1.5 * PIXELS, gap=1 * PIXELS, ) ###k ???? -- note, this doesn't include the label -- ok? plain = DisplayListChunk(SimpleRow( plain_button, CenterY(use_label))) # align = CenterY is nim highlighted = DisplayListChunk( SimpleRow(highlighted_button, CenterY(use_label), pixelgap=0.5)) #k ok to wrap with DisplayListChunk? [seems so] ### KLUGE: without the pixelgap adjustment (to this particular weird-looking value, i guess), # the label moves to the right when highlighted, due to the Boxed being used to position it in the row. ### BUG: CenterY is not perfectly working. Guess -- lbox for TextRect is slightly wrong. ### IDEA: make the borderthickness for Boxed negative so the border is over the edge of the plain button. Might look better. ##e Note: we have no "pressed" appearance, since by the next time we get drawn, the command is already drawn and we ought to be # back to normal. Someday we should do a transient incremental redraw of just this button, with a "pressed and acting" appearance, # which can then go back to normal when the operation completes and does a regular full redraw. # Alternatively, we could switch to using buttons with an on_release_in action only, # and then have ordinarily-drawn pressed etc looks. [070227 comment] # update 070305: let's try to fix that: # appearances for optional willdoit-flicker (confirms unambiguously that the button was hit and will do something) [070307] # [ideally the computation & side effects could overlap the willdoit flicker in time, # but they don't now, which is one reason the flicker is optional] incr_drawable_willdo1 = Instance( SimpleRow(highlighted_button(bordercolor=yellow), pixelgap=0.5)) # label not needed here incr_drawable_willdo2 = Instance( SimpleRow(highlighted_button(bordercolor=blue), pixelgap=0.5)) # note: yellow/blue (matching the usual ending & starting colors which bracket the flicker) looks much better than black/white # what it looks like while we're computing/doing its effects: incr_drawable_doing = Instance( SimpleRow( highlighted_button(bordercolor=orange), ## CenterY(use_label), [removed -- see comment for why -- might be added back] pixelgap=0.5)) # orange warns you that it's not yet done, is also bright & active for action; ### UI FLAW: the orange/yellow distinction is annoying, so it's really only desirable for debugging, # since it shows that the instantiation time is significant but only happens on the first use of a button. # Probably the distinction (and its redraw happening at all) should be a debug_pref or so. ###FIX # (But if there is no distinction, we may want to be sure to redraw the label now if there is any chance it can be different -- # but in current code there's not, since we haven't changed state it might depend on by the time we draw it. # BTW I wonder if redrawing the label (i.e. instantiating this instance of it) ever takes significant time itself?? #k) # what it looks like while we're redrawing (after finishing its internal effects): incr_drawable_done = Instance( SimpleRow(highlighted_button(bordercolor=yellow), pixelgap=0.5)) # label not needed here # yellow means done -- not sure makes sense -- note green means "can do" in some controls def doit(self): """This runs when the user clicks on the button. WARNING: it's NOT just self.do_action() (the public method self's clients, like scripts, can call to do the same action) -- it calls that, but it also has graphical effects. [It may or may not be public (in the Action interface) in the end. If it is, it'll be renamed. #e] """ if self.enabled: # do some incremental drawing [new feature 070305, revised 070307] ###BUG (in some client code of this class): # this won't be able to make clear button quickly show it's disabled until client code is revised and maybe selobj-bugfixed ###DOIT if debug_pref( "testmode: ActionButton willdoit-flicker?", # When set, this flickers the button, like how the mac confirms a menu item choice. # Conclusion after testing: it works fine, and usually looks ok, # but is redundant with "yellow during redraw", # so as long as that's slow enough to see, this has no point and is also making it even slower, # so leave it turned off by default. Choice_boolean_False, prefs_key='A9 devel/exprs/action flicker'): # works [retest###], but I won't make it True by default [070307] ##e someday do this in a way that does not tie up the thread during this, e.g. by letting paintGL do it; # for now it's just experimental for its graphics effects and as a speed test, # and will probably be turned off after testing for i in range(4): if i % 2 == 0: self.draw_incrementally(self.incr_drawable_willdo1) else: self.draw_incrementally(self.incr_drawable_willdo2) # print i, # very fast # todo: delay, if needed to make this visible -- using time.time to delay only if draw timing was not long enough # (with no delay it's almost too fast too see -- sometime I should write the code to measure the actual speed) # (for now assume it's always very fast, so just delay a fixed amount using time.sleep) time.sleep(1.0 / 3 / 4) # 1/3 sec, spread over 4 sleeps self.draw_incrementally( self.incr_drawable_doing ) # this method runs in the Highlightable made in delegate print "ActionButton: doing %r for %r" % (self.text, self ) ### remove self? ##e optim note: this shows self is a different obj each time (at least for make dna cyl button)... # I guess this is due to dna_ribbon_view_toolcorner_expr_maker being a function that makes an expr # which runs again at least on every use of the button (maybe more -- not sure exactly how often). # Should fix that (and it's not this file's fault -- just that the print stmt above reveals the problem). self.do_action() self.draw_incrementally(self.incr_drawable_done) pass else: print "ActionButton: not enabled, so not doing %r for %r" % ( self.text, self) # remove when works [reenabled 070307 ####] pass return def do_action(self): "#doc -- public, also used internally; see doit comment for doc, for now" res = self.command() if res is not None: print "unexpected: %r cmd %r retval was not None: %r" % ( self, self.text, res, ) #e remove if happens legitimately return ###e refile these: def draw_incrementally( self, thing): #070307 #e refile (as for next method below) "#doc" self._incrementally_draw_OpenGL( thing.draw ) #e or call a variant method of thing, which defaults to thing.draw?? nah, use an env var? def _incrementally_draw_OpenGL( self, func ): #070307 #e rename ###e refile into IorE someday, and into Highlightable for now, i think """helper method for incremental drawing by user event handling methods (not part of self.draw called by paintGL). [#doc better] func should contain OpenGL commands for incrementally drawing, in self's coords (but not the swapbuffers at the end). Guess at a requirement within func: [which should be removed and is prob not real now, see below] # don't use drawkid! (because we're not inside a draw method) # (but will this cause trouble for draw methods inside this?? ### NEEDS REVIEW) [but instead we might as well make sure that drawkid's parent-seeing alg will not be messed up, since it'll be used inside whatever subthing draws we call, anyway] """ ###e undefined in API so far: what if func says "draw later" (eg for transparency) -- do we do all that too, before we return?? # guess: yes, but we'll need special drawing-env settings to tell primitives inside func that we're doing incremental drawing, # since it'll affect things like whether it's ok to write into the depth buffer for transparent objs obscuring visible ones # (useful for glselect code but would mess up subsequent incr drawing). def func1(self=self, func=func): res = func() self.env.glpane.swapBuffers() # update display [needed] return res ran_already_flag, funcres = self.run_OpenGL_in_local_coords(func1) # note: this runs in self or first delegate that's a Highlightable, for now; that determines its gl state & coordsys assert ran_already_flag return funcres #e should we change to doing the action on_release_in, rather than on_press? delegate = Highlightable( plain, ##e should this depend on enabled? probably yes, but probably the caller has to pass in the disabled form. ###e at least for the Mac, maybe it also ought to depend on whether the application is active (frontmost) and will respond to clicks. If( enabled, highlighted, plain ), # revised 070109 to depend on enabled [#k does this cause the delegate expr itself to be remade??] on_press=_self.doit, # note: there was a bug in the prior form of this, "on_press = command" -- command to do should depend on enabled -- ##e but i'm not sure if If(enabled,command,None) will work properly ###k TRY IT -- nevermind, using _self.doit now [070208] sbar_text=text #e should sbar_text depend on enabled?? yes, but need to revise callers then too -- some of which make the text depend on it ) pass # end of class ActionButton
class DnaStrand_EditCommand(State_preMixin, EditCommand): """ Command to edit a DnaStrand (chunk object as of 2008-02-14) To edit a Strand, first enter BuildDna_EditCommand (accessed using Build> Dna) then, select a strand chunk of an existing DnaSegment within the DnaGroup you are editing. When you select the strand chunk, it enters DnaStrand_Editcommand, shows its property manager and also shows the resize handles if any. """ cmd = 'Dna Strand' sponsor_keyword = 'DNA' prefix = 'Strand ' # used for gensym cmdname = "DNA_STRAND" commandName = 'DNA_STRAND' featurename = 'Edit_Dna_Strand' command_should_resume_prevMode = True command_has_its_own_gui = True command_can_be_suspended = False # Generators for DNA, nanotubes and graphene have their MT name # generated (in GeneratorBaseClass) from the prefix. create_name_from_prefix = True call_makeMenus_for_each_event = True #Graphics Mode GraphicsMode_class = DnaStrand_GraphicsMode #@see: self.updateHandlePositions for details on how these variables are #used in computation. #@see: DnaStrand_ResizeHandle and handle declaration in this class #definition handlePoint1 = State( Point, ORIGIN) handlePoint2 = State( Point, ORIGIN) handleDirection1 = State(Vector, ORIGIN) handleDirection2 = State(Vector, ORIGIN) #See self._update_resizeHandle_radius where this gets changed. #also see DnaSegment_ResizeHandle to see how its implemented. handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE) #handleColors handleColor1 = State(Color, purple) handleColor2 = State(Color, purple) #TODO: 'cylinderWidth attr used for resize handles -- needs to be renamed #along with 'height_ref attr in exprs.DraggableHandle_AlongLine cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) #Resize Handles for Strand. See self.updateHandlePositions() leftHandle = Instance( DnaStrand_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth'), origin = handlePoint1, fixedEndOfStructure = handlePoint2, direction = handleDirection1, sphereRadius = handleSphereRadius1, handleColor = handleColor1 )) rightHandle = Instance( DnaStrand_ResizeHandle( command = _self, height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth2'), origin = handlePoint2, fixedEndOfStructure = handlePoint1, direction = handleDirection2, sphereRadius = handleSphereRadius2, handleColor = handleColor2 )) def __init__(self, commandSequencer, struct = None): """ Constructor for DnaDuplex_EditCommand """ glpane = commandSequencer State_preMixin.__init__(self, glpane) EditCommand.__init__(self, commandSequencer) self.struct = struct #DnaSegment object to which this strand belongs self._parentDnaSegment = None #It uses BuildDna_EditCommand.flyoutToolbar ( in other words, that #toolbar is still accessible and not changes while in this command) flyoutToolbar = None #Graphics handles for editing the structure . self.handles = [] self.grabbedHandle = None #This is used for comarison purpose in model_changed method to decide #whether to update the sequence. self._previousNumberOfBases = None def init_gui(self): """ Initialize gui. """ #@see DnaSegment_EditCommand.init_gui() for a detailed note. #This command implements similar thing self.create_and_or_show_PM_if_wanted(showPropMgr = False) def model_changed(self): #This MAY HAVE BUG. WHEN -- #debug pref 'call model_changed only when needed' is ON #See related bug 2729 for details. #The following code that updates te handle positions and the strand #sequence fixes bugs like 2745 and updating the handle positions #updating handle positions in model_changed instead of in #self.graphicsMode._draw_handles() is also a minor optimization #This can be further optimized by debug pref #'call model_changed only when needed' but its NOT done because of an # issue menitoned in bug 2729 - Ninad 2008-04-07 EditCommand.model_changed(self) #This also calls the #propMgr.model_changed if self.grabbedHandle is not None: return #For Rattlesnake, PAM5 segment resizing is not supported. #@see: self.hasResizableStructure() if self.hasValidStructure(): isStructResizable, why_not = self.hasResizableStructure() if not isStructResizable: self.handles = [] return elif len(self.handles) == 0: self._updateHandleList() self.updateHandlePositions() #NOTE: The following also updates self._previousParams self._updateStrandSequence_if_needed() def keep_empty_group(self, group): """ Returns True if the empty group should not be automatically deleted. otherwise returns False. The default implementation always returns False. Subclasses should override this method if it needs to keep the empty group for some reasons. Note that this method will only get called when a group has a class constant autdelete_when_empty set to True. (and as of 2008-03-06, it is proposed that dna_updater calls this method when needed. @see: Command.keep_empty_group() which is overridden here. """ bool_keep = EditCommand.keep_empty_group(self, group) if not bool_keep: if self.hasValidStructure(): if group is self.struct: bool_keep = True elif group is self.struct.parent_node_of_class(self.assy.DnaGroup): bool_keep = True return bool_keep def _createPropMgrObject(self): """ Creates a property manager object (that defines UI things) for this editCommand. """ assert not self.propMgr propMgr = self.win.createDnaStrandPropMgr_if_needed(self) return propMgr def _gatherParameters(self): """ Return the parameters from the property manager UI. @return: All the parameters (get those from the property manager): - numberOfBases - dnaForm - basesPerTurn - endPoint1 - endPoint2 @rtype: tuple """ return self.propMgr.getParameters() def _createStructure(self): """ Creates and returns the structure -- TO BE IMPLEMENTED ONCE DNA_MODEL IS READY FOR USE. @return : DnaStrand. @rtype: L{Group} @note: This needs to return a DNA object once that model is implemented """ pass def _modifyStructure(self, params): """ Modify the structure based on the parameters specified. Overrides EditCommand._modifystructure. This method removes the old structure and creates a new one using self._createStructure. This was needed for the structures like this (Dna, Nanotube etc) . . See more comments in the method. """ assert self.struct # parameters have changed, update existing structure self._revertNumber() # self.name needed for done message if self.create_name_from_prefix: # create a new name name = self.name = gensym(self.prefix, self.assy) # (in _build_struct) self._gensym_data_for_reusing_name = (self.prefix, name) else: # use externally created name self._gensym_data_for_reusing_name = None # (can't reuse name in this case -- not sure what prefix it was # made with) name = self.name self.dna = B_Dna_PAM3_SingleStrand() numberOfBases, \ dnaForm, \ dnaModel, \ basesPerTurn, \ duplexRise, \ color_junk = params #see a note about color_junk in DnaSegment_EditCommand._modifyStructure() numberOfBasesToAddOrRemove = self._determine_numberOfBases_to_change() if numberOfBasesToAddOrRemove != 0: resizeEndStrandAtom, resizeEndAxisAtom = \ self.get_strand_and_axis_endAtoms_at_resize_end() if resizeEndAxisAtom: dnaSegment = resizeEndAxisAtom.molecule.parent_node_of_class( self.assy.DnaSegment) resizeEnd_final_position = self._get_resizeEnd_final_position( resizeEndAxisAtom, abs(numberOfBasesToAddOrRemove), duplexRise ) self.dna.modify(dnaSegment, resizeEndAxisAtom, numberOfBasesToAddOrRemove, basesPerTurn, duplexRise, resizeEndAxisAtom.posn(), resizeEnd_final_position, resizeEndStrandAtom = resizeEndStrandAtom ) return def _finalizeStructure(self): """ Overrides EditCommand._finalizeStructure. @see: EditCommand.preview_or_finalize_structure """ if self.struct is not None: #@TODO 2008-03-19:Should we always do this even when strand sequence #is not changed?? Should it waste time in comparing the current #sequence with a previous one? Assigning sequence while leaving the #command will be slow for large files.Need to be optimized if #problematic. #What if a flag is set in self.propMgr.updateSequence() when it #updates the seq for the second time and we check that here. #Thats not good if the method gets called multiple times for some #reason other than the user entered sequence. So not doing here. #Better fix would be to check if sequence gets changed (textChanged) #in DnaSequence editor class .... Need to be revised EditCommand._finalizeStructure(self) self._updateStrandSequence_if_needed() self.assignStrandSequence() def _previewStructure(self): EditCommand._previewStructure(self) self.updateHandlePositions() self._updateStrandSequence_if_needed() def _updateStrandSequence_if_needed(self): if self.hasValidStructure(): new_numberOfBases = self.struct.getNumberOfBases() #@TODO Update self._previousParams again? #This NEEDS TO BE REVISED. BUG MENTIONED BELOW---- #we already update this in EditCommand class. But, it doesn't work for #the following bug -- 1. create a duplex with 5 basepairs, 2. resize #red strand to 2 bases. 3. Undo 4. Redo 5. Try to resize it again to #2 bases -- it doesn't work because self.previousParams stil stores #bases as 2 and thinks nothing got changed! Can we declare #self._previewStructure as a undiable state? Or better self._previousParams #should store self.struct and should check method return value #like self.struct.getNumberOfBases()--Ninad 2008-04-07 if self.previousParams is not None: if new_numberOfBases != self.previousParams[0]: self.propMgr.numberOfBasesSpinBox.setValue(new_numberOfBases) self.previousParams = self._gatherParameters() if not same_vals(new_numberOfBases, self._previousNumberOfBases): self.propMgr.updateSequence() self._previousNumberOfBases = new_numberOfBases def _get_resizeEnd_final_position(self, resizeEndAxisAtom, numberOfBases, duplexRise): final_position = None if self.grabbedHandle: final_position = self.grabbedHandle.currentPosition else: dnaSegment = resizeEndAxisAtom.molecule.parent_node_of_class(self.assy.DnaSegment) other_axisEndAtom = dnaSegment.getOtherAxisEndAtom(resizeEndAxisAtom) axis_vector = resizeEndAxisAtom.posn() - other_axisEndAtom.posn() segment_length_to_add = getDuplexLength('B-DNA', numberOfBases, duplexRise = duplexRise) final_position = resizeEndAxisAtom.posn() + norm(axis_vector)*segment_length_to_add return final_position def getStructureName(self): """ Returns the name string of self.struct if there is a valid structure. Otherwise returns None. This information is used by the name edit field of this command's PM when we call self.propMgr.show() @see: DnaStrand_PropertyManager.show() @see: self.setStructureName """ if self.hasValidStructure(): return self.struct.name else: return None def setStructureName(self, name): """ Sets the name of self.struct to param <name> (if there is a valid structure. The PM of this command callss this method while closing itself @param name: name of the structure to be set. @type name: string @see: DnaStrand_PropertyManager.close() @see: self.getStructureName() @see: DnaSegment_GraphicsMode.leftUp , DnaSegment_editCommand.setStructureName for comments about some issues. """ if self.hasValidStructure(): self.struct.name = name def assignStrandSequence(self): """ Assigns the sequence typed in the sequence editor text field to the selected strand chunk. The method it invokes also assigns a complimentary sequence to the mate strand. @see: Chunk.setStrandSequence """ sequenceString = self.propMgr.sequenceEditor.getPlainSequence() sequenceString = str(sequenceString) #assign strand sequence only if it not the same as the current sequence seq = self.struct.getStrandSequence() if seq != sequenceString: self.struct.setStrandSequence(sequenceString) def editStructure(self, struct = None): """ Edit the structure @param struct: structure to be edited (in this case its a strand chunk) @type struct: chunk or None (this will change post dna data model) """ EditCommand.editStructure(self, struct) if self.hasValidStructure(): self._updatePropMgrParams() #TO BE REVISED post dna data model - 2008-02-14 if isinstance(self.struct.dad , self.assy.DnaSegment): self._parentDnaSegment = self.struct.dad #For Rattlesnake, we do not support resizing of PAM5 model. #So don't append the exprs handles to the handle list (and thus #don't draw those handles. See self.model_changed() isStructResizable, why_not = self.hasResizableStructure() if not isStructResizable: self.handles = [] else: self._updateHandleList() self.updateHandlePositions() def _updatePropMgrParams(self): """ Subclasses may override this method. Update some property manager parameters with the parameters of self.struct (which is being edited) @see: self.editStructure() """ #Format in which params need to be provided to the Property manager #numberOfBases, #dnaForm, #dnaModel, #basesPerTurn, #duplexRise, #color self._previousNumberOfBases = self.struct.getNumberOfBases() numberOfBases = self._previousNumberOfBases color = self.struct.getColor() params_for_propMgr = ( numberOfBases, None, None, None, None, color ) #TODO 2008-03-25: better to get all parameters from self.struct and #set it in propMgr? This will mostly work except that reverse is #not true. i.e. we can not specify same set of params for #self.struct.setProps ...because endPoint1 and endPoint2 are derived. #by the structure when needed. Commenting out following line of code #UPDATE 2008-05-06 Fixes a bug due to which the parameters in propMGr #of DnaSegment_EditCommand are not same as the original structure #(e.g. bases per turn and duplexrise) self.propMgr.setParameters(params_for_propMgr) def _getStructureType(self): """ Subclasses override this method to define their own structure type. Returns the type of the structure this editCommand supports. This is used in isinstance test. @see: EditCommand._getStructureType() (overridden here) """ return self.win.assy.DnaStrand def updateSequence(self): """ Public method provided for convenience. If any callers outside of this command need to update the sequence in the sequence editor, they can simply do currentCommand.updateSequence() rather than currentCommand.propMgr.updateSequence() @see: DnaStrand_ProprtyManager.updateSequence() which does the actual job of updating the sequence string in the sequence editor. """ if self.propMgr is not None: self.propMgr.updateSequence() def hasResizableStructure(self): """ For Rattlesnake release, we dont support strand resizing for PAM5 models. If the structure is not resizable, the handles won't be drawn @see:self.model_changed() @see:DnaStrand_PropertyManager.model_changed() @see: self.editStructure() @see: DnaSegment.is_PAM3_DnaStrand() """ #Note: This method fixes bugs similar to (and including) bug 2812 but #the changes didn't made it to Rattlesnake rc2 -- Ninad 2008-04-16 isResizable = True why_not = '' if not self.hasValidStructure(): isResizable = False why_not = 'It is invalid.' return isResizable, why_not isResizable = self.struct.is_PAM3_DnaStrand() if not isResizable: why_not = 'It needs to be converted to PAM3 model.' return isResizable, why_not #The following fixes bug 2812 strandEndBaseAtom1, strandEndBaseAtom2 = self.struct.get_strand_end_base_atoms() if strandEndBaseAtom1 is strandEndBaseAtom2: isResizable = False why_not = "It is probably a \'closed loop\'." return isResizable, why_not return True, '' def _updateHandleList(self): """ Updates the list of handles (self.handles) @see: self.editStructure @see: DnaSegment_GraphicsMode._drawHandles() """ # note: if handlePoint1 and/or handlePoint2 can change more often than # this runs, we'll need to rerun the two assignments above whenever they # change and before the handle is drawn. An easy way would be to rerun # these assignments in the draw method of our GM. [bruce 080128] self.handles = [] # guess, but seems like a good idea [bruce 080128] self.handles.append(self.leftHandle) self.handles.append(self.rightHandle) def updateHandlePositions(self): """ Update handle positions """ if len(self.handles) == 0: #No handles are appended to self.handles list. #@See self.model_changed() and self._updateHandleList() return self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE axisAtom1 = None axisAtom2 = None strandEndBaseAtom1 = None strandEndBaseAtom2 = None #It could happen (baecause of bugs) that standEndBaseAtom1 and #strandEndBaseAtom2 are one and the same! i.e strand end atom is #not bonded. If this happens, we should throw a traceback bug #exit gracefully. The possible cause of this bug may be --- #(just an example): For some reason, the modify code is unable to #determine the correct bond direction of the resized structure #and therefore, while fusing the original strand with the new one created #by DnaDuplex.modify, it is unable to find bondable pairs! # [- Ninad 2008-03-31] strandEndBaseAtom1, strandEndBaseAtom2 = self.struct.get_strand_end_base_atoms() if strandEndBaseAtom1 is strandEndBaseAtom2: print_compact_stack("bug in updating handle positions, some resized DnaStrand " \ "has only one atom") return if strandEndBaseAtom1: axisAtom1 = strandEndBaseAtom1.axis_neighbor() if axisAtom1: self.handleDirection1 = self._get_handle_direction(axisAtom1, strandEndBaseAtom1) self.handlePoint1 = axisAtom1.posn() if strandEndBaseAtom2: axisAtom2 = strandEndBaseAtom2.axis_neighbor() if axisAtom2: self.handleDirection2 = self._get_handle_direction(axisAtom2, strandEndBaseAtom2) self.handlePoint2 = axisAtom2.posn() # UPDATE 2008-04-15: # Before 2008-04-15 the state attrs for exprs handles were always reset to None #at the beginning of the method. But it is calling model_changed signal #recursively. (se also bug 2729) So, reset thos state attrs only when #needed -- Ninad [ this fix was not in RattleSnake rc1] # Set handlePoints (i.e. their origins) and the handle directions to # None if the atoms used to compute these state attrs are missing. # The GraphicsMode checks if the handles have valid placement # attributes set before drawing it. if strandEndBaseAtom1 is None and strandEndBaseAtom2 is None: #probably a ring self.handles = [] if strandEndBaseAtom1 is None or axisAtom1 is None: self.handleDirection1 = None self.handlePoint1 = None if strandEndBaseAtom2 is None or axisAtom2 is None: self.handleDirection2 = None self.handlePoint2 = None #update the radius of resize handle self._update_resizeHandle_radius(axisAtom1, axisAtom2) #update the display color of the handles self._update_resizeHandle_color(strandEndBaseAtom1, strandEndBaseAtom2) def _update_resizeHandle_radius(self, axisAtom1, axisAtom2): """ Finds out the sphere radius to use for the resize handles, based on atom /chunk or glpane display (whichever decides the display of the end atoms. The default value is 1.2. @see: self.updateHandlePositions() @see: B{Atom.drawing_radius()} """ if axisAtom1 is not None: self.handleSphereRadius1 = max(1.005*axisAtom1.drawing_radius(), 1.005*HANDLE_RADIUS_DEFAULT_VALUE) if axisAtom2 is not None: self.handleSphereRadius2 = max(1.005*axisAtom2.drawing_radius(), 1.005*HANDLE_RADIUS_DEFAULT_VALUE) def _update_resizeHandle_color(self, strandEndBaseAtom1, strandEndBaseAtom2): """ Update the color of resize handles using the current color of the *chunk* of the corresponding strand end atoms. @Note: If no specific color is set to the chunk, it uses 'purple' as the default color (and doesn't use the atom.element.color) . This can be changed if needed. """ if strandEndBaseAtom1 is not None: color = strandEndBaseAtom1.molecule.color if color: self.handleColor1 = color if strandEndBaseAtom2 is not None: color = strandEndBaseAtom2.molecule.color if color: self.handleColor2 = color def _get_handle_direction(self, axisAtom, strandAtom): """ Get the handle direction. """ handle_direction = None strand_rail = strandAtom.molecule.get_ladder_rail() for bond_direction in (1, -1): next_strand_atom = strandAtom.strand_next_baseatom(bond_direction) if next_strand_atom: break if next_strand_atom: next_axis_atom = next_strand_atom.axis_neighbor() if next_axis_atom: handle_direction = norm(axisAtom.posn() - next_axis_atom.posn()) return handle_direction def getDnaRibbonParams(self): """ Returns parameters for drawing the dna ribbon. If the dna rubberband line should NOT be drawn (example when you are removing bases from the strand or if its unable to get dnaSegment) , it retuns None. So the caller should check if the method return value is not None. @see: DnaStrand_GraphicsMode._draw_handles() """ if self.grabbedHandle is None: return None if self.grabbedHandle.origin is None: return None direction_of_drag = norm(self.grabbedHandle.currentPosition - \ self.grabbedHandle.origin) #If the strand bases are being removed (determined by checking the #direction of drag) , no need to draw the rubberband line. if dot(self.grabbedHandle.direction, direction_of_drag) < 0: return None strandEndAtom, axisEndAtom = self.get_strand_and_axis_endAtoms_at_resize_end() #DnaStrand.get_DnaSegment_with_content_atom saely handles the case where #strandEndAtom is None. dnaSegment = self.struct.get_DnaSegment_with_content_atom(strandEndAtom) ribbon1_direction = None if dnaSegment: basesPerTurn = dnaSegment.getBasesPerTurn() duplexRise = dnaSegment.getDuplexRise() ribbon1_start_point = strandEndAtom.posn() if strandEndAtom: ribbon1_start_point = strandEndAtom.posn() for bond_direction, neighbor in strandEndAtom.bond_directions_to_neighbors(): if neighbor and neighbor.is_singlet(): ribbon1_direction = bond_direction break ribbon1Color = strandEndAtom.molecule.color if not ribbon1Color: ribbon1Color = strandEndAtom.element.color return (self.grabbedHandle.origin, self.grabbedHandle.currentPosition, basesPerTurn, duplexRise, ribbon1_start_point, ribbon1_direction, ribbon1Color ) return None def get_strand_and_axis_endAtoms_at_resize_end(self): resizeEndStrandAtom = None resizeEndAxisAtom = None strandEndAtom1, strandEndAtom2 = self.struct.get_strand_end_base_atoms() if self.grabbedHandle is not None: for atm in (strandEndAtom1, strandEndAtom2): axisEndAtom = atm.axis_neighbor() if axisEndAtom: if same_vals(axisEndAtom.posn(), self.grabbedHandle.origin): resizeEndStrandAtom = atm resizeEndAxisAtom = axisEndAtom return (resizeEndStrandAtom, resizeEndAxisAtom) def getCursorText(self): """ Used by DnaStrand_GraphicsMode._draw_handles() @TODO: It also updates the number of bases """ if self.grabbedHandle is None: return #@TODO: This updates the PM as the cursor moves. #Need to rename this method so that you that it also does more things #than just to return a textString. Even better if its called in #self.model_changed but at the moment there is a bug thats why we #are doing this update by calling getCursorText in the #GraphicscMode._draw_handles-- Ninad 2008-04-05 self.update_numberOfBases() if not env.prefs[dnaStrandEditCommand_showCursorTextCheckBox_prefs_key]: return '', black #Note: for Rattlesnake rc2, the text color is green when bases are added #, red when subtracted black when no change. But this implementation is #changed based on Mark's user experience. The text is now always shown #in black color. -- Ninad 2008-04-17 textColor = black text = "" numberOfBases = self.propMgr.numberOfBasesSpinBox.value() numberOfBasesString = self._getCursorText_numberOfBases( numberOfBases) changedBases = self._getCursorText_changedBases( numberOfBases) text = numberOfBasesString if text and changedBases: text += " " # Removed comma. Mark 2008-07-03 text += changedBases return (text , textColor) def _getCursorText_numberOfBases(self, numberOfBases): """ Return the cursor textstring that gives information about the number of bases if the corresponding prefs_key returns True. """ numberOfBasesString = '' if env.prefs[ dnaStrandEditCommand_cursorTextCheckBox_numberOfBases_prefs_key]: numberOfBasesString = "%db"%numberOfBases return numberOfBasesString def _getCursorText_changedBases(self, numberOfBases): """ @see: self.getCursorText() """ changedBasesString = '' if env.prefs[ dnaStrandEditCommand_cursorTextCheckBox_changedBases_prefs_key]: original_numberOfBases = self.struct.getNumberOfBases() changed_bases = numberOfBases - original_numberOfBases if changed_bases > 0: changedBasesString = "(" + "+" + str(changed_bases) + ")" else: changedBasesString = "(" + str(changed_bases) + ")" return changedBasesString def modifyStructure(self): """ Called when a resize handle is dragged to change the length of the segment. (Called upon leftUp) . This method assigns the new parameters for the segment after it is resized and calls preview_or_finalize_structure which does the rest of the job. Note that Client should call this public method and should never call the private method self._modifyStructure. self._modifyStructure is called only by self.preview_or_finalize_structure @see: B{DnaStrand_ResizeHandle.on_release} (the caller) @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the the relevent method in DragHandler API. ) @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior} @see: B{self.preview_or_finalize_structure } @see: B{self._modifyStructure} """ #TODO: need to cleanup this and may be use use something like #self.previousParams = params in the end -- 2008-03-24 (midnight) if self.grabbedHandle is None: return #TODO: Important note: How does NE1 know that structure is modified? #Because number of base pairs parameter in the PropMgr changes as you #drag the handle . This is done in self.getCursorText() ... not the #right place to do it. OR that method needs to be renamed to reflect #this as suggested in that method -- Ninad 2008-03-25 self.preview_or_finalize_structure(previewing = True) self.glpane.gl_update() def update_numberOfBases(self): """ Updates the numberOfBases in the PM while a resize handle is being dragged. @see: self.getCursorText() where it is called. """ #@Note: originally (before 2008-04-05, it was called in #DnaStrand_ResizeHandle.on_drag() but that 'may' have some bugs #(not verified) also see self.getCursorText() to know why it is #called there (basically a workaround for bug 2729 if self.grabbedHandle is None: return currentPosition = self.grabbedHandle.currentPosition resize_end = self.grabbedHandle.origin new_duplexLength = vlen( currentPosition - resize_end ) numberOfBasePairs_to_change = getNumberOfBasePairsFromDuplexLength('B-DNA', new_duplexLength) original_numberOfBases = self.struct.getNumberOfBases() #If the dot product of handle direction and the direction in which it #is dragged is negative, this means we need to subtract bases direction_of_drag = norm(currentPosition - resize_end) if dot(self.grabbedHandle.direction, direction_of_drag ) < 0: total_number_of_bases = original_numberOfBases - numberOfBasePairs_to_change self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases) else: total_number_of_bases = original_numberOfBases + numberOfBasePairs_to_change self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases - 1) def _determine_numberOfBases_to_change(self): """ """ #The Property manager will be showing the current number #of base pairs (w. May be we can use that number directly here? #The following is safer to do so lets just recompute the #number of base pairs. (if it turns out to be slow, we will consider #using the already computed calue from the property manager original_numberOfBases = self.struct.getNumberOfBases() numberOfBasesToAddOrRemove = self.propMgr.numberOfBasesSpinBox.value()\ - original_numberOfBases if numberOfBasesToAddOrRemove > 0: #dna.modify will remove the first base pair it creates #(that basepair will only be used for proper alignment of the #duplex with the existing structure) So we need to compensate for #this basepair by adding 1 to the new number of base pairs. numberOfBasesToAddOrRemove += 1 return numberOfBasesToAddOrRemove def makeMenus(self): """ Create context menu for this command. (Build Dna mode) """ if not hasattr(self, 'graphicsMode'): return selobj = self.glpane.selobj if selobj is None: return self.Menu_spec = [] highlightedChunk = None if isinstance(selobj, Chunk): highlightedChunk = selobj if isinstance(selobj, Atom): highlightedChunk = selobj.molecule elif isinstance(selobj, Bond): chunk1 = selobj.atom1.molecule chunk2 = selobj.atom2.molecule if chunk1 is chunk2 and chunk1 is not None: highlightedChunk = chunk1 if highlightedChunk is None: return if self.hasValidStructure(): if (self.struct is highlightedChunk) or \ (self.struct is highlightedChunk.parent_node_of_class( self.assy.DnaStrand)): item = (("Currently editing %r"%self.struct.name), noop, 'disabled') self.Menu_spec.append(item) return #following should be self.struct.getDnaGroup or self.struct.getDnaGroup #need to formalize method name and then make change. dnaGroup = self.struct.parent_node_of_class(self.assy.DnaGroup) if dnaGroup is None: return if not dnaGroup is highlightedChunk.parent_node_of_class(self.assy.DnaGroup): item = ("Edit unavailable: Member of a different DnaGroup", noop, 'disabled') self.Menu_spec.append(item) return highlightedChunk.make_glpane_context_menu_items(self.Menu_spec, command = self)