mirror of
https://github.com/leozide/leocad.git
synced 2026-10-26 22:10:32 +00:00
Explain piece selection, template persistence, palette management, and online inventory imports with tooltips and contextual Help/F1 support. Link minifigure labels to their selectors and give unlabeled controls accessible names. Rename the inventory dialog to Import Set Inventory to describe its action. Show None for initially omitted minifigure pieces so selectors agree with the preview. Refresh catalogs while preserving existing translations and leaving new help untranslated. Validation: debug build, all 17 initial minifigure selections, 47 generated controls, 17 label buddies, eight F1 routes, Help/Escape behavior, and catalog preservation checks passed.
465 lines
15 KiB
C++
465 lines
15 KiB
C++
#include "lc_global.h"
|
|
#include "lc_minifigdialog.h"
|
|
#include "ui_lc_minifigdialog.h"
|
|
#include "lc_viewwidget.h"
|
|
#include "lc_colorpicker.h"
|
|
#include "minifig.h"
|
|
#include "lc_piecedata.h"
|
|
#include "lc_library.h"
|
|
#include "lc_view.h"
|
|
#include "lc_model.h"
|
|
#include "camera.h"
|
|
#include "lc_doublespinbox.h"
|
|
#include "lc_qutils.h"
|
|
#include "lc_partselectionpopup.h"
|
|
|
|
lcMinifigDialog::lcMinifigDialog(QWidget* Parent)
|
|
: QDialog(Parent), ui(new Ui::lcMinifigDialog)
|
|
{
|
|
ui->setupUi(this);
|
|
lcSetupDialogHelp(this, ui->buttonBox);
|
|
|
|
QGridLayout* MinifigLayout = ui->MinifigLayout;
|
|
|
|
bool HasSpinBox[LC_MFW_NUMITEMS] = { true, true, true, false, false, false, false, true, true, true, true, true, true, true, true, true, true };
|
|
bool IsLeft[LC_MFW_NUMITEMS] = { true, true, true, true, false, false, false, false, true, false, true, false, true, false, true, false, true };
|
|
int LeftRow = 0, RightRow = 2;
|
|
|
|
QString Labels[LC_MFW_NUMITEMS] =
|
|
{
|
|
tr("Hat"), tr("Hat Accessory"), tr("Head"), tr("Neck"), tr("Torso"), tr("Hip"), tr("Hip Accessory"), tr("Left Arm"), tr("Right Arm"), tr("Left Hand"), tr("Right Hand"),
|
|
tr("Left Hand Accessory"), tr("Right Hand Accessory"), tr("Left Leg"), tr("Right Leg"), tr("Left Leg Accessory"), tr("Right Leg Accessory")
|
|
};
|
|
|
|
QSizePolicy PieceButtonSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
|
|
PieceButtonSizePolicy.setHorizontalStretch(2);
|
|
PieceButtonSizePolicy.setVerticalStretch(0);
|
|
|
|
QPixmap Pixmap(1, 1);
|
|
Pixmap.fill(QColor::fromRgba64(0, 0, 0, 0));
|
|
|
|
for (int ItemIndex = 0; ItemIndex < LC_MFW_NUMITEMS; ItemIndex++)
|
|
{
|
|
bool Left = IsLeft[ItemIndex];
|
|
|
|
lcElidableToolButton* PieceButton = new lcElidableToolButton(this);
|
|
mPieceButtons[ItemIndex] = PieceButton;
|
|
PieceButton->setSizePolicy(PieceButtonSizePolicy);
|
|
PieceButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
|
PieceButton->setIcon(Pixmap);
|
|
PieceButton->setObjectName(QString("MinifigPiece%1").arg(ItemIndex));
|
|
PieceButton->setAccessibleName(tr("%1 piece").arg(Labels[ItemIndex]));
|
|
PieceButton->setToolTip(tr("Choose the piece for %1, or None to omit it.").arg(Labels[ItemIndex]));
|
|
PieceButton->setWhatsThis(tr("<p><b>Choose Piece</b></p><p>Choose a piece for this section of the minifigure. Select None to omit it. The preview updates when you select a different piece.</p>"));
|
|
|
|
connect(PieceButton, &QToolButton::clicked, this, &lcMinifigDialog::PieceButtonClicked);
|
|
|
|
lcColorPicker* ColorPicker = new lcColorPicker(this);
|
|
mColorPickers[ItemIndex] = ColorPicker;
|
|
ColorPicker->setObjectName(QString("MinifigColor%1").arg(ItemIndex));
|
|
ColorPicker->setAccessibleName(tr("%1 color").arg(Labels[ItemIndex]));
|
|
ColorPicker->setToolTip(tr("Choose the color for %1.").arg(Labels[ItemIndex]));
|
|
ColorPicker->setWhatsThis(tr("<p><b>Choose Color</b></p><p>Change the color of this minifigure piece. The preview updates immediately, and saving a template includes the chosen color.</p>"));
|
|
|
|
connect(ColorPicker, &lcColorPicker::ColorChanged, this, &lcMinifigDialog::ColorChanged);
|
|
|
|
lcDoubleSpinBox* SpinBox = HasSpinBox[ItemIndex] ? new lcDoubleSpinBox(this) : nullptr;
|
|
mSpinBoxes[ItemIndex] = SpinBox;
|
|
|
|
if (SpinBox)
|
|
{
|
|
SpinBox->setRange(-360.0, 360.0);
|
|
SpinBox->SetSnap(lcFloatPropertySnap::Rotation);
|
|
SpinBox->setSingleStep(5.0);
|
|
SpinBox->setObjectName(QString("MinifigRotation%1").arg(ItemIndex));
|
|
SpinBox->setAccessibleName(tr("%1 rotation").arg(Labels[ItemIndex]));
|
|
SpinBox->setToolTip(tr("Rotate %1, in degrees (-360 to 360).").arg(Labels[ItemIndex]));
|
|
SpinBox->setWhatsThis(tr("<p><b>Rotation</b></p><p>Adjust this piece's angle to pose the minifigure. Values range from -360 to 360 degrees. The available rotation depends on the selected minifigure section.</p>"));
|
|
|
|
connect(SpinBox, QOverload<double>::of(&lcDoubleSpinBox::valueChanged), this, &lcMinifigDialog::AngleChanged);
|
|
}
|
|
|
|
QLabel* Label = new QLabel(Labels[ItemIndex], this);
|
|
Label->setBuddy(PieceButton);
|
|
Label->setToolTip(PieceButton->toolTip());
|
|
Label->setWhatsThis(PieceButton->whatsThis());
|
|
|
|
auto AddRow=[MinifigLayout, Label, PieceButton, ColorPicker, SpinBox](int& Row, int Column)
|
|
{
|
|
if (Row)
|
|
{
|
|
MinifigLayout->setRowMinimumHeight(Row++, 2);
|
|
}
|
|
|
|
MinifigLayout->addWidget(Label, Row++, Column + 0, 1, 3);
|
|
MinifigLayout->addWidget(PieceButton, Row, Column + 0);
|
|
MinifigLayout->addWidget(ColorPicker, Row, Column + 1);
|
|
|
|
if (SpinBox)
|
|
MinifigLayout->addWidget(SpinBox, Row, Column + 2);
|
|
|
|
Row++;
|
|
};
|
|
|
|
if (Left)
|
|
AddRow(LeftRow, 0);
|
|
else
|
|
AddRow(RightRow, 4);
|
|
}
|
|
|
|
QFrame* PreviewFrame = new QFrame(ui->widget);
|
|
QSizePolicy PreviewSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
|
|
PreviewSizePolicy.setHorizontalStretch(3);
|
|
PreviewSizePolicy.setVerticalStretch(0);
|
|
PreviewFrame->setSizePolicy(PreviewSizePolicy);
|
|
PreviewFrame->setFrameShape(QFrame::NoFrame);
|
|
PreviewFrame->setFrameShadow(QFrame::Plain);
|
|
|
|
MinifigLayout->addWidget(PreviewFrame, 0, 3, -1, 1);
|
|
|
|
QGridLayout* PreviewLayout = new QGridLayout(PreviewFrame);
|
|
PreviewLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
mMinifigWizard = new MinifigWizard();
|
|
mView = new lcView(lcViewType::Minifig, mMinifigWizard->GetModel());
|
|
|
|
lcViewWidget* ViewWidget = new lcViewWidget(nullptr, mView);
|
|
ViewWidget->setAccessibleName(tr("Minifigure Preview"));
|
|
ViewWidget->setToolTip(tr("Preview of the chosen pieces, colors, and pose."));
|
|
ViewWidget->setWhatsThis(tr("<p><b>Minifigure Preview</b></p><p>Check the assembled minifigure as you change pieces, colors, or angles. Choose OK to add this configuration to the current model.</p>"));
|
|
ViewWidget->setMinimumWidth(100);
|
|
PreviewLayout->addWidget(ViewWidget);
|
|
|
|
mView->MakeCurrent();
|
|
|
|
mCurrentTemplateName = MinifigWizard::GetDefaultTemplateName();
|
|
mMinifigWizard->LoadTemplate(mCurrentTemplateName);
|
|
|
|
for (int ItemIndex = 0; ItemIndex < LC_MFW_NUMITEMS; ItemIndex++)
|
|
{
|
|
const std::vector<lcMinifigPieceInfo>& PieceList = mMinifigWizard->mSettings[ItemIndex];
|
|
int PieceIndex = mMinifigWizard->GetSelectionIndex(ItemIndex);
|
|
QToolButton* PieceButton = mPieceButtons[ItemIndex];
|
|
|
|
if (mMinifigWizard->mMinifig.PieceData[ItemIndex])
|
|
PieceButton->setText(PieceList[PieceIndex].Description);
|
|
else
|
|
PieceButton->setText(tr("None"));
|
|
|
|
lcColorPicker* ColorPicker = mColorPickers[ItemIndex];
|
|
QSignalBlocker ColorPickerBlocker(ColorPicker);
|
|
|
|
ColorPicker->SetCurrentColor(mMinifigWizard->mMinifig.ColorIndices[ItemIndex]);
|
|
}
|
|
|
|
UpdateTemplateCombo();
|
|
|
|
mMinifigWizard->Calculate();
|
|
mView->GetCamera()->SetViewpoint(lcVector3(0.0f, -270.0f, 90.0f));
|
|
mView->ZoomExtents(lcGeometryBoundsMode::IncludeFallback, false);
|
|
|
|
ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setFocus();
|
|
|
|
connect(ui->TemplateComboBox, &QComboBox::currentTextChanged, this, &lcMinifigDialog::TemplateComboBoxCurrentTextChanged);
|
|
connect(ui->TemplateSaveButton, &QPushButton::clicked, this, &lcMinifigDialog::TemplateSaveButtonClicked);
|
|
connect(ui->TemplateDeleteButton, &QPushButton::clicked, this, &lcMinifigDialog::TemplateDeleteButtonClicked);
|
|
connect(ui->TemplateImportButton, &QPushButton::clicked, this, &lcMinifigDialog::TemplateImportButtonClicked);
|
|
connect(ui->TemplateExportButton, &QPushButton::clicked, this, &lcMinifigDialog::TemplateExportButtonClicked);
|
|
}
|
|
|
|
lcMinifigDialog::~lcMinifigDialog()
|
|
{
|
|
delete mMinifigWizard;
|
|
delete ui;
|
|
}
|
|
|
|
void lcMinifigDialog::UpdateTemplateCombo()
|
|
{
|
|
QSignalBlocker Blocker(ui->TemplateComboBox);
|
|
|
|
ui->TemplateComboBox->clear();
|
|
|
|
if (mCurrentTemplateName.isEmpty())
|
|
ui->TemplateComboBox->addItem(QString());
|
|
|
|
QString CurrentName = mCurrentTemplateName;
|
|
|
|
for (const auto& [Name, Template] : mMinifigWizard->GetTemplates())
|
|
{
|
|
if (Name == MinifigWizard::GetDefaultTemplateName())
|
|
continue;
|
|
|
|
if (mCurrentTemplateModified && Name == mCurrentTemplateName)
|
|
{
|
|
CurrentName += " *";
|
|
ui->TemplateComboBox->addItem(CurrentName);
|
|
}
|
|
else
|
|
ui->TemplateComboBox->addItem(Name);
|
|
}
|
|
|
|
if (ui->TemplateComboBox->count())
|
|
ui->TemplateComboBox->insertSeparator(ui->TemplateComboBox->count());
|
|
ui->TemplateComboBox->addItem(MinifigWizard::GetDefaultTemplateName());
|
|
|
|
ui->TemplateComboBox->setCurrentText(CurrentName);
|
|
}
|
|
|
|
void lcMinifigDialog::TemplateComboBoxCurrentTextChanged(const QString& TemplateName)
|
|
{
|
|
const auto& Templates = mMinifigWizard->GetTemplates();
|
|
const auto& Position = Templates.find(TemplateName);
|
|
|
|
if (Position == Templates.end())
|
|
return;
|
|
|
|
mCurrentTemplateName = TemplateName;
|
|
mCurrentTemplateModified = false;
|
|
|
|
UpdateTemplateCombo();
|
|
|
|
const lcMinifigTemplate& Template = Position->second;
|
|
|
|
mView->MakeCurrent();
|
|
|
|
mMinifigWizard->LoadTemplate(TemplateName);
|
|
|
|
for (int PartIdx = 0; PartIdx < LC_MFW_NUMITEMS; PartIdx++)
|
|
{
|
|
if (!Template.Parts[PartIdx].isEmpty())
|
|
{
|
|
lcPieceData* PieceData = mMinifigWizard->mMinifig.PieceData[PartIdx].Get();
|
|
|
|
if (PieceData)
|
|
{
|
|
for (const lcMinifigPieceInfo& MinifigPieceInfo : mMinifigWizard->mSettings[PartIdx])
|
|
{
|
|
if (PieceData == MinifigPieceInfo.PieceData)
|
|
{
|
|
mPieceButtons[PartIdx]->setText(MinifigPieceInfo.Description);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mPieceButtons[PartIdx]->setText(tr("None"));
|
|
}
|
|
|
|
QSignalBlocker ColorBlocker(mColorPickers[PartIdx]);
|
|
|
|
mColorPickers[PartIdx]->SetCurrentColorCode(Template.ColorCodes[PartIdx]);
|
|
|
|
lcDoubleSpinBox* AngleSpinBox = mSpinBoxes[PartIdx];
|
|
|
|
if (AngleSpinBox)
|
|
{
|
|
QSignalBlocker AngleBlocker(AngleSpinBox);
|
|
|
|
AngleSpinBox->SetValue(Template.Angles[PartIdx]);
|
|
}
|
|
}
|
|
|
|
mView->Redraw();
|
|
}
|
|
|
|
void lcMinifigDialog::TemplateSaveButtonClicked()
|
|
{
|
|
QString CurrentName = mCurrentTemplateName;
|
|
bool Ok;
|
|
QString TemplateName = QInputDialog::getText(this, tr("Save Template"), tr("Template Name:"), QLineEdit::Normal, CurrentName, &Ok);
|
|
|
|
if (!Ok)
|
|
return;
|
|
|
|
if (TemplateName.isEmpty())
|
|
{
|
|
QMessageBox::information(this, tr("Save Template"), tr("Template name cannot be empty."));
|
|
return;
|
|
}
|
|
|
|
if (TemplateName == MinifigWizard::GetDefaultTemplateName())
|
|
{
|
|
QMessageBox::information(this, tr("Save Template"), tr("Cannot overwrite the default template."));
|
|
return;
|
|
}
|
|
|
|
if (mMinifigWizard->GetTemplates().count(TemplateName))
|
|
{
|
|
QString Question = tr("Are you sure you want to overwrite the template '%1'?").arg(TemplateName);
|
|
if (QMessageBox::question(this, tr("Overwrite Template"), Question, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
|
return;
|
|
}
|
|
|
|
lcMinifigTemplate Template;
|
|
|
|
for (int ItemIndex = 0; ItemIndex < LC_MFW_NUMITEMS; ItemIndex++)
|
|
{
|
|
Template.Parts[ItemIndex] = mMinifigWizard->mSettings[ItemIndex][mMinifigWizard->GetSelectionIndex(ItemIndex)].PieceData->mFileName;
|
|
Template.ColorCodes[ItemIndex] = mColorPickers[ItemIndex]->GetCurrentColorCode();
|
|
QDoubleSpinBox* AngleSpinBox = mSpinBoxes[ItemIndex];
|
|
Template.Angles[ItemIndex] = AngleSpinBox ? AngleSpinBox->value() : 0.0f;
|
|
}
|
|
|
|
mMinifigWizard->SaveTemplate(TemplateName, Template);
|
|
|
|
mCurrentTemplateName = TemplateName;
|
|
mCurrentTemplateModified = false;
|
|
|
|
UpdateTemplateCombo();
|
|
}
|
|
|
|
void lcMinifigDialog::TemplateDeleteButtonClicked()
|
|
{
|
|
if (mCurrentTemplateName.isEmpty() || mCurrentTemplateName == MinifigWizard::GetDefaultTemplateName())
|
|
return;
|
|
|
|
QString Template = mCurrentTemplateName;
|
|
QString Question = tr("Are you sure you want to delete the template '%1'?").arg(Template);
|
|
|
|
if (QMessageBox::question(this, tr("Delete Template"), Question, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
|
return;
|
|
|
|
mMinifigWizard->DeleteTemplate(Template);
|
|
|
|
mCurrentTemplateName.clear();
|
|
mCurrentTemplateModified = false;
|
|
|
|
UpdateTemplateCombo();
|
|
}
|
|
|
|
void lcMinifigDialog::TemplateImportButtonClicked()
|
|
{
|
|
QString FileName = QFileDialog::getOpenFileName(this, tr("Import Templates"), "", tr("Minifig Template Files (*.minifig);;All Files (*.*)"));
|
|
|
|
if (FileName.isEmpty())
|
|
return;
|
|
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::ReadOnly))
|
|
{
|
|
QMessageBox::warning(this, tr("Import Templates"), tr("Error reading file '%1':\n%2").arg(FileName, File.errorString()));
|
|
return;
|
|
}
|
|
|
|
QByteArray FileData = File.readAll();
|
|
mMinifigWizard->AddTemplatesJson(FileData);
|
|
|
|
UpdateTemplateCombo();
|
|
}
|
|
|
|
void lcMinifigDialog::TemplateExportButtonClicked()
|
|
{
|
|
QString FileName = QFileDialog::getSaveFileName(this, tr("Export Templates"), "", tr("Minifig Template Files (*.minifig);;All Files (*.*)"));
|
|
|
|
if (FileName.isEmpty())
|
|
return;
|
|
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::WriteOnly))
|
|
{
|
|
QMessageBox::warning(this, tr("Export Templates"), tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
|
|
return;
|
|
}
|
|
|
|
QByteArray Templates = mMinifigWizard->GetTemplatesJson();
|
|
File.write(Templates);
|
|
}
|
|
|
|
void lcMinifigDialog::SetCurrentTemplateModified()
|
|
{
|
|
if (mCurrentTemplateName == MinifigWizard::GetDefaultTemplateName())
|
|
mCurrentTemplateName.clear();
|
|
else
|
|
mCurrentTemplateModified = true;
|
|
}
|
|
|
|
void lcMinifigDialog::PieceButtonClicked()
|
|
{
|
|
QToolButton* PieceButton = qobject_cast<QToolButton*>(sender());
|
|
|
|
if (!PieceButton)
|
|
return;
|
|
|
|
std::array<QToolButton*, LC_MFW_NUMITEMS>::iterator Search = std::find(mPieceButtons.begin(), mPieceButtons.end(), PieceButton);
|
|
|
|
if (Search == mPieceButtons.end())
|
|
return;
|
|
|
|
int ItemIndex = static_cast<int>(std::distance(mPieceButtons.begin(), Search));
|
|
lcPieceData* CurrentPieceData = mMinifigWizard->mMinifig.PieceData[ItemIndex].Get();
|
|
|
|
QPoint Position = PieceButton->mapToGlobal(PieceButton->rect().bottomLeft());
|
|
std::vector<std::pair<lcPieceData*, std::string>> Parts;
|
|
|
|
Parts.reserve(mMinifigWizard->mSettings[ItemIndex].size());
|
|
Parts.emplace_back(nullptr, tr("None").toStdString());
|
|
|
|
for (const lcMinifigPieceInfo& Setting : mMinifigWizard->mSettings[ItemIndex])
|
|
if (Setting.PieceData)
|
|
Parts.emplace_back(Setting.PieceData, Setting.Description);
|
|
|
|
int ColorIndex = mMinifigWizard->mMinifig.ColorIndices[ItemIndex];
|
|
|
|
std::optional<lcPieceData*> Result = lcShowPartSelectionPopup(CurrentPieceData, Parts, ColorIndex, PieceButton, Position);
|
|
|
|
if (!Result.has_value())
|
|
return;
|
|
|
|
lcPieceData* NewPiece = Result.value();
|
|
|
|
SetCurrentTemplateModified();
|
|
UpdateTemplateCombo();
|
|
|
|
mView->MakeCurrent();
|
|
mMinifigWizard->SetPieceData(ItemIndex, NewPiece);
|
|
mView->Redraw();
|
|
|
|
if (NewPiece)
|
|
{
|
|
const std::vector<lcMinifigPieceInfo>& PieceList = mMinifigWizard->mSettings[ItemIndex];
|
|
int PieceIndex = mMinifigWizard->GetSelectionIndex(ItemIndex);
|
|
|
|
PieceButton->setText(PieceList[PieceIndex].Description);
|
|
}
|
|
else
|
|
{
|
|
PieceButton->setText(tr("None"));
|
|
}
|
|
}
|
|
|
|
void lcMinifigDialog::ColorChanged(int Index)
|
|
{
|
|
std::array<lcColorPicker*, LC_MFW_NUMITEMS>::iterator Search = std::find(mColorPickers.begin(), mColorPickers.end(), sender());
|
|
|
|
if (Search == mColorPickers.end())
|
|
return;
|
|
|
|
SetCurrentTemplateModified();
|
|
UpdateTemplateCombo();
|
|
|
|
int ItemIndex = static_cast<int>(std::distance(mColorPickers.begin(), Search));
|
|
|
|
mMinifigWizard->SetColorIndex(ItemIndex, Index);
|
|
mView->Redraw();
|
|
}
|
|
|
|
void lcMinifigDialog::AngleChanged(double Value)
|
|
{
|
|
std::array<lcDoubleSpinBox*, LC_MFW_NUMITEMS>::iterator Search = std::find(mSpinBoxes.begin(), mSpinBoxes.end(), sender());
|
|
|
|
if (Search == mSpinBoxes.end())
|
|
return;
|
|
|
|
SetCurrentTemplateModified();
|
|
UpdateTemplateCombo();
|
|
|
|
int ItemIndex = static_cast<int>(std::distance(mSpinBoxes.begin(), Search));
|
|
|
|
mMinifigWizard->SetAngle(ItemIndex, Value);
|
|
mView->Redraw();
|
|
}
|