Fix part selection highlighting and picker sizing

Highlight the full selected item and preserve thumbnail colors when part names are hidden. Keep cell dimensions stable while thumbnails load and update every entry sharing a completed thumbnail.

Use four columns for the default parts grid and fixed popup layouts for each icon size. Resize open popups when icons, names, or list mode change, account for capped widths and menu margins, and keep complete rows visible with room for at least two when needed. Enable part names by default for fresh preferences.

Validated with a Qt build, 240 sizing cases, and actual popup checks for constrained widths, name toggling, and menu frames across three Qt styles.

Closes #670
This commit is contained in:
Leonardo Zide committed 2026-10-09 10:09:03 -07:00
1 parent 50aa42f195
commit ce26574116
5 files changed
+220 -24

No files matched your search

+103 -2
View File
@@ -10,10 +10,12 @@ lcPartSelectionPopup::lcPartSelectionPopup(PieceInfo* InitialPart, QWidget* Pare
mPartSelectionWidget = new lcPartSelectionWidget(this);
Layout->addWidget(mPartSelectionWidget);
mPartSelectionWidget->SetOrientation(Qt::Horizontal);
mPartSelectionWidget->SetDragEnabled(false);
connect(mPartSelectionWidget, &lcPartSelectionWidget::PartPicked, this, &lcPartSelectionPopup::Accept);
connect(mPartSelectionWidget, &lcPartSelectionWidget::LayoutChanged, this, &lcPartSelectionPopup::ResizeToContents, Qt::QueuedConnection);
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
Layout->addWidget(ButtonBox);
@@ -22,6 +24,34 @@ lcPartSelectionPopup::lcPartSelectionPopup(PieceInfo* InitialPart, QWidget* Pare
QObject::connect(ButtonBox, &QDialogButtonBox::rejected, this, &lcPartSelectionPopup::Reject);
}
QSize lcPartSelectionPopup::GetPartsViewSizeHint(const lcPartSelectionListView* PartsView, const QSize& PopupSize) const
{
const int WidthOverhead = PopupSize.width() - PartsView->sizeHint().width();
const int ViewWidth = qMax(1, qMin(PopupSize.width(), maximumWidth()) - WidthOverhead);
return PartsView->GetPreferredSize(ViewWidth);
}
QSize lcPartSelectionPopup::sizeHint() const
{
QSize Size = QWidget::sizeHint();
const lcPartSelectionListView* PartsView = mPartSelectionWidget->findChild<lcPartSelectionListView*>();
if (!PartsView || PartsView->GetListModel()->GetIconSize() == lcPartSelectionListView::NoIconSize || PartsView->GetListModel()->IsListMode())
return Size;
const int RowHeight = PartsView->GetCellSize().height();
if (RowHeight <= 0)
return Size;
const int Overhead = Size.height() - PartsView->sizeHint().height() + 2 * PartsView->frameWidth();
const int ViewHeight = GetPartsViewSizeHint(PartsView, Size).height();
const int PreferredHeight = Overhead + ViewHeight - 2 * PartsView->frameWidth();
const int AvailableHeight = qMin(PreferredHeight, maximumHeight()) - Overhead;
const int Rows = qMax(1, AvailableHeight / RowHeight);
Size.setHeight(Overhead + Rows * RowHeight);
return Size;
}
void lcPartSelectionPopup::showEvent(QShowEvent* ShowEvent)
{
QWidget::showEvent(ShowEvent);
@@ -32,6 +62,42 @@ void lcPartSelectionPopup::showEvent(QShowEvent* ShowEvent)
mPartSelectionWidget->FocusPartFilterWidget();
}
void lcPartSelectionPopup::SetHeightLimit(int Height)
{
mHeightLimit = Height;
UpdateHeightLimit();
}
void lcPartSelectionPopup::UpdateHeightLimit()
{
int Height = mHeightLimit;
const lcPartSelectionListView* PartsView = mPartSelectionWidget->findChild<lcPartSelectionListView*>();
if (PartsView && PartsView->GetListModel()->GetIconSize() && !PartsView->GetListModel()->IsListMode())
{
const int RowHeight = PartsView->GetCellSize().height();
if (RowHeight > 0)
{
const QSize PopupSize = QWidget::sizeHint();
const int ViewHeight = GetPartsViewSizeHint(PartsView, PopupSize).height();
const int Overhead = PopupSize.height() - PartsView->sizeHint().height() + 2 * PartsView->frameWidth();
const int Rows = qMin(2, (ViewHeight - 2 * PartsView->frameWidth()) / RowHeight);
Height = qMax(Height, Overhead + Rows * RowHeight);
}
}
setMaximumHeight(Height);
QMenu* Menu = qobject_cast<QMenu*>(parent());
if (Menu)
{
QStyleOption Option;
Option.initFrom(Menu);
QStyle* Style = Menu->style();
const int Margin = Style->pixelMetric(QStyle::PM_MenuPanelWidth, &Option, Menu) + Style->pixelMetric(QStyle::PM_MenuVMargin, &Option, Menu);
const QSize MenuSize = Style->sizeFromContents(QStyle::CT_Menu, &Option, QSize(0, Height + 2 * Margin), Menu);
Menu->setMaximumHeight(qMin(QWIDGETSIZE_MAX, MenuSize.height()));
}
}
void lcPartSelectionPopup::Accept()
{
mPickedPiece = mPartSelectionWidget->GetCurrentPart();
@@ -45,6 +111,40 @@ void lcPartSelectionPopup::Reject()
Close();
}
void lcPartSelectionPopup::ResizeToContents()
{
QMenu* Menu = qobject_cast<QMenu*>(parent());
if (!Menu || !Menu->isVisible())
return;
updateGeometry();
UpdateHeightLimit();
// Refresh QMenu's cached widget action geometry as well as its own size.
for (QAction* Action : Menu->actions())
{
QWidgetAction* WidgetAction = qobject_cast<QWidgetAction*>(Action);
if (WidgetAction && WidgetAction->defaultWidget() == this)
{
QActionEvent Event(QEvent::ActionChanged, Action);
QCoreApplication::sendEvent(Menu, &Event);
break;
}
}
for (QScreen* Screen : QGuiApplication::screens())
{
const QRect AvailableGeometry = Screen->availableGeometry();
if (!Screen->geometry().contains(Menu->pos()))
continue;
const int X = qBound(AvailableGeometry.left(), Menu->x(), qMax(AvailableGeometry.left(), AvailableGeometry.right() - Menu->width() + 1));
const int Y = qBound(AvailableGeometry.top(), Menu->y(), qMax(AvailableGeometry.top(), AvailableGeometry.bottom() - Menu->height() + 1));
Menu->move(X, Y);
break;
}
}
void lcPartSelectionPopup::Close()
{
QMenu* Menu = qobject_cast<QMenu*>(parent());
@@ -84,8 +184,9 @@ std::optional<PieceInfo*> lcShowPartSelectionPopup(PieceInfo* InitialPart, const
{
const QSize ScreenSize = PopupScreen->availableGeometry().size();
const QSize MaximumSize(ScreenSize.width() * 3 / 5, ScreenSize.height() * 3 / 5);
Popup->setMaximumSize(MaximumSize);
Menu->setMaximumSize(MaximumSize);
Popup->setMaximumWidth(MaximumSize.width());
Menu->setMaximumWidth(MaximumSize.width());
Popup->SetHeightLimit(MaximumSize.height());
}
Action->setDefaultWidget(Popup);
+7
View File
@@ -2,6 +2,7 @@
class PieceInfo;
class lcPartSelectionWidget;
class lcPartSelectionListView;
class lcPartSelectionPopup : public QWidget
{
@@ -10,6 +11,8 @@ class lcPartSelectionPopup : public QWidget
public:
lcPartSelectionPopup(PieceInfo* InitialPart, QWidget* Parent);
virtual ~lcPartSelectionPopup() = default;
QSize sizeHint() const override;
void SetHeightLimit(int Height);
std::optional<PieceInfo*> GetPickedPart() const
{
@@ -24,15 +27,19 @@ public:
protected slots:
void Accept();
void Reject();
void ResizeToContents();
protected:
void showEvent(QShowEvent* ShowEvent) override;
void Close();
void UpdateHeightLimit();
QSize GetPartsViewSizeHint(const lcPartSelectionListView* PartsView, const QSize& PopupSize) const;
lcPartSelectionWidget* mPartSelectionWidget = nullptr;
PieceInfo* mInitialPart = nullptr;
PieceInfo* mPickedPiece = nullptr;
bool mAccepted = false;
int mHeightLimit = QWIDGETSIZE_MAX;
};
std::optional<PieceInfo*> lcShowPartSelectionPopup(PieceInfo* InitialPart, const std::vector<std::pair<PieceInfo*, std::string>>& CustomParts, int ColorIndex, QWidget* Parent, QPoint Position);
+102 -21
View File
@@ -16,6 +16,20 @@
Q_DECLARE_METATYPE(QList<int>)
void lcPartSelectionItemDelegate::initStyleOption(QStyleOptionViewItem* Option, const QModelIndex& Index) const
{
QStyledItemDelegate::initStyleOption(Option, Index);
Option->showDecorationSelected = true;
if (mListModel->GetIconSize())
Option->decorationSize = QSize(mListModel->GetIconSize(), mListModel->GetIconSize());
// Keep the rendered part colors when the style requests a selected icon.
const QPixmap Pixmap = qvariant_cast<QPixmap>(Index.data(Qt::DecorationRole));
if (!Pixmap.isNull())
Option->icon.addPixmap(Pixmap, QIcon::Selected);
}
void lcPartSelectionItemDelegate::paint(QPainter* Painter, const QStyleOptionViewItem& Option, const QModelIndex& Index) const
{
mListModel->RequestThumbnail(Index.row());
@@ -27,12 +41,16 @@ QSize lcPartSelectionItemDelegate::sizeHint(const QStyleOptionViewItem& Option,
QSize Size = QStyledItemDelegate::sizeHint(Option, Index);
int IconSize = mListModel->GetIconSize();
if (IconSize)
if (IconSize && !mListModel->IsListMode())
{
QWidget* Widget = (QWidget*)parent();
const int PixmapMargin = Widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &Option, Widget) + 1;
const QWidget* Widget = Option.widget;
if (!Widget)
Widget = qobject_cast<QWidget*>(parent());
QStyle* Style = Widget ? Widget->style() : QApplication::style();
const int PixmapMargin = Style->pixelMetric(QStyle::PM_FocusFrameHMargin, &Option, Widget) + 1;
int PixmapWidth = IconSize + 2 * PixmapMargin;
Size.setWidth(qMin(PixmapWidth, Size.width()));
Size.setWidth(PixmapWidth);
Size.setHeight(qMax(PixmapWidth, Size.height()));
}
return Size;
@@ -443,8 +461,6 @@ void lcPartSelectionListModel::ThumbnailReady(lcPartThumbnailId ThumbnailId, QPi
mParts[PartIndex].Pixmap = Pixmap;
emit dataChanged(index(PartIndex, 0), index(PartIndex, 0), { Qt::DecorationRole });
break;
}
}
}
@@ -724,6 +740,7 @@ void lcPartSelectionListView::TogglePartNames()
bool Show = !mListModel->GetShowPartNames();
mListModel->SetShowPartNames(Show);
lcSetProfileInt(LC_PROFILE_PARTS_LIST_NAMES, Show);
UpdateItemGeometry();
}
void lcPartSelectionListView::ToggleDecoratedParts()
@@ -776,6 +793,7 @@ void lcPartSelectionListView::SetPartFilterType(lcPartFilterType Option)
void lcPartSelectionListView::ToggleListMode()
{
mListModel->ToggleListMode();
UpdateItemGeometry();
}
void lcPartSelectionListView::ToggleFixedColor()
@@ -810,13 +828,33 @@ void lcPartSelectionListView::SetIconSize(int Size)
mListModel->SetIconSize(Size, DeviceScale);
UpdateViewMode();
int Width = Size + 2 * frameWidth() + 6;
const int PixmapMargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, this) + 1;
int Width = Size + 2 * frameWidth() + 2 * PixmapMargin;
if (verticalScrollBar())
Width += verticalScrollBar()->sizeHint().width();
int Height = Size + 2 * frameWidth() + 2;
int Height = qMax(Size, fontMetrics().height()) + 2 * frameWidth() + 2 * PixmapMargin;
if (horizontalScrollBar())
Height += horizontalScrollBar()->sizeHint().height();
setMinimumSize(mPartSelectionWidget->GetIsPopup() ? QSize(0, 0) : QSize(Width, Height));
UpdateItemGeometry();
}
void lcPartSelectionListView::UpdateItemGeometry()
{
updateGeometry();
if (mPartSelectionWidget->GetIsPopup())
{
// Invalidate the layout item caches through the picker splitter.
for (QWidget* Widget = parentWidget(); Widget; Widget = Widget->parentWidget())
{
Widget->updateGeometry();
if (Widget == mPartSelectionWidget)
break;
}
}
emit LayoutChanged();
}
void lcPartSelectionListView::startDrag(Qt::DropActions SupportedActions)
@@ -841,17 +879,8 @@ void lcPartSelectionListView::startDrag(Qt::DropActions SupportedActions)
Drag->exec(Qt::CopyAction);
}
QSize lcPartSelectionListView::sizeHint() const
QSize lcPartSelectionListView::GetCellSize() const
{
if (model()->rowCount() == 0)
return QListView::sizeHint();
if (mListModel->GetIconSize() == NoIconSize)
return QSize(500, 350);
int Columns = 5;
int Rows = qMin(4, (model()->rowCount() + Columns - 1) / Columns);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QStyleOptionViewItem Option;
initViewItemOption(&Option);
@@ -859,11 +888,62 @@ QSize lcPartSelectionListView::sizeHint() const
QStyleOptionViewItem Option = viewOptions();
#endif
QSize CellSize = itemDelegate()->sizeHint(Option, model()->index(0,0));
QSize Size(CellSize.width() * Columns + frameWidth() * 2, CellSize.height() * Rows + frameWidth() * 2);
return itemDelegate()->sizeHint(Option, model()->index(0, 0));
}
QSize lcPartSelectionListView::sizeHint() const
{
return GetPreferredSize(QWIDGETSIZE_MAX);
}
QSize lcPartSelectionListView::GetPreferredSize(int MaximumWidth) const
{
if (model()->rowCount() == 0 && (mListModel->GetIconSize() == NoIconSize || mListModel->IsListMode()))
return QListView::sizeHint();
if (mListModel->GetIconSize() == NoIconSize)
return QSize(500, 350);
int Columns = mListModel->IsListMode() ? 1 : 4;
int MaximumRows = 4;
QSize CellSize = GetCellSize();
if (mPartSelectionWidget->GetIsPopup() && !mListModel->IsListMode())
{
switch (mListModel->GetIconSize())
{
case SmallIconSize:
Columns = 9;
MaximumRows = 5;
break;
case MediumIconSize:
Columns = 5;
MaximumRows = 4;
break;
case LargeIconSize:
Columns = 4;
MaximumRows = 3;
break;
case ExtraLargeIconSize:
Columns = 3;
MaximumRows = 4;
break;
}
}
int WidthOverhead = frameWidth() * 2;
if (verticalScrollBar())
Size += QSize(verticalScrollBar()->width() + 1, 0);
WidthOverhead += verticalScrollBar()->sizeHint().width() + 1;
const int Width = qMin(CellSize.width() * Columns + WidthOverhead, MaximumWidth);
if (mPartSelectionWidget->GetIsPopup() && !mListModel->IsListMode() && CellSize.width() > 0)
Columns = qMin(Columns, qMax(1, (Width - WidthOverhead) / CellSize.width()));
int Rows = model()->rowCount() ? qMin(MaximumRows, (model()->rowCount() + Columns - 1) / Columns) : MaximumRows;
QSize Size(Width, CellSize.height() * Rows + frameWidth() * 2);
return Size;
}
@@ -940,6 +1020,7 @@ lcPartSelectionWidget::lcPartSelectionWidget(QWidget* Parent)
setLayout(Layout);
connect(mPartsWidget, &lcPartSelectionListView::PartPicked, this, &lcPartSelectionWidget::PartViewPartPicked);
connect(mPartsWidget, &lcPartSelectionListView::LayoutChanged, this, &lcPartSelectionWidget::LayoutChanged);
connect(mPartsWidget->selectionModel(), &QItemSelectionModel::currentChanged, this, &lcPartSelectionWidget::PartViewSelectionChanged);
connect(mFilterWidget, &QLineEdit::textChanged, this, &lcPartSelectionWidget::FilterChanged);
connect(mCategoriesWidget, &QTreeWidget::currentItemChanged, this, &lcPartSelectionWidget::CategoryChanged);
+7
View File
@@ -50,6 +50,8 @@ public:
QSize sizeHint(const QStyleOptionViewItem& Option, const QModelIndex& Index) const override;
protected:
void initStyleOption(QStyleOptionViewItem* Option, const QModelIndex& Index) const override;
lcPartSelectionListModel* mListModel;
};
@@ -200,6 +202,8 @@ public:
void startDrag(Qt::DropActions SupportedActions) override;
QSize sizeHint() const override;
QSize GetCellSize() const;
QSize GetPreferredSize(int MaximumWidth) const;
void SetCategory(lcPartCategoryType Type, int Index);
void SetCustomParts(const std::vector<std::pair<PieceInfo*, std::string>>& Parts, int ColorIndex);
@@ -230,6 +234,7 @@ public:
signals:
void PartPicked(PieceInfo* Info);
void LayoutChanged();
public slots:
void CustomContextMenuRequested(QPoint Pos);
@@ -253,6 +258,7 @@ protected slots:
protected:
void SetIconSize(int Size);
void UpdateItemGeometry();
void SetPartFilterType(lcPartFilterType Option);
void keyPressEvent(QKeyEvent* KeyEvent) override;
@@ -331,6 +337,7 @@ public:
signals:
void PartPicked(PieceInfo* Info);
void CurrentPartChanged(PieceInfo* Info);
void LayoutChanged();
public slots:
void AddToPalette();
+1 -1
View File
@@ -118,7 +118,7 @@ static lcProfileEntry gProfileEntries[LC_NUM_PROFILE_KEYS] =
lcProfileEntry("Settings", "ImageHeight", 720), // LC_PROFILE_IMAGE_HEIGHT
lcProfileEntry("Settings", "ImageExtension", ".png"), // LC_PROFILE_IMAGE_EXTENSION
lcProfileEntry("Settings", "PartsListIcons", 64), // LC_PROFILE_PARTS_LIST_ICONS
lcProfileEntry("Settings", "PartsListNames", 0), // LC_PROFILE_PARTS_LIST_NAMES
lcProfileEntry("Settings", "PartsListNames", 1), // LC_PROFILE_PARTS_LIST_NAMES
lcProfileEntry("Settings", "PartsListFixedColor", -1), // LC_PROFILE_PARTS_LIST_COLOR
lcProfileEntry("Settings", "PartsListColorLocked", false), // LC_PROFILE_PARTS_LIST_COLOR_LOCKED
lcProfileEntry("Settings", "PartsListDecorated", 1), // LC_PROFILE_PARTS_LIST_DECORATED