summaryrefslogtreecommitdiff
path: root/cad/src/graphics/widgets/ThumbView.py
blob: 3e71617c074b1ce9908b5c1960eaad4a0e348cec (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
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
# Copyright 2004-2009 Nanorex, Inc.  See LICENSE file for details.
"""
ThumbView.py - a simpler OpenGL widget, similar to GLPane
(which unfortunately has a lot of duplicated but partly modified
code copied from GLPane)

@author: Huaicai
@version: $Id$
@copyright: 2004-2009 Nanorex, Inc.  See LICENSE file for details.
"""

from Numeric import dot

from OpenGL.GL import GL_NORMALIZE
from OpenGL.GL import glEnable
from OpenGL.GL import GL_MODELVIEW
from OpenGL.GL import glMatrixMode
from OpenGL.GL import glLoadIdentity
from OpenGL.GL import glClearColor
from OpenGL.GL import GL_COLOR_BUFFER_BIT
from OpenGL.GL import GL_DEPTH_BUFFER_BIT
from OpenGL.GL import glClear
from OpenGL.GL import GL_STENCIL_INDEX
from OpenGL.GL import glReadPixelsi
from OpenGL.GL import GL_DEPTH_COMPONENT
from OpenGL.GL import glReadPixelsf
from OpenGL.GL import glPushMatrix
from OpenGL.GL import glSelectBuffer
from OpenGL.GL import GL_SELECT
from OpenGL.GL import glRenderMode
from OpenGL.GL import glInitNames
from OpenGL.GL import GL_CLIP_PLANE0
from OpenGL.GL import glClipPlane
from OpenGL.GL import GL_RENDER
from OpenGL.GL import glFlush
from OpenGL.GL import GL_STENCIL_BUFFER_BIT
from OpenGL.GL import GL_FALSE
from OpenGL.GL import GL_ALWAYS
from OpenGL.GL import glStencilFunc
from OpenGL.GL import GL_REPLACE
from OpenGL.GL import GL_TRUE
from OpenGL.GL import glDepthMask
from OpenGL.GL import GL_KEEP
from OpenGL.GL import glStencilOp
from OpenGL.GL import GL_STENCIL_TEST
from OpenGL.GL import glDisable
from OpenGL.GL import GL_PROJECTION
from OpenGL.GL import glPopMatrix
from OpenGL.GL import glDepthFunc
from OpenGL.GL import GL_LEQUAL

from OpenGL.GLU import gluUnProject

from PyQt4.Qt import Qt

from geometry.VQT import V, Q, A
from graphics.drawing.drawers import drawFullWindow
from graphics.drawing.gl_lighting import _default_lights
from graphics.drawing.gl_lighting import setup_standard_lights
from model.assembly import Assembly
import foundation.env as env
from utilities import debug_flags


from utilities.debug import print_compact_traceback

from utilities.constants import diTrueCPK
from utilities.constants import gray
from utilities.constants import bluesky, eveningsky, bg_seagreen, bgEVENING_SKY, bgSEAGREEN
from utilities.constants import GL_FAR_Z
from utilities.prefs_constants import bondpointHighlightColor_prefs_key
from utilities.prefs_constants import backgroundGradient_prefs_key
from utilities.prefs_constants import backgroundColor_prefs_key

from foundation.Group import Group
from model.chem import Atom
from model.elements import Singlet
from model.chunk import Chunk

from operations.pastables import find_hotspot_for_pasting

from graphics.widgets.GLPane_minimal import GLPane_minimal
from platform_dependent.PlatformDependent import fix_event_helper

class ThumbView(GLPane_minimal):
    """
    A simple version of OpenGL widget, which can be used to show a simple
    thumb view of models when loading models or color changing.
    General rules for multiple QGLWidget uses: make sure the rendering context
    is current. Remember makeCurrent() will be called implicitly before any
    initializeGL, resizeGL, paintGL virtual functions call. Ideally, this class
    should coordinate with class GLPane in some ways.
    """
    # Note: classes GLPane and ThumbView share lots of code,
    # which ought to be merged into their common superclass GLPane_minimal
    # [bruce 070914 comment; since then some of it has been merged, some
    #  still needs to be] [I merged a bunch more now -- bruce 080912]

    # class constants and/or default values of instance variables [not sure which are which]

    SIZE_FOR_glSelectBuffer = 500
        # different value from that in GLPane_minimal
        # [I don't know whether this matters -- bruce 071003 comment]

    shareWidget = None #bruce 051212
    always_draw_hotspot = False #bruce 060627

    _always_remake_during_movies = True #bruce 090224

    # default values of subclass-specific constants

    permit_draw_bond_letters = False #bruce 071023, overrides superclass

    def __init__(self, parent, name, shareWidget):
        """
        Constructs an instance of a Thumbview.
        """

        useStencilBuffer = False

        GLPane_minimal.__init__(self, parent, shareWidget, useStencilBuffer)

        self.elementMode = None

        #@@@Add the QGLWidget to the parentwidget's grid layout. This is done
        #here for improving the loading speed. Needs further optimization and
        #a better place to put this code if possible. -- Ninad 20070827
        try:
            parent.gridLayout.addWidget(self, 0, 0, 1, 1)
        except:
            print_compact_traceback("bug: Preview Pane's parent widget doesn't" \
            " have a layout. Preview Pane not added to the layout.")
            pass

        self.picking = False

        self.selectedObj = None

        #This enables the mouse bareMotion() event
        self.setMouseTracking(True)

        # clipping planes, as percentage of distance from the eye
        self.near = 0.66
        self.far = 2.0
        # start in perspective mode
        self.ortho = False #True

        self.scale # make sure superclass set this [bruce 080219]

        # default color and gradient values.
        self.backgroundColor = env.prefs[backgroundColor_prefs_key]
        self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ]


    def drawModel(self):
        """
        This is an abstract method for drawing self's "model".

        [subclasses which need to draw anything must override this method]
        """
        pass

    def _drawModel_using_DrawingSets(self): #bruce 090219
        """
        """
        def func():
            self.drawModel()
        self._call_func_that_draws_model( func,
                                          drawing_phase = 'main' )
        return

    def drawSelected(self, obj):
        """
        Draw the selected object.

        [subclasses can override this method]
        """
        pass

    def _drawSelected_using_DrawingSets(self, obj): #bruce 090219
        """
        """
        def func():
            self.drawSelected(obj)
        self._call_func_that_draws_model( func,
                                          drawing_phase = 'selobj',
                                          whole_model = False )
            ### REVIEW: should we use _call_func_that_draws_objects instead?
            # That requires passing a part which contains obj,
            # and I don't know for sure whether there is one, or how to find it
            # in all cases. Guess: this is desirable, but not yet essential.
            # [bruce 090219 comment]
        return

    def _get_assy(self): #bruce 080220
        """
        Return the assy which contains all objects we are drawing
        (or None if this is not possible).

        [subclasses must override this method]
        """
        return None

    def __get_assy(self): #bruce 080220
        # this glue method is a necessary kluge so that the following property
        # for self.assy will use a subclass's overridden version of _get_assy
        return self._get_assy()

    assy = property(__get_assy) #bruce 080220, for per-assy glname dict

    def _setup_lighting(self):
        """
        [private method]
        Set up lighting in the model.
        [Called from both initializeGL and paintGL.]
        """
        # note: there is some duplicated code in this method
        # in GLPane_lighting_methods (has more comments) and ThumbView,
        # but also significant differences. Should refactor sometime.
        # [bruce 060415/080912 comment]

        glEnable(GL_NORMALIZE)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

        #bruce 060415 moved following from ThumbView.initializeGL to this split-out method...
        #bruce 051212 revised lighting code to share prefs and common code with GLPane
        # (to fix bug 1200 and mitigate bugs 475 and 1158;
        #  fully fixing those would require updating lighting in all ThumbView widgets
        #  whenever lighting prefs change, including making .update calls on them,
        #  and is not planned for near future since it's easy enough to close & reopen them)
        try:
            lights = self.shareWidget._lights #bruce 060415 shareWidget --> self.shareWidget; presumably always failed before that
                ###@@@ will this fix some bugs about common lighting prefs??
        except:
            lights = _default_lights

        setup_standard_lights( lights, self.glprefs)
        return

    def resetView(self):
        """
        Reset the view.

        Subclass can override this method with different <scale>, so call
        this version in the overridden version.
        """
        self.pov = V(0.0, 0.0, 0.0)
        self.quat = Q(1, 0, 0, 0)

    def setBackgroundColor(self, color, gradient):
        """
        Set the background  to 'color' or 'gradient' (Sky Blue).
        """
        self.backgroundColor = color

        # Ninad and I discussed this and decided that the background should always be set to skyblue.
        # This issue has to do with Build mode's water surface introducing inconsistencies
        # with the thumbview background color whenever Build mode's bg color is solid.
        # Change to "if 0:" to have the thubview background match the current mode background.
        # This fixes bug 1229.  Mark 060116
        if 1:
            self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ]
        else:
            self.backgroundGradient = gradient

    def paintGL(self):
        """
        Called by QtGL when redrawing is needed. For every redraw, color &
        depth butter are cleared, view projection are reset, view location &
        orientation are also reset.
        """
        if not self.initialised:
            return

        self._call_whatever_waits_for_gl_context_current() #bruce 071103

        glDepthFunc( GL_LEQUAL)

        self.setDepthRange_setup_from_debug_pref()
        self.setDepthRange_Normal()

        from utilities.debug_prefs import debug_pref, Choice_boolean_False
        if debug_pref("always setup_lighting?", Choice_boolean_False):
            #bruce 060415 added debug_pref("always setup_lighting?"), in GLPane and ThumbView [KEEP DFLTS THE SAME!!];
            # see comments in GLPane_lighting_methods
            self._setup_lighting() #bruce 060415 added this call

        self.backgroundColor = env.prefs[backgroundColor_prefs_key]
        c = self.backgroundColor
        glClearColor(c[0], c[1], c[2], 0.0)
        del c
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        self.backgroundGradient = env.prefs[ backgroundGradient_prefs_key ]
        if self.backgroundGradient:

            glMatrixMode(GL_PROJECTION)
            glLoadIdentity()
            glMatrixMode(GL_MODELVIEW)
            glLoadIdentity()

            # Setting to blue sky (default), but might change to something else
            _bgGradient = bluesky
            if self.backgroundGradient == bgEVENING_SKY:
                _bgGradient = eveningsky
            if self.backgroundGradient == bgSEAGREEN:
                _bgGradient = bg_seagreen

            drawFullWindow(_bgGradient)# gradient color

        self._setup_projection()

        self._setup_modelview()
##        glMatrixMode(GL_MODELVIEW)
##        glLoadIdentity()
##        glTranslatef(0.0, 0.0, - self.vdist)
##        q = self.quat
##        glRotatef(q.angle * 180.0 / math.pi, q.x, q.y, q.z)
##        glTranslatef(self.pov[0], self.pov[1], self.pov[2])

        if self.model_is_valid():
            #bruce 080117 [testing this at start of paintGL to skip most of it]:
            # precaution (perhaps a bugfix); see similar code in GLPane.
            #
            # update, bruce 080220: this may have caused a bug by making this
            # not draw a blank-background graphics area when it has no model.
            # E.g. the partlib has no model when first entered.
            # Fixing this by only not drawing the model itself in that case [UNTESTED].
            # (The GLPane always has a model, so has no similar issue.)
            #
            # I'm not moving the coordinate transforms into this if statement,
            # since I don't know if they might be depended on by non-paintGL
            # drawing (e.g. for highlighting, which btw is called "selection"
            # in some method names and comments in this file). [bruce 080220]
            self._drawModel_using_DrawingSets()

    def mousePressEvent(self, event):
        """
        Dispatches mouse press events depending on shift and
        control key state.
        """
        ## Huaicai 2/25/05. This is to fix item 2 of bug 400: make this rendering context
        ## as current, otherwise, the first event will get wrong coordinates
        self.makeCurrent()

        buttons, modifiers = event.buttons(), event.modifiers()
        #print "Button pressed: ", but

        if 1:
            #bruce 060328 kluge fix of undo part of bug 1775 (overkill, but should be ok) (part 1 of 2)
            import foundation.undo_manager as undo_manager
            main_assy = env.mainwindow().assy
            self.__begin_retval = undo_manager.external_begin_cmd_checkpoint(main_assy, cmdname = "(mmkit)")

        if buttons & Qt.LeftButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.graphicsMode.leftShiftDown(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.graphicsMode.leftCntlDown(event)
            else:
                self.leftDown(event)

        if buttons & Qt.MidButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.graphicsMode.middleShiftDown(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.graphicsMode.middleCntlDown(event)
            else:
                self.middleDown(event)

        if buttons & Qt.RightButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.graphicsMode.rightShiftDown(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.graphicsMode.rightCntlDown(event)
            else:
                pass # self.rightDown(event)

    __begin_retval = None

    def mouseReleaseEvent(self, event):
        """
        Only used to detect the end of a freehand selection curve.
        """
        buttons, modifiers = event.buttons(), event.modifiers()

        #print "Button released: ", but

        if buttons & Qt.LeftButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.leftShiftUp(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.leftCntlUp(event)
            else:
                self.leftUp(event)

        if buttons & Qt.MidButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.graphicsMode.middleShiftUp(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.graphicsMode.middleCntlUp(event)
            else:
                self.middleUp(event)

        if buttons & Qt.RightButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.rightShiftUp(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.rightCntlUp(event)
            else:
                pass # self.rightUp(event)

        if 1:
            #bruce 060328 kluge fix of undo part of bug 1775 (part 2 of 2)
            import foundation.undo_manager as undo_manager
            main_assy = env.mainwindow().assy
            undo_manager.external_end_cmd_checkpoint(main_assy, self.__begin_retval)

        return

    def mouseMoveEvent(self, event):
        """
        Dispatches mouse motion events depending on shift and
        control key state.
        """
        ##self.debug_event(event, 'mouseMoveEvent')
        buttons, modifiers = event.buttons(), event.modifiers()

        if buttons & Qt.LeftButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.leftShiftDrag(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.leftCntlDrag(event)
            else:
                pass # self.leftDrag(event)

        elif buttons & Qt.MidButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.middleShiftDrag(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.middleCntlDrag(event)
            else:
                self.middleDrag(event)

        elif buttons & Qt.RightButton:
            if modifiers & Qt.ShiftModifier:
                pass # self.rightShiftDrag(event)
            elif modifiers & Qt.ControlModifier:
                pass # self.rightCntlDrag(event)
            else:
                pass # self.rightDrag(event)

        else:
            #Huaicai: To fix bugs related to multiple rendering contexts existed in our application.
            # See comments in mousePressEvent() for more detail.
            self.makeCurrent()
            self.bareMotion(event)

    def wheelEvent(self, event):
        buttons, modifiers = event.buttons(), event.modifiers()

        # The following copies some code from basicMode.Wheel, but not yet the call of rescale_around_point,
        # since that is not implemented in this class; it ought to be made a method of a new common superclass
        # of this class and GLPane (and there are quite a few methods of GLPane about which that can be said,
        # some redundantly implemented here and some not).
        # [bruce 060829 comment]
        #
        # update [bruce 070402 comment]:
        # sharing that code would now be a bit more complicated (but is still desirable),
        # since GLPane.rescale_around_point is now best called by basicMode.rescale_around_point_re_user_prefs.
        # The real lesson is that even ThumbViews ought to use some kind of "graphicsMode" (like full-fledged modes,
        # even if some aspects of them would not be used), to handle mouse bindings. But this is likely to be
        # nontrivial since full-fledged modes might have extra behavior that's inappropriate but hard to
        # turn off. So if we decide to make ThumbView zoom compatible with that of the main graphics area,
        # the easiest quick way is just to copy and modify rescale_around_point_re_user_prefs and basicMode.Wheel
        # into this class.

        dScale = 1.0/1200.0
        if modifiers & Qt.ShiftModifier:
            dScale *= 0.5
        if modifiers & Qt.ControlModifier:
            dScale *= 2.0
        self.scale *= 1.0 + dScale * event.delta()
            ### BUG: The scale variable needs to set a limit; otherwise, it will
            # set self.near = self.far = 0.0 because of machine precision,
            # which will cause OpenGL Error. [Huaicai 10/18/04]
            # NOTE: this bug may have been fixed in other defs of wheelEvent.
            # TODO: review, and fix it here too (or, better, use common code).
            # See also the longer comment above in this method.
            # [bruce 080917 addendum]
        self.updateGL()
        return

    def bareMotion(self, event):
        wX = event.pos().x()
        wY = self.height - event.pos().y()

        if self.selectedObj is not None:
            stencilbit = glReadPixelsi(wX, wY, 1, 1, GL_STENCIL_INDEX)[0][0]
            if stencilbit: # If it's the same highlighting object, no display change needed.
                return

        self.updateGL()

        self.selectedObj = self.select(wX, wY)
        self.highlightSelected(self.selectedObj)

        return False # russ 080527

    def leftDown(self, event):
        pass

    def leftUp(self, event):
        pass

    def middleDown(self, event):
        pos = event.pos()
        self.trackball.start(pos.x(), pos.y())
        self.picking = True
        return

    def middleDrag(self, event):
        if self.picking:
            pos = event.pos()
            q = self.trackball.update(pos.x(), pos.y())
            self.quat += q
            self.updateGL()
        return

    def middleUp(self, event):
        self.picking = False
        return

    def select(self, wX, wY):
        """
        Use the OpenGL picking/selection to select any object. Return the
        selected object, otherwise, return None. Restore projection and
        modelview matrices before returning.
        """
        ### NOTE: this code is similar to (and was copied and modified from)
        # GLPane_highlighting_methods.do_glselect_if_wanted, but also differs
        # in significant ways (too much to make it worth merging, unless we
        # decide to merge the differing algorithms as well). It's one of
        # several instances of hit-test code that calls glRenderMode.
        # [bruce 060721/080917 comment]
        wZ = glReadPixelsf(wX, wY, 1, 1, GL_DEPTH_COMPONENT)
        gz = wZ[0][0]

        if gz >= GL_FAR_Z: # Empty space was clicked
            return None

        pxyz = A(gluUnProject(wX, wY, gz))
        pn = self.out
        pxyz -= 0.0002 * pn
            # Note: if this runs before the model is drawn, this can have an
            # exception "OverflowError: math range error", presumably because
            # appropriate state for gluUnProject was not set up. That doesn't
            # normally happen but can happen due to bugs (no known open bugs
            # of that kind).
            # Sometimes our drawing area can become "stuck at gray",
            # and when that happens, the same exception can occur from this line.
            # Could it be that too many accidental mousewheel scrolls occurred
            # and made the scale unreasonable? (To mitigate, we should prevent
            # those from doing anything unless we have a valid model, and also
            # reset that scale when loading a new model (latter is probably
            # already done, but I didn't check). See also the comments
            # in def wheelEvent.) [bruce 080220 comment]
        dp = - dot(pxyz, pn)

        # Save projection matrix before it's changed.
        glMatrixMode(GL_PROJECTION)
        glPushMatrix()

        current_glselect = (wX, wY, 1, 1)
        self._setup_projection(glselect = current_glselect)

        glSelectBuffer(self.SIZE_FOR_glSelectBuffer)
        glRenderMode(GL_SELECT)
        glInitNames()
        glMatrixMode(GL_MODELVIEW)
        # Save model view matrix before it's changed.
        glPushMatrix()

        # Draw model using glRenderMode(GL_SELECT) as set up above
        try:
            glClipPlane(GL_CLIP_PLANE0, (pn[0], pn[1], pn[2], dp))
            glEnable(GL_CLIP_PLANE0)
            self._drawModel_using_DrawingSets()
        except:
            #bruce 080917 fixed predicted bugs in this except clause (untested)
            print_compact_traceback("exception in ThumbView._drawModel_using_DrawingSets() during GL_SELECT; ignored; restoring matrices: ")
            glDisable(GL_CLIP_PLANE0)
            glMatrixMode(GL_PROJECTION)
            glPopMatrix()
            glMatrixMode(GL_MODELVIEW)
            glPopMatrix()
            glRenderMode(GL_RENDER)
            return None
        else:
            glDisable(GL_CLIP_PLANE0)
            # Restore model/view matrix
            glPopMatrix()

        # Restore projection matrix and set matrix mode to Model/View
        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        glMatrixMode(GL_MODELVIEW)

        glFlush()

        hit_records = list(glRenderMode(GL_RENDER))
        ## print "%d hits" % len(hit_records)
        for (near, far, names) in hit_records:
            ## print "hit record: near, far, names:", near, far, names
            # note from testing: near/far are too far apart to give actual depth,
            # in spite of the 1-pixel drawing window (presumably they're vertices
            # taken from unclipped primitives, not clipped ones).
            ### REVIEW: this just returns the first candidate object found.
            # The clip plane may restrict the set of candidates well enough to
            # make sure that's the right one, but this is untested and unreviewed.
            # (And it's just my guess that that was Huaicai's intention in
            #  setting up clipping, since it's not documented. I'm guessing that
            #  the plane is just behind the hitpoint, but haven't confirmed this.)
            # [bruce 080917 comment]
            if names:
                name = names[-1]
                assy = self.assy
                obj = assy and assy.object_for_glselect_name(name)
                    #k should always return an obj
                return obj
        return None # from ThumbView.select

    def highlightSelected(self, obj):
        # TODO: merge with GLPane (from which this was copied and modified)
        """
        Highlight the selected object <obj>. In the mean time, we do stencil test to
        update stencil buffer, so it can be used to quickly test if pick is still
        on the same <obj> as last test.
        """
        if not obj:
            return
        if not isinstance(obj, Atom) or (obj.element is not Singlet):
            return

        self._preHighlight()

        self._drawSelected_using_DrawingSets(obj)

        self._endHighlight()

        glFlush()
        self.swapBuffers()
        return

    def _preHighlight(self): ### TODO: rename; move into GLPane_minimal, use in GLPane.py
        """
        Change OpenGL settings to prepare for highlighting.
        """
        self.makeCurrent()
        glClear(GL_STENCIL_BUFFER_BIT)

        glDepthMask(GL_FALSE) # turn off depth writing (but not depth test)
        ## glDisable(GL_DEPTH_TEST)
        glStencilFunc(GL_ALWAYS, 1, 1)
        glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
        glEnable(GL_STENCIL_TEST)

        self.setDepthRange_Highlighting()

        glMatrixMode(GL_MODELVIEW)
        return

    def _endHighlight(self):
        """
        Restore OpenGL settings changed by _preHighlight to standard values.
        """
        glDepthMask(GL_TRUE)
        ## glEnable(GL_DEPTH_TEST)
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP)
        glDisable(GL_STENCIL_TEST)

        self.setDepthRange_Normal()

        glMatrixMode(GL_MODELVIEW)
        return

    def saveLastView(self): #bruce 060627 for compatibility with GLPane (for sake of assy.update_parts)
        pass

    def forget_part(self, part): #bruce 060627 for compatibility with GLPane (for sake of chunk.kill)
        pass

    pass # end of class ThumbView

# ==

class ElementView(ThumbView):
    """
    Element graphical display class.
    """
    # note: as of 080403 this is only used by elementColors.py and elementSelector.py
    def __init__(self, parent, name, shareWidget = None):
        ThumbView.__init__(self, parent, name, shareWidget)
        self.scale = 2.0 #5.0 ## the possible largest rvdw of all elements
        self.pos = V(0.0, 0.0, 0.0)
        self.mol = None

        ## Dummy attributes. A kludge, just try to make other code
        ##  think it looks like a glpane object.
        self.displayMode = 0
        self.selatom = None

    def resetView(self, scale = 2.0):
        """
        Reset current view.
        """
        ThumbView.resetView(self)
        self.scale = scale

    def drawModel(self):
        """
        The method for element drawing.
        """
        if self.mol:
            self.mol.draw(self, None)

    def model_is_valid(self): #bruce 080117
        """
        whether our model is currently valid for drawing
        [overrides GLPane_minimal method]
        """
        return self.mol and self.mol.assy.assy_valid

    def _get_assy(self):
        """
        [overrides ThumbView method]
        """
        return self.mol and self.mol.assy

    def refreshDisplay(self, elm, dispMode = diTrueCPK):
        """
        Display the new element or the same element but new display mode.
        """
        self.makeCurrent()
        self.mol = self.constructModel(elm, self.pos, dispMode)
        self.updateGL()

    def updateColorDisplay(self, elm, dispMode = diTrueCPK):
        """
        Display the new element or the same element but new display mode.
        """
        self.makeCurrent()
        self.mol = self.constructModel(elm, self.pos, dispMode)
        self.updateGL()


    def constructModel(self, elm, pos, dispMode):
        """
        This is to try to repeat what 'make_Atom_and_bondpoints()' method does,
        but hope to remove some stuff not needed here.
        The main purpose is to build the geometry model for element display.

        @param elm: An object of class Elem
        @param elm: L{Elem}

        @param dispMode: the display mode of the atom
        @type  dispMode: int

        @return: the Chunk which contains the geometry model.
        @rtype: L{Chunk}
        """
        assy = Assembly(None, run_updaters = False)
        assy.set_glpane(self) # sets .o and .glpane
        mol = Chunk(assy, 'dummy')
        atm = Atom(elm.symbol, pos, mol)
        atm.display = dispMode
        ## bruce 050510 comment: this is approximately how you should change the atom type (e.g. to sp2) for this new atom:
        ## atm.set_atomtype_but_dont_revise_singlets('sp2')
        ## see also atm.element.atomtypes -> a list of available atomtype objects for that element
        ## (which can be passed to set_atomtype_but_dont_revise_singlets)
        atm.make_bondpoints_when_no_bonds()
        return mol

    def drawSelected(self, obj):
        """
        Override the parent version. Specific drawing code for the object.
        """
        if isinstance(obj, Atom) and (obj.element is Singlet):
            obj.draw_in_abs_coords(self, env.prefs[bondpointHighlightColor_prefs_key])

    pass # end of class ElementView

class MMKitView(ThumbView):
    """
    Currently used as the GLWidget for the graphical display and manipulation
    for element/clipboard/part. Initial attempt was to subclass this for each
    of above type models, but find trouble to dynamically change the GLWidget
    when changing tab page.
    """
    # note: as of 080403 this is constructed only by PM_PreviewGroupBox.py
    # and required (assert isinstance(self.elementViewer, MMKitView)) by
    # PM_Clipboard.py, PM_MolecularModelingKit.py, and PM_PartLib.py.
    # I think that means it is used for MMKit atoms, PasteFromClipboard_Command clipboard,
    # and PartLib parts. [bruce 080403 comment]
    always_draw_hotspot = True
        #bruce 060627 to help with bug 2028
        # (replaces a horribe kluge in old code which broke a fix to that bug)

    def __init__(self, parent, name, shareWidget = None):
        ThumbView.__init__(self, parent, name, shareWidget)
        self.scale = 2.0
        self.pos = V(0.0, 0.0, 0.0)
        self.model = None

        ## Dummy attributes. A kludge, just try to make other code
        ##  think it looks like a glpane object.
        self.displayMode = 0
        self.selatom = None

        self.hotspotAtom = None #The current hotspot singlet for the part
        self.lastHotspotChunk = None # The previous chunk of the hotspot for the part

        hybrid_type_name = None
        elementMode = True  #Used to differentiate elment page versus clipboard/part page

    def drawModel(self):
        """
        The method for element drawing.
        """
        if self.model:
            if isinstance(self.model, Chunk) or \
               isinstance(self.model, Group):
                self.model.draw(self, None)
            else: ## Assembly
                self.model.draw(self)
        return

    def model_is_valid(self): #bruce 080117, revised 080220, 080913
        """
        whether our model is currently valid for drawing
        [overrides GLPane_minimal method]
        """
        return self.assy and self.assy.assy_valid

    def _get_assy(self): #bruce 080220
        """
        [overrides ThumbView method]
        """
        if self.model:
            if isinstance(self.model, Chunk) or \
               isinstance(self.model, Group):
                assy = self.model.assy
                    # Note: assy can be None, if the dna updater kills a bare Ax,
                    # since its chunk is then killed and its .assy set to None.
                    # But the following is too verbose to be useful; also it
                    # seemingly happens even for Ss3 (why??).
                    # Todo: check if it's due to Ax (ok to not print it here)
                    # or to something we didn't expect (needs print so as not
                    # to risk hiding other bugs).
                ## if res is None:
                ##     print "dna updater fyi: in model_is_valid: " \
                ##        "%r.model %r has .assy None" % \
                ##        (self, self.model)
                return assy
            else: ## self.model is an Assembly
                return self.model
        return None

    def refreshDisplay(self, elm, dispMode = diTrueCPK):
        """
        Display the new element or the same element but new display mode.
        """
        self.makeCurrent()
        self.model = self.constructModel(elm, self.pos, dispMode)
        self.updateGL()

    def changeHybridType(self, name):
        self.hybrid_type_name = name

    def resetView(self):
        """
        Reset current view.
        """
        ThumbView.resetView(self)
        self.scale = 2.0

    def drawSelected(self, obj):
        """
        Override the parent version. Specific drawing code for the object.
        """
        if isinstance(obj, Atom) and (obj.element is Singlet):
            obj.draw_in_abs_coords(self, env.prefs[bondpointHighlightColor_prefs_key])
        return

    def constructModel(self, elm, pos, dispMode):
        """
        This is to try to repeat what 'make_Atom_and_bondpoints()' method does,
        but hope to remove some stuff not needed here.
        The main purpose is to build the geometry model for element display.

        @param elm: An object of class Elem
        @param elm: L{Elem}

        @param dispMode: the display mode of the atom
        @type  dispMode: int

        @return: the Chunk which contains the geometry model.
        @rtype: L{Chunk}
        """
        assy = Assembly(None, run_updaters = False)
        assy.set_glpane(self) # sets .o and .glpane
        mol = Chunk(assy, 'dummy')
        atm = Atom(elm.symbol, pos, mol)
        atm.display = dispMode
        ## bruce 050510 comment: this is approximately how you should change the atom type (e.g. to sp2) for this new atom:
        if self.hybrid_type_name:
            atm.set_atomtype_but_dont_revise_singlets(self.hybrid_type_name)
        ## see also atm.element.atomtypes -> a list of available atomtype objects for that element
        ## (which can be passed to set_atomtype_but_dont_revise_singlets)
        atm.make_bondpoints_when_no_bonds()

        self.elementMode = True
        return mol

    def leftDown(self, event):
        """
        When in clipboard mode, set hotspot if a Singlet is highlighted.
        """
        if self.elementMode: return

        obj = self.selectedObj
        if isinstance(obj, Atom) and (obj.element is Singlet):
            mol = obj.molecule
            if not mol is self.lastHotspotChunk:
                if self.lastHotspotChunk: # Unset previous hotspot [bruce 060629 fix bug 1974 -- only if in same part]
                    if mol.part is self.lastHotspotChunk.part and mol.part is not None:
                        # Old and new hotspot chunks are in same part. Unset old hotspot,
                        # so as to encourage there to be only one per Part.
                        #   This should happen when you try to make more than one hotspot in one
                        # library part or clipboard item, using the MMKit to make both.
                        #   It might make more sense for more general code in Part to prevent
                        # more than one hotspot per part... but we have never decided whether
                        # that would be a good feature. (I have long suspected that hotspots
                        # should be replaced by some sort of jig, to give more control....)
                        #   I don't know if this case can ever happen as of now, since multichunk
                        # clipboard items aren't shown in MMKit -- whether it can happen now
                        # depends on whether any multichunk library parts have bondpoints on
                        # more than one chunk. [bruce 060629]
                        if env.debug() and self.lastHotspotChunk.hotspot: #bruce 060629 re bug 1974
                            print "debug: unsetting hotspot of %r (was %r)" % \
                                  (self.lastHotspotChunk, self.lastHotspotChunk.hotspot)
                        self.lastHotspotChunk.set_hotspot(None)
                    else:
                        # Don't unset hotspot in this case (doing so was causing bug 1974).
                        if env.debug() and self.lastHotspotChunk.hotspot:
                            print "debug: NOT unsetting hotspot of %r" % (self.lastHotspotChunk, )
                        pass
                self.lastHotspotChunk = mol
                    # [as of 060629, the only purpose of this is to permit the above code to unset it in some cases]

            mol.set_hotspot(obj)

            if 1:
                #bruce 060328 fix gl_update part of bug 1775 (the code looks like that was a bug forever, don't know for sure)
                main_glpane = env.mainwindow().glpane
                if mol.part is main_glpane.part:
                    main_glpane.gl_update()

            self.hotspotAtom = obj
            self.updateGL()
        return

    def gl_update(self): #bruce 070502 bugfix (can be called when ESPImage jigs appear in a partlib part)
        self.updateGL() #k guess at correct/safe thing to do
        return

    def gl_update_highlight(self): #bruce 070626 precaution (not sure if any code will call this)
        self.gl_update()
        return

    def gl_update_for_glselect(self): #bruce 070626 precaution (not sure if any code will call this)
        self.gl_update()
        return

    def updateModel(self, newObj):
        """
        Set new chunk or Assembly for display.
        """
        self.model = newObj

        #Reset hotspot related stuff for a new Assembly
        if isinstance(newObj, Assembly):
            self._find_and_set_hotSpotAtom_in_new_model(newObj)

        self._fitInWindow()
        self.elementMode = False
        self.updateGL()

    def _find_and_set_hotSpotAtom_in_new_model(self, newModel):
        """
        If the model being viewed in the thumbView window already has a hotspot,
        set it as self.hotSpotAtom (which then will be used by client code)

        @see: self.updateModel
        @Note that , in self.leftDown, we can actually temporarily change the
        the hotspot for the partlib model. But then if you view another part and
        go back to this model, the hotspot will be reset to the one that already
        exists in the model (or None if one doesn't exist)
        """
        assert isinstance(newModel, Assembly)
        chunkList = []
        def func(node):
            if isinstance(node, Chunk):
                chunkList.append(node)
        newModel.part.topnode.apply2all(func)


        ok = False
        if len(chunkList) == 1:
            ok, hotspot_or_whynot = find_hotspot_for_pasting(chunkList[0])
        elif len(chunkList) > 1:
            for chunk in chunkList:
                ok, hotspot_or_whynot = find_hotspot_for_pasting(chunk)
                if ok:
                    break
        if ok:
            self.hotspotAtom = hotspot_or_whynot
            self.lastHotspotChunk = self.hotspotAtom.molecule
        else:
            self.hotspotAtom = None
            self.lastHotspotChunk = None
        return

    def setDisplay(self, disp):
        self.displayMode = disp
        return

    def _fitInWindow(self):
        if not self.model:
            return

        self.quat = Q(1, 0, 0, 0)

        if isinstance(self.model, Chunk):
            self.model._recompute_bbox()
            bbox = self.model.bbox
        else: ## Assembly
            part = self.model.part
            bbox = part.bbox
        self.scale = bbox.scale()

        # guess: the following is a KLUGE for width and height
        # being the names of Qt superclass methods
        # but also being assigned to ints by our own code.
        # I don't know why it could ever be needed, since resizeGL
        # sets them -- maybe this can be called before that ever is??
        # [bruce 080912 comment]
        if isinstance(self.width, int):
            width = self.width
        else:
            width = float(self.width())
        if isinstance(self.height, int):
            height = self.height
        else:
            height = float(self.height())

        aspect = width / height
            # REVIEW: likely bug: integer division is possible [bruce 080912 comment]

        ##aspect = float(self.width) / self.height
        if aspect < 1.0:
            self.scale /= aspect
        center = bbox.center()
        self.pov = V(-center[0], -center[1], -center[2])

    pass # end of class MMKitView

# end