mirror of
https://github.com/leozide/leocad.git
synced 2026-10-26 22:10:32 +00:00
Parse dialog numbers using their validator locale so localized decimals and grouped input round trip correctly, while displaying values without digit grouping. Keep invalid input open with a validation message instead of silently saving or clamping it, and serialize Blender settings in their canonical format. Use a 1–32768 pixel range for image dimensions across Save Image, HTML export, command-line output, POV-Ray, and Blender. Bound export step edits by the model step count, grid spacing to 10000 studs, and area-light grids to 64 samples per axis. Preserve existing show/hide step bounds and avoid a total-pixel cap. Validation: macOS Qt debug build and numeric dialog checks across ten locales, including localized input, validation messages, image dimension boundaries, and settings round trips.
352 lines
10 KiB
C++
352 lines
10 KiB
C++
#include "lc_global.h"
|
|
#include "lc_qutils.h"
|
|
#include "lc_application.h"
|
|
#include "lc_library.h"
|
|
#include "lc_model.h"
|
|
#include "lc_piecedata.h"
|
|
#include "lc_string.h"
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <windows.h>
|
|
#include <TlHelp32.h>
|
|
#endif
|
|
|
|
void lcSetupWidgetHelp(QWidget* Root, QDialogButtonBox* ButtonBox)
|
|
{
|
|
const auto ShowHelp = [Root](QWidget* Widget)
|
|
{
|
|
while (Widget && Widget->whatsThis().isEmpty() && Widget != Root)
|
|
Widget = Widget->parentWidget();
|
|
|
|
if (Widget && !Widget->whatsThis().isEmpty())
|
|
QWhatsThis::showText(Widget->mapToGlobal(QPoint(0, Widget->height())), Widget->whatsThis(), Widget);
|
|
};
|
|
|
|
if (ButtonBox)
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::helpRequested, Root, [Root, ShowHelp]()
|
|
{
|
|
ShowHelp(Root);
|
|
});
|
|
|
|
QShortcut* HelpShortcut = new QShortcut(QKeySequence(Qt::Key_F1), Root);
|
|
HelpShortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
|
|
|
const auto HelpActivated = [Root, ShowHelp]()
|
|
{
|
|
QWidget* Focus = QApplication::focusWidget();
|
|
// Popup windows can share the shortcut scope across a QWidget window boundary.
|
|
ShowHelp(Focus ? Focus : Root);
|
|
};
|
|
|
|
QObject::connect(HelpShortcut, &QShortcut::activated, Root, HelpActivated);
|
|
// A chooser can overlap its owning dock's shortcut scope; both routes start at the focused control.
|
|
QObject::connect(HelpShortcut, &QShortcut::activatedAmbiguously, Root, HelpActivated);
|
|
}
|
|
|
|
void lcSetupDialogHelp(QDialog* Dialog, QDialogButtonBox* ButtonBox)
|
|
{
|
|
lcSetupWidgetHelp(Dialog, ButtonBox);
|
|
}
|
|
|
|
QString lcFormatValue(float Value, int Precision)
|
|
{
|
|
QString String = QString::number(Value, 'f', Precision);
|
|
const qsizetype Dot = String.indexOf('.');
|
|
|
|
if (Dot != -1)
|
|
{
|
|
while (String.endsWith('0'))
|
|
String.chop(1);
|
|
|
|
if (String.endsWith('.'))
|
|
String.chop(1);
|
|
}
|
|
|
|
return String;
|
|
}
|
|
|
|
QString lcFormatValueLocalized(float Value)
|
|
{
|
|
QLocale Locale = QLocale::system();
|
|
Locale.setNumberOptions(QLocale::OmitGroupSeparator);
|
|
QString DecimalPoint = Locale.decimalPoint();
|
|
QString String = Locale.toString(Value, 'f', 4);
|
|
|
|
if (String.indexOf(DecimalPoint) != -1)
|
|
{
|
|
while (String.endsWith('0'))
|
|
String.chop(1);
|
|
|
|
if (String.endsWith(DecimalPoint))
|
|
String.chop(1);
|
|
}
|
|
|
|
return String;
|
|
}
|
|
|
|
QString lcFormatIntegerLocalized(qlonglong Value, QLocale Locale)
|
|
{
|
|
Locale.setNumberOptions(Locale.numberOptions() | QLocale::OmitGroupSeparator);
|
|
return Locale.toString(Value);
|
|
}
|
|
|
|
float lcParseValueLocalized(const QString& Value)
|
|
{
|
|
return QLocale::system().toFloat(Value);
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
int lcTerminateChildProcess(QWidget* Parent, const qint64 Pid, const qint64 Ppid)
|
|
{
|
|
DWORD PID = DWORD(Pid);
|
|
DWORD PPID = DWORD(Ppid);
|
|
HANDLE hSnapshot = INVALID_HANDLE_VALUE, hProcess = INVALID_HANDLE_VALUE;
|
|
PROCESSENTRY32 PE32;
|
|
|
|
if ((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, PPID)) == INVALID_HANDLE_VALUE)
|
|
{
|
|
QMessageBox::warning(Parent, QObject::tr("Stop Blender Process"), QString("%1 failed: %1").arg("CreateToolhelp32Snapshot").arg(GetLastError()));
|
|
return -1;
|
|
}
|
|
PE32.dwSize = sizeof(PROCESSENTRY32);
|
|
if (Process32First(hSnapshot, &PE32) == FALSE)
|
|
{
|
|
QMessageBox::warning(Parent, QObject::tr("Stop Blender Process"), QString("%1 failed: %2").arg("Process32First").arg(GetLastError()));
|
|
CloseHandle(hSnapshot);
|
|
return -2;
|
|
}
|
|
do
|
|
{
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
QRegularExpression ExeExpression("^(?:cmd\\.exe|conhost\\.exe|blender\\.exe)$", QRegularExpression::CaseInsensitiveOption);
|
|
#else
|
|
QRegExp ExeExpression("^(?:cmd\\.exe|conhost\\.exe|blender\\.exe)$", Qt::CaseInsensitive);
|
|
#endif
|
|
|
|
if (QString::fromWCharArray(PE32.szExeFile).contains(ExeExpression))
|
|
{
|
|
if ((PE32.th32ProcessID == PID && PE32.th32ParentProcessID == PPID) || PE32.th32ParentProcessID == PID)
|
|
{
|
|
if ((hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, PE32.th32ProcessID)) == INVALID_HANDLE_VALUE)
|
|
{
|
|
QMessageBox::warning(Parent, QObject::tr("Stop Blender Process"), QObject::tr("%1 failed: %2").arg("OpenProcess").arg(GetLastError()));
|
|
return -3;
|
|
}
|
|
else
|
|
{
|
|
TerminateProcess(hProcess, 9);
|
|
CloseHandle(hProcess);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (Process32Next(hSnapshot, &PE32));
|
|
|
|
CloseHandle(hSnapshot);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool lcRunElevatedProcess(const LPCWSTR ExeName, const LPCWSTR Arguments, const LPCWSTR WorkingFolder)
|
|
{
|
|
SHELLEXECUTEINFO ShellExecuteInfo = {};
|
|
|
|
ShellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFO);
|
|
ShellExecuteInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
|
|
ShellExecuteInfo.hwnd = NULL;
|
|
ShellExecuteInfo.lpVerb = L"runas";
|
|
ShellExecuteInfo.lpFile = ExeName;
|
|
ShellExecuteInfo.lpParameters = Arguments;
|
|
ShellExecuteInfo.lpDirectory = WorkingFolder;
|
|
ShellExecuteInfo.nShow = SW_SHOW;
|
|
ShellExecuteInfo.hInstApp = NULL;
|
|
|
|
if (!ShellExecuteEx(&ShellExecuteInfo) || !ShellExecuteInfo.hProcess)
|
|
return false;
|
|
|
|
if (WaitForSingleObject(ShellExecuteInfo.hProcess, INFINITE))
|
|
return false;
|
|
|
|
DWORD ExitCode;
|
|
|
|
if (!GetExitCodeProcess(ShellExecuteInfo.hProcess, &ExitCode))
|
|
return false;
|
|
|
|
CloseHandle(ShellExecuteInfo.hProcess);
|
|
|
|
return ExitCode == 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
// Resize all columns to content except for one stretching column. (taken from QT creator)
|
|
lcTreeWidgetColumnStretcher::lcTreeWidgetColumnStretcher(QTreeWidget* TreeWidget, int ColumnToStretch)
|
|
: QObject(TreeWidget->header()), mColumnToStretch(ColumnToStretch), mInteractiveResize(false), mStretchWidth(0)
|
|
{
|
|
parent()->installEventFilter(this);
|
|
connect(TreeWidget->header(), &QHeaderView::sectionResized, this, &lcTreeWidgetColumnStretcher::SectionResized);
|
|
|
|
QHideEvent Fake;
|
|
lcTreeWidgetColumnStretcher::eventFilter(parent(), &Fake);
|
|
}
|
|
|
|
void lcTreeWidgetColumnStretcher::SectionResized(int LogicalIndex, int OldSize, int NewSize)
|
|
{
|
|
Q_UNUSED(OldSize)
|
|
|
|
if (LogicalIndex == mColumnToStretch)
|
|
{
|
|
QHeaderView* HeaderView = qobject_cast<QHeaderView*>(parent());
|
|
|
|
if (HeaderView->isVisible())
|
|
mInteractiveResize = true;
|
|
|
|
mStretchWidth = NewSize;
|
|
}
|
|
}
|
|
|
|
bool lcTreeWidgetColumnStretcher::eventFilter(QObject* Object, QEvent* Event)
|
|
{
|
|
if (Object == parent())
|
|
{
|
|
QHeaderView* HeaderView = qobject_cast<QHeaderView*>(Object);
|
|
|
|
if (Event->type() == QEvent::Show)
|
|
{
|
|
for (int i = 0; i < HeaderView->count(); ++i)
|
|
HeaderView->setSectionResizeMode(i, QHeaderView::Interactive);
|
|
|
|
mStretchWidth = HeaderView->sectionSize(mColumnToStretch);
|
|
|
|
}
|
|
else if (Event->type() == QEvent::Hide)
|
|
{
|
|
if (!mInteractiveResize)
|
|
for (int i = 0; i < HeaderView->count(); ++i)
|
|
HeaderView->setSectionResizeMode(i, i == mColumnToStretch ? QHeaderView::Stretch : QHeaderView::ResizeToContents);
|
|
}
|
|
else if (Event->type() == QEvent::Resize)
|
|
{
|
|
if (HeaderView->sectionResizeMode(mColumnToStretch) == QHeaderView::Interactive) {
|
|
|
|
const int StretchWidth = HeaderView->isVisible() ? mStretchWidth : 32;
|
|
|
|
HeaderView->resizeSection(mColumnToStretch, StretchWidth);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
lcPieceIdStringModel::lcPieceIdStringModel(lcModel* Model, QObject* Parent)
|
|
: QAbstractListModel(Parent)
|
|
{
|
|
lcPiecesLibrary* Library = lcGetPiecesLibrary();
|
|
mSortedPieces = Library->GetParts();
|
|
|
|
mSortedPieces.erase(std::remove_if(mSortedPieces.begin(), mSortedPieces.end(), [Model](lcPieceData* PieceData)
|
|
{
|
|
return PieceData->IsModel() && PieceData->GetModel()->IncludesModel(Model);
|
|
}), mSortedPieces.end());
|
|
|
|
auto PieceCompare = [](lcPieceData* PieceData1, lcPieceData* PieceData2)
|
|
{
|
|
return strcmp(PieceData1->m_strDescription, PieceData2->m_strDescription) < 0;
|
|
};
|
|
|
|
std::sort(mSortedPieces.begin(), mSortedPieces.end(), PieceCompare);
|
|
}
|
|
|
|
QModelIndex lcPieceIdStringModel::Index(lcPieceData* PieceData) const
|
|
{
|
|
for (size_t PieceDataIndex = 0; PieceDataIndex < mSortedPieces.size(); PieceDataIndex++)
|
|
if (mSortedPieces[PieceDataIndex] == PieceData)
|
|
return index(static_cast<int>(PieceDataIndex), 0);
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
std::vector<bool> lcPieceIdStringModel::GetFilteredRows(const QString& FilterText) const
|
|
{
|
|
const std::string Text = FilterText.toStdString();
|
|
std::vector<bool> FilteredRows(mSortedPieces.size());
|
|
|
|
for (size_t PieceDataIndex = 0; PieceDataIndex < mSortedPieces.size(); PieceDataIndex++)
|
|
{
|
|
const lcPieceData* PieceData = mSortedPieces[PieceDataIndex];
|
|
|
|
FilteredRows[PieceDataIndex] = (lcstrcasestr(PieceData->m_strDescription, Text.c_str()) || lcstrcasestr(PieceData->mFileName, Text.c_str()));
|
|
}
|
|
|
|
return FilteredRows;
|
|
}
|
|
|
|
int lcPieceIdStringModel::rowCount(const QModelIndex& Parent) const
|
|
{
|
|
Q_UNUSED(Parent);
|
|
|
|
return static_cast<int>(mSortedPieces.size());
|
|
}
|
|
|
|
QVariant lcPieceIdStringModel::data(const QModelIndex& Index, int Role) const
|
|
{
|
|
if (Index.row() < static_cast<int>(mSortedPieces.size()))
|
|
{
|
|
if (Role == Qt::DisplayRole)
|
|
return QString::fromLatin1(mSortedPieces[Index.row()]->m_strDescription);
|
|
else if (Role == Qt::UserRole)
|
|
return QVariant::fromValue(reinterpret_cast<void*>(mSortedPieces[Index.row()]));
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
lcColorDialogPopup::lcColorDialogPopup(const QColor& InitialColor, QWidget* Parent)
|
|
: QWidget(Parent)
|
|
{
|
|
setAccessibleName(tr("Choose Color"));
|
|
setWhatsThis(tr("<p><b>Choose Color</b></p><p>Pick a color using the swatches or enter its component values. Choose OK to use the selected color, or Cancel to keep the original color. Editing the controls alone does not apply the color.</p>"));
|
|
|
|
QVBoxLayout* Layout = new QVBoxLayout(this);
|
|
Layout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
mColorDialog = new QColorDialog(InitialColor, this);
|
|
mColorDialog->setWindowFlags(Qt::Widget);
|
|
mColorDialog->setOptions(QColorDialog::DontUseNativeDialog | QColorDialog::NoButtons);
|
|
|
|
Layout->addWidget(mColorDialog);
|
|
mColorDialog->setAccessibleName(tr("Custom Color"));
|
|
mColorDialog->setToolTip(tr("Choose a color or enter its component values."));
|
|
|
|
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::Help, this);
|
|
Layout->addWidget(ButtonBox);
|
|
lcSetupWidgetHelp(this, ButtonBox);
|
|
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::accepted, this, &lcColorDialogPopup::Accept);
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::rejected, this, &lcColorDialogPopup::Reject);
|
|
}
|
|
|
|
void lcColorDialogPopup::Accept()
|
|
{
|
|
QColor Color = mColorDialog->currentColor();
|
|
|
|
emit ColorSelected(Color);
|
|
|
|
Close();
|
|
}
|
|
|
|
void lcColorDialogPopup::Reject()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void lcColorDialogPopup::Close()
|
|
{
|
|
QMenu* Menu = qobject_cast<QMenu*>(parent());
|
|
|
|
if (Menu)
|
|
Menu->close();
|
|
}
|