From 58198c6ecd8572f1c7db42b90c8189954c9a32fc Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Wed, 4 Mar 2026 20:23:09 +0300 Subject: [PATCH] =?UTF-8?q?=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE:=20=20=20=20=20=20CMakeLists.txt=20=09=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20appimage-bu?= =?UTF-8?q?ild.nix=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB:=20=20=20=20assets/icons/erlu.ico=20=09=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20=20=20=20=20de?= =?UTF-8?q?fault.nix=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB:=20=20=20=20packaging/windows/erlu.rc=20=09=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20=20=20=20=20sr?= =?UTF-8?q?c/MainWindow.cpp=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE:=20=20=20=20=20=20src/MainWindow.h=20=09=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20=20=20=20=20sr?= =?UTF-8?q?c/items/BlockItem.cpp=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE:=20=20=20=20=20=20src/items/HeaderFooterItem.cpp?= =?UTF-8?q?=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20?= =?UTF-8?q?=20=20=20=20src/main.cpp=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE:=20=20=20=20=20=20src/plugins/color/ColorsPl?= =?UTF-8?q?ugin.cpp=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:?= =?UTF-8?q?=20=20=20=20=20=20src/plugins/color/translations/colors=5Fen.ts?= =?UTF-8?q?=20=09=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20?= =?UTF-8?q?=20=20=20=20src/plugins/color/translations/colors=5Ffr.ts=20=09?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=BE:=20=20=20=20?= =?UTF-8?q?=20=20src/plugins/color/translations/colors=5Fru.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 63 +++++++ appimage-build.nix | 121 +++++++++++++ assets/icons/erlu.ico | Bin 0 -> 32606 bytes default.nix | 2 +- packaging/windows/erlu.rc | 1 + src/MainWindow.cpp | 152 ++++++++++++---- src/MainWindow.h | 2 +- src/items/BlockItem.cpp | 3 +- src/items/HeaderFooterItem.cpp | 3 +- src/main.cpp | 3 + src/plugins/color/ColorsPlugin.cpp | 182 ++++++++++---------- src/plugins/color/translations/colors_en.ts | 9 +- src/plugins/color/translations/colors_fr.ts | 13 +- src/plugins/color/translations/colors_ru.ts | 13 +- 14 files changed, 428 insertions(+), 139 deletions(-) create mode 100644 appimage-build.nix create mode 100644 assets/icons/erlu.ico create mode 100644 packaging/windows/erlu.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index 434fd2d..90e08ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,43 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets Svg) qt_standard_project_setup() +set(APP_TS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/translations/idef0_en.ts + ${CMAKE_CURRENT_SOURCE_DIR}/translations/idef0_fr.ts + ${CMAKE_CURRENT_SOURCE_DIR}/translations/idef0_ru.ts +) + +set(COLORS_TS_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/src/plugins/color/translations/colors_en.ts + ${CMAKE_CURRENT_SOURCE_DIR}/src/plugins/color/translations/colors_fr.ts + ${CMAKE_CURRENT_SOURCE_DIR}/src/plugins/color/translations/colors_ru.ts +) + +find_program(QT_LRELEASE_EXECUTABLE NAMES lrelease-qt6 lrelease REQUIRED) + +function(generate_qm_files OUT_VAR OUT_DIR) + set(_qm_files "") + foreach(_ts_file IN LISTS ARGN) + get_filename_component(_ts_name "${_ts_file}" NAME_WE) + set(_qm_file "${OUT_DIR}/${_ts_name}.qm") + add_custom_command( + OUTPUT "${_qm_file}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${OUT_DIR}" + COMMAND "${QT_LRELEASE_EXECUTABLE}" "${_ts_file}" -qm "${_qm_file}" + DEPENDS "${_ts_file}" + VERBATIM + ) + list(APPEND _qm_files "${_qm_file}") + endforeach() + set(${OUT_VAR} "${_qm_files}" PARENT_SCOPE) +endfunction() + +generate_qm_files( + APP_QM_FILES + "${CMAKE_CURRENT_BINARY_DIR}/translations" + ${APP_TS_FILES} +) + qt_add_library(idef0_core SHARED src/MainWindow.h src/MainWindow.cpp src/items/DiagramScene.h src/items/DiagramScene.cpp @@ -25,6 +62,12 @@ qt_add_executable(erlu_idef0_editor src/main.cpp ) +if(WIN32) + target_sources(erlu_idef0_editor PRIVATE + packaging/windows/erlu.rc + ) +endif() + qt_add_resources(erlu_idef0_editor "app_icons" PREFIX "/icons" BASE assets/icons @@ -79,6 +122,17 @@ if(BUILD_COLORS_PLUGIN) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/color" INSTALL_RPATH "\$ORIGIN/../../lib" ) + generate_qm_files( + COLORS_QM_FILES + "${CMAKE_CURRENT_BINARY_DIR}/plugins/color/translations" + ${COLORS_TS_FILES} + ) +endif() + +add_custom_target(translations ALL DEPENDS ${APP_QM_FILES} ${COLORS_QM_FILES}) +add_dependencies(erlu_idef0_editor translations) +if(BUILD_COLORS_PLUGIN) + add_dependencies(colorsplugin translations) endif() set_target_properties(erlu_idef0_editor PROPERTIES @@ -101,6 +155,15 @@ if(BUILD_COLORS_PLUGIN) ) endif() +install(FILES ${APP_QM_FILES} + DESTINATION translations +) +if(BUILD_COLORS_PLUGIN) + install(FILES ${COLORS_QM_FILES} + DESTINATION plugins/color/translations + ) +endif() + if(UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/packaging/linux/erlu-idef0-editor.desktop DESTINATION share/applications) diff --git a/appimage-build.nix b/appimage-build.nix new file mode 100644 index 0000000..9d3404c --- /dev/null +++ b/appimage-build.nix @@ -0,0 +1,121 @@ +{ pkgs ? import {} +, buildColorsPlugin ? true +}: + +# Builds a distributable AppImage from the local sources (see default.nix). +# Uses linuxdeployqt + appimagetool AppImages to bundle Qt runtime bits and +# generate the final .AppImage artifact. +let + inherit (pkgs) lib; + + # Reuse the main package but skip the nixpkgs Qt wrapper so we can lay down + # the raw binaries and their closure into the AppDir. + erlu = (import ./default.nix { + inherit pkgs buildColorsPlugin; + }).overrideAttrs (_: { + dontWrapQtApps = true; + }); + + appimagetoolSrc = pkgs.fetchurl { + url = "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"; + sha256 = "sha256-ptceK2zWb46NFsN60WRliYXgz1/KqVDJCkgokMudE+A="; + }; +in + pkgs.runCommand "erlu-idef0-editor-appimage" + { + nativeBuildInputs = [ + pkgs.squashfsTools + pkgs.patchelf + pkgs.binutils + pkgs.findutils + pkgs.coreutils + pkgs.file + pkgs.imagemagick + pkgs.nix + ]; + } '' + set -euo pipefail + export HOME=$PWD/home + mkdir -p "$HOME" + + # Extract appimagetool AppImage without running it + extract_appimage() { + local src="$1" + local dest="$2" + mkdir -p "$dest" + local offset + offset=$(LC_ALL=C readelf -h "$src" | awk 'NR==13{e_shoff=$5} NR==18{e_shentsize=$5} NR==19{e_shnum=$5} END{print e_shoff+e_shentsize*e_shnum}') + unsquashfs -q -d "$dest" -o "$offset" "$src" + chmod -R u+w "$dest" + } + + extract_appimage ${appimagetoolSrc} appimagetool + + appdir=$PWD/AppDir + mkdir -p "$appdir"/usr/{bin,share/applications,share/icons/hicolor/scalable/apps,share/mime/packages} + + # Core binary and runtime data + cp ${erlu}/bin/erlu_idef0_editor "$appdir/usr/bin/erlu_idef0_editor" + chmod +x "$appdir/usr/bin/erlu_idef0_editor" + + if [ -d ${erlu}/share ]; then + cp -r ${erlu}/share/* "$appdir/usr/share/" + fi + + if [ -d ${erlu}/plugins ]; then + cp -r ${erlu}/plugins "$appdir/usr/" + fi + + # Ensure we can amend the copied tree (Nix store files are read-only) + chmod -R u+w "$appdir" + + # Copy the full dependency closure into /nix/store inside the AppDir so + # the existing RPATHs and interpreter paths keep working at runtime. + mkdir -p "$appdir/nix/store" + for p in $(nix-store -qR ${erlu}); do + cp -a "$p" "$appdir/nix/store/" + done + + # Desktop integration bits + cp ${./packaging/linux/erlu-idef0-editor.desktop} "$appdir/usr/share/applications/erlu-idef0-editor.desktop" + cp ${./assets/icons/erlu.svg} "$appdir/usr/share/icons/hicolor/scalable/apps/erlu.svg" + cp ${./packaging/linux/idef0.xml} "$appdir/usr/share/mime/packages/idef0.xml" + + # linuxdeployqt expects the .desktop and icon at the AppDir root too + cp "$appdir/usr/share/applications/erlu-idef0-editor.desktop" "$appdir/" + cp "$appdir/usr/share/icons/hicolor/scalable/apps/erlu.svg" "$appdir/erlu.svg" + + # Minimal AppRun launcher + cat > "$appdir/AppRun" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +HERE="$(dirname "$(readlink -f "$0")")" +export PATH="$HERE/usr/bin:$PATH" +exec "$HERE/usr/bin/erlu_idef0_editor" "$@" +EOF + chmod +x "$appdir/AppRun" + + # Derive the type-2 runtime from the appimagetool AppImage (avoids network fetch) + runtime=$PWD/runtime + runtime_offset=$(LC_ALL=C readelf -h ${appimagetoolSrc} | awk 'NR==13{e_shoff=$5} NR==18{e_shentsize=$5} NR==19{e_shnum=$5} END{print e_shoff+e_shentsize*e_shnum}') + head -c "$runtime_offset" ${appimagetoolSrc} > "$runtime" + chmod +x "$runtime" + + # Wrapper to force appimagetool to use the local runtime (and log its use) + cat > appimagetool-wrapper <&2 +ls -l "$PWD/appimagetool/usr/bin" >&2 || true +exec "$PWD/appimagetool/usr/bin/appimagetool" --runtime-file "$runtime" "\$@" +EOF + chmod +x appimagetool-wrapper + + export PATH="$PWD/appimagetool/usr/bin:${pkgs.file}/bin:$PATH" + + # Build the AppImage directly with appimagetool + ./appimagetool-wrapper "$appdir" + + mkdir -p "$out" + mv ./*.AppImage "$out/erlu-idef0-editor-${erlu.version}.AppImage" + '' diff --git a/assets/icons/erlu.ico b/assets/icons/erlu.ico new file mode 100644 index 0000000000000000000000000000000000000000..45425419569bdfa5aa911b15a30af1d8cdde80e5 GIT binary patch literal 32606 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 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) { diff --git a/src/MainWindow.h b/src/MainWindow.h index b3d82f4..0becf4c 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -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; } diff --git a/src/items/BlockItem.cpp b/src/items/BlockItem.cpp index 55d9eb3..df10f34 100644 --- a/src/items/BlockItem.cpp +++ b/src/items/BlockItem.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #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); diff --git a/src/items/HeaderFooterItem.cpp b/src/items/HeaderFooterItem.cpp index 8f16880..55435a7 100644 --- a/src/items/HeaderFooterItem.cpp +++ b/src/items/HeaderFooterItem.cpp @@ -11,6 +11,7 @@ #include #include #include +#include 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(); diff --git a/src/main.cpp b/src/main.cpp index 4f51be8..c655c20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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" }; diff --git a/src/plugins/color/ColorsPlugin.cpp b/src/plugins/color/ColorsPlugin.cpp index 85917fb..9e1b09f 100644 --- a/src/plugins/color/ColorsPlugin.cpp +++ b/src/plugins/color/ColorsPlugin.cpp @@ -1,94 +1,98 @@ -#include "plugins/color/ColorsPlugin.h" - -#include -#include -#include -#include -#include -#include - -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 +#include +#include +#include +#include +#include + +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; +} diff --git a/src/plugins/color/translations/colors_en.ts b/src/plugins/color/translations/colors_en.ts index 7f50bf0..febb8c8 100644 --- a/src/plugins/color/translations/colors_en.ts +++ b/src/plugins/color/translations/colors_en.ts @@ -1,4 +1,4 @@ - + @@ -8,8 +8,8 @@ Select item color - Set item color… - Set item color… + Set item color... + Set item color... Clear item colors @@ -17,3 +17,6 @@ + + + diff --git a/src/plugins/color/translations/colors_fr.ts b/src/plugins/color/translations/colors_fr.ts index 96e6bf9..142830c 100644 --- a/src/plugins/color/translations/colors_fr.ts +++ b/src/plugins/color/translations/colors_fr.ts @@ -1,19 +1,22 @@ - + QObject Select item color - Choisir la couleur de l'élément + Choisir la couleur de l'Г©lГ©ment - Set item color… - Définir la couleur de l'élément… + Set item color... + Définir la couleur de l'élément... Clear item colors - Réinitialiser les couleurs des éléments + RГ©initialiser les couleurs des Г©lГ©ments + + + diff --git a/src/plugins/color/translations/colors_ru.ts b/src/plugins/color/translations/colors_ru.ts index d7819f5..f64456e 100644 --- a/src/plugins/color/translations/colors_ru.ts +++ b/src/plugins/color/translations/colors_ru.ts @@ -1,19 +1,22 @@ - + QObject Select item color - Выберите цвет элемента + Задать цвет элемента - Set item color… - Задать цвет элемента… + Set item color... + Задать цвет элемента... Clear item colors - Сбросить цвета элементов + Очистить цвета элементов + + +