A short introduction to clarify the context:
I have an editable CustomCombobox
(with a CustomLineEdit
). This also contains a QMenu
, which opens when the Enter
key is pressed.
My problem:
I want the content/text of the CustomLineEdit
to be deleted when it gets the Focus
. But only if this focus comes from outside. Opening and closing the Dropdown
and QMenu
should be excluded from this.
Enclosed is a more detailed code snippet. Reason:
Some events already take place in the said CustomCombobox
. Even if I am quite sure that a smaller example could suffice:
Better safe than sorry.
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
SECONDARY_FONTSIZE_MODIFIER = -1
SECONDARY_FONT = QtGui.QColor("gray")
def getfontheight_adjustfontsize(font, offset=0):
if isinstance(font, QtGui.QFont) and isinstance(offset, int):
if offset != 0:
newsize = font.pointSize() + offset if font.pointSize() > -offset else font.pointSize()
font.setPointSize(newsize)
fontheight = QtGui.QFontMetrics(font).height()
return fontheight
class ComboBoxItemDelegate(QtWidgets.QStyledItemDelegate):
#This delegate will make the Combobox Items look nice
def paint(self, painter, option, index):
primary = index.data(QtCore.Qt.DisplayRole) or ""
secondary = index.data(QtCore.Qt.UserRole) or ""
# Highlight Background if you mouseover
if option.state & QtWidgets.QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
else:
painter.fillRect(option.rect, option.palette.base())
primaryFont = option.font
secondaryFont = QtGui.QFont(option.font)
primaryHeight = getfontheight_adjustfontsize(primaryFont)
secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)
totalTextHeight = primaryHeight + secondaryHeight
rect = option.rect.adjusted(5, 0, -5, 0)
y_offset = rect.top() + (rect.height() - totalTextHeight) // 2
primaryRect = QtCore.QRect(rect.left(), y_offset, rect.width(), primaryHeight)
secondaryRect = QtCore.QRect(rect.left(), y_offset + primaryHeight, rect.width(), secondaryHeight)
#Draw Primary
painter.save()
painter.setFont(primaryFont)
painter.setPen(option.palette.text().color())
painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, primary)
#Draw Secondary
painter.setFont(secondaryFont)
painter.setPen(SECONDARY_FONT)
painter.drawText(secondaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, secondary)
painter.restore()
def sizeHint(self, option, index):
# Determine the display size of the combo box items via their font size
primaryFont = option.font
secondaryFont = QtGui.QFont(option.font)
primaryHeight = getfontheight_adjustfontsize(primaryFont)
secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)
totalHeight = primaryHeight + secondaryHeight + 2 # additional spacing
size = super().sizeHint(option, index)
size.setHeight(totalHeight)
return size
class CustomLineEdit(QtWidgets.QLineEdit):
# This CustomClass is SPECIFICALLY made for CustomCombobox
def __init__(self, parent=None):
super().__init__(parent)
def focusInEvent(self, event):
super().focusInEvent(event)
# Deletes the content if the focus does NOT come from a/any popup (e.g. dropdown or QMenu)
if event.reason() != QtCore.Qt.PopupFocusReason:
self.clear()
def paintEvent(self, event):
# Only change the “passive widget” display/painting
# ~(aka only if the widget and the associated popups have NO focus)~
if self.hasFocus():
super().paintEvent(event)
return
top = QtWidgets.QApplication.activePopupWidget()
if top is not None and top.parent() == self.parent():
super().paintEvent(event)
return
painter = QtGui.QPainter(self)
#Basic Background
opt = QtWidgets.QStyleOptionFrame()
self.initStyleOption(opt)
#getting parent for "what to draw" (primary/secondary) and font
combo = self.parent()
if isinstance(combo, QtWidgets.QComboBox):
current_index = combo.currentIndex()
model_index = combo.model().index(current_index, 0)
display_text = model_index.data(QtCore.Qt.DisplayRole)
user_text = model_index.data(QtCore.Qt.UserRole)
primaryFont = combo.font()
secondaryFont = combo.font()
else:
print('We should NEVER be here!!! Parent NOT QComboBox! Use only at your own risk!')
display_text = self.text()
user_text = ""
primaryFont = self.font()
secondaryFont = self.font()
# Getting "to be painted"-Area and divide them into areas
text_rect = self.style().subElementRect(QtWidgets.QStyle.SE_LineEditContents, opt, self)
text_rect = text_rect.adjusted(2, 0, 0, 0) #custom padding to align with dropdown items
primaryHeight = getfontheight_adjustfontsize(primaryFont)
secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)
totalTextHeight = primaryHeight + secondaryHeight
y_offset = text_rect.top() + (text_rect.height() - totalTextHeight) // 2
primaryRect = QtCore.QRect(text_rect.left(), y_offset, text_rect.width(), primaryHeight)
secondaryRect = QtCore.QRect(text_rect.left(), y_offset + primaryHeight, text_rect.width(), secondaryHeight)
# Draw primary (top)
painter.setFont(primaryFont)
painter.setPen(self.palette().color(QtGui.QPalette.Text))
painter.drawText(primaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(display_text))
# Draw secondary (bottom)
painter.setFont(secondaryFont)
painter.setPen(SECONDARY_FONT)
painter.drawText(secondaryRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(user_text))
class CustomComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setEditable(True)
self.setLineEdit(CustomLineEdit(self))
self.setCompleter(None)
# sizeHint is needed or else the font/strings will look compressed
def sizeHint(self):
primaryFont = self.font()
secondaryFont = self.font()
primaryHeight = getfontheight_adjustfontsize(primaryFont)
secondaryHeight = getfontheight_adjustfontsize(secondaryFont, SECONDARY_FONTSIZE_MODIFIER)
totalHeight = primaryHeight + secondaryHeight + 4 # extra spacing
size = super().sizeHint()
size.setHeight(totalHeight)
return size
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
items = [{'additional_info':'111111', 'Item':'1111'},
{'additional_info':'WORLD', 'Item':'G.I. JOE'},
{'additional_info':'NOVABRAIN', 'Item':'FLATEARTH'},
{'additional_info':'SUPERSTAR', 'Item':'BOB THE BUILDER'}]
for item in items:
self.initial_filling(item['Item'], item['additional_info'])
def initial_filling(self, primary, secondary):
self.searchComboBox.addItem(primary)
index = self.searchComboBox.model().index(self.searchComboBox.count()-1, 0)
self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)
def init_ui(self):
self.setWindowTitle("My best Widget")
self.resize(324, 500)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
layout = QtWidgets.QVBoxLayout(central_widget)
# Creating the Combobox
self.searchComboBox = CustomComboBox()
self.searchComboBox.setItemDelegate(ComboBoxItemDelegate(self.searchComboBox))
self.searchComboBox.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
layout.addWidget(self.searchComboBox)
#Usually triggers API. Just think of it as "make menu" function
self.searchComboBox.lineEdit().returnPressed.connect(self.on_search_return)
#put currentindex_item on top of the dropdownlist
self.searchComboBox.currentIndexChanged.connect(self.change_dropdownitem_order)
# Part 1 of 2:
# Clear focus if an item was selected via GUI
self.searchComboBox.activated.connect(lambda index: self.searchComboBox.clearFocus())
# !(not relevant)! Dummy Table !(not relevant)!
self.table = QtWidgets.QTableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["Some", "Nice", "Table"])
self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
self.table.verticalHeader().setVisible(False)
self.table.setShowGrid(False)
layout.addWidget(self.table)
def on_search_return(self):
#PREPARE INPUT FOR API CALL AND GETTING RESULT
symbol = self.searchComboBox.currentText().strip().upper()
if not symbol:
return
#.... RESULTS FROM API
results = [{'additional_info':'HELLO', 'Item':symbol},
{'additional_info':'WORLD', 'Item':symbol},
{'additional_info':'GALAXY', 'Item':symbol},
{'additional_info':'STAR', 'Item':symbol}]
self.show_lookup_menu(results)
def show_lookup_menu(self, results):
#Creating QMenu based on results
menu = QtWidgets.QMenu(self.searchComboBox)
menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
for result in results:
text = f"{result['additional_info']} - {result['Item']}"
action = QtWidgets.QAction(text, menu)
action.setData(result)
menu.addAction(action)
menu.triggered.connect(self.on_menu_action_triggered)
pos = self.searchComboBox.mapToGlobal(QtCore.QPoint(0, self.searchComboBox.height()))
menu.popup(pos)
def on_menu_action_triggered(self, action):
result = action.data()
if result:
print("You selected:", result)
primary = result["Item"]
secondary = result["additional_info"]
# The following code iterates through the combobox and checks if the chosen/selected combination
# already exists
found_index = -1
for i in range(self.searchComboBox.count()):
if self.searchComboBox.itemText(i) == primary:
index = self.searchComboBox.model().index(i, 0)
if self.searchComboBox.model().data(index, role=QtCore.Qt.UserRole) == secondary:
found_index = i
break
if found_index != -1:
#if it does, select it.
self.searchComboBox.setCurrentIndex(found_index)
else:
#else add it then select it.
self.searchComboBox.addItem(primary)
new_index = self.searchComboBox.count() - 1
index = self.searchComboBox.model().index(new_index, 0)
self.searchComboBox.model().setData(index, secondary, role=QtCore.Qt.UserRole)
self.searchComboBox.setCurrentIndex(new_index)
# Part 2 of 2:
# Clear focus if an item was "selected" via Code (setCurrentIndex)
self.searchComboBox.clearFocus()
def change_dropdownitem_order(self, index):
# We assume that the just selected item is important
# and therefore place it at the top of the combobox dropdown
# (very volatile itemlist, will change from day to day)
if self.searchComboBox.count() > 1:
model = self.searchComboBox.model()
current_index = model.index(index, 0)
display = model.data(current_index, QtCore.Qt.DisplayRole)
user = model.data(current_index, QtCore.Qt.UserRole)
with QtCore.QSignalBlocker(self.searchComboBox):
self.searchComboBox.removeItem(index)
self.searchComboBox.insertItem(0, display, user)
self.searchComboBox.setCurrentIndex(0)
def main():
qt_app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(qt_app.exec_())
if __name__ == "__main__":
main()
My current "best" solution is using the focusInEvent
of the CustomLineEdit
with QtCore.Qt.PopupFocusReason
.
However, focusInEvent
ignores the focus change from all popups
to the CustomWidget
. Not just its own.