Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Separator to ComboBox #638

Merged
merged 10 commits into from
Mar 28, 2024
Merged

Conversation

qin-yu
Copy link
Contributor

@qin-yu qin-yu commented Mar 14, 2024

Hey @tlambert03,

I looked into magicgui and added a very simple mechanism for ComboBox to add separators by introducing a Separator class. Please have a look and tell me if you like it or not. Do I need to do the documentation somewhere or someone is in charge of this?

Many thanks,
Qin

Example

By giving a list ["a", Separator(), "b"] to magicgui for creating a dropdown list, the ComboBox backend go through the list, check if the data is a Separator and insert a separator there using .insertSeparator().

I put a modified version of the example script from your repo:

from magicgui import magicgui
from magicgui.types import Separator

sep = [Separator(i) for i in range(4)]
a = [1, 2, sep[0], 4, sep[1], 6, 7, sep[2], 9, sep[3], 11, 12, sep[0], 14, sep[0]]
b = [1, 2, sep[0], 4, sep[1], 6, 7, sep[2], 9, sep[3], 6, 7, sep[0], 9, sep[0]]


@magicgui(call_button="Test Separator",
          a={"widget_type": "ComboBox", "choices": a},
          b={"widget_type": "ComboBox", "choices": b},
          result_widget=True)
def test_separator(a=a[0], b=b[0]):
    return


test_separator.show(run=True)

a with unique elements:

image

b with duplicates:

image

@qin-yu qin-yu mentioned this pull request Mar 14, 2024
Copy link

codecov bot commented Mar 14, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 89.16%. Comparing base (781a54a) to head (86a5967).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #638      +/-   ##
==========================================
+ Coverage   88.90%   89.16%   +0.26%     
==========================================
  Files          39       39              
  Lines        4667     4689      +22     
==========================================
+ Hits         4149     4181      +32     
+ Misses        518      508      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

qin-yu added 2 commits March 14, 2024 17:30
This comment is likely to be unnecessary:

```pytb
src/magicgui/schema/_guiclass.py:260: error: Unused "type: ignore" comment  [unused-ignore]
                signals = {k: events[k] for k in events}  # type: ignore
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Found 1 error in 1 file (checked 1 source file)
```
@qin-yu qin-yu force-pushed the qy/add-separator branch from 4e31f07 to 31395b1 Compare March 14, 2024 16:31
@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 17, 2024

Hey @tlambert03, I've completed the necessary changes, and I believe this Pull Request is now ready for your review. Tbh, this is my first time using Codecov, so I would greatly appreciate your feedback on its integration and any adjustments you might recommend. Thank you for your support!

Copy link
Member

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

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

this looks excellent! nice and simple too. Thanks @qin-yu!

I think the only thing that gave me a little pause was the thickness parameter. Since it's not really "thickness" per-se, but rather "number of". Someone could also do Sep(), Sep() if they really wanted multiples, and Qt itself doesn't really have thickness.

Did you add it cause it's something you know you want though?

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 17, 2024

Since it's not really "thickness" per-se, but rather "number of". Someone could also do Sep(), Sep() if they really wanted multiples, and Qt itself doesn't really have thickness.

Did you add it cause it's something you know you want though?

Oops, I guess I went a bit too custom with my solution. I was working on this app using Napari, and I noticed that a single separator just wasn’t cutting it in terms of visibility. It took me a while to figure out that changing thickness/padding wasn’t straightforward. So, I ended up with this workaround in my own project.

You’re right about using Sep(), Sep() for multiple separators. If you don't like fake thickness implementation, let's remove it and mention this workaround in docstrings.

Thanks for the heads-up! @tlambert03

@tlambert03
Copy link
Member

yeah, i'm not totally opposed to it, but maybe would prefer to consider alternatives before adding it to the public API :) let's just go with a parameter-free sentinel for now.

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 17, 2024

Absolutely, I’m with you on maintaining a clean public API. I’ll go ahead and remove the parameter, then submit a new commit a bit later.

Maybe our conversation here could serve as a good reference for future users. Do you think it would be beneficial to include a note in the docstring?

@tlambert03
Copy link
Member

Do you think it would be beneficial to include a note in the docstring?

sure, if you'd like, you could indicate that it can be used multiple times to create multiple lines 👍

@tlambert03
Copy link
Member

I was working on this app using Napari, and I noticed that a single separator just wasn’t cutting it in terms of visibility. It took me a while to figure out that changing thickness/padding wasn’t straightforward.

by the way, I see this as a problem with napari's style sheets, and something you could take up with napari developers. And not something we should make special considerations for here in magicgui.

The default styling of separators actually looks quite nice, and in-line with the OS standards:

Screenshot 2024-03-17 at 3 23 04 PM

napari applies custom styling to everything, which makes it hard to see:

Screenshot 2024-03-17 at 3 22 33 PM

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 17, 2024

by the way, I see this as a problem with napari's style sheets, and something you could take up with napari developers. And not something we should make special considerations for here in magicgui.

Indeed, my experience with magicgui has been primarily within Napari, so my thoughts were anchored in that context 😢

I've updated the code again. Given that the Separator sentinel would remain identical, I've made it a singleton too. I've also adjusted the tests accordingly and updated the docs. I removed a comma to keep the docstring <= 88 limit. Maybe it's the best thing we can do, but it looks okay.

image

tests/test_widgets.py Outdated Show resolved Hide resolved
@tlambert03
Copy link
Member

one more question: can you perhaps look into how this could be implemented on the jupyter widgets backend? by modifying magicgui.backends._ipynb.widgets.ComboBox._mgui_set_choices

src/magicgui/backends/_qtpy/widgets.py Outdated Show resolved Hide resolved
tests/test_widgets.py Outdated Show resolved Hide resolved
@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

Thanks for the thorough reviews. I've just updated the _qtpy backend based on your feedback. Planning to dive into magicgui.backends._ipynb.widgets.ComboBox._mgui_set_choices first thing tomorrow. Really appreciate your guidance here!

@qin-yu qin-yu force-pushed the qy/add-separator branch from a770359 to 2fe9693 Compare March 18, 2024 02:37
@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

one more question: can you perhaps look into how this could be implemented on the jupyter widgets backend? by modifying magicgui.backends._ipynb.widgets.ComboBox._mgui_set_choices

Regarding ipynb support nuances:

  • I noticed ipynb allows for duplicate choice names with distinct data. Should we align this with the Qt backend's behavior to ensure consistency across platforms?
  • Given Jupyter Widgets lacks native support for separators, I propose two approaches:
    1. Exclude separators entirely in _mgui_set_choice() to adhere to our principle of not incorporating features unsupported by the backend.
    2. Omit separators only within _mgui_set_choices() to parallel the Qt backend behavior. This way, a Separator in Qt equates to a conceptual Separator in ipynb, even though it's not visually represented.

What do you think? @tlambert03

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

I just realised that _mgui_set_choice() from ipynb is not used. Didn't fully understand this when you said "by modifying magicgui.backends._ipynb.widgets.ComboBox._mgui_set_choices"

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

I've adjusted magicgui.backends._ipynb.widgets.ComboBox's _mgui_set_choices() method to skip over Separators, given that Jupyter Widgets doesn’t natively support such a concept. I also fixed #639 in the same place, I hope the changes align with your design.

image

Regarding _mgui_set_choice() (without the ‘s’), it appears this method has no effects, so I've temporarily set it to pass. However, should there be a hidden use case or future need, here's a draft implementation I propose:

def _mgui_set_choice(self, choice_name: str, data: Any) -> None:
    """Set the data associated with a choice."""
    if data is Separator:
        self._ipywidget.options = (*self._ipywidget.options, ("----", Separator))
    else:
        options = self._ipywidget.options
        for item in options:
            print(item, choice_name, data)
            if (isinstance(item, tuple) and item[0] == choice_name) or item == choice_name:
                options = [
                    item for item in options
                    if (not isinstance(item, tuple) or item[0] != choice_name) and item != choice_name
                ]
        self._ipywidget.options = (*options, (choice_name, data))

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

(There is one failed test but I don't think it relates to this PR)

Revert the behaviour change of ipynb duplicate items
@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

image

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 18, 2024

Hey @tlambert03 I encountered an error in the CI workflow Test (3.10, ubuntu-latest, pyside6) / ubuntu-latest py3.10 pyside6, and I'm not sure how to resolve it. Could you take a look and provide some guidance?

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 19, 2024

All checks passed. It looks like we're in a good position to wrap up the PR. I'm assuming there's nothing further I need to do on my end if you're planning to handle the squash merge?

@tlambert03
Copy link
Member

thank you @qin-yu! busy couple days here. yes I will merge soon after having one more look. thanks again!

tests/test_widgets.py Outdated Show resolved Hide resolved
@@ -908,7 +908,7 @@ def _mgui_bind_change_callback(self, callback):
self._event_filter.valueChanged.connect(callback)

def _mgui_get_count(self) -> int:
"""Return the number of items in the dropdown."""
"""Return the number of items in the dropdown, including separator items."""
Copy link
Member

Choose a reason for hiding this comment

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

this actually becomes a bit problematic. The protocol specifies that _mgui_get_count should return the number of choices:

@abstractmethod
def _mgui_get_count(self) -> int:
"""Return number of choices."""
raise NotImplementedError

so, it's not really up to the backend here to change those semantics. The ComboBox itself (via CategoricalWidget) declares __len__ to be the number of choices (not the number of widgets)

def __len__(self) -> int:
"""Return the number of choices."""
return self._widget._mgui_get_count()

so, assert len(combo_a) == len(combo_a.choices) should technically be true, but is not currently so in the tests. So, I'm afraid this needs to account for that as well.

I know these are annoying things, but this feature is a revealing itself to be a bit tricky to implement in a way that isn't potentially surprising and too magical. It is blurring the lines between model (i.e. the values in choices) and the view (how they look in the combobox) a bit.

i'm honestly not sure what the best thing to do here is. one alternative is to go back to what you started with, and just allow for choices to include the separator. that is, when you call widget.choices you do see all the Separator singletons in there (they would need to be subbed back in for None). Or, we just fix the count issue as well and stick with it as it is. That would then mean that it's basically impossible to query where the separators are, and whether they're in there (but you could also set them). Which do you prefer?

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 21, 2024

i'm honestly not sure what the best thing to do here is. one alternative is to go back to what you started with, and just allow for choices to include the separator. that is, when you call widget.choices you do see all the Separator singletons in there (they would need to be subbed back in for None). Or, we just fix the count issue as well and stick with it as it is. That would then mean that it's basically impossible to query where the separators are, and whether they're in there (but you could also set them). Which do you prefer?

Indeed, the decision should be guided by the principle of least astonishment: opting for the approach that aligns with most developers' expectations and simplifies common tasks in ComboBox usage.

Although integrating separators as selectable items offers transparency and consistency—a notion shaped by my experiences with Qt, where separators are considered part of the item set—it's essential to acknowledge that separators are typically not viewed as actionable components. Therefore, omitting separators from counts and choices presents a more user-friendly approach, simplifying development by removing the need to distinguish between separators and actual items.

If you know that many of our users choose magicgui to bypass complexities in Qt, opting to conceal separators within the ComboBox would enhance simplicity.

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 21, 2024

It's pretty straightforward, actually! I just made the necessary adjustments to align with the protocol.

@@ -248,7 +250,7 @@ def _mgui_get_current_choice(self) -> str:

def _mgui_set_choice(self, choice_name: str, data: Any) -> None:
"""Set the data associated with a choice."""
self._ipywidget.options = (*self._ipywidget.options, (choice_name, data))
pass
Copy link
Member

Choose a reason for hiding this comment

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

this change looks suspicious. why are we removing the ability to set a single choice? Won't this change mess up calling CategoricalWidget.set_choice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right. I may have gotten hung up on the protocol's definition of choice_name update. While my commit seemed to function without breaking tests, this change was completely unnecessary. I'm changing it later.

Copy link
Member

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

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

thanks again @qin-yu . I think i'm happy to go with this now. Please just address the remaining comment on the new change to _IPySupportsChoices._mgui_set_choice which i think needs to be reverted

@qin-yu
Copy link
Contributor Author

qin-yu commented Mar 28, 2024

Hi @tlambert03, just pushed the final change for this PR! Apologies for the delay – I was recently on holiday. It's been a valuable learning experience collaborating with you on this PR. Working on a well-maintained Python package with comprehensive CIs like this is a great way to improve contribution practices. Thanks for your patience and guidance!

Copy link
Member

@tlambert03 tlambert03 left a comment

Choose a reason for hiding this comment

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

it was very pleasant working with you too! Thanks a lot for the PR. hope you'll do it again :)

@tlambert03 tlambert03 merged commit fb2367e into pyapp-kit:main Mar 28, 2024
39 checks passed
@tlambert03 tlambert03 changed the title Add Separator to ComboBox feat: Add Separator to ComboBox Mar 29, 2024
@tlambert03 tlambert03 added the enhancement New feature or request label Mar 29, 2024
@qin-yu qin-yu deleted the qy/add-separator branch March 29, 2024 22:56
@qin-yu
Copy link
Contributor Author

qin-yu commented May 26, 2024

Hey Talley, please don't forget to make available v0.8.3 on conda-forge!

@tlambert03
Copy link
Member

looks like the release failed: https://github.com/pyapp-kit/magicgui/actions/runs/8476765032 ... sorry about that, will do again

@qin-yu
Copy link
Contributor Author

qin-yu commented May 26, 2024

Thanks! But I think it failed again (actually no test can pass in the last few weeks)

@tlambert03
Copy link
Member

Yep I saw thanks, I'll fix when I get the chance. Won't be this weekend

@qin-yu
Copy link
Contributor Author

qin-yu commented May 26, 2024

No worries, thanks for the information. Enjoy the rest of your weekend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants