summaryrefslogtreecommitdiff
path: root/cad/src/modelTree/modelTreeGui.py
blob: 77159999a50313da94c61bc39841523b6c647109 (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
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
# Copyright 2006-2008 Nanorex, Inc.  See LICENSE file for details.
"""
modelTreeGui.py - provide a Qt4-compatible Model Tree widget,
inherited by modelTree.py to provide NE1's Model Tree of Nodes.

@author: Will, Bruce
@version: $Id$
@copyright: 2006-2008 Nanorex, Inc.  See LICENSE file for details.

Goals (of Will's refactoring, as part of the Qt3 -> Qt4 port):

There is now a clear simple API for the model tree, which amounts to a refactoring of the code
previously appearing in TreeView.py, TreeWidget.py, and modelTree.py. [For history, see the
modelTree.py docstring.]

The API needs to accomplish three things. First it must work in just this file with a very
restricted set of data, and in this context I [Will] need to bang on issues of selection, display, and
other GUI stuff. Second it needs to work with the uiPrototype.py stuff for the new user interface.
Third it needs to migrate into the NE-1 code cleanly, ideally with just a change of the filename.

Recent history:

Those goals were partly achieved, and in particular the API superclasses add a lot of clarity,
but in the implementation there were serious missing features and logic bugs... I have solved
some of these by bringing back some of the old code, and abandoning use of Qt's concept of selected
items, and rewriting the node-selection-related behaviors. In doing this, I added a few methods
to the APIs (not all of which are documented there -- see ##doc comments to find those that aren't).

There remain some serious cosmetic and performance bugs, relative to the Qt3 version, which I'm
still working on. [bruce 070508]

Update 070612: I [bruce] did a major rewrite to fix scrollbar bugs (circa May 07), mentioned in
comments below, before A9 release, using QScrollArea instead of QTreeView. Now I am removing the
obsolete code which used QTreeView, but some comments still refer to its classes. What's being
removed includes the classes:

  class _our_QItemDelegate(QItemDelegate): # removed
  class _our_TreeItem: # removed
  class _QtTreeModel(QAbstractItemModel): # removed
  class ModelTreeGui_QTreeView(QTreeView, ModelTreeGui_common): # removed

late 2007 and/or early 2008: Mark and Bruce implemented atom content indicators,
though some bugs remain

080507: Bruce partly implemented GLPane -> MT cross-highlighting

Bruce 081216 is doing some cleanup and refactoring, including splitting
ModelTree and TreeModel into separate objects with separate classes and
_api classes, and splitting some code into separate files.
"""

import sys
import time

from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.Qt import Qt
from PyQt4.Qt import QScrollArea
from PyQt4.Qt import QIcon
from PyQt4.Qt import QDrag
from PyQt4.Qt import QMimeData
from PyQt4.Qt import QPoint
from PyQt4.Qt import QPixmap
from PyQt4.Qt import QPainter
from PyQt4.Qt import QFontMetrics
from PyQt4.Qt import QLineEdit
from PyQt4.Qt import QColor
from PyQt4.Qt import QRect
from PyQt4.Qt import QPalette

import foundation.env as env

from modelTree.ModelTreeGUI_api import ModelTreeGUI_api
from modelTree.Node_as_MT_DND_Target import Node_as_MT_DND_Target #bruce 071025

from platform_dependent.PlatformDependent import fix_plurals

from PM.PM_Colors  import getPalette

from utilities.GlobalPreferences import pref_show_node_color_in_MT
from utilities.Log import quote_html
from utilities.constants import AC_INVISIBLE, AC_HAS_INDIVIDUAL_DISPLAY_STYLE
from utilities.debug import print_compact_traceback, print_compact_stack
from utilities.debug_prefs import debug_pref, Choice_boolean_True, Choice_boolean_False, Choice
from utilities.icon_utilities import getpixmap
from utilities.icon_utilities import imagename_to_pixmap
from utilities.prefs_constants import mtColor_prefs_key

from widgets.menu_helpers import makemenu_helper
from widgets.simple_dialogs import grab_text_line_using_dialog
from widgets.widget_helpers import RGBf_to_QColor


_DEBUG0 = False # debug api compliance
_DEBUG = False # debug selection stuff
_DEBUG2 = False # mouse press event details
_DEBUG3 = False # stack, begin, end, for event processing
_DEBUG4 = False # print mousePress gl_updates

_ICONSIZE = (22, 22)

ITEM_HEIGHT = _ICONSIZE[1] # in theory, these need not be the same,
    # but other values are untested [bruce 070529 guess]

_cached_icons = {} #bruce 070529 presumed optimization


def _paintnode(node, painter, x, y, widget, option_holder = None):
    """
    Draw node in painter.
    x,y are coords for topleft of type-icon.
    widget (the modelTreeGui) is used for palette.
    @param option_holder: the MT_View, used for dictionary attribute
                          _f_nodes_to_highlight and (in future)
                          for some prefs flags.
    """
    #bruce 070529 split this out
    #bruce 080507 added option_holder
    ### todo: revise appearance of highlighted nodes

    ## print "_paintnode",node, painter, x, y, widget
        # x is 20, 40, etc (indent level)
        # y is 0 for the top item, incrs by 22 for other items -- but this varies as we scroll.

    # find state of node which determines how we draw it
    selected = node.picked
    disabled = node.is_disabled() ### might be slow!
    display_prefs = display_prefs_for_node(node)
        # someday these might also depend on parent node and/or on ModelTreeGui (i.e. widget)

    text_color = None
    if pref_show_node_color_in_MT():
        # [bruce 080507 new feature, mainly for testing]
        # review: should the modelTree itself (treemodel) test this pref
        # and tell us the text_color for each node?
        ### is this debug_pref test too slow? if it is, so is the debug_pref lower down...
        # could fix using option_holder to know the pref values

        # note: this should work for all nodes with a .color attribute,
        # not just chunks, but to work with Groups (including DnaStrand etc)
        # it would need some revisions, e.g. call a new Node method
        # get_node_color_for_MT (which would be a good refactoring anyway)
        text_color = getattr(node, 'color', None) # node.color

    highlighted = option_holder and node in option_holder._f_nodes_to_highlight
        # fyi: for now, this means "highlighted in glpane",
        # but in the future we'll also use it for MT mouseover highlighting

    # draw it

    node_icon = node.node_icon(display_prefs) # a QPixmap object

    try:
        pixmap = _cached_icons[node_icon]
    except KeyError:
        # print "making pixmap for node_icon", node_icon # should occur once per unique icon per session
        pixmap = QIcon(node_icon).pixmap(_ICONSIZE[0], _ICONSIZE[1]) # vary the size
        _cached_icons[node_icon] = pixmap
        pass

    # draw it (icon and label), possibly transforming the painter before and during this.

    # Note about save/restore: Qt 4.1 doc says:
    #  After painting, you should ensure that the painter is returned to the state it was supplied in
    #  when this function was called. For example, it may be useful to call QPainter::save()
    #  before painting and QPainter::restore() afterwards.

    need_save = disabled or selected or (text_color is not None) or highlighted
        #bruce 070529 presumed optimization (not usually set for most nodes)

    if need_save:
        painter.save()

    if disabled: # before
        # draw icon and label in slanted form
        painter.shear(0, -0.5)
        painter.translate(0.5 * y + 4, 0.0)

    painter.drawPixmap(x, y, pixmap)

    # Draw a special symbol in the upper right corner of the node icon if one
    # of the following conditions is true:
    # - draw a small yellow dot if the node contains atoms that have their
    #   display style set to something other than diDEFAULT.
    # - draw a small white ghost if the node contains invisible or hidden
    #   chunks and/or atoms.
    # - draw a small yellow ghost if the node contains invisible or hidden
    #   chunks and/or atoms _and_ the node also contains atoms that have their
    #   display style set to something other than diDEFAULT.
    # Mark 2008-03-04
    #
    # revised by bruce 080306, but not yet in final form, or in final place in this file. ###
    # What remains to do (assuming this works so far):
    # - some optimizations in the code for maintaining the node flags this accesses
    # - MT needs to subscribe to changes in this data for nodes it draws (and get mt_update when that changes)
    # - MT needs to include this data in what it compares about a node when deciding if redraw is needed.
    #   Until it does, even an MT click often won't update the MT as needed.
    #   (So to test this as it is so far, modify the model, then save and reopen the file.)
    #
    # Also need to revise behavior. See Qs below. Here are notes about the answers: [080314]
    ##yellow ghost: chunks in groups count (for both indicators)
    ##but hidden whole can coexist with ghost if the ghost is about hidden atoms
    ## (since unhide of them requires another step)
    ## (and the latter is nim, we'll make bug report)

    node_symbols = debug_pref("Model Tree: add content symbols to node icons?",
                                  #bruce 080416 renamed this, special -> content,
                                  # for clarity
                              Choice_boolean_True, #bruce 080307 False -> True
                              non_debug = True,
                              prefs_key = True #bruce 080307 (safe now)
                            )
    if node_symbols:

        flags = node.get_atom_content(AC_INVISIBLE | AC_HAS_INDIVIDUAL_DISPLAY_STYLE)

        # Hidden nodes get ghosts. [hmm, probably not right ### FIX]
        node_has_invisible_contents = node.hidden

        # NIM: so do nodes with hidden sub-nodes. ##### IMPLEM

        # So do nodes which contain invisible (aka hidden) atoms:
        if flags & AC_INVISIBLE:
            node_has_invisible_contents = True

        # Nodes which contain atoms with non-default display styles get yellow dots
        # (or yellow ghosts if they already would have ghosts).
        node_has_special_display_contents = False
        if flags & AC_HAS_INDIVIDUAL_DISPLAY_STYLE:
            node_has_special_display_contents = True

        # What about non-default display styles on chunks? ### DECIDE, FIX

        # Now draw all this.
        if node_has_invisible_contents and node_has_special_display_contents:
            painter.drawPixmap(x, y,
                               imagename_to_pixmap("modeltree/yellow_ghost.png"))
        elif node_has_invisible_contents:
            painter.drawPixmap(x, y,
                               imagename_to_pixmap("modeltree/white_ghost.png"))
        elif node_has_special_display_contents:
            painter.drawPixmap(x, y,
                               imagename_to_pixmap("modeltree/yellow_dot.png"))

    if highlighted:
        background = Qt.yellow # initial test, NOT IDEAL since obscures selectedness ######
    elif selected:
        background = widget.palette().highlight() #k what type is this? QColor or something else?
    else:
        background = None

    if background is not None:
        # draw a selection or highlight color as text bg color
        ###BUG: should use color role so it depends on app being in bg or fg
        ###     (like it does for native QTreeView selectedness)
        # (note: this used to be done before drawing the icon, but even then it did not affect the icon.
        #  so it would be ok in either place.)
##        background = painter.background()
##        backgroundMode = painter.backgroundMode()
        painter.setBackground(background)
        painter.setBackgroundMode(Qt.OpaqueMode)

    if text_color is not None:
        painter.setPen(RGBf_to_QColor(text_color))

    yoffset = _ICONSIZE[1] / 2 + 4
    painter.drawText(x + _ICONSIZE[0] + 4, y + yoffset, node.name)

    if need_save:
        painter.restore()

    # About fixing bug 2372 by drawing the "red donut" to show the current Part:
    # the following code works, and would be correct if we ported the sg and condition code,
    # but it looks too ugly to consider using, IMHO (on my iMac G5).
    # Maybe we should draw some sort of icon for this? [bruce 070514]
    #
    ##    ## sg = assy.current_selgroup_iff_valid()
    ##    dotcolor = Qt.red
    ##    if 1: ### sg == node and node != assy.tree:
    ##        painter.save()
    ##        painter.setPen(QPen(dotcolor, 3)) # 3 is pen thickness;
    ##            # btw, this also sets color of the "moving 1 item"
    ##            # at bottom of DND graphic!
    ##        h = 9
    ##        dx,dy = -38,8 # topleft of what we draw;
    ##            # 0,0 is topleft corner of icon; neg coords work, not sure how far or how safe
    ##        painter.drawEllipse(x + dx, y + dy, h, h)
    ##        painter.restore()
    return

# ==

_buttons = dict(left = Qt.LeftButton,
                mid = Qt.MidButton,
                right = Qt.RightButton )

_modifiers = dict(shift = Qt.ShiftModifier,
                  control = Qt.ControlModifier,
                  alt = Qt.AltModifier,
                  meta = Qt.MetaModifier,
                  keypad = Qt.KeypadModifier,
                  X11_Mode_switch = Qt.GroupSwitchModifier )
    # http://www.riverbankcomputing.com/Docs/PyQt4/html/qt.html says:
    #   Note: On Mac OS X, the ControlModifier value corresponds to the Command keys on the Macintosh keyboard,
    #   and the MetaModifier value corresponds to the Control keys. The KeypadModifier value will also be set
    #   when an arrow key is pressed as the arrow keys are considered part of the keypad.
    # Thus, on any platform we can use ControlModifier for de-select, but context menu requests on the Mac
    # may involve either RightButton or MetaModifier. (Also, Qt seems to mess them up in a complicated way,
    # splitting them into an event with inconsistent button/buttons flags (LMB and RMB respectively), followed
    # by a contextMenuEvent call, at least if we don't use setContextMenuPolicy.

def describe_buttons(buttons):
    return _describe( int(buttons), _buttons)

def describe_modifiers(modifiers):
    return _describe( int(modifiers), _modifiers)

def _describe( flags, name_val_dict):
    origflags = flags
    flags = int(flags)
    res = []
    items = name_val_dict.items()
    items.sort() #e suboptimal order
    for name, val in items:
        val = int(val)
        if val & flags:
            res.append(name)
            flags = flags &~ val
    if flags:
        res.append("unknown flags %#x" % flags)
    desc = ", ".join(res) # might be empty
    return "%#x (%s)" % (int(origflags), desc)

# ==

# Things can be simpler if we admit that display_prefs don't yet depend on the ModelTreeGui object.
# So for now I'm pulling this out of that object, so _our_QItemDelegate.paint can access it without having a pointer to that object.
# I'm also optimizing it, since it'll be called a lot more often now.
# (Even better would be to optimize the node_icon API so we can pass .open directly.
#  We don't want to let the node use its own .open, in case in future we show one node in different MTs.)
# [bruce 070511]

def display_prefs_for_node(node):
    """
    Return a dict of the *incremental* display prefs for the node,
    relative to whatever its parent passes as display prefs for its own MT_kids.
    Any prefs that it should inherit from its parent's env should be left out of this dict.
    The complete set of display prefs can be used by node_icon to decide what icon the treeview
    should display for this node. (It can also use the node class or state.)
       In current code, the only thing this needs to include is whether this node
    is openable, and if so, whether it's open.
    [Subclasses can override this.]
    [Using this cumbersomeness just to say whether the item is open
     is not worthwhile in hindsight. Fix it sometime. #e]
    """
    if node.openable():
        if node.open:
            return _DISPLAY_PREFS_OPEN
        else:
            return _DISPLAY_PREFS_CLOSED
    else:
        return _DISPLAY_PREFS_LEAF
    pass

_DISPLAY_PREFS_OPEN   = dict(openable = True, open = True)
_DISPLAY_PREFS_CLOSED = dict(openable = True, open = False)
_DISPLAY_PREFS_LEAF   = {}

# ==

class DoNotDrop(Exception): pass

class ModelTreeGui_common(ModelTreeGUI_api):
    """
    The part of our ModelTreeGui implementation which is the same
    for either type of Qt widget used to show it.
    """
    # not private, but only used in this file so far [bruce 080306 comment]
    #bruce 070529 split this out of class ModelTreeGui
    def __init__(self, win, treemodel):
        self.win = win
        self.treemodel = treemodel
        self._mousepress_info_for_move = None
            # set by any mousePress that supports mouseMove not being a noop,
            # to info about what that move should do #bruce 070509
        self._ongoing_DND_info = None
            # set to a tuple of a private format, during a DND drag
            # (not used during a selection-drag #k)
        self.setAcceptDrops(True)
        if _DEBUG0:
            self._verify_api_compliance()

        # Make sure certain debug_prefs are visible from the start,
        # in the debug_prefs menu -- but only the ones that matter
        # for the MT implem we're using in this session.
        # WARNING (kluge): the defaults and other options are
        # duplicated code, but are only honored in these calls
        # (since these calls happen first).

        # these are used in both MT implems:
        self.debug_pref_use_fancy_DND_graphic()
        self.MT_debug_prints()
        return

    def topmost_selected_nodes(self, topnode = None, whole_nodes_only = False):
        """
        @return: a list of all selected nodes which are not inside selected Groups

        @see: TreeModel_api version, for more detailed documentation
        """
        #bruce 070529 moved method body into self.treemodel
        #bruce 081216 removed this from ModelTreeGUI_api,
        # since it's just a convenience method in this implem
        return self.treemodel.topmost_selected_nodes(topnode = topnode,
                                                     whole_nodes_only = whole_nodes_only )

    def MT_debug_prints(self):
        return debug_pref("MT debug: debug prints", Choice_boolean_False, prefs_key = True)

    def display_prefs_for_node(self, node):
        """
        For doc, see the global function of the same name.
        """
        return display_prefs_for_node(node) #bruce 070511 revised this to make it a global function

    # == DND start methods

    def _start_DND(self, item, option_modifier):
        thisnode = item.node
        drag_type = option_modifier and 'copy' or 'move'
            #bruce 070509 kluge condition, not sure fully correct (Qt3 code used fix_event then looked for MidButton)
            ###REVIEW: what sets the drag cursor -- does it redundantly compute this? Or does the cursor think it's *always* copy?
        nodes = self._dnd_nodes
            # set in mousepress, using info we no longer have
            # (due to its having already altered the selection)

        # now filter these nodes for the ones ok to drag, in the same way the Qt3 code did --
        # and not in every drag move event method [bruce 070509]
        nodes = self.filter_drag_nodes( drag_type, nodes ) # might be None
        if not nodes: # whether None or []
            return # message already emitted (if one is desired)

        # work around visual bug due to unwanted deselection of some of these nodes (during mousePressEvent)
        self.unpick_all()
        for node in nodes:
            node.pick()
        self.mt_update()
        # if that doesn't work, try self.repaint()

        self._ongoing_DND_info = (nodes, drag_type)

        # as a temporary workaround for the DND graphic being inadequate,
        # at least print a decent sbar message about it.

        # make up some text to be dragged in case the custom pixmap doesn't work or is nim
        dragobj_text = "%s %d item(s)" % (drag_type, len(nodes)) # e.g. "copy 5 item(s)"
        dragobj_text = fix_plurals(dragobj_text) # e.g. "copy 5 items" or "move 1 item" -- no "ing" to keep it as short as possible

        # make the dragobj [move sbar code above this? #e]
##        dragsource = self
##        dragobj = QTextDrag( dragobj_text, dragsource)
##            # \n and \r were each turned to \n, \n\r gave two \ns - even in "od -c"; as processed through Terminal
        dragobj = QDrag(self)
        mimedata = QMimeData()
        mimedata.setText("need a string here for a valid mimetype")
        dragobj.setMimeData(mimedata)

        pixmap = self.get_pixmap_for_dragging_nodes(drag_type, nodes)
        if pixmap:
            dragobj.setPixmap(pixmap)

        sbar_text = self.get_whatting_n_items_text( drag_type, nodes)
        self.statusbar_message( sbar_text)

        dragobj.setHotSpot(QPoint(-18, 8)) #bruce 070514 tweak hotspot
        requested_action = {'copy':Qt.CopyAction, 'move':Qt.MoveAction}[drag_type]
            #bruce 070514 fix bug in DND cursor copy/move indication
        dragobj.start( requested_action)

        return # nodes

    def filter_drag_nodes(self, drag_type, nodes):
        #bruce 070509 copied this from Qt3 TreeWidget.py [#k same as in Qt4 TreeWidget??]
        """
        See which of the given nodes can be dragged (as a group) in the given way.
        Return a subset of them to be actually dragged
        (having emitted a warning, if desired, if this is not all of them),
        or someday perhaps a processed version of them (e.g. you could pre-make copies for a 'copy' drag),
        or None (*not* just a list [] of 0 nodes to drag! that might be possible to drag!)
        if you want to refuse this drag (also after emitting a suitable warning).
        """
        if drag_type == 'move':
            nodes_ok = filter( lambda n: n.drag_move_ok(), nodes)
        else:
            nodes_ok = filter( lambda n: n.drag_copy_ok(), nodes)
        oops = len(nodes) - len(nodes_ok)
        if oops:
            ## msg = "some selected nodes can't be dragged that way -- try again" ###e improve msg
##            msg = "The Part can't be moved"
##                # kluge: this is the only known case! (that I can remember...) #e generalize this
##            self.redmsg(msg)
            #bruce 070509 commented that out, since I think it will often
            # happen by accident for "small move onto self"
            # note: self.redmsg is probably not yet defined
            return None
        return nodes_ok # same as nodes for now, but we might change above code so it's not

    def debug_pref_use_fancy_DND_graphic(self):
        return debug_pref("Model Tree: use fancy DND graphic?",
                          Choice_boolean_True,
                          non_debug = True,
                          prefs_key = True)

    def get_pixmap_for_dragging_nodes(self, drag_type, nodes):
        if self.debug_pref_use_fancy_DND_graphic():
            pixmap = self.get_pixmap_for_dragging_nodes_Qt3(drag_type, nodes)
        else:
            # stub version, just the icon from the first node
            display_prefs = self.display_prefs_for_node(nodes[0])
                #bruce 070504 bugfix (partial fix only)
            pixmap = nodes[0].node_icon(display_prefs)
        return pixmap

    def get_whatting_n_items_text(self, drag_type, nodes):
        """
        return something like 'moving 1 item' or 'copying 5 items'
        """
        ing_dict = { "move":"moving", "copy":"copying" }
        whatting = ing_dict[ drag_type] # moving or copying?
        return fix_plurals( "%s %d item(s)" % (whatting, len(nodes)) )
            # not quite the same as the shorter text for the QDragObject itself;
            #e might become more different in the future if we include the class
            # when they're all nodes of the same subclass...

    def statusbar_message(self, msg):
        # note: spelling difference.
        # maybe todo: rename all methods of that name like this.
        # [bruce 070531 & 081216]
        # todo: store current msg for this widget, so we can share statusbar
        # with other widgets; or better, the method we're calling should do that
        # for all widgets (or their parts) in a uniform way
        env.history.statusbar_msg( msg)

    # == Qt3 code for drag graphic, not yet ported, used only if debug_pref set [copied from Qt3/TreeWidget.py, bruce 070511]
    # == [update: I think this is now fully ported and used by default, long before 080507]

    def get_pixmap_for_dragging_nodes_Qt3(self, drag_type, nodes):
        #e [also put the drag-text-making code in another method near here? or even the dragobj maker?
        # or even the decision of what to do when the motion is large enough (ie let drag_handler role be only to notice that,
        # not to do whatever should be done at that point? as if its role was to filter our events into higher level ones
        # where one of them would be enoughMotionToStartADragEvent?] ###e yes, lots of cleanup is possible here...
        """
        Our current drag_handler can call this to ask us for a nice-looking pixmap
        representing this drag_type of these nodes,
        to put into its QDragObject as a custom pixmap.
        If we're not yet sure how to make one, we can just return None.
        """
        listview = self
        ##pixmap = QPixmap("/Nanorex/Working/cad/src/butterfly.png") # this works
        ##print pixmap,pixmap.width(),pixmap.height(),pixmap.size(),pixmap.depth()
        w,h = 160,130

        if 1:
            # but that's not good if len(nodes) is large, so use the following;
            # note that it depends on the details of hardcoded stuff in other functions
            # and those don't even have comments warning about that! ###
            # [bruce 050202 10:20am]:
            item = self.nodeItem(nodes[0]) # Grab a node to find out it's height
            ih = item.height()
            h = max(1,min(3,len(nodes))) * ih + ih # the last 24 is a guess for the text at the bottom
            w = self.get_drag_pixmap_width(nodes)

            if len(nodes)>3:
                h += 10 # for the "..."
            pass

        pixmap = QPixmap(w,h) # should have size w,h, depth of video mode, dflt optimization

        ##print pixmap,pixmap.width(),pixmap.height(),pixmap.size(),pixmap.depth()
        ## pixmap.fill(Qt.red) # makes it red; what's dragged is a pretty transparent version of this, but red... (looks nice)

        # CAREFUL: calling pixmap.fill() with no arguments creates problems
        # on Windows and Linux.  Text will not be drawn.  Be sure to include
        # a QColor argument so that the QPainter's setPen color can work
        # as expected.  Mark 050205
        #
        #pixmap.fill() # makes pixmap white, but text can't be drawn

        ## pixmap.fill(listview,0,0)
        ## following would fill with this widget's bgcolor...
        ## but it actually looks worse: very faint stripes, all faintly visible

        p = QPainter(pixmap)

        # Determine the pixmap's background and text color
        if 1: ## sys.platform == 'darwin': # Mac
            hicolor = Qt.white
            textcolor = Qt.black
# Note: I disabled the following special case, since it's not yet ported to Qt4 (it caused bug 2395) -- bruce 070514
##        else: # Window and Linux
##            colorgroup = listview.palette().active()
##            hicolor = QColor (colorgroup.highlight())
##            textcolor = QColor (colorgroup.highlightedText())

        pixmap.fill(hicolor) # Pixmap backgroup color
        p.setPen(textcolor) # Pixmap text draw color

        try:
            self.paint_nodes(p, drag_type, nodes)
            return pixmap
        except:
            p.end() # this is needed to avoid segfaults here
            print_compact_traceback("error making drag-pixmap: ")
            return None
        pass

    def paint_node(self, p, drag_type, node):
        """
        paint one node's item into QPainter p, and translate it down by the item's height
        """
        #e someday also paint the little openclose triangles if they are groups?
        #e unselect items first, at least for copy?
        item = self.nodeItem(node)
        width = self.paint_item( p, item) # was 99 for one example -- note, font used was a bit too wide
        height = item.height() # was 24
        p.translate(0,height)
        return width, height
            #btw exception in above (when two subr levels up and different named vars) caused a segfault!
            # [before we caught exception, and did p.end(), in routine creating painter p]
            ##Qt: QPaintDevice: Cannot destroy paint device that is being painted.  Be sure to QPainter::end() painters!
            ##NameError: global name 'item' is not defined
            ##Segmentation fault
            ##Exit 139
            # This means I need to catch exceptions inside the functions that make painters!
            # And keep the painters inside so they refdecr on the way out.
            # (Or actually "end" them... in fact that turns out to be needed too.)

    def paint_nodes(self, p, drag_type, nodes):
        """
        paint a dragobject picture of these nodes into the QPainter p; if you give up, raise an exception; return w,h used??
        """
        nn = len(nodes)
        if not nn:
            print nodes,"no nodes??" # when i try to drag the clipboard?? because unselected.
            # need to filter it somehow... should be done now, so this should no longer happen.
            # also the history warning is annoying... otoh i am not *supposed* to drag it so nevermind.
            return
        if nn >= 1:
            self.paint_node( p, drag_type, nodes[0])
        if nn >= 2:
            self.paint_node( p, drag_type, nodes[1])
        if nn >= 4: # yes, i know 4 and 3 appear in reversed order in this code...
            # put in "..."
            p.drawText(0,6," ...") # y=6 is baseline. It works! Only required 5 or 6 steps of directed evolution to get it right.
            p.translate(0,10) # room for a "..." we plan to put in above # 8 is a guess
        if nn >= 3:
            self.paint_node( p, drag_type, nodes[-1])
        #e also put in the same text we'd put into the statusbar
        text = self.get_whatting_n_items_text(drag_type, nodes)
        item = self.nodeItem(nodes[0]) # Grab a node to find out it's height
        h = item.height()
        w = self.get_drag_pixmap_width(nodes)
        flags = 0 # guess
        p.drawText(4,4,w,h,flags,text) # in this drawText version, we're supplying bounding rect, not baseline.
            #e want smaller font, italic, colored...
        return

    def get_drag_pixmap_width(self, nodes):
        return 168 ###STUB

        ## pt = QPainter(self, True) # Qt3 version
        #print "before QP"
        pt = QPainter(self) # this prints:
            ## QPainter::begin: Widget painting can only begin as a result of a paintEvent
            # then later we get
            ## QPainter::end: Painter not active, aborted
        #print "after QP"
        try:
            fontmetrics = pt.fontMetrics()
            w = pt.fontMetrics().width("Copying 99 items") ## AttributeError: width
            maxw = w * 1.5
            for node in nodes:
                item = self.nodeItem(node)
                cw = item.width(fontmetrics, self, 0)
                if  cw > w: w = cw
        except:
            w = 160
            print_compact_traceback( "Exception in get_drag_pixmap_width: w = %r: " % (w,) )
        pt.end()
            # guess: this line is what prints
            ## QPainter::end: Painter not active, aborted
            # and that must raise an exception which prevents the NameError from maxw.
            # But why don't I see that exception? Maybe it's something weirder than an exception?!?
        return min(maxw, w + 8)

    # == DND event methods ###SLATED FOR REVIEW and likely eventual rewrite or replacement

    def dragEnterEvent(self, event):
        event.acceptProposedAction()

    def dragMoveEvent(self, event):
        event.acceptProposedAction()

    def dropEvent(self, event):
        try:
            self._do_drop_or_raise_DoNotDrop(event)
        except DoNotDrop:
            event.ignore()
            self.mt_update()
        self._ongoing_DND_info = None # nodes
        return

    def _do_drop_or_raise_DoNotDrop(self, event): #bruce 070511 split this out
        """
        [private] Do a drop, or raise a DoNotDrop exception.
        Print a message, but do appropriate updates only if we succeed.
        """
        #bruce 070511 brought in several error messages from Qt3/TreeWidget.py,
        # modified some of them, made them use statusbar_message rather than history
        # redmsg
        item, rectjunk = self.item_and_rect_at_event_pos(event)
        if item is None:
            msg = "drop into empty space ignored (drops under groups " \
                  "not supported; drop onto them instead)"
            self.statusbar_message( msg)
            raise DoNotDrop()
        nodes, drag_type = self._ongoing_DND_info
        targetnode = item.node
        if targetnode in nodes:
            # don't print a message -- probably common for small mouse motions
            # (thus, don't leave this for drop_on_ok to find)
            # (not verified by test that it *will* find it, though it ought to)
            if _DEBUG2:
                print "debug warning: MT DND: targetnode in nodes, refusing drop" # new behavior, bruce 070509
            #e should generalize based on what Qt3 code does [obs cmt?]
            raise DoNotDrop()
        ok, whynot = Node_as_MT_DND_Target(targetnode).drop_on_ok(drag_type, nodes)
        if not ok:
            msg = "drop refused by %s" % quote_html(targetnode.name)
            if whynot:
                msg += ": %s" % (whynot,) #bruce 080303
            self.statusbar_message( msg )
            raise DoNotDrop()

        event.acceptProposedAction() # this should come after any DoNotDrop we might raise

        #bruce 070511 fixing some bugs by bringing in copiednodes & oldpart & unpick/pick code from Qt3/TreeWidget.py

        oldpart = nodes[0].part #bruce 060203
        oldassy = nodes[0].assy # not needed (assy can't be different yet) but ok for now [bruce 070511 comment]

        if drag_type == 'move':
            #bruce 060203 see if this helps implement NFR/bug 932 (which says, don't pick moved nodes or open their drop target);
            # do this first to make sure they're not picked when we move them... which might change current part [unverified claim].
            for node1 in nodes:
                node1.unpick()

        copiednodes = Node_as_MT_DND_Target(targetnode).drop_on(drag_type, nodes)
            #bruce 050203: copiednodes is a list of copied nodes made by drop_on
            # (toplevel only, when groups are copied).
            # For a move, it's []. We use it to select the copies, below.

        #bruce 050203 cause moved nodes to remain picked;
        # for copied nodes, we want the copies not originals to be picked.
        #bruce 060203 no longer pick moved nodes if moved into a different part, but still pick copies,
        # or nodes moved into the same part (or entire parts moved as a whole, only possible w/in clipboard).
        if drag_type == 'move':
            # this case rewritten by bruce 060203 re bug/NFR 932 (whose full fix also involved other files)
            self.unpick_all() # probably redundant now
            # pick the moved nodes, iff they are still in the same part.
            # run update_parts to help us; this covers case of moving an entire part w/in the clipboard,
            # in which it should remain picked.
            # (Someday it might be useful to look at nodes[0].find_selection_group() instead...)
            self.win.assy.update_parts()
                # FYI: drop_on sometimes does update_parts, but not always, so do it here to be safe. Optim this later.
                # Note: knowing anything about parts, and maybe even knowing self.win.assy, violates modularity
                # (re supposed generality of TreeWidget as opposed to modelTree); fix this later.
                # (I guess just about this entire method violates modularity, and probably so does much else in this file.
                #  As an older comment said:
                #  Note that this behavior should be subclass-specific, as should any knowledge of "the clipboard" at all!
                #  This needs review and cleanup -- maybe all this selection behavior needs to ask nodes what to do.)
            newpart = nodes[0].part
            if oldpart is newpart:
                for node1 in nodes:
                    node1.pick()
            pass
        else:
            self.unpick_all()
            # Pre-060203 code: we pick the copies iff they remain in the main part.
            # The reason we don't pick them otherwise is:
            # - NFR 932 thinks we shouldn't, in some cases (tho it probably doesn't think that
            #   if they are and remain in one clipboard part)
            # - there's a bug in drop_on which can put them in more than one clipboard item,
            #   but the selection is confined to one clipboard item.
            # With a little work we could improve this (in the cases not affected by that bug).
            # [comment revised, bruce 060203]
##            if not targetnode.in_clipboard(): # [method removed, 090119]
##                for node1 in copiednodes:
##                    node1.pick()
            #bruce 060203 revising this to use similar scheme to move case (but not when copies get split up):
            # pick the copies if they're in the same part as the originals.
            # (I believe this will either pick all copies or none of them.)
            self.win.assy.update_parts()
            for node1 in copiednodes:
                if node1.part is oldpart:
                    node1.pick()
            pass

        # ... too common for a history message, i guess...
        msg = "dropped %d item(s) onto %s" % \
              (len(nodes), quote_html(targetnode.name))
            #e should be more specific about what happened to them...
            # ask the target node itself? have drop_on return this info?
        msg = fix_plurals(msg)
        self.statusbar_message( msg)
        #bruce 050203: mt_update is not enough, in case selection changed
        # (which can happen as a side effect of nodes moving under new dads in the tree)
        self.win.win_update()
        return

    def unpick_all(self):
        self.treemodel.unpick_all()

    # == mouse click and selection drag events, and DND start code

    def mouseMoveEvent(self, event):
        if _DEBUG3:
            print "begin mouseMoveEvent"

        try:
            if self._mousepress_info_for_move is None:
                return

            (item, eventInRect, option_modifier) = self._mousepress_info_for_move

            # might we be a DND, or a dragged-out selection [nim]?
            # for DND we need item and eventInRect (and if true, we only start one if one is not ongoing);
            # for selection we need not eventInRect (and perhaps item).

            if item and eventInRect:
                # possibly start a DND (if not already doing one)
                if self._ongoing_DND_info is not None:
                    return
                ### old LOGIC BUGS, hopefully fixed as of bruce 070509
                # - should not do this unless we decided the press event was in a place that would permit it
                # - the dragged object should depend on that press, not on the cursor position now (unlike how index is computed below)
                dragLength = (event.globalPos() - self.mouse_press_qpoint).manhattanLength()
                if dragLength < 10: ## QApplication.startDragDistance(): ###REVIEW: is this the best distance threshhold to use?
                    # the mouse hasn't moved far enough yet
                    return
                self._start_DND( item, option_modifier)
                return

            #e dragging out a selection is nim; the hard parts include:
            # - need to know what items were in between the ones hit
            # - need to recompute selected items based on original ones,
            #   altered ones by drag, and selection inference rules
            #   for both picking (into kids) and unpicking (into parents)
            # - need faster update of state and display than we have
            # [bruce 070609 comments]
            return
        finally:
            if _DEBUG3:
                print "end mouseMoveEvent"
        pass

    # ==

    _mousepress_info_for_doubleclick = None, None # always a pair; always set; but only matters in QScrollArea implem

    def mousePressEvent(self, event, _doubleclick = False):
        """
        called by Qt on mousePress, or by our own method with private option _doubleclick = True
        """

        ## self.mouse_press_scrollpos = self.get_scrollpos("start of mousePressEvent")
        ###BUG: messes up manual scroll + click elsewhere (causing mt_update) -- this pos rules, discarding the manual scroll.
        # partial workaround for now -- do this only for a cmenu click... still causes that bug right after it, i think...
        # unless we del the attr then? no, we don't know *when* to del it -- "after updates caused by event X"???
            # This is more often correct than the one we get during a later paintEvent after a cmenu command,
            # for unknown reasons, so save it and use it (to preserve scroll position) instead of whatever we get then.
            # Note: this is unlikely to cure the scroll-jump-to-top that sometimes happens (on Mac) when the app is
            # brought to the foreground (also for unknown reasons, but the mouseclick that does that can even be on
            #  the dock area, presumably unseen by this object). Maybe we can intercept an "app now in foreground event"
            # and change its behavior? Should I browse the Qt source code for stray scrollToTops etc?

        self._mousepress_info_for_move = None

        _prior_mousepress_info_for_doubleclick = self._mousepress_info_for_doubleclick
        self._mousepress_info_for_doubleclick = None, None
            # Note: set below; used only when called later with _doubleclick, which is only done
            # by the QScrollArea implem subclass, not the QTreeView one.

        qp = event.globalPos()  # clone the point to keep it constant
        self.mouse_press_button = button = event.button() #bruce 070509
        self.mouse_press_buttons = buttons = event.buttons()
        self.mouse_press_modifiers = modifiers = event.modifiers()

        # simulate effect of fix_buttons / fix_event in the type-of-event analysis
        # [bruce 070509; later should ###REVIEW for consistency, or use one of those functions]
        option_modifier = (button == Qt.MidButton) or (modifiers & Qt.AltModifier) ###TODO: other conds like this should use this flag
        contextMenu = (button == Qt.RightButton) or (modifiers & Qt.MetaModifier)

        if contextMenu and hasattr(self, 'get_scrollpos'): # hasattr is kluge, varies by subclass [bruce 070529]
            # do this asap in case its value changes for mysterious reasons (as seems plausible from experience)
            self.mouse_press_scrollpos = self.get_scrollpos("start of mousePressEvent for cmenu")
            ###BUG: see comment for same thing at start of method
        else:
            self.mouse_press_scrollpos = None
            del self.mouse_press_scrollpos

        if _DEBUG2:
            print "\nMT mousePressEvent: button %r, buttons %s, modifiers %s, globalPos %r, pos %r" % \
                  (button,
                   describe_buttons(buttons), # str and repr only show the class, which is PyQt4.QtCore.MouseButtons; int works
                   describe_modifiers(modifiers),
                   (qp.x(), qp.y()),
                   (event.pos().x(), event.pos().y())
                  )

        self.mouse_press_qpoint = QPoint(qp.x(), qp.y()) # used in mouseMoveEvent to see if drag length is long enough

        item, rect = self.item_and_rect_at_event_pos(event)
            # note about coordinates:
            # in QTreeView implem, I am not sure which coords event and this rect are in, contents or viewport.
            # in QScrollArea implem, they should be in viewport (once item_and_rect_at_event_pos is bugfixed).

        eventInRect = False # might be changed below; says whether event hits rect including icon(??) and name/label
        if item is not None:
            self.statusbar_message( quote_html( item.node.name)) #bruce 070511 -- no other way to see long node names!
                # (at least for now -- tooltips are off for some reason, MT can't be widened, and it has no horizontal scrollbar,
                #  though it did a few days ago -- don't know why.)
            alreadySelected = item.node.picked #bruce 070509
            if _DEBUG2:
                print "visualRect coords",rect.left(), rect.right(), rect.top(), rect.bottom()
            qfm = QFontMetrics(QLineEdit(self).font())
            rect.setWidth(qfm.width(item.node.name) + _ICONSIZE[0] + 4)
            if _DEBUG2:
                print "visualRect coords, modified:",rect.left(), rect.right(), rect.top(), rect.bottom()
                # looks like icon and text, a bit taller than text (guesses)
            eventInRect = rect.contains(event.pos())
            if _DEBUG2:
                print "real item: eventInRect = %r, item = %r, alreadySelected = %r" % \
                      (eventInRect, item, alreadySelected)
        else:
            alreadySelected = False # bruce 070509
            if _DEBUG2:
                print "no item"
            pass

        if _doubleclick:
            # Either handle a double click (if all is ok for that), or do nothing for this event (return early).
            # (Question: will there be a mouseRelease event for this??)
            if eventInRect and not contextMenu \
               and not (modifiers & Qt.ShiftModifier) and not (modifiers & Qt.ControlModifier):
                olditem, oldrect_junk = _prior_mousepress_info_for_doubleclick
                if olditem and (olditem.node is item.node):
                    self.handle_doubleclick( event, item, rect) # this should do all needed updates
            return

        if eventInRect and not contextMenu \
           and not (modifiers & Qt.ShiftModifier) and not (modifiers & Qt.ControlModifier):
            assert item
            self._mousepress_info_for_doubleclick = (item, rect) # use this only if next call has _doubleclick true

        # if not eventInRect, should also figure out if we are to left or right (maybe; nim),
        # or on openclose decoration (certainly; done now)
        if not eventInRect and item and item.node.openable():
            # we're on item's row, and item has an openclose decoration (or should have one anyway) -- are we on it?
            left_delta = rect.left() - event.pos().x()
            if _DEBUG2:
                print "left_delta is %r" % left_delta
            if 0 < left_delta < 20:
                # guess; precise value doesn't matter for correctness, only for "feel" of the UI
                # (see also OPENCLOSE_AREA_WIDTH, but that's defined only for one of our two implems)
                # we presume they're toggling the openclose decoration
                node = item.node
                node.open = not node.open
                self.mt_update()
##                self.win.glpane.gl_update()
                if _DEBUG3:
                    print "returning from mousePressEvent (openclose case)"
                return
            pass

        # ISSUE: if this press starts a DND, we don't want the same
        # selection effects as if it doesn't. What did the Qt3 code do?
        # ###REVIEW that....
        # for now, save some info for mouseMoveEvent to use.
        # (Might be clearer if saved in separate attrs? #e)
        if not contextMenu:
            self._mousepress_info_for_move = (item, eventInRect, option_modifier)
                # value determines whether a mouse move after this press might
                # drag nodes, drag out a selection (nim), or do nothing
            if item:
                # figure out nodes to drag by DND, since we might change them
                # below but wish we hadn't (kluge until better fix is found)
                if alreadySelected:
                    self._dnd_nodes = self.topmost_selected_nodes( whole_nodes_only = True)
                        # note: using whole_nodes_only = True here should
                        # fix bug 2948 for DND [bruce 081218]
                else:
                    self._dnd_nodes = [item.node]

        # Flags indicating actions we might take (mostly, modifying selection)
        unselectOthers = False
            # if you set this, you must also set selectThis or unselectThis,
            # unless there's no item
            # i.e. not eventInRect ###k I think [bruce 070509]; see if we do
        selectThis = False
        unselectThis = False
        toggleThis = False # toggle selection state

        # See "Feature:Model Tree" in the public wiki for model tree mouse controls
        ###REVIEW: is it right to look at event.buttons() but ignore event.button(), here?
        # Evidently not (at least for cmenu on Mac)... Revising it [bruce 070509]
        if 1:
            if modifiers & Qt.ShiftModifier:
                if modifiers & Qt.ControlModifier:
                    # shift control left click
                    unselectOthers = False
                    toggleThis = eventInRect
                else:
                    # shift left click
                    unselectOthers = False
                    selectThis = eventInRect
                    unselectThis = False #bruce 070509 revised, was unselectThis = not eventInRect
            else:
                if modifiers & Qt.ControlModifier:
                    # control left click
                    ## pass
                    unselectThis = eventInRect #bruce 070509 revised, was a noop
                    selectThis = False
                else:
                    # left click
                    selectThis = eventInRect
                    unselectThis = False #bruce 070509 revised, was unselectThis = not eventInRect
                    if not (alreadySelected and contextMenu):
                        # this cond is bruce 070509 bugfix; this would make it wrong for DND,
                        # but to avoid that we saved the selected nodes earlier (see comments there for caveats).
                        unselectOthers = True

        if _DEBUG:
            print
            print "unselectOthers", unselectOthers
            print "selectThis", selectThis
            print "unselectThis", unselectThis
            print "toggleThis", toggleThis
            print "contextMenu", contextMenu
            print "SELECTED BEFORE <<<"
            print self.topmost_selected_nodes( whole_nodes_only = False)
            print ">>>"

        assert not (selectThis and toggleThis)   # confusing case to be avoided
        assert not (selectThis and unselectThis)   # confusing case to be avoided
        assert not eventInRect or not unselectOthers or (selectThis or unselectThis) #bruce 070509 added this

        # take whatever actions are required (one or more of the following, in order -- selection and maybe cmenu)

        updateGui = False # whether we need to update MT and/or GLPane at the end

        ###TODO: optimize by only setting updateGui if something changes
        if unselectOthers:
            ###REVIEW: this actually unselects all (this node too), not just
            # others -- is that ok? Even if it's inside a picked group?
            # I think it's ok provided we reselect it due to selectThis
            # (or specifically unselect it too).
            self.unpick_all() #bruce 070509 optimization
            updateGui = True
        if selectThis and item is not None:
            item.node.pick()
            updateGui = True
        if unselectThis and item is not None:
            item.node.unpick()
            updateGui = True
        if toggleThis and item is not None:
            assert not unselectOthers
            if item.node.picked:
                item.node.unpick()
            else:
                item.node.pick()
            updateGui = True

        if contextMenu:
            # Note: some of the above selection actions may have also been done.
            # In the QScrollArea implem, we need to redraw the MT to show them;
            # in the QTreeView implem, we apparently didn't need to, but it shouldn't hurt.
            if updateGui:
                self.mt_update()
            self._renamed_contextMenuEvent(event)
            # Why no updateGui = True here? Because the cmenu commands are supposed to do their own updates as needed.
            # But it probably has little effect now to not do it, because we probably do it anyway
            # during the selection phase above.

        if eventInRect and not contextMenu \
           and not (modifiers & Qt.ShiftModifier) and not (modifiers & Qt.ControlModifier):
            # plain left click --
            # also do direct left click effects for nodes that have any
            # [new feature, bruce 080213]
            assert item.node
            try:
                item.node.ModelTree_plain_left_click
            except AttributeError:
                pass # soon, this should never happen
            else:
                self.mt_update()
                    # kluge (not sure it'll work) -- try to update the MT
                    # at the start, not end, of long-running side effects
                    # in the following method, like animating a view change.
                    # (But we will still do it again below, in case something
                    #  changed during a long op here (this is ok since it
                    #  should be fast if nothing changed).
                item.node.ModelTree_plain_left_click()
            pass

        if _DEBUG:
            print "SELECTED AFTER <<<"
            print self.topmost_selected_nodes( whole_nodes_only = False)
            print ">>>"

        if updateGui: # this is often overkill, needs optim
            if _DEBUG3:
                print "doing updateGui at end of mousePressEvent"
            self.mt_update()
            self.win.glpane.gl_update()
            if _DEBUG4:
                ###### BUG: clicks in MT do gl_update as this shows,
                # but don't always redraw with the correct picked state!
                # Not yet reproducable except when testing fixed bug 2948.
                print "mt did gl_update, selected nodes are", self.topmost_selected_nodes()

        if _DEBUG3:
            print "end mousePressEvent"

        return # from mousePressEvent

    def mouseReleaseEvent(self, event):
        if _DEBUG3:
            print "begin/end mouseReleaseEvent (almost-noop method)"
        self._ongoing_DND_info = None

    # ==

    def contextMenuEvent(self, event): ###bruce hack, temporary, just to make sure it's no longer called directly
        if 1:
            print "\n *** fyi: MT: something called self.contextMenuEvent directly -- likely bug ***\n"
            print_compact_stack("hmm, who called it?: ")
                # Note: this can be called directly by app.exec_() in main.py,
                # and is, if we use microsoft mouse right click on iMac G5 in Qt 4.2.2, *or* control-left-click;
                # if that can't be changed (by finding an interceptable caller in common with mousePressEvent),
                # we might need to revise this to also do selection click actions,
                # but OTOH, for reasons unknown, mousePressedEvent is running *first* as if it was a left click
                # (but with an unknown modifier flag, the same one for either right-click or control-left-click),
                # which might remove the need for that if we can be sure it will always happen.
                #    We also need to know whether all this happens the same way for control-left-click on Mac (I guess yes),
                # and/or for other mice, and/or on Windows, and/or in later versions of Qt.
                #    Need to ###REVIEW:
                # - Qt docs for this (both about modifiers, and buttons/button, and sequence of event calls for contextMenuEvent)
                # - comments in fix_buttons (which we're not using but maybe should be)
                # - is it related to buttons not always including button?
                #   What's happening in that first event is buttons = 1 (left), button = 2 (right).
                #   This still leaves unexplained (1) who calls contextMenuEvent, (2) why two events occur at all.
                #   (Could it have anything to do with the tab control?? ###REVIEW)
                #   What about for a plain left click? Then we get button 1, buttons 0x1 (left) (inconsistent).
        return self._renamed_contextMenuEvent(event)

    def _renamed_contextMenuEvent(self, event):
        if _DEBUG3:
            print "begin _renamed_contextMenuEvent"
            # See TreeWidget.selection_click() call from TreeWidget.menuReq(), in the Qt 3 code...
            # but that's done by our caller, not us. We're no longer called directly from Qt, only from other methods here.
            # [bruce 070509]

        if hasattr(self, 'get_scrollpos'): #kluge, varies by subclass [bruce 070529]
            self.get_scrollpos("start of _renamed_contextMenuEvent")###k see if this is more correct than the one we get later...
                # not sure, since querying it may have had the effect of fixing the bug of it changing later! not sure yet...

        event.accept() ##k #bruce 070511, see if this fixes scroll to top for these events --
            # note, event is probably a mouse press, not a cmenu event per se
        nodeset = self.topmost_selected_nodes( whole_nodes_only = False)
        nodeset_whole = self.topmost_selected_nodes( whole_nodes_only = True)
            # note: nodeset_whole (with whole_nodes_only = True) is part of
            # fixing bug 2948, along with code to use it when certain cmenu
            # commands are run. REVIEW: it is not used for all cmenu commands,
            # since for some of them it might be suboptimal UI design
            # (e.g. Hide/Unhide).
        if 0:
            if len(nodeset_whole) != len(nodeset):
                print "len(nodeset_whole) = %d,  len(nodeset) = %d" % (len(nodeset_whole) , len(nodeset)) ###
                assert len(nodeset_whole) < len(nodeset)
            pass
        ###TODO: we should consolidate the several checks for the optflag condition into one place, maybe mousePressEvent.
        optflag = (((self.mouse_press_buttons & Qt.MidButton) or
                    (self.mouse_press_modifiers & Qt.AltModifier)) and 'Option' or None)
        cmenu_spec = self.treemodel.make_cmenuspec_for_set(nodeset, nodeset_whole, optflag)
        menu = makemenu_helper(self, cmenu_spec)
            #bruce 070514 fix bug 2374 and probably others by using makemenu_helper
        menu.exec_(event.globalPos())
        if _DEBUG3:
            print "end _renamed_contextMenuEvent"
        return

    # ==

    # Note: if we ever want our own keyPressEvent or keyReleaseEvent bindings, see the code and comments
    # in TreeWidget.keyPressEvent about doing this correctly, and for how Qt3 did the arrow key actions.
    # We will need the filter_key call it does (or an equivalent use of wrap_key_event as done in GLPane)
    # or NE1 will have a Mac-specific delete key bug whenever the MT has the focus. This does not happen now
    # since MWsemantics handles our key presses, and it passes them to GLPane, which has the bugfix for that.
    # See also the comments in filter_key(). [bruce 070517 comment]

    # ==

    def get_scrollpos(self, msg = ""):
        """
        Return the current scrollposition (as x,y, in scrollbar units), and if _DEBUG3 also print it using msg.
        """
        ## res = ( self.horizontalOffset(), self.verticalOffset() )
            # This is in pixels, and it apparently works, but it's not useful
            # because setting it (using QWidget.scroll) doesn't work properly and has bad side effects.
            # So, get it in "scrollbar units" instead, and set it by talking directly to the scrollbars.
        hsb = self.horizontalScrollBar()
        x = hsb and hsb.value() or 0
        vsb = self.verticalScrollBar()
        y = vsb and vsb.value() or 0
        res = x,y
        if _DEBUG3:
            if msg:
                msg = " (%s)" % (msg,)
            print "get_scrollpos%s returns %r" % (msg, res,)
        return res

    def set_scrollpos(self, pos): # used only in QTreeView implem, but should be correct in QScrollArea implem too
        """
        Set the scrollposition (as x,y, in scrollbar units), and if _DEBUG3 print various warnings if anything looks funny.
        """
        x, y = pos # this is in scrollbar units, not necessarily pixels
        hsb = self.horizontalScrollBar()
        if hsb:
            hsb.setValue(x) # even if x is 0, since we don't know if the current value is 0
        else:
            if x and _DEBUG3:
                print "warning: can't set scrollpos x = %r since no horizontalScrollBar" % (x,)
        vsb = self.verticalScrollBar()
        if vsb:
            vsb.setValue(y)
        else:
            if y and _DEBUG3:
                print "warning: can't set scrollpos y = %r since no verticalScrollBar" % (y,)
        pos1 = self.get_scrollpos("verifying set_scrollpos")
        if pos != pos1:
            if _DEBUG3:
                print "warning: tried to set scrollpos to %r, but it's now %r" % (pos,pos1)
        return

    def rename_node_using_dialog(self, node): #bruce 070531
        """
        [newly in public API -- ###doc that. Used by callers in more than one file.]
        Put up a dialog to let the user rename the given node. (Only one node for now.)
        Emit an appropriate statusbar message, and do necessary updates if successful.
        @see: Node.rename_using_dialog()
        """
        node.rename_using_dialog()
        return

    pass # end of class ModelTreeGui_common

# ==

# newer version of model tree gui which doesn't use QTreeView [bruce 070529] (older one is removed now)

MT_CONTENT_WIDTH = 500 # fixed content width for now

OPENCLOSE_AREA_WIDTH = 20
INDENT_OFFSET = 20
INDENT_0 = 20

MT_CONTENT_TOP_Y = 0

def x_for_indent(n):
    return INDENT_0 + INDENT_OFFSET * n

class MT_View(QtGui.QWidget):
    """
    contents view for ModelTreeGui
    """
    def __init__(self, parent, palette_widget, modeltreegui):
        QtGui.QWidget.__init__(self, parent)
        self.palette_widget = palette_widget #e rename?
        self.modeltreegui = modeltreegui
        self.treemodel = self.modeltreegui.treemodel ###KLUGE? not sure. consider passing this directly?
        self.get_icons()
        return

    def mt_update(self):
        self.modeltreegui.mt_update()

    def get_icons(self):
        # note: geticon calls QIcon, but painter has no drawIcon, and drawPixmap doesn't accept them,
        # so here we use getpixmap which calls QPixmap. We could also try QImage and painter.drawImage
        # (known to work for crate.bmp and other test images, but presumably slower).
        ## painter.drawImage(QtCore.QPoint(0, 0), self.image)

        if 0:
            # these are the pixmaps meant for groupbox buttons on the Mac; they're workable here:
            self.mac_collapsed = getpixmap("ui/modeltree/mac_collapsed_icon.png") # 16 x 15, centering looks best
            self.mac_expanded = getpixmap("ui/modeltree/mac_expanded_icon.png") # 16 x 8 (.width() x .height()), centering looks best
            # (The Windows groupbox icons, win_expand_icon.png and win_collapse_icon.png, are not useful in an MT.)
##            print "size:", self.mac_expanded.width(), self.mac_expanded.height(), # size 16 8

        # As of 070530 9pm the following are renamed copies of the Mac groupbox icons above,
        # and are the same on Mac and Windows, but we should replace them with imitations of
        # the standard ones for those platforms.
        # We load them all here, since a debug_pref can switch between Mac and Win styles at runtime.
        # The comments describe their conventional appearance (NIM for Win style) and its interpretation.
        self.mac_collapsed = getpixmap("ui/modeltree/mac_collapsed_mtnode.png") # triangle pointing right (indicates "collapsed")
        self.mac_expanded =  getpixmap("ui/modeltree/mac_expanded_mtnode.png") # triangle pointing down (indicates "expanded")
        self.win_collapsed = getpixmap("ui/modeltree/win_collapsed_mtnode.png") # plus in a box (indicates "expand action")
        self.win_expanded =  getpixmap("ui/modeltree/win_expanded_mtnode.png") # minus in a box (indicates "collapse action")

        ### TODO: also get content indicator icons here, ie yellow ghost @@@@

        # Default MT style depends on platform, but user can change it at runtime
        # to the same set of possibilities on all platforms. For now this uses debug_prefs.
        if sys.platform == 'darwin':
            # Macintosh
            self._icon_style_choice = Choice(['mac', 'win'])
            self._openclose_lines_choice = Choice_boolean_False
        else:
            # Windows or Linux
            self._icon_style_choice = Choice(['win', 'mac'])
            self._openclose_lines_choice = Choice_boolean_True
        return

    def _setup_openclose_style(self):
        """
        [private]
        As an optimization, choose the openclose icons (etc) just once before drawing.
        """
        style = debug_pref("Model Tree: openclose icon style", self._icon_style_choice,
                           non_debug = True, prefs_key = "A9/MT openclose icon style",
                           call_with_new_value = (lambda val: self.mt_update()) )
        if style == 'mac':
            self.collapsed_mtnode_icon = self.mac_collapsed
            self.expanded_mtnode_icon = self.mac_expanded
        else:
            self.collapsed_mtnode_icon = self.win_collapsed
            self.expanded_mtnode_icon = self.win_expanded

        self.draw_openclose_lines = debug_pref("Model Tree: openclose lines", self._openclose_lines_choice,
                           non_debug = True, prefs_key = "A9/MT openclose lines",
                           call_with_new_value = (lambda val: self.mt_update()) )
        return

    def sizeHint(self):
        # Note: this probably has no effect, since it's overridden by a resize in every mt_update.
        return QtCore.QSize(MT_CONTENT_WIDTH, 700)

    _painted = None # not {}, so not mutable; after first paintEvent will be a
        # dictionary, which maps node to (x, y) for whatever nodes were painted
        # during the last full repaint [bruce 080507]

    def paintEvent(self, event):
        # Note: if this paintEvent was in QAbstractScrollArea, Qt doc for that warns:
        #   Note: If you open a painter, make sure to open it on the viewport().
        # But, it's not in that (or its subclass QScrollArea), it's in the widget we're setting in that.
        # Note: we're painting using contents coords, not viewport coords (and this is working, as expected).
        # Question: would this change cliprect?
        ## painter.setViewport(0, 0, self.width(), self.height())
        if self.modeltreegui.MT_debug_prints():
            print 'paint' # note: this worked once we called update (it didn't require sizeHint to be defined)
        self._setup_full_repaint_variables()
            # needed before _paintnode can use us as its option_holder,
            # and for other reasons
        painter = QtGui.QPainter()
        painter.begin(self)
        try:
            topnodes = self.treemodel.get_topnodes()
            x, y = x_for_indent(0), MT_CONTENT_TOP_Y
            for node in topnodes:
                y = self.paint_subtree(node, painter, x, y)
            pass
        finally:
            painter.end()
        return

    def _setup_full_repaint_variables(self): #bruce 080507 split this out, extended it
        self._setup_openclose_style()
        # todo: optim: implem this: self._setup_prefs(),
        # so _paintnode needn't repeatedly test the same debug_prefs for each node
        self._f_nodes_to_highlight = self.treemodel.get_nodes_to_highlight()
            # a dictionary from node to an arbitrary value;
            # in future, could store highlight color, etc
        self._painted = {} # modified in paint_subtree when we call _paintnode
        return

    def paint_subtree(self, node, painter, x, y, line_to_pos = None, last_child = True):
        """
        Paint node and its visible subtree (at x,y in painter);
        return the y value to use for the next lower node.
           If this is a child node, the y position to which an "openclose line"
        (in case we're drawing them) needs to be drawn up to (from the center
         of our openclose icon) is passed in line_to_pos as an int; otherwise
        line_to_pos is None. Also, if last_child is False, we draw the openclose
        line (if we're drawing them) a bit lower down as well, so we can draw it
        before our openclose icon so as not to obscure it. Warning: those optional
        args are sometimes passed positionally.
        """
        openable = node.openable()
        open = node.open and openable
        if open:
            children = node.MT_kids() #bruce 080306 use MT_kids
        else:
            children = ()
        # openclose line
        draw_openclose_lines = self.draw_openclose_lines
        next_line_to_pos = y + ITEM_HEIGHT # easiest to do this all the time
        if draw_openclose_lines:
            # draw them first (including the start of one going down, if needed), so they won't obscure icons.
            # In this section we draw the ones that go through (under) the center of our own openclose icon.
            doanything = (line_to_pos is not None) or children
            if doanything:
                # compute openclose area center:
                lx1 = x - OPENCLOSE_AREA_WIDTH / 2
                ly1 = y + ITEM_HEIGHT / 2
                painter.save() # otherwise we mess up the node text color -- I guess _paintnode is not self-contained re that
                painter.setPen(QColor(Qt.gray)) # width 0, solid line
            if line_to_pos is not None:
                # vertical line
                if last_child:
                    painter.drawLine(lx1, line_to_pos, lx1, ly1 )
                else:
                    painter.drawLine(lx1, line_to_pos, lx1, y + ITEM_HEIGHT ) # this goes a bit lower down;
                        # the knowledge of how much lower down is also known to the caller
                        # (i.e. this recursive function, in children loop below)
                # horizontal line
                painter.drawLine(lx1, ly1, x + _ICONSIZE[0]/2, ly1)
            if children:
                painter.drawLine(lx1 + INDENT_OFFSET, ly1,
                                 lx1 + INDENT_OFFSET, next_line_to_pos)
            if doanything:
                painter.restore()
        # node type icon and node name text (possibly looking selected and/or disabled)
        _paintnode(node, painter, x, y, self.palette_widget, option_holder = self)
        self._painted[node] = (x, y)
        # openclose decoration -- uses style set up by _setup_openclose_style for Mac or Win
        if openable:
            if open:
                pixmap = self.expanded_mtnode_icon
            else:
                pixmap = self.collapsed_mtnode_icon
            w = pixmap.width()
            h = pixmap.height()
            painter.drawPixmap(x - (OPENCLOSE_AREA_WIDTH + w)/2, y + (ITEM_HEIGHT - h)/2, pixmap)
                # this adjusts posn for pixmap size, to center the pixmap
            pass
        y += ITEM_HEIGHT
        if open:
            x += INDENT_OFFSET
            for child in children:
                its_last_child = (child is children[-1]) # wrong if children can occur twice
                y0 = y
                y = self.paint_subtree(child, painter, x, y, next_line_to_pos, its_last_child)
                # following only matters if not its_last_child
                next_line_to_pos = y0 + ITEM_HEIGHT
        return y

    def any_of_these_nodes_are_painted(self, nodes): #bruce 080507, for cross-highlighting
        """
        @return: whether any of the given nodes were painted during the last
        full repaint of self.

        @rtype: boolean
        """
        if not self._painted:
            # called too early; not an error
            return False
        for node in nodes:
            where = self._painted.get(node) # (x, y) or None
            if where is not None:
                return True
            continue
        return False

    def look_for_y(self, y):
        """
        Given y (in contents coordinates), find the node drawn under it,
        and return (node, its depth, its y0).
        If no node is under it, return None, None, None.
        """
        y0 = MT_CONTENT_TOP_Y
        d = 0 # indent level
        if y < y0:
            return None, None, None
        for child in self.treemodel.get_topnodes():
            resnode, resdepth, resy0, y0 = self.look_for_y_recursive( child, y0, d, y)
            if resnode:
                return resnode, resdepth, resy0
        return None, None, None

    def look_for_y_recursive(self, node, y0, d, y):
        """
        assuming node is drawn at vertical position y0 (in contents coordinates)
        and indent level d, find the node in its visible subtree (perhaps node itself)
        which is drawn over position y; return (that node, its depth, its y0, None), or (None, None, None, next_y0),
        where next_y0 is the y coordinate for the next node below the given node and its visible subtree.
        Assume without checking that y >= y0.
        """
        y0 += ITEM_HEIGHT
        if y < y0:
            return node, d, y0 - ITEM_HEIGHT, None
        if node.open and node.openable():
            d += 1
            for child in node.MT_kids(): #bruce 080108 change for Block: use MT_kids
                resnode, resdepth, resy0, y0 = self.look_for_y_recursive( child, y0, d, y)
                if resnode:
                    return resnode, resdepth, resy0, y0
        return (None, None, None, y0)

    def statusbar_message(self, text): #bruce 070531; still used?
        self.modeltreegui.statusbar_message(text)
        return

    pass # end of class MT_View

# ===

class FakeItem:
    def __init__(self, node):
        self.node = node
    def height(self):
        return ITEM_HEIGHT
    pass

class ModelTreeGui(QScrollArea, ModelTreeGui_common):
    """
    The GUI part of the NE1 model tree widget.
    """
    # REVIEW: rename class and module to ModelTreeGUI?
    # (class is now ending Gui not GUI; module is now lowercase at start)
    # [bruce 081216 question]
    #
    #bruce 070529-30 rewrite of some of [now-removed] class ModelTreeGui_QTreeView
    def __init__(self, win, name, treemodel, parent = None):
        ## print "what are these args?", win, name, treemodel, parent
        # win = <MWsemantics.MWsemantics object at 0x4ce8a08>
        # name = modelTreeView
        # treemodel = <modelTree.TreeModel.TreeModel instance at 0x4cfb3a0>
        # parent = <PyQt4.QtGui.QWidget object at 0x4cff468>
        del name
        QScrollArea.__init__(self, parent)
        ###e  setWidgetResizable ? (default False - honors size of widget - done when fitToWindow checked, in the imageviewer example)
        # adjustSize? (called when *not* fitToWindow -- more like our case... i don't fully understand it from docs)
        # updateGeometry? (call when sizeHint changed)
##        self.setSizePolicy(QtGui.QSizePolicy.Ignored, QtGui.QSizePolicy.Ignored)

        self.setContextMenuPolicy(Qt.PreventContextMenu) #bruce 070509 change; I hope it will prevent direct calls of contextMenuEvent

        # this has to be done after QScrollArea.__init__, since it assumes QWidget init has occurred
        ModelTreeGui_common.__init__(self, win, treemodel) ## mouse and DND methods -- better off in view widget or here??
            # will this intercept events meant for the scrollbars themselves??? need to use the contents event methods?

        self.view = MT_View( self, self, self) # args are: parent, palette_widget, modeltreegui
        self.setWidget(self.view)

        # Model Tree background color. Mark 2007-06-04
        mtColor = RGBf_to_QColor(env.prefs[mtColor_prefs_key])
        self.setPalette(getPalette(None, QPalette.Window, mtColor))

        #e not sure if mt_update would be safe at this point (were cooperating objects fully initialized?)

##        self._debug_scrollbars("init") # they have arbitrary-looking values at this point, later corrected, evidently

        return

    def _scrollbars(self):
        hsb = self.horizontalScrollBar()
        vsb = self.verticalScrollBar()
        return hsb, vsb

    def _debug_scrollbars(self, when): # not normally called except when debugging
        print "debug_scrollbars (%s)" % when
        hsb, vsb = self._scrollbars() # they are always there, even when not shown, it seems
##        if hsb:
##            print "hsb pageStep = %r, singleStep = %r, minimum = %r, maximum = %r" % \
##                  (hsb.pageStep(), hsb.singleStep(), hsb.minimum(), hsb.maximum())
        if vsb:
            print "vsb pageStep = %r, singleStep = %r, minimum = %r, maximum = %r" % \
                  (vsb.pageStep(), vsb.singleStep(), vsb.minimum(), vsb.maximum())
        return

    def nodeItem(self, node):
        return FakeItem(node)

    def mt_update(self):
        """
        part of the public API:
        - recompute desired size for scroll area based on visible nodes
        - resize scroll area if needed
        - _do_widget_updates to make sure we get redrawn soon
        """
        if self.MT_debug_prints():
            print "mt_update", time.asctime()

        # probably we need to scan the nodes and decide what needs remaking
        # (on next paintevent) and how tall it is; should we do this in MT_View? yes.
        self.__count = 0
        def func(node):
            self.__count += 1 # using a local var doesn't work, due to Python scoping
        self.treemodel.recurseOnNodes(func, visible_only = True)
        NUMBER_OF_BLANK_ITEMS_AT_BOTTOM = 1
            # not 0, to let user be certain they are at the end
            # [bruce 080306 new feature]
        self.__count += NUMBER_OF_BLANK_ITEMS_AT_BOTTOM
        height = self.__count * ITEM_HEIGHT
        if self.MT_debug_prints():
            print "mt_update: total height", height

##        self._debug_scrollbars("mt_update pre-resize")

        self.view.resize(MT_CONTENT_WIDTH, height)
        #e updateGeometry? guess: this does that itself. I don't know for sure,
        # but the scrollbars do seem to adjust properly.

##        self._debug_scrollbars("mt_update post-resize")

        hsb, vsb = self._scrollbars()
        if vsb:
            vsb.setSingleStep( ITEM_HEIGHT )
            vsb.setPageStep( vsb.pageStep() / ITEM_HEIGHT * ITEM_HEIGHT )
                #k Note: It might be enough to do setSingleStep once during init, since the value seems to stick --
                # but the page step is being quantized, so that has to be redone every time.
                # NFR: I don't know how to set a "motion quantum" which affects user-set positions too.

##        self._debug_scrollbars("mt_update post-correct")

        self._do_widget_updates()

##        self._debug_scrollbars("mt_update post-update")
        return

    def _do_widget_updates(self): #bruce 080507 split this out
        """
        [private]

        Call QWidget.update on our widgets, so that Qt will redraw us soon
        (which it will do by calling paintEvent in our MT_View, self.view)
        """
        self.view.update() # this works
        self.update() # this alone doesn't update the contents, but do it anyway
            # to be sure we get the scrollbars (will it be enough?)
        return

    def paint_item(self, painter, item): # probably only used for DND drag graphic (in superclass)
        x,y = 0,0
        node = item.node
        _paintnode(node, painter, x, y,
                   self.view, # self.view is a widget used for its palette
                   option_holder = None # could be self.view,
                       # but only if we told it to _setup_full_repaint_variables
                  )
        width = 160 ###k guess; should compute in paint and return, or in an aux subr if Qt would not like that
        return width

    def repaint_some_nodes(self, nodes): #bruce 080507, for cross-highlighting
        """
        For each node in nodes, repaint that node, if it was painted the last
        time we repainted self as a whole. (Not an error if it wasn't.)
        """
        # we can't do it this way (using a hypothetical method,
        # not yet implemented in class MT_View), since we're not
        # inside paintEvent:
        ## self.view.repaint_some_nodes(nodes)
        if self.view.any_of_these_nodes_are_painted(nodes):
            self._do_widget_updates() # also called by mt_update
        return

    def item_and_rect_at_event_pos(self, event):
        """
        Given a mouse event, return the item under it (or None if no item),
        and a QRect encompassing certain(?) parts of it (or None if no item).
        ### which parts?
        ### what coords?
        """
        pos = event.pos()
        x, y = pos.x(), pos.y()
##        print "viewport pos x = %r, y = %r" % (x,y)

        dx, dy = self.get_scrollpos("item_and_rect_at_event_pos") # in pixels (will that remain true if we scroll by items?)

        cx = x + dx
        cy = y + dy

        # do item hit test in contents (inner) coords cx, cy, but return item's rect in viewport (outer) coords x, y.
        # Note: in variable names for x or y coords, we use 'c' to mean "contents coords" (otherwise viewport),
        # and suffix '0' to mean "coordinate of item reference point (top left of type icon)" (otherwise of mouse event).

        node, depth, cy0 = self.view.look_for_y( cy)
        if node:
            item = self.nodeItem(node)
            if 1:
                cx0 = x_for_indent(depth)
                    # Note: this is correct if the rect is supposed to be including the type icon -- is it? I think so.
                    # (This affects whether clicks on type icon can select the node. I think it's good if they can.)
                x0 = cx0 - dx
                y0 = cy0 - dy
                w = MT_CONTENT_WIDTH #STUB, but good enough for now --
                    # maybe better than being correct re clicks to right of text?? not sure.
                    # To make it correct, could use the code that makes a temporary QLineEdit, if slow is ok (I think it is)...
                    # Wait, that code is being used on exactly this value in our caller, so what we set here doesn't matter!
                h = ITEM_HEIGHT
                rect = QRect( x0, y0, w, h) # viewport coords
            return item, rect
        else:
            return None, None
        pass

    def set_scrollpos(self, pos):
        assert 0, "don't use set_scrollpos in this QScrollArea-using subclass, without thinking twice!"

    def mouseDoubleClickEvent(self, event):
        self.mousePressEvent(event, _doubleclick = True) # this calls the following method in self (sometimes)

    def handle_doubleclick(self, event, item, rect):
        # print "handle_doubleclick" # this gets called when expected
        # temporary kluge, 070531, to be released for A9: use a dialog.
        # (Later we'll try using an in-place QLineEdit; some stub code for that is elsewhere in this file.)
        node = item.node
        self.rename_node_using_dialog( node) # note: this checks node.rename_enabled() first
        return

    pass # end of class ModelTreeGui

# bugs in new class ModelTreeGui based on QScrollArea [070531 2pm PT]:
#
# - mouse events:
#   openclose - works now
#   selection - some works, rest not tested
#   cmenus - some work, except bug [fixed now] in selection update when menu is shown, i.e.
#    it fails to do click select behavior in visible way, when putting up menu, tho it probably does it internally
#    [fixed, tho update is too slow -- incremental redrawing of changed items (when only selection changes) would be better]
#   DND - seems to work; not fully tested
#   renaming - in place is NIM; using dialog works now
#   key events: arrow keys should move the selectedness up and down in the nodes, but now they move the scrollbar.
#
# - drawing:
#   - decoration icons need improvement
#   - bg color is wrong (or, need transparent icons)
#   - it's missing the header that says "model tree" -- we decided that's a feature, not a bug
#   - too slow
#     - set painter state less often? (esp for text)
#     - profile it?
#     - see if text or icons are taking the time
#     - is icon caching helping?
#     - make an image to keep it in, so it scrolls faster?
#     Maybe enough to just draw only in inval rect.
#
# - other:
#   - hsb always there, fixed content width
#   - rename_enabled ignored -- probably fixed
#   - maybe so are some properties about DND being permitted (not sure)
#
# - highly desired NFRs for A10:
#   - cross-highlighting with GLPane
#   - DND:
#     - autoscrolling during DND
#     - drop on group puts node before other members [done, in another file, for NE1 v1.0.0, bruce 080414]
#     - drop-point highlighting, between nodes

# end