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

новый файл:    cmake/Info.plist.in
	новый файл:    default.nix
	новый файл:    desktop.nix
	новый файл:    packaging/linux/idef0-editor.desktop
	новый файл:    packaging/linux/idef0.xml
	новый файл:    packaging/windows/idef0-file-association.reg.in
	изменено:      src/MainWindow.cpp
	изменено:      src/MainWindow.h
	изменено:      src/items/ArrowItem.cpp
	изменено:      src/items/ArrowItem.h
	изменено:      src/items/BlockItem.cpp
	изменено:      src/items/BlockItem.h
	изменено:      src/items/DiagramScene.cpp
	изменено:      src/items/DiagramScene.h
	новый файл:    src/plugins/Manual.md
	новый файл:    src/plugins/PluginApi.h
	новый файл:    src/plugins/PluginManager.cpp
	новый файл:    src/plugins/PluginManager.h
	новый файл:    src/plugins/color/ColorsPlugin.cpp
	новый файл:    src/plugins/color/ColorsPlugin.h
	новый файл:    src/plugins/color/translations/colors_en.ts
	новый файл:    src/plugins/color/translations/colors_fr.ts
	новый файл:    src/plugins/color/translations/colors_ru.ts
	новый файл:    translations/README.txt
	новый файл:    translations/idef0_en.ts
	новый файл:    translations/idef0_fr.ts
	новый файл:    translations/idef0_ru.ts
This commit is contained in:
Gregory Bednov 2026-02-25 23:25:45 +03:00
commit 630c952382
28 changed files with 2720 additions and 90 deletions

View file

@ -32,7 +32,6 @@
#include <QCloseEvent>
#include <QInputDialog>
#include <QShortcut>
#include <QColorDialog>
#include <QApplication>
#include <QPalette>
#include <QStyleHints>
@ -44,10 +43,13 @@
#include <QPageSize>
#include <QPageLayout>
#include <QPainter>
#include <QCoreApplication>
#include <QtGlobal>
#include "items/BlockItem.h"
#include "plugins/PluginManager.h"
static const char* kDiagramFileFilter = "IDEF0 Diagram (*.idef0);;JSON Diagram (*.json)";
static const char* kPdfFileFilter = "PDF (*.pdf)";
static const char* kDiagramFileFilter = QT_TR_NOOP("IDEF0 Diagram (*.idef0);;JSON Diagram (*.json)");
static const char* kPdfFileFilter = QT_TR_NOOP("PDF (*.pdf)");
static QPageSize::PageSizeId pageSizeFromMeta(const QVariantMap& meta) {
const QString size = meta.value("pageSize", "A4").toString();
@ -59,7 +61,8 @@ static QPageSize::PageSizeId pageSizeFromMeta(const QVariantMap& meta) {
static QPageLayout::Orientation pageOrientationFromMeta(const QVariantMap& meta) {
const QString orient = meta.value("pageOrientation").toString();
if (orient == QObject::tr("Portrait") || orient == "Portrait") {
const QString portrait = QCoreApplication::translate("MainWindow", "Portrait");
if (orient == portrait || orient == "Portrait") {
return QPageLayout::Portrait;
}
return QPageLayout::Landscape;
@ -125,7 +128,9 @@ void MainWindow::setupActions() {
auto* editMenu = menuBar()->addMenu(tr("&Edit"));
auto* actSwapNums = new QAction(tr("Swap numbers…"), this);
auto* actCallMech = new QAction(tr("Call Mechanism add"), this);
editMenu->addAction(actSwapNums);
editMenu->addAction(actCallMech);
auto* viewMenu = menuBar()->addMenu(tr("&View"));
auto* actFit = new QAction(tr("Fit"), this);
@ -134,6 +139,7 @@ void MainWindow::setupActions() {
m_view->fitInView(m_scene->itemsBoundingRect().adjusted(-50,-50,50,50), Qt::KeepAspectRatio);
});
viewMenu->addAction(actFit);
auto* pluginsMenu = menuBar()->addMenu(tr("&Plugins"));
auto* actZoomIn = new QAction(tr("Scale +"), this);
actZoomIn->setShortcut(QKeySequence::ZoomIn);
@ -149,12 +155,6 @@ void MainWindow::setupActions() {
});
viewMenu->addAction(actZoomOut);
m_actColorfulMode = new QAction(tr("Colorful mode"), this);
m_actColorfulMode->setCheckable(true);
m_actColorfulMode->setChecked(m_welcomeState.value("colorfulMode", false).toBool());
viewMenu->addSeparator();
viewMenu->addAction(m_actColorfulMode);
m_actDarkMode = new QAction(tr("Dark mode"), this);
m_actDarkMode->setCheckable(true);
m_actDarkMode->setChecked(m_welcomeState.value("darkMode", false).toBool());
@ -165,67 +165,22 @@ void MainWindow::setupActions() {
m_actFollowSystemTheme->setChecked(m_welcomeState.value("followSystemTheme", false).toBool());
viewMenu->addAction(m_actFollowSystemTheme);
m_actArrowColor = new QAction(tr("Arrow color..."), this);
m_actBorderColor = new QAction(tr("Block border color..."), this);
m_actFontColor = new QAction(tr("Block font color..."), this);
viewMenu->addAction(m_actArrowColor);
viewMenu->addAction(m_actBorderColor);
viewMenu->addAction(m_actFontColor);
auto ensureDefaultColor = [this](const char* key, const QColor& fallback) {
QColor c(m_welcomeState.value(key).toString());
if (!c.isValid()) m_welcomeState[key] = fallback.name();
};
ensureDefaultColor("arrowColor", QColor("#2b6ee6"));
ensureDefaultColor("blockBorderColor", QColor("#0f766e"));
ensureDefaultColor("blockFontColor", QColor("#991b1b"));
auto applyModes = [this](bool mark) {
const bool colorful = m_actColorfulMode->isChecked();
const bool followSystem = m_actFollowSystemTheme->isChecked();
const bool dark = m_actDarkMode->isChecked();
m_welcomeState["colorfulMode"] = colorful;
m_welcomeState["followSystemTheme"] = followSystem;
m_welcomeState["darkMode"] = dark;
m_welcomeState["resolvedDarkMode"] = effectiveDarkMode();
m_actArrowColor->setEnabled(colorful);
m_actBorderColor->setEnabled(colorful);
m_actFontColor->setEnabled(colorful);
m_actDarkMode->setEnabled(!followSystem);
applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool());
m_scene->applyMeta(m_welcomeState);
if (mark) markDirty(true);
};
connect(m_actColorfulMode, &QAction::toggled, this, [applyModes]{ applyModes(true); });
connect(m_actDarkMode, &QAction::toggled, this, [applyModes]{ applyModes(true); });
connect(m_actFollowSystemTheme, &QAction::toggled, this, [applyModes]{ applyModes(true); });
connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this, [applyModes](Qt::ColorScheme){
applyModes(false);
});
connect(m_actArrowColor, &QAction::triggered, this, [this, applyModes]{
const QColor cur(m_welcomeState.value("arrowColor").toString());
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#2b6ee6"), this, tr("Arrow color"));
if (!chosen.isValid()) return;
m_welcomeState["arrowColor"] = chosen.name();
applyModes(true);
});
connect(m_actBorderColor, &QAction::triggered, this, [this, applyModes]{
const QColor cur(m_welcomeState.value("blockBorderColor").toString());
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#0f766e"), this, tr("Block border color"));
if (!chosen.isValid()) return;
m_welcomeState["blockBorderColor"] = chosen.name();
applyModes(true);
});
connect(m_actFontColor, &QAction::triggered, this, [this, applyModes]{
const QColor cur(m_welcomeState.value("blockFontColor").toString());
const QColor chosen = QColorDialog::getColor(cur.isValid() ? cur : QColor("#991b1b"), this, tr("Block font color"));
if (!chosen.isValid()) return;
m_welcomeState["blockFontColor"] = chosen.name();
applyModes(true);
});
applyModes(false);
connect(actSwapNums, &QAction::triggered, this, [this]{
@ -327,6 +282,59 @@ void MainWindow::setupActions() {
if (dlg.exec() != QDialog::Accepted) return;
exportPdf(scope->currentIndex() == 1, pageNumbers->isChecked());
});
connect(actCallMech, &QAction::triggered, this, [this]{
if (!m_scene) return;
const auto sel = m_scene->selectedItems();
if (sel.size() != 1) {
QMessageBox::information(this, tr("Call Mechanism"), tr("Select exactly one block to add a call mechanism."));
return;
}
auto* target = qgraphicsitem_cast<BlockItem*>(sel.first());
if (!target) {
QMessageBox::information(this, tr("Call Mechanism"), tr("Select a block to add a call mechanism."));
return;
}
if (m_scene->hasCallMechanism(target)) {
QMessageBox::information(this, tr("Call Mechanism"), tr("Call mechanism already exists for this block."));
return;
}
struct Option { QString display; BlockItem* block = nullptr; };
QVector<Option> options;
for (QGraphicsItem* it : m_scene->items()) {
auto* b = qgraphicsitem_cast<BlockItem*>(it);
if (!b) continue;
const QString n = b->number();
if (n.isEmpty() || n.compare("A0", Qt::CaseInsensitive) == 0) continue;
if (b == target) continue;
Option opt{QStringLiteral("%1 — %2").arg(n, b->title()), b};
options.push_back(opt);
}
std::sort(options.begin(), options.end(), [](const Option& a, const Option& b){ return a.display.localeAwareCompare(b.display) < 0; });
if (options.isEmpty()) {
QMessageBox::information(this, tr("Call Mechanism"), tr("No eligible target blocks found."));
return;
}
QStringList displayList;
for (const auto& opt : options) displayList << opt.display;
bool ok = false;
const QString choice = QInputDialog::getItem(this, tr("Call Mechanism"), tr("Choose target block:"), displayList, 0, false, &ok);
if (!ok || choice.isEmpty()) return;
BlockItem* refBlock = nullptr;
for (const auto& opt : options) {
if (opt.display == choice) { refBlock = opt.block; break; }
}
if (!refBlock) return;
const QString label = QStringLiteral("%1 %2").arg(refBlock->number(), refBlock->title());
if (!m_scene->startCallMechanism(target, refBlock, label)) {
QMessageBox::information(this, tr("Call Mechanism"), tr("Call mechanism already exists for this block or selection is invalid."));
return;
}
statusBar()->showMessage(tr("Click bottom frame to place call mechanism arrow. Esc to cancel."), 5000);
});
m_pluginManager = new PluginManager(this, pluginsMenu, this);
m_pluginManager->loadPlugins();
}
static QString currencySymbolForLocale(const QLocale& loc) {
@ -358,15 +366,15 @@ bool MainWindow::showWelcome() {
// General tab
auto* general = new QWidget(dlg);
auto* genForm = new QFormLayout(general);
auto* author = new QLineEdit(general);
author->setPlaceholderText(tr("John Doe"));
author->setText(m_welcomeState.value("author").toString());
auto* title = new QLineEdit(general);
title->setPlaceholderText(tr("Main process"));
title->setText(m_welcomeState.value("title").toString());
auto* organization = new QLineEdit(general);
organization->setPlaceholderText(tr("Example.Org Inc."));
organization->setText(m_welcomeState.value("organization").toString());
auto* author = new QLineEdit(general);
author->setPlaceholderText(tr("John Doe"));
author->setText(m_welcomeState.value("author").toString());
auto* initials = new QLineEdit(general);
initials->setPlaceholderText(tr("JD"));
initials->setText(m_welcomeState.value("initials").toString());
@ -378,6 +386,9 @@ bool MainWindow::showWelcome() {
genForm->addRow(tr("Organization:"), organization);
genForm->addRow(tr("Author:"), author);
genForm->addRow(tr("Author's initials:"), initials);
setTabOrder(title, organization);
setTabOrder(organization, author);
setTabOrder(author, initials);
general->setLayout(genForm);
tabs->addTab(general, tr("General"));
@ -557,21 +568,13 @@ bool MainWindow::loadDiagramFromPath(const QString& path) {
if (root.contains("meta")) {
m_welcomeState = root.value("meta").toMap();
}
if (m_actColorfulMode && m_actDarkMode && m_actFollowSystemTheme) {
const QSignalBlocker b1(m_actColorfulMode);
if (m_actDarkMode && m_actFollowSystemTheme) {
const QSignalBlocker b2(m_actDarkMode);
const QSignalBlocker b3(m_actFollowSystemTheme);
m_actColorfulMode->setChecked(m_welcomeState.value("colorfulMode", false).toBool());
m_actDarkMode->setChecked(m_welcomeState.value("darkMode", false).toBool());
m_actFollowSystemTheme->setChecked(m_welcomeState.value("followSystemTheme", false).toBool());
m_actDarkMode->setEnabled(!m_actFollowSystemTheme->isChecked());
}
if (m_actArrowColor && m_actBorderColor && m_actFontColor) {
const bool colorful = m_welcomeState.value("colorfulMode", false).toBool();
m_actArrowColor->setEnabled(colorful);
m_actBorderColor->setEnabled(colorful);
m_actFontColor->setEnabled(colorful);
}
m_welcomeState["resolvedDarkMode"] = effectiveDarkMode();
applyAppPalette(m_welcomeState.value("resolvedDarkMode").toBool());
m_scene->applyMeta(m_welcomeState);

View file

@ -8,12 +8,14 @@ class QGraphicsView;
class DiagramScene;
class QDialog;
class QAction;
class PluginManager;
class MainWindow final : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(const QString& startupPath = QString(), QWidget* parent = nullptr);
bool openDiagramPath(const QString& path);
DiagramScene* scene() const { return m_scene; }
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
@ -26,12 +28,9 @@ private:
QString m_currentFile;
bool m_dirty = false;
bool m_pinchHandled = false;
QAction* m_actColorfulMode = nullptr;
QAction* m_actFollowSystemTheme = nullptr;
QAction* m_actDarkMode = nullptr;
QAction* m_actArrowColor = nullptr;
QAction* m_actBorderColor = nullptr;
QAction* m_actFontColor = nullptr;
PluginManager* m_pluginManager = nullptr;
void setupUi();
void setupActions();

View file

@ -12,6 +12,7 @@
#include <QLineEdit>
#include <QDebug>
#include <QObject>
#include <QCoreApplication>
#include "DiagramScene.h"
#include <algorithm>
#include <QSet>
@ -35,6 +36,8 @@ QColor ArrowItem::s_textColor = QColor(10, 10, 10);
ArrowItem::ArrowItem(QGraphicsItem* parent)
: QGraphicsPathItem(parent)
{
static int s_nextId = 1;
m_internalId = s_nextId++;
QPen pen(s_lineColor);
pen.setWidthF(1.4);
pen.setCapStyle(Qt::RoundCap);
@ -101,6 +104,18 @@ void ArrowItem::setLabelSource(ArrowItem* src) {
m_labelSource = src;
}
void ArrowItem::setCustomColor(const QColor& color) {
if (!color.isValid()) return;
m_customColor = color;
updatePath();
}
void ArrowItem::clearCustomColor() {
if (!m_customColor) return;
m_customColor.reset();
updatePath();
}
ArrowItem* ArrowItem::labelSourceRoot() const {
const ArrowItem* cur = this;
QSet<const ArrowItem*> visited;
@ -674,10 +689,13 @@ void ArrowItem::updateLabelItem(const QVector<QPointF>& pts) {
// Обновляем маршрут ортогонально, добавляя/схлопывая сегменты при необходимости.
void ArrowItem::updatePath() {
QPen themedPen = pen();
themedPen.setColor(s_lineColor);
themedPen.setColor(m_customColor.value_or(s_lineColor));
if (m_isCallMechanism) {
themedPen.setStyle(Qt::DashLine);
}
setPen(themedPen);
if (m_labelItem) {
m_labelItem->setBrush(s_textColor);
m_labelItem->setBrush(m_customColor.value_or(s_textColor));
}
const QRectF oldSceneRect = mapRectToScene(boundingRect());
@ -912,7 +930,13 @@ void ArrowItem::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
void ArrowItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* e) {
if (e->button() == Qt::LeftButton && !m_labelLocked) {
bool ok = false;
const QString text = QInputDialog::getText(nullptr, QObject::tr("Arrow label"), QObject::tr("Label:"), QLineEdit::Normal, m_label, &ok);
const QString text = QInputDialog::getText(
nullptr,
QCoreApplication::translate("ArrowItem", "Arrow label"),
QCoreApplication::translate("ArrowItem", "Label:"),
QLineEdit::Normal,
m_label,
&ok);
if (ok) {
setLabel(text);
setLabelHidden(false);

View file

@ -49,6 +49,7 @@ public:
qreal topOffset() const { return m_topOffset; }
qreal bottomOffset() const { return m_bottomOffset; }
int type() const override { return Type; }
int internalId() const { return m_internalId; }
bool isInterface() const { return m_isInterface; }
bool isInterfaceStub() const { return m_isInterface && m_interfaceStubOnly; }
bool isLabelLocked() const { return m_labelLocked; }
@ -58,6 +59,13 @@ public:
void resetInterfaceStub();
void setLabelLocked(bool locked);
static void setVisualTheme(const QColor& lineColor, const QColor& textColor);
void setCustomColor(const QColor& color);
void clearCustomColor();
std::optional<QColor> customColor() const { return m_customColor; }
void setCallMechanism(bool v) { m_isCallMechanism = v; }
bool isCallMechanism() const { return m_isCallMechanism; }
void setCallRefId(int id) { m_callRefId = id; }
int callRefId() const { return m_callRefId; }
void updatePath();
std::optional<QPointF> hitTest(const QPointF& scenePos, qreal radius) const;
@ -87,6 +95,10 @@ private:
ArrowItem* m_labelSource = nullptr;
static QColor s_lineColor;
static QColor s_textColor;
std::optional<QColor> m_customColor;
int m_internalId = -1;
bool m_isCallMechanism = false;
int m_callRefId = -1;
DragPart m_dragPart = DragPart::None;
QPointF m_lastDragScenePos;

View file

@ -32,6 +32,9 @@ BlockItem::BlockItem(QString title, QGraphicsItem* parent, int id)
m_rect(0, 0, 200, 100),
m_id(id)
{
if (m_title.isEmpty()) {
m_title = tr("Function");
}
setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges);
}
@ -45,11 +48,15 @@ void BlockItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
// фон
p->setPen(Qt::NoPen);
p->setBrush(isSelected() ? s_selectedBackgroundColor : s_backgroundColor);
const QColor borderColor = m_customColor.value_or(s_borderColor);
const QColor fontColor = m_customColor.value_or(s_fontColor);
const QColor foregroundColor = m_customColor.value_or(s_foregroundColor);
const QColor selectedBg = m_customColor ? m_customColor->lighter(160) : s_selectedBackgroundColor;
p->setBrush(isSelected() ? selectedBg : s_backgroundColor);
p->drawRoundedRect(m_rect, 6, 6);
// рамка
QPen pen(s_borderColor);
QPen pen(borderColor);
pen.setWidthF(1.5);
p->setPen(pen);
p->setBrush(Qt::NoBrush);
@ -65,22 +72,22 @@ void BlockItem::paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) {
fold.closeSubpath();
p->setBrush(s_backgroundColor.darker(108));
p->drawPath(fold);
p->setPen(QPen(s_borderColor.lighter(130), 1.0));
p->setPen(QPen(borderColor.lighter(130), 1.0));
p->drawLine(m_rect.topLeft() + QPointF(ear, 0), m_rect.topLeft() + QPointF(0, ear));
}
// заголовок
p->setPen(s_fontColor);
p->setPen(fontColor);
p->drawText(m_rect.adjusted(10, 8, -10, -8), Qt::AlignLeft | Qt::AlignTop, m_title);
if (!m_number.isEmpty()) {
QFont f = p->font();
f.setBold(true);
p->setFont(f);
p->setPen(s_foregroundColor);
p->setPen(foregroundColor);
p->drawText(m_rect.adjusted(8, 4, -8, -8), Qt::AlignRight | Qt::AlignBottom, m_number);
}
if (m_price.has_value()) {
p->setPen(s_foregroundColor);
p->setPen(foregroundColor);
p->drawText(m_rect.adjusted(8, 4, -8, -8), Qt::AlignLeft | Qt::AlignBottom, formattedPrice());
}
}
@ -89,12 +96,14 @@ void BlockItem::setTitle(const QString& t) {
if (t == m_title) return;
m_title = t;
update();
emit titleChanged(m_title);
}
void BlockItem::setNumber(const QString& n) {
if (n == m_number) return;
m_number = n;
update();
emit numberChanged(m_number);
}
void BlockItem::setPrice(std::optional<qreal> price) {
@ -103,6 +112,18 @@ void BlockItem::setPrice(std::optional<qreal> price) {
update();
}
void BlockItem::setCustomColor(const QColor& color) {
if (!color.isValid()) return;
m_customColor = color;
update();
}
void BlockItem::clearCustomColor() {
if (!m_customColor) return;
m_customColor.reset();
update();
}
void BlockItem::setCurrencyFormat(const QString& symbol, const QString& placement) {
QString effectiveSymbol = symbol;
if (effectiveSymbol.isEmpty()) {

View file

@ -12,7 +12,7 @@ public:
enum class Port { Input, Control, Output, Mechanism };
enum { Type = UserType + 1 };
explicit BlockItem(QString title = "Function", QGraphicsItem* parent = nullptr, int id = -1);
explicit BlockItem(QString title = QString(), QGraphicsItem* parent = nullptr, int id = -1);
QRectF boundingRect() const override;
void paint(QPainter* p, const QStyleOptionGraphicsItem*, QWidget*) override;
@ -43,9 +43,14 @@ public:
const QColor& border,
const QColor& font,
const QColor& selectedBackground);
void setCustomColor(const QColor& color);
void clearCustomColor();
std::optional<QColor> customColor() const { return m_customColor; }
signals:
void geometryChanged();
void titleChanged(const QString& title);
void numberChanged(const QString& number);
protected:
QVariant itemChange(GraphicsItemChange change, const QVariant& value) override;
@ -62,6 +67,7 @@ private:
bool m_hasDecomposition = false;
QSet<ArrowItem*> m_arrows;
std::optional<qreal> m_price;
std::optional<QColor> m_customColor;
static QString s_currencySymbol;
static QString s_currencyPlacement;

View file

@ -37,9 +37,10 @@ DiagramScene::DiagramScene(QObject* parent)
}
BlockItem* DiagramScene::createBlockAt(const QPointF& scenePos) {
auto* b = new BlockItem("Function", nullptr, m_nextBlockId++);
auto* b = new BlockItem(QString(), nullptr, m_nextBlockId++);
addItem(b);
b->setNumber(assignNumber(b));
connectBlockSignals(b);
const QRectF r = m_contentRect.isNull() ? sceneRect() : m_contentRect;
const QPointF desired = scenePos - QPointF(100, 50); // центрируем
const QRectF br = b->boundingRect().translated(desired);
@ -59,6 +60,7 @@ BlockItem* DiagramScene::createBlockWithId(const QPointF& scenePos, int id, cons
addItem(b);
b->setPos(scenePos);
if (b->number().isEmpty()) b->setNumber(assignNumber(b));
connectBlockSignals(b);
return b;
}
@ -155,9 +157,9 @@ void DiagramScene::updateMeta(const QVariantMap& patch) {
}
QString DiagramScene::currentNodeLabel() const {
if (m_currentBlockId < 0) return QStringLiteral("TOP");
if (m_currentBlockId < 0) return tr("TOP");
if (!m_currentPrefix.isEmpty()) return m_currentPrefix;
return QStringLiteral("A0");
return tr("A0");
}
QString DiagramScene::currentDiagramTitle() const {
@ -594,6 +596,9 @@ DiagramScene::Snapshot DiagramScene::captureSnapshot() const {
blk.hasDecomp = hasDecomp;
blk.number = b->number();
blk.price = b->price();
if (auto col = b->customColor()) {
blk.color = col->name();
}
s.blocks.push_back(blk);
}
}
@ -621,9 +626,14 @@ DiagramScene::Snapshot DiagramScene::captureSnapshot() const {
ar.isInterface = a->isInterface();
ar.isInterfaceStub = a->isInterfaceStub();
ar.labelLocked = a->isLabelLocked();
ar.callMechanism = a->isCallMechanism();
ar.callRefId = a->callRefId();
if (a->interfaceEdge()) {
ar.interfaceEdge = encodeEp(*a->interfaceEdge());
}
if (auto col = a->customColor()) {
ar.color = col->name();
}
s.arrows.push_back(std::move(ar));
}
}
@ -655,6 +665,10 @@ void DiagramScene::restoreSnapshot(const Snapshot& snap, bool resetHistoryState)
if (b.price.has_value()) {
blk->setPrice(b.price);
}
if (b.color.has_value()) {
const QColor col(*b.color);
if (col.isValid()) blk->setCustomColor(col);
}
blockMap.insert(b.id, blk);
m_nextBlockId = std::max(m_nextBlockId, b.id + 1);
}
@ -710,10 +724,17 @@ for (const auto& j : snap.junctions) {
ar->setLabelHidden(a.labelHidden);
ar->setLabelInherited(a.labelInherited);
ar->setLabelLocked(a.labelLocked);
ar->setCallMechanism(a.callMechanism);
ar->setCallRefId(a.callRefId);
if (a.color.has_value()) {
const QColor col(*a.color);
if (col.isValid()) ar->setCustomColor(col);
}
ar->finalize();
}
m_restoringSnapshot = false;
updateCallMechanismLabels();
if (resetHistoryState) {
resetHistory(captureSnapshot());
}
@ -742,9 +763,59 @@ void DiagramScene::scheduleSnapshot() {
QTimer::singleShot(0, this, [this]{
m_snapshotScheduled = false;
pushSnapshot();
updateCallMechanismLabels();
});
}
void DiagramScene::connectBlockSignals(BlockItem* b) {
if (!b) return;
connect(b, &BlockItem::titleChanged, this, [this]{ updateCallMechanismLabels(); });
connect(b, &BlockItem::numberChanged, this, [this]{ updateCallMechanismLabels(); });
}
void DiagramScene::updateCallMechanismLabels() {
QHash<int, BlockItem*> blockById;
for (QGraphicsItem* it : items()) {
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
blockById.insert(b->id(), b);
}
}
for (QGraphicsItem* it : items()) {
auto* a = qgraphicsitem_cast<ArrowItem*>(it);
if (!a || !a->isCallMechanism()) continue;
BlockItem* ref = blockById.value(a->callRefId(), nullptr);
if (!ref) continue;
const QString num = ref->number();
const QString title = ref->title();
const QString label = num.isEmpty() ? title : QStringLiteral("%1 %2").arg(num, title);
a->setLabel(label);
a->setLabelHidden(false);
}
}
void DiagramScene::purgeBrokenCallMechanisms() {
QSet<BlockItem*> blocks;
for (QGraphicsItem* it : items()) {
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) blocks.insert(b);
}
QVector<ArrowItem*> toRemove;
for (QGraphicsItem* it : items()) {
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
if (!a->isCallMechanism()) continue;
auto from = a->from();
auto to = a->to();
bool ok = true;
if (from.block && !blocks.contains(from.block)) ok = false;
if (to.block && !blocks.contains(to.block)) ok = false;
if (!ok) toRemove.push_back(a);
}
}
for (ArrowItem* a : toRemove) {
removeItem(a);
delete a;
}
}
void DiagramScene::undo() {
if (m_historyIndex <= 0) return;
m_historyIndex -= 1;
@ -1022,6 +1093,7 @@ void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* e) {
maybeSnapshotMovedItems();
m_itemDragActive = false;
}
purgeBrokenCallMechanisms();
QGraphicsScene::mouseReleaseEvent(e);
}
@ -1056,6 +1128,47 @@ void DiagramScene::keyPressEvent(QKeyEvent* e) {
QGraphicsScene::keyPressEvent(e);
}
bool DiagramScene::hasCallMechanism(const BlockItem* target) const {
if (!target) return false;
const auto all = items();
for (QGraphicsItem* it : all) {
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
if (!a->isCallMechanism()) continue;
const auto from = a->from();
if (from.block && from.block == target) {
return true;
}
}
}
return false;
}
bool DiagramScene::startCallMechanism(BlockItem* origin, BlockItem* refBlock, const QString& label) {
if (!origin || !refBlock) return false;
if (origin == refBlock) return false;
if (hasCallMechanism(origin)) return false;
cancelCurrentDrag();
auto* a = new ArrowItem();
addItem(a);
ArrowItem::Endpoint from;
from.scenePos = origin->portScenePos(BlockItem::Port::Mechanism) + QPointF(0, 40);
from.port = BlockItem::Port::Mechanism;
ArrowItem::Endpoint to;
to.block = origin;
to.port = BlockItem::Port::Mechanism;
a->setFrom(from);
a->setTo(to);
a->setCallMechanism(true);
a->setCallRefId(refBlock->id());
a->setLabel(label);
a->setLabelHidden(false);
a->setLabelLocked(true);
a->setLabelInherited(false);
a->finalize();
pushSnapshot();
return true;
}
void DiagramScene::cancelCurrentDrag() {
if (m_dragArrow) {
if (m_dragArrow->isInterface()) {
@ -1119,7 +1232,10 @@ void DiagramScene::deleteSelection() {
delete it;
}
if (!toDelete.isEmpty()) pushSnapshot();
if (!toDelete.isEmpty()) {
purgeBrokenCallMechanisms();
pushSnapshot();
}
}
void DiagramScene::requestSnapshot() {
@ -1278,6 +1394,7 @@ QVariantMap DiagramScene::exportToVariant() {
o["hasDecomp"] = b.hasDecomp;
o["number"] = b.number;
if (b.price.has_value()) o["price"] = *b.price;
if (b.color.has_value()) o["color"] = *b.color;
blocks.append(o);
}
root["blocks"] = blocks;
@ -1307,6 +1424,9 @@ QVariantMap DiagramScene::exportToVariant() {
o["isInterfaceStub"] = a.isInterfaceStub;
o["labelLocked"] = a.labelLocked;
o["interfaceEdge"] = endpointToJson(a.interfaceEdge);
o["callMechanism"] = a.callMechanism;
o["callRefId"] = a.callRefId;
if (a.color.has_value()) o["color"] = *a.color;
arrows.append(o);
}
root["arrows"] = arrows;
@ -1340,7 +1460,7 @@ bool DiagramScene::importFromVariant(const QVariantMap& map) {
Snapshot snap;
for (const auto& vb : root.value("blocks").toArray()) {
const auto o = vb.toObject();
const auto posArr = o.value("pos").toArray();
const auto posArr = o.value("pos").toArray();
if (posArr.size() != 2) continue;
DiagramScene::Snapshot::Block blk;
blk.id = o.value("id").toInt();
@ -1351,6 +1471,8 @@ bool DiagramScene::importFromVariant(const QVariantMap& map) {
if (o.contains("price") && !o.value("price").isNull()) {
blk.price = o.value("price").toDouble();
}
const QString blkColor = o.value("color").toString();
if (!blkColor.isEmpty()) blk.color = blkColor;
snap.blocks.push_back(blk);
}
for (const auto& vj : root.value("junctions").toArray()) {
@ -1376,6 +1498,10 @@ bool DiagramScene::importFromVariant(const QVariantMap& map) {
ar.isInterfaceStub = o.value("isInterfaceStub").toBool(false);
ar.labelLocked = o.value("labelLocked").toBool(false);
ar.interfaceEdge = endpointFromJson(o.value("interfaceEdge").toObject());
ar.callMechanism = o.value("callMechanism").toBool(false);
ar.callRefId = o.value("callRefId").toInt(-1);
const QString arrowColor = o.value("color").toString();
if (!arrowColor.isEmpty()) ar.color = arrowColor;
snap.arrows.push_back(ar);
}
return snap;

View file

@ -23,12 +23,16 @@ public:
QRectF contentRect() const { return m_contentRect; }
QString currentNodeLabel() const;
QString currentDiagramTitle() const;
int currentBlockId() const { return m_currentBlockId; }
bool goDownIntoSelected();
bool goDownIntoBlock(BlockItem* b);
bool goUp();
void propagateLabelFrom(ArrowItem* root);
QVariantMap exportToVariant();
bool importFromVariant(const QVariantMap& map);
bool startCallMechanism(BlockItem* origin, BlockItem* refBlock, const QString& label);
bool hasCallMechanism(const BlockItem* origin) const;
void updateCallMechanismLabels();
signals:
void changed();
void metaChanged(const QVariantMap& meta);
@ -48,7 +52,7 @@ private:
std::optional<QPointF> localPos;
std::optional<QPointF> scenePos;
};
struct Block { int id; QString title; QPointF pos; bool hasDecomp = false; QString number; std::optional<qreal> price; };
struct Block { int id; QString title; QPointF pos; bool hasDecomp = false; QString number; std::optional<qreal> price; std::optional<QString> color; };
struct Junction { int id; QPointF pos; };
struct Arrow {
Endpoint from;
@ -64,6 +68,9 @@ private:
bool isInterfaceStub = false;
bool labelLocked = false;
Endpoint interfaceEdge;
std::optional<QString> color;
bool callMechanism = false;
int callRefId = -1;
};
QVector<Block> blocks;
QVector<Junction> junctions;
@ -113,6 +120,8 @@ private:
QString currentPathKey() const;
QString childKeyFor(int blockId) const;
QString assignNumber(BlockItem* b);
void connectBlockSignals(BlockItem* b);
void purgeBrokenCallMechanisms();
enum class Edge { None, Left, Right, Top, Bottom };
Edge hitTestEdge(const QPointF& scenePos, QPointF* outScenePoint = nullptr) const;

79
src/plugins/Manual.md Normal file
View file

@ -0,0 +1,79 @@
# Plugin Development Manual
This application loads runtime plugins as plain shared libraries located in `plugins/`. Plugins are isolated from internal types and talk to the host via the C ABI defined in `PluginApi.h`.
## Plugin API
- Header: `src/plugins/PluginApi.h`
- Entry point: a single exported C function
```c
extern "C" bool idef0_plugin_init_v1(Idef0Host* host);
```
- You **must not** include or rely on any other app headers.
`Idef0Host` exposes function pointers:
| Function | Purpose |
| --- | --- |
| `size_t selected_items(void* opaque, Idef0SelectedItem* out, size_t max)` | Returns count of selected items, fills kind (`IDEF0_ITEM_BLOCK` / `IDEF0_ITEM_ARROW`) and `id`. |
| `bool set_item_color(void* opaque, int kind, int id, const char* color_hex)` | Set per-item color override as `#rrggbb`. |
| `bool clear_item_color(void* opaque, int kind, int id)` | Clear color override. |
| `void add_menu_action(void* opaque, const char* text, Idef0ActionCallback cb, void* user_data)` | Add an action under the Plugins menu; `cb` gets `(user_data, host)`. |
| `const char* plugin_dir(void* opaque)` | Absolute directory of this plugin library (use to load plugin-local resources/translations). |
## Minimal Example (C++)
```cpp
#include "plugins/PluginApi.h"
#include <QObject> // for tr(), optional
#include <QColorDialog> // optional UI
static void onSetRed(void*, Idef0Host* host) {
Idef0SelectedItem items[32];
size_t n = host->selected_items(host->opaque, items, 32);
for (size_t i = 0; i < n; ++i) {
host->set_item_color(host->opaque, items[i].kind, items[i].id, "#ff0000");
}
}
extern "C" bool idef0_plugin_init_v1(Idef0Host* host) {
if (!host || !host->add_menu_action) return false;
host->add_menu_action(host->opaque, "Set red", &onSetRed, nullptr);
return true;
}
```
## Build with CMake
```cmake
cmake_minimum_required(VERSION 3.21)
project(myplugin LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Widgets) # only if you use Qt UI
add_library(myplugin MODULE myplugin.cpp)
target_include_directories(myplugin PRIVATE /path/to/app/src) # for plugins/PluginApi.h
target_link_libraries(myplugin PRIVATE Qt6::Widgets) # optional
set_target_properties(myplugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins"
INSTALL_RPATH "\$ORIGIN/../lib"
)
install(TARGETS myplugin LIBRARY DESTINATION plugins RUNTIME DESTINATION plugins)
```
If you dont need Qt widgets, omit `find_package(Qt6...)` and link against nothing extra.
## Deployment
- Place the built library next to the app in `plugins/`:
- Linux: `plugins/libmyplugin.so`
- Windows: `plugins/myplugin.dll`
- macOS: `plugins/libmyplugin.dylib`
- Ensure it is built against the same `PluginApi.h` version as the app.
## Best Practices
- Keep plugin state in your own globals; the host provides only opaque pointers.
- Validate inputs (e.g., color strings) and return early on null host functions.
- Use `selected_items` first, then act via `set_item_color`/`clear_item_color`.
- Add menu actions via `add_menu_action` for user-visible triggers. The host calls your callbacks on activation.

37
src/plugins/PluginApi.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
extern "C" {
enum Idef0ItemKind {
IDEF0_ITEM_BLOCK = 1,
IDEF0_ITEM_ARROW = 2
};
struct Idef0SelectedItem {
int kind; // Idef0ItemKind
int id;
};
struct Idef0Host;
typedef void (*Idef0ActionCallback)(void* user_data, Idef0Host* host);
struct Idef0Host {
void* opaque; // host-specific pointer
// Fills up to max entries, returns actual count.
size_t (*selected_items)(void* opaque, Idef0SelectedItem* out, size_t max);
// Set hex color (#rrggbb) for item id/kind. Returns true on success.
bool (*set_item_color)(void* opaque, int kind, int id, const char* color_hex);
// Clear override color.
bool (*clear_item_color)(void* opaque, int kind, int id);
// Add menu action under Plugins; callback invoked on trigger.
void (*add_menu_action)(void* opaque, const char* text, Idef0ActionCallback cb, void* user_data);
// Absolute directory where this plugin library resides.
const char* (*plugin_dir)(void* opaque);
};
// Plugin entry point must be exported with this name.
typedef bool (*Idef0PluginInit)(Idef0Host* host);
} // extern "C"

View file

@ -0,0 +1,175 @@
#include "plugins/PluginManager.h"
#include <QCoreApplication>
#include <QDir>
#include <QMenu>
#include <QDebug>
#include <QFileInfo>
#include <QColor>
#include "MainWindow.h"
#include "items/DiagramScene.h"
#include "items/BlockItem.h"
#include "items/ArrowItem.h"
PluginManager::PluginManager(MainWindow* window, QMenu* pluginsMenu, QObject* parent)
: QObject(parent),
m_window(window),
m_pluginsMenu(pluginsMenu)
{
}
QMenu* PluginManager::pluginsMenu() const {
return m_pluginsMenu;
}
DiagramScene* PluginManager::currentScene() const {
return m_window ? m_window->scene() : nullptr;
}
QWidget* PluginManager::window() const {
return m_window;
}
QStringList PluginManager::pluginSearchPaths() const {
QStringList paths;
const QString appDir = QCoreApplication::applicationDirPath();
paths << (appDir + "/plugins");
paths << (appDir + "/../plugins");
return paths;
}
void PluginManager::loadPlugins() {
const QStringList roots = pluginSearchPaths();
qInfo() << "PluginManager scanning paths:" << roots;
QStringList queue = roots;
while (!queue.isEmpty()) {
const QString dirPath = queue.takeFirst();
QDir dir(dirPath);
if (!dir.exists()) continue;
// Enqueue subdirectories for recursive scan (one level deep is enough here).
const auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo& di : subdirs) {
queue << di.absoluteFilePath();
}
// Load plugins from this directory.
const auto entries = dir.entryInfoList(QDir::Files);
for (const QFileInfo& fi : entries) {
if (!QLibrary::isLibrary(fi.absoluteFilePath())) continue;
auto lib = std::make_unique<QLibrary>(fi.absoluteFilePath());
if (!lib->load()) {
qWarning() << "Plugin load failed:" << fi.absoluteFilePath() << lib->errorString();
continue;
}
auto init = reinterpret_cast<Idef0PluginInit>(lib->resolve("idef0_plugin_init_v1"));
if (!init) {
qWarning() << "Plugin missing entrypoint idef0_plugin_init_v1:" << fi.absoluteFilePath();
continue;
}
auto ctx = makeContext(fi.absoluteFilePath());
if (!ctx) continue;
if (!init(&ctx->host)) {
qWarning() << "Plugin init returned false:" << fi.absoluteFilePath();
continue;
}
qInfo() << "Plugin loaded:" << fi.absoluteFilePath();
m_plugins.emplace_back(std::move(lib), std::move(ctx));
}
}
}
std::unique_ptr<PluginManager::HostContext> PluginManager::makeContext(const QString& pluginPath) {
auto ctx = std::make_unique<HostContext>();
ctx->manager = this;
ctx->pluginPath = pluginPath;
ctx->pluginDirUtf8 = QFileInfo(pluginPath).absolutePath().toUtf8();
ctx->host.opaque = ctx.get();
ctx->host.selected_items = &PluginManager::apiSelectedItems;
ctx->host.set_item_color = &PluginManager::apiSetItemColor;
ctx->host.clear_item_color = &PluginManager::apiClearItemColor;
ctx->host.add_menu_action = &PluginManager::apiAddMenuAction;
ctx->host.plugin_dir = &PluginManager::apiPluginDir;
return ctx;
}
void PluginManager::addAction(const QString& text, Idef0ActionCallback cb, void* userData, HostContext* ctx) {
if (!m_pluginsMenu || !cb || !ctx) return;
QAction* act = m_pluginsMenu->addAction(text);
connect(act, &QAction::triggered, this, [cb, userData, ctx]{
cb(userData, &ctx->host);
});
}
size_t PluginManager::apiSelectedItems(void* opaque, Idef0SelectedItem* out, size_t max) {
auto* ctx = static_cast<HostContext*>(opaque);
if (!ctx || !ctx->manager || !ctx->manager->m_window || !out || max == 0) return 0;
DiagramScene* scene = ctx->manager->currentScene();
if (!scene) return 0;
const auto sel = scene->selectedItems();
const size_t selCount = static_cast<size_t>(sel.size());
const size_t count = std::min(selCount, max);
for (size_t i = 0; i < count; ++i) {
Idef0SelectedItem item{0, -1};
if (auto* b = qgraphicsitem_cast<BlockItem*>(sel[int(i)])) {
item.kind = IDEF0_ITEM_BLOCK;
item.id = b->id();
} else if (auto* a = qgraphicsitem_cast<ArrowItem*>(sel[int(i)])) {
item.kind = IDEF0_ITEM_ARROW;
item.id = a->internalId();
}
out[i] = item;
}
return count;
}
static bool applyColorToScene(DiagramScene* scene, int kind, int id, const QColor& col, bool clear) {
if (!scene) return false;
bool changed = false;
const auto items = scene->items();
for (QGraphicsItem* it : items) {
if (kind == IDEF0_ITEM_BLOCK) {
if (auto* b = qgraphicsitem_cast<BlockItem*>(it)) {
if (b->id() == id) {
clear ? b->clearCustomColor() : b->setCustomColor(col);
changed = true;
}
}
} else if (kind == IDEF0_ITEM_ARROW) {
if (auto* a = qgraphicsitem_cast<ArrowItem*>(it)) {
if (a->internalId() == id) {
clear ? a->clearCustomColor() : a->setCustomColor(col);
a->updatePath();
changed = true;
}
}
}
}
if (changed) scene->requestSnapshot();
return changed;
}
bool PluginManager::apiSetItemColor(void* opaque, int kind, int id, const char* color_hex) {
auto* ctx = static_cast<HostContext*>(opaque);
if (!ctx || !ctx->manager || !ctx->manager->m_window || !color_hex) return false;
QColor c(color_hex);
if (!c.isValid()) return false;
return applyColorToScene(ctx->manager->currentScene(), kind, id, c, false);
}
bool PluginManager::apiClearItemColor(void* opaque, int kind, int id) {
auto* ctx = static_cast<HostContext*>(opaque);
if (!ctx || !ctx->manager || !ctx->manager->m_window) return false;
return applyColorToScene(ctx->manager->currentScene(), kind, id, QColor(), true);
}
void PluginManager::apiAddMenuAction(void* opaque, const char* text, Idef0ActionCallback cb, void* user_data) {
auto* ctx = static_cast<HostContext*>(opaque);
if (!ctx || !ctx->manager || !text || !cb) return;
ctx->manager->addAction(QString::fromUtf8(text), cb, user_data, ctx);
}
const char* PluginManager::apiPluginDir(void* opaque) {
auto* ctx = static_cast<HostContext*>(opaque);
if (!ctx) return nullptr;
return ctx->pluginDirUtf8.constData();
}

View file

@ -0,0 +1,55 @@
#pragma once
#include <QObject>
#include <QLibrary>
#include <memory>
#include <vector>
#include "plugins/PluginApi.h"
class QMenu;
class MainWindow;
class DiagramScene;
class PluginManager final : public QObject {
Q_OBJECT
public:
explicit PluginManager(MainWindow* window, QMenu* pluginsMenu, QObject* parent = nullptr);
void loadPlugins();
QMenu* pluginsMenu() const;
DiagramScene* currentScene() const;
QWidget* window() const;
private:
struct HostContext {
PluginManager* manager = nullptr;
QString pluginPath;
QByteArray pluginDirUtf8;
Idef0Host host;
};
struct LoadedPlugin {
std::unique_ptr<QLibrary> lib;
std::unique_ptr<HostContext> ctx;
LoadedPlugin() = default;
LoadedPlugin(std::unique_ptr<QLibrary>&& l, std::unique_ptr<HostContext>&& c)
: lib(std::move(l)), ctx(std::move(c)) {}
LoadedPlugin(LoadedPlugin&&) noexcept = default;
LoadedPlugin& operator=(LoadedPlugin&&) noexcept = default;
LoadedPlugin(const LoadedPlugin&) = delete;
LoadedPlugin& operator=(const LoadedPlugin&) = delete;
};
QStringList pluginSearchPaths() const;
std::unique_ptr<HostContext> makeContext(const QString& pluginPath);
void addAction(const QString& text, Idef0ActionCallback cb, void* userData, HostContext* ctx);
static size_t apiSelectedItems(void* opaque, Idef0SelectedItem* out, size_t max);
static bool apiSetItemColor(void* opaque, int kind, int id, const char* color_hex);
static bool apiClearItemColor(void* opaque, int kind, int id);
static void apiAddMenuAction(void* opaque, const char* text, Idef0ActionCallback cb, void* user_data);
static const char* apiPluginDir(void* opaque);
MainWindow* m_window = nullptr;
QMenu* m_pluginsMenu = nullptr;
std::vector<LoadedPlugin> m_plugins;
};

View file

@ -0,0 +1,92 @@
#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() && !c.contains("C")) 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"));
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;
}

View file

@ -0,0 +1,2 @@
#pragma once
#include "plugins/PluginApi.h"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>QObject</name>
<message>
<source>Select item color</source>
<translation>Select item color</translation>
</message>
<message>
<source>Set item color</source>
<translation>Set item color</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Clear item colors</translation>
</message>
</context>
</TS>

View file

@ -0,0 +1,19 @@
<?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;élément</translation>
</message>
<message>
<source>Set item color</source>
<translation>Définir la couleur de l&apos;élément</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Réinitialiser les couleurs des éléments</translation>
</message>
</context>
</TS>

View file

@ -0,0 +1,19 @@
<?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>
</message>
<message>
<source>Set item color</source>
<translation>Задать цвет элемента</translation>
</message>
<message>
<source>Clear item colors</source>
<translation>Сбросить цвета элементов</translation>
</message>
</context>
</TS>