summaryrefslogtreecommitdiff
path: root/cad/src/commandToolbar/CommandToolbar.py
blob: 37e6e8c7f331698650cabb9651c30e96f9a3b9a4 (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
# Copyright 2007 Nanorex, Inc.  See LICENSE file for details.
"""
CommandToolbar.py - controls the main hierarchical toolbar for giving commands

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


Module classification: [bruce 071228]

This mainly contains control logic (especially regarding flyout
toolbar behavior and appearance) for any hierarchical
command toolbar which operates like NE1's main Command Toolbar.

Accordingly, after future refactoring, it could be part of a
general-purpose "command toolbar widget" package. But at the
moment, it inherits a hardcoding of NE1's specific Command Toolbar
layout and contents, via its superclass Ui_CommandToolbar,
which is what actually needs to be refactored.

So we have two imperfect choices for classifying it:

(1) put it in ne1_ui just because of its superclass; or

(2) classify it as a widget independent of ne1_ui,
but tolerate an import in it from ne1_ui for now,
until the superclass gets refactored (which might
not happen soon).

I think it's best to classify it "optimisitically"
using scheme (2), and let the wrong-direction import
remind us that the superclass needs refactoring.

I am assuming that will cause no inter-package import cycle,
since no cycle has been reported involving this module.

So for now, the superclass is in "ne1_ui", but this module
can go into its own toplevel "CommandToolbar" package.
(Not just into "widgets" or a subpackage, since it is a major
component of an app, and might still assume it's a singleton
or have client code which assumes that. In future, we can
consider making the general part non-singleton and putting
it into its own subpackage of "widgets".)

After future refactoring of the superclass Ui_CommandToolbar
(described in its module classification comment), its general
part would join this module as part of the same package
for a "general command toolbar widget".

BTW, I think it should be the case, and might already
be the case, that whatever state this class needs to help
maintain (even given its not-yet-refactored superclass)
will actually reside in the Command Sequencer and/or
in running commands, along with the logic for how
to modify that state. This class's object is just the
UI that makes the Command Sequencer state visible
and user-controllable.

BTW2, another needed refactoring (perhaps more related
to Ui_CommandToolbar than to this module) is to make
sure commands don't need to update their own UI elements
in various ways; for more on this see

http://www.nanoengineer-1.net/mediawiki/index.php?title=NE1_module/package_organization#Command_Toolbar


History:

ninad 20070109: created this in QT4 branch and subsequently modified it.
ninad 20070125: moved ui generation code to a new file Ui_CommandManager
ninad 20070403: implemented 'Subcontrol Area'  in the command toolbar, related
             changes (mainly revised _updateFlyoutToolBar,_setFlyoutDictionary)
             and added/modified several docstrings
ninad 20070623: Moved _createFlyoutToolbar from modes to here and related changes
mark 20071226: Renamed CommandManager to CommandToolbar.

ninad 20080715: a) Workaround for Qt4.3 bug 2916 (disabled 'extension indicator'
 button ">>" in the flyout toolbar.  b) some refactoring to  move common code
 in classes FlyoutToolbar and NE1_QWidgetAction.
"""

from PyQt4 import QtGui
from PyQt4.Qt import Qt
from PyQt4.Qt import SIGNAL
from PyQt4.Qt import QToolButton
from PyQt4.Qt import QMenu
from PyQt4.Qt import QWidgetAction
from foundation.wiki_help import QToolBar_WikiHelp


from ne1_ui.toolbars.Ui_CommandToolbar import Ui_CommandToolbar
from utilities.debug import print_compact_traceback, print_compact_stack

class CommandToolbar(Ui_CommandToolbar):
    """
    Command Toolbar is the big toolbar above the 3D graphics area and
    the model tree. It is divided into the B{Control Area} and the
    B{Flyout Toolbar Area}.

    The  B{Control Area} is a fixed toolbar on the left hand side with a
    purple background color and contains buttons with drop down menus.
    When you click on a button in the Control Area, the Flyout Toolbar Area
    is updated and displays the menu of that button (in most cases).

    The B{Flyout Toolbar Area} is divided into two areas, the
    B{Subcontrol Area} (light olive background color) and the
    B{Command Area} (light gray background color).
    The Command Area shows commands based on the checked
    SubControl Area button.  Thus it could be empty in some situations.
    """

    _f_current_flyoutToolbar = None
        #CommandToolbar._f_current_flyoutToolbar is the current flyout toolbar
        #that is displayed in the 'flyout area' of the command toolbar.
        #This attr value usually changes when the command stack changes or
        #when user clicks on a control button (exceptions apply).
        #Example: When Build > Dna command is entered, it sets this attr on the
        #commandToolbar class to the 'BuildDnaFlyout' object.
        #When that command is exited, BuildDnaFlyout is first 'deactivated' and
        #the self._f_current_flyoutToolbar is assigned a new value (The flyout
        #object of the next command entered. This can even be 'None' if the
        #next command doesn't have a flyoutToolbar)
        #@see: self._setControlButtonMenu_in_flyoutToolbar()
        #@see: self.resetToDefaultState()
        #In the above methods, this attr value is changed.
        #@see: AbstractFlyout.deActivateFlyoutToolbar()
        #@see: bug 2937
        #@see: self.command_update_flyout()



    _f_previous_flyoutToolbar = None
        #Suppose user is in a command whose custom flyout toolbar is shown.
        #Now user clicks on another control button in the Command Toolbar,
        #so, that control button menu is shown in the flyout area (instead of
        #command specific custom flyout). When this happens, the
        #self._f_current_flyoutToolbar is set to None. But, the present value
        #of self._f_current_flyoutToolbar must be stored so as to 'deactivate'
        #that flyout toolbar properly when user leaves the command for which
        #that custom flyout is meant for. This is done by using the above
        #class attr.
        #@see: self._setControlButtonMenu_in_flyoutToolbar()
        #@see: self.resetToDefaultState()
        #In the above methods, this attr value is changed.
        #@see: AbstractFlyout.deActivateFlyoutToolbar()
        #@see: bug 2937
        #@see: self.command_update_flyout()


    def __init__(self, win):
        """
        Constructor for class CommandToolbar.

        @param win: Mainwindow object
        @type  win: L{MWsemantics}
        """
        self.flyoutDictionary = None

        Ui_CommandToolbar.__init__(self, win)

        self.setupUi()

        self._makeConnections()

        if not self.cmdButtonGroup.checkedButton():
            self.cmdButtonGroup.button(0).setChecked(True)

        self.in_a_mode = None
        self.currentAction = None
        self._updateFlyoutToolBar(self.cmdButtonGroup.checkedId())

    def _makeConnections(self):
        """
        Connect signals to slots.
        """
        self.win.connect(self.cmdButtonGroup, SIGNAL("buttonClicked(int)"),
                         self.controlButtonChecked)



    def controlButtonChecked(self, btnId):
        """
        Updates the flyout toolbar based on the button checked in the
        control area.
        """
        self._updateFlyoutToolBar(btnId, self.in_a_mode)

    def resetToDefaultState(self):
        """
        Reset the Command Toolbar to the default state. i.e. the state which
        the user sees when NE1 is started -- Build button in the control area
        of the toolbar checked and the flyout toolbar on the right hand side
        shows the sub-menu items of the build control area.
        @see: baseCommand.command_update_flyout() which calls this while entering
        the NE1 default command 'SelectChunks_Command(fixes bugs like 2682, 2937)

        @see: self._setControlButtonMenu_in_flyoutToolbar()
        """
        #First deactivate all the flyouts (i.e current flyout and previous
        #flyout if any) This fixes bugs like bug 2937. This ensures that
        #all the 'custom' flyouts (seen while in a command) are always
        #deactivated if the current command dosesn't have a flyout toolbar
        #Thus, clicking on a 'Control Ara button' in the command toolbar
        #will always ensure that it will display that control button's menu
        #in the flyout area instead of a command specific custom flyout.
        #See bug 2937 and self._setControlButtonMenu_in_flyoutToolbar()
        for flyout in (self._f_current_flyoutToolbar,
                       self._f_previous_flyoutToolbar):
            if flyout:
                flyout.deActivateFlyoutToolbar()


        self._f_current_flyoutToolbar = None
        self._f_previous_flyoutToolbar = None

        self.cmdButtonGroup.button(0).setChecked(True)
        #Now update the command toolbar (flyout area) such that it shows
        #the sub-menu items of the control  button
        self._setControlButtonMenu_in_flyoutToolbar(self.cmdButtonGroup.checkedId())

        ##FYI Same effect can be acheived by using the following commented out
        ##code. But its not obvious so not using it.
        ###self.updateCommandToolbar(None, None, entering = False)

    def _updateFlyoutToolBar(self, btnId =0, in_a_mode = False):
        """
        Update the Flyout toolbar commands based on the checked button
        in the control area and the checked action in the 'subcontrol' area
        of the flyout toolbar.
        """
        ##in_a_mode : inside a mode or while editing a feature etc.

        if self.currentAction:
            action = self.currentAction
        else:
            action = None

        if in_a_mode:
            self._showFlyoutToolBar(True)
            self.flyoutToolBar.clear()
            #Menu of the checked button in the Control Area of the
            #command toolbar
            menu = self.cmdButtonGroup.checkedButton().menu()

            if menu:
                for a in menu.actions():
                    # 'action' is self.currentAction which is obtained
                    #when the 'updateCommandToolbar method is called
                    #while entering a mode , for instance.
                    if a is action :
                        flyoutActionList = []
                        flyoutDictionary = self._getFlyoutDictonary()

                        if not flyoutDictionary:
                            print_compact_traceback(
                                "bug: Flyout toolbar not" \
                                "created as flyoutDictionary doesn't exist")
                            return
                        #Dictonary object randomly orders key:value pairs.
                        #The following ensures that the keys
                        #(i.e. subcontrol area actions) are properly ordered
                        #This key order while adding these actions to the
                        # the flyout toolbar subcontrol area.
                        keyList = self.getOrderedKeyList(flyoutDictionary)

                        flyoutActionList.extend(keyList)
                        #Now append commands corresponding to
                        #the checked action in sub-control area
                        for k in keyList:
                            if k.isChecked():
                                #ordered_key is a list that contains a tuple
                                #i.e. ordered_key = [(counter, key)]
                                #so order_key[0] = (counter, key)
                                #This is required because our flyoutDictinary
                                #has keys of type:[ (counter1, key1),
                                #                          (counter2, key2), ..]
                                ordered_key = [(counter, key)
                                               for (counter, key)
                                               in flyoutDictionary.keys()
                                               if key is k]
                                #Values corresponding to the ordered_key
                                commands = flyoutDictionary[ordered_key[0]]
                                #Add these commands after the subcontrol area.
                                flyoutActionList.extend(commands)
                        #Now add all actions collected in 'flyoutActionList
                        #to the flyout toolbar.

                        self.flyoutToolBar.addActions(flyoutActionList)

                        return
                self._setControlButtonMenu_in_flyoutToolbar(
                    self.cmdButtonGroup.checkedId())
        else:
            self._showFlyoutToolBar(True)
            try:
                if self.cmdButtonGroup.button(btnId).isChecked():
                    self._setControlButtonMenu_in_flyoutToolbar(
                        self.cmdButtonGroup.checkedId())
            except:
                print_compact_traceback(
                    "command button doesn't have a menu, or other exception; No actions added " \
                    "to the flyout toolbar: " )
                return


    def _setControlButtonMenu_in_flyoutToolbar(self, btnId):
        """
        Sets the menu of a control area button in the flyout toolbar
        on the right.
        @param btnId: The index of the toolbutton in the control area,
                      that user clicked on.
        @type  btnId: int
        @see: AbstractFlyout.activateFlyoutToolbar() , another place
        where the self._f_current_flyoutToolbar value is changed.
        @see: self.resetToDefaultState()
        """

        #When the menu in the Control button is set in the flyout toolbar,
        #make sure to reset the self._f_current_flyoutToolbar value to 'None'
        #This value may change again when baseCommand.command_update_flyout()
        #is called (when command stack changes)

        #First store the _f_current_flyoutToolbar to _f_previous_flyoutToolbar.
        #before setting _f_current_flyoutToolbar to None. Note that when command
        #stack changes, both current flyout and previous flyout will be 'deactivated'
        # if, the new command doesn't have a flyout toolbar of its own.
        #see baseCommand.command_update_flyout()
        #@see: self.resetToDefaultState() which actually deactivates the current
        #and previous flyouts.

        #Why do we need to the value of current flyout to previous flyout?
        #-- consider steps in bug 2937. When user does step 2, two things happen
        #a) We store the current flyout value to previous flyout and b) then
        #the current flyout is set to None. If we don't do (a), the Move flyout
        #will never be deactivated by the current command and cause bug 2937
        if self._f_current_flyoutToolbar:
            self._f_previous_flyoutToolbar = self._f_current_flyoutToolbar

        self._f_current_flyoutToolbar = None

        self.flyoutToolBar.clear()
        menu = self.cmdButtonGroup.button(btnId).menu()
        self.flyoutToolBar.addActions(menu.actions())

    def check_controlAreaButton_containing_action(self, action = None):
        """
        This method is called once while entering a command. It makes
        sure to check the control area button which lists <action> as its
        menu item. (not true for BuildAtoms subclasses such as partlib mode)

        This ensures that when you enter a command, the flyout toolbar is set
        to show the Exit command etc buttons specific to that command. (The user
        can then always click on some other control button to display some
        different flyout toolbar. But by default, we ensure to show the flyout
        toolbar that the user will obviously expect to see after entering a
        command

        @see: bug 2600, 2801 (this method fixes these bugs)
        @TODO: Ater more testing, deprecate code in
               Ui_DnaFlyout.activateFlyoutToolbar and some other files that fixes
               bug 2600.
        """
        buttonToCheck = None

        if action:
            for controlAreaButton in self.cmdButtonGroup.buttons():
                if buttonToCheck:
                    break

                menu = controlAreaButton.menu()
                if menu:
                    for a in menu.actions():
                        if a is action:
                            buttonToCheck = controlAreaButton
                            break

        if buttonToCheck:
            buttonToCheck.setChecked(True)

    def updateCommandToolbar(self, action, obj, entering = True): #Ninad 070125
        """
        Update the command toolbar (i.e. show the appropriate toolbar)
        depending upon, the command button pressed .
        It calls a private method that updates the SubcontrolArea and flyout
        toolbar based on the ControlArea button checked and also based on the
        SubcontrolArea button checked.

        @param obj: Object that requests its own Command Manager flyout toolbar
                    This can be a B{mode} or a B{generator}.
        """
        if entering:
            self._createFlyoutToolBar(obj)
            self.in_a_mode = entering
            #This fixes bugs like 2600, 2801
            self.check_controlAreaButton_containing_action(action)
            self.currentAction = action
            self._updateFlyoutToolBar(in_a_mode = self.in_a_mode)
        else:
            self.in_a_mode = False
            self._updateFlyoutToolBar(self.cmdButtonGroup.checkedId(),
                                      in_a_mode = self.in_a_mode )

    def _createFlyoutDictionary(self, params):
        """
        Create the dictonary objet with subcontrol area actions as its
        'keys' and corresponding command actions as the key 'values'.

        @param params: A tuple that contains 3 lists:
        (subControlAreaActionList, commandActionLists, allActionsList)

        @return: flyoutDictionary (dictionary object)
        """

        subControlAreaActionList, commandActionLists, allActionsList = params
        #The subcontrol area button and its command list form a 'key:value pair
        #in a python dictionary object
        flyoutDictionary = {}

        counter = 0
        for k in subControlAreaActionList:
            # counter is used to sort the keys in the order in which they
            #were added
            key = (counter, k)
            flyoutDictionary[key] = commandActionLists[counter]
            #Also add command actions to the 'allActionsList'
            allActionsList.extend(commandActionLists[counter])
            counter += 1

        return flyoutDictionary



    def _createFlyoutToolBar(self, obj):
        """
        Creates the flyout tool bar in the Command Manager.
        @see: NE1_QWidgetAction.setToolButtonPalette()
        """
        #This was earlier defined in each mode needing a flyout toolbar
        params = obj.getFlyoutActionList()
        # XXXXXXXXXXXXXXXX The semantics of these lists need to be clearly defined!
        subControlAreaActionList, commandActionLists, allActionsList = params

        flyoutDictionary = self._createFlyoutDictionary(params)

        #Following counts the total number of actions in the 'command area'
        # if this is zero, that means our subcontrol area itself is a command
        #area, In this case, no special rendering is needed for this subcontrol
        #area, and so its buttons are rendered as if they were command area
        #buttons (but internally its still the 'subcontrol area'.--ninad20070622
        cmdActionCount = 0
        for l in commandActionLists:
            cmdActionCount += len(l)

        widgetActions = filter(lambda act:
                               isinstance(act, QtGui.QWidgetAction),
                               allActionsList)


        self.flyoutToolBar.clear()
        #========
        #Do this customization BEFORE adding actions to the toolbar.
        #Reason: custom properties for the button that appears
        #can not be set AFTER the actions are added to the QToolBar given that
        #the NE1_QWidgetAction.createWidget() method is implemented. (This
        #method gets called when we do toolbar.addAction()

        #Example: The commented out code below won't change the toolbutton
        #properties -- a leaset in Qt4.3
        ##for widget in action.createdWidgets():
                    ##if isinstance(widget, QToolButton):
                        ##btn = widget
                        ##break
                    ##btn.setPalette(somePalette)
                    ##btn.setText('someText')
        # [Ninad 2008-07-15 comment]
        #=========


        for action in widgetActions:
            #Set a different color palette for the 'SubControl' buttons in
            #the command toolbar.
            if [key for (counter, key) in flyoutDictionary.keys()
                if key is action]:


                if cmdActionCount > 0:
                    subControlPalette = self.getCmdMgrSubCtrlAreaPalette()
                    action.setToolButtonPalette(subControlPalette)

                else:
                    cmdPalette = self.getCmdMgrCommandAreaPalette()
                    action.setToolButtonPalette(cmdPalette)



        self.flyoutToolBar.addActions(allActionsList)

        ##for action in allActionsList:
            ##if isinstance(action, QtGui.QWidgetAction):
                ##btn = None
                ####for widget in action.associatedWidgets():
                ##for widget in action.createdWidgets():
                    ##if isinstance(widget, QToolButton):
                        ##btn = widget
                        ##break

                ##if btn is None:
                    ##print_compact_stack("bug: Action to be added to the flyout toolbar has no"\
                          ##"ToolButton associated with it")
                    ##continue


        self._setFlyoutDictionary(flyoutDictionary)


    def _showFlyoutToolBar(self, bool_show):
        """
        Hide or show flyout toolbar depending upon what is requested.
        At the same time toggle the display of the spacer item
        (show it when the toolbar is hidden).
        """
        if bool_show:
            self.flyoutToolBar.show()
            self.layout().removeItem(self.spacerItem)
        else:
            self.flyoutToolBar.hide()
            self.layout().addItem(self.spacerItem)

    def _setFlyoutDictionary(self, dictionary):
        """
        Set the flyout dictionary that stores subcontrol area buttons in the
        flyout toolbar as the keys and their corresponding command buttons
        as values
        @param dictionary: dictionary object.
        Its key is of the type (counter, subcontrolAreaAction). The counter is
        used to sort the keys in the order in which they were created.
        and value is the 'list of commands'  corresponding to the
        sub control button.
        """
        if isinstance(dictionary, dict):
            self.flyoutDictionary = dictionary
        else:
            assert isinstance(dictionary, dict)
            self.flyoutDictionary = None

    def _getFlyoutDictonary(self):
        """
        Returns the flyout dictonary object.
        @return: self.flyoutDictionary whose
        key : is of the type (counter, subcontrolAreaAction). The counter is
        used to sort the keys in the order in which they were created.
        and value is the 'list of commands'  corresponding to the
        sub control button.
        """
        if self.flyoutDictionary:
            return self.flyoutDictionary
        else:
            print "fyi: flyoutDictionary doesn't exist. Returning None"
            return self.flyoutDictionary

    def getOrderedKeyList(self, dictionary):
        """
        Orders the keys of a dictionary and returns it as a list.
        Effective only when the dictonary key is a tuple of format
        (counter, key)
        @param: dictinary object
        @return: a list object which contains ordered keys of the dictionary
        object.
        """
        keyList = dictionary.keys()
        keyList.sort()
        return [keys for (counter, keys) in keyList]

    def _getNumberOfVisibleToolButtons(self):
        """
          """
        numOfVisibletoolbuttons = 0
        rect = self.flyoutToolBar.visibleRegion().boundingRect()
        visibleWidth = rect.width()
        numOfVisibletoolbuttons = int(visibleWidth)/ 75
        return numOfVisibletoolbuttons