"Special Items" in QMenu



  • Some programs like Maya and Wings3D have a menu, which includes a special link on some menu items on the right side (usually a square) that triggers a different feature. For example, Wings3D might have an item called "Cube", which makes a cube with default parameters. However, if you click on the highlightable/selectable box beside the "Cube" item, it will perform a different operation like bring up a popup box that allows more customizable features. I attached an image of the Wings3D example.

    Is this an easy way to do add these special menu items using QMenu and QAction? Can I just create a special layout for the QMenu or do I need to subclass it to get more fine-grained control? I know Maya 2011 just switched to Qt from another framework, so they must have implemented it somehow. I've looked at QWidgetAction, but it seems to be the wrong direction.

    !http://www.scbots.com/link_images/wings_menu.png(Wings3D menu with special items)!



  • If you only need a few items customized, QWidgetAction would probably be the way to go. If you want to use the feature extensively like Maya does, you'll need to implement a custom menu class. Unfortunately, setting a special layout doesn't work. QMenu internally uses style settings directly to layout the items.

    I know Maya 2011 uses a custom Qt which has been modified for some backward compatibility. This feature might be one of the modifications they made.



  • One untested idea: you probably could use the "image" associated with the action. I have never tried but once is part of the constructor I assume it works... Don't know if you can have it on the right side though.

    But if this is a "cross platform", and especially a phone, application you need to be careful. The N900, for example, has it is own way of dealing with menus/actions.



  • Would using a QWidgetAction just entail making a horizontal box layout and adding to single-action QMenus to the QWidgetAction? That way I may get mouseover highlighting to look natural.



  • [quote author="strattonbrazil" date="1291831458"]Would using a QWidgetAction just entail making a horizontal box layout and adding to single-action QMenus to the QWidgetAction? That way I may get mouseover highlighting to look natural. [/quote]

    I think that would work.

    Here is another (crazy) idea. Add widgets for just the "special link" controls to the QMenu, but position them manually using geometries obtained from QMenu::actionGeometry().



  • Finally got around to implementing this using the two QMenu approach. Took awhile because a lot of the QMenu code is pretty complex and many functions aren't directly exposed in the QMenu API.

    The idea was pretty simple, but I had to go back and add a bunch of overrides for little things like when the mouse enters one of the internal QMenus and doesn't dehighlight any of the other actions (or vice versa). And it's pretty easy to connect to the main QMenu, so triggering the option box can emit the action from the outer menu just as the submenu does.

    @
    import sys
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *

    class OptionBoxAction(QWidgetAction):
    class InWidgetMenu(QMenu):
    def enterEvent(self, event):
    # unhighlight neigboring actions (shouldn't this be built in?)
    #
    super(OptionBoxAction.InWidgetMenu, self).enterEvent(event)
    self.parent().parent().setActiveAction(None)

        def leaveEvent(self, event):
            super(OptionBoxAction.InWidgetMenu, self).leaveEvent(event)
            self.update()
    
        def mouseMoveEvent(self, event):
            super(OptionBoxAction.InWidgetMenu, self).mouseMoveEvent(event)
            self.update()
    
    class MainMenu(InWidgetMenu):
        def paintEvent(self, event):
            p = QPainter(self)
            action = self.actions()[0] # should only have one action
            rect = QRect(self.actionGeometry(action))
            rect.setWidth(self.width()) # QActions don't automatically resize
            opt = QStyleOptionMenuItem()
            self.initStyleOption(opt, action)
            self.style().drawControl(QStyle.CE_MenuItem, opt, p)
    
    class CustomMenu(InWidgetMenu):
        def paintEvent(self, event):
            p = QPainter(self)
            action = self.actions()[0] # should only have one action
            rect = QRect(self.actionGeometry(action))
    
            opt = QStyleOptionMenuItem()
            self.initStyleOption(opt, action)
            opt.text = '' # only show icon
            opt.icon = QIcon('./optionBox.png')
            self.style().drawControl(QStyle.CE_MenuItem, opt, p, self)
    
    def __init__(self, *args, **kwargs):
        super(OptionBoxAction, self).__init__(*args, **kwargs)
    
        self._widget = QWidget()
        self.setDefaultWidget(self._widget)
    
        self._menuLeft = self.MainMenu()
        self._menuRight = self.CustomMenu()
        self._menuRight.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
        self._menuRight.setMaximumWidth(30)
    
        self._widget.setLayout(QHBoxLayout())
        self._widget.layout().addWidget(self._menuLeft)
        self._widget.layout().addWidget(self._menuRight)
        self._widget.layout().setMargin(0)
        self._widget.layout().setSpacing(0)
    
    def setMainAction(self, action):
        self._menuLeft.clear()
        self._menuLeft.addAction(action)
        self.connect(action, SIGNAL('triggered(bool)'), self.mainTrigger)
    
    def getParentMenu(self):
        return self._menuLeft.parent().parent()
    _parentMenu = property(getParentMenu)
    
    def mainTrigger(self, checked): 
        self._parentMenu.hide()
        self._parentMenu.emit(SIGNAL("triggered(QAction*)"), self._menuLeft.actions()[0])
    
    def setCustomAction(self, action):
        self._menuRight.clear()
        self._menuRight.addAction(action)
        self.connect(action, SIGNAL('triggered(bool)'), self.customTrigger)
    
    def customTrigger(self, checked):
        self._parentMenu.hide()
        self._parentMenu.emit(SIGNAL("triggered(QAction*)"), self._menuRight.actions()[0])
    

    app = QApplication(sys.argv)
    window = QMainWindow()

    testMenu = QMenu("Test")

    add a submenu

    subMenu = testMenu.addMenu('Sub Menu of Options')
    subMenu.addAction('Option #1')
    subMenu.addAction('Option #2')

    add the action (with the option box)

    optionBoxAction = OptionBoxAction(testMenu)
    optionBoxAction.setMainAction(QAction('Test', testMenu))
    optionBoxAction.setCustomAction(QAction('Never render this', testMenu))

    add some other actions

    testMenu.addAction(optionBoxAction)
    testMenu.addAction('Really Long Action')
    testMenu.addAction('With checks').setCheckable(True)

    def triggerEvent(action):
    print('Event triggered: %s' % action.text())

    wire up the QMenu (should automatically connect signal in 'optionBoxAction')

    QObject.connect(testMenu, SIGNAL('triggered(QAction*)'), triggerEvent)

    window.menuBar().addMenu(testMenu)
    window.show()
    app.exec_()
    @


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.