Files
leocad/qt/lc_qutils.cpp
T
Leonardo Zide bc3d0f66bd Fix popup placement on secondary monitors
Color pickers compared global coordinates with screen dimensions, placing
Minifig Wizard and Blender preferences palettes far from their controls on
secondary monitors. The numeric ladder also used incorrect half-size bounds.

Share monitor selection and available-area clamping through
lcGetPopupPosition(), using the anchor or cursor to select the monitor.
Size color palettes before placement and reuse the helper when resizing
part-selection menus. Preserve monitor offsets, reserve desktop work areas,
and align oversized popups to the screen's top or left edge.

Validation: macOS Qt 6 build and 49 offscreen placement checks across four
simulated monitors, including negative coordinates, oversized popups, and
primary-screen fallback. Windows hardware testing remains outstanding.

Fixes #1041
2026-10-23 14:58:56 -07:00

380 lines
11 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
QPoint lcGetPopupPosition(const QPoint& Position, const QSize& Size, const QPoint& ScreenPosition)
{
QScreen* Screen = QGuiApplication::screenAt(ScreenPosition);
if (!Screen)
Screen = QGuiApplication::primaryScreen();
if (!Screen)
return Position;
const QRect AvailableGeometry = Screen->availableGeometry();
const int X = qBound(AvailableGeometry.left(), Position.x(), qMax(AvailableGeometry.left(), AvailableGeometry.right() - Size.width() + 1));
const int Y = qBound(AvailableGeometry.top(), Position.y(), qMax(AvailableGeometry.top(), AvailableGeometry.bottom() - Size.height() + 1));
return QPoint(X, Y);
}
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 lcFormatFloatLocalized(float Value, QLocale Locale)
{
Locale.setNumberOptions(Locale.numberOptions() | QLocale::OmitGroupSeparator);
QString DecimalPoint = Locale.decimalPoint();
QString String = Locale.toString(Value, 'f', 4);
if (String.indexOf(DecimalPoint) != -1)
{
while (String.endsWith(Locale.zeroDigit()))
String.chop(1);
if (String.endsWith(DecimalPoint))
String.chop(1);
}
return String;
}
QString lcFormatDoubleLocalized(double Value, QLocale Locale, char Format, int Precision)
{
Locale.setNumberOptions(Locale.numberOptions() | QLocale::OmitGroupSeparator);
return Locale.toString(Value, Format, Precision);
}
QString lcFormatIntegerLocalized(qlonglong Value, QLocale Locale)
{
Locale.setNumberOptions(Locale.numberOptions() | QLocale::OmitGroupSeparator);
return Locale.toString(Value);
}
std::optional<float> lcParseFloatLocalized(const QString& Value, QLocale Locale)
{
bool Ok;
const float Number = Locale.toFloat(Value, &Ok);
if (!Ok || !std::isfinite(Number))
return std::nullopt;
return Number;
}
#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();
}