PyQt4 dropdownlist with actions - python

I want to create a drop down list in PyQt4, that executes an action when an element is selected. Also, some options may not be available at some time. They should still be in the list, but greyed out.
I tried attaching a menu to a QToolButton, but I can not even see the menu.
How is it done?
Thanks!
Nathan

Use a popup. You can trigger a popup anywhere, using the QMenu.exec_ method and passing the point at which you want the menu to appear.
I created a button that remembered where it was clicked, and connected that to the method to create and display the popup.
class MemoryButton(QPushButton):
def __init__(self, *args, **kw):
QPushButton.__init__(self, *args, **kw)
self.last_mouse_pos = None
def mousePressEvent(self, event):
self.last_mouse_pos = event.pos()
QPushButton.mousePressEvent(self, event)
def mouseReleaseEvent(self, event):
self.last_mouse_pos = event.pos()
QPushButton.mouseReleaseEvent(self, event)
def get_last_pos(self):
if self.last_mouse_pos:
return self.mapToGlobal(self.last_mouse_pos)
else:
return None
button = MemoryButton("Click Me!")
def popup_menu():
popup = QMenu()
menu = popup.addMenu("Do Action")
def _action(check):
print "Action Clicked!"
menu.addAction("Action").triggered.connect(_action)
popup.exec_(button.get_last_pos())
button.clicked.connect(popup_menu)

QToolButton has ToolButtonPopupMode enum that controls how it handles menus and multiple actions. When set to QToolButton::MenuButtonPopup, it will display the arrow that is typical of buttons that have menu options.
To use it set the appropriate popup mode and then you can either add a menu to the QToolButton using setMenu or you can add actions using addAction. QToolButton should then respond as expected to clicks on the menu, whether Action generated or an actual QMenu.

Related

Switching Buttons in QMessageBox

I have this code:
def initUI(self):
mainLayout = QVBoxLayout()
# Temporary: Dialog Testing Button #
self.buttonTest = button('Dialog Testing')
self.buttonTest.clicked.connect(self.TestFile)
mainLayout.addWidget(self.buttonTest)
self.setLayout(mainLayout)
# Connects to dialogTesting Button
def dialogMessage(self, message):
dialog = QMessageBox(self)
dialog.setWindowTitle('Sample Text')
dialog.setText(message)
dialog.show()
# Connects with dialogMessage class.
def TestFile(self):
self.dialogMessage("When will non-OK buttons appear?")
return
I get results like this:
How can I change what buttons appear in the popup?
You can change the buttons by using an argument for the buttons parameter when you instantiate a QMessageBox. You use the Standard Button constants. Here is a silly example:
dialog = QMessageBox(self, buttons=QMessageBox.Ok+QMessageBox.Save+QMessageBox.Yes )
If you don't like the choices for standard buttons, you can make your own using addButton. This answer shows how to use addButton.

Wrong action triggered when pressing enter in QMenu

I have a context menu with a checkbox and a spinbox. Both work as expected, except that when I am typing a number in the spinbox, I tend to hit enter to close the menu, but that also toggles the checkbox. Does anyone know how to prevent that?
minimal example:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QFrame, QMenu, QAction, QWidgetAction, QFormLayout, QSpinBox
from PyQt5.QtCore import Qt
t = True
v = 0
class SpinAction(QWidgetAction):
def __init__(self, parent):
super(SpinAction, self).__init__(parent)
w = QWidget()
layout = QFormLayout(w)
self.spin = QSpinBox()
layout.addRow('value', self.spin)
w.setLayout(layout)
self.setDefaultWidget(w)
def _menu(position):
menu = QMenu()
test_action = QAction(text="Test", parent=menu, checkable=True)
test_action.setChecked(t)
test_action.toggled.connect(toggle_test)
spin_action = SpinAction(menu)
spin_action.spin.setValue(v)
spin_action.spin.valueChanged.connect(spin_changed)
menu.addAction(test_action)
menu.addAction(spin_action)
action = menu.exec_(w.mapToGlobal(position))
def toggle_test(val):
global t
t = val
def spin_changed(val):
global v
v = val
app = QApplication(sys.argv)
w = QFrame()
w.setContextMenuPolicy(Qt.CustomContextMenu)
w.customContextMenuRequested.connect(_menu)
w.show()
sys.exit(app.exec_())
The widget must accept focus, so a proper focus policy (StrongFocus or WheelFocus) must be set, but since it's a container you should also set the focus proxy to the spin, so that it will handle the focus for it.
In this way the keyboard navigation of the menu will also work properly by using arrows or Tab to focus the spinbox.
The action also has to be set as the active action of the menu in order to prevent the menu to accept events that may trigger other actions, and a way to achieve that is to install an event filter on the spin and check for events that should make the widget action as the active one, like FocusIn and Enter event.
Note that this results in some unintuitive results from the UX perspective: by default, hovering a normal QAction makes it the active one, but the user might want to move the mouse away from the spinbox in order to edit it without having the mouse on it, and by doing it another action could become active; in the meantime the spin should still keep the focus for obvious reasons. The result is that the other action might seem visually selected while the spinbox has focus. There is no obvious solution for this, and that's why widgets used for QWidgetActions should always be chosen with care.
class SpinAction(QWidgetAction):
FocusEvents = QtCore.QEvent.FocusIn, QtCore.QEvent.Enter
ActivateEvents = QtCore.QEvent.KeyPress, QtCore.QEvent.Wheel)
WatchedEvents = FocusEvents + ActivateEvents
def __init__(self, parent):
# ...
w.setFocusPolicy(self.spin.focusPolicy())
w.setFocusProxy(self.spin)
self.spin.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.spin and event.type() in self.WatchedEvents:
if isinstance(self.parent(), QtWidgets.QMenu):
self.parent().setActiveAction(self)
if event.type() in self.FocusEvents:
self.spin.setFocus()
return super().eventFilter(obj, event)
Note: the obj == self.spin condition is required since QWidgetAction already installs an event filter. A more elegant solution is to create a QObject subclass intended for that purpose only, and only overriding eventFilter().

Qt menu artifact when calling input dialog

I am building a PyQt application which is supposed to receive mouse right-click drags on a QGraphicsView, draw a "lasso" (a line stretching from the drag origin to the mouse position, and a circle at the mouse position), and then, on mouse release, erase the lasso graphics and display an input dialog for the next part of the app.
For some reason, when I use the mouse to click "Ok" on the input dialog, a menu artifact appears on the QGraphicsView which contained the lasso. The menu artifact is a drop-down menu line that says "(check mark) Exit". Occasionally it may be the context menu for one of my custom QGraphicsObjects as well - but for whatever reason, calling the dialog and then clicking "Ok" results in an unintended right-click-like event on the QGraphicsView.
This only seems to happen when the last step before method return is the QInputDialog - replacing it with a pass or a call to some other method does not result in the artifact. I'd be very grateful to anyone with a clue to what is causing this problem!
Here's the minimal code:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QMainWindow):
# The app main window.
def __init__(self):
super(Window, self).__init__()
# Initialize window UI
self.initUI()
def initUI(self, labelText=None):
# Set user-interface attributes.
# Set up menu-, tool-, status-bars and add associated actions.
self.toolbar = self.addToolBar('Exit')
# Create a menu item to exit the app.
exitAction = QtGui.QAction(QtGui.QIcon('icons/exit.png'), '&Exit', self)
exitAction.triggered.connect(QtGui.qApp.quit)
self.toolbar.addAction(exitAction)
# Create the main view.
self.viewNetwork = NetworkPortal()
self.viewNetwork.setMinimumWidth(800)
self.viewNetwork.setMinimumHeight(800)
self.setCentralWidget(self.viewNetwork)
self.show()
class NetworkPortal(QtGui.QGraphicsView):
# A view which allows you to see and manipulate a network of nodes.
def __init__(self):
super(NetworkPortal, self).__init__(QtGui.QGraphicsScene())
# Add the CircleThing graphic to the scene.
circleThing = CircleThing()
self.scene().addItem(circleThing)
class CircleThing(QtGui.QGraphicsEllipseItem):
# Defines the graphical object.
def __init__(self):
super(CircleThing, self).__init__(-10, -10, 20, 20)
# Set flags for the graphical object.
self.setFlags(
QtGui.QGraphicsItem.ItemIsSelectable |
QtGui.QGraphicsItem.ItemIsMovable |
QtGui.QGraphicsItem.ItemSendsScenePositionChanges
)
self.dragLine = None
self.dragCircle = None
def mouseMoveEvent(self, event):
# Reimplements mouseMoveEvent to drag out a line which can, on
# mouseReleaseEvent, form a new Relationship or create a new Thing.
# If just beginning a drag,
if self.dragLine == None:
# Create a new lasso line.
self.startPosX = event.scenePos().x()
self.startPosY = event.scenePos().y()
self.dragLine = self.scene().addLine(
self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y(),
QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine)
)
# Create a new lasso circle at the location of the drag position.
self.dragCircle = QtGui.QGraphicsEllipseItem(-5, -5, 10, 10)
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
self.scene().addItem(self.dragCircle)
# If a drag is already in progress,
else:
# Move the lasso line and circle to the drag position.
self.dragLine.setLine(QtCore.QLineF(self.startPosX,
self.startPosY,
event.scenePos().x(),
event.scenePos().y()))
self.dragCircle.setPos(event.scenePos().x(),
event.scenePos().y())
def mouseReleaseEvent(self, event):
# If the line already exists,
if self.dragLine != None:
# If the released button was the right mouse button,
if event.button() == QtCore.Qt.RightButton:
# Clean up the link-drag graphics.
self.scene().removeItem(self.dragLine)
self.dragLine = None
self.scene().removeItem(self.dragCircle)
self.dragCircle = None
# Create the related Thing.
# Display input box querying for name value.
entry, ok = QtGui.QInputDialog.getText(None, 'Enter some info: ',
'Input:', QtGui.QLineEdit.Normal, '')
return
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
newWindow = Window()
sys.exit(app.exec_())
This is a guess on my part, but I've seen this kind of weird thing before.
Some Qt widgets have default behavior on certain types of events. I've never used QGraphicsView, but often the default for a right click is to open a context-sensitive pop-up menu (usually useless, in my experience). That may be happening in your case, which would explain why you only see this when there's a right click.
You can suppress the default Qt behavior by calling event.ignore() before returning from mouseReleaseEvent.
There was not a direct answer to the cause of this bug, but using a QTimer.singleShot() to call the dialog (in combination with a QSignalMapper in order to enter parameters) is a functional workaround that separates the dialog from the method in which the bug was occurring. Thanks to #Avaris for this one.

Simulate user clicking in QSystemTrayIcon

Even through the activated slot is being executed, the menu is still not showing. I traced through manually clicking the tray icon and the simulated click, and its going through the same execution logic.
Currently I have
class MyClass(QObject):
def __init__():
self._testSignal.connect(self._test_show)
self.myTrayIcon.activated.connect(lambda reason: self._update_menu_and_show(reason))
def show():
self._testSignal.emit()
#pyqtSlot()
def _test_show():
self._trayIcon.activated.emit(QtWidgets.QSystemTrayIcon.Trigger)
#QtCore.pyqtSlot()
def _update_menu_and_show(reason):
if reason in (QtWidgets.QSystemTrayIcon.Trigger):
mySystemTrayIcon._update_menu()
...
class MySystemTrayIcon(QSystemTrayIcon):
def _update_menu(self):
# logic to populate menu
self.setContextMenu(menu)
...
MyClass().show()
Here is how I made the context menu associated with the tray icon pop up
class MyClass(QObject):
def __init__():
self._testSignal.connect(self._test_show)
self.myTrayIcon.activated.connect(lambda reason: self._update_menu_and_show(reason))
def show():
self._testSignal.emit()
#pyqtSlot()
def _test_show():
self._trayIcon.activated.emit(QSystemTrayIcon.Context)
#QtCore.pyqtSlot()
def _update_menu_and_show(reason):
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.Context):
mySystemTrayIcon._update_menu()
# Trigger means user initiated, Context used for simulated
# if simulated seems like we have to tell the window to explicitly show
if reason == QSystemTrayIcon.Context:
mySystemTrayIcon.contextMenu().setWindowFlags(QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.FramelessWindowHint)
pos = mySystemTrayIcon.geometry().bottomLeft()
mySystemTrayIcon.contextMenu().move(pos)
mySystemTrayIcon.contextMenu().show()
...
class MySystemTrayIcon(QSystemTrayIcon):
def _update_menu(self):
# logic to populate menu
self.setContextMenu(menu)
...
MyClass().show()
It seems you have to set the WindowStaysOnTopHint on the context menu so that it will appear.
This solution is specific to mac since it assumes the taskbar is on the top.
One side effect is that the context menu is always on top, even if the user clicks somewhere else. I placed an event filter on the context menu, the only useful event that it registered was QEvent.Leave

Gtk3: Dynamically adding to and removing a button from an EventBox: Button won't receive mouse press signals

I'm using python and PyGObjects (the introspection lib) for Gtk 3 here.
Consider the following code:
from gi.repository import Gtk
class InternalWidget(Gtk.Button):
def __init__(self):
super(InternalWidget, self).__init__()
self.set_size_request(100,100)
self.connect("button-press-event", self.on_press)
def on_press(self, *args):
print "The Internal Widget was clicked."
class ExternalEventBox(Gtk.EventBox):
def __init__(self):
super(ExternalEventBox, self).__init__()
self.fixed = Gtk.Fixed()
self.add(self.fixed)
self.internal_widget = InternalWidget()
self.set_size_request(200, 200)
self.connect("button-press-event", self.on_press)
self.connect("enter-notify-event", self.on_enter)
self.connect("leave-notify-event", self.on_leave)
def on_enter(self, *args):
self.fixed.put(self.internal_widget, 50,50)
self.show_all()
def on_leave(self, *args):
self.fixed.remove(self.internal_widget)
def on_press(self,*args):
print "The External Event Box was clicked."
w = Gtk.Window(Gtk.WindowType.TOPLEVEL)
w.connect("delete-event", Gtk.main_quit)
w.add(ExternalEventBox())
w.show_all()
Gtk.main()
Above, whenever the mouse enters the ExternalEventBox, a button (InternalWidget) is added to it as a child. When the mouse leaves the ExternalEventBox, the button is removed as a child of the ExternalEventBox.
Now, if you run the code (which you can), the button appears and disappears properly. However, clicking on the button, contrary to what is expected, only sends a signal to the containing ExternalEventBox, whereas the button receives no signal.
Interestingly, the expected behavior (clicking on the button actually clicks it) happens when the button, rather than being dynamically added and removed, is added once in the constructor of the event box, and never removed.
Is this a bug, or am I just missing something?
Edit: In a nutshell, I only get "The External Event Box was clicked.", but never "The Internal Widget was clicked.".
Update: I filed a bug report.
You need to set the EventBox event window to be below it's children using .set_above_child(false)
Here's the docs for it: GtkEventBox
If the window is above, all events inside the event box will go to the event box. If the window is below, events in windows of child widgets will first got to that widget, and then to its parents.

Categories

Resources