Modify QTableWidgetItem context menu when editing - python

I'm trying to add actions to the context menu that pops up when I right click while editing a cell’s content in a QTableWidget. I tried redefining the contextMenuEvent() method from QTableWidget but it is never called in this context. I also tried to call cellWidget() method on the items, but as I expected it returned None. I'm guessing that a temporary QLineEdit widget is created whenever a cell is in edit mode but couldn't find anything to confirm that.
Is there any way to access the context menu of QTableWidgetItems when in edit mode?
I've looked extensively through the documentation but to no avail.

The context menu that you must handle is the editor provided by the delegate. So in this case a QStyledItemDelegate must be implemented:
from PyQt5 import QtCore, QtGui, QtWidgets
class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
if isinstance(editor, QtWidgets.QLineEdit):
editor.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
editor.setProperty("index", QtCore.QPersistentModelIndex(index))
editor.customContextMenuRequested.connect(self.handle_context_menu)
return editor
def handle_context_menu(self, pos):
editor = self.sender()
if isinstance(editor, QtWidgets.QLineEdit):
index = editor.property("index")
menu = editor.createStandardContextMenu()
action = menu.addAction("New Action")
action.setProperty("index", index)
action.triggered.connect(self.handle_add_new_action_triggered)
menu.exec_(editor.mapToGlobal(pos))
def handle_add_new_action_triggered(self):
action = self.sender()
if isinstance(action, QtWidgets.QAction):
index = action.property("index")
print(index.row(), index.column(), index.data())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
view = QtWidgets.QTableWidget(3, 5)
delegate = StyledItemDelegate(view)
view.setItemDelegate(delegate)
view.resize(640, 480)
view.show()
sys.exit(app.exec_())

Related

How to work with the "?" (what's this widget) on the title bar of a PyQT Dialog

On the right of the title bar of a PyQt QDialog (see below, next to the "x") there is a "?" that is supposed to help the user query help for any other widget on the Dialog window.
What should I do (programmatically) to get it to work. Once the "?" isClicked, one should be able to capture the next widget clicked and provide a ToolTip or something like that. In PyQt, I do not know how to capture the isClicked event on the "?".
I have seen a couple of posts where the question was how to make the "?" disappear, but the discussion there uses Qt, not PyQt, so I do not understand it, and they are not talking about what I need. I need to make it work as intended. See How can I hide/delete the "?" help button on the "title bar" of a Qt Dialog? and PyQt4 QInputDialog and QMessageBox window flags
You can set the whatsThis property to any widget you want:
self.someWidget.setWhatsThis('hello!')
From that point on, whenever you click on the "?" button and then click on that widget, a tooltip with that text will be shown.
Since the "what's this" mode is individually set to widgets, there's no easy way to capture it globally (as far as I know of) because if the widget has no whatsthis property set that feature won't be available for it.
Also, whenever you enter the "what's this" mode, the cursor will probably change according to the contents of the whatsthis property: if it's not set, the cursor will probably show a "disabled" icon.
I've created a basic workaround for this issue, which automatically enables any child widget's whatsthis (if none is already set) whenever the mode is activated: as soon as the EnterWhatsThisMode is fired, it automatically installs a custom object that acts as an event filter, and emits a signal if the whatsthis event is called; as soon as the mode exits, the filter is removed.
I used a separate object for the event filter because there's no way to know what event filter have been already installed to a widget, and if you already installed the parent's one, removing it automatically would be an issue.
class WhatsThisWatcher(QtCore.QObject):
whatsThisRequest = QtCore.pyqtSignal(QtWidgets.QWidget)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.WhatsThis:
self.whatsThisRequest.emit(source)
return super(WhatsThisWatcher, self).eventFilter(source, event)
class W(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout(self)
hasWhatsThisButton = QtWidgets.QPushButton('Has whatsThis')
layout.addWidget(hasWhatsThisButton)
hasWhatsThisButton.setWhatsThis('I am a button!')
noWhatsThisButton = QtWidgets.QPushButton('No whatsThis')
layout.addWidget(noWhatsThisButton)
self.whatsThisWatchedWidgets = []
self.whatsThisWatcher = WhatsThisWatcher()
self.whatsThisWatcher.whatsThisRequest.connect(self.showCustomWhatsThis)
whatsThisButton = QtWidgets.QPushButton('Set "What\'s this" mode')
layout.addWidget(whatsThisButton)
whatsThisButton.clicked.connect(QtWidgets.QWhatsThis.enterWhatsThisMode)
def event(self, event):
if event.type() == QtCore.QEvent.EnterWhatsThisMode:
for widget in self.findChildren(QtWidgets.QWidget):
if not widget.whatsThis():
# install the custom filter
widget.installEventFilter(self.whatsThisWatcher)
# set an arbitrary string to ensure that the "whatsThis" is
# enabled and the cursor is correctly set
widget.setWhatsThis('whatever')
self.whatsThisWatchedWidgets.append(widget)
elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
while self.whatsThisWatchedWidgets:
widget = self.whatsThisWatchedWidgets.pop()
# reset the whatsThis string to none and uninstall the filter
widget.setWhatsThis('')
widget.removeEventFilter(self.whatsThisWatcher)
return super(W, self).event(event)
def showCustomWhatsThis(self, widget):
widgetPos = widget.mapTo(self, QtCore.QPoint())
QtWidgets.QWhatsThis.showText(
QtGui.QCursor.pos(),
'There is no "what\'s this" for {} widget at coords {}, {}'.format(
widget.__class__.__name__, widgetPos.x(), widgetPos.y()),
widget)
A couple of notes about this:
I used a button to activate the whatsthis mode, as on my window manager on Linux there's no window title button for that;
Some widgets may contain subwidgets, and you'll get those instead of the "main" one (the most common case are QAbstractScrollArea descendands, like QTextEdit or QGraphicsView, which might return the viewport, the inner "widget" or the scrollbars);
By default the task of that button is to enable whatsThis: press "?", then press the widget and you will see the message associated with whatsThis property.
If you want to add other actions(open url, add QToolTip, etc) you can monitor the QEvent::EnterWhatsThisMode and QEvent::LeaveWhatsThisMode events overriding the event() method or using an eventFilter().
from PyQt5 import QtCore, QtGui, QtWidgets
class Dialog(QtWidgets.QDialog):
def event(self, event):
if event.type() == QtCore.QEvent.EnterWhatsThisMode:
print("enter")
QtGui.QDesktopServices.openUrl(QtCore.QUrl("foo_url"))
elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
print("leave")
return super().event(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Dialog()
w.setWhatsThis("Whats this")
w.setWindowFlags(
QtCore.Qt.WindowContextHelpButtonHint | QtCore.Qt.WindowCloseButtonHint
)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

Select all items in QListView and deselect all when directory is changed

I created a window like this The link! and when I select a directory I would like to select all items in the right Qlist, and when I change to a different directory, I would deselect the items in the previous directory and select all items in my current directory.
How can I approach this?
To select and deselect all items you must use selectAll() and clearSelection(), respectively. But the selection must be after the view that is updated and for this the layoutChanged signal is used, also the selection mode must be set to QAbstractItemView::MultiSelection.
import sys
from PyQt5 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super(Widget, self).__init__(*args, **kwargs)
hlay = QtWidgets.QHBoxLayout(self)
self.treeview = QtWidgets.QTreeView()
self.listview = QtWidgets.QListView()
hlay.addWidget(self.treeview)
hlay.addWidget(self.listview)
path = QtCore.QDir.rootPath()
self.dirModel = QtWidgets.QFileSystemModel(self)
self.dirModel.setRootPath(QtCore.QDir.rootPath())
self.dirModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.AllDirs)
self.fileModel = QtWidgets.QFileSystemModel(self)
self.fileModel.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Files)
self.treeview.setModel(self.dirModel)
self.listview.setModel(self.fileModel)
self.treeview.setRootIndex(self.dirModel.index(path))
self.listview.setRootIndex(self.fileModel.index(path))
self.listview.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
self.treeview.clicked.connect(self.on_clicked)
self.fileModel.layoutChanged.connect(self.on_layoutChanged)
#QtCore.pyqtSlot(QtCore.QModelIndex)
def on_clicked(self, index):
self.listview.clearSelection()
path = self.dirModel.fileInfo(index).absoluteFilePath()
self.listview.setRootIndex(self.fileModel.setRootPath(path))
#QtCore.pyqtSlot()
def on_layoutChanged(self):
self.listview.selectAll()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
This only requires a few lines, and the only variable you need is the one that references your QListView.
Knowing when the directory is changed is probably the least of your problems and handled in a way you're already happy with.
Let's say your QListView object is called list_view
Upon clicking on/changing your directory, run these lines:
list_view.clearSelection()
list_view.model().layoutChanged.emit()
And that's it. That will deselect all items in your QListView and update the view to show that nothing is highlighted.

How to disable default context menu of QTableView in pyqt?

I am trying to disable a default context menu of QTableView in pyqt.
I have re-implemented the contextMenuEvent but it works on 1st time right click. When I click on the same Item 2nd time the default context menu reappears. (Image attached below for referance.)
I tried "QTableView.setContextMenuPolicy(Qt.NoContextMenu)" but it didn't work. Also referred the answers of similar type questions but still the issue is unresolved.
Any idea?
Ex. showing Re-implemented context menu in QTableView.
def contextMenuEvent(self, event):
menu = QMenu(self)
CutAction = QAction(self.view)
CutAction.setText("&Cut")
menu.addAction(CutAction)
CutAction.setIcon(QIcon(":/{0}.png".format("Cut")))
CutAction.setShortcut("Ctrl+X")
self.connect(CutAction, SIGNAL("triggered()"), self.cut)
with the code that shows I can not reproduce your problem, even so the solution is to use Qt::CustomContextMenu by enabling the signal customContextMenuRequested, and in the corresponding slot you have to implement the logic:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class TableView(QTableView):
def __init__(self, *args, **kwargs):
super(TableView, self).__init__(*args, **kwargs)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onCustomContextMenuRequested)
def onCustomContextMenuRequested(self, pos):
menu = QMenu()
CutAction = menu.addAction("&Cut")
menu.addAction(CutAction)
CutAction.setIcon(QIcon(":/{0}.png".format("Cut")))
CutAction.setShortcut("Ctrl+X")
CutAction.triggered.connect(self.cut)
menu.exec_(self.mapToGlobal(pos))
def cut(self):
pass
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = TableView()
model = QStandardItemModel(10, 10, w)
w.setModel(model)
w.show()
sys.exit(app.exec_())

Notification when QDockWidget's tab is clicked?

I need to execute a block of code when the user clicks on the tab of a tabbified QDockWidget. So far I've been doing this via a hack using the "visibilityChanged" event but this is now causing issues (for example, if I have several tabbified dock widgets and I drag one out so that it is floating, the tabbified one underneath will fire its "visibilityChanged" event which I will mistakenly interpret as the user clicking the tab). How can I receive proper notification when a user clicks on a QDockWidgets' tab? I've experimented with the "focusInEvent" of QDockWidget but it doesn't seem to fire when the tab is clicked.
When you use tabifyDockWidget() method QMainWindow creates a QTabBar, this is not directly accessible but using findChild() you can get it, and then use the tabBarClicked signal
from PyQt4 import QtCore, QtGui
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
first_dock = None
for i in range(10):
dock = QtGui.QDockWidget("title {}".format(i), self)
dock.setWidget(QtGui.QTextEdit()) # testing
self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock)
if first_dock:
self.tabifyDockWidget(first_dock, dock)
else:
first_dock = dock
dock.raise_()
tabbar = self.findChild(QtGui.QTabBar, "")
tabbar.tabBarClicked.connect(self.onTabBarClicked)
def onTabBarClicked(self, index):
tabbar = self.sender()
text = tabbar.tabText(index)
print("index={}, text={}".format(index, text))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

uncheck radiobutton - PyQt4

In this sample of code:
from PyQt4.QtGui import QDialog, QPushButton, QRadioButton, QHBoxLayout, QApplication, QButtonGroup
import sys
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent=None)
button = QPushButton('Button')
self.radiobutton1 = QRadioButton('1')
self.radiobutton2 = QRadioButton('2')
#self.group = QButtonGroup()
#self.group.addButton(self.radiobutton1)
#self.group.addButton(self.radiobutton2)
#self.group.setExclusive(False)
layout = QHBoxLayout()
layout.addWidget(button)
layout.addWidget(self.radiobutton1)
layout.addWidget(self.radiobutton2)
self.setLayout(layout)
button.clicked.connect(self.my_method)
def my_method(self):
self.radiobutton1.setChecked(False)
self.radiobutton2.setChecked(False)
app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
When the button clicked I expect the selected radioButton to be unchecked, but that never happens. If I uncomment the comment lines and run the code, then I can uncheck radioButtons. But another problem occurs. Because the group is not exclusive, I can set both radioButtons checked something that must not happens.
What should I do to be able to unckeck the buttons while only one button at a time can be selected?
This feels like cheating, but it works:
import sys
import PyQt4.QtGui as QtGui
class Form(QtGui.QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
button = QtGui.QPushButton('Button')
button.clicked.connect(self.my_method)
self.radiobutton1 = QtGui.QRadioButton('1')
self.radiobutton2 = QtGui.QRadioButton('2')
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
layout.addWidget(self.radiobutton1)
layout.addWidget(self.radiobutton2)
self.setLayout(layout)
self.group = QtGui.QButtonGroup()
self.group.addButton(self.radiobutton1)
self.group.addButton(self.radiobutton2)
def my_method(self):
self.group.setExclusive(False)
self.radiobutton1.setChecked(False)
self.radiobutton2.setChecked(False)
self.group.setExclusive(True)
app = QtGui.QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
As you've pointed out, when self.group.setExclusive(False) is set, you can untoggle both radio buttons.
And when self.group.setExclusive(True), only one radio button can be set.
So my_method simply calls self.group.setExclusive(False) so it can unset both radio buttons, then resets self.group.setExclusive(True).
PS. I think parent should not be set to None on this line:
super(Form, self).__init__(parent = None)
since if a non-trivial parent is sent to Form, you would probably want to pass that parent on to QDialog.__init__.
To anyone looking for a simple fix to this very annoying problem, connect each button to a slot that controls the CheckState of the other buttons.
Simply add the list of buttons you want to a QButtonGroup, get the list of buttons, check that the sender is not the same button, and uncheck others.
Assuming that you instantiate your buttons in a loop, you can easily implement this:
self.bg = QButtonGroup()
self.bg.setExclusive(False)
for button in list_of_buttons:
self.bg.addButton(button)
button.clicked.connect(self.uncheck_other_buttons)
def uncheck_other_btns(self):
for button in self.bg.buttons(): # returns the list of all added buttons
if self.sender() != button: # in PyQt5, button.objectName() fails if name isn't set,
# instead, simply check that the signal sender() object
# is not the same object as the clicked button
button.setChecked(False) # then set all other buttons to be unchecked

Categories