summaryrefslogtreecommitdiff
path: root/cad/src/utilities/debug_prefs.py
blob: 02886366265a80cc1e0c663d9d1f48b02331355f (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
# Copyright 2005-2008 Nanorex, Inc.  See LICENSE file for details.
"""
debug_prefs.py -- user-settable flags to help with debugging;
serves as a prototype of general first-class-preference-variables system.

Also contains some general color/icon/pixmap utilities which should be refiled.

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

History:

By Bruce, 050614
"""

from utilities.constants import noop
from utilities.constants import black, white, red, green, blue, gray, orange, yellow, magenta, pink

from utilities.Comparison import same_vals

    # note: qt4transition imports debug_pref from this module, which is one reason this module can't import
    # print_compact_traceback at toplevel. This should be fixed; it should be ok for this
    # module to import from debug. (There may be other reasons it can't in the current code.)
    # [bruce 070613 comment]

import foundation.env as env
# see below for "import preferences" at runtime; we can't import it here due to errors caused by recursive import

_NOT_PASSED = [] # private object for use as keyword arg default [bruce 070110, part of fixing bug of None as Choice value]
    # (note, the same global name is used for different objects in preferences.py and debug_prefs.py)

debug_prefs = {} # maps names of debug prefs to "pref objects"

def debug_pref(name, dtype, **options ): #bruce 070613 added call_with_new_value
    """
    Public function for declaring and registering a debug pref and querying its value.
       Example call: chem.py rev 1.151 (might not  be present in later versions).
       Details: If debug_prefs[name] is known (so far in this session),
    return its current value, and perhaps record the fact that it was used.
       If it's not known, record its existence in the list of active debug prefs
    (visible in a submenu of the debug menu [#e and/or sometimes on the dock])
    and set it to have datatype dtype, with an initial value of dtype's default value,
    or as determined from prefs db if prefs_key option is passed and if this pref has been stored there.
    (Treat dtype's default value as prefs db default value, in that case.)
    (And then record usage and return value, just as in the other case.)
       If prefs_key is passed, also arrange to store changes to its value in user prefs db.
    The prefs_key option can be True (meaning use the name as the key, after a hardcoded prefix),
    or a string (the actual prefs key).
       If non_debug option is true, pref is available even to users who don't set ATOM-DEBUG.
       If call_with_new_value is passed (as a named argument -- no positional call is supported),
    and is not None, it will be called with the new value whenever the value is changed from the
    debug menu. It will NOT be called immediately with the current value, since some debug prefs
    are called extremely often, and since the caller can easily do this if desired. If this option
    was passed before (for the same debug_pref), the old function will be discarded and not called again.
       WARNING: the call_with_new_value function is only called for changes caused by use of the debug menu;
    it probably should also be called for prefs db changes done by other means, but that's not yet
    implemented. (It will be easy to add when it's needed.)
    """
    call_with_new_value = options.pop('call_with_new_value', None)
    try:
        dp = debug_prefs[name]
    except KeyError:
        debug_prefs[name] = dp = DebugPref(name, dtype, **options)
    dp.set_call_with_new_value_function(call_with_new_value)
    return dp.current_value()

def debug_pref_object(name): #bruce 060213 experiment
    # might be useful for adding subscribers, but, this implem has error
    # if no one called debug_pref on name yet!
    # basic logic of scheme for this needs revision.
    return debug_prefs[name]

class Pref:
    # might be merged with the DataType (aka PrefDataType) objects;
    # only used in DebugPref as of long before 080215
    """
    Pref objects record all you need to know about a currently active
    preference lvalue [with optional persistence as of 060124]
    """
    # class constants or instance variable initial values (some might be overridden in subclasses)
    prefs_key = None
    print_changes = False
    non_debug = False # should this be True here and False in DebugPref subclass? decide when it matters.
    classname_for_repr = 'Pref' #bruce 070430
##    starts_out_from_where = "from prefs db"
    def __init__(self, name, dtype, prefs_key = False, non_debug = False, subs = ()): #bruce 060124 added prefs_key & non_debug options
        #e for now, name is used locally (for UI, etc, and maybe for prefs db);
        # whether/how to find this obj using name is up to the caller
        self.name = name
        assert name and type(name) == type("") #bruce 060124 added assert (don't know if this was already an implicit requirement)
        self.dtype = dtype # a DataType object
        self.value = self._dfltval = dtype.get_defaultValue() # might be changed below  #bruce 070228 added self._dfltval
        if prefs_key: #bruce 060124 new feature
            if prefs_key is True:
                prefs_key = "_debug_pref_key:" + name #e should prefix depend on release-version??
            assert type(prefs_key) == type(""), "prefs_key must be True or a string"
            assert prefs_key # implied by the other asserts/ifs
            self.prefs_key = prefs_key
            import foundation.preferences as preferences # make sure env.prefs is initialized [bruce 070110 precaution]
                # (evidently ok this early, but not sure if needed -- it didn't fix the bug in a Choice of None I commented on today)
            self.value = env.prefs.get( prefs_key, self.value ) ###k guess about this being a fully ok way to store a default value
                # Note: until I fixed preferences.py today, this failed to store a default value when self.value was None. [bruce 070110]
            # note: in this case, self.value might not matter after this, but in case it does we keep it in sync before using it,
            # or use it only via self.current_value() [bruce 060209 bugfix]
            if self.print_changes and not same_vals(self.value, self._dfltval): #bruce 070228 new feature for debug_pref
                # note: we use same_vals to avoid bugs in case of tuples or lists of Numeric arrays.
                # note: this only does printing of initial value being non-default;
                # printing of changes by user is done elsewhere, and presently goes
                # to both history widget and console print. We'll follow the same policy
                # here -- but if it's too early to print to history widget, env.history
                # will print to console, and we don't want it to get printed there twice,
                # so we check for that before printing it to console ourselves. [bruce 071018]
                msg = "Note: %s (default %r) starts out %r" % \
                      (self, self._dfltval, self.value) ## + " %s" % self.starts_out_from_where
                if not getattr(env.history, 'too_early', False):
                    print msg
                env.history.message(msg, quote_html = True, color = 'orange')
            pass
        self.non_debug = non_debug # show up in debug_prefs submenu even when ATOM-DEBUG is not set?
        self.subscribers = [] # note: these are only called for value changes due to debug menu
        for sub in subs:
            self.subscribe_to_changes(sub)
        self.subscribe_to_changes( self._fulfill_call_with_new_value )
            # note: creates reference cycle (harmless, since we're never destroyed)
        return
    __call_with_new_value_function = None
    def set_call_with_new_value_function(self, func):
        """
        Save func as our new "call with new value" function. If not None,
        it will be called whenever our value changes due to use of the debug menu.
        (In the future we may extend this to also call it if the value changes by other means.)
        It will be discarded if this method is called again (to set a new such function or None).
        """
        self.__call_with_new_value_function = func
        ## don't do this: self._fulfill_call_with_new_value()
        return
    def _fulfill_call_with_new_value(self):
        from utilities.debug import print_compact_traceback # do locally to avoid recursive import problem
        func = self.__call_with_new_value_function
        if func is not None:
            val = self.current_value()
            try:
                func(val)
            except:
                print_compact_traceback("exception ignored in %s's call_with_new_value function (%r): " % (self, func) )
                # (but don't remove func, even though this print might be verbose)
                # Note: if there are bugs where func becomes invalid after a relatively rare change
                # (like after a new file is loaded), we might need to make this a history error and make it non-verbose.
            pass
        return
    def subscribe_to_changes(self, func): #bruce 060216, untested, maybe not yet called, but intended to remain as a feature ###@@@
        """
        Call func with no arguments after every change to our value from the debug menu,
        until func() first returns true or raises an exception (for which we'll print a traceback).
           Note: Doesn't detect independent changes to env.prefs[prefs_key] -- for that, suggest using
        env.pref's subscription system instead.
        """
        self.subscribers.append(func)
    def unsubscribe(self, func):
        self.subscribers.remove(func) # error if not subscribed now
    def current_value(self):
        if self.prefs_key:
            # we have to look it up in env.prefs instead of relying on self.value,
            # in case it was independently changed in prefs db by other code [bruce 060209 bugfix]
            # (might also help with usage tracking)
            self.value = env.prefs[self.prefs_key] #k probably we could just return this and ignore self.value in this case
        return self.value
    def current_value_is_not_default(self): #bruce 080312
        return not same_vals( self.current_value(), self._dfltval)
    def changer_menuspec(self):
        """
        Return a menu_spec suitable for including in some larger menu (as item or submenu is up to us)
        which permits this pref's value to be seen and/or changed;
        how to do this depends on datatype (#e and someday on other prefs!)
        """
        def newval_receiver_func(newval):
            from utilities.debug import print_compact_traceback # do locally to avoid recursive import problem
            assert self.dtype.legal_value(newval), "illegal value for %r: %r" % (self, newval)
                ###e change to ask dtype, since most of them won't have a list of values!!! this method is specific to Choice.
            if self.current_value() == newval: #bruce 060126; revised 060209 to use self.current_value() rather than self.value
                if self.print_changes:
                    print "redundant change:",
                ##return??
            self.value = newval
            extra = ""
            if self.prefs_key:
                env.prefs[self.prefs_key] = newval
                # note: when value is looked up, this is the assignment that counts (it overrides self.value) [as of 060209 bugfix]
                extra = " (also in prefs db)"
            if self.print_changes:
                ## msg = "changed %r to %r%s" % (self, newval, extra)
                msg = "changed %s to %r%s" % (self, newval, extra) # shorter version for console too [bruce 080215]
                print msg
                msg = "changed %s to %r%s" % (self, newval, extra) # shorter version (uses %s) for history [bruce 060126]
                env.history.message(msg, quote_html = True, color = 'gray') #bruce 060126 new feature
            for sub in self.subscribers[:]:
                #bruce 060213 new feature; as of 070613 this also supports the call_with_new_value feature
                from utilities.debug import print_compact_traceback # do locally to avoid recursive import problem
                try:
                    unsub = sub()
                except:
                    print_compact_traceback("exception ignored in some subscriber to changes to %s: " % self)
                    unsub = 1
                if unsub:
                    self.subscribers.remove(sub) # should work properly even if sub is present more than once
                continue
            return # from newval_receiver_func
        return self.dtype.changer_menuspec(self.name, newval_receiver_func, self.current_value())
    def __repr__(self):
        extra = ""
        if self.prefs_key:
            extra = " (prefs_key %r)" % self.prefs_key
        return "<%s %r at %#x%s>" % (self.classname_for_repr, self.name, id(self), extra)
    def __str__(self):
        return "<%s %r>" % (self.classname_for_repr, self.name,)
    pass

# ==

class DebugPref(Pref):
    classname_for_repr = 'debug_pref' #bruce 070430, for clearer History messages
    print_changes = True
##    starts_out_from_where = "(from debug prefs submenu)"
    pass

# == datatypes

class DataType:
    """
    abstract class for data types for preferences
    (subclasses are for specific kinds of data types;
     instances are for data types themselves,
     but are independent from a specific preference-setting
     that uses that datatype)
    (a DataType object might connote pref UI aspects, so it's not just
     about the data, but it's largely a datatype; nonetheless,
     consider renaming it to PrefDataType or so ###e)
    """
    #e some default method implems; more could be added
    ###e what did i want to put here??
    def changer_menu_text(self, instance_name, curval = None):
        """
        Return some default text for a menu meant to display and permit changes to a pref setting
        of this type and the given instance-name (of the pref variable) and current value.
        (API kluge: curval = None means curval not known, unless None's a legal value.
        Better to separate these into two args, perhaps optionally if that can be clean. #e)
        """
        if curval is None and not self.legal_value(curval): #####@@@@ use it in the similar-code place
            cvtext = ": ?" # I think this should never happen in the present calling code
        else:
            cvtext = ": %s" % self.short_name_of_value(curval)
        return "%s" % instance_name + cvtext
    def short_name_of_value(self, value):
        return self.name_of_value(value)
    def normalize_value(self, value):
        """
        most subclasses should override this; see comments in subclass methods;
        not yet decided whether it should be required to detect illegal values, and if so, what to do then;
        guess: most convenient to require nothing about calling it with illegal values; but not sure;
        ##e maybe split into public and raw forms, public has to detect illegals and raise ValueError (only).
        """
        return value # wrong for many subclasses, but not all (assuming value is legal)
    def legal_value(self, value):
        """
        Is value legal for this type? [Not sure whether this should include "after self.normalize_value" or not]
        """
        try:
            self.normalize_value(value) # might raise recursion error if that routine checks for value being legal! #e clean up
            return True # not always correct!
        except:
            # kluge, might hide bugs, but at least in this case (and no bugs)
            # we know we'd better not consider this value legal!
            return False
    pass

def autoname(thing):
    return `thing` # for now

class Choice(DataType):
    #e might be renamed ChoicePrefType, or renamed ChoicePref and merged
    # with Pref class to include a prefname etc
    """
    DataType for a choice between one of a few specific values,
    perhaps with names and comments and order and default.
    """
    # WARNING: before 070110, there was a bug if None was used as one of the choices, but it should be ok now,
    # except that the "API kluge: curval = None means curval not known, unless None's a legal value"
    # in docstring of self.changer_menu_text has not been reviewed regarding this. ###e [bruce 070110 comment]
    def __init__(self, values = None, names = None, names_to_values = None, defaultValue = _NOT_PASSED):
        #e names_to_values should be a dict from names to values; do we union these inits or require no redundant ones? Let's union.
        if values is not None:
            values = list(values) #e need more ways to use the init options
        else:
            values = []
        if names is not None:
            assert len(names) <= len(values)
            names = list(names)
        else:
            names = []
        while len(names) < len(values):
            i = len(names) # first index whose value needs a name
            names.append( autoname(values[i]) )
        if names_to_values:
            items = names_to_values.items()
            items.sort()
            for name, value in items:
                names.append(name)
                values.append(value)
        self.names = names
        self.values = values
        #e nim: comments
        self._defaultValue = self.values[0] # might be changed below
        self.values_to_comments = {}
        self.values_to_names = {}
        for name, value in zip(self.names, self.values):
            self.values_to_names[value] = name
            if defaultValue is not _NOT_PASSED and defaultValue == value: # even if defaultValue is None!
                # There used to be a bug when None was a legal value but no defaultValue was passed,
                # in which this code would change self._defaultValue to None. I fixed that bug using _NOT_PASSED.
                # This is one of two changes which makes None acceptable as a Choice value.
                # The other is in preferences.py dated today. [bruce 070110]
                self._defaultValue = value
    def name_of_value(self, value):
        return self.values_to_names[value]
    def get_defaultValue(self):
        # WARNING [can be removed soon if nothing goes wrong]:
        # When I renamed this to make it consistent in capitalization
        # with similar attributes, I also renamed it to be clearly a get method,
        # since in most code this name is used for a public attribute instead.
        # AFAIK it has only two defs and two calls, all in this file. [bruce 070831]
        return self._defaultValue
    def legal_value(self, value):
        """
        Is value legal for this type?
        [Not sure whether this should include "after self.normalize_value" or not]
        """
        return value in self.values
    def changer_menuspec( self, instance_name, newval_receiver_func, curval = None): # for Choice (aka ChoicePrefType)
        #e could have special case for self.values == [True, False] or the reverse
        text = self.changer_menu_text( instance_name, curval) # e.g. "instance_name: curval"
        submenu = submenu_from_name_value_pairs( zip(self.names, self.values),
                                                 newval_receiver_func,
                                                 curval = curval,
                                                 indicate_defaultValue = True, #bruce 070518 new feature
                                                 defaultValue = self._defaultValue
                                                )
        #e could add some "dimmed info" and/or menu commands to the end of submenu

        #e could use checkmark to indicate non_default = not same_vals(self._defaultValue, curval),
        # but too confusing if _defaultValue is True and curval is False...
        # so nevermind unless I figure out a nonfusing indication of that
        # that is visible outside the list of values submenu
        # and doesn't take too much room.
        # Maybe appending a very short string to the text, in changer_menu_text?
        # [bruce 080312 comment]
        return ( text, submenu )
    pass

Choice_boolean_False = Choice([False, True])
Choice_boolean_True =  Choice([False, True], defaultValue = True)

def submenu_from_name_value_pairs( nameval_pairs,
                                   newval_receiver_func,
                                   curval = None,
                                   mitem_value_func = None,
                                   indicate_defaultValue = False,
                                   defaultValue = None
                                   ):
    #bruce 080312 revised to use same_vals (2 places)
    from utilities.debug import print_compact_traceback # do locally to avoid recursive import problem
    submenu = []
    for name, value in nameval_pairs:
        text = name
        if indicate_defaultValue and same_vals(value, defaultValue):
            #bruce 070518 new feature
            text += " (default)"
        command = ( lambda ## event = None,
                           func = newval_receiver_func,
                           val = value :
                        func(val) )
        mitem = ( text,
                  command,
                  same_vals(curval, value) and 'checked' or None
                )
        if mitem_value_func is not None:
            try:
                res = "<no retval yet>" # for error messages
                res = mitem_value_func(mitem, value)
                if res is None:
                    continue # let func tell us to skip this item ###k untested
                assert type(res) == type((1, 2))
                mitem = res
            except:
                print_compact_traceback("exception in mitem_value_func, or error in retval (%r): " % (res,))
                    #e improve, and atom_debug only
                pass
        submenu.append(mitem)
    return submenu

class ColorType(DataType): #e might be renamed ColorPrefType or ColorPref
    """
    Pref Data Type for a color. We store all colors internally as a 3-tuple of floats
    (but assume ints in [0,255] are also enough -- perhaps that would be a better internal format #e).
    """
    #e should these classes all be named XxxPrefType or so? Subclasses might specialize in prefs-UI but not datatype per se...
    def __init__(self, defaultValue = None):
        if defaultValue is None:
            defaultValue = (0.5, 0.5, 0.5) # gray
        self._defaultValue = self.normalize_value( defaultValue)
    def normalize_value(self, value):
        """
        Turn any standard kind of color value into the kind we use internally -- a 3-tuple of floats from 0.0 to 1.0.
        Return the normalized value.
        If value is not legal, we might just return it or we might raise an exception.
        (Preferably ValueError, but for now this is NIM, it might be any exception, or none.)
        """
        #e support other python types for value? Let's support 3-seq of ints or floats, for now.
        # In future might want to support color name strings, QColor objects, ....
        r,g,b = value
        value = r,g,b # for error messages in assert
        assert type(r) == type(g) == type(b), \
               "color r,g,b components must all have same type (float or int), not like %r" % (value,)
        assert type(r) in (type(1), type(1.0)), "color r,g,b components must be float or int, not %r" % (value,)
        if type(r) == type(1):
            r = r/255.0 #e should check int range
            g = g/255.0
            b = b/255.0
        #e should check float range
        value = r,g,b # not redundant with above
        return value
    def name_of_value(self, value):
        return "Color(%0.3f, %0.3f, %0.3f)" # Color() is only used in this printform, nothing parses it (for now) #e could say RGB
    def short_name_of_value(self, value):
        return "%d,%d,%d" % self.value_as_int_tuple(value)
    def value_as_int_tuple(self, value):
        r, g, b = value # assume floats
        return tuple(map( lambda component: int(component * 255 + 0.5), (r, g, b) ))
    def value_as_QColor(self, value = None): ###k untested??
        #e API is getting a bit klugy... we're using a random instance as knowing about the superset of colors,
        # and using its default value as the value here...
        if value is None:
            value = self.get_defaultValue()
        rgb = self.value_as_int_tuple(value)
        from PyQt4.Qt import QColor
        return QColor(rgb[0], rgb[1], rgb[2]) #k guess
    def get_defaultValue(self):
        return self._defaultValue
    def changer_menuspec( self, instance_name, newval_receiver_func, curval = None):
        # in the menu, we'd like to put up a few recent colors, and offer to choose a new one.
        # but in present architecture we have no access to any recent values! Probably this should be passed in.
        # For now, just use the curval and some common vals.
        text = self.changer_menu_text( instance_name, curval) # e.g. "instance_name: curval"
        values = [black, white, red, green, blue, gray, orange, yellow, magenta, pink] #e need better order, maybe submenus
            ##e self.recent_values()
            #e should be able to put color names in menu - maybe even translate numbers to those?
        values = map( self.normalize_value, values) # needed for comparison
        if curval not in values:
            values.insert(0, curval)
        names = map( self.short_name_of_value, values)
        # include the actual color in the menu item (in place of the checkmark-position; looks depressed when "checked")
        def mitem_value_func( mitem, value):
            """
            add options to mitem based on value and return new mitem
            """
            ###e should probably cache these things? Not sure... but it might be needed
            # (especially on Windows, based on Qt doc warnings)
            iconset = iconset_from_color( value)
                #e need to improve look of "active" icon in these iconsets (checkmark inside? black border?)
            return mitem + (('iconset',iconset),)
        submenu = submenu_from_name_value_pairs( zip(names, values),
                                                 newval_receiver_func,
                                                 curval = curval,
                                                 mitem_value_func = mitem_value_func )
        submenu.append(( "Choose...", pass_chosen_color_lambda( newval_receiver_func, curval ) ))
            #e need to record recent values somewhere, include some of them in the menu next time
        #k does that let you choose by name? If not, QColor has a method we could use to look up X windows color names.
        return ( text, submenu )
    pass

def pass_chosen_color_lambda( newval_receiver_func, curval, dialog_parent = None): #k I hope None is ok as parent
    def func():
        from PyQt4.Qt import QColorDialog
        color = QColorDialog.getColor( qcolor_from_anything(curval), dialog_parent)
        if color.isValid():
            newval = color.red()/255.0, color.green()/255.0, color.blue()/255.0
            newval_receiver_func(newval)
        return
    return func

def qcolor_from_anything(color):
    from PyQt4.Qt import QColor
    if isinstance(color, QColor):
        return color
    if color is None:
        color = (0.5, 0.5, 0.5) # gray
    return ColorType(color).value_as_QColor() ###k untested call

def contrasting_color(qcolor, notwhite = False ):
    """
    return a QColor which contrasts with qcolor;
    if notwhite is true, it should also contrast with white.
    """
    rgb = qcolor.red(), qcolor.green(), qcolor.blue() / 2 # blue is too dark, have to count it as closer to black
    from PyQt4.Qt import Qt
    if max(rgb) > 90: # threshhold is a guess, mostly untested; even blue=153 seemed a bit too low so this is dubiously low.
        # it's far enough from black (I hope)
        return Qt.black
    if notwhite:
        return Qt.cyan
    return Qt.white

def pixmap_from_color_and_size(color, size):
    """
    #doc; size can be int or (int,int)
    """
    if type(size) == type(1):
        size = size, size
    w,h = size
    qcolor = qcolor_from_anything(color)
    from PyQt4.Qt import QPixmap
    qp = QPixmap(w,h)
    qp.fill(qcolor)
    return qp

def iconset_from_color(color):
    """
    Return a QIcon suitable for showing the given color in a menu item or (###k untested, unreviewed) some other widget.
    The color can be a QColor or any python type we use for colors (out of the few our helper funcs understand).
    """
    # figure out desired size of a small icon
    from PyQt4.Qt import QIcon
    #size = QIcon.iconSize(QIcon.Small) # a QSize object
    #w, h = size.width(), size.height()
    w, h = 16, 16   # hardwire it and be done
    # get pixmap of that color
    pixmap = pixmap_from_color_and_size( color, (w,h) )
    # for now, let Qt figure out the Active appearance, etc. Later we can draw our own black outline, or so. ##e
    iconset = QIcon(pixmap)
    checkmark = ("checkmark" == debug_pref("color checkmark", Choice(["checkmark","box"])))

    modify_iconset_On_states( iconset, color = color, checkmark = checkmark )
    return iconset

def modify_iconset_On_states( iconset, color = white, checkmark = False, use_color = None):
    #bruce 050729 split this out of iconset_from_color
    """
    Modify the On states of the pixmaps in iconset, so they can be distinguished from the (presumably equal) Off states.
    (Warning: for now, only the Normal On states are modified, not the Active or Disabled On states.)
    By default, the modification is to add a surrounding square outline whose color contrasts with white,
    *and* also with the specified color if one is provided. If checkmark is true, the modification is to add a central
    checkmark whose color contrasts with white, *or* with the specified color if one is provided.
    Exception to all that: if use_color is provided, it's used directly rather than any computed color.
    """
    from PyQt4.Qt import QIcon, QPixmap
    if True:
        ## print 'in debug_prefs.modify_iconset_On_states : implement modify_iconset_On_states for Qt 4'
        return
    for size in [QIcon.Small, QIcon.Large]: # Small, Large = 1,2
        for mode in [QIcon.Normal]: # Normal = 0; might also need Active for when mouse over item; don't yet need Disabled
            # make the On state have a checkmark; first cause it to generate both of them, and copy them both
            # (just a precaution so it doesn't make Off from the modified On,
            #  even though in my test it treats the one we passed it as Off --
            #  but I only tried asking for Off first)
##            for state in [QIcon.Off, QIcon.On]: # Off = 1, On = 0, apparently!
##                # some debug code that might be useful later:
##                ## pixmap = iconset.pixmap(size, mode, state) # it reuses the same pixmap for both small and large!!! how?
##                ## generated = iconset.isGenerated(size, mode, state) # only the size 1, state 1 (Small Off) says it's not generated
##                ## print "iconset pixmap for size %r, mode %r, state %r (generated? %r) is %r" % \
##                ##       (size, mode, state, generated, pixmap)
##                pixmap = iconset.pixmap(size, mode, state)
##                pixmap = QPixmap(pixmap) # copy it ###k this might not be working; and besides they might be copy-on-write
##                ## print pixmap # verify unique address
##                iconset.setPixmap(pixmap, size, mode, state)
            # now modify the On pixmap; assume we own it
            state = QIcon.On
            pixmap = iconset.pixmap(size, mode, state)
            #e should use QPainter.drawPixmap or some other way to get a real checkmark and add it,
            # but for initial test this is easiest and will work: copy some of this color into middle of black.
            # Warning: "size" localvar is in use as a loop iterator!
            psize = pixmap.width(), pixmap.height() #k guess
            w,h = psize
##            from utilities import debug_flags
##            if debug_flags.atom_debug:
##                print "atom_debug: pixmap(%s,%s,%s) size == %d,%d" % (size, mode, state, w,h)
            from PyQt4.Qt import copyBlt
            if checkmark:
                if use_color is None:
                    use_color = contrasting_color( qcolor_from_anything(color))
                pixmap2 = pixmap_from_color_and_size( use_color, psize)
                for x,y in [(-2,0),(-1,1),(0,2),(1,1),(2,0),(3,-1),(4,-2)]:
                    # this imitates Qt's checkmark on Mac; is there an official source?
                    # (it might be more portable to grab pixels from a widget or draw a QCheckListItem into a pixmap #e)
                    x1,y1 = x + w/2 - 1, y + h/2 - 1
                    copyBlt( pixmap, x1,y1, pixmap2, x1,y1, 1,3 )
                iconset.setPixmap(pixmap, size, mode, state) # test shows re-storing it is required (guess: copy on write)
            else:
                if use_color is None:
                    use_color = contrasting_color( qcolor_from_anything(color), notwhite = True)
                pixmap2 = pixmap_from_color_and_size( use_color, psize)
                    ###e needs to choose white if actual color is too dark (for a checkmark)
                    # or something diff than both black and white (for an outline, like we have now)
                copyBlt( pixmap2, 2,2, pixmap, 2,2, w-4, h-4 )
                    # args: dest, dx, dy, source, sx, sy, w,h. Doc hints that overwriting edges might crash.
                iconset.setPixmap(pixmap2, size, mode, state)
                # note: when I had a bug which made pixmap2 too small (size 1x1), copyBlt onto it didn't crash,
                # and setPixmap placed it into the middle of a white background.
            pass
        pass
    return # from modify_iconset_On_states

# ==

#bruce 060124 changes: always called, but gets passed debug_flags.atom_debug as an arg to filter the prefs,
# and has new API to return a list of menu items (perhaps empty) rather than exactly one.

from utilities import debug_flags

def debug_prefs_menuspec( atom_debug): #bruce 080312 split this up
    """
    Return the debug_prefs section for the debug menu, as a list of zero or more
    menu items or submenus (as menu_spec tuples or lists).
    """

    # first exercise all our own debug_prefs, then get the sorted list
    # of all known ones.

    if debug_flags.atom_debug: #bruce 050808 (it's ok that this is not the atom_debug argument)
        testcolor = debug_pref("atom_debug test color", ColorType(green))

    max_menu_length = debug_pref(
        "(max debug prefs submenu length?)", #bruce 080215
             # initial paren or space puts it at start of menu
         Choice([10, 20, 30, 40, 50, 60, 70], defaultValue = 20),
             #bruce 080317 changed default from 40 to 20, and revised order
         non_debug = True, #bruce 080317
         prefs_key = "A10/max debug prefs submenu length" # revised, bruce 080317
     )

    non_default_submenu = True # could be enabled by its own debug_pref if desired

    items = [(name.lower(), name, pref) for name, pref in debug_prefs.items()]
        # use name.lower() as sortkey, in this function and some of the subfunctions;
        # name is included to disambiguate cases where sortkey is identical
    items.sort()

    # then let each subsection process this in its own way.

    if non_default_submenu:
        part1 = _non_default_debug_prefs_menuspec( items )
    else:
        part1 = []

    part2 = _main_debug_prefs_menuspec( items, max_menu_length, atom_debug)

    return part1 + part2

def _non_default_debug_prefs_menuspec( items): #bruce 080312 added this
    """
    [private helper for debug_prefs_menuspec]

    Return a submenu for the debug_prefs currently set to a non-default value,
    if there are any. Items have a private format coming from our caller.
    """
    non_default_items = [item for item in items if item[-1].current_value_is_not_default()]
    if not non_default_items:
        return []
    submenu = [pref.changer_menuspec() for (sortkey_junk, name_junk, pref) in non_default_items]
    text = "debug prefs currently set (%d)" % len(submenu)
    return [ (text, submenu) ]

def _main_debug_prefs_menuspec( items, max_menu_length, atom_debug):
    """
    [private helper for debug_prefs_menuspec]

    Return a list of zero or more menu items or submenus (as menu_spec tuples or lists)
    usable to see and edit settings of all active debug prefs (for atom_debug true)
    or all the ones that have their non_debug flag set (for atom_debug false).

    Items have a private format coming from our caller.
    """
    items_wanted = []
        # will become a list of (sortkey, menuspec) pairs
    for sortkey, name_junk, pref in items:
        if pref.non_debug or atom_debug:
            items_wanted.append( (sortkey, pref.changer_menuspec()) )
                # note: sortkey is not used below for sorting (that
                # happened in caller), but is used for determining ranges.
        # print name_junk, to see the list for determining good ranges below
    if not items_wanted:
        if atom_debug:
            return [ ( "debug prefs submenu", noop, "disabled" ) ]
        else:
            return []
    elif len(items_wanted) <= max_menu_length:
        submenu = [menuspec for sortkey, menuspec in items_wanted]
        return [ ( "debug prefs submenu", submenu) ]
    else:
        # split the menu into smaller sections;
        # use the first splitting scheme of the following which works
        # (or the last one even if it doesn't work)
        schemes = [
            # these ranges were made by evenly splitting 62 debug prefs
            # (there are a lot each of d, e, g, u, and a bunch each of c, m, p, t)
            ("a-e", "f-z"),
            ("a-d", "e-j", "k-z"),
            ("a-c", "d-e", "f-m", "n-z")
         ]
        best_so_far = None
        for scheme in schemes:
            # split according to scheme, then stop if it's good enough
            pointer = 0
            res = []
            good_enough = True # set to False if any submenu is still too long
            total = 0 # count items, for an assert
            for range in scheme:
                # grab the remaining ones up to the end of this range
                one_submenu = []
                while (pointer < len(items_wanted) and
                       (range == scheme[-1] or
                        items_wanted[pointer][0][0] <= range[-1])):
                    menuspec = items_wanted[pointer][1]
                    pointer += 1
                    one_submenu.append( menuspec )
                # and put them into a submenu labelled with the range
                good_enough = good_enough and len(one_submenu) <= max_menu_length
                total += len(one_submenu)
                res.append( ("debug prefs (%s)" % range, one_submenu) )
                    # (even if one_submenu is empty)
                continue # next range
            assert total == len(items_wanted)
            assert len(res) == len(scheme) # revise if we drop empty ones
            best_so_far = res
            if good_enough:
                break
            continue
        # good enough, or as good as we can get
        assert best_so_far
        return best_so_far
    pass

# ==

# specific debug_pref exerciser/access functions can go here,
# if they need to be imported early during startup or by several source files
# [this location is deprecated -- use GlobalPreferences.py instead. --bruce 080215]

def _permit_property_pane():
    """
    should we set up this session to look a bit worse,
    but permit experimental property pane code to be used?
    """
    return debug_pref("property pane debug pref offered? (next session)",
                      Choice_boolean_False,
                      non_debug = True,
                      prefs_key = "A8 devel/permit property pane")

_this_session_permit_property_pane = 'unknown' # too early to evaluate _permit_property_pane() during this import

def this_session_permit_property_pane():
    """
    this has to give the same answer throughout one session
    """
    global _this_session_permit_property_pane
    if _this_session_permit_property_pane == 'unknown':
        _this_session_permit_property_pane = _permit_property_pane()
        if _this_session_permit_property_pane:
            _use_property_pane() # get it into the menu right away, otherwise we can't change it until it's too late
    return _this_session_permit_property_pane

def _use_property_pane():
    return debug_pref("property pane (for HJ dialog)?", Choice_boolean_False, non_debug = True,
                      prefs_key = "A8 devel/use property pane")

def use_property_pane():
    """
    should we actually use a property pane?
    (only works if set before the first time a participating dialog is used)
    (only offered in debug menu if a property pane is permitted this session)
    """
    return this_session_permit_property_pane() and _use_property_pane()

def debug_pref_History_print_every_selected_object(): #bruce 070504
    res = debug_pref("History: print every selected object?",
                      Choice_boolean_False,
                      non_debug = True,
                      prefs_key = "A9/History/print every selected object?"
                     )
    return res

# == test code

if __name__ == '__main__':
    spinsign = debug_pref("spinsign",Choice([1,-1]))
    testcolor = debug_pref("test color", ColorType(green))
    print debug_prefs_menuspec(True)

# end