summaryrefslogtreecommitdiff
path: root/cad/src/graphics/behaviors/confirmation_corner.py
blob: d5ae18b33f289f14e41af98799cff43a7b08a215 (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
# Copyright 2007-2008 Nanorex, Inc.  See LICENSE file for details.
"""
confirmation_corner.py -- helpers for modes with a confirmation corner
(or other overlay widgets).

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

Note: confirmation corners make use of two methods added to the "GraphicsMode API"
(the one used by GLPane to interface to glpane.graphicsMode for mouse and drawing)
for their sake, currently [071015] defined only in basicGraphicsMode:
draw_overlay, and mouse_event_handler_for_event_position. These are general enough
to handle all kinds of overlays, in principle, but the current implem and
some API details may be just barely general enough for the confirmation
corner.

Those method implems assume that Command subclasses (at least those which
can be found by GraphicsMode.draw_overlay -- namely, those which supply their
own PM, as of 080905) override want_confirmation_corner_type
to return the kind of confirmation corner they want at a given moment.
"""

import os

from exprs.ExprsConstants import PIXELS
from exprs.images import Image
from exprs.Overlay import Overlay

from exprs.instance_helpers import get_glpane_InstanceHolder

from exprs.Rect import Rect # needed for Image size option, not just for testing
##from constants import green # only for testing

from exprs.projection import DrawInCorner ##, DrawInCorner_projection
from utilities.prefs_constants import UPPER_RIGHT

from utilities.debug import print_compact_traceback ##, print_compact_stack

from utilities.debug_prefs import debug_pref, Choice_boolean_False

# button region codes (must all be true values;
# these are used as indices in various dicts or functions,
# and are used as components of cctypes like 'Done+Cancel')
BUTTON_CODES = ('Done', 'Cancel', 'Transient-Done')

# ==

class MouseEventHandler_API: #e refile #e some methods may need graphicsMode and/or glpane arg...
    """
    API (and default method implems) for the MouseEventHandler interface
    (for objects used as glpane.mouse_event_handler) [abstract class]
    """
    def mouseMoveEvent(self, event):
        """
        """
        pass
    def mouseDoubleClickEvent(self, event):
        """
        """
        pass
    def mousePressEvent(self, event):
        """
        """
        pass
    def mouseReleaseEvent(self, event):
        """
        """
        pass
    def update_cursor(self, graphicsMode, wpos):
        """
        Perform side effects in graphicsMode (assumed to be a basicGraphicsMode subclass)
        to give it the right cursor for being over self
        at position <wpos> (in OpenGL window coords).
        """
        ###e may need more args (like mod keys, etc),
        # or official access to more info (like glpane.button),
        # to choose the cursor
        pass
    def want_event_position(self, wX, wY):
        """
        Return a true value if self wants to handle mouse events
        at the given OpenGL window coords, false otherwise.

        Note: some implems, in the true case, actually return some data
        indicating what cursor and display state they want to use; it's not
        yet decided whether this is supported in the official API (it's not yet)
        or merely permitted for internal use (it is and always will be).
        """
        pass
    def draw(self):
        """
        """
        pass
    pass

# ==

# exprs for images

# overlay image (command-specific icon)

# This draws a 22 x 22 icon in the upper left corner of the glpane.
# I need to be able to change the origin of the icon so it can be drawn at
# a different location inside the confirmation corner, but I cannot
# figure out how to do this. I will discuss with Bruce soon. -Mark 2008-03-23

_overlay_image = Image(convert = 'RGBA',
                       decal = False,
                       blend = True,
                       #ideal_width = 22,
                       #ideal_height = 22,
                       size = Rect(22 * PIXELS))

from exprs.transforms import Translate
from exprs.Exprs import V_expr
from exprs.Rect import Spacer

def _expr_for_overlay_imagename(imagename, dx = 0, dy = 0):
    # WARNING: this is not optimized (see comment for _expr_for_imagename()).
    image_expr = _overlay_image( imagename )
        # NOTE: If the desired dx,dy depends on other settings,
        # like whether one or two CC buttons are shown,
        # then it's simplest to make more variants of this expr,
        # with dx, dy hardcoded differently in each one.
        # Or if that's not practical, let me know and I'll
        # revise the code that draws this to accomodate that variability.
        # Also make sure to revise the code that calls each one
        # (i.e. a modified copy of _expr_instance_for_overlay_imagename)
        # to use a different "index" even when using the same imagename.
        # (For example, it could include dx,dy in the index.)
        # [bruce 080324]
    return DrawInCorner(corner = UPPER_RIGHT)(
        Overlay(
            Spacer(22 * PIXELS),
            Translate( image_expr, V_expr(dx * PIXELS, dy * PIXELS, 0)),
         )
     )

# ==

# button images

_trans_image = Image(convert = 'RGBA', decal = False, blend = True,
                    # don't need (I think): alpha_test = False
                    shape = 'upper-right-half', #bruce 070628 maybe this will fix bug 2474 (which I can't see on Mac);
                        # note that it has a visible effect on Mac (makes the intended darker edge of the buttons less thick),
                        # but this seems ok.
                    clamp = True, # this removes the artifacts that show the edges of the whole square of the image file
                    ideal_width = 100, ideal_height = 100, size = Rect(100 * PIXELS))

def _expr_for_imagename(imagename):
    # WARNING: this is not optimized -- it recomputes and discards this expr on every access!
    # (But it looks like its caller, _expr_instance_for_imagename, caches the expr instances,
    #  so only the uninstantiated expr itself is recomputed each time, so it's probably ok.
    #  [bruce 080323 comment])

    if '/' not in imagename:
        imagename = os.path.join( "ui/confcorner", imagename)
    image_expr = _trans_image( imagename )
    return DrawInCorner(corner = UPPER_RIGHT)( image_expr )

# ==

# These IMAGENAMES are used only for a preloading optimization.
# If this list is not complete, it will not cause bugs,
# it will just mean the first use of certain images is slower
# (but startup is faster by the same amount).
# [bruce 080324 comment]

IMAGENAMES = """
CancelBig.png
CancelBig_Pressed.png
DoneBig.png
DoneBig_Pressed.png
DoneSmall_Cancel_Pressed.png
DoneSmall.png
DoneSmall_Pressed.png
TransientDoneSmall.png
TransientDoneSmall_Pressed.png
TransientDoneSmall_Cancel_Pressed.png
TransientDoneBig.png
TransientDoneBig_Pressed.png""".split()

# ==

class cc_MouseEventHandler(MouseEventHandler_API): #e rename # an instance can be returned from find_or_make_confcorner_instance
    """
    ###doc
    """

    # initial values of state variables, etc
    _last_button_position = False # False or an element of BUTTON_CODES
    _pressed_button = False # False or an element of BUTTON_CODES; valid regardless of self.glpane.in_drag

    _cctype = -1 # intentionally illegal value, different from any real value

    # review: are self.glpane and self.command, set below, private?
    # if so, should rename them to indicate this. [bruce 080323 comment]

    def __init__(self, glpane):
        self.glpane = glpane
        for imagename in IMAGENAMES:
            self._preload(imagename) # to avoid slowness when each image is first used in real life
        return

    def _preload(self, imagename):
        self._expr_instance_for_imagename(imagename)
        return

    def _expr_instance_for_imagename(self, imagename):
        ih = get_glpane_InstanceHolder(self.glpane)
        index = imagename # might have to be more unique if we start sharing this InstanceHolder with anything else
        expr = _expr_for_imagename(imagename)
        expr_instance = ih.Instance( expr, index, skip_expr_compare = True)
        return expr_instance

    def _expr_instance_for_overlay_imagename(self, imagename, dx = 0, dy = 0):
        ih = get_glpane_InstanceHolder(self.glpane)
        index = 1, imagename # might have to be more unique if we start sharing this InstanceHolder with anything else
        expr = _expr_for_overlay_imagename(imagename, dx, dy)
        expr_instance = ih.Instance( expr, index, skip_expr_compare = True)
        return expr_instance

    def _f_advise_find_args(self, cctype, command):
        """
        [friend method; can be called as often as every time this is drawn;
         cctype can be None or one of a few string constants.]

        Set self._button_codes correctly for cctype and command,
        also saving those in attrs of self of related names.

        self.command is used later for:
        - finding PM buttons for doing actions
        - finding the icon for the Done button, from the PM
        """
        self.command = command
        if self._cctype != cctype:
            # note: no point in updating drawing here if cctype changes,
            # since we're only called within glpane calling graphicsMode.draw_overlay.
            ## self._update_drawing()
            self._cctype = cctype
            if cctype:
                self._button_codes = cctype.split('+')
                assert len(self._button_codes) in (1, 2)
                for bc in self._button_codes:
                    assert bc in BUTTON_CODES
            else:
                self._button_codes = []
        return

    # == event position (internal and _API methods), and other methods ###DESCRIBE better

    def want_event_position(self, wX, wY):
        """
        MouseEventHandler_API method:
        Return False if we don't want to be the handler for this event and immediately after it;
        return a true button-region-code if we do.

        @note: Only called externally when mouse is pressed (glpane.in_drag will already be set then),
        or moves when not pressed (glpane.in_drag will be unset); deprecated for internal calls.
        The current implem does not depend on only being called at those times, AFAIK.
        """
        return self._button_region_for_event_position(wX, wY)

    def _button_region_for_event_position(self, wX, wY):
        """
        Return False if wX, wY is not over self, or a button-region-code
        (whose boolean value is true; an element of BUTTON_CODES) if it is,
        which says which button region of self it's over (regardless of pressed state of self).
        """
        # correct implem, but button-region size & shape is hardcoded
        dx = self.glpane.width - wX
        dy = self.glpane.height - wY
        if dx + dy <= 100:
            # this event is over the CC triangular region; which of our buttons is it over?
            if len(self._button_codes) == 2:
                if -dy >= -dx: # note: not the same as wY >= wX if glpane is not square!
                    return self._button_codes[0] # top half of corner triangle; usually 'Done'
                else:
                    return self._button_codes[1] # right half of corner triangle; usually 'Cancel'
            elif len(self._button_codes) == 1:
                return self._button_codes[0]
            else:
                return False # can this ever happen? I don't know, but if it does, it should work.
        return False

    def _transient_overlay_icon_name(self, imagename):
        """
        Returns the transient overlay icon filename to include in the
        confirmation corner. This is the current command's icon that is used
        in the title of the property manager.

        @param imagename: Confirmation corner imagename.
        @type  imagename: string

        @return: Iconpath of the image to use as an overlay in the confimation
                 corner, delta x and delta y translation for positioning the
                 icon in the confirmation corner.
        @rtype:  string, int, int
        """
        if "Transient" in imagename:
            if "Big" in imagename:
                dx = -46
                dy = -6
            else:
                dx = -51
                dy = -1
            return (self.command.propMgr.iconPath, dx, dy)
        return (None, 0, 0)

    def draw(self):
        """
        MouseEventHandler_API method: draw self. Assume background is already
        correct (so our implem can be the same, whether the incremental drawing
        optim for the rest of the GLPane content is operative or not).
        """
        if 0:
            print "draw CC for cctype %r and state %r, %r" \
                  % (self._cctype,
                     self._pressed_button,
                     self._last_button_position)

        # figure out what image expr to draw

        # NOTE: this is currently not nearly as general as the rest of our
        # logic, regarding what values of self._button_codes are supported.
        # If we need it to be more general, we can split the expr into two
        # triangular pieces, using Image's shape option and Overlay, so its
        # two buttons are independent (as is done in some of the tests in
        # exprs/test.py).

        if self._button_codes == []:
            # the easy case
            return
        elif self._button_codes == ['Cancel']:
            if self._pressed_button == 'Cancel':
                imagename = "CancelBig_Pressed.png"
            else:
                imagename = "CancelBig.png"
        elif self._button_codes == ['Done']:
            if self._pressed_button == 'Done':
                imagename = "DoneBig_Pressed.png"
            else:
                imagename = "DoneBig.png"
        elif self._button_codes == ['Done', 'Cancel']:
            if self._pressed_button == 'Done':
                imagename = "DoneSmall_Pressed.png"
            elif self._pressed_button == 'Cancel':
                imagename = "DoneSmall_Cancel_Pressed.png"
            else:
                imagename = "DoneSmall.png"
        elif self._button_codes == ['Transient-Done', 'Cancel']:
            if self._pressed_button == 'Transient-Done':
                imagename = "TransientDoneSmall_Pressed.png"
            elif self._pressed_button == 'Cancel':
                imagename = "TransientDoneSmall_Cancel_Pressed.png"
            else:
                imagename = "TransientDoneSmall.png"
        elif self._button_codes == ['Transient-Done']:
            if self._pressed_button == 'Transient-Done':
                imagename = "TransientDoneBig_Pressed.png"
            else:
                imagename = "TransientDoneBig.png"
        else:
            assert 0, "unsupported list of buttoncodes: %r" \
                   % (self._button_codes,)

        expr_instance = self._expr_instance_for_imagename(imagename)
            ### REVIEW: worry about value of PIXELS vs perspective?
            ###         worry about depth writes?
        expr_instance.draw()
            # Note: this draws expr_instance in the same coordsys used for
            # drawing the model.

        overlay_imagename, dx, dy = self._transient_overlay_icon_name(imagename)
        if overlay_imagename:
            expr_instance = \
                self._expr_instance_for_overlay_imagename(overlay_imagename,
                                                          dx, dy)
            expr_instance.draw()

        return

    def update_cursor(self, graphicsMode, wpos):
        """
        MouseEventHandler_API method; change cursor based on current state and
        event position
        """
        assert self.glpane is graphicsMode.glpane
        win = graphicsMode.win # for access to cursors
        wX, wY = wpos
        bc = self._button_region_for_event_position(wX, wY)
        # figure out want_cursor (False or a button code; in future there may
        # be other codes for modified cursors)
        if not self._pressed_button:
            # mouse is not down; cursor reflects where we are at the moment
            # (False or a button code)
            want_cursor = bc
        else:
            # a button is pressed; cursor reflects whether this button will act
            # or not (based on whether we're over it now or not)
            # (for now, if the button will act, the cursor does not look any
            # different than if we're hovering over the button, but revising
            # that would be easy)
            if self._pressed_button == bc:
                want_cursor = bc
            else:
                want_cursor = False
        # show the cursor indicated by want_cursor
        if want_cursor:
            assert want_cursor in BUTTON_CODES
            if want_cursor == 'Done':
                cursor = win._confcorner_OKCursor
            elif want_cursor == 'Transient-Done':
                cursor = win.confcorner_TransientDoneCursor
            else:
                cursor = win._confcorner_CancelCursor
            self.glpane.setCursor(cursor)
        else:
            # We want to set a cursor which indicates that we'll do nothing.
            # Modes won't tell us that cursor, but they'll set it as a side
            # effect of graphicsMode.update_cursor_for_no_MB().
            # Actually, they may set the wrong cursor then (e.g. BuildCrystal_Command,
            # which looks at glpane.modkeys, but if we're here with modkeys
            # we're going to ignore them). If that proves to be misleading,
            # we'll revise this.
            self.glpane.setCursor(win.ArrowCursor)
                # in case the following method does nothing (can happen)
            try:
                graphicsMode.update_cursor_for_no_MB()
                    # _no_MB is correct, even though a button is presumably
                    # pressed.
            except:
                print_compact_traceback("bug: exception (ignored) in %r.update_cursor_for_no_MB(): " % (graphicsMode,) )
                pass
        return

    # == mouse event handling (part of the _API)

    def mousePressEvent(self, event):
        # print "meh press"
        wX, wY = wpos = self.glpane._last_event_wXwY
        bc = self._button_region_for_event_position(wX, wY)
        self._last_button_position = bc # this is for knowing when our appearance might change
        self._pressed_button = bc # this and glpane.in_drag serve as our state variables
        if not bc:
            print "bug: not bc in meh.mousePressEvent" # should never happen; if it does, do nothing
        else:
            self._update_drawing()
        return

    def mouseMoveEvent(self, event): ###e should we get but & mod as new args, or from glpane attrs set by fix_event??
        wX, wY = wpos = self.glpane._last_event_wXwY # or we could get these from event
        bc = self._button_region_for_event_position(wX, wY)
        if self._last_button_position != bc:
            self._last_button_position = bc
            self._update_drawing()
            self._do_update_cursor()
        return

    def mouseReleaseEvent(self, event):
        # print "meh rel"
        wX, wY = self.glpane._last_event_wXwY
        bc = self._button_region_for_event_position(wX, wY)
        if self._last_button_position != bc:
            print "unexpected: self._last_button_position != bc in meh.mouseReleaseEvent (should be harmless)" ###
        self._last_button_position = bc
        if self._pressed_button and self._pressed_button == bc:
            #e in future: if action might take time, maybe change drawing appearance to indicate we're "doing it"
            self._do_action(bc)
        self._pressed_button = False
        self._update_drawing() # might be redundant with _do_action (which may need to update even more, I don't know for sure)
            # Note: this might not be needed if no action happens -- depends on nature of highlighting;
            # note that you can press one button and release over the other, and then the other might need to highlight
            # if it has mouseover highlighting (tho in current design, it doesn't).
        self._do_update_cursor() # probably not needed (but should be ok as a precaution)
        return

    # == internal update methods

    def _do_update_cursor(self): ### TODO: REVISE DOCSTRING; it's unclear after recent changes [bruce 070628]
        """
        internal helper for calling our external API method update_cursor with the right arguments --
        but only if we're still responsible for the cursor according to the GLPane --
        otherwise, call the one that is!
        """
        self.glpane.graphicsMode.update_cursor()
            #bruce 070628 revised this as part of fixing bug 2476 (leftover CC Done cursor).
            # Before, it called our own update_cursor, effectively assuming we're still active,
            # wrong after a release and button action. Now, this is redundant in that case, but
            # should be harmless; it might still be needed in the move case (though probably self
            # is always active then).
        return

    def _update_drawing(self):
        ### TODO: figure out if our appearance has changed, and do nothing if not (important optim)
        # (be careful about whether we're the last CC to be drawn, if there's more than one and they get switched around!
        #  we might need those events about enter/leave that the glpane doesn't yet send us; or some about changing the cc) ###
        self.glpane.gl_update_confcorner() # note: as of 070627 this is the same as gl_update -- NEEDS OPTIM to be incremental

    # == internal action methods

    def _do_action(self, buttoncode):
        """
        do the action corresponding to buttoncode (protected from exceptions)
        """
        #e in future: statusbar message?

        # print "_do_action", buttoncode

        ###REVIEW: maybe all the following should be a Command method?
        # Note: it will all get revised and cleaned up once we have a command stack
        # and we can just tell the top command to do Done or Cancel.

        done_button, cancel_button = self.command._KLUGE_visible_PM_buttons()
            # each one is either None, or a QToolButton (a true value) currently displayed on the current PM
        if buttoncode in ['Done', 'Transient-Done']:
            button = done_button
        else:
            button = cancel_button
        if not button:
            print "bug (ignored): %r trying to do action for nonexistent %r button" % (self, buttoncode) #e more info?
        else:
            try:
                # print "\nabout to click %r button == %r" % (buttoncode, button)
                button.click()
                    # This should make the button emit the clicked signal -- not sure if it will also emit
                    # the pressed and released signals. The Qt doc (for QAbstractButton.click) just says
                    # "All the usual signals associated with a click are emitted as appropriate."
                    # Our code mostly connects to clicked, but for a few buttons (not the ones we have here, I think)
                    # connects to pressed or released. Search for those words used in a SIGNAL macro to find them.

                # Assume the click handler did whatever updates were required,
                # as it would need to do if the user pressed the button directly,
                # so no updates are needed here. That's good, because only the event handler
                # knows if some are not needed (as an optimization).

                # print "did the click"
            except:
                print_compact_traceback("bug: exception (ignored) when using %r button == %r: " % (buttoncode, button,) )
                pass
            # we did the action; now (at least for Done or Cancel), we should inactivate self as mouse_event_handler
            # and then do update_cursor, which the following does (part of fixing bug 2476 (leftover CC Done cursor))
            # [bruce 070628]
            self.glpane.set_mouse_event_handler(None)
            pass
        return

    pass # end of class cc_MouseEventHandler

# ==

def find_or_make_confcorner_instance(cctype, command):
    """
    Return a confirmation corner instance for command, of the given cctype.
    [Public; called from basicGraphicsMode.draw_overlay]
    """
    try:
        command._confirmation_corner__cached_meh
    except AttributeError:
        command._confirmation_corner__cached_meh = cc_MouseEventHandler(command.glpane)
    res = command._confirmation_corner__cached_meh
    res._f_advise_find_args(cctype, command) # in case it wants to store these
        # (especially since it's shared for different values of them)
    return res
    # see also exprs/cc_scratch.py

# end