Issue
I have a menu that I want to add to a QToolBar.
I know that I can add the menuAction()
of the menu to the tool bar, but while that will properly show the "menu hint" on its side and popup the menu by clicking on it, clicking the main area of the button will have no effect.
That action is not supposed to have any result when triggered: the menu is used to set the font color in a text editor, and since it automatically updates its icon based on the current color, making it checkable (to set/unset the font color) is ineffective.
What I want is that the menu will be shown, no matter where the user clicks.
I know that I can add the action, then use widgetForAction()
to get the actual QToolButton, and then change its popupMode
, but since I know that I will have more situations like this, I was looking for a better approach.
This answer suggests to use QPushButton instead, and add that button to the toolbar, but that solution is not ideal: QPushButton is styled slightly differently from the default QToolButton, and, as the documentation suggests, even if I use a QToolButton it will not respect the ToolButtonStyle
.
Here is a basic MRE of my current code. Please consider that the ColorMenu
class is intended to be extended for other features (background text, colors for table borders and backgrounds, etc) by using subclasses:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ColorMenu(QMenu):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle('Text color')
self.group = QActionGroup(self)
iconSize = self.style().pixelMetric(QStyle.PM_LargeIconSize)
pm = QPixmap(iconSize, iconSize)
pm.fill(self.palette().text().color())
self.defaultAction = self.addAction(QIcon(pm), 'Default color')
self.defaultAction.setCheckable(True)
self.group.addAction(self.defaultAction)
self.addSeparator()
self.customColorAction = self.addAction('Custom color')
self.customColorAction.setVisible(False)
self.customColorAction.setCheckable(True)
self.group.addAction(self.customColorAction)
self.addSeparator()
self.baseColorActions = []
colors = {}
# get valid global colors
for key, value in Qt.__dict__.items():
if (
isinstance(value, Qt.GlobalColor)
and 1 < value < 19
):
# make names more readable
if key.startswith('light'):
key = 'light {}'.format(key[5:].lower())
elif key.startswith('dark'):
key = 'dark {}'.format(key[4:].lower())
colors[value] = key.capitalize()
# more logical sorting of global colors
for i in (2, 4, 5, 6, 3, 7, 13, 8, 14, 9, 15, 10, 16, 11, 17, 12, 18):
color = QColor(Qt.GlobalColor(i))
pm = QPixmap(iconSize, iconSize)
pm.fill(color)
action = self.addAction(QIcon(pm), colors[i])
action.setData(color)
action.setCheckable(True)
self.group.addAction(action)
self.baseColorActions.append(action)
self.setColor(None)
def setColor(self, color):
if isinstance(color, QBrush) and color.style():
color = color.color()
elif isinstance(color, (Qt.GlobalColor, int):
color = QColor(color)
if instance(color, QColor) and color.isValid():
for action in self.baseColorActions:
if action.data() == color:
self.setIcon(action.icon())
action.setChecked(True)
self.customColorAction.setVisible(False)
break
else:
iconSize = self.style().pixelMetric(QStyle.PM_LargeIconSize)
pm = QPixmap(iconSize, iconSize)
pm.fill(color)
icon = QIcon(pm)
self.setIcon(icon)
self.customColorAction.setIcon(icon)
self.customColorAction.setData(color)
self.customColorAction.setVisible(True)
self.customColorAction.setChecked(True)
return
self.setIcon(self.defaultAction.icon())
self.defaultAction.setChecked(True)
self.customColorAction.setVisible(False)
class Editor(QMainWindow):
def __init__(self):
super().__init__()
self.editor = QTextEdit()
self.setCentralWidget(self.editor)
self.formatMenu = self.menuBar().addMenu('Format')
self.colorMenu = ColorMenu(self)
self.formatMenu.addMenu(self.colorMenu)
self.toolbar = QToolBar('Format')
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
self.toolbar.addAction(self.colorMenu.menuAction())
self.editor.currentCharFormatChanged.connect(self.updateColorMenu)
self.colorMenu.triggered.connect(self.setTextColor)
def setTextColor(self, action):
# assume that the action.data() has a color value, if not, revert to the default
if action.data():
self.editor.setTextColor(action.data())
else:
tc = self.editor.textCursor()
fmt = tc.charFormat()
fmt.clearForeground()
tc.setCharFormat(fmt)
def updateColorMenu(self, fmt):
self.colorMenu.setColor(fmt.foreground())
app = QApplication([])
editor = Editor()
editor.show()
app.exec()
Solution
A possibility is to use a subclass of QMenu and implement a specialized function that will return a dedicated action.
This is a bit of a hack/workaround, but may be effective in some situations.
That new action will:
- be created by providing the tool bar, which will become its parent (to ensure proper deletion if the tool bar is destroyed);
- forcibly show the menu when triggered;
- update itself (title and icon) whenever the menu action is changed;
class ColorMenu(QMenu):
# ...
def toolBarAction(self, toolbar):
def triggerMenu():
try:
button = toolbar.widgetForAction(action)
if isinstance(button, QToolButton):
button.showMenu()
else:
print('Warning: action triggered from somewhere else')
except (TypeError, RuntimeError):
# the action has been destroyed
pass
def updateAction():
try:
action.setIcon(self.icon())
action.setText(self.title())
except (TypeError, RuntimeError):
# the action has been destroyed
pass
action = QAction(self.icon(), self.title(), toolbar)
action.triggered.connect(triggerMenu)
self.menuAction().changed.connect(updateAction)
action.setMenu(self)
return action
class Editor(QMainWindow):
def __init__(self):
# ...
# replace the related line with the following
self.toolbar.addAction(self.colorMenu.toolBarAction(self.toolbar))
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.