Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3914.feature.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Selection widget is now supported in the Qt backend.
1 change: 0 additions & 1 deletion docs/en/reference/data/apis_by_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ General widgets:
- web
unsupported:
- textual
- qt

Slider:
description: A widget for selecting a value within a range. The range is shown as a horizontal line, and the selected value is shown as a draggable marker.
Expand Down
Binary file added docs/en/reference/images/selection-qt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions qt/src/toga_qt/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .widgets.button import Button
from .widgets.imageview import ImageView
from .widgets.label import Label
from .widgets.selection import Selection
from .widgets.switch import Switch
from .widgets.textinput import TextInput
from .window import MainWindow, Window
Expand Down Expand Up @@ -45,6 +46,7 @@
"Container",
"Box",
"Label",
"Selection",
"Switch",
"TextInput",
"ImageView",
Expand Down
58 changes: 58 additions & 0 deletions qt/src/toga_qt/widgets/selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from contextlib import contextmanager

from PySide6.QtWidgets import QComboBox
from travertino.size import at_least

from .base import Widget


class Selection(Widget):
def create(self):
self.native = QComboBox()
self.native.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
self.native.currentTextChanged.connect(self.qt_on_current_text_changed)
self._send_notifications = True

@contextmanager
def suspend_notifications(self):
self._send_notifications = False
yield
self._send_notifications = True

def qt_on_current_text_changed(self, text):
if self._send_notifications:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could use currentIndexChanged signal here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the signal used here make any difference? AFAICT, we get a signal even if the text remains the same. See the selection example - the on_change example handler has 2 copies of every entry, and changing from dubnium to dubnium triggers a change.

The only oddity here is that the handler accepts an index parameter, but the actual content (AFAICT) is the text. That wouldn't be the case if currentIndexChanged was used.

Copy link
Contributor

@johnzhou721 johnzhou721 Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind... I missed that the implementation removes and then readds an item. Good catch @freakboy3742

Edit -- nevermind... I was getting myself messed up a bit in the impl code here.

Copy link
Contributor Author

@windelbouwman windelbouwman Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the signal used here make any difference? AFAICT, we get a signal even if the text remains the same. See the selection example - the on_change example handler has 2 copies of every entry, and changing from dubnium to dubnium triggers a change.

The only oddity here is that the handler accepts an index parameter, but the actual content (AFAICT) is the text. That wouldn't be the case if currentIndexChanged was used.

According to Qt documentation, the currentTextChanged signal only fires when the text actually changes (which is what we need in our case, and also the reason I used this signal instead of currentIndexChanged). So switching from the first dubnium to the second dubnium does not fire a currentTextChanged event.

Copy link
Contributor

@johnzhou721 johnzhou721 Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@windelbouwman I think using currentIndexChanged would be better -- both GTK and Cocoa backends emit on_change when we change from first Dubium to the second Dubium. So that would be the desired behavior.

EDIT -- this should also remove the need to suspend notifications in the change(self, item) thing but I'm not very sure.

self.interface.on_change()

def clear(self):
self.native.clear()

def insert(self, index, item):
self.native.insertItem(index, self.interface._title_for_item(item))

def change(self, item):
index = self.interface._items.index(item)
with self.suspend_notifications():
self.native.setItemText(index, self.interface._title_for_item(item))
self.interface.refresh()

def remove(self, index, item):
current_index = self.native.currentIndex()
with self.suspend_notifications():
self.native.removeItem(index)
if index == current_index:
if self.native.count() > 0:
self.native.setCurrentIndex(0)
else:
self.interface.on_change()

def select_item(self, index, item):
self.native.setCurrentIndex(index)

def get_selected_index(self):
index = self.native.currentIndex()
return None if index == -1 else index

def rehint(self):
content_size = self.native.sizeHint()
self.interface.intrinsic.width = at_least(content_size.width())
self.interface.intrinsic.height = content_size.height()
25 changes: 25 additions & 0 deletions qt/tests_backend/widgets/selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from PySide6.QtWidgets import QComboBox

from .base import SimpleProbe


class SelectionProbe(SimpleProbe):
native_class = QComboBox

def assert_resizes_on_content_change(self):
pass

@property
def titles(self):
titles = [self.native.itemText(index) for index in range(self.native.count())]
return titles

@property
def selected_title(self):
if self.native.currentIndex() < 0:
return None
else:
return self.native.currentText()

async def select_item(self):
self.native.setCurrentIndex(1)