summaryrefslogtreecommitdiff
path: root/cad/src/graphics/widgets/GLPane.py
blob: 5b08845b33faaa3864e2c9a48ae4050c161b4aaa (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
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
# Copyright 2004-2009 Nanorex, Inc.  See LICENSE file for details.
"""
GLPane.py -- NE1's main model view. A subclass of Qt's OpenGL widget, QGLWidget.

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

NOTE: If you add code to class GLPane, please carefully consider
whether it belongs in one of its mixin superclasses (GLPane_*.py)
instead of in the main class in this file.

These separate files are topic-specific and should be kept as self-contained
as possible, in methods, attributes, helper functions, and imports.
(Someday, some of them should evolve into separate cooperating
objects and not be part of GLPane's class hierarchy at all.)

(Once the current refactoring is complete, most new code will belong
 in one of those superclass files, not in this file. [bruce 080912])

Module classification: [bruce 080104]

It's graphics/widgets, but this is not as obvious as it sounds.
It is "optimistic", as if we'd already refactored -- it's not fully
accurate today.

Refactoring needed: [bruce 080104]

- split into several classes (either an inheritance tree, or cooperating
objects); [080910 doing an initial step to this: splitting into mixins
in their own files]

- move more common code into GLPane_minimal [mostly done, 080914 or so,
except for GL_SELECT code, common to several files];

Some of this will make more practical some ways of optimizing the graphics code.

History:

Mostly written by Josh; partly revised by Bruce for mode code revision, 040922-24.
Revised by many other developers since then (and perhaps before).

bruce 080910 splitting class GLPane into several mixin classes in other files.

bruce 080925 removed support for GLPANE_IS_COMMAND_SEQUENCER
"""

# TEST_DRAWING has been moved to GLPane_rendering_methods

_DEBUG_SET_SELOBJ = False # do not commit with true

import sys

from OpenGL.GL import GL_STENCIL_BITS
from OpenGL.GL import glGetInteger

from command_support.GraphicsMode_API import GraphicsMode_interface # for isinstance assertion

import foundation.env as env

from utilities import debug_flags

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

from utilities.debug import print_compact_traceback, print_compact_stack

from utilities.prefs_constants import compassPosition_prefs_key
from utilities.prefs_constants import defaultProjection_prefs_key
from utilities.prefs_constants import startupGlobalDisplayStyle_prefs_key
from utilities.prefs_constants import startup_GLPane_scale_prefs_key
from utilities.prefs_constants import GLPane_scale_for_atom_commands_prefs_key
from utilities.prefs_constants import GLPane_scale_for_dna_commands_prefs_key

from utilities.constants import diDEFAULT
from utilities.constants import diDNACYLINDER
from utilities.constants import diPROTEIN
from utilities.constants import default_display_mode

from utilities.GlobalPreferences import pref_show_highlighting_in_MT

from graphics.widgets.GLPane_lighting_methods import GLPane_lighting_methods
from graphics.widgets.GLPane_frustum_methods import GLPane_frustum_methods
from graphics.widgets.GLPane_event_methods import GLPane_event_methods
from graphics.widgets.GLPane_rendering_methods import GLPane_rendering_methods
from graphics.widgets.GLPane_highlighting_methods import GLPane_highlighting_methods
from graphics.widgets.GLPane_text_and_color_methods import GLPane_text_and_color_methods
from graphics.widgets.GLPane_stereo_methods import GLPane_stereo_methods
from graphics.widgets.GLPane_view_change_methods import GLPane_view_change_methods

from graphics.widgets.GLPane_minimal import GLPane_minimal

from foundation.changes import SubUsageTrackingMixin
from graphics.widgets.GLPane_mixin_for_DisplayListChunk import GLPane_mixin_for_DisplayListChunk

from PyQt4.Qt  import QApplication, QCursor, Qt

# ==

class GLPane(
    # these superclasses need to come first, since they need to
    # override methods in GLPane_minimal:
    GLPane_lighting_methods, # needs to override _setup_lighting
    GLPane_frustum_methods, # needs to override is_sphere_visible etc
    GLPane_event_methods, # needs to override makeCurrent, setCursor,
        # and many *Event methods (not sure if GLPane_minimal defines
        # those, so it may not matter)
    GLPane_rendering_methods, # needs to override paintGL, several more

    # these don't yet need to come first, but we'll put them first
    # anyway in case someone adds a default def of some method
    # into GLPane_minimal in the future:
    GLPane_highlighting_methods,
    GLPane_text_and_color_methods,
    GLPane_stereo_methods,
    GLPane_view_change_methods,

    GLPane_minimal, # the "main superclass"; inherits QGLWidget

    SubUsageTrackingMixin,
    GLPane_mixin_for_DisplayListChunk
    ):
    """
    Widget for OpenGL graphics and associated mouse/key input,
    with lots of associated/standard behavior and helper methods.

    Note: if you want to add code to this class, consider whether it
    ought to go into one of the mixin superclasses listed above.
    (Once the current refactoring is complete, most new code will belong
     in one of those superclasses, not in this file. [bruce 080912])
    See module docstring for more info.

    Qt-called methods are overridden in some superclasses:
    * GLPane_event_methods overrides makeCurrent, setCursor, and many *Event methods
    * this class overrides paintGL, resizeGL, initializeGL

    Effectively a singleton object:

    * owned by main window (perhaps indirectly via class PartWindow)

    * never remade duringan NE1 session

    * contains public references to other singletons
      (e.g. win (permanent), assy (sometimes remade))

    * some old code stores miscellaneous attributes
      inside it (e.g. shape, stored by Build Crystal)

    A few of the GLPane's public attributes:

    * several "point of view" attributes (some might be inherited
      from superclass GLPane_minimal)

    * graphicsMode - an instance of GraphicsMode_interface
    """
    # Note: classes GLPane and ThumbView still share lots of code,
    # which ought to be merged into their common superclass GLPane_minimal.
    # [bruce 070914/080909 comment]

    always_draw_hotspot = False #bruce 060627; not really needed, added for compatibility with class ThumbView

    permit_shaders = True #bruce 090224

    assy = None #bruce 080314

    # the stereo attributes are maintained by the methods in
    # our superclass GLPane_stereo_methods, and used in that class,
    # and GLPane_rendering_methods, and GLPane_event_methods.
    stereo_enabled = False # piotr 080515 - stereo disabled by default
    stereo_images_to_draw = (0,)
    current_stereo_image = 0

    # note: is_animating is defined and maintained in our superclass
    # GLPane_view_change_methods
    ## self.is_animating = False

    _resize_just_occurred = False #bruce 080922

    #For performance reasons, whenever the global display style
    #changes, the current cursor is replaced with an hourglass cursor.
    #The following flag determines whether to turn off this wait cursor.
    #(It is turned off after global display style operation is complete.
    # see self._setWaitCursor_globalDisplayStyle().) [by Ninad, late 2008]
    _waitCursor_for_globalDisplayStyleChange = False

    def __init__(self, assy, parent = None, name = None, win = None):
        """
        """
        self.name = name or "glpane" # [bruce circa 080910 revised this; probably not used]
        shareWidget = None
        useStencilBuffer = True

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

        self.partWindow = parent # used in GLPane_event_methods superclass

        self._nodes_containing_selobj = []
            # will be set during set_selobj to a list of 0 or more nodes
            # [bruce 080507 new feature]
        self._nodes_containing_selobj_is_from_selobj = None
            # records which selobj was used to set _nodes_containing_selobj

        self.stencilbits = 0 # conservative guess, will be set to true value below

        if not self.format().stencil():
            # It's apparently too early to also test "or glGetInteger(GL_STENCIL_BITS) < 1" --
            # that glGet returns None even when the bits are actually there
            # (on my system this is 8 when tested later). Guess: this won't work until
            # the context is initialized.
            msg = ("Warning: your graphics hardware did not provide an OpenGL stencil buffer.\n"
                   "This will slow down some graphics operations.")
            ## env.history.message( regmsg( msg)) -- too early for that to work (need to fix that sometime, to queue the msg)
            print msg
            if debug_flags.atom_debug:
                print "atom_debug: details of lack of stencil bits: " \
                      "self.format().stencil() = %r, glGetInteger(GL_STENCIL_BITS) = %r" % \
                      ( self.format().stencil() , glGetInteger(GL_STENCIL_BITS) )
                    # Warning: these values can be False, None -- I don't know why, since None is not an int!
                    # But see above for a guess. [bruce 050610]
            pass
        else:
            ## self.stencilbits = int( glGetInteger(GL_STENCIL_BITS) ) -- too early!
            self.stencilbits = 1 # conservative guess that if we got the buffer, it has at least one bitplane
                #e could probably be improved by testing this in initializeGL or paintGL (doesn't matter yet)
##            if debug_flags.atom_debug:
##                print "atom_debug: glGetInteger(GL_STENCIL_BITS) = %r" % ( glGetInteger(GL_STENCIL_BITS) , )
            pass

        # [bruce 050419 new feature:]
        # The current Part to be displayed in this GLPane.
        # Logically this might not be the same as it's assy's current part, self.assy.part,
        # though in initial implem it will be the same except
        # when the part is changing... but the brief difference is important
        # since that's how the GLPane knows which previous part to store its
        # current view attributes in, before grabbing them from the new current part.
        # But some code might (incorrectly in principle, ok for now)
        # use self.assy.part when it should be using self.part.
        # The only thing we're sure self.part must be used for is to know in which
        # part the view attributes belong.
        self.part = None

        # Other "current preference" attributes. ###e Maybe some of these should
        # also be part-specific and/or saved in the mmp file? [bruce 050418]

        # == User Preference initialization ==

        # Get glpane related settings from prefs db.
        # Default values are set in "prefs_table" in prefs_constants.py.
        # Mark 050919.

        self._init_background_from_prefs() # defined in GLPane_text_and_color_methods

        self.compassPosition = env.prefs[compassPosition_prefs_key]
            ### TODO: eliminate self.compassPosition (which is used and set,
            # in sync with this pref, in ne1_ui/prefs/Preferences.py)
            # and just use the pref directly when rendering. [bruce 080913 comment]

        self.ortho = env.prefs[defaultProjection_prefs_key]
            # REVIEW: should self.ortho be replaced with a property that refers
            # to that pref, or would that be too slow? If too slow, is there
            # a refactoring that would clean up the current requirement
            # to keep these in sync? [bruce 080913 questions]

        self.setViewProjection(self.ortho)
            # This updates the checkmark in the View menu. Fixes bug #996 Mark 050925.

        # default display style for objects in the window.
        # even though there is only one assembly to a window,
        # this is here in anticipation of being able to have
        # multiple windows on the same assembly.
        # Start the GLPane's current display mode in "Default Display Mode" (pref).
        self.displayMode = env.prefs[startupGlobalDisplayStyle_prefs_key]
            # TODO: rename self.displayMode (widely used as a public attribute
            # of self) to self.displayStyle. [bruce 080910 comment]

        # piotr 080714: Remember last non-reduced display style.
        if self.displayMode == diDNACYLINDER or \
           self.displayMode == diPROTEIN:
            self.lastNonReducedDisplayMode = default_display_mode
        else:
            self.lastNonReducedDisplayMode = self.displayMode
        #self.win.statusBar().dispbarLabel.setText( "Current Display: " + dispLabel[self.displayMode] )

        # == End of User Preference initialization ==

        self._init_GLPane_rendering_methods()

        self.setAssy(assy) # leaves self.currentCommand/self.graphicsMode as nullmode, as of 050911
            # note: this loads view from assy.part (presumably the default view)

        self._init_GLPane_event_methods()

        return # from GLPane.__init__

    def resizeGL(self, width, height):
        """
        Called by QtGL when the drawing window is resized.
        """
        #bruce 080912 moved most of this into superclass
        self._resize_just_occurred = True
        GLPane_minimal.resizeGL(self, width, height) # call superclass method
        self.gl_update() # REVIEW: ok for superclass?
            # needed here? (Guess yes, to set needs_repaint flag)
        return

    _defer_statusbar_msg = False
    _deferred_msg = None
    _selobj_statusbar_msg = " "

    def paintGL(self):
        """
        [PRIVATE METHOD -- call gl_update instead!]

        The main OpenGL-widget-drawing method, called internally by Qt when our
        superclass needs to repaint (and quite a few other times when it
        doesn't need to).

        THIS METHOD SHOULD NOT BE CALLED DIRECTLY
        BY OUR OWN CODE -- CALL gl_update INSTEAD.
        """
        # debug_prefs related to bug 2961 in swapBuffers on Mac
        # (maybe specific to Leopard; seems to be a MacOS bug in which
        #  swapBuffers works but the display itself is not updated after
        #  that, or perhaps is not updated from the correct physical buffer)
        # [bruce 081222]
        debug_print_paintGL_calls = \
            debug_pref("GLPane: print all paintGL calls?",
                        Choice_boolean_False,
                        non_debug = True, # temporary ###
                        prefs_key = True )
        manual_swapBuffers = \
            debug_pref("GLPane: do swapBuffers manually?",
                        # when true, disable QGLWidget.autoBufferSwap and
                        # call swapBuffers ourselves when desired;
                        # this is required for some of the following to work,
                        # so it's also done when they're active even if it's False
                        Choice_boolean_False,
                        non_debug = True, # temporary ###
                        prefs_key = True )
        no_swapBuffers_when_nothing_painted = \
            debug_pref("GLPane: no swapBuffers when paintGL returns early?",
                        # I hoped this might fix some bugs in zoom to area
                        # rubber rect when swapBuffers behaves differently
                        # (e.g. after the bug in swapBuffers mentioned above),
                        # but it doesn't.
                        Choice_boolean_True,
                            # using True fixes a logic bug in this case,
                            # which fixes "related bug (B)" in bug report 2961
                        non_debug = True, # temporary ###
                        prefs_key = True )
        simulate_swapBuffers_with_CopyPixels = False
##        simulate_swapBuffers_with_CopyPixels = \
##            debug_pref("GLPane: simulate swapBuffers with CopyPixels?", # NIM
##                        Choice_boolean_False,
##                        non_debug = True, # temporary ###
##                        prefs_key = True )
        debug_verify_swapBuffers = False
##        debug_verify_swapBuffers = \
##            debug_pref("GLPane: verify swapBuffers worked?", # NIM
##                        # verify by reading back a changed pixel, one in each corner
##                        Choice_boolean_False,
##                        non_debug = True, # temporary ###
##                        prefs_key = True )
        if debug_verify_swapBuffers:
            manual_swapBuffers = True # required
        if simulate_swapBuffers_with_CopyPixels:
            pass ## manual_swapBuffers = True
                # not strictly required, left out for now, but put it in if this becomes real ###


        if debug_print_paintGL_calls:
            print
            print "calling paintGL"

        #bruce 081230 part of a fix for bug 2964
        # (statusbar not updated by hover highlighted object, on Mac OS 10.5.5-6)
        self._defer_statusbar_msg = True
        self._deferred_msg = None
        glselect_was_wanted = self.glselect_wanted
            # note: self.glselect_wanted is defined and used in a mixin class
            # in another file; review: can this be more modular?

        painted_anything = self._paintGL()
            # _paintGL method is defined in GLPane_rendering_methods
            # (probably paintGL itself would work fine if defined there --
            #  untested, since having it here seems just as good)

        want_swapBuffers = True # might be modified below

        if painted_anything:
            if debug_print_paintGL_calls:
                print " it painted (redraw %d)" % env.redraw_counter
            pass
        else:
            if debug_print_paintGL_calls:
                print " it didn't paint ***"
                    # note: not seen during zoom to area (nor is painted); maybe no gl_update then?
            if no_swapBuffers_when_nothing_painted:
                want_swapBuffers = False
            pass

        if want_swapBuffers and manual_swapBuffers:
            # do it now (and later refrain from doing it automatically)
            self.do_swapBuffers( debug = debug_print_paintGL_calls,
                                 use_CopyPixels = simulate_swapBuffers_with_CopyPixels,
                                 verify = debug_verify_swapBuffers )
            want_swapBuffers = False # it's done, so we no longer want to do it this frame

        # tell Qt whether to do swapBuffers itself when we return
        if want_swapBuffers:
            self.setAutoBufferSwap(True)
            ## assert not simulate_swapBuffers_with_CopyPixels # Qt can't do this on its own swapBuffers call -- ok for now
            assert not debug_verify_swapBuffers # Qt can't do this on its own swapBuffers call
        else:
            self.setAutoBufferSwap(False)
            if debug_print_paintGL_calls:
                print " (not doing autoBufferSwap)"

        # note: before the above code was added, we could test the default state
        # like this:
        ## if not self.autoBufferSwap():
        ##     print " *** BUG: autoBufferSwap is off"
        # but it never printed, so there is no bug there
        # when the above-mentioned bug in swapBuffers occurs.

        #bruce 081230 part of a fix for bug 2964
        self._defer_statusbar_msg = False
        if self._deferred_msg is not None:
            # do this now rather than inside set_selobj (re bug 2964)
            env.history.statusbar_msg( self._deferred_msg)
        elif glselect_was_wanted:
            # This happens when the same selobj got found again;
            # this is common when rulers are on, and rare but happens otherwise
            # (especially when moving mouse slowly from one selobj to another);
            # when it happens, I think we need to use this other way to get the
            # desired message out. Review: could we simplify by making
            # this the *only* way, or would that change the interaction
            # between this code and unrelated sources of statusbar_msg calls
            # whose messages we should avoid overriding unless selobj changes?
            env.history.statusbar_msg( self._selobj_statusbar_msg )
            pass

        self._resize_just_occurred = False

        self._resetWaitCursor_globalDisplayStyle()

        return

    def do_swapBuffers( self,
                        debug = False,
                        use_CopyPixels = False,
                        verify = False ): #bruce 081222
        """
        """
        if verify:
            # record pixels, change colors to make buffers differ, compare later
            print "verify is nim" ###
        if use_CopyPixels:
            print "use_CopyPixels is nim, no swapBuffers is occurring" ### Q: does this prevent all drawing??
        else:
            self.swapBuffers()
        if verify:
            # compare, print
            pass ###
        return

    # ==

    #bruce 080813 get .graphicsMode from commandSequencer

    def _get_graphicsMode(self):
        res = self.assy.commandSequencer.currentCommand.graphicsMode
            # don't go through commandSequencer.graphicsMode,
            # maybe that attr is not needed
        assert isinstance(res, GraphicsMode_interface)
        return res

    graphicsMode = property( _get_graphicsMode)

    # ==

    # self.part maintenance [bruce 050419]

    def set_part(self, part):
        """
        change our current part to the one given, and take on that part's view;
        ok if old or new part is None;
        note that when called on our current part,
        effect is to store our view into it
        (which might not actually be needed, but is fast enough and harmless)
        """
        if self.part is not part:
            self.gl_update() # we depend on this not doing the redraw until after we return
        self._close_part() # saves view into old part (if not None)
        self.part = part
        self._open_part() # loads view from new part (if not None)

    def forget_part(self, part):
        """
        [public]
        if you know about this part, forget about it (call this from dying parts)
        """
        if self.part is part:
            self.set_part(None)
        return

    def _close_part(self):
        """
        [private]
        save our current view into self.part [if not None] and forget about self.part
        """
        if self.part:
            self._saveLastViewIntoPart( self.part)
        self.part = None

    def _open_part(self):
        """
        [private]
        after something set self.part, load our current view from it
        """
        if self.part:
            self._setInitialViewFromPart( self.part)
        # else our current view doesn't matter
        return

    def saveLastView(self):
        """
        [public method]
        update the view of all parts you are displaying (presently only one or none) from your own view
        """
        if self.part:
            self._saveLastViewIntoPart( self.part)

    #bruce 050418 split the following NamedView methods into two methods each,
    # so MWsemantics can share code with them. Also revised their docstrings,
    # and revised them for assembly/part split (i.e. per-part csys records),
    # and renamed them as needed to reflect that.
    #
    #bruce 080912: the ones that are slot methods are now moved to
    # GLPane_view_change_methods.py. The "view" methods that remain are involved
    # in the relation between self and self.part, rather than in implementing
    # view changes for the UI, so they belong here rather than in that file.

    def _setInitialViewFromPart(self, part):
        """
        Set the initial (or current) view used by this GLPane
        to the one stored in part.lastView, i.e. to part's "Last View".
        """
        # Huaicai 1/27/05: part of the code of this method comes
        # from original setAssy() method. This method can be called
        # after setAssy() has been called, for example, when opening
        # an mmp file.

        self.snapToNamedView( part.lastView) # defined in GLPane_view_change_methods

    def _saveLastViewIntoPart(self, part):
        """
        Save the current view used by this GLPane into part.lastView,
        which (when this part's assy is later saved in an mmp file)
        will be saved as that part's "Last View".
        [As of 050418 this still only gets saved in the file for the main part]
        """
        # Huaicai 1/27/05: before mmp file saving, this method
        # should be called to save the last view user has, which will
        # be used as the initial view when it is opened again.
        part.lastView.setToCurrentView(self)

    # ==

    def setAssy(self, assy): #bruce 050911 revised this
        """
        [bruce comment 040922, partly updated 080812]
        This is called from self.__init__, and from
        MWSemantics._make_and_init_assy when user asks to open a new file
        or close current file.

        Apparently, it is supposed to forget whatever is happening now,
        and reinitialize the entire GLPane. However, it does nothing to
        cleanly leave the current mode, if any; my initial guess [040922]
        is that that's a bug. (As of 040922 I didn't yet try to fix that...
        only to emit a warning when it happens. Any fix requires modifying
        our callers.)

        I also wonder if setAssy ought to do some of the other things
        now in __init__, e.g. setting some display prefs to their
        defaults.

        Yet another bug (in how it's called now): user is evidently not given
        any chance to save unsaved changes, or get back to current state if the
        openfile fails... tho I'm not sure I'm right about that, since I didn't
        test it.

        Revised 050911: leaves mode as nullmode.
        """
        if self.assy:
            # make sure the old assy (if any) was closed [bruce 080314]
            # Note: if future changes to permit MDI allow one GLPane to switch
            # between multiple assys, then the following might not be a bug.
            # Accordingly, we only complain, we don't close it.
            # Callers should close it before calling this method.
            if not self.assy.assy_closed:
                print "\nlikely bug: GLPane %r .setAssy(%r) but old assy %r " \
                      "was not closed" % (self, assy, self.assy)
            ##e should previous self.assy be destroyed, or at least
            # made to no longer point to self? [bruce 051227 question]
            pass
        self.assy = assy
        mainpart = assy.tree.part
        assert mainpart #bruce 050418; depends on the order in which
            # global objects (glpane, assy) are set up during startup
            # or when opening a new file, so it might fail someday.
            # It might not be needed if set_part (below) doesn't mind
            # a mainpart of None and/or if we initialize our viewpoint
            # to default, according to an older version of this comment.
            # [comment revised, bruce 080812]

        assy.set_glpane(self) # sets assy.o and assy.glpane
            # logically I'd prefer to move this to just after set_part,
            # but right now I have no time to fully analyze whether set_part
            # might depend on this having been done, so I won't move it down
            # for now. [bruce 080314]

        self.set_part( mainpart)

        self.assy.commandSequencer._reinit_modes()
            # TODO: move this out of this method, now that it's the usual case

        return # from GLPane.setAssy

    # == update methods [refactored between self and CommandSequencer, bruce 080813]

    def update_after_new_graphicsMode(self): # maybe rename to update_GLPane_after_new_graphicsMode?
        """
        do whatever updates are needed in self after self.graphicsMode might have changed
        (ok if this is called more than needed, except it might be slower)

        @note: this should only do updates related specifically to self
               (as a GLPane). Any updates to assy, command sequencer or stack,
               active commands, or other UI elements should be done elsewhere.
        """
        # note: as of 080813, called from _cseq_update_after_new_mode and Move_Command

        # TODO: optimize: some of this is not needed if the old & new graphicsMode are equivalent...
        # the best solution is to make them the same object in that case,
        # i.e. to get their owning commands to share that object,
        # and then to compare old & new graphicsMode objects before calling this. [bruce 071011]

        # note: self.selatom is deprecated in favor of self.selobj.
        # self.selobj will be renamed, perhaps to self.objectUnderMouse.
        # REVIEW whether it belongs in self at all (vs the graphicsMode,
        #  or even the currentCommand if it can be considered part of the model
        #  like the selection is). [bruce 071011]

        # selobj

        if self.selatom is not None: #bruce 050612 precaution (scheme could probably be cleaned up #e)
            if debug_flags.atom_debug:
                print "atom_debug: update_after_new_graphicsMode storing None over self.selatom", self.selatom
            self.selatom = None
        if self.selobj is not None: #bruce 050612 bugfix; to try it, in Build drag selatom over Select Atoms toolbutton & press it
            if debug_flags.atom_debug:
                print "atom_debug: update_after_new_graphicsMode storing None over self.selobj", self.selobj
            self.set_selobj(None)

        # event handlers

        self.set_mouse_event_handler(None) #bruce 070628 part of fixing bug 2476 (leftover CC Done cursor)

        # cursor (is this more related to event handlers or rendering?)

        self.graphicsMode.update_cursor()
            # do this always (since set_mouse_event_handler only does it if the handler changed) [bruce 070628]
            # Note: the above updates are a good idea,
            # but they don't help with generators [which as of this writing don't change self.currentCommand],
            # thus the need for other parts of that bugfix -- and given those, I don't know if this is needed.
            # But it seems a good precaution even if not. [bruce 070628]

        # rendering-related

        if sys.platform == 'darwin':
            self._set_widget_erase_color()
            # sets it from self.backgroundColor;
            # attr and method defined in GLPane_text_and_color_methods;
            # see comments in that method's implem for caveats

        self.gl_update() #bruce 080829

        return

    def update_GLPane_after_new_command(self): #bruce 080813
        """
        [meant to be called only from CommandSequencer._cseq_update_after_new_mode]
        """
        self._adjust_GLPane_scale_if_needed()
        return

    def _adjust_GLPane_scale_if_needed(self): # by Ninad
        """
        Adjust the glpane scale while in a certain command.

        Behavior --

        Default scale remains the same (i.e. value of
        startup_GLPane_scale_prefs_key)

        If user enters BuildDna command and if --
        a) there is no model in the assembly AND
        b) user didn't change the zoom factor , the glpane.scale would be
        adjusted to 50.0 (GLPane_scale_for_dna_commands_prefs_key)

        If User doesn't do anything in BuildDna AND also doesn't modify the zoom
        factor, exiting BuildDna and going into the default command
        (or any command such as BuildAtoms), it should restore zoom scale to
        10.0 (value for GLPane_scale_for_atom_commands_prefs_key)

        @see: self.update_after_new_current_command() where it is called. This
              method in turn, gets called after you enter a new command.
        @see: Command.start_using_new_mode()
        """
        #Implementing this method fixes bug 2774

        #bruce 0808013 revised order of tests within this method

        #hasattr test fixes bug 2813
        if hasattr(self.assy.part.topnode, 'members'):
            numberOfMembers = len(self.assy.part.topnode.members)
        else:
            #It's a clipboard part, probably a chunk or a jig not contained in
            #a group.
            numberOfMembers = 1

        if numberOfMembers != 0: # do nothing except for an empty part
            return

        # TODO: find some way to refactor this to avoid knowing the
        # explicit list of commandNames (certainly) and prefs_keys /
        # preferred scales (if possible). At least, define a
        # "command scale kind" attribute to test here in place of
        # having the list of command names. [bruce 080813 comment]

        dnaCommands = ('BUILD_DNA', 'INSERT_DNA', 'DNA_SEGMENT', 'DNA_STRAND')

        startup_scale = float(env.prefs[startup_GLPane_scale_prefs_key])

        dna_preferred_scale = float(
            env.prefs[GLPane_scale_for_dna_commands_prefs_key])

        atom_preferred_scale = float(
            env.prefs[GLPane_scale_for_atom_commands_prefs_key])

        if self.assy.commandSequencer.currentCommand.commandName in dnaCommands:
            if self.scale == startup_scale:
                self.scale = dna_preferred_scale
        else:
            if self.scale == dna_preferred_scale:
                self.scale = atom_preferred_scale

        return

    def _setWaitCursor_globalDisplayStyle(self):
        """
        For performance reasons, whenever the global display style
        changes, the current cursor is replaced with an hourglass cursor,
        by calling this method.

        @see: self._paintGL()
        @see: self.setGlobalDisplayStyle()
        @see: self._setWaitCursor()
        @see: self._resetWaitCursor_globalDisplayStyle
        """
        if self._waitCursor_for_globalDisplayStyleChange:
            #This avoids setting the wait cursor twice.
            return
        self._waitCursor_for_globalDisplayStyleChange = True
        self._setWaitCursor()

    def _resetWaitCursor_globalDisplayStyle(self):
        """
        Reset hourglass cursor that was set while NE1 was changing the global
        display style of the model (by _setWaitCursor_globalDisplayStyle).

        @see: self._paintGL()
        @see: self.setGlobalDisplayStyle()
        @see: self._setWaitCursor()
        @see: self._setWaitCursor_globalDisplayStyle()
        """
        if self._waitCursor_for_globalDisplayStyleChange:
            self._resetWaitCursor()
            self._waitCursor_for_globalDisplayStyleChange = False
        return

    def _setWaitCursor(self):
        """
        Set the hourglass cursor whenever required.
        """
        QApplication.setOverrideCursor( QCursor(Qt.WaitCursor) )

    def _resetWaitCursor(self):
        """
        Reset the hourglass cursor.

        @see: self._paintGL()
        @see: self.setGlobalDisplayStyle()
        @see: self._setWaitCursor()
        @see: self._setWaitCursor_globalDisplayStyle()
        """
        QApplication.restoreOverrideCursor()

    def setGlobalDisplayStyle(self, disp):
        """
        Set the global display style of self (the GLPane).

        @param disp: The desired global display style, which
                     should be one of the following constants
                     (this might not be a complete list;
                      for all definitions see constants.py):
                     - diDEFAULT (the "NE1 start-up display style", as defined
                       in the Preferences | General dialog)
                     - diLINES (Lines display style)
                     - diTUBES (Tubes display style)
                     - diBALL (Ball and stick display style)
                     - diTrueCPK (Space filling display style)
                     - diDNACYLINDER (DNA cylinder display style)
        @type  disp: int

        @note: doesn't update the MT, and callers typically won't need to,
               since the per-node display style icons are not changing.

        @see: setDisplayStyle methods in some model classes

        @see: setDisplayStyle_of_selection method in another class
        @see: self.self._setWaitCursor_globalDisplayStyle()
        """

        self._setWaitCursor_globalDisplayStyle()

        # review docstring: what about diINVISIBLE? diPROTEIN?
        if disp == diDEFAULT:
            disp = env.prefs[ startupGlobalDisplayStyle_prefs_key ]
        #e someday: if self.displayMode == disp, no actual change needed??
        # not sure if that holds for all init code, so being safe for now.
        self.displayMode = disp
            # note: chunks check this when needed before drawing, so this code
            # doesn't need to tell them to invalidate their display lists.

        # piotr 080714: Remember last non-reduced display style.
        if disp != diDNACYLINDER and \
           disp != diPROTEIN:
            self.lastNonReducedDisplayMode = disp

        # Huaicai 3/29/05: Add the condition to fix bug 477 (keep this note)
        if self.assy.commandSequencer.currentCommand.commandName == 'CRYSTAL':
            self.win.statusBar().dispbarLabel.setEnabled(False)
            self.win.statusBar().globalDisplayStylesComboBox.setEnabled(False)
        else:
            self.win.statusBar().dispbarLabel.setEnabled(True)
            self.win.statusBar().globalDisplayStylesComboBox.setEnabled(True)

        self.win.statusBar().globalDisplayStylesComboBox.setDisplayStyle(disp)
        return

    _needs_repaint = True #bruce 050516
        # used/modified in both this class and GLPane_rendering_methods
        # (see also wants_gl_update)

    def gl_update(self): #bruce 050127
        """
        External code should call this when it thinks the GLPane needs
        redrawing, rather than directly calling paintGL, unless it really
        knows it needs to wait until the redrawing has been finished
        (which should be very rare).

        Unlike calling paintGL directly (which can be very slow for
        large models, and redoes all its work each time it's called),
        this method is ok to call many times during the handling of one
        user event, since this will cause only one call of paintGL, after
        that user event handler has finished.

        @see: gl_update_duration (defined in superclass GLPane_view_change_methods)
        """
        self._needs_repaint = True
        # (To restore the pre-050127 behavior, it would be sufficient to
        # change the next line from "self.update()" to "self.paintGL()".)
        self.update()
            # QWidget.update() method -- ask Qt to call self.paintGL()
            # (via some sort of paintEvent to our superclass)
            # very soon after the current event handler returns
            #
            ### REVIEW: why does this not call self.updateGL() like ThumbView.py does?
            # I tried calling self.updateGL() here, and got an exception
            # inside a paintGL subroutine (AttributeError on self.guides),
            # captured somewhere inside it by print_compact_stack,
            # which occurred before a print statement here just after my call.
            # The toplevel Python call shown in print_compact_stack was paintGL.
            # This means: updateGL causes an immediate call by Qt into paintGL,
            # in the same event handler, but from C code which stops print_compact_stack
            # from seeing what was outside it. (Digression: we could work around
            # that by grabbing the stack frame here and storing it somewhere!)
            # Conclusion: never call updateGL in GLPane, and review whether
            # ThumbView should continue to call it. [bruce 080912 comment]
        return

    def gl_update_highlight(self): #bruce 070626
        """
        External code should call this when it thinks the hover-highlighting in self
        needs redrawing (but when it doesn't need to report any other need for redrawing).
           This is an optimization, since if there is no other reason to redraw,
        the highlighting alone may be redrawn faster by using a saved color/depth image of
        everything except highlighting, rather than by redrawing everything else.
        [That optim is NIM as of 070626.]
        """
        self.gl_update() # stub for now
        return

    def gl_update_for_glselect(self): #bruce 070626
        """
        External code should call this instead of gl_update when the only reason
        it would have called that is to make us notice self.glselect_wanted and use it to
        update self.selobj. [That optim is NIM as of 070626.]
        """
        self.gl_update() # stub for now
        return

    def gl_update_confcorner(self): #bruce 070627
        """
        External code should call this when it thinks the confirmation corner may need
        redrawing (but when it doesn't need to report any other need for redrawing).
           This is an optimization, since if there is no other reason to redraw,
        the confirmation corner alone may be redrawn faster by using a saved color image of
        the portion of the screen it covers (not including the CC overlay itself),
        rather than by redrawing everything.
        [That optim is NIM as of 070627 and A9.1. Its OpenGL code would make use of the
         conf_corner_bg_image code in this class. Deciding when to do it vs full update
         is the harder part.]
        """
        self.gl_update() # stub for now
        return

    # The following behavior (in several methods herein) related to wants_gl_update
    # should probably also be done in ThumbViews
    # if they want to get properly updated when graphics prefs change. [bruce 050804 guess]

    wants_gl_update = True #bruce 050804
        # this is set to True after we redraw, and to False by wants_gl_update_was_True
        # used/modified in both this class and GLPane_rendering_methods
        # (see also _needs_repaint)

    def wants_gl_update_was_True(self): #bruce 050804
        """
        Outside code should call this if it changes what our redraw would draw,
        and then sees self.wants_gl_update being true,
        if it might not otherwise call self.gl_update
        (which is also ok to do, but might be slower -- whether it's actually slower is not known).

        This can also be used as an invalidator for passing to self.end_tracking_usage().
        """
        #bruce 070109 comment: it looks wrong to me that the use of this as an invalidator in end_tracking_usage
        # is not conditioned on self.wants_gl_update, either inside or outside this routine. I'm not sure it's really wrong,
        # and I didn't analyze all calls of this, nor how it might interact with self._needs_repaint.
        # If it's wrong, it means the intended optim (avoiding lots of Qt update calls) is not happening.
        #bruce 080913 comment: I doubt the intended optim matters, anyway.
        # REVIEW whether this should simply be scrapped in favor of just
        # calling gl_update instead.
        self.wants_gl_update = False
        self.gl_update()

    # ==

    ## selobj = None #bruce 050609

    _selobj = None #bruce 080509 made this private

    def __get_selobj(self): #bruce 080509
        return self._selobj

    def __set_selobj(self, val): #bruce 080509
        self.set_selobj(val)
            # this indirection means a subclass could override set_selobj,
            # or that we could revise this code to pass it an argument
        return

    selobj = property( __get_selobj, __set_selobj)
        #bruce 080509 bugfix for MT crosshighlight sometimes lasting too long

    def set_selobj(self, selobj, why = "why?"):
        """
        Set self._selobj to selobj (might be None) and do appropriate updates.
        Possible updates include:

        - env.history.statusbar_msg( selobj.mouseover_statusbar_message(),
                                     or "" if selobj is None )

        - help the model tree highlight the nodes containing selobj

        @note: as of 080509, all sets of self.selobj call this method, via a
               property. If some of them don't want all our side effects,
               they will need to call this method directly and pass options
               [nim] to prevent those.
        """
        # note: this method should access and modify self._selobj,
        # not self.selobj (setting that here would cause an infinite recursion).
        if selobj is not self._selobj:
            previous_selobj = self._selobj
            self._selobj = selobj #bruce 080507 moved this here

            # Note: we don't call gl_update_highlight here, so the caller needs to
            # if there will be a net change of selobj. I don't know if we should call it here --
            # if any callers call this twice with no net change (i.e. use this to set selobj to None
            # and then back to what it was), it would be bad to call it here. [bruce 070626 comment]
            if _DEBUG_SET_SELOBJ:
                # todo: make more calls pass "why" argument
                print_compact_stack("_DEBUG_SET_SELOBJ: %r -> %r (%s): " % (previous_selobj, selobj, why))
            #bruce 050702 partly address bug 715-3 (the presently-broken Build mode statusbar messages).
            # Temporary fix, since Build mode's messages are better and should be restored.
            if selobj is not None:
                try:
                    try:
                        #bruce 050806 let selobj control this
                        method = selobj.mouseover_statusbar_message
                            # only defined for some objects which inherit Selobj_API
                    except AttributeError:
                        msg = "%s" % (selobj,)
                    else:
                        msg = method()
                except:
                    msg = "<exception in selobj statusbar message code>"
                    if debug_flags.atom_debug:
                        #bruce 070203 added this print; not if 1 in case it's too verbose due as mouse moves
                        print_compact_traceback(msg + ': ')
                    else:
                        print "bug: %s; use ATOM_DEBUG to see details" % msg
            else:
                msg = " "

            self._selobj_statusbar_msg = msg #bruce 081230 re bug 2964

            if self._defer_statusbar_msg:
                self._deferred_msg = msg #bruce 081230 re bug 2964
            else:
                env.history.statusbar_msg(msg)

            if pref_show_highlighting_in_MT():
                # help the model tree highlight the nodes containing selobj
                # [bruce 080507 new feature]
                self._update_nodes_containing_selobj(
                    selobj, # might be None, and we do need to call this then
                    repaint_nodes = True,
                    # this causes a side effect which is the only reason we're called here
                    even_if_selobj_unchanged = False
                    # optimization;
                    # should be safe, since changes to selobj parents or node parents
                    # which would otherwise require this to be passed as true
                    # should also call mt_update separately, thus doing a full
                    # MT redraw soon enough
                )
            pass # if selobj is not self._selobj

        self._selobj = selobj # redundant (as of bruce 080507), but left in for now

        #e notify more observers?
        return

    def _update_nodes_containing_selobj(self,
                                        selobj,
                                        repaint_nodes = False,
                                        even_if_selobj_unchanged = False
                                        ): #bruce 080507
        """
        private; recompute self._nodes_containing_selobj from the given selobj,
        optimizing when selobj and/or our result has not changed,
        and do updates as needed and as options specify.

        @return: None

        @param repaint_nodes: if this is true and we change our cached value of
                              self._nodes_containing_selobj, then also call the
                              model tree method repaint_some_nodes.

        @param even_if_selobj_unchanged: if this is true, assume that our
                                         result might differ even if selobj
                                         itself has not changed since our
                                         last call.

        @note: called in two circumstances:
        - when we know selobj has changed (repaint_nodes should be True then)
        - when the MT explicitly calls get_nodes_containing_selobj
          (repaint_nodes should probably be False then)

        @note: does not use or set self._selobj (assuming external code it calls
               doesn't do so).

        @warning: if this method or code it calls sets self.selobj, that would
                  cause infinite recursion, since self.selobj is a property
                  whose setter calls this method.
        """
        # review: are there MT update bugs related to dna updater moving nodes
        # into new groups?
        if selobj is self._nodes_containing_selobj_is_from_selobj and \
           not even_if_selobj_unchanged:
            # optimization: nothing changed, no updates needed
            return

        # recompute and perhaps update
        nodes = []
        if selobj is not None:
            try:
                method = selobj.nodes_containing_selobj
            except AttributeError:
                # should never happen, since Selobj_API defines this method
                print "bug: no method nodes_containing_selobj in %r" % (selobj,)
                pass
            else:
                try:
                    nodes = method()
                    assert type(nodes) == type([])
                except:
                    msg = "bug in %r.nodes_containing_selobj " \
                        "(or invalid return value from it)" % (selobj,)
                    print_compact_traceback( msg + ": ")
                    nodes = []
                pass
            pass
        if self._nodes_containing_selobj != nodes:
            # assume no need to sort, since they'll typically be
            # returned in inner-to-outer order
            all_changed_nodes = self._nodes_containing_selobj + nodes
                # assume duplications are ok;
                # could optim by leaving out any nodes that appear
                # in both lists, assuming the way they're highlighted in
                # the MT doesn't depend on anything but their presence
                # in the list; doesn't matter for now, since the MT
                # redraw is not yet incremental [as of 080507]
            self._nodes_containing_selobj = nodes
            if repaint_nodes:
                ## self._nodes_containing_selobj_has_changed(all_changed_nodes)
                self.assy.win.mt.repaint_some_nodes( all_changed_nodes)
                    # review: are we sure self.assy.win.mt.repaint_some_nodes will never
                    # use self.selobj by accessing it externally?
        self._nodes_containing_selobj_is_from_selobj = selobj
        return

    def get_nodes_containing_selobj(self): #bruce 080507
        """
        @return: a list of nodes that contain self.selobj
                 (possibly containing some nodes more than once).

        @warning: repeated calls with self.selobj unchanged are *not* optimized.
                  (Doing so correctly would be difficult.)
                  Callers should temporarily store our return value as needed.
        """
        self._update_nodes_containing_selobj(
            self.selobj,
            repaint_nodes = False,
            even_if_selobj_unchanged = True
        )
        return self._nodes_containing_selobj

    pass # end of class GLPane

# end