How to scroll Qt widgets by keyboard? - python

This is a portion of a Python class that I wrote:
def keyPressEvent(self, event):
if event.text() == 'n':
self.widget.scroll(0, -self.height())
elif event.text() == 'p':
self.widget.scroll(0, self.height())
When the user presses n, the widget widget (which has a scrollbar), will scroll downwards by self.height() pixels (which is the entire height of the window; i.e. next page). Opposite is when p is pressed: returns to previous page.
But the problem is that:
Usually, a proper scroller will stop when it reaches page's top. But pressing p will keep going backwards towards infinity in the back, where there is an indefinitely deep void of nothingness!
Opposite is true for n; jumps into some unseen abyss into the distant future, beyond the communication horizon.
Question. How to make a proper scroller using keyboard bindings? A proper scroller is one where it saturates at page's boundaries.
Appendix
This is more code (still trimmed to be readable). My goal here is to show what kind of widgets I am using to address a question in a comment.
#!/usr/bin/python3
import sys
import random
try:
from PySide6 import QtCore, QtWidgets
except ModuleNotFoundError:
from PySide2 import QtCore, QtWidgets
# ... <omitted for brevity> ...
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# create ui's elements
self.scroll = QtWidgets.QScrollArea()
self.widget = QtWidgets.QWidget()
self.hbox = QtWidgets.QHBoxLayout()
# link ui's elements
self.setCentralWidget(self.scroll)
self.scroll.setWidget(self.widget)
self.widget.setLayout(self.hbox)
# configure ui's elements
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll.setHorizontalScrollBarPolicy(
QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.columns = []
self.add_column()
self.show()
# ... <omitted for brevity> ...
def add_column(self):
# ... <omitted for brevity> ...
# ... <omitted for brevity> ...
def keyPressEvent(self, event):
# ... <omitted for brevity> ...
if event.text() == c['key_next_page']:
self.widget.scroll(0, -self.height())
elif event.text() == c['key_prev_page']:
self.widget.scroll(0, self.height())
# ... <omitted for brevity> ...

The scroll() function rarely needs to be directly used.
QScrollBar already calls it internally, by overriding the virtual function of its inherited class (QAbstractScrollArea), scrollContentsBy(). As the documentation says:
Calling this function in order to scroll programmatically is an error, use the scroll bars instead (e.g. by calling QScrollBar::setValue() directly).
So, the solution is usually quite simple: instead of scrolling the widget, set the value of the scroll bar. Note that this will always work, even when scroll bars are hidden.
Before providing the final code, please note two aspects:
when checking for keys, it's usually better to use the key enumeration instead of the text() value of the key event; there are many reasons for this, but, most importantly, it's related to the fact that "keys" are usually mapped to numeric values, and using the actual internal value is generally more accurate and reliable than comparing strings;
you shall not override the keyPressEvent() of the parent (unless you really know what you're doing and why): if the parent has other widgets that only handle certain keys but not what you're looking for, you may end up in scrolling the area for the wrong reason; suppose you have complex layout that also has a QSpinBox outside of the scroll area: that control doesn't generally handle the P key, but the user might press it by mistake (trying to press 0), and they'll see an unexpected behavior: they typed the wrong key, and another, unrelated widget got scrolled;
The solution is to subclass QScrollArea and override its keyPressEvent() instead, then set the value of the scroll bar using its pageStep() property (which, for QScrollArea, equals to the width or height of the scrolled widget).
from PyQt5 import QtCore, QtWidgets
class KeyboardScrollArea(QtWidgets.QScrollArea):
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_N, QtCore.Qt.Key_P):
sb = self.verticalScrollBar()
if (event.key() == QtCore.Qt.Key_P
and sb.value() > 0):
sb.setValue(sb.value() - sb.pageStep())
return
elif (event.key() == QtCore.Qt.Key_N
and sb.value() < sb.maximum()):
sb.setValue(sb.value() + sb.pageStep())
return
super().keyPressEvent(event)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
scroll = KeyboardScrollArea()
widget = QtWidgets.QWidget()
scroll.setWidget(widget)
scroll.setWidgetResizable(True)
layout = QtWidgets.QVBoxLayout(widget)
for i in range(50):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
scroll.show()
app.exec_()
Note that I explicitly checked that the scroll bar value was greater than 0 (for "page up") and lass then the maximum. That is because when an event is not handled, it should normally be propagated to the parent. Suppose you have a custom scroll area inside another custom scroll area: you may want to be able to keep moving up/down the outer scroll area whenever the inner one cannot scroll any more.
Another possibility is to install an event filter on the scroll area, and listen for KeyPress events, then do the same as above (and, in that case, return True when the scroll actually happens). The concept is basically the same, the difference is that you don't need to subclass the scroll area (but a subclass will still be required, as the event filter must override eventFilter()).

Related

Pyqt5 detect scrolling by clicking below handle

I am using pyqt5 to build an application where I have two plain text edit boxes on the main window.
I'm making it so when one scrolls the other does too, and I did it! using valuechanged signal, but whenever the user clicks below the handle, the signal isn't emitted for some reason even the handle scrolled down and so did the content of the text box.
Can anyone help?
I tried using all the signals that are listed in the pyqt5 documentation but to no avail.
Here's my code
class CustomLineEdit(QPlainTextEdit):
scrolled = pyqtSignal(int) # signal to emit if scrolled
def __init__(self) -> None:
super(CustomLineEdit, self).__init__()
self.verticalScrollBar().valueChanged.connect(self.on_scroll)
def on_scroll(self, value):
self.scrolled.emit(value)
QPlainTextEdit has a very peculiar behavior, which has been implemented in order to provide high performance even with very large documents.
One of the aspects that change between the more "standard" QTextEdit (as also explained in the documentation) is that:
[it] replaces a pixel-exact height calculation with a line-by-line respectively paragraph-by-paragraph scrolling approach
In order to do so, the height of the document is always the line count (not the pixel size of the document), which requires special handling of the vertical scroll bar. Unfortunately, one of the drawback of this implementation is that some value changes in the scroll bar values are not emitted with the standard valueChanged signal, because QPlainTextEdit changes the range or value of the scroll bar using QSignalBlocker, preventing any connection to be notified.
Luckily, QAbstractSlider (from which QScrollBar inherits) has a sliderChange() virtual function that is always called after the following changes happen:
SliderRangeChange (the minimum and maximum range has changed);
SliderOrientationChange (horizontal to vertical and vice versa, not useful to us);
SliderStepsChange (the "step" normally used for the arrow buttons, probably not useful too for this case);
SliderValueChange (the actual value change);
Considering this, we can create a subclass of QSlider and override that to check for real changes, no matter if the signals have been blocked. The trick is to keep track of the previous scroll bar value, which we can normally assume as always being 0 during initialization.
Then we directly connect the custom scroll bar signal with that of the CustomLineEdit and eventually connect that signal to whatever we need.
class MyScrollBar(QScrollBar):
oldValue = 0
realValueChanged = pyqtSignal(int)
def sliderChange(self, change):
super().sliderChange(change)
new = self.value()
if self.oldValue != new:
self.oldValue = new
self.realValueChanged.emit(new)
class CustomLineEdit(QPlainTextEdit):
scrolled = pyqtSignal(int) # signal to emit if scrolled
def __init__(self, *args, **kwargs) -> None:
super(CustomLineEdit, self).__init__(*args, **kwargs)
vbar = MyScrollBar(Qt.Vertical, self)
self.setVerticalScrollBar(vbar)
vbar.realValueChanged.connect(self.scrolled)
Note that this might not be ideal when using the signal for another "sibling" QPlainTextEdit that doesn't share the same line count. For instance, you may be comparing two slightly different documents, or they have different sizes that may affect the vertical scroll bar depending on the word wrap setting.
Another possibility would be to always use the value as a proportion of the maximum: proportional = self.value() / self.maximum(). This obviously requires to change the signal signature to float.
In the following example I'm creating two CustomLineEdit instances and connect their respective signals respectively. Even using slightly different text contents will always keep the scrolling proportional, for both scroll bars.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class MyScrollBar(QScrollBar):
oldValue = 0
realValueChanged = pyqtSignal(float)
def sliderChange(self, change):
super().sliderChange(change)
new = self.value()
if new:
new /= self.maximum()
if self.oldValue != new:
self.oldValue = new
self.realValueChanged.emit(new)
class CustomLineEdit(QPlainTextEdit):
scrolled = pyqtSignal(float)
def __init__(self, *args, **kwargs) -> None:
super(CustomLineEdit, self).__init__(*args, **kwargs)
vbar = MyScrollBar(Qt.Vertical, self)
self.setVerticalScrollBar(vbar)
vbar.realValueChanged.connect(self.scrolled)
def scroll(self, value):
if self.verticalScrollBar().maximum():
# NOTE: it's *very* important to use round() and not int()
self.verticalScrollBar().setValue(
round(value * self.verticalScrollBar().maximum()))
app = QApplication([])
edit1 = CustomLineEdit('\n'.join('line {}'.format(i+1) for i in range(20)))
edit2 = CustomLineEdit('\n'.join('line {}'.format(i+1) for i in range(21)))
# connect the signal of each edit to the other
edit1.scrolled.connect(edit2.scroll)
edit2.scrolled.connect(edit1.scroll)
test = QWidget()
lay = QHBoxLayout(test)
lay.addWidget(edit1)
lay.addWidget(edit2)
test.show()
app.exec()

Pyqt5 image coordinates

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.
for examples my image is 800*753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800*753)
def resimac(self):
filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
self.image=QtGui.QImage(filename[0])
self.pixmap=QtGui.QPixmap.fromImage(self.image)
self.resim1.setPixmap(self.pixmap)
self.resim1.mousePressEvent=self.getPixel
def getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
print("X=",x," y= ",y)
Since you didn't provide a minimal, reproducible example, I'm going to assume that you're probably setting the scaledContents property, but that could also be not true (in case you set a maximum or fixed size for the label).
There are some other serious issues about your answer, I'll address them at the end of this answer.
The point has to be mapped to the pixmap coordinates
When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the scaledContents property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it's maximized to a screen smaller than the space the window needs).
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, not on the pixmap.
First of all, even if it doesn't seem to be that important, you'll have to consider that every widget can have some contents margins: the widget will still receive events that happen inside the area of those margins, even if they are outside its actual contents, so you'll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that's true, you'll have to translate the event position to that rectangle to get its position according to the pixmap.
Then, if the scaledContents property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you'll need to scale the position.
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.
# click on the horizontal center of the widget
mouseX = 100
pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0
pixmapX = mouseX * xRatio
# the resulting "x" is the horizontal center of the pixmap
# pixmapX = 200
On the other hand, if the contents are not scaled you'll have to consider the QLabel alignment property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use and the localization (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you'll have to be aware of that.
In the following example I'm trying to take care about all of that (I'd have to be honest, I'm not 100% sure, as there might be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting mousePressEvent as you did, I'm using an event filter, I'll explain the reason for it afterwards.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout(self)
self.getImageButton = QtWidgets.QPushButton('Select')
layout.addWidget(self.getImageButton)
self.getImageButton.clicked.connect(self.resimac)
self.resim1 = QtWidgets.QLabel()
layout.addWidget(self.resim1)
self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
# I'm assuming the following...
self.resim1.setScaledContents(True)
self.resim1.setFixedSize(701,451)
# install an event filter to "capture" mouse events (amongst others)
self.resim1.installEventFilter(self)
def resimac(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
if not filename:
return
self.resim1.setPixmap(QtGui.QPixmap(filename))
def eventFilter(self, source, event):
# if the source is our QLabel, it has a valid pixmap, and the event is
# a left click, proceed in trying to get the event position
if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and
event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton):
self.getClickedPosition(event.pos())
return super().eventFilter(source, event)
def getClickedPosition(self, pos):
# consider the widget contents margins
contentsRect = QtCore.QRectF(self.resim1.contentsRect())
if pos not in contentsRect:
# outside widget margins, ignore!
return
# adjust the position to the contents margins
pos -= contentsRect.topLeft()
pixmapRect = self.resim1.pixmap().rect()
if self.resim1.hasScaledContents():
x = pos.x() * pixmapRect.width() / contentsRect.width()
y = pos.y() * pixmapRect.height() / contentsRect.height()
pos = QtCore.QPoint(x, y)
else:
align = self.resim1.alignment()
# for historical reasons, QRect (which is based on integer values),
# returns right() as (left+width-1) and bottom as (top+height-1),
# and so their opposite functions set/moveRight and set/moveBottom
# take that into consideration; using a QRectF can prevent that; see:
# https://doc.qt.io/qt-5/qrect.html#right
# https://doc.qt.io/qt-5/qrect.html#bottom
pixmapRect = QtCore.QRectF(pixmapRect)
# the pixmap is not left aligned, align it correctly
if align & QtCore.Qt.AlignRight:
pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
elif align & QtCore.Qt.AlignHCenter:
pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
# the pixmap is not top aligned (note that the default for QLabel is
# Qt.AlignVCenter, the vertical center)
if align & QtCore.Qt.AlignBottom:
pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
elif align & QtCore.Qt.AlignVCenter:
pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)
if not pos in pixmapRect:
# outside image margins, ignore!
return
# translate coordinates to the image position and convert it back to
# a QPoint, which is integer based
pos = (pos - pixmapRect.topLeft()).toPoint()
print('X={}, Y={}'.format(pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Now. A couple of suggestions.
Don't overwrite existing child object methods with [other] object's instance attributes
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses function caching for virtual functions; this means that as soon as a virtual is called the first time, that function will always be called in the future. While your approach could work in simple cases (especially if the overwriting happens within the parent's __init__), it's usually prone to unexpected behavior that's difficult to debug if you're not very careful.
And that's exactly your case: I suppose that resimac is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. But if the user, for some reason, clicks on the label before a new pixmap is loaded, your supposedly overwritten method will never get called: at that time, you've not overwritten it yet, so the user clicks the label, Qt calls the QLabel's base class mousePressEvent implementation, and then that method will always be called from that point on, no matter if you try to overwrite it.
To work around that, you have at least 3 options:
use an event filter (as the example above); an event filter is something that "captures" events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget's parent or not (that's mostly the case of key/mouse events: if a widget isn't "interested" about one of those events, it "tells" its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
subclass the widget and manually add it to your GUI within your code;
subclass it and "promote" the widget if you're using Qt's Designer;
You don't need to use a QImage for a QLabel.
This is not that an issue, it's just a suggestion: QPixmap already uses (sort of) fromImage within its C++ code when constructing it with a path as an argument, so there's no need for that.
Always, always provide usable, Minimal Reproducible Example code.
See:
https://stackoverflow.com/help/how-to-ask
https://stackoverflow.com/help/minimal-reproducible-example
It could take time, even hours to get an "MRE", but it's worth it: there'll always somebody that could answer you, but doesn't want to or couldn't dig into your code for various reasons (mostly because it's incomplete, vague, inusable, lacking context, or even too expanded). If, for any reason, there'll be just that one user, you'll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you'll probably get plenty of interactions and useful insight from it.

PySide Qt4 - Qtabwidget - Disable drag and drop of a single q tab widget

I have a qtabwidget with one or more tabs in it at any given time.
I would like the user to be able to rearrange the second through last tab in any order, but the first tab to remain at index 0.
From everything I've found, there is no way to enable setMovable independently for each tab.
The best workaround I've come up with thus far is to simply move the first tab back to position 0 if the user moves it (or another tab in front of it). This obviously isn't ideal, but it would be acceptable if it worked correctly... It works for a short while, however it occasionally crashed the application (It appears to happen when user drags a tab before it and holds the mouse there, so its in a constant loop of trying to rearrange and something low level crashes)
Any other suggestions on a feasible workaround (either to this method, or a similar widget where this capability would be easier to implement)? I figure its probably possible to re-class the QTabWidget in a way that it would ignore mouse drags on the first tab, but I'm not sure how I could go about preventing another tab from being moved before it...
The only way I've found, yet, to "pin" the first tab of a QTabWidget is by using a subclass of QTabBar. The global strategy consists to install an eventFilter on the subclass ofQTabBar and to conditionally block the MouseMove events in order to:
fix the position of the first tab at index 0;
prevent the left edge of the other tabs to be moved farther to the left of the right edge of the first tab. This prevent the other tabs to be moved in front of the first tab.
The code below present a simple application to show how this can be done.
import sys
from PySide import QtGui, QtCore
class myQTabBar(QtGui.QTabBar):
def __init__(self, *args, **kargs):
super(myQTabBar, self).__init__(*args, **kargs)
self.setMovable(True)
self.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Type.MouseMove:
if source.currentIndex() == 0: # Block MouseMove for first tab.
return True
else: # For remaining tabs:
# block MouseMove if the left edge of the moving tab goes
# farther to the left than the right edge of first tab.
moving_leftEdge = event.pos().x() - self.edge_offset
fixed_rightEdge = self.tabRect(0).width()
if moving_leftEdge < fixed_rightEdge:
return True
elif event.type() == QtCore.QEvent.Type.MouseButtonPress:
# Get mouse click horizontal position.
xclick = event.pos().x()
# Get the left edge horizontal position of the targeted tab.
xleft = self.tabRect(self.tabAt(event.pos())).x()
# Compute and store offset between mouse click horizontal
# position and the left edge of the targeted tab
self.edge_offset = xclick - xleft
return QtGui.QWidget.eventFilter(self, source, event)
class myQTabWidget(QtGui.QTabWidget):
def __init__(self, *args, **kargs):
super(myQTabWidget, self).__init__(*args, **kargs)
tab_bar = myQTabBar()
self.setTabBar(tab_bar)
self.addTab(QtGui.QWidget(), 'Tab1')
self.addTab(QtGui.QWidget(), 'Tab2')
self.addTab(QtGui.QWidget(), 'Tab3')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
instance1 = myQTabWidget()
instance1.show()
sys.exit(app.exec_())
Which results in:

What is the most efficient way to display multiple pixmaps into a scroll area?

I am trying to make an application that displays a PDF file, using PyQt4 and python-poppler-qt4.
So far I have managed to display the entire document by loading pixmaps generated with Poppler, set on a QLabel and appended to a QFrame. The QFrame is displayed in a QScrollArea.
It looks pretty good, until implementing zooming, which is done by regenerating the pixmaps all over again, with an incremented resolution. This process requires the entire document to be rendered into pixmaps, which obviously takes time and results into an unwanted lag.
Logic wants that I should display images of the pages I am seeing only (it sounds like quantum physics). I have two options in mind:
create blank pages with QLabels and load the image onto them when they become visible in the scroll area;
create only one page and add or remove precedent or subsequent pages right before it should be displayed.
I am not sure I am on the right track or whether there is an alternative.
The first option seems more feasible, because the visibility of a blank page determines when the pixmap has to be uploaded (although I have no idea how to delete that pixmap when the page is hidden). Yet I am not sure that zooming will be faster this way, since a document of, say, 600 pages, will have to be regenerated, albeit with blank pages.
The second option should definitely improve zooming since 1 to 4 pages at a time would have to be regenerated when zooming. In that second case however, I am not sure how to trigger the construction of pages.
What would you suggest?
wouldn't it be simple to forget the QLabels and directly draw the image:
from PyQt4.QtGui import *
import sys
app = QApplication(sys.argv)
class Test(QWidget):
def __init__(self):
super(Test, self).__init__()
self.painter = QPainter()
# placeholder for the real stuff to draw
self.image = QImage("/tmp/test.jpg")
def paintEvent(self, evt):
rect = evt.rect()
evt.accept()
print rect
self.painter.begin(self)
zoomedImage = self.image # ... calculate this for your images
sourceRect = rect # ... caluclate this ...
# draw it directly
self.painter.drawImage(rect, self.image, sourceRect)
self.painter.end()
t = Test()
t.setGeometry(0,0,600,800)
s = QScrollArea()
s.setWidget(t)
s.setGeometry(0,0,300,400)
s.show()
app.exec_()
I've worked out an answer, using option 1 in the question:
def moveEvent(self, event):
self.checkVisibility()
event.ignore()
def resizeEvent(self, event):
self.checkVisibility()
event.ignore()
def checkVisibility(self):
print "Checking visibility"
for page in self.getPages():
if not page.visibleRegion().isEmpty():
if page.was_visible:
pass
else:
print page.page_number, "became visible"
page.was_visible = True
self.applyImageToPage(page)
else:
if page.was_visible:
print page.page_number, "became invisible"
page.was_visible = False
else:
pass
def applyImageToPage(self, page):
print "applying image to page", page.page_number
source = self.getSourcePage(self.getPageNumber(page))
scale = self.display.scale
# this is where the error occurs
image = source.renderToImage(72 * scale, 72 * scale)
pixmap = QtGui.QPixmap.fromImage(image)
page.setPixmap(pixmap)

Using PyQT, how do you filter mousePressEvent for a QComboBox with custom list

I've got a QComboBox with a custom list object.
The custom list object has a custom mousePressEvent so that when the user click on one of the circles with a +/- (a twisty), the list is expanded/collapsed.
When I use the list with the combo box, when the user clicks on a twisty, the list is expanded/collapsed, but the selection is changed, and the list is hidden. How can I filter this so that when the user click on a twisty, the selection is not changed, and the list not hidden.
Additional screenshots
All of the nodes collapsed:
List hidden:
QT has a eventFilter that "captures" QEvent.MouseButtonRelease. So what I have done is installed my own eventFilter that filters QEvent.MouseButtonRelease events if the user click on a node.
In my list object I have the following method:
def mousePressEvent (self, e):
self.colapse_expand_click = False
if <user clicked node>:
colapse_expand_node()
e.accept ()
self.colapse_expand_click = True
The mousePressEvent runs before mouseReleaseEvent.
Then in the custom combobox, I filter the event:
class RevisionSelectorWidget(QtGui.QComboBox):
def __init__(self, parent = None):
QtGui.QComboBox.__init__(self, parent)
self.log_list = RevisionSelectorLogList(self)
self.setView(self.log_list)
self.log_list.installEventFilter(self)
self.log_list.viewport().installEventFilter(self)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.MouseButtonRelease:
if self.log_list.colapse_expand_click:
return True
return False
Off the top of my head, you could subclass QComboBox and override hideEvent(QHideEvent) (inherited from QWidget)
def hideEvent(self, event):
if self.OkToHide():
event.accept()
else:
event.ignore()
Your screenshot looks like an interesting use of a combo box, I'm curious as to why you haven't used a TreeView style control instead of a list?
Edit (Mar 14 2009):
I looked at the Qt source code and it looks like when the keyboard and mouse events are captured, that as soon as qt has decided to emit the "activated(int index)" signal, "hidePopup()" has been called.
So apart from rewriting their event filter code, another option is to connect the "activated(int index)" or "highlighted(int index)" signal to a slot that can call "showPopup()" which would re-raise the list items. If you get a nasty disappear/appear paint issue you may have to get Qt to delay the paint events while the popup is visible.
Hope that helps!

Categories