ComboBox: key event when popup is open

Fixes #4826

The previous patch changed that the ComboBox was no longer focused when
the popup is shown, so make a hack around that
This commit is contained in:
Olivier Goffart 2024-12-06 13:35:03 +01:00
parent e03e812984
commit 69d201d6a0
7 changed files with 235 additions and 76 deletions

View file

@ -4,14 +4,18 @@
export component ComboBoxBase {
in property <[string]> model;
in property <bool> enabled <=> i-focus-scope.enabled;
out property <bool> has-focus: i-focus-scope.has-focus && root.enabled;
out property <bool> has-focus: (i-focus-scope.has-focus || popup-has-focus) && root.enabled;
out property <bool> pressed <=> i-touch-area.pressed;
out property <bool> has-hover: i-touch-area.has-hover;
in-out property <int> current-index: 0;
in-out property <string> current-value: root.model[root.current-index];
// Set from the ComboBox when the popup has the focus
in-out property <bool> popup-has-focus;
callback selected(current-value: string);
callback show-popup();
callback close-popup();
public function select(index: int) {
if !root.enabled {
@ -34,6 +38,21 @@ export component ComboBoxBase {
root.select(Math.min(root.current-index + 1, root.model.length - 1));
}
public function popup-key-handler(event: KeyEvent) -> EventResult {
if (event.text == Key.UpArrow) {
root.move-selection-up();
return accept;
} else if (event.text == Key.DownArrow) {
root.move-selection-down();
return accept;
} else if (event.text == Key.Return || event.text == Key.Escape) {
root.close-popup();
return accept;
}
return reject;
}
function reset-current() {
root.current-index = 0;
}
@ -59,6 +78,12 @@ export component ComboBoxBase {
forward-focus: i-focus-scope;
i-focus-scope := FocusScope {
changed has-focus => {
if self.has-focus {
// this means the popup was closed and we get back the focus
root.popup-has-focus = false;
}
}
key-pressed(event) => {
if (!self.enabled) {
return reject;

View file

@ -40,6 +40,9 @@ export component ComboBox {
show-popup => {
popup.show();
}
close-popup => {
popup.close();
}
}
background := Rectangle {
@ -90,26 +93,39 @@ export component ComboBox {
width: root.width;
height: root.visible-items * CosmicSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
touch-area := TouchArea {
clicked => {
base.select(index);
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}
}
}
}
}
}
}

View file

@ -46,6 +46,9 @@ export component ComboBox {
show-popup => {
popup.show();
}
close-popup => {
popup.close();
}
}
FocusBorder {
@ -96,7 +99,7 @@ export component ComboBox {
VerticalLayout {
alignment: center;
Rectangle {
horizontal-stretch: 0;
min-width: 16px;
@ -160,24 +163,37 @@ export component ComboBox {
width: root.width;
height: root.visible-items * CupertinoSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
for value[index] in root.model : ListItem {
padding-horizontal: 0;
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
pressed-x: touch-area.pressed-x;
pressed-y: touch-area.pressed-y;
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
touch-area := TouchArea {
clicked => {
base.select(index);
for value[index] in root.model : ListItem {
padding-horizontal: 0;
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
pressed-x: touch-area.pressed-x;
pressed-y: touch-area.pressed-y;
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}

View file

@ -52,6 +52,9 @@ export component ComboBox {
show-popup => {
popup.show();
}
close-popup => {
popup.close();
}
}
background := Rectangle {
@ -100,21 +103,33 @@ export component ComboBox {
width: root.width;
height: root.visible-items * FluentSizeSettings.item-height + 2 * root.popup-padding;
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
MenuBorder {
ScrollView {
VerticalLayout {
alignment: start;
padding: root.popup-padding;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
for value[index] in root.model : ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
touch-area := TouchArea {
clicked => {
base.select(index);
touch-area := TouchArea {
clicked => {
base.select(index);
}
}
}
}

View file

@ -48,6 +48,9 @@ export component ComboBox {
show-popup => {
popup.show();
}
close-popup => {
popup.close();
}
}
background := Rectangle {
@ -96,19 +99,32 @@ export component ComboBox {
border-radius: 4px;
}
ScrollView {
VerticalLayout {
alignment: start;
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
for value[index] in root.model: ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
ScrollView {
VerticalLayout {
alignment: start;
touch-area := StateLayer {
clicked => {
base.select(index);
for value[index] in root.model: ListItem {
item: { text: value };
is-selected: index == root.current-index;
has-hover: touch-area.has-hover;
pressed: touch-area.pressed;
touch-area := StateLayer {
clicked => {
base.select(index);
}
}
}
}

View file

@ -36,6 +36,10 @@ export component ComboBox {
big-popup.show();
}
}
close-popup => {
small-popup.close();
big-popup.close();
}
}
big-popup := PopupWindow {
@ -50,19 +54,32 @@ export component ComboBox {
height: 100%;
}
ScrollView {
VerticalLayout {
alignment: start;
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta2.has-hover;
combobox: true;
ScrollView {
VerticalLayout {
alignment: start;
ta2 := TouchArea {
clicked => {
base.select(index);
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta2.has-hover;
combobox: true;
ta2 := TouchArea {
clicked => {
base.select(index);
}
}
}
}
@ -79,17 +96,30 @@ export component ComboBox {
height: 100%;
}
VerticalLayout {
FocusScope {
init => {
self.focus();
base.popup-has-focus = true;
}
changed has-focus => {
base.popup-has-focus = self.has-focus;
}
key-pressed(event) => {
return base.popup-key-handler(event);
}
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta.has-hover;
combobox: true;
VerticalLayout {
ta := TouchArea {
clicked => {
base.select(index);
for value[index] in root.model: NativeStandardListViewItem {
item: { text: value };
is-selected: root.current-index == index;
has-hover: ta.has-hover;
combobox: true;
ta := TouchArea {
clicked => {
base.select(index);
}
}
}
}

View file

@ -90,6 +90,47 @@ assert_eq!(instance.get_current_index(), 1);
assert_eq!(instance.get_output(), "selected(Bbb,1)\nselected(Ccc,2)\nselected(Bbb,1)\n");
instance.set_output(Default::default());
// show the popup
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Return));
assert_eq!(instance.get_output(), "");
// click outside causes the popup to close
slint_testing::send_mouse_click(&instance, 100., 10.);
assert_eq!(instance.get_output(), "");
assert_eq!(instance.get_has_focus(), true);
slint_testing::send_mouse_click(&instance, 100., 10.);
assert_eq!(instance.get_output(), "clicked-under\n");
assert_eq!(instance.get_has_focus(), true);
instance.set_output(Default::default());
instance.set_current_index(0);
// show the popup
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Return));
// The arrow change the values while the popup is shown
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow));
assert_eq!(instance.get_current_value(), "Bbb");
assert_eq!(instance.get_current_index(), 1);
assert_eq!(instance.get_output(), "selected(Bbb,1)\n");
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::DownArrow));
assert_eq!(instance.get_current_value(), "Ccc");
assert_eq!(instance.get_current_index(), 2);
assert_eq!(instance.get_output(), "selected(Bbb,1)\nselected(Ccc,2)\n");
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::UpArrow));
assert_eq!(instance.get_current_value(), "Bbb");
assert_eq!(instance.get_current_index(), 1);
assert_eq!(instance.get_output(), "selected(Bbb,1)\nselected(Ccc,2)\nselected(Bbb,1)\n");
// close the popup
slint_testing::send_keyboard_string_sequence(&instance, &SharedString::from(Key::Escape));
assert_eq!(instance.get_current_value(), "Bbb");
assert_eq!(instance.get_current_index(), 1);
assert_eq!(instance.get_output(), "selected(Bbb,1)\nselected(Ccc,2)\nselected(Bbb,1)\n");
instance.set_output(Default::default());
slint_testing::send_mouse_click(&instance, 100., 10.);
assert_eq!(instance.get_output(), "clicked-under\n");
assert_eq!(instance.get_has_focus(), true);
// Set current-index to -1
instance.set_current_index(-1);
mock_elapsed_time(500);