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

add WCollapsibleGroupBox, use for controller mapping info boxes and settings #14324

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ronso0
Copy link
Member

@ronso0 ronso0 commented Feb 12, 2025

This adds a QGroupBox-derived WCollapsibleGroupBox that can be collapsed/expanded by clicking its title.
Screenshot_2025-02-12_18-16-08

IMHO this fixes the regression with the new tab'ed controller page where the device and mapping info would push the setting and I/O tabs down.
It also allows to focus on a certain settings group when adjusting the mapping.

  • Down/Right arrow like on treeview branches
  • state of Device / Mapping info boxes is persistent per session (per controller)
  • state of settings boxes is restored / persists as long as the mapping is selected
    (they are expanded every time the controller page is shown because the controller settings are rebuild each time)

WCollapsibleGroupBox abuses QGroupBox::setChecked() mechanism. Originally this is a checkbox in the title bar and toggling it enables/disables all children.
Adopting this saves us from reimplementing the checked state tracking, signal etc.
This is not touched but also irrelevant since the content is now hidden anyway when it's collapsed.
Collapsing is achieved by simply setting the maximum height to height of the title row.
Inspired by ctkCollapsibleGroupBox, just without the complexity of hiding/showing child widgets (which requires some small, low-risk hacks to get the appearance right under all circumstances).

I imagine this groupbox can be helpful in other preferences pages, too.
(as quick fix for long pages, compared to restructuring into tab or otherwise splitting pages)

@ronso0 ronso0 force-pushed the controller-sett-group-hide branch from 160c07b to eb616c7 Compare February 13, 2025 16:43
@ronso0 ronso0 force-pushed the controller-sett-group-hide branch from eb616c7 to bbe7749 Compare February 13, 2025 19:04
@ronso0 ronso0 marked this pull request as ready for review February 13, 2025 20:08
Copy link
Contributor

@cr7pt0gr4ph7 cr7pt0gr4ph7 left a comment

Choose a reason for hiding this comment

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

LGTM 👍 Just a few small comment-related nitpicks. No major objections.

src/widget/wcollapsiblegroupbox.h Outdated Show resolved Hide resolved
src/controllers/dlgprefcontroller.cpp Outdated Show resolved Hide resolved
src/controllers/dlgprefcontroller.cpp Outdated Show resolved Hide resolved
Comment on lines 1089 to 1100
const QString title = pBox->title();
pBox->setCheckable(true);
if (m_settingsCollapsedStates.contains(title)) {
pBox->setChecked(m_settingsCollapsedStates.value(title));
}

connect(pBox,
&WCollapsibleGroupBox::toggled,
this,
[this, title](bool checked) {
m_settingsCollapsedStates.insert(title, checked);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

This will slightly misbehave when multiple groups have the same title. I think that's OK, but maybe we should add a comment similar to this:

// Note: If multiple groups happen to have the same name (which should
// not normally happen in well-behaved controller mappings, but is not
// strictly prohibited), the last one to be expanded/collapsed determines
// the state that will be restored.

Copy link
Member

Choose a reason for hiding this comment

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

how about we just store whether the groupbox is collapsed in the groupbox itself?

Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, the MIDI learning wizard can cause the mapping to be reloaded while the preferences dialog is open, and this is an attempt to restore the expansion state in that case (because reloading also means that the mapping-defined GUI widgets are destroyed and recreated).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that, too. The primary reason we need (want) to restore the state in the first place is that the settings widgets are rebuild every time the Preferences are shown. Same when pressing Cancel.

We may re-show the mapping only if it has been changed (I/O mapping, setting(s)). Eg. we could compare each setting's value with its saved value (when a setting is toggled on and of again) and somehow link that to mapping->setDirty().
But that seems a lot of work compared to the QHash implementation.

src/controllers/dlgprefcontroller.cpp Outdated Show resolved Hide resolved
src/widget/wcollapsiblegroupbox.cpp Outdated Show resolved Hide resolved
@ronso0
Copy link
Member Author

ronso0 commented Feb 14, 2025

Thanks for your review!

I pushed the fixup and will squash before merge.

Copy link
Member

@Swiftb0y Swiftb0y left a comment

Choose a reason for hiding this comment

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

couple thoughts

Comment on lines 1089 to 1100
const QString title = pBox->title();
pBox->setCheckable(true);
if (m_settingsCollapsedStates.contains(title)) {
pBox->setChecked(m_settingsCollapsedStates.value(title));
}

connect(pBox,
&WCollapsibleGroupBox::toggled,
this,
[this, title](bool checked) {
m_settingsCollapsedStates.insert(title, checked);
});
Copy link
Member

Choose a reason for hiding this comment

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

how about we just store whether the groupbox is collapsed in the groupbox itself?

Comment on lines -56 to +57
auto pContainer = make_parented<QGroupBox>(m_label, pParent);
auto pContainer = make_parented<WCollapsibleGroupBox>(m_label, pParent);
Copy link
Member

Choose a reason for hiding this comment

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

wdyt of making this configurable in the settings XML? Shouldn't be hard IMO.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean with an extra xml node?
If yes, I 'd rather not put that work on mapping developers. I mean collapsing top-level groups is desirable, and on lower levels it only makes the GUI more complex.

But maybe we can check if the groupbox' parent is a WCollapsibleGroupBox, too, and auto-set checkable if it's not.
Will check..

Copy link
Member Author

Choose a reason for hiding this comment

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

Naa, we already query the groups in DlgPrefController so that's the place to set the checkable IMO.

Copy link
Member

Choose a reason for hiding this comment

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

Do you mean with an extra xml node?

Either that or add an attribute to the node

If yes, I 'd rather not put that work on mapping developers.

Right. On the flipside (and why I was suggesting to make it configurable), what if devs really want some settings to be easily visible and hide others (more advances ones for instance)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah okay, so you're proposing to allow setting the default collapsed state in xml (not the collapsible feature in general).

Copy link
Member

Choose a reason for hiding this comment

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

yeah, that works too.

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of the collapsed default state! I was looking at adding some kin d of depends or condition to help with the large amount of settings in #13653, but this looks like anm elegant solution to address this with a minimum effort.

@ronso0 ronso0 force-pushed the controller-sett-group-hide branch from c91d5d5 to 6d6e02a Compare February 14, 2025 21:49
Copy link
Member

@acolombier acolombier left a comment

Choose a reason for hiding this comment

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

Thank you for adding this, this is looking very good! Just some minor comments and feedbacks

Comment on lines -56 to +57
auto pContainer = make_parented<QGroupBox>(m_label, pParent);
auto pContainer = make_parented<WCollapsibleGroupBox>(m_label, pParent);
Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of the collapsed default state! I was looking at adding some kin d of depends or condition to help with the large amount of settings in #13653, but this looks like anm elegant solution to address this with a minimum effort.

WCollapsibleGroupBox::WCollapsibleGroupBox(QWidget* pParent)
: QGroupBox(pParent),
m_minH(-1),
m_maxH(10000) {
Copy link
Member

Choose a reason for hiding this comment

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

Could we use a constant here? Or maybe some std::numeric_limits?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried std::numeric_limits<int>::quiet_NaN() earlier but it didn't work as expected, and I was lazy / it was late ; )
Will check again..

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, that's only available for floating-point types.
so std::numeric_limits<T>::min seems to be the only applicable member?

}

bool WCollapsibleGroupBox::event(QEvent* pEvent) {
if (isCheckable()) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you please refactor some of these nested condition to flatten it and use early return instead? It is a bit hard to follow at the moment

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I'll try.

Copy link
Member Author

Choose a reason for hiding this comment

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

Though early return isn't really applicable.

Copy link
Contributor

@cr7pt0gr4ph7 cr7pt0gr4ph7 Feb 15, 2025

Choose a reason for hiding this comment

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

Suggested change
if (isCheckable()) {
if (!isCheckable()) {
// Only enable the special expand/collapse behaviors
// when the users has requested them via setCheckable(true).
// Otherwise, this should just behave like a normal QGroupBox.
QGroupBox::event(pEvent);
return;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's the only one.

class CollapsibleGroupBoxStyle : public QProxyStyle {
Q_OBJECT
public:
explicit CollapsibleGroupBoxStyle(QStyle* pStyle = 0)
Copy link
Member

Choose a reason for hiding this comment

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

Should this be nullptr instead of 0?

Copy link
Member Author

Choose a reason for hiding this comment

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

Umm, right.
Seems I mixed up the original Qt implementation with that of ctkCollapsibleGroupBox

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
explicit CollapsibleGroupBoxStyle(QStyle* pStyle = 0)
explicit CollapsibleGroupBoxStyle(QStyle* pStyle = nullptr)

Comment on lines +36 to +37
int m_minH;
int m_maxH;
Copy link
Member

Choose a reason for hiding this comment

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

Does H stands for "height" here? How do you feel about making it explicit?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, no prob: m_minHeight, m_maxHeight then

@cr7pt0gr4ph7
Copy link
Contributor

I already have an additional use case for WCollapsibleGroupBox as soon as its merged: Making the File Info section of the track info dialog collapsible, as the latter has the tendency to exceed the screen height on wide but "height-challenged"1 laptop screens when combined with even slightly larger font sizes.

Also, could you make sure/test that the WCollapsibleGroupBox is well-behaved when using keyboard navigation?

  • The header should be reachable using the keyboard, both from outside the groupbox, and from the inside (when expanded), and should have suitable visual feedback when focused via the keyboard.
  • Clicking on the header should move the focus to it.
  • Spacebar should expand/collapse the groupbox when the header is focused.
  • Controls inside the box should be reachable via Tab when expanded, and skipped over when collapsed.
  • (I'm not 100% sure what the correct behavior should be when the groupbox is programmatically collapsed while keyboard focus is in it. Should the focus stay on the previously focused but now invisible control, or should focus move to the groupbox header? Disabling the controls inside collapsed groupbox seems to actually be a good thing because it prevents making accidental edits to those controls.)

Footnotes

  1. A question for the native English speakers: What's the correct adjective for the opposite of tall when you specifically mean a small height instead of a small overall size? It's wide vs. narrow, and tall vs. ...? I know there is short, but a short screen does not really sound right to me.

Comment on lines +79 to +81
} else if (pEvent->type() == QEvent::LayoutRequest ||
pEvent->type() == QEvent::ContentsRectChange ||
pEvent->type() == QEvent::Resize) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It might make sense to replace this if/else blocks with a switch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants