изменено: 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

@ -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)

121
appimage-build.nix Normal file
View file

@ -0,0 +1,121 @@
{ pkgs ? import <nixpkgs> {}
, 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 <<EOF
#!${pkgs.coreutils}/bin/env bash
set -euxo pipefail
echo "appimagetool-wrapper: runtime=$runtime pwd=\$PWD args=\$@" >&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"
''

BIN
assets/icons/erlu.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -8,7 +8,7 @@ let
in
stdenv.mkDerivation rec {
pname = "erlu_idef0_editor";
version = "0.1.0";
version = "1.0.1";
src = lib.cleanSourceWith {
src = ./.;

View file

@ -0,0 +1 @@
IDI_APP_ICON ICON "../../assets/icons/erlu.ico"

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>