изменено: CMakeLists.txt

новый файл:    appimage-build.nix
	новый файл:    assets/icons/erlu.ico
	изменено:      default.nix
	новый файл:    packaging/windows/erlu.rc
	изменено:      src/MainWindow.cpp
	изменено:      src/MainWindow.h
	изменено:      src/items/BlockItem.cpp
	изменено:      src/items/HeaderFooterItem.cpp
	изменено:      src/main.cpp
	изменено:      src/plugins/color/ColorsPlugin.cpp
	изменено:      src/plugins/color/translations/colors_en.ts
	изменено:      src/plugins/color/translations/colors_fr.ts
	изменено:      src/plugins/color/translations/colors_ru.ts
This commit is contained in:
Gregory Bednov 2026-03-04 20:23:09 +03:00
commit 58198c6ecd
14 changed files with 428 additions and 139 deletions

View file

@ -54,6 +54,14 @@ static const char* kDiagramFileFilter = QT_TR_NOOP("IDEF0 Diagram (*.idef0);;JSO
static const char* kPdfFileFilter = QT_TR_NOOP("PDF (*.pdf)");
static const char* kMarkdownFileFilter = QT_TR_NOOP("Markdown (*.md)");
static QFileDialog::Options themedFileDialogOptions() {
QFileDialog::Options options;
#if defined(Q_OS_WIN)
options |= QFileDialog::DontUseNativeDialog;
#endif
return options;
}
static QPageSize::PageSizeId pageSizeFromMeta(const QVariantMap& meta) {
const QString size = meta.value("pageSize", "A4").toString();
if (size == "A1") return QPageSize::A1;
@ -95,7 +103,7 @@ static QVariantMap mergeWithDiagramState(QVariantMap globalMeta, const QVariantM
return globalMeta;
}
MainWindow::MainWindow(const QString& startupPath, QWidget* parent)
MainWindow::MainWindow(const QString& startupPath, QWidget* parent, bool skipStartupPrompt)
: QMainWindow(parent)
{
setupUi();
@ -106,7 +114,7 @@ MainWindow::MainWindow(const QString& startupPath, QWidget* parent)
QTimer::singleShot(0, this, &QWidget::close);
return;
}
} else if (!promptStartup()) {
} else if (!skipStartupPrompt && !promptStartup()) {
QTimer::singleShot(0, this, &QWidget::close);
return;
}
@ -262,7 +270,8 @@ void MainWindow::setupActions() {
connect(actNew, &QAction::triggered, this, [this]{ newDiagram(); });
connect(actOpen, &QAction::triggered, this, [this]{
const QString path = QFileDialog::getOpenFileName(this, tr("Open diagram"), QString(), tr(kDiagramFileFilter));
const QString path = QFileDialog::getOpenFileName(
this, tr("Open diagram"), QString(), tr(kDiagramFileFilter), nullptr, themedFileDialogOptions());
if (!path.isEmpty()) {
loadDiagramFromPath(path);
}
@ -293,14 +302,16 @@ void MainWindow::setupActions() {
connect(actSave, &QAction::triggered, this, [this, saveTo]{
if (m_currentFile.isEmpty()) {
const QString path = QFileDialog::getSaveFileName(this, tr("Save diagram"), QString(), tr(kDiagramFileFilter));
const QString path = QFileDialog::getSaveFileName(
this, tr("Save diagram"), QString(), tr(kDiagramFileFilter), nullptr, themedFileDialogOptions());
if (!path.isEmpty()) saveTo(path);
} else {
saveTo(m_currentFile);
}
});
connect(actSaveAs, &QAction::triggered, this, [this, saveTo]{
const QString path = QFileDialog::getSaveFileName(this, tr("Save diagram as"), QString(), tr(kDiagramFileFilter));
const QString path = QFileDialog::getSaveFileName(
this, tr("Save diagram as"), QString(), tr(kDiagramFileFilter), nullptr, themedFileDialogOptions());
if (!path.isEmpty()) saveTo(path);
});
connect(actExportPdf, &QAction::triggered, this, [this]{
@ -411,9 +422,77 @@ static QString symbolPlacementDefault(const QLocale& loc, const QString& sym) {
return "?1";
}
static void applyWindowsMenuPaletteFix(QMainWindow* window) {
#if defined(Q_OS_WIN)
if (!window || !window->menuBar()) return;
const QPalette pal = qApp->palette();
const QString menuBg = pal.color(QPalette::Window).name(QColor::HexRgb);
const QString popupBg = pal.color(QPalette::Base).name(QColor::HexRgb);
const QString text = pal.color(QPalette::Active, QPalette::WindowText).name(QColor::HexRgb);
const QString selectedBg = pal.color(QPalette::Highlight).name(QColor::HexRgb);
const QString selectedText = pal.color(QPalette::HighlightedText).name(QColor::HexRgb);
const QString disabledText = pal.color(QPalette::Disabled, QPalette::WindowText).name(QColor::HexRgb);
window->menuBar()->setStyleSheet(QStringLiteral(
"QMenuBar { background-color: %1; color: %2; }"
"QMenuBar::item { background: transparent; color: %2; padding: 4px 8px; }"
"QMenuBar::item:selected { background-color: %3; color: %4; }"
"QMenuBar::item:disabled { color: %5; }"
"QMenu { background-color: %6; color: %2; border: 1px solid %1; }"
"QMenu::item { color: %2; }"
"QMenu::item:selected { background-color: %3; color: %4; }"
"QMenu::item:disabled { color: %5; }")
.arg(menuBg, text, selectedBg, selectedText, disabledText, popupBg));
#else
Q_UNUSED(window);
#endif
}
static void applyWindowsDialogPaletteFix(bool darkMode) {
#if defined(Q_OS_WIN)
if (!darkMode) {
qApp->setStyleSheet(QString());
return;
}
const QPalette pal = qApp->palette();
const QString window = pal.color(QPalette::Window).name(QColor::HexRgb);
const QString base = pal.color(QPalette::Base).name(QColor::HexRgb);
const QString text = pal.color(QPalette::Active, QPalette::WindowText).name(QColor::HexRgb);
const QString border = pal.color(QPalette::Mid).name(QColor::HexRgb);
const QString btn = pal.color(QPalette::Button).name(QColor::HexRgb);
const QString btnText = pal.color(QPalette::Active, QPalette::ButtonText).name(QColor::HexRgb);
const QString disabled = pal.color(QPalette::Disabled, QPalette::WindowText).name(QColor::HexRgb);
const QString highlight = pal.color(QPalette::Highlight).name(QColor::HexRgb);
const QString highlightedText = pal.color(QPalette::HighlightedText).name(QColor::HexRgb);
qApp->setStyleSheet(QStringLiteral(
"QDialog, QMessageBox, QInputDialog, QFileDialog, QColorDialog { background-color: %1; color: %2; }"
"QTabWidget::pane { background-color: %1; border: 1px solid %5; }"
"QTabBar::tab { background-color: %3; color: %4; border: 1px solid %5; padding: 4px 10px; }"
"QTabBar::tab:selected { background-color: %1; color: %2; }"
"QTabBar::tab:disabled { color: %6; }"
"QTabWidget QWidget { background-color: %1; color: %2; }"
"QLabel, QCheckBox, QRadioButton, QGroupBox { color: %2; }"
"QPushButton { background-color: %3; color: %4; border: 1px solid %5; padding: 4px 10px; }"
"QPushButton:disabled { color: %6; }"
"QLineEdit, QPlainTextEdit, QTextEdit, QAbstractSpinBox, QComboBox {"
" background-color: %7; color: %2; border: 1px solid %5; selection-background-color: %8; selection-color: %9; }"
"QAbstractItemView, QListView, QTreeView, QTableView {"
" background-color: %7; color: %2; border: 1px solid %5; selection-background-color: %8; selection-color: %9; }"
"QDialogButtonBox QPushButton { min-width: 80px; }")
.arg(window, text, btn, btnText, border, disabled, base, highlight, highlightedText));
#else
Q_UNUSED(darkMode);
#endif
}
bool MainWindow::showWelcome() {
auto* dlg = new QDialog(this);
dlg->setWindowTitle(tr("Welcome"));
dlg->setPalette(qApp->palette());
#if defined(Q_OS_WIN)
dlg->setStyleSheet(qApp->styleSheet());
#endif
auto* tabs = new QTabWidget(dlg);
// General tab
@ -445,20 +524,6 @@ bool MainWindow::showWelcome() {
general->setLayout(genForm);
tabs->addTab(general, tr("General"));
// Numbering tab (placeholder)
auto* numbering = new QWidget(dlg);
auto* numLayout = new QVBoxLayout(numbering);
numLayout->addStretch();
numbering->setLayout(numLayout);
tabs->addTab(numbering, tr("Numbering"));
// Display tab (placeholder)
auto* display = new QWidget(dlg);
auto* disLayout = new QVBoxLayout(display);
disLayout->addStretch();
display->setLayout(disLayout);
tabs->addTab(display, tr("Display"));
// ABC Units tab
auto* units = new QWidget(dlg);
auto* unitsForm = new QFormLayout(units);
@ -577,17 +642,31 @@ void MainWindow::resetScene() {
void MainWindow::newDiagram() {
if (!showWelcome()) return;
resetScene();
const QRectF bounds = m_scene->contentRect().isNull() ? m_scene->sceneRect() : m_scene->contentRect();
m_scene->createBlockAt(bounds.center());
m_currentFile.clear();
markDirty(false);
// Keep main window visible and focused after closing modal creation dialogs.
QTimer::singleShot(0, this, [this]{
setWindowState(windowState() & ~Qt::WindowMinimized);
showNormal();
raise();
activateWindow();
auto* win = new MainWindow(QString(), nullptr, true);
win->m_welcomeState = m_welcomeState;
if (win->m_actDarkMode && win->m_actFollowSystemTheme) {
const QSignalBlocker b2(win->m_actDarkMode);
const QSignalBlocker b3(win->m_actFollowSystemTheme);
win->m_actDarkMode->setChecked(win->m_welcomeState.value("darkMode", false).toBool());
win->m_actFollowSystemTheme->setChecked(win->m_welcomeState.value("followSystemTheme", false).toBool());
win->m_actDarkMode->setEnabled(!win->m_actFollowSystemTheme->isChecked());
}
win->m_welcomeState["resolvedDarkMode"] = win->effectiveDarkMode();
win->applyAppPalette(win->m_welcomeState.value("resolvedDarkMode").toBool());
win->resetScene();
const QRectF bounds = win->m_scene->contentRect().isNull() ? win->m_scene->sceneRect() : win->m_scene->contentRect();
win->m_scene->createBlockAt(bounds.center());
win->m_currentFile.clear();
win->markDirty(false);
win->resize(size());
win->show();
QTimer::singleShot(0, win, [win]{
win->setWindowState(win->windowState() & ~Qt::WindowMinimized);
win->showNormal();
win->raise();
win->activateWindow();
});
}
@ -601,7 +680,8 @@ bool MainWindow::promptStartup() {
box.exec();
if (box.clickedButton() == cancelBtn) return false;
if (box.clickedButton() == openBtn) {
const QString path = QFileDialog::getOpenFileName(this, tr("Open diagram"), QString(), tr(kDiagramFileFilter));
const QString path = QFileDialog::getOpenFileName(
this, tr("Open diagram"), QString(), tr(kDiagramFileFilter), nullptr, themedFileDialogOptions());
if (path.isEmpty()) return false;
return loadDiagramFromPath(path);
}
@ -659,7 +739,8 @@ bool MainWindow::loadDiagramFromPath(const QString& path) {
}
bool MainWindow::exportPdf(bool allDiagrams, bool numberPages, bool forceLightTheme) {
QString path = QFileDialog::getSaveFileName(this, tr("Export to PDF"), QString(), tr(kPdfFileFilter));
QString path = QFileDialog::getSaveFileName(
this, tr("Export to PDF"), QString(), tr(kPdfFileFilter), nullptr, themedFileDialogOptions());
if (path.isEmpty()) return false;
if (QFileInfo(path).suffix().isEmpty()) path += ".pdf";
@ -765,7 +846,8 @@ bool MainWindow::exportPdf(bool allDiagrams, bool numberPages, bool forceLightTh
bool MainWindow::exportMarkdownExplanation() {
if (!m_scene) return false;
QString path = QFileDialog::getSaveFileName(this, tr("Export to Markdown"), QString(), tr(kMarkdownFileFilter));
QString path = QFileDialog::getSaveFileName(
this, tr("Export to Markdown"), QString(), tr(kMarkdownFileFilter), nullptr, themedFileDialogOptions());
if (path.isEmpty()) return false;
if (QFileInfo(path).suffix().isEmpty()) path += ".md";
@ -909,6 +991,8 @@ void MainWindow::applyAppPalette(bool darkMode) {
static const QPalette defaultPalette = qApp->palette();
if (!darkMode) {
qApp->setPalette(defaultPalette);
applyWindowsDialogPaletteFix(false);
applyWindowsMenuPaletteFix(this);
return;
}
QPalette p;
@ -925,6 +1009,8 @@ void MainWindow::applyAppPalette(bool darkMode) {
p.setColor(QPalette::Highlight, QColor(75, 110, 180));
p.setColor(QPalette::HighlightedText, QColor(255, 255, 255));
qApp->setPalette(p);
applyWindowsDialogPaletteFix(true);
applyWindowsMenuPaletteFix(this);
}
bool MainWindow::openDiagramPath(const QString& path) {

View file

@ -13,7 +13,7 @@ class PluginManager;
class MainWindow final : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(const QString& startupPath = QString(), QWidget* parent = nullptr);
explicit MainWindow(const QString& startupPath = QString(), QWidget* parent = nullptr, bool skipStartupPrompt = false);
bool openDiagramPath(const QString& path);
DiagramScene* scene() const { return m_scene; }

View file

@ -16,6 +16,7 @@
#include <QCheckBox>
#include <QVBoxLayout>
#include <QLocale>
#include <QApplication>
#include "DiagramScene.h"
@ -281,7 +282,7 @@ QString BlockItem::formattedPrice() const {
}
void BlockItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
auto* dlg = new QDialog();
auto* dlg = new QDialog(QApplication::activeWindow());
dlg->setWindowTitle(tr("Edit block"));
auto* layout = new QVBoxLayout(dlg);

View file

@ -11,6 +11,7 @@
#include <QButtonGroup>
#include <QVBoxLayout>
#include <QGraphicsSceneMouseEvent>
#include <QApplication>
HeaderFooterItem::HeaderFooterItem(DiagramScene* scene)
: QGraphicsObject(nullptr)
@ -221,7 +222,7 @@ void HeaderFooterItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
return;
}
auto* dlg = new QDialog();
auto* dlg = new QDialog(QApplication::activeWindow());
dlg->setWindowTitle(tr("Edit header/footer"));
auto* layout = new QVBoxLayout(dlg);
auto* form = new QFormLayout();

View file

@ -73,6 +73,9 @@ int main(int argc, char** argv) {
const QStringList searchPaths = {
QDir::currentPath() + "/translations",
QCoreApplication::applicationDirPath() + "/translations",
QCoreApplication::applicationDirPath() + "/../translations",
QCoreApplication::applicationDirPath() + "/../Resources/translations",
QCoreApplication::applicationDirPath() + "/../share/idef0/translations",
":/i18n"
};

View file

@ -1,94 +1,98 @@
#include "plugins/color/ColorsPlugin.h"
#include <QColorDialog>
#include <QObject>
#include <QTranslator>
#include <QLocale>
#include <QCoreApplication>
#include <QDebug>
static void ensureTranslator(Idef0Host* host) {
static bool loaded = false;
static QTranslator* translator = nullptr;
if (loaded || !host || !host->plugin_dir) return;
const char* dirC = host->plugin_dir(host->opaque);
if (!dirC) return;
const QString dir = QString::fromUtf8(dirC);
const QString baseLocale = QLocale().name().replace('-', '_');
const QString shortLocale = baseLocale.section('_', 0, 0);
QStringList candidates;
auto addCandidate = [&](const QString& c) {
if (c.isEmpty()) return;
if (c == QStringLiteral("C") || c == QStringLiteral("POSIX")) return;
candidates << c;
};
addCandidate(baseLocale);
addCandidate(shortLocale);
const QString sysName = QLocale::system().name().replace('-', '_');
addCandidate(sysName);
addCandidate(sysName.section('_', 0, 0));
for (const QString& lang : QLocale::system().uiLanguages()) {
const QString norm = QString(lang).replace('-', '_');
addCandidate(norm);
addCandidate(norm.section('_', 0, 0));
}
const QString envLang = QString::fromLocal8Bit(qgetenv("LANG")).section('.', 0, 0).replace('-', '_');
addCandidate(envLang);
addCandidate(envLang.section('_', 0, 0));
candidates.removeDuplicates();
if (candidates.isEmpty()) candidates << QStringLiteral("en");
translator = new QTranslator(qApp);
qInfo() << "[colors plugin] translator search in" << dir << "candidates" << candidates;
for (const QString& loc : candidates) {
if (loc.isEmpty()) continue;
const QString baseName = QStringLiteral("colors_%1").arg(loc);
if (translator->load(baseName, dir + "/translations")) {
qApp->installTranslator(translator);
qInfo() << "[colors plugin] translator loaded" << baseName;
loaded = true;
break;
}
}
if (!loaded) {
qWarning() << "[colors plugin] translator not found, falling back to default language";
delete translator;
translator = nullptr;
}
}
static void onSetColor(void*, Idef0Host* host) {
if (!host || !host->selected_items || !host->set_item_color) return;
ensureTranslator(host);
#include "plugins/color/ColorsPlugin.h"
#include <QColorDialog>
#include <QObject>
#include <QTranslator>
#include <QLocale>
#include <QCoreApplication>
#include <QDebug>
static void ensureTranslator(Idef0Host* host) {
static bool loaded = false;
static QTranslator* translator = nullptr;
if (loaded || !host || !host->plugin_dir) return;
const char* dirC = host->plugin_dir(host->opaque);
if (!dirC) return;
const QString dir = QString::fromUtf8(dirC);
const QString baseLocale = QLocale().name().replace('-', '_');
const QString shortLocale = baseLocale.section('_', 0, 0);
QStringList candidates;
auto addCandidate = [&](const QString& c) {
if (c.isEmpty()) return;
if (c == QStringLiteral("C") || c == QStringLiteral("POSIX")) return;
candidates << c;
};
addCandidate(baseLocale);
addCandidate(shortLocale);
const QString sysName = QLocale::system().name().replace('-', '_');
addCandidate(sysName);
addCandidate(sysName.section('_', 0, 0));
for (const QString& lang : QLocale::system().uiLanguages()) {
const QString norm = QString(lang).replace('-', '_');
addCandidate(norm);
addCandidate(norm.section('_', 0, 0));
}
const QString envLang = QString::fromLocal8Bit(qgetenv("LANG")).section('.', 0, 0).replace('-', '_');
addCandidate(envLang);
addCandidate(envLang.section('_', 0, 0));
candidates.removeDuplicates();
if (candidates.isEmpty()) candidates << QStringLiteral("en");
translator = new QTranslator(qApp);
qInfo() << "[colors plugin] translator search in" << dir << "candidates" << candidates;
for (const QString& loc : candidates) {
if (loc.isEmpty()) continue;
const QString baseName = QStringLiteral("colors_%1").arg(loc);
if (translator->load(baseName, dir + "/translations")) {
qApp->installTranslator(translator);
qInfo() << "[colors plugin] translator loaded" << baseName;
loaded = true;
break;
}
}
if (!loaded) {
qWarning() << "[colors plugin] translator not found, falling back to default language";
delete translator;
translator = nullptr;
}
}
static void onSetColor(void*, Idef0Host* host) {
if (!host || !host->selected_items || !host->set_item_color) return;
ensureTranslator(host);
Idef0SelectedItem items[64];
const size_t count = host->selected_items(host->opaque, items, 64);
if (count == 0) return;
QColor initial("#2b6ee6");
const QColor chosen = QColorDialog::getColor(initial, nullptr, QObject::tr("Select item color"));
QColorDialog::ColorDialogOptions options;
#if defined(Q_OS_WIN)
options |= QColorDialog::DontUseNativeDialog;
#endif
const QColor chosen = QColorDialog::getColor(initial, nullptr, QObject::tr("Select item color"), options);
if (!chosen.isValid()) return;
const QByteArray hex = chosen.name(QColor::HexRgb).toUtf8();
for (size_t i = 0; i < count; ++i) {
host->set_item_color(host->opaque, items[i].kind, items[i].id, hex.constData());
}
}
static void onClearColor(void*, Idef0Host* host) {
if (!host || !host->selected_items || !host->clear_item_color) return;
ensureTranslator(host);
Idef0SelectedItem items[64];
const size_t count = host->selected_items(host->opaque, items, 64);
for (size_t i = 0; i < count; ++i) {
host->clear_item_color(host->opaque, items[i].kind, items[i].id);
}
}
extern "C" bool idef0_plugin_init_v1(Idef0Host* host) {
if (!host || !host->add_menu_action) return false;
ensureTranslator(host);
qInfo() << "[colors plugin] init, plugin dir:" << (host->plugin_dir ? host->plugin_dir(host->opaque) : "(none)");
host->add_menu_action(host->opaque, QObject::tr("Set item color").toUtf8().constData(), &onSetColor, nullptr);
host->add_menu_action(host->opaque, QObject::tr("Clear item colors").toUtf8().constData(), &onClearColor, nullptr);
return true;
}
const QByteArray hex = chosen.name(QColor::HexRgb).toUtf8();
for (size_t i = 0; i < count; ++i) {
host->set_item_color(host->opaque, items[i].kind, items[i].id, hex.constData());
}
}
static void onClearColor(void*, Idef0Host* host) {
if (!host || !host->selected_items || !host->clear_item_color) return;
ensureTranslator(host);
Idef0SelectedItem items[64];
const size_t count = host->selected_items(host->opaque, items, 64);
for (size_t i = 0; i < count; ++i) {
host->clear_item_color(host->opaque, items[i].kind, items[i].id);
}
}
extern "C" bool idef0_plugin_init_v1(Idef0Host* host) {
if (!host || !host->add_menu_action) return false;
ensureTranslator(host);
qInfo() << "[colors plugin] init, plugin dir:" << (host->plugin_dir ? host->plugin_dir(host->opaque) : "(none)");
host->add_menu_action(host->opaque, QObject::tr("Set item color...").toUtf8().constData(), &onSetColor, nullptr);
host->add_menu_action(host->opaque, QObject::tr("Clear item colors").toUtf8().constData(), &onClearColor, nullptr);
return true;
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
@ -8,8 +8,8 @@
<translation>Select item color</translation>
</message>
<message>
<source>Set item color</source>
<translation>Set item color</translation>
<source>Set item color...</source>
<translation>Set item color...</translation>
</message>
<message>
<source>Clear item colors</source>
@ -17,3 +17,6 @@
</message>
</context>
</TS>

View file

@ -1,19 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr_FR">
<context>
<name>QObject</name>
<message>
<source>Select item color</source>
<translation>Choisir la couleur de l&apos;ément</translation>
<translation>Choisir la couleur de l&apos;Г©©ment</translation>
</message>
<message>
<source>Set item color</source>
<translation>Définir la couleur de l&apos;élément</translation>
<source>Set item color...</source>
<translation>Définir la couleur de l'élément...</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Réinitialiser les couleurs des éments</translation>
<translation>RГ©initialiser les couleurs des Г©©ments</translation>
</message>
</context>
</TS>

View file

@ -1,19 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="ru_RU">
<context>
<name>QObject</name>
<message>
<source>Select item color</source>
<translation>Выберите цвет элемента</translation>
<translation>Задать цвет элемента</translation>
</message>
<message>
<source>Set item color</source>
<translation>Задать цвет элемента</translation>
<source>Set item color...</source>
<translation>Задать цвет элемента...</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Сбросить цвета элементов</translation>
<translation>Очистить цвета элементов</translation>
</message>
</context>
</TS>