summaryrefslogtreecommitdiff
path: root/cad/src/graphics/widgets/DynamicTip.py
blob: 25a64ce0626b698235720d55ad5a4029719f6c2c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# Copyright 2004-2008 Nanorex, Inc.  See LICENSE file for details.
"""
DynamicTip.py - supports dynamic, informative tooltips of highlighted objects in GLPane

History:
060817 Mark created dynamicTip class
060818 Ninad moved DynamicTip class into this file DynamicTip.py and added more

@author: Mark, Ninad
@version: $Id$
@copyright: 2004-2008 Nanorex, Inc.  See LICENSE file for details.

TODO: This needs refactoring into the part which has the mechanics
of displaying the tooltip at the right time (some of the code for which
must be in the glpane passed to the constructor, and is in fact only in
class GLPane), and the part which knows what the tooltip should contain.

One way would be to separate it into an abstract class and concrete subclass,
mostly just by splitting the existing methods between them (except that
maybeTip, mostly tooltip mechanics, has some model-specific knowledge),
but we'd then need to either tell GLPane where to get the model-specific
concrete class, or have it ask its .graphicsMode for that.

Alternatively, we could just have this class ask the glpane.graphicsMode
for the content of what/whether to display in the tooltip, and move that code
from this into a new method in GraphicsMode (adding a stub version to the
GraphicsMode API). That's probably simpler and better.

However we do it, when classifying modules, the tooltip mechanics part should
end up being classified in the same way as the GLPane, and the tooltip-text-
determining part in the same way as GraphicsMode.
"""

import math
import time

from PyQt4.Qt import QToolTip, QRect, QPoint

import foundation.env as env
from model.chem import Atom
from model.elements import Singlet
from model.bonds import Bond
from model.jigs import Jig
from geometry.VQT import vlen
from geometry.VQT import atom_angle_radians

from platform_dependent.PlatformDependent import fix_plurals

from utilities.Log import redmsg, quote_html

from utilities.debug_prefs import debug_pref
from utilities.debug_prefs import Choice_boolean_False

from utilities.prefs_constants import dynamicToolTipWakeUpDelay_prefs_key
from utilities.prefs_constants import dynamicToolTipAtomDistancePrecision_prefs_key
from utilities.prefs_constants import dynamicToolTipBendAnglePrecision_prefs_key
from utilities.prefs_constants import dynamicToolTipTorsionAnglePrecision_prefs_key
from utilities.prefs_constants import dynamicToolTipAtomChunkInfo_prefs_key
from utilities.prefs_constants import dynamicToolTipBondChunkInfo_prefs_key
from utilities.prefs_constants import dynamicToolTipAtomPosition_prefs_key
from utilities.prefs_constants import dynamicToolTipAtomDistanceDeltas_prefs_key
from utilities.prefs_constants import dynamicToolTipBondLength_prefs_key
from utilities.prefs_constants import dynamicToolTipAtomMass_prefs_key
from utilities.prefs_constants import dynamicToolTipVdwRadiiInAtomDistance_prefs_key

# russ 080715: For graphics debug tooltip.
from OpenGL.GL import GL_BACK
from OpenGL.GL import GL_DEPTH_COMPONENT
from OpenGL.GL import GL_FRONT
from OpenGL.GL import GL_READ_BUFFER
from OpenGL.GL import GL_RGBA
from OpenGL.GL import GL_STENCIL_INDEX
from OpenGL.GL import GL_UNSIGNED_BYTE
from OpenGL.GL import glGetInteger
from OpenGL.GL import glReadBuffer
from OpenGL.GL import glReadPixels
from OpenGL.GL import glReadPixelsi
from OpenGL.GL import glReadPixelsf

_last_tipText = None # only used for debugging

class DynamicTip: # Mark and Ninad 060817.
    """
    For the support of dynamic, informative tooltips of a highlighted object in the GLPane.
    """
    def __init__(self, glpane):
        """
        @param glpane: An instance of class GLPane (since only it has the necessary code
                       to set some timer-related attributes we depend on).
        """
        self.glpane = glpane

        # <toolTipShown> is a flag set to True when a tooltip is currently displayed for the
        # highlighted object under the cursor.
        self.toolTipShown = False

    def maybeTip(self, helpEvent):
        """
        Determines if this tooltip should be displayed. The tooltip will be displayed at
        helpEvent.globalPos() if an object is highlighted and the mouse hasn't moved for
        some period of time, called the "wake up delay" period, which is a user pref
        (not yet implemented in the Preferences dialog) currently set to 1 second.

        maybeTip() is called by GLPane.timerEvent() whenever the cursor is not moving to
        determine if the tooltip should be displayed.

        @param helpEvent: a QHelpEvent constructed by the caller
        @type helpEvent: QHelpEvent
        """
        # docstring used to also say:
        ## For more details about this member, see Qt documentation on QToolTip.maybeTip().
        # but this is unclear to me (since this class does not inherit from
        # QToolTip), so I removed it. [bruce 081208]

        debug = debug_pref("GLPane: graphics debug tooltip?",
                           Choice_boolean_False,
                           prefs_key = True )

        glpane = self.glpane
        selobj = glpane.selobj

        if debug:
            # russ 080715: Graphics debug tooltip.
            # bruce 081208/081211: revised, moved out of _getToolTipText,
            # made debug_pref.
            # note: we don't use glpane.MousePos since it's not reliable --
            # only some graphicsModes store it, and only in mouse press events.

            # Note: double buffering applies only to the color buffer,
            # not the stencil or depth buffers, which have only one copy.
            # The setting of GL_READ_BUFFER should have no effect on
            # glReadPixelsf from those buffers.
            # [bruce 081211 comment, based on Russ report of OpenGL doc]

            pos = helpEvent.pos()
            wX = pos.x()
            wY = glpane.height - pos.y() #review: off by 1??
            wZ = glReadPixelsf(wX, wY, 1, 1, GL_DEPTH_COMPONENT)[0][0]
            stencil = glReadPixelsi(wX, wY, 1, 1, GL_STENCIL_INDEX)[0][0]
            savebuff = glGetInteger(GL_READ_BUFFER)
            whichbuff = {GL_FRONT:"front", GL_BACK:"back"}.get(savebuff, "unknown")
            redraw_counter = env.redraw_counter
            # Pixel data is sign-wrapped, in spite of specifying unsigned_byte.
            def us(b):
                if b < 0:
                    return 256 + b
                else:
                    return b
            def pixvals(buff):
                glReadBuffer(buff)
                gl_format, gl_type = GL_RGBA, GL_UNSIGNED_BYTE
                rgba = glReadPixels( wX, wY, 1, 1, gl_format, gl_type )[0][0]
                return (
                    "rgba %u, %u, %u, %u" %
                    (us(rgba[0]), us(rgba[1]), us(rgba[2]), us(rgba[3]))
                 )
            def redifnot(v1, v2, text):
                if v1 != v2:
                    return redmsg(text)
                else:
                    return text
            front_pixvals = pixvals(GL_FRONT)
            back_pixvals = pixvals(GL_BACK)
            glReadBuffer(savebuff)      # restore the saved value
            tipText = (
                "env.redraw = %d; selobj = %s<br>" % (redraw_counter, quote_html(str(selobj)),) +
                    # note: sometimes selobj is an instance of _UNKNOWN_SELOBJ_class... relates to testmode bug from renderText
                    # (confirmed that renderText zaps stencil and that that alone causes no bug in other graphicsmodes)
                    # TODO: I suspect this can be printed even during rendering... need to print glpane variables
                    # which indicate whether we're doing rendering now, e.g. current_glselect, drawing_phase;
                    # also modkeys (sp?), glselect_wanted
                "mouse position (xy): %d, %d<br>" % (wX, wY,) +
                "depth %f, stencil %d<br>" % (wZ, stencil) +
                redifnot(whichbuff, "back",
                         "current read buffer: %s<br>" % whichbuff ) +
                redifnot(glpane.glselect_wanted, 0,
                         "glselect_wanted: %s<br>" % (glpane.glselect_wanted,) ) +
                redifnot(glpane.current_glselect, False,
                         "current_glselect: %s<br>" % (glpane.current_glselect,) ) +
                redifnot(glpane.drawing_phase, "?",
                         "drawing_phase: %s<br>" % (glpane.drawing_phase,) ) +
                "front: " + front_pixvals + "<br>" +
                redifnot(back_pixvals, front_pixvals,
                         "back:  " + back_pixvals )
             )
            global _last_tipText
            if tipText != _last_tipText:
                print
                print tipText
                _last_tipText = tipText
            pass # use tipText below

        else:

            # <motionlessCursorDuration> is the amount of time the cursor (mouse) has been motionless.
            motionlessCursorDuration = time.time() - glpane.cursorMotionlessStartTime

            # Don't display the tooltip yet if <motionlessCursorDuration> hasn't exceeded the "wake up delay".
            # The wake up delay is currently set to 1 second in prefs_constants.py. Mark 060818.
            if motionlessCursorDuration < env.prefs[dynamicToolTipWakeUpDelay_prefs_key]:
                self.toolTipShown = False
                return

            # If an object is not currently highlighted, don't display a tooltip.
            if not selobj:
                return

            # If the highlighted object is a singlet,
            # don't display a tooltip for it.
            if isinstance(selobj, Atom) and (selobj.element is Singlet):
                return

            if self.toolTipShown:
                # The tooltip is already displayed, so return.
                # Do not allow tip() to be called again or it will "flash".
                return

            tipText = self._getToolTipText()

            pass

        # show the tipText

        if not tipText:
            tipText = ""
            # This makes sure that dynamic tip is not displayed when
            # the highlightable object is 'unknown' to the dynamic tip class.
            # (From QToolTip.showText doc: "If text is empty the tool tip is hidden.")

        showpos = helpEvent.globalPos()

        if debug:
            # show it a little lower to avoid the cursor obscuring the tooltip.
            # (might be useful even when not debugging, depending on the cursor)
            # [bruce 081208]
            showpos = showpos + QPoint(0, 10)

        QToolTip.showText(showpos, tipText)  #@@@ ninad061107 works fine but need code review

        self.toolTipShown = True

    def _getToolTipText(self): # Mark 060818, Ninad 060818
        """
        Return the tooltip text to display, which depends on what is selected
        and what is highlighted (found by examining self.glpane).

        @return: tooltip text to be shown
        @rtype:  str

        Features implemented:
         - If nothing is selected, return the name of the highlighted object.
         - If one atom is selected, return the distance between it and the
           highlighted atom.
         - If two atoms are selected, return the angle between them and the
           highlighted atom.
         - Preferences for setting the precision (decimal place) for each
           measurement.
         - Preferences for displaying atom chunk info, bond chunk info, Atom
           distance Deltas, atom coordinates, bond length (nuclear distance),
           bond type.
         - Displays Jig info

        For later:
         - If three atoms are selected, return the torsion angle between them
           and the highlighted atom.
         - We also need to truncate long item info strings. For example, if
           an item has a very long name it should truncate it with 3 dots,
           like "item na...")
        """
        glpane = self.glpane

        #ninad060831 - First I defined the following in the _init method of this class. But the preferences were
        #not updated immediately when changed from prefs dialog. So I moved those definitions below and now it works fine

        self.atomDistPrecision = env.prefs[dynamicToolTipAtomDistancePrecision_prefs_key] #int
        self.bendAngPrecision  = env.prefs[dynamicToolTipBendAnglePrecision_prefs_key] #int
        self.torsionAngPrecision = env.prefs[dynamicToolTipTorsionAnglePrecision_prefs_key] #int
        self.isAtomChunkInfo  = env.prefs[dynamicToolTipAtomChunkInfo_prefs_key]#boolean
        self.isBondChunkInfo  = env.prefs[dynamicToolTipBondChunkInfo_prefs_key]#boolean
        self.isAtomPosition   = env.prefs[dynamicToolTipAtomPosition_prefs_key]#boolean
        self.isAtomDistDeltas = env.prefs[dynamicToolTipAtomDistanceDeltas_prefs_key]#boolean
        self.isBondLength     = env.prefs[dynamicToolTipBondLength_prefs_key] #boolean
        self.isAtomMass       = env.prefs[dynamicToolTipAtomMass_prefs_key] #boolean



        objStr = self.getHighlightedObjectInfo(self.atomDistPrecision)

        selectedAtomList = glpane.assy.getOnlyAtomsSelectedByUser()
        selectedJigList  = glpane.assy.getSelectedJigs()

        ppa2 = glpane.assy.ppa2 # previously picked atom
        ppa2Exists = self.lastPickedInSelAtomList(ppa2, selectedAtomList)

        ppa3 = glpane.assy.ppa3 #atom picked before ppa2
        ppa3Exists = self.lastTwoPickedInSelAtomList(ppa2, ppa3, selectedAtomList) #checks if *both* ppa2 and ppa3 exist

        if len(selectedAtomList) == 0:
            return objStr

        #ninad060818 Give the distance info if only one atom is selected in the glpane (and is not the highlighted one)
        #If a 'last picked' atom exists (and is still selected, then it returns distance between that last picked and highlighted
        #If the highlighted atom is also selected/last picked , only give atom info don't give '0' distance info"
        #Known bug: If many atoms selected, if ppa2 and ppa2 exists and if ppa2 is deleted, it doesn't display the distance between
        #highlighted and ppa3. (as of 060818 it doesn't even display the atom info ..but thats not a bug just NIY that I need to
        #handle somewhere else.

        elif isinstance(glpane.selobj, Atom) and (len(selectedAtomList) == 1 ):
            if self.getDistHighlightedAtomAndSelectedAtom(selectedAtomList,
                                                          ppa2,
                                                          self.atomDistPrecision):
                distStr = self.getDistHighlightedAtomAndSelectedAtom(selectedAtomList,
                                                                     ppa2,
                                                                     self.atomDistPrecision)
                return objStr + "<br>" + distStr
            else:
                return objStr

        #ninad060821 Give the angle info if only 2 atoms are selected (and the selection doesn't include highlighted atom)
        #if ppa2 and ppa3 both exist (and still selected) then it returns angle between them
        #If the highlighted atom is also selected/last picked/lasttolastpicked , only give atom and distance info don't give angle info
        #If distance info is not available for some reasons (e.g. no ppa2 or more than 2 atoms region selected  etc, return Distance info only)

        elif  isinstance(glpane.selobj, Atom) and ( len(selectedAtomList) == 2 or len(selectedAtomList) == 3):
            if self.getAngleHighlightedAtomAndSelAtoms(ppa2,
                                                       ppa3,
                                                       selectedAtomList,
                                                       self.bendAngPrecision):
                angleStr = \
                    self.getAngleHighlightedAtomAndSelAtoms(ppa2,
                                                            ppa3,
                                                            selectedAtomList,
                                                            self.bendAngPrecision)
                return objStr + "<br>" + angleStr
            else:
                if self.getDistHighlightedAtomAndSelectedAtom(selectedAtomList,
                                                              ppa2,
                                                              self.atomDistPrecision):
                    distStr = \
                        self.getDistHighlightedAtomAndSelectedAtom(selectedAtomList,
                                                                   ppa2,
                                                                   self.atomDistPrecision)
                    return objStr + "<br>" + distStr
                else:
                    return objStr

        #ninad060822 For all other cases, simply return the object info.
        else:
            return objStr #@@@ ninad060818 ...if we begin to support other objects (other than jig/chunk/bonds/atoms)
                                #then we need to retirn glpane.selobj

        """elif "three atoms are selected":
            self
            torsionStr = self.getTorsionHighlightedAtomAndSelAtoms()
            angleStr = self.getAngleHighlightedAtomAndSelAtoms()
            distStr = self.getDistHighlightedAtomAndSelectedAtom()
            return torsionStr + "<br>" + angleStr + "<br>" + distStr"""


    def getHighlightedObjectInfo(self, atomDistPrecision):
        """
        Returns the info such as name, id, xyz coordinates etc of the highlighed object.
        """
        glpane        = self.glpane
        atomposn      = None
        atomChunkInfo = None
        selobj = glpane.selobj

        #      ---- Atom Info ----
        if isinstance(selobj, Atom):
            atomInfoStr = selobj.getToolTipInfo(self.isAtomPosition,
                                                 self.isAtomChunkInfo,
                                                 self.isAtomMass,
                                                 atomDistPrecision)
            return atomInfoStr

        #       ----Bond Info----
        if isinstance(selobj, Bond):
            bondInfoStr = selobj.getToolTipInfo(self.isBondChunkInfo,
                                                 self.isBondLength,
                                                 atomDistPrecision)
            return  bondInfoStr

        #          ---- Jig Info ----
        if isinstance(selobj, Jig):
            jigStr = selobj.getToolTipInfo()
            return jigStr

        if isinstance(selobj, glpane.assy.Chunk):
            chunkStr = selobj.getToolTipInfo()
            return chunkStr

        #@@@ninad060818 In future if we support other object types in glpane, do we need a check for that?
        # e.g. else: return "unknown object" .


    def getDistHighlightedAtomAndSelectedAtom(self, selectedAtomList, ppa2, atomDistPrecision):
        """
        Returns the distance between the selected atom and the highlighted atom.
        If there is only one atom selected and is same as highlighed atom,
        then it returns an empty string.  (then the function calling this
        routine needs to handle that case.)
        """

        glpane          =  self.glpane
        selectedAtom    =  None
        atomDistDeltas  =  None
        #initial value of distStr. (an empty string)
        distStr = ''

        if len(selectedAtomList) > 2: # ninad060824 don't show atom distance info when there are more than 2 atoms selected. Fixes bug2225
            return False

        #ninad060821 It is posible that 2 atoms are selected and one is highlighted. This condition allows the function use in the conditional loop that shows angle between the selkected and highlighted atoms
        if  len(selectedAtomList) ==2 and glpane.selobj in selectedAtomList: #this means the highlighted object is also in this list
            i = selectedAtomList.index(glpane.selobj)
            if i == 0: #ninad060821 This is a clumsy way of knowing which atom is which. Okay for now since there are only 2 atoms
                selectedAtom = selectedAtomList[1]
            else:
                selectedAtom = selectedAtomList[0]

        if len(selectedAtomList) == 1:
            #ninad060821 disabled the case where many atoms are selected and there still exists a last picked.  I did this becasue
            #it is confusing. Example: I picked an atom. Now I region selected another atom (after pick operation) then I highlight an atom.
            #it shows the distance between the highlighed and the picked and not highlighted and region selected. This is not good
            #If we have a way to know when region select operation was performed (before or after) then we can implement it
            #again. Probably selectedAtomList maintains this record? Should we check the list to see if ppa2 comes before or after the
            # region selection?  It is complecated. Need to discuss with Bruce and Mark. Not implementing it right now.
            #This also invalidates the need to pass ppa2 as an arg to this function. Still keeping it until I hear back from Bruce/Mark

            #if ppa2:
                #selectedAtom = ppa2 #ninad060818 This handles a case when there are many atoms selected and there still exists a 'last picked' one
            #else:
                #selectedAtom = selectedAtomList[0] #buggy if there are more than 2 atoms selected. But its okay because I am handling it correctly elsewhere where (I am calling this function) ninad060821
            selectedAtom = selectedAtomList[0]

        xyz = glpane.selobj.posn()

         # round the distance value using atom distance precision preference ninad 060822

         #ninad060822: Note: In prefs constant.py, I am using for example--
         # ('atom_distance_precision', 'int', dynamicToolTipAtomDistancePrecision_prefs_key, 3)
         #Notice that the digit is not 3.0  but is simply 3 as its an integer.
         #I changed to to plain 3 because I got a Deprecation warning: integer arg expected, got float

        roundedDist = str(round(vlen(xyz - selectedAtom.posn()), atomDistPrecision))

        if env.prefs[dynamicToolTipVdwRadiiInAtomDistance_prefs_key]:
            rvdw1 = glpane.selobj.element.rvdw
            rvdw2 = selectedAtom.element.rvdw
            dist = vlen(xyz - selectedAtom.posn()) + rvdw1 + rvdw2
            roundedDistIncludingVDWString = ("2.Including Vdw radii:" \
                               " %s A") %(str(round(dist, atomDistPrecision)))
        else:
            roundedDistIncludingVDWString = ''


        #ninad060818 No need to display disance info if highlighed object and
        #lastpicked/ only selected object are identical
        if selectedAtom:
            if selectedAtom is not glpane.selobj:
                distStr = ("<font color=\"#0000FF\">Distance %s-%s :</font><br>"\
                           "1.Center to center:</font>"\
                           " %s A" %(glpane.selobj,
                                          selectedAtom,
                                          roundedDist))

                if roundedDistIncludingVDWString:
                    distStr += "<br>" + roundedDistIncludingVDWString

                atomDistDeltas = self.getAtomDistDeltas(self.isAtomDistDeltas,
                                                        atomDistPrecision,
                                                        selectedAtom)
                if atomDistDeltas:
                    distStr += "<br>" + atomDistDeltas

        return distStr

    def getAngleHighlightedAtomAndSelAtoms(self, ppa2, ppa3, selectedAtomList, bendAngPrecision):
        """
        Returns the angle between the last two selected atoms and the current highlighted atom.
        If the highlighed atom is also one of the selected atoms and there are only 2 selected atoms other than
        the highlighted one then it returns None.(then the function calling this routine needs to handle that case.)
        """
        glpane = self.glpane
        lastSelAtom = None
        secondLastSelAtom = None

        ppa3Exists = self.lastTwoPickedInSelAtomList(ppa2, ppa3, selectedAtomList) #checks if *both* ppa2 and ppa3 exist

        if  len(selectedAtomList) ==3 and glpane.selobj in selectedAtomList:
            if ppa3Exists and not (glpane.selobj is ppa2 or glpane.selobj is ppa3):
                lastSelAtom = ppa2
                secondLastSelAtom = ppa3
            else:
                #ninad060825 revised the following. Earlier code in v1.8 was correct but this one is simpler. Suggested by Bruce. I have tested it and is safe.
                tempAtomList =list(selectedAtomList)
                tempAtomList.remove(glpane.selobj)
                lastSelAtom = tempAtomList[0]
                secondLastSelAtom = tempAtomList[1]

        if len(selectedAtomList) == 2: #here I (ninad) don't care about whether itselected atom is also highlighted. It is handled below.
            if ppa3Exists:
                lastSelAtom = ppa2
                secondLastSelAtom = ppa3
            else:
                 lastSelAtom = selectedAtomList[0]
                 secondLastSelAtom = selectedAtomList[1]
            #ninad060821 No need to display angle info if highlighed object and lastpicked or secondlast picked
            #  object are identical
            if glpane.selobj in selectedAtomList:
                return False

        if lastSelAtom and secondLastSelAtom:
            angle = atom_angle_radians( glpane.selobj, lastSelAtom,secondLastSelAtom ) * 180 / math.pi
            roundedAngle = str(round(angle, bendAngPrecision))
            angleStr = fix_plurals("<font color=\"#0000FF\">Angle %s-%s-%s:</font> %s degree(s)"
            %(glpane.selobj, lastSelAtom, secondLastSelAtom, roundedAngle))
            return angleStr
        else:
            return False

    def getTorsionHighlightedAtomAndSelAtoms(self):
        """
        Return the torsion angle between the last 3 selected atoms and the highlighed atom,
        If the highlighed atom is also selected, it excludes it while finding the last 3 selected atoms.
        If the highlighed atom is also one of the selected atoms and there are only 2 selected  atoms other than
        the highlighted one then it returns None. (then the function calling this routine needs to handle that case.)
        """
        return False

    def lastPickedInSelAtomList(self, ppa2, selectedAtomList):
        """
        Checks whether the last atom picked (ppa2) exists in the atom list.

        @return: True if I{ppa2} is in the atom list.
        @rtype:  bool
        """
        if ppa2 in selectedAtomList:
            return True
        else:
            return False

    def lastTwoPickedInSelAtomList(self, ppa2, ppa3, selectedAtomList):
        """
        Checks whether *both* the last two picked atoms (ppa2 and ppa3) exist
        in I{selectedAtomList}.

        @return: True if both atoms exist in the atom list.
        @rtype:  bool
        """
        if (ppa2 and ppa3) in selectedAtomList:
            return True
        else:
            return False

    def lastThreePickedInSelAtomList(self, ppa2, ppa3, ppa4):
        """
        Checks whether *all* three picked atoms (ppa2 , ppa3 and ppa4) exist in the atom list.

        @return: True if all three exist in the atom list.
        @rtype:  bool

        @note: there is no ppa4 yet - ninad060818
        """
        pass

    def getAtomDistDeltas(self, isAtomDistDeltas, atomDistPrecision, selectedAtom):
        """
        Returns atom distance deltas (delX, delY, delZ) string if the
        'Show atom distance delta info' in dynamic tooltip is checked from
        the user prefs. Otherwise returns None.
        """
        glpane = self.glpane
        if isAtomDistDeltas:
            xyz        = glpane.selobj.posn()
            xyzSelAtom = selectedAtom.posn()
            deltaX = str(round(vlen(xyz[0] - xyzSelAtom[0]), atomDistPrecision))
            deltaY = str(round(vlen(xyz[1] - xyzSelAtom[1]), atomDistPrecision))
            deltaZ = str(round(vlen(xyz[2] - xyzSelAtom[2]), atomDistPrecision))
            atomDistDeltas = "<font color=\"#0000FF\">DeltaX:</font> " \
                           + deltaX + "<br>" \
                           + "<font color=\"#0000FF\">DeltaY:</font> " \
                           + deltaY + "<br>" \
                           + "<font color=\"#0000FF\">DeltaZ:</font> " \
                           + deltaZ
            return atomDistDeltas
        else:
            return None

# end