Building on to an awesome answer from here by eyllanesc, I have added QListWidget to the collapsible box.
There is a text box below where user will provide search string and my aim is to highlight that item in the QListWidget (if present).
The current code works fine ie it will highlight the text and scroll it to top if the QToolButton is already expanded but it will just open and highlight the item but wont scroll it to top if it is not already expanded. (So the user now doesn't know if he has found the item or not as he cant see it highlighted.) Strange thing is if I press enter again then it will scroll it to top.
I tried various things like making the QlistWidget active, in focus etc but didnt help.
Please tell me what I am missing so that I dont need to press Enter twice in case the QToolButton is not expanded already.
EDIT: Removing the animation part from code as suggested.
import time
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QListWidget, QLabel, QApplication, QVBoxLayout, \
QWidget, QSizePolicy, QToolButton, QScrollArea, QFrame, QDockWidget, QLineEdit, QHBoxLayout, QAbstractItemView
collapsed_list = ['What', 'should', 'be', 'done', 'to', 'fix', 'this', 'issue?', 'I', 'am', 'confused']
class CollapsibleDemo(QWidget):
def __init__(self, title="", parent=None):
super(CollapsibleDemo, self).__init__(parent)
self.toggle_button = QToolButton(
text=title, checkable=True, checked=False
)
self.toggle_button.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
)
self.toggle_button.setToolButtonStyle(
QtCore.Qt.ToolButtonTextBesideIcon
)
self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.content_area = QScrollArea(
maximumHeight=0, minimumHeight=0
)
self.content_area.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(10, 10, 1, 1)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
@QtCore.pyqtSlot()
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(
QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
)
if not checked:
self.content_area.setMaximumHeight(self.content_height + self.collapsed_height)
else:
self.content_area.setMaximumHeight(0)
def setContentLayout(self, layout):
self.content_area.setLayout(layout)
self.collapsed_height = (
self.sizeHint().height() - self.content_area.maximumHeight()
)
self.content_height = layout.sizeHint().height()
def set_text(self, title):
self.toggle_button.setText(title)
class Try(QMainWindow):
def __init__(self, ):
super().__init__()
self.width = 800
self.height = 800
self.init_ui()
def init_ui(self):
self.resize(self.width, self.height)
self.create_background()
self.add_collapsed_list_box()
self.add_find_text_box()
self.vlay.addStretch()
self.find_text_box.setFocus()
def create_background(self):
dock = QDockWidget()
self.setCentralWidget(dock)
dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
scroll = QScrollArea()
dock.setWidget(scroll)
content = QWidget()
scroll.setWidget(content)
scroll.setWidgetResizable(True)
self.vlay = QVBoxLayout(content)
self.vlay.setSpacing(10)
def add_collapsed_list_box(self):
self.box = CollapsibleDemo(f"Whats inside here!")
self.vlay.addWidget(self.box)
lay_diff = QVBoxLayout()
self.qlist = QListWidget()
self.qlist.addItems(collapsed_list)
lay_diff.addWidget(self.qlist)
self.box.setContentLayout(lay_diff)
def add_find_text_box(self):
self.find_text_box = QLineEdit("am")
find_label = QLabel(" Search text: ")
enter_label = QLabel(" (Press Enter)")
hlayout = QHBoxLayout()
hlayout.addWidget(find_label)
hlayout.addWidget(self.find_text_box)
hlayout.addWidget(enter_label)
hlayout.addStretch(3)
self.vlay.addLayout(hlayout)
self.find_text_box.returnPressed.connect(self.find_selected)
def find_selected(self):
user_text = self.find_text_box.text()
if user_text in collapsed_list:
if not self.box.toggle_button.isChecked():
self.box.on_pressed()
self.box.toggle_button.setChecked(True)
item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
item.setSelected(True)
self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop)
if __name__ == "__main__":
app = QApplication([])
window = Try()
window.show()
app.exec()
There are two problems:
animation.start()
is instantly executed; this means that at that point the value of the animation is still the start value;scrollToItem
will not work, because the scroll area has not layed out its contents yet;In order to solve the problem, the solution is to delay the scrollToItem
call using a singleShot
QTimer.
If an animation is being used, it's mandatory that you wait for the animation to end, because the scroll bars are being updated along with the box size.
A possible solution is to create a local function that delays the scrollToItem
and, if the box is collapsed, call it only whenever the animation is completed by connecting to the animation finished
signal, and then disconnect it (this is very important!).
def find_selected(self):
user_text = self.find_text_box.text()
if user_text in collapsed_list:
collapsed = not self.box.toggle_button.isChecked()
if collapsed:
self.box.on_pressed()
self.box.toggle_button.setChecked(True)
item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
item.setSelected(True)
def scrollTo():
QtCore.QTimer.singleShot(1,
lambda: self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop))
if collapsed:
# disconnect the function!!!
self.box.toggle_animation.finished.disconnect(scrollTo)
if collapsed:
self.box.toggle_animation.finished.connect(scrollTo)
else:
scrollTo()
If you're not interested in the animation, then it's even easier:
class CollapsibleDemo(QWidget):
def __init__(self, title="", parent=None):
# ...
# remove all the animations in here
@QtCore.pyqtSlot()
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(
QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
)
if checked:
self.setFixedHeight(self.collapsed_height)
self.content_area.setFixedHeight(0)
else:
self.setFixedHeight(self.expanded_height)
self.content_area.setFixedHeight(self.content_height)
def setContentLayout(self, layout):
self.content_area.setLayout(layout)
self.collapsed_height = self.sizeHint().height() - self.content_area.maximumHeight()
self.content_height = layout.sizeHint().height()
self.expanded_height = self.collapsed_height + self.content_height
def set_text(self, title):
self.toggle_button.setText(title)
class Try(QMainWindow):
def find_selected(self):
user_text = self.find_text_box.text()
if user_text in collapsed_list:
if not self.box.toggle_button.isChecked():
self.box.on_pressed()
self.box.toggle_button.setChecked(True)
item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
item.setSelected(True)
QtCore.QTimer.singleShot(1, lambda:
self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop))
As explained in the comments, animations make sense when their duration is at least 50-100ms; using a duration too short makes the animation useless (computers are not able to show the effect and our eyes would not be able to see it anyway) and only complicates things (you have to wait for the animation to end).