diff --git a/GUI/CMakeLists.txt b/GUI/CMakeLists.txt index cdf31ce..8f11d9c 100644 --- a/GUI/CMakeLists.txt +++ b/GUI/CMakeLists.txt @@ -12,9 +12,13 @@ include( FindPkgConfig ) add_compile_definitions(QT_NO_KEYWORDS) -find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets SerialPort Network REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets SerialPort Network REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets SerialPort Network OpenGLWidgets REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets SerialPort Network OpenGLWidgets REQUIRED) find_package(fmt) +# Dear ImGui + the node editor used by the track panels. qtimgui (vendored under +# qtimgui/) provides the Qt/OpenGL backend and is compiled straight into train-gui. +find_package(imgui CONFIG REQUIRED) +find_package(unofficial-imgui-node-editor CONFIG REQUIRED) if(UNIX) find_package(Threads) find_package(dbus-cxx COMPONENTS qt) @@ -111,12 +115,14 @@ set(PROJECT_SOURCES lcc/cdi/grouptype.cpp lcc/cdi/grouptype.h trainutils.qrc - panels/paneldisplay.h panels/paneldisplay.cpp - panels/turnoutdisplay.h panels/turnoutdisplay.cpp - panels/panellayout.h panels/panellayout.cpp - - panels/paneltoolswidget.h panels/paneltoolswidget.cpp panels/paneltoolswidget.ui common/turnout.h common/turnout.cpp + + qtimgui/src/ImGuiRenderer.h qtimgui/src/ImGuiRenderer.cpp + qtimgui/src/QtImGui.h qtimgui/src/QtImGui.cpp + panels/imguipanelwidget.h panels/imguipanelwidget.cpp + panels/panelmodel.h panels/panelmodel.cpp + panels/panelfields.h panels/panelfields.cpp + panels/panelstorage.h panels/panelstorage.cpp ) if(UNIX) @@ -152,9 +158,6 @@ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(train-gui ${PROJECT_SOURCES} loconet/loconetturnout.h loconet/loconetturnout.cpp - panels/propertyeditor.h panels/propertyeditor.cpp panels/propertyeditor.ui - panels/connectable.h panels/connectable.cpp - panels/tracksegment.h panels/tracksegment.cpp loconet/loconetprogrammer.h loconet/loconetprogrammer.cpp loconet/loconetprogrammingresult.h loconet/loconetprogrammingresult.cpp loconet/loconetprogrammingrequest.h loconet/loconetprogrammingrequest.cpp @@ -196,12 +199,16 @@ target_link_libraries(train-gui PRIVATE fmt::fmt log4cxx log4cxx-qt + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + imgui::imgui + unofficial::imgui-node-editor::imgui-node-editor ) target_include_directories(train-gui PRIVATE ${train-required_INCLUDE_DIRS} ../LCC ../LocoNetCommunications ../LoconetTCPUtil + qtimgui/src ) target_compile_options(train-gui PRIVATE $<$: /Zc:preprocessor /Zc:__cplusplus> diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index 12dc2b8..88d729e 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -17,15 +17,14 @@ #include "throttledisplay.h" #include "loconet/loconetthrottle.h" #include "lccmemorydisplay.h" -#include "panels/paneldisplay.h" -#include "panels/paneltoolswidget.h" +#include "panels/imguipanelwidget.h" +#include "panels/panelstorage.h" #include "systemconnection.h" #include "speedo/speedoconnection.h" #include "speedmatcher.h" #include "speedometerdisplay.h" #include -#include #include #include #include @@ -44,9 +43,11 @@ MainWindow::MainWindow(QWidget *parent) ads::CDockManager::setAutoHideConfigFlags(ads::CDockManager::DefaultAutoHideConfig); m_dockManager = new ads::CDockManager(this); - m_panelTools = new PanelToolsWidget(this); - m_panelToolboxWidget = new ads::CDockWidget("Panel Toolbox"); - m_panelToolboxWidget->setWidget(m_panelTools); + // Not in mainwindow.ui (no actionOpenPanel there to auto-connect to) -- + // added the same way the per-panel/per-connection submenus below are, + // programmatically, since it only needs to exist once at the top level. + QAction* openPanelAction = ui->menuPanels->addAction(tr("Open panel...")); + connect(openPanelAction, &QAction::triggered, this, &MainWindow::onOpenPanelTriggered); connect(ui->menu_loconet_connect_to, &QMenu::aboutToShow, this, &MainWindow::scanForLoconetConnections); @@ -563,60 +564,73 @@ void MainWindow::on_action_loconet_manual_Serial_triggered() void MainWindow::on_actionNewPanel_triggered() { - ads::CDockWidget* DockWidget = new ads::CDockWidget("Panel"); - PanelDisplay* panelDisp = new PanelDisplay(); - if(!m_state->m_connections.empty()){ - panelDisp->addTurnout(m_state->m_connections[0]->getDCCTurnout(1)); - } + ads::CDockWidget* dockWidget = new ads::CDockWidget("Panel"); + ImguiPanelWidget* panel = new ImguiPanelWidget(m_state); + dockWidget->setWidget(panel); + m_dockManager->addDockWidget(ads::TopDockWidgetArea, dockWidget); - QScrollArea* scrollArea = new QScrollArea(); - scrollArea->setWidget(panelDisp); - scrollArea->setAlignment(Qt::AlignTop | Qt::AlignLeft); + newPanelAdded(panel, dockWidget); +} - DockWidget->setWidget(scrollArea); - m_dockManager->addDockWidget(ads::TopDockWidgetArea, DockWidget); +void MainWindow::onOpenPanelTriggered() +{ + const QStringList names = PanelStorage::savedPanelNames(); + const QString name = QInputDialog::getItem(this, "Open Panel", "Select a panel to open", names, 0, false); + if(name.isEmpty()){ + return; + } - newPanelAdded(panelDisp, DockWidget); -} + ads::CDockWidget* dockWidget = new ads::CDockWidget("Panel"); + ImguiPanelWidget* panel = new ImguiPanelWidget(m_state); + if(!panel->load(name)){ + LOG4CXX_ERROR_FMT(logger, "Could not load panel '{}'", name.toStdString()); + } + dockWidget->setWindowTitle(panel->getName()); + dockWidget->setWidget(panel); + m_dockManager->addDockWidget(ads::TopDockWidgetArea, dockWidget); -void MainWindow::newPanelAdded(PanelDisplay* panel, ads::CDockWidget* dockWidget){ - bool firstPanel = m_panels.empty(); + newPanelAdded(panel, dockWidget); +} +void MainWindow::newPanelAdded(ImguiPanelWidget* panel, ads::CDockWidget* dockWidget){ LOG4CXX_DEBUG_FMT(logger, "Added new panel"); m_panels.push_back(panel); - connect(m_panelTools, &PanelToolsWidget::allowMovingChanged, - panel, &PanelDisplay::allowMovingChanged); - - panel->setPanelToolsWidget(m_panelTools); - - if(firstPanel){ - m_dockManager->addAutoHideDockWidget(ads::SideBarLeft, m_panelToolboxWidget); - } - QMenu* menu = ui->menuPanels->addMenu(panel->getName()); QAction* actionRename = menu->addAction("Rename panel"); connect(actionRename, &QAction::triggered, - [panel,this](){ - QString oldName = panel->getName(); + [panel, menu, dockWidget, this](){ QString newName = QInputDialog::getText(this, "New Name", "Input new name of panel", QLineEdit::Normal, panel->getName()); - LOG4CXX_DEBUG_FMT(logger, "Rename panel {} to {}", oldName.toStdString(), newName.toStdString()); + if(newName.isEmpty()){ + return; + } + panel->setName(newName); + menu->setTitle(newName); + dockWidget->setWindowTitle(newName); }); - QAction* resizePanelAction = menu->addAction(tr("Set panel size...")); - connect(resizePanelAction, &QAction::triggered, this, [panel, this]() { - bool ok1, ok2; - int w = QInputDialog::getInt(this, tr("Panel Width"), - tr("Width (pixels):"), panel->width(), - 100, 10000, 1, &ok1); - if (!ok1) return; - int h = QInputDialog::getInt(this, tr("Panel Height"), - tr("Height (pixels):"), panel->height(), - 100, 10000, 1, &ok2); - if (!ok2) return; - panel->setPanelSize(QSize(w, h)); + QAction* actionSave = menu->addAction("Save panel"); + connect(actionSave, &QAction::triggered, + [panel, this](){ + if(!panel->save()){ + LOG4CXX_ERROR_FMT(logger, "Could not save panel '{}'", panel->getName().toStdString()); + } }); + QAction* actionSaveAs = menu->addAction("Save panel as..."); + connect(actionSaveAs, &QAction::triggered, + [panel, menu, dockWidget, this](){ + QString newName = QInputDialog::getText(this, "Save As", "Input name to save panel as", QLineEdit::Normal, panel->getName()); + if(newName.isEmpty()){ + return; + } + if(panel->saveAs(newName)){ + menu->setTitle(newName); + dockWidget->setWindowTitle(newName); + }else{ + LOG4CXX_ERROR_FMT(logger, "Could not save panel as '{}'", newName.toStdString()); + } + }); } diff --git a/GUI/mainwindow.h b/GUI/mainwindow.h index 4dad658..96a7908 100644 --- a/GUI/mainwindow.h +++ b/GUI/mainwindow.h @@ -13,8 +13,7 @@ namespace Ui { class MainWindow; } QT_END_NAMESPACE struct TrainUtilsState; -class PanelDisplay; -class PanelToolsWidget; +class ImguiPanelWidget; class MainWindow : public QMainWindow { @@ -49,6 +48,7 @@ private Q_SLOTS: void on_action_loconet_manual_Serial_triggered(); void on_actionNewPanel_triggered(); + void onOpenPanelTriggered(); void on_action_speedo_Manual_Serial_triggered(); @@ -59,16 +59,12 @@ private Q_SLOTS: void addSubmenusLoconetConnection(QMenu* parentMenu, QString connectionName); void addSubmenusSpeedoConnection(QMenu* parentMenu, QString connectionName); void newConnectionMade(std::shared_ptr conn); - void newPanelAdded(PanelDisplay* panel, ads::CDockWidget* dockWidget = nullptr); + void newPanelAdded(ImguiPanelWidget* panel, ads::CDockWidget* dockWidget); private: Ui::MainWindow *ui; ads::CDockManager* m_dockManager; TrainUtilsState* m_state; - QVector m_panels; - - // panel tools - ads::CDockWidget* m_panelToolboxWidget; - PanelToolsWidget* m_panelTools; + QVector m_panels; }; #endif // MAINWINDOW_H diff --git a/GUI/panels/connectable.cpp b/GUI/panels/connectable.cpp deleted file mode 100644 index 012fab1..0000000 --- a/GUI/panels/connectable.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "connectable.h" - -Connectable::Connectable() {} - -Connectable::~Connectable(){} diff --git a/GUI/panels/connectable.h b/GUI/panels/connectable.h deleted file mode 100644 index f5394ff..0000000 --- a/GUI/panels/connectable.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef CONNECTABLE_H -#define CONNECTABLE_H - -#include -#include - -class Connectable -{ -public: - Connectable(); - virtual ~Connectable(); - - virtual QVector connectionPoints() = 0; -}; - -#endif // CONNECTABLE_H diff --git a/GUI/panels/imguipanelwidget.cpp b/GUI/panels/imguipanelwidget.cpp new file mode 100644 index 0000000..812f4d5 --- /dev/null +++ b/GUI/panels/imguipanelwidget.cpp @@ -0,0 +1,1027 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include "imguipanelwidget.h" +#include "panelfields.h" +#include "panelstorage.h" +#include "../common/turnout.h" + +namespace ed = ax::NodeEditor; + +static log4cxx::LoggerPtr logger = log4cxx::Logger::getLogger("traingui.ImguiPanelWidget"); + +namespace { + +// imgui-node-editor requires non-zero, globally-unique node/pin ids. Each +// panel item gets a block of ids: one for its node, one per pin. +constexpr uint64_t kNodeEditorIdStride = 8; + +uint64_t nodeEditorNodeIdValue(PanelItemId id){ + return static_cast(id) * kNodeEditorIdStride; +} + +uint64_t nodeEditorPinIdValue(PanelItemId id, int pinIndex){ + return static_cast(id) * kNodeEditorIdStride + 1 + static_cast(pinIndex); +} + +// Inverse of nodeEditorPinIdValue(): recovers which node/pin a hovered/hit +// ed::PinId refers to. Valid because 1+pinIndex (1..3) is always < the stride. +PanelItemId nodeIdFromPinIdValue(uint64_t pinIdValue){ + return static_cast(pinIdValue / kNodeEditorIdStride); +} + +int pinIndexFromPinIdValue(uint64_t pinIdValue){ + return static_cast(pinIdValue % kNodeEditorIdStride) - 1; +} + +// Inverse of nodeEditorNodeIdValue(). +PanelItemId panelItemIdFromNodeIdValue(uint64_t nodeIdValue){ + return static_cast(nodeIdValue / kNodeEditorIdStride); +} + +// Point on a cubic bezier at parameter t, matching TrackSegment::bezierPoint(). +ImVec2 cubicBezierPoint(ImVec2 a, ImVec2 ca, ImVec2 cb, ImVec2 b, float t){ + const float u = 1.0f - t; + const float uu = u * u; + const float tt = t * t; + return ImVec2( + uu * u * a.x + 3.0f * uu * t * ca.x + 3.0f * u * tt * cb.x + tt * t * b.x, + uu * u * a.y + 3.0f * uu * t * ca.y + 3.0f * u * tt * cb.y + tt * t * b.y); +} + +// Shortest distance from p to the line segment a-b, matching TrackSegment::distanceToSegment(). +float distanceToLineSegment(ImVec2 p, ImVec2 a, ImVec2 b){ + const ImVec2 pa(p.x - a.x, p.y - a.y); + const ImVec2 ba(b.x - a.x, b.y - a.y); + const float denom = ba.x * ba.x + ba.y * ba.y; + if(denom == 0.0f){ + return std::sqrt(pa.x * pa.x + pa.y * pa.y); + } + const float t = std::clamp((pa.x * ba.x + pa.y * ba.y) / denom, 0.0f, 1.0f); + const float dx = p.x - (a.x + t * ba.x); + const float dy = p.y - (a.y + t * ba.y); + return std::sqrt(dx * dx + dy * dy); +} + +} // namespace + +// Redraw interval. Immediate mode plus the node editor's own animations +// (selection, navigation easing) make update-on-input unreliable, so just run a +// steady frame loop while the panel is actually visible. +static constexpr int kRepaintIntervalMs = 16; + +// The node editor fits the view to its content on the first frame, before node +// sizes have been measured, which leaves a brand new panel at a nonsense zoom. +// Re-fit once the content has settled. +static constexpr int kSettleFrames = 3; + +ImguiPanelWidget::ImguiPanelWidget(TrainUtilsState* state, QWidget* parent) + : QOpenGLWidget{parent} + , m_state(state) +{ + m_name = "Panel"; + + // Needed so key events reach qtimgui's event filter. + setFocusPolicy(Qt::StrongFocus); + + m_repaintTimer.setInterval(kRepaintIntervalMs); + connect(&m_repaintTimer, &QTimer::timeout, + this, QOverload<>::of(&ImguiPanelWidget::update)); +} + +ImguiPanelWidget::~ImguiPanelWidget() +{ + makeCurrent(); + if(m_editor){ + ed::DestroyEditor(m_editor); + m_editor = nullptr; + } + // NOTE: qtimgui offers no teardown for a RenderRef, so the ImGuiRenderer and + // its ImGui context outlive this widget. Panels are long-lived, so this is + // tolerable for now; fixing it means adding a destroy() to GUI/qtimgui. + doneCurrent(); +} + +void ImguiPanelWidget::initializeGL() +{ + // defaultRender must be false: true would share one process-wide renderer + // (and one ImGui context) between every panel. + m_imgui = QtImGui::initialize(this, false); + + LOG4CXX_DEBUG_FMT(logger, "Initialized imgui panel '{}'", m_name.toStdString()); +} + +bool ImguiPanelWidget::ensureEditor() +{ + if(m_editor){ + return true; + } + + // The node editor fits its view to its content on its very first frame and + // keeps the result. The dock has not been laid out on our first paint -- + // the widget is around 200x70 there -- so creating the editor that early + // leaves a brand new panel sitting at roughly an 8x zoom. Wait until the + // widget size has settled before the editor ever sees a frame. + const QSize current = size(); + if(current != m_lastSize){ + m_lastSize = current; + m_stableSizeFrames = 0; + return false; + } + if(++m_stableSizeFrames < kStableSizeFrames){ + return false; + } + + ed::Config config; + // Keep the editor's own settings (node sizes, pan and zoom) in memory rather + // than letting it write NodeEditor.json next to the binary. Node positions + // are owned by the panel itself, so nothing here needs to outlive the panel. + config.SettingsFile = nullptr; + config.UserPointer = this; + config.SaveSettings = [](const char* data, size_t size, + ed::SaveReasonFlags, void* userPointer) { + auto* self = static_cast(userPointer); + self->m_editorSettings.assign(data, size); + return true; + }; + config.LoadSettings = [](char* data, void* userPointer) { + auto* self = static_cast(userPointer); + if(data){ + memcpy(data, self->m_editorSettings.data(), self->m_editorSettings.size()); + } + return self->m_editorSettings.size(); + }; + m_editor = ed::CreateEditor(&config); + + // WORKAROUND: imgui-node-editor 0.9.3's background/grid draw commands + // (imgui_node_editor.cpp, "Draw grid") end up with an ImDrawCmd::ClipRect + // that never receives the fixup ImGuiEx::Canvas::LeaveLocalSpace() applies + // to everything else drawn inside ed::Begin/End -- confirmed by dumping the + // raw ClipRect values: after panning by (dx, 0) the grid's clip rect sits at + // (correct.x - dx, ...) instead of (correct.x, ...), i.e. it's simply + // missing the pan offset. At zoom 1 a widget positioned at the window + // origin masks this (the missing offset is ~0); at any other pan or zoom it + // leaves a stale, wrongly-positioned/sized rectangle on screen -- reported + // as "the viewport moves opposite to the mouse" and reproduced independent + // of DPI. Nodes, links and our own draw-list content are unaffected (their + // clip rects were confirmed correct in the same dump). + // + // Rather than patch the vendored library, make ed::'s own background/grid + // fully transparent so its broken clip rect never has anything visible to + // clip, and draw our own background + grid instead, directly on the + // window's draw list, which -- like the placeholder bezier below -- is + // confirmed to pan and zoom correctly. + ed::SetCurrentEditor(m_editor); + ed::Style& style = ed::GetStyle(); + style.Colors[ed::StyleColor_Bg] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + style.Colors[ed::StyleColor_Grid] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + ed::SetCurrentEditor(nullptr); + + return true; +} + +void ImguiPanelWidget::showEvent(QShowEvent* event) +{ + QOpenGLWidget::showEvent(event); + m_repaintTimer.start(); +} + +void ImguiPanelWidget::hideEvent(QHideEvent* event) +{ + m_repaintTimer.stop(); + QOpenGLWidget::hideEvent(event); +} + +void ImguiPanelWidget::applyKeyboardModifiers() +{ + const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers(); + ImGuiIO& io = ImGui::GetIO(); + + const bool ctrl = mods.testFlag(Qt::ControlModifier); + const bool shift = mods.testFlag(Qt::ShiftModifier); + const bool alt = mods.testFlag(Qt::AltModifier); + const bool super = mods.testFlag(Qt::MetaModifier); + + // Direct assignment takes effect for the frame NewFrame() just started. + io.KeyCtrl = ctrl; + io.KeyShift = shift; + io.KeyAlt = alt; + io.KeySuper = super; + + // ...and queue real key events so ImGui's own key state stays consistent. + io.AddKeyEvent(ImGuiMod_Ctrl, ctrl); + io.AddKeyEvent(ImGuiMod_Shift, shift); + io.AddKeyEvent(ImGuiMod_Alt, alt); + io.AddKeyEvent(ImGuiMod_Super, super); +} + +void ImguiPanelWidget::paintGL() +{ + QtImGui::newFrame(m_imgui); + + if(!m_imguiConfigured){ + // Our ImGui context is current from here on; stop it writing imgui.ini + // into whatever the working directory happens to be. + ImGui::GetIO().IniFilename = nullptr; + m_imguiConfigured = true; + } + + applyKeyboardModifiers(); + + drawFrame(); + + ImGui::Render(); + QtImGui::render(m_imgui); +} + +void ImguiPanelWidget::drawFrame() +{ + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + + ImGui::Begin("##panelroot", nullptr, + ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoBringToFrontOnFocus + | ImGuiWindowFlags_NoNavFocus + | ImGuiWindowFlags_NoSavedSettings); + + m_dbgAvail = ImGui::GetContentRegionAvail(); + + if(!ensureEditor()){ + ImGui::End(); + ImGui::PopStyleVar(); + return; + } + + ed::SetCurrentEditor(m_editor); + + // Must run before ed::Begin(): see the comment on drawGrid() for why. + drawGrid(); + + ed::Begin("TrackPanel", ImVec2(0.0f, 0.0f)); + + drawCanvas(); + + m_frame++; + + ed::End(); + + // Needs the editor current (GetNodePosition/SetNodePosition), but must run + // after ed::End() -- not before -- so it sees where the user actually + // dropped a node this frame, not last frame's position. + syncTurnoutTransforms(); + handleOperateModeClicks(); + if(m_mode == PanelMode::Edit){ + handleSegmentSelection(); + handleSegmentDrawingClicks(); + } + + ed::SetCurrentEditor(nullptr); + + ImGui::End(); + ImGui::PopStyleVar(); + + drawToolbox(viewport); + drawDebugWindow(viewport); +} + +void ImguiPanelWidget::drawGrid() +{ + // Our own replacement for ed::'s built-in background/grid -- see the + // WORKAROUND comment in ensureEditor() for why. + // + // Must be called BEFORE ed::Begin(), not just outside ed::'s node/pin + // drawing: ed::Begin() itself is what leaves the window's draw list + // mid-channel-split (it draws its own, now-invisible, grid on a channel it + // never switches back off of before returning). Any of our own drawing + // done after ed::Begin() returns, even before the first node, inherits + // that same channel and the same broken clip rect -- confirmed by this + // exact bug still reproducing when this function was called first thing + // inside drawCanvas() instead. Calling it here, before the window's draw + // list has been split into channels at all, avoids the whole mechanism: + // this is plain, unsplit ImGui drawing in screen space, using + // ed::ScreenToCanvas()/CanvasToScreen() (confirmed correct -- they're what + // the debug window's own readouts use) to place a grid that still tracks + // panning and zoom. Being the first thing drawn on the window's draw list + // this frame, it renders behind everything ed::Begin()/drawCanvas() add + // afterward. + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(ImVec2(0.0f, 0.0f), m_dbgAvail, IM_COL32(38, 38, 45, 255)); + + const ImVec2 topLeft = ed::ScreenToCanvas(ImVec2(0.0f, 0.0f)); + const ImVec2 bottomRight = ed::ScreenToCanvas(m_dbgAvail); + + constexpr float kGridStep = 32.0f; + const ImU32 gridColor = IM_COL32(120, 120, 120, 40); + for(float x = fmodf(topLeft.x, kGridStep); x < bottomRight.x; x += kGridStep){ + const ImVec2 top = ed::CanvasToScreen(ImVec2(x, topLeft.y)); + const ImVec2 bottom = ed::CanvasToScreen(ImVec2(x, bottomRight.y)); + drawList->AddLine(top, bottom, gridColor); + } + for(float y = fmodf(topLeft.y, kGridStep); y < bottomRight.y; y += kGridStep){ + const ImVec2 left = ed::CanvasToScreen(ImVec2(topLeft.x, y)); + const ImVec2 right = ed::CanvasToScreen(ImVec2(bottomRight.x, y)); + drawList->AddLine(left, right, gridColor); + } +} + +void ImguiPanelWidget::drawCanvas() +{ + // ed::GetNodeBackgroundDrawList() must be called AFTER ed::EndNode() -- + // calling it between BeginNode/EndNode trips an assertion inside + // ImDrawListSplitter::SetCurrentChannel (see drawTurnoutNode()). + for(TurnoutNode& node : m_model.turnouts()){ + drawTurnoutNode(node); + } + + drawSegments(); + drawSegmentPreview(); + + // ed::BeginDelete()/EndDelete() and the right-click "insert node" popup + // (ed::ShowBackgroundContextMenu()) must run before ed::End(), per the + // node-editor's own examples -- unlike GetHoveredNode()/GetHoveredPin(), + // which tolerate either side of End() (handleOperateModeClicks() and + // handleSegmentDrawingClicks() call them after; see the note there for + // why "after" is in fact required for anything driven by + // ed::IsBackgroundClicked()/ed::GetBackgroundClickButtonIndex()). + if(m_mode == PanelMode::Edit){ + handleSegmentInsertMenu(); + handleDelete(); + } +} + +void ImguiPanelWidget::drawTurnoutNode(TurnoutNode& node) +{ + const ed::NodeId nodeId(nodeEditorNodeIdValue(node.id)); + + // Only seed the node editor's own position the first time it sees this + // node; afterward, position is owned by the user dragging it (read back + // in syncTurnoutTransforms()), not re-pushed from the model every frame. + if(m_seededNodes.insert(node.id).second){ + ed::SetNodePosition(nodeId, node.position); + } + + ed::BeginNode(nodeId); + ImGui::Dummy(ImVec2(kNodeSize, kNodeSize)); + // GetItemRectMin()/Max() return canvas-space coordinates while inside + // ed::Begin/End (confirmed in Phase 1), not screen coordinates. + const ImVec2 origin = ImGui::GetItemRectMin(); + + const TurnoutGeometry geo = turnoutGeometry(node.hand, node.rotationDegrees); + + for(int i = 0; i < 3; i++){ + const ImVec2 pinCenter(origin.x + geo.pins[i].x, origin.y + geo.pins[i].y); + ImGui::SetCursorScreenPos(ImVec2(pinCenter.x - 4.0f, pinCenter.y - 4.0f)); + ed::BeginPin(ed::PinId(nodeEditorPinIdValue(node.id, i)), + i == 0 ? ed::PinKind::Input : ed::PinKind::Output); + ed::PinPivotAlignment(ImVec2(0.5f, 0.5f)); + ImGui::Dummy(ImVec2(8.0f, 8.0f)); + ed::EndPin(); + } + + ed::EndNode(); + + if(ImGui::IsItemVisible()){ + ImDrawList* bg = ed::GetNodeBackgroundDrawList(nodeId); + for(const auto& leg : geo.legs){ + bg->AddLine(ImVec2(origin.x + leg[0].x, origin.y + leg[0].y), + ImVec2(origin.x + leg[1].x, origin.y + leg[1].y), + IM_COL32(230, 230, 230, 255), 3.0f); + } + + const char* stateText = "N/A"; + if(node.turnout){ + switch(node.turnout->getState()){ + case Turnout::TurnoutState::Unknown: stateText = "unknown"; break; + case Turnout::TurnoutState::Closed: stateText = "closed"; break; + case Turnout::TurnoutState::Thrown: stateText = "thrown"; break; + } + } + bg->AddText(ImVec2(origin.x, origin.y + kTurnoutContentSize / 4.0f), + IM_COL32(230, 230, 230, 255), stateText); + } + + // Rotation is an editing action -- keep it unavailable in Operate mode, + // matching the QWidget implementation (there, nothing could ever become + // m_selectedWidget, and so the rotate handle could never appear, unless + // m_allowMoving was on). + if(m_mode == PanelMode::Edit && ed::IsNodeSelected(nodeId)){ + drawRotateHandle(node, origin); + } +} + +void ImguiPanelWidget::drawRotateHandle(TurnoutNode& node, ImVec2 nodeTopLeft) +{ + const ImVec2 centerCanvas(nodeTopLeft.x + kNodeSize / 2.0f, nodeTopLeft.y + kNodeSize / 2.0f); + const double rad = node.rotationDegrees * std::numbers::pi / 180.0; + const float handleDistance = kTurnoutContentSize / 2.0f + kRotateHandleDistance; + const ImVec2 handleCanvas(centerCanvas.x + std::sin(rad) * handleDistance, + centerCanvas.y - std::cos(rad) * handleDistance); + + // Draw and hit-test the handle in screen space so its size stays constant + // regardless of zoom, and so a click on it is claimed here rather than + // being interpreted as a click on the node/canvas underneath -- the same + // reason PanelDisplay::eventFilter existed in the QWidget implementation. + ed::Suspend(); + + const ImVec2 centerScreen = ed::CanvasToScreen(centerCanvas); + const ImVec2 handleScreen = ed::CanvasToScreen(handleCanvas); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddLine(centerScreen, handleScreen, IM_COL32(90, 90, 90, 200)); + + ImGui::SetCursorScreenPos(ImVec2(handleScreen.x - kRotateHandleRadius, + handleScreen.y - kRotateHandleRadius)); + ImGui::PushID(static_cast(node.id)); + ImGui::InvisibleButton("##rotate_handle", ImVec2(kRotateHandleRadius * 2.0f, kRotateHandleRadius * 2.0f)); + const bool dragging = ImGui::IsItemActive(); + ImGui::PopID(); + + drawList->AddCircleFilled(handleScreen, kRotateHandleRadius, IM_COL32(255, 255, 255, 255)); + drawList->AddCircle(handleScreen, kRotateHandleRadius, IM_COL32(30, 140, 30, 255), 0, 2.0f); + + if(dragging){ + const ImVec2 mouse = ImGui::GetIO().MousePos; + const ImVec2 delta(mouse.x - centerScreen.x, mouse.y - centerScreen.y); + // 0 degrees = straight up, positive = clockwise, matching QPainter::rotate() + // (see turnoutGeometry()) and the QWidget implementation this replaces. + double angle = std::atan2(delta.x, -delta.y) * 180.0 / std::numbers::pi; + if(ImGui::GetIO().KeyCtrl){ + angle = std::lround(angle / kRotationSnapDegrees) * kRotationSnapDegrees; + } + node.rotationDegrees = angle; + } + + ed::Resume(); +} + +void ImguiPanelWidget::syncTurnoutTransforms() +{ + if(m_mode == PanelMode::Operate){ + // Nothing moves while operating: force every node back to its last + // known-good position every frame, so a click that the node editor + // interpreted as the start of a drag never actually goes anywhere + // (matches the QWidget implementation, where dragging only ever + // engaged via a right-click that Operate mode never generated). + for(TurnoutNode& node : m_model.turnouts()){ + ed::SetNodePosition(ed::NodeId(nodeEditorNodeIdValue(node.id)), node.position); + } + return; + } + + const bool snapToGrid = ImGui::GetIO().KeyCtrl; + + for(TurnoutNode& node : m_model.turnouts()){ + const ed::NodeId nodeId(nodeEditorNodeIdValue(node.id)); + ImVec2 pos = ed::GetNodePosition(nodeId); + + const bool moved = (pos.x != node.position.x) || (pos.y != node.position.y); + if(snapToGrid && moved){ + pos.x = std::lround(pos.x / kGridSize) * kGridSize; + pos.y = std::lround(pos.y / kGridSize) * kGridSize; + ed::SetNodePosition(nodeId, pos); + } + + node.position = pos; + } +} + +void ImguiPanelWidget::handleOperateModeClicks() +{ + if(m_mode != PanelMode::Operate){ + m_clickCandidateId = 0; + return; + } + + const ImGuiIO& io = ImGui::GetIO(); + + if(ImGui::IsMouseClicked(ImGuiMouseButton_Left)){ + const ed::NodeId hovered = ed::GetHoveredNode(); + m_clickCandidateId = hovered.Get() + ? static_cast(hovered.Get() / kNodeEditorIdStride) + : 0; + m_clickStartMouse = io.MousePos; + m_clickStartTime = ImGui::GetTime(); + } + + if(ImGui::IsMouseReleased(ImGuiMouseButton_Left) && m_clickCandidateId != 0){ + const ed::NodeId hoveredNow = ed::GetHoveredNode(); + const PanelItemId releasedOverId = hoveredNow.Get() + ? static_cast(hoveredNow.Get() / kNodeEditorIdStride) + : 0; + + const float dx = std::abs(io.MousePos.x - m_clickStartMouse.x); + const float dy = std::abs(io.MousePos.y - m_clickStartMouse.y); + const double elapsed = ImGui::GetTime() - m_clickStartTime; + + if(releasedOverId == m_clickCandidateId + && dx < kClickMaxMovePixels && dy < kClickMaxMovePixels + && elapsed > 0.0 && elapsed < kClickMaxSeconds){ + if(TurnoutNode* node = m_model.findTurnout(m_clickCandidateId)){ + LOG4CXX_DEBUG_FMT(logger, "Toggle turnout T{}", node->id); + if(node->turnout){ + node->turnout->toggleTurnout(); + } + } + } + + m_clickCandidateId = 0; + } +} + +ImguiPanelWidget::ResolvedSegment ImguiPanelWidget::resolveSegment(TrackSegmentEdge& seg) const +{ + ResolvedSegment r; + r.a = resolveSegmentEnd(m_model, seg.a); + r.b = resolveSegmentEnd(m_model, seg.b); + + if(seg.straight){ + // Recompute as the endpoints move/rotate, matching + // TrackSegment::straightenControlOffsets() -- keeps a straight + // segment tracking a moving/rotating turnout as a straight line + // until the user actually drags a control handle. + const ImVec2 diff(r.b.x - r.a.x, r.b.y - r.a.y); + seg.controlOffsetA = ImVec2(diff.x / 3.0f, diff.y / 3.0f); + seg.controlOffsetB = ImVec2(-diff.x / 3.0f, -diff.y / 3.0f); + } + + r.ca = ImVec2(r.a.x + seg.controlOffsetA.x, r.a.y + seg.controlOffsetA.y); + r.cb = ImVec2(r.b.x + seg.controlOffsetB.x, r.b.y + seg.controlOffsetB.y); + return r; +} + +void ImguiPanelWidget::drawSegments() +{ + ImDrawList* drawList = ImGui::GetWindowDrawList(); + for(TrackSegmentEdge& seg : m_model.segments()){ + const ResolvedSegment r = resolveSegment(seg); + drawList->AddBezierCubic(r.a, r.ca, r.cb, r.b, IM_COL32(230, 230, 230, 255), 3.0f); + + if(m_mode == PanelMode::Edit && m_tool == Tool::Select && seg.id == m_selectedSegmentId){ + drawSegmentHandles(seg, r); + } + } +} + +void ImguiPanelWidget::drawSegmentHandles(TrackSegmentEdge& seg, const ResolvedSegment& r) +{ + // Dashed tethers from each endpoint to its control point, matching + // PanelDisplay::paintEvent()'s presentation for a selected segment. Drawn + // in canvas space -- same reasoning as the bezier curve itself -- so no + // Suspend() is needed just for these lines. + auto addDashedLine = [](ImDrawList* dl, ImVec2 from, ImVec2 to, ImU32 color){ + constexpr float kDashLength = 6.0f; + const float dx = to.x - from.x; + const float dy = to.y - from.y; + const float length = std::sqrt(dx * dx + dy * dy); + if(length < 1.0f){ + return; + } + const int steps = std::max(1, static_cast(length / kDashLength)); + for(int i = 0; i < steps; i += 2){ + const int endStep = std::min(i + 1, steps); + dl->AddLine(ImVec2(from.x + dx * (float(i) / steps), from.y + dy * (float(i) / steps)), + ImVec2(from.x + dx * (float(endStep) / steps), from.y + dy * (float(endStep) / steps)), + color, 1.0f); + } + }; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + addDashedLine(drawList, r.a, r.ca, IM_COL32(150, 150, 150, 220)); + addDashedLine(drawList, r.b, r.cb, IM_COL32(150, 150, 150, 220)); + + // The handles themselves are drawn/hit-tested in screen space, same + // reasoning as drawRotateHandle(): fixed size regardless of zoom, and + // claimed here before the node editor's own background click handling + // sees them. + ed::Suspend(); + + ImDrawList* fg = ImGui::GetWindowDrawList(); + ImGui::PushID(static_cast(seg.id)); + + const ImVec2 canvasHandles[2] = { r.ca, r.cb }; + ImVec2* offsets[2] = { &seg.controlOffsetA, &seg.controlOffsetB }; + const ImVec2 endpoints[2] = { r.a, r.b }; + const char* ids[2] = { "##ctrlA", "##ctrlB" }; + + for(int i = 0; i < 2; i++){ + const ImVec2 handleScreen = ed::CanvasToScreen(canvasHandles[i]); + ImGui::SetCursorScreenPos(ImVec2(handleScreen.x - kRotateHandleRadius, + handleScreen.y - kRotateHandleRadius)); + ImGui::InvisibleButton(ids[i], ImVec2(kRotateHandleRadius * 2.0f, kRotateHandleRadius * 2.0f)); + if(ImGui::IsItemActive()){ + const ImVec2 mouseCanvas = ed::ScreenToCanvas(ImGui::GetIO().MousePos); + *offsets[i] = ImVec2(mouseCanvas.x - endpoints[i].x, mouseCanvas.y - endpoints[i].y); + seg.straight = false; + } + fg->AddCircleFilled(handleScreen, kRotateHandleRadius, IM_COL32(255, 255, 255, 255)); + fg->AddCircle(handleScreen, kRotateHandleRadius, IM_COL32(30, 140, 30, 255), 0, 2.0f); + } + + ImGui::PopID(); + ed::Resume(); +} + +void ImguiPanelWidget::drawSegmentPreview() +{ + if(!m_drawingSegment){ + return; + } + + const ImVec2 start = resolveSegmentEnd(m_model, m_drawStart); + // io.MousePos is already canvas space here: drawCanvas() (which calls + // this) runs inside ed::Begin/End, not suspended, and imgui-node-editor + // rewrites io.MousePos into canvas coordinates for that whole span + // (ImGuiEx::Canvas::EnterLocalSpace()) -- restoring it to real screen + // coordinates only inside a Suspend()/Resume() block or after ed::End(). + // (drawRotateHandle()/drawSegmentHandles() convert explicitly because + // they run *inside* such a Suspend block; handleOperateModeClicks() needs + // no conversion either, because it runs *after* ed::End().) + const ImVec2 endCanvas = ImGui::GetIO().MousePos; + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + ImVec2 prev = start; + for(const ImVec2& control : m_drawControls){ + drawList->AddLine(prev, control, IM_COL32(90, 200, 255, 200), 2.0f); + drawList->AddCircleFilled(control, 4.0f, IM_COL32(90, 200, 255, 255)); + prev = control; + } + drawList->AddLine(prev, endCanvas, IM_COL32(90, 200, 255, 200), 2.0f); +} + +void ImguiPanelWidget::handleSegmentSelection() +{ + if(m_tool != Tool::Select){ + return; + } + + if(ed::GetHoveredNode().Get() != 0 || ed::GetHoveredPin().Get() != 0){ + if(ImGui::IsMouseClicked(ImGuiMouseButton_Left)){ + // Let the node editor's own node/pin selection win. + m_selectedSegmentId = 0; + } + return; + } + + // 0 = left (matches Config::SelectButtonIndex's default); -1 = no + // background click completed this frame. This is deliberately not + // ImGui::IsMouseClicked(): that fires on the press frame, while + // GetBackgroundClickButtonIndex() is only populated by ed::End()'s own + // per-frame action processing (confirmed by reading + // SelectAction::Accept() in imgui_node_editor.cpp) -- the two essentially + // never coincide, which is exactly why this function must be called + // after ed::End() (see the header) rather than from inside drawCanvas(). + // It also naturally excludes clicks on the toolbox/debug windows: ed:: + // only ever reports a background click for its own canvas. + if(ed::GetBackgroundClickButtonIndex() != 0){ + return; + } + + // io.MousePos is real screen space here (this runs after ed::End()). + const ImVec2 mouseCanvas = ed::ScreenToCanvas(ImGui::GetIO().MousePos); + // A constant screen-pixel hit tolerance, expressed in canvas units at the + // current zoom (GetCurrentZoom() is the inverse scale, so multiplying + // converts "screen pixels" to "canvas units" -- confirmed in Phase 1). + const float toleranceCanvas = kSegmentHitTolerance * ed::GetCurrentZoom(); + + PanelItemId hitId = 0; + float bestDist = toleranceCanvas; + for(TrackSegmentEdge& seg : m_model.segments()){ + const ResolvedSegment r = resolveSegment(seg); + constexpr int kSamples = 24; + ImVec2 prev = r.a; + for(int i = 1; i <= kSamples; i++){ + const ImVec2 next = cubicBezierPoint(r.a, r.ca, r.cb, r.b, float(i) / kSamples); + const float dist = distanceToLineSegment(mouseCanvas, prev, next); + if(dist <= bestDist){ + bestDist = dist; + hitId = seg.id; + } + prev = next; + } + } + + m_selectedSegmentId = hitId; + if(hitId != 0){ + ed::ClearSelection(); + } +} + +void ImguiPanelWidget::handleSegmentDrawingClicks() +{ + if(m_tool != Tool::DrawSegment){ + m_drawingSegment = false; + m_drawControls.clear(); + return; + } + + if(m_drawingSegment && ImGui::IsKeyPressed(ImGuiKey_Escape)){ + m_drawingSegment = false; + m_drawControls.clear(); + return; + } + + const ed::PinId hoveredPin = ed::GetHoveredPin(); + if(ImGui::IsMouseClicked(ImGuiMouseButton_Left) && hoveredPin.Get() != 0){ + SegmentEnd end; + end.nodeId = nodeIdFromPinIdValue(hoveredPin.Get()); + end.pinIndex = pinIndexFromPinIdValue(hoveredPin.Get()); + + if(!m_drawingSegment){ + m_drawingSegment = true; + m_drawStart = end; + m_drawControls.clear(); + }else{ + finishSegmentDraw(end); + } + return; + } + + // See the note on handleSegmentSelection() for why + // GetBackgroundClickButtonIndex() rather than IsMouseClicked() here. + if(m_drawingSegment && m_drawControls.size() < 2 + && ed::GetBackgroundClickButtonIndex() == 0){ + // io.MousePos is real screen space here (this runs after ed::End()). + m_drawControls.push_back(ed::ScreenToCanvas(ImGui::GetIO().MousePos)); + } +} + +void ImguiPanelWidget::handleSegmentInsertMenu() +{ + if(m_tool != Tool::DrawSegment || !m_drawingSegment){ + return; + } + + // Right-click on empty canvas: offer to insert a node partway through. + // openPopupPos is captured here, still unsuspended (this runs inside + // ed::Begin/End, from drawCanvas()), so it's already in canvas space -- + // exactly what insertTurnoutIntoDraw() needs, with no further conversion, + // even though the click that ultimately consumes it (the "Turnout" menu + // item) may land on a later, suspended frame. + const ImVec2 openPopupPos = ImGui::GetIO().MousePos; + + ed::Suspend(); + if(ed::ShowBackgroundContextMenu()){ + ImGui::OpenPopup("Insert Node"); + } + if(ImGui::BeginPopup("Insert Node")){ + if(ImGui::MenuItem("Turnout")){ + insertTurnoutIntoDraw(openPopupPos); + } + ImGui::Separator(); + if(ImGui::MenuItem("Cancel")){ + m_drawingSegment = false; + m_drawControls.clear(); + } + ImGui::EndPopup(); + } + ed::Resume(); +} + +void ImguiPanelWidget::finishSegmentDraw(SegmentEnd end) +{ + TrackSegmentEdge& seg = m_model.addSegment(m_drawStart, end); + + if(!m_drawControls.empty()){ + const ImVec2 a = resolveSegmentEnd(m_model, seg.a); + const ImVec2 b = resolveSegmentEnd(m_model, seg.b); + seg.controlOffsetA = ImVec2(m_drawControls[0].x - a.x, m_drawControls[0].y - a.y); + if(m_drawControls.size() >= 2){ + seg.controlOffsetB = ImVec2(m_drawControls[1].x - b.x, m_drawControls[1].y - b.y); + }else{ + // Only one control point was placed: mirror it across the + // segment so the curve bends smoothly at both ends rather than + // snapping straight at the end the user didn't touch. + seg.controlOffsetB = ImVec2(-seg.controlOffsetA.x, -seg.controlOffsetA.y); + } + seg.straight = false; + } + // else: leave seg.straight = true; resolveSegment() keeps it tracking as + // a straight line between its (possibly moving) endpoints. + + m_drawingSegment = false; + m_drawControls.clear(); +} + +void ImguiPanelWidget::insertTurnoutIntoDraw(ImVec2 canvasPos) +{ + // Centre the new node on the click. + TurnoutNode& node = m_model.addTurnout( + ImVec2(canvasPos.x - kNodeSize / 2.0f, canvasPos.y - kNodeSize / 2.0f)); + + // Connect the in-progress segment to whichever of the new node's three + // pins is nearest the click. + const TurnoutGeometry geo = turnoutGeometry(node.hand, node.rotationDegrees); + int nearest = 0; + float bestDistSq = std::numeric_limits::max(); + for(int i = 0; i < 3; i++){ + const ImVec2 pinPos(node.position.x + geo.pins[i].x, node.position.y + geo.pins[i].y); + const float dx = pinPos.x - canvasPos.x; + const float dy = pinPos.y - canvasPos.y; + const float distSq = dx * dx + dy * dy; + if(distSq < bestDistSq){ + bestDistSq = distSq; + nearest = i; + } + } + + finishSegmentDraw(SegmentEnd{node.id, nearest, ImVec2()}); + + // Keep going: start a new segment from a different pin on the same node. + // There's no real notion of "the next free pin" without a richer + // connectivity model, so this just keeps the flow going rather than + // silently stopping partway through what the user was drawing. + m_drawingSegment = true; + m_drawStart = SegmentEnd{node.id, (nearest + 1) % 3, ImVec2()}; + m_drawControls.clear(); +} + +void ImguiPanelWidget::handleDelete() +{ + // Segments aren't node-editor items, so their deletion is ours to handle. + if(m_selectedSegmentId != 0 && ImGui::IsKeyPressed(ImGuiKey_Delete)){ + m_model.removeSegment(m_selectedSegmentId); + m_selectedSegmentId = 0; + } + + // Node deletion goes through ed::'s own delete flow (Del key while a node + // is selected). PanelModel::removeTurnout() cascades to drop any segment + // that was attached to the deleted node. + if(ed::BeginDelete()){ + ed::NodeId nodeId; + while(ed::QueryDeletedNode(&nodeId)){ + if(ed::AcceptDeletedItem()){ + const PanelItemId id = static_cast(nodeId.Get() / kNodeEditorIdStride); + m_model.removeTurnout(id); + m_seededNodes.erase(id); + } + } + } + ed::EndDelete(); +} + +void ImguiPanelWidget::drawToolbox(const ImGuiViewport* viewport) +{ + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + 12.0f, viewport->Pos.y + 12.0f), + ImGuiCond_FirstUseEver); + if(ImGui::Begin("Panel toolbox", nullptr, ImGuiWindowFlags_AlwaysAutoResize)){ + bool operate = (m_mode == PanelMode::Operate); + if(ImGui::RadioButton("Operate", operate)){ + m_mode = PanelMode::Operate; + } + ImGui::SameLine(); + if(ImGui::RadioButton("Edit", !operate)){ + m_mode = PanelMode::Edit; + } + + // Everything below is an editing action -- matches the "Tool (Edit + // only)" split in the design. + ImGui::BeginDisabled(m_mode != PanelMode::Edit); + + if(ImGui::Button("Add Turnout")){ + // Stagger repeated adds so they don't land exactly on top of one + // another; the user can drag it wherever it belongs. + const float offset = 24.0f * static_cast(m_model.turnouts().size() % 8); + m_model.addTurnout(ImVec2(offset, offset)); + m_tool = Tool::Select; + } + + bool selectTool = (m_tool == Tool::Select); + if(ImGui::RadioButton("Select", selectTool)){ + m_tool = Tool::Select; + } + ImGui::SameLine(); + if(ImGui::RadioButton("Draw Segment", !selectTool)){ + m_tool = Tool::DrawSegment; + } + + if(m_selectedSegmentId != 0 && ImGui::Button("Make Straight")){ + if(TrackSegmentEdge* seg = m_model.findSegment(m_selectedSegmentId)){ + seg->straight = true; + } + } + + ImGui::EndDisabled(); + + if(m_mode == PanelMode::Edit){ + drawProperties(); + } + } + ImGui::End(); +} + +void ImguiPanelWidget::drawProperties() +{ + ed::SetCurrentEditor(m_editor); + ed::NodeId selectedNodeId; + const int selectedCount = ed::GetSelectedNodes(&selectedNodeId, 1); + ed::SetCurrentEditor(nullptr); + + if(selectedCount != 1){ + return; + } + TurnoutNode* node = m_model.findTurnout(panelItemIdFromNodeIdValue(selectedNodeId.Get())); + if(!node){ + return; + } + + ImGui::Separator(); + ImGui::Text("Turnout properties"); + std::vector fields = fieldsFor(*node, m_state); + renderPanelFields(fields); +} + +void ImguiPanelWidget::drawDebugWindow(const ImGuiViewport* viewport) +{ + ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x - 260.0f, + viewport->Pos.y + 12.0f), + ImGuiCond_FirstUseEver); + if(ImGui::Begin("Panel debug", nullptr, ImGuiWindowFlags_AlwaysAutoResize)){ + const ImGuiIO& io = ImGui::GetIO(); + ImGui::Text("%s", m_name.toStdString().c_str()); + ImGui::Text("%.1f FPS dpr %.2f", io.Framerate, io.DisplayFramebufferScale.x); + ImGui::Text("Ctrl %d Shift %d Alt %d", io.KeyCtrl, io.KeyShift, io.KeyAlt); + + ed::SetCurrentEditor(m_editor); + // GetCurrentZoom() returns the inverse scale: 0.5 means zoomed in 2x. + ImGui::Text("1/zoom %.3f", ed::GetCurrentZoom()); + ImGui::Text("canvas %.0fx%.0f", m_dbgAvail.x, m_dbgAvail.y); + { + const ImVec2 origin = ed::CanvasToScreen(ImVec2(0.0f, 0.0f)); + ImGui::Text("origin: %.1f,%.1f", origin.x, origin.y); + } + ImGui::Text("mouse: %.1f,%.1f", io.MousePos.x, io.MousePos.y); + ImGui::Text("hover node %llu pin %llu", (unsigned long long)ed::GetHoveredNode().Get(), + (unsigned long long)ed::GetHoveredPin().Get()); + ImGui::Text("draw %d ctl %zu segs %zu sel %u", + m_drawingSegment, m_drawControls.size(), m_model.segments().size(), + m_selectedSegmentId); + for(const TurnoutNode& node : m_model.turnouts()){ + ImGui::Text("T%u pos %.1f,%.1f rot %.2f", node.id, + node.position.x, node.position.y, node.rotationDegrees); + } + ed::SetCurrentEditor(nullptr); + } + ImGui::End(); +} + +QString ImguiPanelWidget::getName() const +{ + return m_name; +} + +void ImguiPanelWidget::setName(QString name) +{ + m_name = name; +} + +bool ImguiPanelWidget::save() const +{ + return PanelStorage::save(m_model, m_name); +} + +bool ImguiPanelWidget::saveAs(QString name) +{ + m_name = name; + return save(); +} + +bool ImguiPanelWidget::load(QString panelName) +{ + if(!PanelStorage::load(m_model, panelName, m_state)){ + return false; + } + m_name = panelName; + + // The model was just fully replaced -- nothing in it has been seeded into + // the node editor yet, and any prior selection refers to ids that may no + // longer exist (or now mean something else, if the loaded file reuses + // them). drawTurnoutNode() re-seeds every node's position from the model + // the first time it sees each id, same as on a brand new panel. + m_seededNodes.clear(); + m_selectedSegmentId = 0; + m_drawingSegment = false; + m_drawControls.clear(); + m_tool = Tool::Select; + if(m_editor){ + ed::SetCurrentEditor(m_editor); + ed::ClearSelection(); + ed::SetCurrentEditor(nullptr); + } + return true; +} diff --git a/GUI/panels/imguipanelwidget.h b/GUI/panels/imguipanelwidget.h new file mode 100644 index 0000000..4e8691b --- /dev/null +++ b/GUI/panels/imguipanelwidget.h @@ -0,0 +1,227 @@ +#ifndef IMGUIPANELWIDGET_H +#define IMGUIPANELWIDGET_H + +#include +#include +#include +#include + +#include +#include +#include + +#include "QtImGui.h" +#include "panelmodel.h" + +namespace ax { +namespace NodeEditor { +struct EditorContext; +} +} + +struct TrainUtilsState; + +/** + * Operate: clicking a turnout throws it; nothing on the panel can be moved or + * rotated (any drag is undone every frame -- see syncTurnoutTransforms()). + * Edit: clicking a turnout selects/drags it instead; the rotate handle and + * grid/angle snapping are available. Matches the QWidget implementation's + * m_allowMoving flag, split into two explicit, mutually exclusive modes. + */ +enum class PanelMode { + Operate, + Edit +}; + +/** + * Edit-mode tool. Select: click a node to drag/rotate it (Phase 2/3), click a + * segment to select it and drag its bezier handles. DrawSegment: click a pin + * to start a segment, click empty canvas to place up to two bezier control + * points, click a second pin to finish, or right-click empty canvas for a + * menu to insert a new node (today just Turnout) partway through. + */ +enum class Tool { + Select, + DrawSegment +}; + +/** + * Hosts a track panel. Everything inside the panel is drawn with Dear ImGui + * (via the vendored qtimgui backend) on top of imgui-node-editor -- this widget + * is the only Qt-aware part of the panel. + * + * Each widget owns its own ImGui context and its own node editor context, so + * several panels can be open in different docks at the same time. + */ +class ImguiPanelWidget : public QOpenGLWidget +{ + Q_OBJECT + +public: + explicit ImguiPanelWidget(TrainUtilsState* state, QWidget* parent = nullptr); + ~ImguiPanelWidget() override; + + QString getName() const; + void setName(QString name); + + /** Saves under the current name (see setName()/getName()). */ + bool save() const; + /** Renames (as setName() does) and saves under the new name. */ + bool saveAs(QString name); + /** Replaces this panel's whole model with panelName's saved contents and + * renames to match; false (model left untouched) if nothing could be + * loaded. Rebinding a loaded turnout's Turnout needs the TrainUtilsState + * passed to the constructor -- see PanelStorage::load(). */ + bool load(QString panelName); + +protected: + void initializeGL() override; + void paintGL() override; + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + +private: + /** + * Push Qt's current modifier state into ImGui. + * + * qtimgui only touches the modifiers from key events, and it assigns + * io.KeyCtrl/KeyShift/KeyAlt directly. Since ImGui 1.87 those fields are + * recomputed by NewFrame() from io.KeyMods, which is only fed by + * AddKeyEvent(ImGuiMod_*) -- and qtimgui's key map contains no modifier + * keys at all. Without this the modifiers would always read as released, + * which would break snap-to-grid, snap-rotate and the node editor's own + * ctrl-multiselect. Polling Qt also means modifiers work even when this + * widget does not hold keyboard focus. + * + * Must be called after QtImGui::newFrame(), which is what makes our ImGui + * context current and runs NewFrame(). + */ + bool ensureEditor(); + void applyKeyboardModifiers(); + + void drawFrame(); + void drawCanvas(); + void drawGrid(); + void drawToolbox(const struct ImGuiViewport* viewport); + /** The selected turnout's editable fields (panelfields.h), shown in the + * toolbox in Edit mode. Needs the editor current (to ask ed:: which node + * is selected), so it brackets its own SetCurrentEditor() call rather + * than relying on drawFrame()'s -- drawToolbox() runs after that's + * already been cleared for the frame. */ + void drawProperties(); + void drawDebugWindow(const struct ImGuiViewport* viewport); + + void drawTurnoutNode(TurnoutNode& node); + /** nodeTopLeft is in node-editor canvas space (as returned by GetItemRectMin() while inside ed::Begin/End). */ + void drawRotateHandle(TurnoutNode& node, ImVec2 nodeTopLeft); + /** Pulls each node's position back from the node editor after ed::End(), applying Ctrl + * grid-snap in Edit mode, or forcing it back to the model's position every frame in + * Operate mode so an accidental drag never sticks. */ + void syncTurnoutTransforms(); + /** Click-to-throw disambiguation (Operate mode only): mirrors the press/release timing + * and movement thresholds TurnoutDisplay::mousePressEvent/mouseReleaseEvent used, since + * the node editor -- not a per-widget event handler -- now owns the click/drag itself. */ + void handleOperateModeClicks(); + + // --- Track segments (Phase 4) --- + + /** A segment's endpoints/control points resolved to this frame's canvas positions. */ + struct ResolvedSegment { + ImVec2 a, b, ca, cb; + }; + /** Recomputes controlOffsetA/B from the endpoints when seg.straight, so a + * straight segment keeps tracking a moving/rotating endpoint. */ + ResolvedSegment resolveSegment(TrackSegmentEdge& seg) const; + void drawSegments(); + /** Bezier control handles for the selected segment: drawn/dragged in a Suspend + * block, same reasoning as drawRotateHandle(). */ + void drawSegmentHandles(TrackSegmentEdge& seg, const ResolvedSegment& r); + /** The dashed line while actively drawing a new segment, following the cursor. */ + void drawSegmentPreview(); + /** Select tool: hit-tests a left-click against every segment (only when it + * didn't land on a node/pin, which take priority). Must run after ed::End() -- + * see the note on handleSegmentDrawingClicks() for why. */ + void handleSegmentSelection(); + /** DrawSegment tool: the click-driven parts of the state machine described on + * the Tool enum (starting/finishing on a pin, placing control points on empty + * canvas). Must run after ed::End(): ed::IsBackgroundClicked() and + * ed::GetBackgroundClickButtonIndex() are only populated by ed::End()'s own + * per-frame action processing, so querying them earlier (e.g. from inside + * drawCanvas(), between ed::Begin() and ed::End()) always reads stale data -- + * confirmed by a precise click landing exactly on a rendered segment still + * failing to select it when this was called too early. */ + void handleSegmentDrawingClicks(); + /** DrawSegment tool: the right-click "insert a node here" popup. Must run + * before ed::End() (ed::ShowBackgroundContextMenu(), like ed::BeginDelete(), + * is part of the node editor's own per-frame processing inside Begin/End -- + * confirmed against imgui-node-editor's own blueprints-example.cpp). */ + void handleSegmentInsertMenu(); + /** Ends the in-progress segment by connecting its start to `end`, converting + * any placed control points into TrackSegmentEdge's endpoint-relative offsets. */ + void finishSegmentDraw(SegmentEnd end); + /** Inserts a Turnout node at canvasPos partway through drawing a segment: connects + * the in-progress segment to whichever of its 3 pins is nearest, then continues + * drawing from another pin on that same node. */ + void insertTurnoutIntoDraw(ImVec2 canvasPos); + /** Segment deletion (ours to handle) and node deletion (via ed::BeginDelete(), + * which PanelModel::removeTurnout() cascades to the segments attached to it). */ + void handleDelete(); + + // Turnout artwork is drawn inside a fixed square big enough that rotating it + // never clips (the diagonal of the 50x50 artwork is ~71px). + static constexpr float kNodeSize = 72.0f; + /** Consecutive paints at the same size before the editor is created. */ + static constexpr int kStableSizeFrames = 2; + /** Matches PanelDisplay::kGridSize in the QWidget implementation this replaces. */ + static constexpr float kGridSize = 10.0f; + /** Matches PanelDisplay::kRotationSnapDegrees. */ + static constexpr double kRotationSnapDegrees = 5.0; + /** Matches PanelDisplay::kRotateHandleDistance -- offset beyond the content's half-height. */ + static constexpr float kRotateHandleDistance = 20.0f; + static constexpr float kRotateHandleRadius = 6.0f; + /** Matches TurnoutDisplay::mouseReleaseEvent's click-vs-drag thresholds. */ + static constexpr float kClickMaxMovePixels = 10.0f; + static constexpr double kClickMaxSeconds = 1.0; + /** Matches TrackSegment::PADDING, the curve click/select tolerance. */ + static constexpr float kSegmentHitTolerance = 8.0f; + + /** Owned by MainWindow; lifetime covers this widget's -- may be null in + * tests, in which case connection binding fields are simply omitted. */ + TrainUtilsState* m_state = nullptr; + QtImGui::RenderRef m_imgui = nullptr; + ax::NodeEditor::EditorContext* m_editor = nullptr; + QTimer m_repaintTimer; + QString m_name; + bool m_imguiConfigured = false; + int m_frame = 0; + QSize m_lastSize; + int m_stableSizeFrames = 0; + ImVec2 m_dbgAvail; + PanelMode m_mode = PanelMode::Operate; + Tool m_tool = Tool::Select; + /** The node hovered when the left mouse button went down, awaiting release + * to decide whether it was a click (toggle) or the start of a drag. 0 = none. */ + PanelItemId m_clickCandidateId = 0; + ImVec2 m_clickStartMouse; + double m_clickStartTime = 0.0; + /** The node editor's own settings blob (node sizes, pan, zoom). */ + std::string m_editorSettings; + + PanelModel m_model; + /** Node ids the node editor has already been given an initial position for + * (see drawTurnoutNode() -- after the first frame, position is owned by + * the node editor/user dragging, not re-seeded from the model each frame). */ + std::set m_seededNodes; + + /** 0 = no segment selected. Segments aren't node-editor items, so their + * selection is tracked here rather than queried from ed::. */ + PanelItemId m_selectedSegmentId = 0; + /** Set while the DrawSegment tool has an in-progress segment (i.e. after + * the user has clicked a starting pin but before they've finished it). */ + bool m_drawingSegment = false; + SegmentEnd m_drawStart; + /** Canvas-space bezier control points placed so far this draw (0-2). */ + std::vector m_drawControls; +}; + +#endif // IMGUIPANELWIDGET_H diff --git a/GUI/panels/paneldisplay.cpp b/GUI/panels/paneldisplay.cpp deleted file mode 100644 index b5ee3cb..0000000 --- a/GUI/panels/paneldisplay.cpp +++ /dev/null @@ -1,536 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include "paneldisplay.h" -#include "panellayout.h" -#include "paneltoolswidget.h" - -static log4cxx::LoggerPtr logger = log4cxx::Logger::getLogger("traingui.PanelDisplay"); - -PanelDisplay::PanelDisplay(QWidget *parent) - : QWidget{parent} -{ - m_name = "Panel"; - setFixedSize(m_panelSize); -} - -void PanelDisplay::paintEvent(QPaintEvent *event){ - QWidget::paintEvent(event); - QPainter painter(this); - - // Draw border around the panel - { - QPen borderPen; - borderPen.setWidth(2); - borderPen.setBrush(Qt::black); - borderPen.setStyle(Qt::SolidLine); - painter.setPen(borderPen); - painter.drawRect(rect().adjusted(0, 0, -1, -1)); - } - - if(m_selectedWidget){ - QPen pen; - pen.setWidth(3); - pen.setBrush(Qt::green); - pen.setStyle(Qt::DashDotLine); - painter.setPen(pen); - painter.setBrush(Qt::NoBrush); - - TurnoutDisplay* rotatable = dynamic_cast(m_selectedWidget); - if(rotatable){ - // Draw a box around the turnout's actual (rotated) artwork, - // not its padded, always-axis-aligned widget bounds. - QRect content = rotatable->contentRect(); - - painter.save(); - painter.translate(rotatable->pos()); - painter.translate(content.center()); - painter.rotate(rotatable->rotation()); - painter.translate(-content.center()); - painter.drawRect(content); - painter.restore(); - - QPoint center = rotatable->pos() + content.center(); - QPoint handlePos = rotateHandlePos(rotatable); - - QPen handleLinePen; - handleLinePen.setWidth(1); - handleLinePen.setBrush(Qt::darkGray); - handleLinePen.setStyle(Qt::DashLine); - painter.setPen(handleLinePen); - painter.drawLine(center, handlePos); - - QPen handlePen; - handlePen.setWidth(2); - handlePen.setBrush(Qt::darkGreen); - painter.setPen(handlePen); - painter.setBrush(Qt::white); - painter.drawEllipse(handlePos, kRotateHandleRadius, kRotateHandleRadius); - painter.setBrush(Qt::NoBrush); - }else if(TrackSegment* selectedSegment = dynamic_cast(m_selectedWidget)){ - // Draw the bezier control handles instead of a bounding box - QPoint a = selectedSegment->endpointA(); - QPoint b = selectedSegment->endpointB(); - QPoint controlA = selectedSegment->controlPointA(); - QPoint controlB = selectedSegment->controlPointB(); - - QPen handleLinePen; - handleLinePen.setWidth(1); - handleLinePen.setBrush(Qt::darkGray); - handleLinePen.setStyle(Qt::DashLine); - painter.setPen(handleLinePen); - painter.drawLine(a, controlA); - painter.drawLine(b, controlB); - - QPen handlePen; - handlePen.setWidth(2); - handlePen.setBrush(Qt::darkGreen); - painter.setPen(handlePen); - painter.setBrush(Qt::white); - painter.drawEllipse(controlA, kRotateHandleRadius, kRotateHandleRadius); - painter.drawEllipse(controlB, kRotateHandleRadius, kRotateHandleRadius); - painter.setBrush(Qt::NoBrush); - }else{ - // Draw a box around the widget - QPoint topLeft = m_selectedWidget->pos(); - QPoint topRight(topLeft.x() + m_selectedWidget->size().width(), topLeft.y()); - QPoint bottomRight(topLeft.x() + m_selectedWidget->size().width(), topLeft.y() + m_selectedWidget->size().height()); - QPoint bottomLeft(topLeft.x(), topLeft.y() + m_selectedWidget->size().height()); - QPainterPath path; - path.moveTo(topLeft); - path.lineTo(topRight); - path.lineTo(bottomRight); - path.lineTo(bottomLeft); - path.lineTo(topLeft); - - painter.drawPath(path); - } - } - - if(m_drawConnectionPoints){ - // Rebuild the cached list of all connection points - m_connectionPoints.clear(); - for(TurnoutDisplay* disp : m_turnouts){ - int idx = 0; - for(const QPoint& p : disp->connectionPoints()){ - QPoint center = disp->pos() + p; - m_connectionPoints.push_back({disp, idx, center}); - idx++; - } - } - for(const SegmentConnection& sc : m_segments){ - int idx = 0; - for(const QPoint& p : sc.segment->connectionPoints()){ - m_connectionPoints.push_back({sc.segment, idx++, sc.segment->pos() + p}); - } - } - - for(const CachedConnectionPoint& cp : m_connectionPoints){ - painter.save(); - QPoint topLeft(cp.center.x() - 5, cp.center.y() - 5); - painter.translate(topLeft); - - // a connection point is just a white semi-transparent box - // with a black border - QRect box(0, 0, 10, 10); - - QBrush semiTransparent(QColor(255, 255, 255, 128)); - painter.fillRect(box, semiTransparent); - - QPainterPath path; - path.addRect(box); - - QPen pen; - pen.setWidth(2); - pen.setBrush(Qt::black); - painter.setPen(pen); - - painter.drawPath(path); - painter.restore(); - } - - // Draw the in-progress connection line while dragging - if(m_connectingState == ConnectingState::Connecting){ - QWidget* startWidget = dynamic_cast(m_startEndpoint.connectable); - QPoint startCenter = startWidget->pos() - + m_startEndpoint.connectable->connectionPoints()[m_startEndpoint.index]; - QPen linePen; - linePen.setWidth(2); - linePen.setBrush(Qt::darkGray); - linePen.setStyle(Qt::DashLine); - painter.setPen(linePen); - painter.drawLine(startCenter, m_currentMousePos); - } - } -} - -void PanelDisplay::addTurnout(std::shared_ptr turnout){ - TurnoutDisplay* td = new TurnoutDisplay(this); - td->setTurnout(turnout); - td->setGeometry(50, 50, td->width(), td->height()); - td->setVisible(true); - td->configureInteraction(!m_allowMoving && !m_drawConnectionPoints); - td->installEventFilter(this); - m_turnouts.push_back(td); - - connect(td, &TurnoutDisplay::connectionPointsUpdated, - this, &PanelDisplay::connectionPointsUpdated); -} - -QSize PanelDisplay::sizeHint() const -{ - return m_panelSize; -} - -void PanelDisplay::setPanelSize(QSize size) { - m_panelSize = size; - setFixedSize(size); -} - -void PanelDisplay::mousePressEvent(QMouseEvent* event){ - QWidget* widgetAtPos = childAt(event->pos()); - - // A track segment's actual clickable curve can be much thinner than its - // padded bounding box, which can also be covered by an overlapping - // sibling (e.g. a turnout's bounding box). Prefer an actual curve hit - // over whatever childAt() finds geometrically, so clicking a segment - // near a turnout still selects the segment. - for(const SegmentConnection& sc : m_segments){ - if(sc.segment->hitTest(event->pos())){ - widgetAtPos = sc.segment; - break; - } - } - - LOG4CXX_DEBUG_FMT(logger, "press button: {} pos: {},{} widget: {}", - (int)event->button(), - event->pos().x(), - event->pos().y(), - widgetAtPos ? "valid" : "invalid"); - - // First let's check to see if we are selecting a connection point - if(m_drawConnectionPoints && event->button() == Qt::LeftButton){ - QPoint mousePos = event->pos(); - for(const CachedConnectionPoint& cp : m_connectionPoints){ - QRect box(cp.center.x() - 5, cp.center.y() - 5, 10, 10); - if(box.contains(mousePos)){ - m_connectingState = ConnectingState::Connecting; - m_startEndpoint = {cp.connectable, cp.index}; - m_currentMousePos = mousePos; - return; - } - } - return; - } - - // Check for a hit on the rotate handle of the currently selected turnout, - // or a bezier control handle of the currently selected segment. - if(event->button() == Qt::LeftButton && tryStartHandleDrag(event->pos())){ - return; - } - - if(!widgetAtPos){ - m_selectedWidget = nullptr; - m_tools->setCurrentSelectedWidget(nullptr); - update(this->rect()); - return; - } - m_tools->setCurrentSelectedWidget(widgetAtPos); - - if(event->button() == Qt::RightButton && m_allowMoving){ - m_selectedWidget = widgetAtPos; - m_movingWidgetStart = widgetAtPos->pos(); - m_mouseStart = event->pos(); - }else{ - m_selectedWidget = nullptr; - } - - update(this->rect()); -} - -void PanelDisplay::mouseMoveEvent(QMouseEvent *event){ - if(m_connectingState == ConnectingState::Connecting){ - m_currentMousePos = event->pos(); - update(this->rect()); - return; - } - - if(m_rotatingWidget){ - QPoint delta = event->pos() - m_rotationCenter; - // 0 degrees = straight up, positive = clockwise, matching QPainter::rotate() - double angle = std::atan2(delta.x(), -delta.y()) * 180.0 / std::numbers::pi; - if(event->modifiers() & Qt::ControlModifier){ - angle = std::lround(angle / kRotationSnapDegrees) * kRotationSnapDegrees; - } - m_rotatingWidget->setRotation(angle); - updateAttachedSegments(m_rotatingWidget); - update(this->rect()); - return; - } - - if(m_draggingControlSegment){ - if(m_draggingControlIsA){ - m_draggingControlSegment->setControlPointA(event->pos()); - }else{ - m_draggingControlSegment->setControlPointB(event->pos()); - } - update(this->rect()); - return; - } - - if(m_selectedWidget == nullptr){ - return; - } - - // Track segments aren't independently movable -- they only follow - // their connected endpoints (see updateAttachedSegments). - if(dynamic_cast(m_selectedWidget)){ - return; - } - - int newX = m_movingWidgetStart.x(); - int newY = m_movingWidgetStart.y(); - int diffX = std::abs(m_mouseStart.x() - event->pos().x()); - int diffY = std::abs(m_mouseStart.y() - event->pos().y()); - if(event->pos().x() < m_mouseStart.x()){ - newX -= diffX; - }else{ - newX += diffX; - } - if(event->pos().y() < m_mouseStart.y()){ - newY -= diffY; - }else{ - newY += diffY; - } - - // Snap to an invisible grid while Ctrl is held - if(event->modifiers() & Qt::ControlModifier){ - newX = std::lround(newX / static_cast(kGridSize)) * kGridSize; - newY = std::lround(newY / static_cast(kGridSize)) * kGridSize; - } - - // Clamp so the widget stays within the panel bounds - newX = std::max(0, std::min(newX, width() - m_selectedWidget->width())); - newY = std::max(0, std::min(newY, height() - m_selectedWidget->height())); - - m_selectedWidget->setGeometry(newX, newY, m_selectedWidget->width(), m_selectedWidget->height()); - - // Update any segments whose endpoints are attached to the moved widget - Connectable* movedConnectable = dynamic_cast(m_selectedWidget); - if(movedConnectable){ - updateAttachedSegments(movedConnectable); - } - - update(this->rect()); -} - -void PanelDisplay::mouseReleaseEvent(QMouseEvent* event){ - if(m_rotatingWidget && event->button() == Qt::LeftButton){ - m_rotatingWidget = nullptr; - releaseMouse(); - update(this->rect()); - return; - } - - if(m_draggingControlSegment && event->button() == Qt::LeftButton){ - m_draggingControlSegment = nullptr; - releaseMouse(); - update(this->rect()); - return; - } - - if(m_connectingState != ConnectingState::Connecting || event->button() != Qt::LeftButton){ - return; - } - - m_connectingState = ConnectingState::NotConnecting; - - QPoint mousePos = event->pos(); - for(const CachedConnectionPoint& cp : m_connectionPoints){ - // Don't connect a point to itself - if(cp.connectable == m_startEndpoint.connectable && cp.index == m_startEndpoint.index){ - continue; - } - QRect box(cp.center.x() - 5, cp.center.y() - 5, 10, 10); - if(box.contains(mousePos)){ - createSegment(m_startEndpoint, {cp.connectable, cp.index}); - update(this->rect()); - return; - } - } - - update(this->rect()); -} - -QPoint PanelDisplay::endpointPos(const ConnectionEndpoint& ep) const { - return dynamic_cast(ep.connectable)->pos() - + ep.connectable->connectionPoints()[ep.index]; -} - -void PanelDisplay::updateAttachedSegments(Connectable* connectable) { - for(SegmentConnection& sc : m_segments){ - if(sc.a.connectable == connectable || sc.b.connectable == connectable){ - sc.segment->setEndpoints(endpointPos(sc.a), endpointPos(sc.b)); - } - } -} - -TrackSegment* PanelDisplay::createSegment(ConnectionEndpoint a, ConnectionEndpoint b) { - TrackSegment* seg = new TrackSegment(this); - seg->setEndpoints(endpointPos(a), endpointPos(b)); - seg->configureInteraction(!m_drawConnectionPoints); - seg->setVisible(true); - seg->lower(); - seg->installEventFilter(this); - m_segments.push_back({seg, a, b}); - connect(seg, &TrackSegment::connectionPointsUpdated, - this, &PanelDisplay::connectionPointsUpdated); - return seg; -} - -QPoint PanelDisplay::rotateHandlePos(TurnoutDisplay* td) const { - QRect content = td->contentRect(); - QPoint center = td->pos() + content.center(); - double rad = td->rotation() * std::numbers::pi / 180.0; - double dist = content.height() / 2.0 + kRotateHandleDistance; - int dx = std::lround(std::sin(rad) * dist); - int dy = std::lround(-std::cos(rad) * dist); - return center + QPoint(dx, dy); -} - -bool PanelDisplay::tryStartHandleDrag(QPoint panelPos) { - if(!m_selectedWidget){ - return false; - } - - TurnoutDisplay* rotatable = dynamic_cast(m_selectedWidget); - if(rotatable){ - QPoint handlePos = rotateHandlePos(rotatable); - QRect handleRect(handlePos.x() - kRotateHandleRadius, handlePos.y() - kRotateHandleRadius, - kRotateHandleRadius * 2, kRotateHandleRadius * 2); - if(handleRect.contains(panelPos)){ - m_rotatingWidget = rotatable; - m_rotationCenter = rotatable->pos() + rotatable->contentRect().center(); - grabMouse(); - return true; - } - return false; - } - - TrackSegment* selectedSegment = dynamic_cast(m_selectedWidget); - if(selectedSegment){ - QPoint controlA = selectedSegment->controlPointA(); - QPoint controlB = selectedSegment->controlPointB(); - QRect controlARect(controlA.x() - kRotateHandleRadius, controlA.y() - kRotateHandleRadius, - kRotateHandleRadius * 2, kRotateHandleRadius * 2); - QRect controlBRect(controlB.x() - kRotateHandleRadius, controlB.y() - kRotateHandleRadius, - kRotateHandleRadius * 2, kRotateHandleRadius * 2); - if(controlARect.contains(panelPos)){ - m_draggingControlSegment = selectedSegment; - m_draggingControlIsA = true; - grabMouse(); - return true; - } - if(controlBRect.contains(panelPos)){ - m_draggingControlSegment = selectedSegment; - m_draggingControlIsA = false; - grabMouse(); - return true; - } - } - return false; -} - -bool PanelDisplay::eventFilter(QObject* watched, QEvent* event) { - // Handles (the turnout rotate handle, bezier control handles) are drawn - // by PanelDisplay itself, on top of everything, but a click there can - // still land on a normal, interactive child widget underneath (e.g. a - // turnout) which would otherwise consume it for its own click handling - // before PanelDisplay::mousePressEvent ever sees it. Intercept clicks on - // an active handle here, before the child gets a chance to. - if(event->type() == QEvent::MouseButtonPress){ - QWidget* childWidget = qobject_cast(watched); - QMouseEvent* mouseEvent = static_cast(event); - if(childWidget && mouseEvent->button() == Qt::LeftButton){ - QPoint panelPos = childWidget->pos() + mouseEvent->pos(); - if(tryStartHandleDrag(panelPos)){ - return true; - } - } - } - return QWidget::eventFilter(watched, event); -} - -void PanelDisplay::setPanelToolsWidget(PanelToolsWidget* widget){ - m_tools = widget; - - connect(widget, &PanelToolsWidget::addDCCTurnout, - this, &PanelDisplay::addBlankTurnout); - connect(widget, &PanelToolsWidget::drawConnectionPointsChanged, - this, &PanelDisplay::drawConnectionPointsChanged); -} - -void PanelDisplay::allowMovingChanged(bool allow_moving){ - m_allowMoving = allow_moving; - - if(!m_allowMoving){ - m_selectedWidget = nullptr; - } - - for(TurnoutDisplay* td : m_turnouts){ - td->configureInteraction(!allow_moving); - } - for(SegmentConnection& sc : m_segments){ - sc.segment->configureInteraction(!allow_moving); - } -} - -void PanelDisplay::addBlankTurnout(){ - LOG4CXX_DEBUG(logger, "Adding blank turnout"); - TurnoutDisplay* td = new TurnoutDisplay(this); - td->setGeometry(10, 10, td->width(), td->height()); - td->setVisible(true); - td->configureInteraction(!m_allowMoving && !m_drawConnectionPoints); - td->installEventFilter(this); - m_turnouts.push_back(td); - update(this->rect()); - td->update(); - - connect(td, &TurnoutDisplay::connectionPointsUpdated, - this, &PanelDisplay::connectionPointsUpdated); -} - -void PanelDisplay::connectionPointsUpdated(){ - update(this->rect()); -} - -void PanelDisplay::drawConnectionPointsChanged(bool connection_points){ - m_drawConnectionPoints = connection_points; - if(!connection_points){ - m_connectingState = ConnectingState::NotConnecting; - } - for(TurnoutDisplay* td : m_turnouts){ - td->configureInteraction(!connection_points); - } - for(SegmentConnection& sc : m_segments){ - sc.segment->configureInteraction(!connection_points); - } - update(this->rect()); -} - -void PanelDisplay::setName(QString name){ - m_name = name; -} - -QString PanelDisplay::getName(){ - return m_name; -} diff --git a/GUI/panels/paneldisplay.h b/GUI/panels/paneldisplay.h deleted file mode 100644 index 0c1d101..0000000 --- a/GUI/panels/paneldisplay.h +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef PANELDISPLAY_H -#define PANELDISPLAY_H - -#include -#include "turnoutdisplay.h" -#include "tracksegment.h" - -class PanelToolsWidget; - -class PanelDisplay : public QWidget -{ - Q_OBJECT - Q_PROPERTY(QString getName READ getName WRITE setName) - -public: - explicit PanelDisplay(QWidget *parent = nullptr); - - void setPanelToolsWidget(PanelToolsWidget* widget); - - void addTurnout(std::shared_ptr turnout); - - void setName(QString name); - QString getName(); - -Q_SIGNALS: - -protected: - QSize sizeHint() const override; - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - bool eventFilter(QObject* watched, QEvent* event) override; - -public Q_SLOTS: - void allowMovingChanged(bool allow_moving); - void drawConnectionPointsChanged(bool connection_points); - void addBlankTurnout(); - void connectionPointsUpdated(); - void setPanelSize(QSize size); - -private: - static constexpr int kGridSize = 10; - static constexpr int kRotateHandleRadius = 6; - static constexpr int kRotateHandleDistance = 20; - static constexpr double kRotationSnapDegrees = 5.0; - - enum class ConnectingState{ - NotConnecting, - Connecting, - }; - - struct ConnectionEndpoint { - Connectable* connectable = nullptr; - int index = -1; - }; - - // Cached per-frame list rebuilt during paintEvent - struct CachedConnectionPoint { - Connectable* connectable; - int index; - QPoint center; // in PanelDisplay coordinates - }; - - struct SegmentConnection { - TrackSegment* segment; - ConnectionEndpoint a; - ConnectionEndpoint b; - }; - - QPoint endpointPos(const ConnectionEndpoint& ep) const; - TrackSegment* createSegment(ConnectionEndpoint a, ConnectionEndpoint b); - void updateAttachedSegments(Connectable* connectable); - QPoint rotateHandlePos(TurnoutDisplay* td) const; - // If panelPos hits the rotate handle of the selected turnout, or a - // bezier control handle of the selected segment, starts dragging it - // (grabbing the mouse so the whole drag routes here regardless of - // which child widget is under the cursor) and returns true. - bool tryStartHandleDrag(QPoint panelPos); - - QVector m_turnouts; - QVector m_segments; - QWidget* m_selectedWidget = nullptr; - QPoint m_movingWidgetStart; - QPoint m_mouseStart; - TurnoutDisplay* m_rotatingWidget = nullptr; - QPoint m_rotationCenter; - TrackSegment* m_draggingControlSegment = nullptr; - bool m_draggingControlIsA = false; - bool m_editing = false; - PanelToolsWidget* m_tools = nullptr; - bool m_allowMoving = false; - bool m_drawConnectionPoints = false; - QVector m_connectionPoints; - ConnectingState m_connectingState = ConnectingState::NotConnecting; - ConnectionEndpoint m_startEndpoint; - QPoint m_currentMousePos; - - // non-GUI properties - QString m_name; - QSize m_panelSize = QSize(1280, 800); -}; - -#endif // PANELDISPLAY_H diff --git a/GUI/panels/panelfields.cpp b/GUI/panels/panelfields.cpp new file mode 100644 index 0000000..27a1049 --- /dev/null +++ b/GUI/panels/panelfields.cpp @@ -0,0 +1,144 @@ +#include + +#include "panelfields.h" +#include "../systemconnection.h" +#include "../trainutils_state.h" + +namespace { + +// Re-resolves node.turnout from whichever connection/address are currently +// stored on it. Called after either field changes so the two always agree -- +// mirrors what SystemConnection::getDCCTurnout() itself does (idempotent, +// cached by switch number), so flipping back to a previous address/connection +// pair returns the same Turnout rather than creating a duplicate. +void rebindTurnout(TurnoutNode& node, TrainUtilsState* state){ + if(!state || node.connectionName.isEmpty() || node.dccAddress <= 0){ + node.turnout.reset(); + return; + } + std::shared_ptr conn = TrainUtils::connectionByName(state, node.connectionName); + node.turnout = conn ? conn->getDCCTurnout(node.dccAddress) : nullptr; +} + +} // namespace + +std::vector fieldsFor(TurnoutNode& node, TrainUtilsState* state){ + std::vector fields; + + fields.push_back(PanelField{ + "Name", PanelField::Kind::Text, + [&node](){ return QVariant(node.name); }, + [&node](const QVariant& v){ node.name = v.toString(); }, + {}, 0.0, 0.0, + }); + + fields.push_back(PanelField{ + "Hand", PanelField::Kind::Enum, + [&node](){ return QVariant(node.hand == TurnoutHand::Left ? 0 : 1); }, + [&node](const QVariant& v){ node.hand = (v.toInt() == 0) ? TurnoutHand::Left : TurnoutHand::Right; }, + QStringList{"Left", "Right"}, 0.0, 0.0, + }); + + fields.push_back(PanelField{ + "Rotation", PanelField::Kind::Double, + [&node](){ return QVariant(node.rotationDegrees); }, + [&node](const QVariant& v){ node.rotationDegrees = v.toDouble(); }, + {}, -360.0, 360.0, + }); + + // "(none)" plus every currently open connection's name -- rebuilt fresh + // each frame (fieldsFor() is only ever called for the current frame's + // properties panel), so a newly opened/closed connection shows up + // immediately without this panel needing to know when that happens. + QStringList connectionNames{"(none)"}; + if(state){ + for(const std::shared_ptr& conn : state->m_connections){ + connectionNames << conn->name(); + } + } + fields.push_back(PanelField{ + "Connection", PanelField::Kind::Enum, + [&node, connectionNames](){ + const int idx = connectionNames.indexOf(node.connectionName); + return QVariant(idx < 0 ? 0 : idx); + }, + [&node, state, connectionNames](const QVariant& v){ + const int idx = v.toInt(); + node.connectionName = (idx <= 0) ? QString() : connectionNames[idx]; + rebindTurnout(node, state); + }, + connectionNames, 0.0, 0.0, + }); + + fields.push_back(PanelField{ + "DCC Address", PanelField::Kind::Int, + [&node](){ return QVariant(node.dccAddress); }, + [&node, state](const QVariant& v){ + node.dccAddress = v.toInt(); + rebindTurnout(node, state); + }, + {}, 0.0, 2048.0, + }); + + return fields; +} + +void renderPanelFields(std::vector& fields){ + constexpr float kLabelColumnWidth = 110.0f; + + for(PanelField& field : fields){ + ImGui::PushID(field.label.toUtf8().constData()); + ImGui::TextUnformatted(field.label.toUtf8().constData()); + ImGui::SameLine(kLabelColumnWidth); + ImGui::SetNextItemWidth(140.0f); + + switch(field.kind){ + case PanelField::Kind::Text: { + const std::string current = field.get().toString().toStdString(); + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s", current.c_str()); + if(ImGui::InputText("##value", buf, sizeof(buf))){ + field.set(QVariant(QString::fromUtf8(buf))); + } + break; + } + case PanelField::Kind::Double: { + float value = static_cast(field.get().toDouble()); + if(ImGui::DragFloat("##value", &value, 1.0f, + static_cast(field.min), static_cast(field.max), "%.1f")){ + field.set(QVariant(static_cast(value))); + } + break; + } + case PanelField::Kind::Int: { + int value = field.get().toInt(); + if(ImGui::DragInt("##value", &value, 1.0f, + static_cast(field.min), static_cast(field.max))){ + field.set(QVariant(value)); + } + break; + } + case PanelField::Kind::Enum: { + // ImGui::Combo needs the label strings to outlive the call, so copy + // them into local storage rather than pointing at temporaries. + std::vector storage; + storage.reserve(field.enumLabels.size()); + for(const QString& label : field.enumLabels){ + storage.push_back(label.toStdString()); + } + std::vector items; + items.reserve(storage.size()); + for(const std::string& s : storage){ + items.push_back(s.c_str()); + } + int current = field.get().toInt(); + if(ImGui::Combo("##value", ¤t, items.data(), static_cast(items.size()))){ + field.set(QVariant(current)); + } + break; + } + } + + ImGui::PopID(); + } +} diff --git a/GUI/panels/panelfields.h b/GUI/panels/panelfields.h new file mode 100644 index 0000000..d5a99aa --- /dev/null +++ b/GUI/panels/panelfields.h @@ -0,0 +1,43 @@ +#ifndef PANELFIELDS_H +#define PANELFIELDS_H + +#include +#include + +#include +#include + +#include "panelmodel.h" + +struct TrainUtilsState; + +/** + * One editable field in the properties panel: a label plus get/set closures + * over whichever item is currently selected. Replaces QMetaProperty + * reflection (the old QWidget panel's approach, which cannot survive the + * move off QWidgets) with an explicit, per-type list built fresh each frame + * by fieldsFor() below and rendered generically by renderPanelFields(). + */ +struct PanelField { + enum class Kind { Text, Double, Int, Enum }; + + QString label; + Kind kind = Kind::Text; + std::function get; + std::function set; + QStringList enumLabels; // Kind::Enum only + double min = 0.0, max = 0.0; // Kind::Double/Int only +}; + +/** + * Name/hand/rotation plus connection + DCC address binding. Setting either + * of the last two re-resolves node.turnout via SystemConnection::getDCCTurnout() + * (or clears it if no connection/address is chosen), which is what makes a + * decorative, freshly-added turnout into one that actually throws over the bus. + */ +std::vector fieldsFor(TurnoutNode& node, TrainUtilsState* state); + +/** Renders each field with the ImGui widget matching its Kind, calling set() on edit. */ +void renderPanelFields(std::vector& fields); + +#endif // PANELFIELDS_H diff --git a/GUI/panels/panellayout.cpp b/GUI/panels/panellayout.cpp deleted file mode 100644 index 7f7bdbf..0000000 --- a/GUI/panels/panellayout.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "panellayout.h" - -PanelLayout::PanelLayout(QWidget *parent) - : QLayout{parent} -{ - -} - -void PanelLayout::addItem(QLayoutItem *item){ - m_items.push_back(item); -} - -QSize PanelLayout::sizeHint() const{ - return QSize(400,400); -} - -void PanelLayout::setGeometry(const QRect& rect){ - QLayout::setGeometry(rect); -} - -QLayoutItem *PanelLayout::itemAt(int index) const{ - if(m_items.length() <= index){ - return m_items[index]; - } - return nullptr; -} - -QLayoutItem *PanelLayout::takeAt(int index){ - if(m_items.length() <= index){ - QLayoutItem* item = m_items[index]; - m_items.erase(m_items.begin() + index); - return item; - } - return nullptr; -} - -int PanelLayout::count() const{ - return m_items.size(); -} diff --git a/GUI/panels/panellayout.h b/GUI/panels/panellayout.h deleted file mode 100644 index b2759c3..0000000 --- a/GUI/panels/panellayout.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef PANELLAYOUT_H -#define PANELLAYOUT_H - -#include -#include -#include - -class PanelLayout : public QLayout -{ - Q_OBJECT -public: - explicit PanelLayout(QWidget *parent = nullptr); - - void addItem(QLayoutItem *item) override; - QSize sizeHint() const override; - void setGeometry(const QRect& rect) override; - QLayoutItem *itemAt(int index) const override; - QLayoutItem *takeAt(int index) override; - int count() const; - -Q_SIGNALS: - -private: - QVector m_items; - -}; - -#endif // PANELLAYOUT_H diff --git a/GUI/panels/panelmodel.cpp b/GUI/panels/panelmodel.cpp new file mode 100644 index 0000000..a30e1ac --- /dev/null +++ b/GUI/panels/panelmodel.cpp @@ -0,0 +1,189 @@ +#include +#include + +#include "panelmodel.h" + +namespace { + +// Rotate p about (cw/2, ch/2) by rotationDegrees, matching QTransform::rotate()'s +// convention (screen/Y-down coordinates, positive = clockwise) -- the same +// convention TurnoutDisplay::paintEvent()/updateConnectionPoints() used. +ImVec2 rotateAboutContentCenter(ImVec2 p, double rotationDegrees, float cw, float ch){ + if(rotationDegrees == 0.0){ + return p; + } + const float cx = cw / 2.0f; + const float cy = ch / 2.0f; + const double rad = rotationDegrees * std::numbers::pi / 180.0; + const float cosA = static_cast(std::cos(rad)); + const float sinA = static_cast(std::sin(rad)); + const float dx = p.x - cx; + const float dy = p.y - cy; + return ImVec2(cx + dx * cosA - dy * sinA, + cy + dx * sinA + dy * cosA); +} + +// Applies, in order, the same transform stack the QWidget implementation +// applied (turnoutdisplay.cpp): rotate about the content centre, then mirror +// vertically for a Left-handed turnout, then place within the node's padded +// bounding box. +ImVec2 turnoutLocalToNode(ImVec2 p, TurnoutHand hand, double rotationDegrees){ + constexpr float cw = kTurnoutContentSize; + constexpr float ch = kTurnoutContentSize; + + p = rotateAboutContentCenter(p, rotationDegrees, cw, ch); + + if(hand == TurnoutHand::Left){ + p.y = ch - p.y; + } + + // Centre the cw x ch content within the padded, fixed-size node box (see + // kNodeSize in imguipanelwidget.h -- currently 72, giving 11px of padding + // on each side so a full rotation never clips the bounding box). + constexpr float kNodeSize = 72.0f; + constexpr float contentOffset = (kNodeSize - cw) / 2.0f; + p.x += contentOffset; + p.y += contentOffset; + return p; +} + +} // namespace + +TurnoutGeometry turnoutGeometry(TurnoutHand hand, double rotationDegrees){ + constexpr float cw = kTurnoutContentSize; + constexpr float ch = kTurnoutContentSize; + + TurnoutGeometry geo; + + // Through leg. + geo.legs[0][0] = turnoutLocalToNode(ImVec2(0.0f, ch / 4.0f), hand, rotationDegrees); + geo.legs[0][1] = turnoutLocalToNode(ImVec2(cw, ch / 4.0f), hand, rotationDegrees); + + // Diverging diagonal. + ImVec2 divergeStart(cw / 3.0f, ch / 4.0f); + ImVec2 divergeEnd(cw / 2.0f + cw / 4.0f, ch / 2.0f + ch / 4.0f); + geo.legs[1][0] = turnoutLocalToNode(divergeStart, hand, rotationDegrees); + geo.legs[1][1] = turnoutLocalToNode(divergeEnd, hand, rotationDegrees); + + // Diverging outgoing leg. + geo.legs[2][0] = turnoutLocalToNode(divergeEnd, hand, rotationDegrees); + geo.legs[2][1] = turnoutLocalToNode(ImVec2(cw, ch / 2.0f + ch / 4.0f), hand, rotationDegrees); + + // Anchor points: 0 incoming, 1 normal outgoing, 2 diverged outgoing. + geo.pins[0] = turnoutLocalToNode(ImVec2(0.0f, ch / 4.0f), hand, rotationDegrees); + geo.pins[1] = turnoutLocalToNode(ImVec2(cw, ch / 4.0f), hand, rotationDegrees); + geo.pins[2] = turnoutLocalToNode(ImVec2(cw, ch / 2.0f + ch / 4.0f), hand, rotationDegrees); + + return geo; +} + +TurnoutNode& PanelModel::addTurnout(ImVec2 position){ + TurnoutNode node; + node.id = m_nextId++; + node.name = QString("Turnout %1").arg(node.id); + node.position = position; + m_turnouts.push_back(node); + return m_turnouts.back(); +} + +TurnoutNode& PanelModel::addTurnoutWithId(PanelItemId id, ImVec2 position){ + TurnoutNode node; + node.id = id; + node.name = QString("Turnout %1").arg(id); + node.position = position; + m_turnouts.push_back(node); + if(id >= m_nextId){ + m_nextId = id + 1; + } + return m_turnouts.back(); +} + +TurnoutNode* PanelModel::findTurnout(PanelItemId id){ + for(TurnoutNode& node : m_turnouts){ + if(node.id == id){ + return &node; + } + } + return nullptr; +} + +void PanelModel::removeTurnout(PanelItemId id){ + for(auto it = m_turnouts.begin(); it != m_turnouts.end(); ++it){ + if(it->id == id){ + m_turnouts.erase(it); + break; + } + } + + // A segment attached to a node that no longer exists has nowhere sensible + // to anchor itself, so it goes too, rather than silently collapsing to (0,0). + for(auto it = m_segments.begin(); it != m_segments.end();){ + if(it->a.nodeId == id || it->b.nodeId == id){ + it = m_segments.erase(it); + }else{ + ++it; + } + } +} + +TrackSegmentEdge& PanelModel::addSegment(SegmentEnd a, SegmentEnd b){ + TrackSegmentEdge seg; + seg.id = m_nextId++; + seg.a = a; + seg.b = b; + m_segments.push_back(seg); + return m_segments.back(); +} + +TrackSegmentEdge& PanelModel::addSegmentWithId(PanelItemId id, SegmentEnd a, SegmentEnd b){ + TrackSegmentEdge seg; + seg.id = id; + seg.a = a; + seg.b = b; + m_segments.push_back(seg); + if(id >= m_nextId){ + m_nextId = id + 1; + } + return m_segments.back(); +} + +TrackSegmentEdge* PanelModel::findSegment(PanelItemId id){ + for(TrackSegmentEdge& seg : m_segments){ + if(seg.id == id){ + return &seg; + } + } + return nullptr; +} + +void PanelModel::removeSegment(PanelItemId id){ + for(auto it = m_segments.begin(); it != m_segments.end(); ++it){ + if(it->id == id){ + m_segments.erase(it); + return; + } + } +} + +ImVec2 resolveSegmentEnd(const PanelModel& model, const SegmentEnd& end){ + if(end.nodeId == 0){ + return end.freePos; + } + for(const TurnoutNode& node : model.turnouts()){ + if(node.id == end.nodeId){ + const TurnoutGeometry geo = turnoutGeometry(node.hand, node.rotationDegrees); + const ImVec2& pin = geo.pins[end.pinIndex]; + return ImVec2(node.position.x + kNodeContentPadding + pin.x, + node.position.y + kNodeContentPadding + pin.y); + } + } + // Referenced node is gone (shouldn't normally happen -- removeTurnout() + // cascades -- but fall back to something rather than an uninitialized point). + return end.freePos; +} + +void PanelModel::clear(){ + m_turnouts.clear(); + m_segments.clear(); + m_nextId = 1; +} diff --git a/GUI/panels/panelmodel.h b/GUI/panels/panelmodel.h new file mode 100644 index 0000000..d948d34 --- /dev/null +++ b/GUI/panels/panelmodel.h @@ -0,0 +1,168 @@ +#ifndef PANELMODEL_H +#define PANELMODEL_H + +#include +#include +#include + +#include +#include + +class Turnout; +class PanelModel; + +/** + * Identifies an item on a panel (a turnout node, a track segment, ...), + * independent of how imgui-node-editor numbers its own nodes/pins. Never 0. + */ +using PanelItemId = uint32_t; + +enum class TurnoutHand { + Left, + Right +}; + +/** + * Everything a turnout node needs to be drawn, saved and reloaded. + * Framework-agnostic: no Qt widgets, no ImGui/node-editor calls. + */ +struct TurnoutNode { + PanelItemId id = 0; + QString name; + TurnoutHand hand = TurnoutHand::Right; + double rotationDegrees = 0.0; + /** Canvas coordinates (node-editor space), node top-left. */ + ImVec2 position{0.0f, 0.0f}; + + /** Which SystemConnection this is bound to, for rebinding on load. */ + QString connectionName; + int dccAddress = 0; + /** May be null: a turnout dropped on the panel with no connection/address + * chosen yet is decorative only, same as the old addBlankTurnout(). */ + std::shared_ptr turnout; +}; + +/** + * Canonical turnout artwork and anchor-point geometry, shared by rendering + * and hit-testing so they can never drift out of sync -- the QWidget + * implementation this replaces computed the same numbers twice, by hand, in + * TurnoutDisplay::paintEvent() and TurnoutDisplay::updateConnectionPoints(). + * + * All coordinates are in the node's own local space: (0,0) is the top-left of + * the node's fixed-size bounding box, matching where a turnout node's + * ImGui::Dummy(kNodeSize, kNodeSize) item starts. + */ +struct TurnoutGeometry { + // Three straight legs: the through route, the diverging diagonal, and the + // diverging outgoing leg. + ImVec2 legs[3][2]; + // Anchor/pin points: 0 = incoming, 1 = normal outgoing, 2 = diverged outgoing. + ImVec2 pins[3]; +}; + +/** + * Size of the turnout artwork's content box (before the padding that lets it + * rotate without clipping its bounding box -- see kNodeSize in + * imguipanelwidget.h, which is the padded, actual node size). + */ +constexpr float kTurnoutContentSize = 50.0f; + +TurnoutGeometry turnoutGeometry(TurnoutHand hand, double rotationDegrees); + +/** + * imgui-node-editor's default Style::NodePadding is (8,8,8,8) -- ed::BeginNode() + * indents its content by (left,top) before anything is drawn, so a node's content + * origin (what drawTurnoutNode() anchors pins to, via ImGui::GetItemRectMin()) sits + * this far past node.position, not exactly on it. Duplicated here (rather than read + * from ed::GetStyle(), which needs a current editor context this framework-agnostic + * file has no access to) so resolveSegmentEnd() anchors segments to the same point + * pins are actually drawn/hit-tested at -- matches kNodeSize's existing duplication + * pattern in panelmodel.cpp. + */ +constexpr float kNodeContentPadding = 8.0f; + +/** + * One endpoint of a track segment: either a turnout's pin (nodeId != 0) or a + * free-floating point with no connection (nodeId == 0). Mirrors + * PanelDisplay::ConnectionEndpoint, but by id/index rather than by a raw + * Connectable* pointer, since panel items are no longer widgets. + */ +struct SegmentEnd { + PanelItemId nodeId = 0; + /** 0 = incoming, 1 = normal outgoing, 2 = diverged outgoing. Ignored when nodeId == 0. */ + int pinIndex = -1; + /** Only meaningful when nodeId == 0. Canvas coordinates. */ + ImVec2 freePos{0.0f, 0.0f}; +}; + +/** + * A cubic-bezier track segment between two endpoints. Framework-agnostic -- + * resolving an endpoint to its current canvas position (a turnout's pin + * moves as the turnout is dragged/rotated) is done each frame by whoever + * draws it; this struct only stores what was actually chosen/dragged. + * + * controlOffsetA/B and straight carry over verbatim from TrackSegment + * (tracksegment.cpp) -- storing control points as offsets from their + * governing endpoint is what makes a curve move rigidly with a rotating + * turnout, and "straight" is what lets it re-track a moving endpoint as a + * straight line until the user actually drags a control handle. + */ +struct TrackSegmentEdge { + PanelItemId id = 0; + SegmentEnd a, b; + ImVec2 controlOffsetA{0.0f, 0.0f}; + ImVec2 controlOffsetB{0.0f, 0.0f}; + bool straight = true; +}; + +/** + * Resolves a SegmentEnd to its current canvas-space position: the free point + * if unconnected, or the referenced turnout's current pin position (which + * moves as the turnout is dragged/rotated) if connected. Returns freePos + * (0,0 by default) if the referenced node no longer exists. + */ +ImVec2 resolveSegmentEnd(const PanelModel& model, const SegmentEnd& end); + +/** + * Owns every item placed on a track panel. Framework-agnostic: no Qt widgets, + * no ImGui/node-editor calls -- ImguiPanelWidget reads and writes this once + * per frame. + */ +class PanelModel +{ +public: + TurnoutNode& addTurnout(ImVec2 position); + TurnoutNode* findTurnout(PanelItemId id); + /** Also removes any segment with an endpoint on this node. */ + void removeTurnout(PanelItemId id); + + const std::vector& turnouts() const { return m_turnouts; } + std::vector& turnouts() { return m_turnouts; } + + TrackSegmentEdge& addSegment(SegmentEnd a, SegmentEnd b); + TrackSegmentEdge* findSegment(PanelItemId id); + void removeSegment(PanelItemId id); + + const std::vector& segments() const { return m_segments; } + std::vector& segments() { return m_segments; } + + /** + * PanelStorage::load()'s entry points: insert an item with its + * previously-saved id (preserving segment-to-node references) instead of + * allocating a fresh one, bumping the id allocator past it so later + * addTurnout()/addSegment() calls (e.g. the "Add Turnout" button) never + * collide with anything just loaded. + */ + TurnoutNode& addTurnoutWithId(PanelItemId id, ImVec2 position); + TrackSegmentEdge& addSegmentWithId(PanelItemId id, SegmentEnd a, SegmentEnd b); + + /** Empties the model (PanelStorage::load() calls this before repopulating). */ + void clear(); + +private: + PanelItemId m_nextId = 1; + std::vector m_turnouts; + std::vector m_segments; +}; + +#endif // PANELMODEL_H diff --git a/GUI/panels/paneltoolswidget.cpp b/GUI/panels/paneltoolswidget.cpp deleted file mode 100644 index 02e9111..0000000 --- a/GUI/panels/paneltoolswidget.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include -#include -#include -#include -#include - -#include "paneltoolswidget.h" -#include "ui_paneltoolswidget.h" -#include "turnoutdisplay.h" -#include "tracksegment.h" -#include "propertyeditor.h" - -#include -#include - -static log4cxx::LoggerPtr logger = log4cxx::Logger::getLogger("traingui.PanelToolsWidget"); - -PanelToolsWidget::PanelToolsWidget(QWidget *parent) - : QWidget(parent) - , ui(new Ui::PanelToolsWidget) -{ - ui->setupUi(this); - QVBoxLayout* propsVbox = new QVBoxLayout(ui->toolboxWidget); - propsVbox->setContentsMargins(0, 0, 0, 0); - - // QVBoxLayout* elementsVbox = new QVBoxLayout(ui->elementsDisplay); - // TurnoutDisplay* td = new TurnoutDisplay(); - // td->configureInteraction(false); - // td->setProperty("layoutElement", true); - // elementsVbox->addWidget(td); - - connect(ui->allowMoving, &QCheckBox::clicked, - this, &PanelToolsWidget::allowMovingChanged); - connect(ui->connectElements, &QCheckBox::clicked, - this, &PanelToolsWidget::drawConnectionPointsChanged); -} - -PanelToolsWidget::~PanelToolsWidget() -{ - delete ui; -} - -void PanelToolsWidget::setCurrentSelectedWidget(QWidget* widget){ - for(QWidget* wid : m_currentProps){ - ui->toolboxWidget->layout()->removeWidget(wid); - delete wid; - } - m_currentProps.clear(); - - if(widget == nullptr){ - return; - } - - // Loop through all of the properties of this widget, - // add them to the properties list - const QMetaObject* metaObj = widget->metaObject(); - for(int idx = 0; idx < metaObj->propertyCount(); idx++){ - QMetaProperty prop = metaObj->property(idx); - QString propName = prop.name(); - if(!propName.startsWith("traingui_")){ - continue; - } - propName = propName.remove(0, 9); - PropertyEditor* propEditor = new PropertyEditor(this, widget, prop); - - ui->toolboxWidget->layout()->addWidget(propEditor); - m_currentProps.append(propEditor); - } - - if(TrackSegment* segment = dynamic_cast(widget)){ - QPushButton* straightenButton = new QPushButton("Make Straight", this); - connect(straightenButton, &QPushButton::clicked, segment, &TrackSegment::makeStraight); - - ui->toolboxWidget->layout()->addWidget(straightenButton); - m_currentProps.append(straightenButton); - } -} - -void PanelToolsWidget::mousePressEvent(QMouseEvent* event){ - // QWidget* widgetAtPos = childAt(event->pos()); - // LOG4CXX_DEBUG_FMT(logger, "press button: {} pos: {},{} widget: {}", - // (int)event->button(), - // event->pos().x(), - // event->pos().y(), - // widgetAtPos ? "valid" : "invalid"); - // if(widgetAtPos->property("layoutElement").isValid()){ - // // cool, this is something that we can drag and drop - // // TODO: figure out how to do this drag and drop properly. - // // TurnoutDisplay* td = new TurnoutDisplay(nullptr, Qt::Window | Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput | Qt::WindowDoesNotAcceptFocus | Qt::Popup); - // // td->show(); - // } -} - -// void mouseMoveEvent(QMouseEvent *event) override; -void PanelToolsWidget::mouseReleaseEvent(QMouseEvent *event){ - -} - -void PanelToolsWidget::on_addTurnoutButton_clicked() -{ - // int dccVal = QInputDialog::getInt(this, "DCC switch number", "Please put DCC switch number", 1, 1, 2048); - Q_EMIT addDCCTurnout(); -} - diff --git a/GUI/panels/paneltoolswidget.h b/GUI/panels/paneltoolswidget.h deleted file mode 100644 index 53735b9..0000000 --- a/GUI/panels/paneltoolswidget.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef PANELTOOLSWIDGET_H -#define PANELTOOLSWIDGET_H - -#include - -namespace Ui { -class PanelToolsWidget; -} - -class PanelToolsWidget : public QWidget -{ - Q_OBJECT - -public: - explicit PanelToolsWidget(QWidget *parent = nullptr); - ~PanelToolsWidget(); - - /** - * Set the currently selected widget for panel editing. - * @param widget - */ - void setCurrentSelectedWidget(QWidget* widget); - -Q_SIGNALS: - void allowMovingChanged(bool new_moving); - void addDCCTurnout(); - void drawConnectionPointsChanged(bool new_draw_connections); - -protected: - void mousePressEvent(QMouseEvent* event) override; - // void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - -private Q_SLOTS: - void on_addTurnoutButton_clicked(); - -private: - Ui::PanelToolsWidget *ui; - QVector m_currentProps; -}; - -#endif // PANELTOOLSWIDGET_H diff --git a/GUI/panels/paneltoolswidget.ui b/GUI/panels/paneltoolswidget.ui deleted file mode 100644 index c0df7fc..0000000 --- a/GUI/panels/paneltoolswidget.ui +++ /dev/null @@ -1,111 +0,0 @@ - - - PanelToolsWidget - - - - 0 - 0 - 400 - 518 - - - - Form - - - - - - Panel Toolbox - - - - - - - Allow moving - - - - - - - Connect Elements - - - - - - - Qt::Horizontal - - - - - - - Add new element - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Add Turnout - - - - - - - - - - Qt::Horizontal - - - - - - - Properties - - - - - - - - - - Qt::Vertical - - - - 40 - 20 - - - - - - - - - diff --git a/GUI/panels/propertyeditor.cpp b/GUI/panels/propertyeditor.cpp deleted file mode 100644 index cb9e4fc..0000000 --- a/GUI/panels/propertyeditor.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "propertyeditor.h" -#include "ui_propertyeditor.h" - -PropertyEditor::PropertyEditor(QWidget *parent, QObject* modifyObj, QMetaProperty prop) - : QWidget(parent) - , ui(new Ui::PropertyEditor) -{ - ui->setupUi(this); - m_objToModify = modifyObj; - m_objProperty = prop; - - QVariant prop_value = prop.read(modifyObj); - - ui->name_label->setText(QString(prop.name()).remove(0, 9)); - if(prop.isEnumType()){ - int proper_idx = 0; - ui->lineEdit->hide(); - QMetaEnum enumMeta = prop.enumerator(); - for(int x = 0; x < enumMeta.keyCount(); x++){ - ui->comboBox->addItem(enumMeta.key(x)); - if(enumMeta.value(x) == prop_value){ - proper_idx = x; - } - } - - ui->comboBox->setCurrentIndex(proper_idx); - }else{ - ui->comboBox->hide(); - ui->lineEdit->setText(prop_value.toString()); - } -} - -PropertyEditor::~PropertyEditor() -{ - delete ui; -} - -void PropertyEditor::on_comboBox_activated(int index) -{ - QMetaEnum enumMeta = m_objProperty.enumerator(); - m_objProperty.write(m_objToModify, enumMeta.value(index)); -} - -void PropertyEditor::on_lineEdit_editingFinished() -{ - m_objProperty.write(m_objToModify, ui->lineEdit->text()); -} - diff --git a/GUI/panels/propertyeditor.h b/GUI/panels/propertyeditor.h deleted file mode 100644 index 7eaeb9a..0000000 --- a/GUI/panels/propertyeditor.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef PROPERTYEDITOR_H -#define PROPERTYEDITOR_H - -#include -#include - -namespace Ui { -class PropertyEditor; -} - -class PropertyEditor : public QWidget -{ - Q_OBJECT - -public: - explicit PropertyEditor(QWidget *parent, QObject* modifyObj, QMetaProperty prop); - ~PropertyEditor(); - -private Q_SLOTS: - void on_comboBox_activated(int index); - void on_lineEdit_editingFinished(); - -private: - Ui::PropertyEditor *ui; - QObject* m_objToModify; - QMetaProperty m_objProperty; -}; - -#endif // PROPERTYEDITOR_H diff --git a/GUI/panels/propertyeditor.ui b/GUI/panels/propertyeditor.ui deleted file mode 100644 index e7a4ff1..0000000 --- a/GUI/panels/propertyeditor.ui +++ /dev/null @@ -1,46 +0,0 @@ - - - PropertyEditor - - - - 0 - 0 - 400 - 44 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - TextLabel - - - - - - - - - - - - - - diff --git a/GUI/panels/tracksegment.cpp b/GUI/panels/tracksegment.cpp deleted file mode 100644 index 985d81c..0000000 --- a/GUI/panels/tracksegment.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -#include "tracksegment.h" - -TrackSegment::TrackSegment(QWidget *parent) - : QWidget{parent} -{ - setAttribute(Qt::WA_TranslucentBackground); - setAutoFillBackground(false); -} - -void TrackSegment::setEndpoints(QPoint parentA, QPoint parentB) { - m_parentA = parentA; - m_parentB = parentB; - // While the curve hasn't been manually shaped, keep it exactly straight - // by recomputing the control points fresh every time an endpoint moves, - // rather than dragging along stale offsets from a now-stale line. - if(m_straight){ - straightenControlOffsets(); - } - updateGeometry(); - Q_EMIT connectionPointsUpdated(); -} - -QPoint TrackSegment::endpointA() const { - return m_parentA; -} - -QPoint TrackSegment::endpointB() const { - return m_parentB; -} - -QPoint TrackSegment::controlPointA() const { - return m_parentA + m_controlOffsetA; -} - -QPoint TrackSegment::controlPointB() const { - return m_parentB + m_controlOffsetB; -} - -void TrackSegment::setControlPointA(QPoint parentPoint) { - m_straight = false; - m_controlOffsetA = parentPoint - m_parentA; - updateGeometry(); -} - -void TrackSegment::setControlPointB(QPoint parentPoint) { - m_straight = false; - m_controlOffsetB = parentPoint - m_parentB; - updateGeometry(); -} - -bool TrackSegment::isStraight() const { - return m_straight; -} - -void TrackSegment::makeStraight() { - m_straight = true; - straightenControlOffsets(); - updateGeometry(); -} - -void TrackSegment::straightenControlOffsets() { - QPoint diff = m_parentB - m_parentA; - m_controlOffsetA = diff / 3; - m_controlOffsetB = -diff / 3; -} - -void TrackSegment::updateGeometry() { - QPoint controlA = controlPointA(); - QPoint controlB = controlPointB(); - - int minX = std::min({m_parentA.x(), m_parentB.x(), controlA.x(), controlB.x()}) - PADDING; - int minY = std::min({m_parentA.y(), m_parentB.y(), controlA.y(), controlB.y()}) - PADDING; - int maxX = std::max({m_parentA.x(), m_parentB.x(), controlA.x(), controlB.x()}) + PADDING; - int maxY = std::max({m_parentA.y(), m_parentB.y(), controlA.y(), controlB.y()}) + PADDING; - - int w = std::max(maxX - minX, 2 * PADDING + 1); - int h = std::max(maxY - minY, 2 * PADDING + 1); - - m_localA = m_parentA - QPoint(minX, minY); - m_localB = m_parentB - QPoint(minX, minY); - m_localControlA = controlA - QPoint(minX, minY); - m_localControlB = controlB - QPoint(minX, minY); - - setGeometry(minX, minY, w, h); - update(); -} - -QVector TrackSegment::connectionPoints() { - return {m_localA, m_localB}; -} - -void TrackSegment::configureInteraction(bool interaction) { - m_interactive = interaction; -} - -QSize TrackSegment::sizeHint() const { - return QSize(2 * PADDING + 1, 2 * PADDING + 1); -} - -void TrackSegment::paintEvent(QPaintEvent* /*event*/) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, true); - - QPen pen; - pen.setWidth(3); - pen.setBrush(Qt::black); - painter.setPen(pen); - - QPainterPath path(m_localA); - path.cubicTo(m_localControlA, m_localControlB, m_localB); - painter.drawPath(path); -} - -void TrackSegment::mousePressEvent(QMouseEvent* event) { - if (!m_interactive || event->button() == Qt::RightButton) { - event->ignore(); - return; - } - if (distanceToCurve(event->pos()) > PADDING) { - event->ignore(); - return; - } - event->accept(); -} - -bool TrackSegment::hitTest(QPoint parentPoint) const { - return distanceToCurve(parentPoint - pos()) <= PADDING; -} - -void TrackSegment::mouseReleaseEvent(QMouseEvent* event) { - event->ignore(); -} - -QPointF TrackSegment::bezierPoint(float t) const { - float u = 1.0f - t; - QPointF a(m_localA); - QPointF b(m_localControlA); - QPointF c(m_localControlB); - QPointF d(m_localB); - return u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * d; -} - -float TrackSegment::distanceToCurve(QPoint p) const { - constexpr int kSamples = 24; - QPoint prev = m_localA; - float minDist = std::numeric_limits::max(); - for(int i = 1; i <= kSamples; i++){ - QPoint next = bezierPoint(float(i) / kSamples).toPoint(); - minDist = std::min(minDist, distanceToSegment(p, prev, next)); - prev = next; - } - return minDist; -} - -float TrackSegment::distanceToSegment(QPoint p, QPoint a, QPoint b) { - QPointF pa = p - a; - QPointF ba = b - a; - double denom = QPointF::dotProduct(ba, ba); - if (denom == 0.0) { - return QLineF(QPointF(p), QPointF(a)).length(); - } - float t = std::clamp((float)(QPointF::dotProduct(pa, ba) / denom), 0.f, 1.f); - return QLineF(QPointF(p), QPointF(a) + t * ba).length(); -} diff --git a/GUI/panels/tracksegment.h b/GUI/panels/tracksegment.h deleted file mode 100644 index 704f228..0000000 --- a/GUI/panels/tracksegment.h +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef TRACKSEGMENT_H -#define TRACKSEGMENT_H - -#include - -#include "connectable.h" - -class TrackSegment : public QWidget, public Connectable -{ - Q_OBJECT -public: - explicit TrackSegment(QWidget *parent = nullptr); - void setEndpoints(QPoint parentA, QPoint parentB); - QVector connectionPoints() override; - - // Endpoints and bezier control points, all in the parent's (panel's) - // coordinate space. - QPoint endpointA() const; - QPoint endpointB() const; - QPoint controlPointA() const; - QPoint controlPointB() const; - void setControlPointA(QPoint parentPoint); - void setControlPointB(QPoint parentPoint); - - // Whether the curve is currently following the endpoints as a straight - // line (true), or has been manually shaped by dragging a control point - // (false). makeStraight() resets it back to following the endpoints. - bool isStraight() const; - void makeStraight(); - - // True if parentPoint (in the parent's coordinate space) is close - // enough to the rendered curve to count as a click on it. Used so a - // click meant for the segment isn't lost to an overlapping sibling's - // (e.g. a turnout's) bounding box. - bool hitTest(QPoint parentPoint) const; - -Q_SIGNALS: - void connectionPointsUpdated(); - -public Q_SLOTS: - void configureInteraction(bool interaction); - -protected: - QSize sizeHint() const override; - void paintEvent(QPaintEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - -private: - void updateGeometry(); - void straightenControlOffsets(); - QPointF bezierPoint(float t) const; - float distanceToCurve(QPoint p) const; - static float distanceToSegment(QPoint p, QPoint a, QPoint b); - - QPoint m_parentA; - QPoint m_parentB; - // Control point positions, stored as offsets from their governing - // endpoint so that they move rigidly with it as the segment is - // reconnected (e.g. when a turnout it's attached to moves or rotates). - // Only meaningful once m_straight is false -- while straight, they're - // recomputed fresh on every setEndpoints() call instead. - QPoint m_controlOffsetA; - QPoint m_controlOffsetB; - bool m_straight = true; - - QPoint m_localA; - QPoint m_localB; - QPoint m_localControlA; - QPoint m_localControlB; - - bool m_interactive = true; - static constexpr int PADDING = 8; -}; - -#endif // TRACKSEGMENT_H diff --git a/GUI/panels/turnoutdisplay.cpp b/GUI/panels/turnoutdisplay.cpp deleted file mode 100644 index 3d802fa..0000000 --- a/GUI/panels/turnoutdisplay.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include -#include -#include -#include - -#include "turnoutdisplay.h" -#include "../common/turnout.h" - -#include -#include - -static log4cxx::LoggerPtr logger = log4cxx::Logger::getLogger("traingui.TurnoutDisplay"); - -TurnoutDisplay::TurnoutDisplay(QWidget *parent, Qt::WindowFlags f) - : QWidget{parent, f} -{ - // The widget is padded well beyond the visible turnout artwork (see - // contentRect()) so rotation never clips; keep that padding transparent - // so it doesn't visually or spatially mask whatever's behind it (e.g. a - // nearby track segment's controls). - setAttribute(Qt::WA_TranslucentBackground); - setAutoFillBackground(false); - resize(kBoundingSize, kBoundingSize); - updateConnectionPoints(); -} - -void TurnoutDisplay::updateConnectionPoints(){ - QRect content = contentRect(); - int cw = content.width(); - int ch = content.height(); - - // Mirror the same transform stack applied in paintEvent, so the - // connection points always match what's drawn, without needing to - // wait for an actual paint to happen. - QTransform transform; - transform.translate(content.left(), content.top()); - if(m_turnoutType == TurnoutType::Left){ - transform.translate(0, ch); - transform.scale(1, -1); - } - if(m_rotation != 0.0){ - QPointF center(cw / 2.0, ch / 2.0); - transform.translate(center.x(), center.y()); - transform.rotate(m_rotation); - transform.translate(-center.x(), -center.y()); - } - - // Connection points: - // 0 = incoming - // 1 = normal outgoing - // 2 = diverged outgoing - m_connectionPoints.clear(); - m_connectionPoints.push_back(transform.map(QPoint(0, ch / 4))); - m_connectionPoints.push_back(transform.map(QPoint(cw, ch / 4))); - m_connectionPoints.push_back(transform.map(QPoint(cw, ch / 2 + ch / 4))); - - Q_EMIT connectionPointsUpdated(); -} - -QRect TurnoutDisplay::contentRect() const { - return QRect((width() - kContentWidth) / 2, (height() - kContentHeight) / 2, - kContentWidth, kContentHeight); -} - -void TurnoutDisplay::paintEvent(QPaintEvent *event){ - QPainter painter(this); - - QRect content = contentRect(); - int cw = content.width(); - int ch = content.height(); - - // Move into the content's local coordinate space (0,0) .. (cw,ch) - painter.translate(content.topLeft()); - - if(m_turnoutType == TurnoutType::Left){ - painter.translate(0, ch); - painter.scale(1, -1); - } - - if(m_rotation != 0.0){ - QPointF center(cw / 2.0, ch / 2.0); - painter.translate(center); - painter.rotate(m_rotation); - painter.translate(-center); - } - - QPoint straight_start(0, ch / 4); - QPoint straight_end(cw, ch / 4); - painter.drawLine(straight_start, straight_end); - // Go 1/3 of the way and draw our diverging route - QPoint start_diverge(cw / 3, ch / 4); - QPoint end_diverge(cw / 2 + cw / 4, ch / 2 + ch / 4); - painter.drawLine(start_diverge, end_diverge); - // draw the remainder of the diverging route - QPoint diverged_start = end_diverge; - QPoint diverged_end(cw, ch / 2 + ch / 4); - painter.drawLine(diverged_start, diverged_end); - - painter.setTransform(QTransform()); - QFont f; - f.setPointSize(8); - painter.setFont(f); - QString turnoutInfoString; - if(m_turnout){ - switch(m_turnout->getState()){ - case Turnout::TurnoutState::Unknown: - turnoutInfoString = "unknown"; - break; - case Turnout::TurnoutState::Closed: - turnoutInfoString = "closed"; - break; - case Turnout::TurnoutState::Thrown: - turnoutInfoString = "thrown"; - break; - } - }else{ - turnoutInfoString = "N/A"; - } - painter.drawText(QPoint(content.left(), content.top() + ch / 4), turnoutInfoString); -} - -QSize TurnoutDisplay::sizeHint() const -{ - return QSize(kBoundingSize, kBoundingSize); -} - -void TurnoutDisplay::mousePressEvent(QMouseEvent* event){ - if(!m_interactive){ - event->ignore(); - return; - } - - LOG4CXX_DEBUG_FMT(logger, "press button: {} pos: {},{}", - (int)event->button(), - event->pos().x(), - event->pos().y()); - if(event->button() == Qt::MouseButton::LeftButton){ - m_mousePressStart = QDateTime::currentDateTime(); - m_mousePressLocation = event->pos(); - } -} - -void TurnoutDisplay::mouseReleaseEvent(QMouseEvent* event){ - if(!m_interactive){ - event->ignore(); - return; - } - - LOG4CXX_DEBUG_FMT(logger, "release button: {} pos: {},{}", - (int)event->button(), - event->pos().x(), - event->pos().y()); - - if(event->button() != Qt::MouseButton::LeftButton){ - return; - } - - // make sure that we're not trying to move anything. - // if not, toggle the turnout - int absX = std::abs(m_mousePressLocation.x() - event->pos().x()); - int absY = std::abs(m_mousePressLocation.y() - event->pos().y()); - int64_t msecs_diff = m_mousePressStart.msecsTo(QDateTime::currentDateTime()); - if((absX < 10) && - (absY < 10) && - (msecs_diff < 1000) && - (msecs_diff > 0) && - m_turnout){ - LOG4CXX_DEBUG_FMT(logger, "Toggle turnout"); - m_turnout->toggleTurnout(); - } -} - -// void TurnoutDisplay::mouseMoveEvent(QMouseEvent* event){ -// } - - -void TurnoutDisplay::setTurnout(std::shared_ptr turnout){ - m_turnout = turnout; - - connect(m_turnout.get(), &Turnout::stateChanged, - this, &TurnoutDisplay::stateChanged); -} - -void TurnoutDisplay::stateChanged(){ - update(); -} - -void TurnoutDisplay::configureInteraction(bool interaction){ - m_interactive = interaction; -} - -TurnoutDisplay::TurnoutType TurnoutDisplay::turnoutType(){ - return m_turnoutType; -} - -QString TurnoutDisplay::name() const{ - return m_name; -} - -void TurnoutDisplay::setName(QString name){ - if(name != m_name){ - m_name = name; - Q_EMIT nameChanged(); - } -} - -void TurnoutDisplay::setTurnoutType(TurnoutType type){ - LOG4CXX_DEBUG(logger, "Turnout type changed"); - m_turnoutType = type; - updateConnectionPoints(); - update(this->rect()); -} - -double TurnoutDisplay::rotation() const{ - return m_rotation; -} - -void TurnoutDisplay::setRotation(double degrees){ - if(degrees != m_rotation){ - m_rotation = degrees; - updateConnectionPoints(); - update(this->rect()); - } -} - -QVector TurnoutDisplay::connectionPoints(){ - return m_connectionPoints; -} diff --git a/GUI/panels/turnoutdisplay.h b/GUI/panels/turnoutdisplay.h deleted file mode 100644 index 662719f..0000000 --- a/GUI/panels/turnoutdisplay.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef TURNOUTDISPLAY_H -#define TURNOUTDISPLAY_H - -#include -#include -#include - -#include "../common/turnout.h" -#include "connectable.h" - -class TurnoutDisplay : public QWidget, public Connectable -{ - Q_OBJECT - // Q_PROPERTY(QString traingui_turnout READ turnout WRITE setTurnout) -public: - enum class TurnoutType{ - Left, - Right - }; - Q_ENUM(TurnoutType) - Q_PROPERTY(TurnoutType traingui_turnout_type READ turnoutType WRITE setTurnoutType) - Q_PROPERTY(QString traingui_name READ name WRITE setName NOTIFY nameChanged) - - explicit TurnoutDisplay(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); - - void setTurnout(std::shared_ptr turnout); - TurnoutType turnoutType(); - - QString name() const; - - double rotation() const; - - // The turnout artwork is drawn centered within a larger, fixed-size - // widget so that rotating it never clips it against the widget bounds. - // This is that inner drawing area, in widget-local (unrotated) coordinates. - QRect contentRect() const; - - QVector connectionPoints() override; - -Q_SIGNALS: - void connectionPointsUpdated(); - void nameChanged(); - -public Q_SLOTS: - void configureInteraction(bool interaction); - void setTurnoutType(TurnoutType type); - void setName(QString name); - void setRotation(double degrees); - -protected: - QSize sizeHint() const override; - void paintEvent(QPaintEvent *event) override; - void mousePressEvent(QMouseEvent* event) override; - // void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - -private: - void stateChanged(); - void updateConnectionPoints(); - -private: - // Size of the actual turnout artwork. - static constexpr int kContentWidth = 50; - static constexpr int kContentHeight = 50; - // Size of the widget itself: large enough that the content, rotated by - // any angle, still fits (the diagonal of a 50x50 square is ~71px). - static constexpr int kBoundingSize = 72; - - std::shared_ptr m_turnout; - bool m_interactive = true; - QDateTime m_mousePressStart; - QPoint m_mousePressLocation; - TurnoutType m_turnoutType = TurnoutType::Right; - QVector m_connectionPoints; - QString m_name; - double m_rotation = 0.0; -}; - -#endif // TURNOUTDISPLAY_H diff --git a/GUI/qtimgui/CMakeLists.txt b/GUI/qtimgui/CMakeLists.txt new file mode 100644 index 0000000..df9f594 --- /dev/null +++ b/GUI/qtimgui/CMakeLists.txt @@ -0,0 +1,10 @@ +# minimum require +cmake_minimum_required(VERSION 3.25.0) + +# global solution +project(qtimgui VERSION 1.0.0 LANGUAGES CXX) + +# goto subs +#add_subdirectory(examples) +#add_subdirectory(modules) +add_subdirectory(src) diff --git a/GUI/qtimgui/LICENSE b/GUI/qtimgui/LICENSE new file mode 100644 index 0000000..7c7077f --- /dev/null +++ b/GUI/qtimgui/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Ryohei Ikegami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/GUI/qtimgui/src/CMakeLists.txt b/GUI/qtimgui/src/CMakeLists.txt new file mode 100644 index 0000000..68e221c --- /dev/null +++ b/GUI/qtimgui/src/CMakeLists.txt @@ -0,0 +1,50 @@ +project(qtimgui) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 COMPONENTS Core Quick Gui Widgets OpenGLWidgets REQUIRED) + +set(QT_IMGUI_SOURCES + ImGuiRenderer.cpp + QtImGui.cpp +) + +set(QT_IMGUI_HEADERS + ImGuiRenderer.h + QtImGui.h +) + +# qt_imgui_quick: library with a qt renderer for Qml / QtQuick applications +add_library(qt_imgui_quick STATIC ${QT_IMGUI_SOURCES}) +target_include_directories(qt_imgui_quick PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries( + qt_imgui_quick + PUBLIC + imgui + Qt6::Gui + Qt6::OpenGLWidgets + ) +if (ANDROID) + target_link_libraries(qt_imgui_quick PUBLIC log dl GLESv2 z) +endif() +install(TARGETS qt_imgui_quick ARCHIVE DESTINATION qtimgui/lib) +install(FILES ${QT_IMGUI_HEADERS} DESTINATION qtimgui/include) + +# qt_imgui_widget: library with a qt renderer for classic Qt Widget applications +add_library(qt_imgui_widgets STATIC ${QT_IMGUI_SOURCES}) +target_include_directories(qt_imgui_widgets PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries( + qt_imgui_widgets + PUBLIC + imgui + Qt6::Gui + Qt6::OpenGLWidgets + ) +if (ANDROID) + target_link_libraries(qt_imgui_widgets PUBLIC log dl GLESv2 z) +endif() +target_compile_definitions(qt_imgui_widgets PUBLIC QT_WIDGETS_LIB) +install(TARGETS qt_imgui_widgets ARCHIVE DESTINATION qtimgui/lib) +install(FILES ${QT_IMGUI_HEADERS} DESTINATION qtimgui/include) diff --git a/GUI/qtimgui/src/ImGuiRenderer.cpp b/GUI/qtimgui/src/ImGuiRenderer.cpp new file mode 100644 index 0000000..e804f4b --- /dev/null +++ b/GUI/qtimgui/src/ImGuiRenderer.cpp @@ -0,0 +1,554 @@ +#include "ImGuiRenderer.h" + +#include +#include +#include +#include +#include + +#ifdef ANDROID +#define GL_VERTEX_ARRAY_BINDING 0x85B5 // Missing in android as of May 2020 +#define USE_GLSL_ES +#endif + +#ifdef USE_GLSL_ES +#define IMGUIRENDERER_GLSL_VERSION "#version 300 es\n" +#else +#define IMGUIRENDERER_GLSL_VERSION "#version 330\n" +#endif + +namespace QtImGui { + +namespace { + +// Keyboard mapping. Dear ImGui use those indices to peek into the io.KeysDown[] array. +const QHash KEY_MAP = { + { Qt::Key_Tab, ImGuiKey::ImGuiKey_Tab }, + { Qt::Key_Left, ImGuiKey::ImGuiKey_LeftArrow }, + { Qt::Key_Right, ImGuiKey::ImGuiKey_RightArrow }, + { Qt::Key_Up, ImGuiKey::ImGuiKey_UpArrow }, + { Qt::Key_Down, ImGuiKey::ImGuiKey_DownArrow }, + { Qt::Key_PageUp, ImGuiKey::ImGuiKey_PageUp }, + { Qt::Key_PageDown, ImGuiKey::ImGuiKey_PageDown }, + { Qt::Key_Home, ImGuiKey::ImGuiKey_Home }, + { Qt::Key_End, ImGuiKey::ImGuiKey_End }, + { Qt::Key_Insert, ImGuiKey::ImGuiKey_Insert }, + { Qt::Key_Delete, ImGuiKey::ImGuiKey_Delete }, + { Qt::Key_Backspace, ImGuiKey::ImGuiKey_Backspace }, + { Qt::Key_Space, ImGuiKey::ImGuiKey_Space }, + { Qt::Key_Enter, ImGuiKey::ImGuiKey_Enter }, + { Qt::Key_Return, ImGuiKey::ImGuiKey_Enter }, + { Qt::Key_Escape, ImGuiKey::ImGuiKey_Escape }, + { Qt::Key_A, ImGuiKey::ImGuiKey_A }, + { Qt::Key_C, ImGuiKey::ImGuiKey_C }, + { Qt::Key_V, ImGuiKey::ImGuiKey_V }, + { Qt::Key_X, ImGuiKey::ImGuiKey_X }, + { Qt::Key_Y, ImGuiKey::ImGuiKey_Y }, + { Qt::Key_Z, ImGuiKey::ImGuiKey_Z } +}; + +#ifndef QT_NO_CURSOR +const QHash CURSOR_MAP = { + { ImGuiMouseCursor_Arrow, Qt::CursorShape::ArrowCursor }, + { ImGuiMouseCursor_TextInput, Qt::CursorShape::IBeamCursor }, + { ImGuiMouseCursor_ResizeAll, Qt::CursorShape::SizeAllCursor }, + { ImGuiMouseCursor_ResizeNS, Qt::CursorShape::SizeVerCursor }, + { ImGuiMouseCursor_ResizeEW, Qt::CursorShape::SizeHorCursor }, + { ImGuiMouseCursor_ResizeNESW, Qt::CursorShape::SizeBDiagCursor }, + { ImGuiMouseCursor_ResizeNWSE, Qt::CursorShape::SizeFDiagCursor }, + { ImGuiMouseCursor_Hand, Qt::CursorShape::PointingHandCursor }, + { ImGuiMouseCursor_NotAllowed, Qt::CursorShape::ForbiddenCursor }, +}; +#endif + +QByteArray g_currentClipboardText; + +} // namespace + +void ImGuiRenderer::initialize(WindowWrapper *window) { + m_window.reset(window); + initializeOpenGLFunctions(); + + g_ctx = ImGui::CreateContext(); + ImGui::SetCurrentContext(g_ctx); + + // Setup backend capabilities flags + ImGuiIO &io = ImGui::GetIO(); + #ifndef QT_NO_CURSOR + io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) + io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) + #endif + io.BackendPlatformName = "qtimgui"; + + // io.RenderDrawListsFn = [](ImDrawData *drawData) { + // instance()->renderDrawList(drawData); + // }; + io.SetClipboardTextFn = [](void *user_data, const char *text) { + Q_UNUSED(user_data); + QGuiApplication::clipboard()->setText(text); + }; + io.GetClipboardTextFn = [](void *user_data) { + Q_UNUSED(user_data); + g_currentClipboardText = QGuiApplication::clipboard()->text().toUtf8(); + return (const char *)g_currentClipboardText.data(); + }; + + window->installEventFilter(this); +} + +void ImGuiRenderer::renderDrawList(ImDrawData *draw_data) +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) + const ImGuiIO& io = ImGui::GetIO(); + int fb_width = (int)(io.DisplaySize.x * io.DisplayFramebufferScale.x); + int fb_height = (int)(io.DisplaySize.y * io.DisplayFramebufferScale.y); + float sx = io.DisplayFramebufferScale.x; + float sy = io.DisplayFramebufferScale.y; + + if (fb_width == 0 || fb_height == 0) + return; + draw_data->ScaleClipRects(io.DisplayFramebufferScale); + + // Backup GL state + GLint last_active_texture; glGetIntegerv(GL_ACTIVE_TEXTURE, &last_active_texture); + glActiveTexture(GL_TEXTURE0); + GLint last_program; glGetIntegerv(GL_CURRENT_PROGRAM, &last_program); + GLint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); + GLint last_array_buffer; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); + GLint last_element_array_buffer; glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &last_element_array_buffer); + GLint last_vertex_array; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array); + GLint last_blend_src_rgb; glGetIntegerv(GL_BLEND_SRC_RGB, &last_blend_src_rgb); + GLint last_blend_dst_rgb; glGetIntegerv(GL_BLEND_DST_RGB, &last_blend_dst_rgb); + GLint last_blend_src_alpha; glGetIntegerv(GL_BLEND_SRC_ALPHA, &last_blend_src_alpha); + GLint last_blend_dst_alpha; glGetIntegerv(GL_BLEND_DST_ALPHA, &last_blend_dst_alpha); + GLint last_blend_equation_rgb; glGetIntegerv(GL_BLEND_EQUATION_RGB, &last_blend_equation_rgb); + GLint last_blend_equation_alpha; glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &last_blend_equation_alpha); + GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport); + GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box); + GLboolean last_enable_blend = glIsEnabled(GL_BLEND); + GLboolean last_enable_cull_face = glIsEnabled(GL_CULL_FACE); + GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST); + GLboolean last_enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST); + + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + glEnable(GL_SCISSOR_TEST); + + // Setup viewport, orthographic projection matrix + glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height); + const float ortho_projection[4][4] = + { + { 2.0f/io.DisplaySize.x, 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f/-io.DisplaySize.y, 0.0f, 0.0f }, + { 0.0f, 0.0f, -1.0f, 0.0f }, + {-1.0f, 1.0f, 0.0f, 1.0f }, + }; + glUseProgram(g_ShaderHandle); + glUniform1i(g_AttribLocationTex, 0); + glUniformMatrix4fv(g_AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]); + glBindVertexArray(g_VaoHandle); + + // Will project scissor/clipping rectangles into framebuffer space + ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports + ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) + + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawIdx* idx_buffer_offset = 0; + + glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle); + glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)cmd_list->VtxBuffer.Size * sizeof(ImDrawVert), (const GLvoid*)cmd_list->VtxBuffer.Data, GL_STREAM_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_ElementsHandle); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (GLsizeiptr)cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx), (const GLvoid*)cmd_list->IdxBuffer.Data, GL_STREAM_DRAW); + + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + pcmd->UserCallback(cmd_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // Apply scissor/clipping rectangle (Y is inverted in OpenGL) + glScissor((int)(clip_min.x / sx), (int)(fb_height - clip_max.y / sy) , (int)((clip_max.x - clip_min.x) / sx) , (int)((clip_max.y - clip_min.y) / sy)); + + // Bind texture, Draw + glBindTexture(GL_TEXTURE_2D, (GLuint)(size_t)pcmd->GetTexID()); + glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset + pcmd->IdxOffset); + } + } + } + + // Restore modified GL state + glUseProgram(last_program); + glBindTexture(GL_TEXTURE_2D, last_texture); + glActiveTexture(last_active_texture); + glBindVertexArray(last_vertex_array); + glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer); + glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha); + glBlendFuncSeparate(last_blend_src_rgb, last_blend_dst_rgb, last_blend_src_alpha, last_blend_dst_alpha); + if (last_enable_blend) glEnable(GL_BLEND); else glDisable(GL_BLEND); + if (last_enable_cull_face) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE); + if (last_enable_depth_test) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); + if (last_enable_scissor_test) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST); + glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]); + glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]); +} + +bool ImGuiRenderer::createFontsTexture() +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + // Build texture atlas + ImGuiIO& io = ImGui::GetIO(); + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bits (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. + + // Upload texture to graphics system + GLint last_texture; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); + glGenTextures(1, &g_FontTexture); + glBindTexture(GL_TEXTURE_2D, g_FontTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + + // Store our identifier + // NOTE(TrainUtils): ImTextureID became ImU64 rather than void* in Dear ImGui + // 1.91.5, so go through an integer here to work with either definition. + io.Fonts->TexID = (ImTextureID)(intptr_t)g_FontTexture; + + // Restore state + glBindTexture(GL_TEXTURE_2D, last_texture); + + return true; +} + +bool ImGuiRenderer::createDeviceObjects() +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + // Backup GL state + GLint last_texture, last_array_buffer, last_vertex_array; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); + glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array); + + const GLchar *vertex_shader = + IMGUIRENDERER_GLSL_VERSION + "uniform mat4 ProjMtx;\n" + "in vec2 Position;\n" + "in vec2 UV;\n" + "in vec4 Color;\n" + "out vec2 Frag_UV;\n" + "out vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " Frag_UV = UV;\n" + " Frag_Color = Color;\n" + " gl_Position = ProjMtx * vec4(Position.xy,0,1);\n" + "}\n"; + + const GLchar* fragment_shader = + IMGUIRENDERER_GLSL_VERSION + "precision mediump float;" + "uniform sampler2D Texture;\n" + "in vec2 Frag_UV;\n" + "in vec4 Frag_Color;\n" + "out vec4 Out_Color;\n" + "void main()\n" + "{\n" + " Out_Color = Frag_Color * texture( Texture, Frag_UV.st);\n" + "}\n"; + + g_ShaderHandle = glCreateProgram(); + g_VertHandle = glCreateShader(GL_VERTEX_SHADER); + g_FragHandle = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(g_VertHandle, 1, &vertex_shader, 0); + glShaderSource(g_FragHandle, 1, &fragment_shader, 0); + glCompileShader(g_VertHandle); + glCompileShader(g_FragHandle); + glAttachShader(g_ShaderHandle, g_VertHandle); + glAttachShader(g_ShaderHandle, g_FragHandle); + glLinkProgram(g_ShaderHandle); + + g_AttribLocationTex = glGetUniformLocation(g_ShaderHandle, "Texture"); + g_AttribLocationProjMtx = glGetUniformLocation(g_ShaderHandle, "ProjMtx"); + g_AttribLocationPosition = glGetAttribLocation(g_ShaderHandle, "Position"); + g_AttribLocationUV = glGetAttribLocation(g_ShaderHandle, "UV"); + g_AttribLocationColor = glGetAttribLocation(g_ShaderHandle, "Color"); + + glGenBuffers(1, &g_VboHandle); + glGenBuffers(1, &g_ElementsHandle); + + glGenVertexArrays(1, &g_VaoHandle); + glBindVertexArray(g_VaoHandle); + glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle); + glEnableVertexAttribArray(g_AttribLocationPosition); + glEnableVertexAttribArray(g_AttribLocationUV); + glEnableVertexAttribArray(g_AttribLocationColor); + +#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT)) + glVertexAttribPointer(g_AttribLocationPosition, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)OFFSETOF(ImDrawVert, pos)); + glVertexAttribPointer(g_AttribLocationUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)OFFSETOF(ImDrawVert, uv)); + glVertexAttribPointer(g_AttribLocationColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)OFFSETOF(ImDrawVert, col)); +#undef OFFSETOF + + createFontsTexture(); + + // Restore modified GL state + glBindTexture(GL_TEXTURE_2D, last_texture); + glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); + glBindVertexArray(last_vertex_array); + + return true; +} + +void ImGuiRenderer::newFrame() +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + if (!g_FontTexture) + createDeviceObjects(); + + ImGuiIO& io = ImGui::GetIO(); + + // Setup display size (every frame to accommodate for window resizing) + io.DisplaySize = ImVec2(m_window->size().width(), m_window->size().height()); + io.DisplayFramebufferScale = ImVec2(m_window->devicePixelRatio(), m_window->devicePixelRatio()); + + // Setup time step + double current_time = QDateTime::currentMSecsSinceEpoch() / double(1000); + io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f/60.0f); + if (io.DeltaTime <= 0.0f) io.DeltaTime = 0.00001f; + g_Time = current_time; + + + // If ImGui wants to set cursor position (for example, during navigation by using keyboard) + // we need to do it here (before getting `QCursor::pos()` below). + setCursorPos(io); + + // Setup inputs + // (we already got mouse wheel, keyboard keys & characters from glfw callbacks polled in glfwPollEvents()) + if (m_window->isActive()) + { + const QPoint pos = m_window->mapFromGlobal(QCursor::pos()); + io.MousePos = ImVec2(pos.x(), pos.y()); // Mouse position in screen coordinates (set to -1,-1 if no mouse / on another screen, etc.) + } + else + { + io.MousePos = ImVec2(-1,-1); + } + + for (int i = 0; i < 3; i++) + { + io.MouseDown[i] = g_MousePressed[i]; + } + + io.MouseWheelH = g_MouseWheelH; + io.MouseWheel = g_MouseWheel; + g_MouseWheelH = 0; + g_MouseWheel = 0; + + + updateCursorShape(io); + + + // Start the frame + ImGui::NewFrame(); +} + +void ImGuiRenderer::render() +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + auto drawData = ImGui::GetDrawData(); + renderDrawList(drawData); +} + +ImGuiRenderer::ImGuiRenderer() + : g_ctx(nullptr) +{ +} + +ImGuiRenderer::~ImGuiRenderer() +{ + // remove this context + ImGui::DestroyContext(g_ctx); +} + +void ImGuiRenderer::onMousePressedChange(QMouseEvent *event) +{ + g_MousePressed[0] = event->buttons() & Qt::LeftButton; + g_MousePressed[1] = event->buttons() & Qt::RightButton; + g_MousePressed[2] = event->buttons() & Qt::MiddleButton; +} + +void ImGuiRenderer::onWheel(QWheelEvent *event) +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + // Handle horizontal component + if(event->pixelDelta().x() != 0) + { + g_MouseWheelH += event->pixelDelta().x() / (ImGui::GetTextLineHeight()); + } else { + // Magic number of 120 comes from Qt doc on QWheelEvent::pixelDelta() + g_MouseWheelH += event->angleDelta().x() / 120.0f; + } + + // Handle vertical component + if(event->pixelDelta().y() != 0) + { + // 5 lines per unit + g_MouseWheel += event->pixelDelta().y() / (5.0 * ImGui::GetTextLineHeight()); + } else { + // Magic number of 120 comes from Qt doc on QWheelEvent::pixelDelta() + g_MouseWheel += event->angleDelta().y() / 120.0f; + } +} + +void ImGuiRenderer::onKeyPressRelease(QKeyEvent *event) +{ + // Select current context + ImGui::SetCurrentContext(g_ctx); + + ImGuiIO& io = ImGui::GetIO(); + + const bool key_pressed = (event->type() == QEvent::KeyPress); + + // Translate `Qt::Key` into `ImGuiKey`, and apply 'pressed' state for that key + const auto key_it = KEY_MAP.constFind( event->key() ); + if (key_it != KEY_MAP.constEnd()) { // Qt's key found in KEY_MAP + const ImGuiKey imgui_key = *(key_it); + io.AddKeyEvent(imgui_key, key_pressed); + } + + if (key_pressed) { + const QString text = event->text(); + if (text.size() == 1) { + io.AddInputCharacter( text.at(0).unicode() ); + } + } + +#ifdef Q_OS_MAC + io.KeyCtrl = event->modifiers() & Qt::MetaModifier; + io.KeyShift = event->modifiers() & Qt::ShiftModifier; + io.KeyAlt = event->modifiers() & Qt::AltModifier; + io.KeySuper = event->modifiers() & Qt::ControlModifier; // Comamnd key +#else + io.KeyCtrl = event->modifiers() & Qt::ControlModifier; + io.KeyShift = event->modifiers() & Qt::ShiftModifier; + io.KeyAlt = event->modifiers() & Qt::AltModifier; + io.KeySuper = event->modifiers() & Qt::MetaModifier; +#endif +} + +void ImGuiRenderer::updateCursorShape(const ImGuiIO& io) +{ + // NOTE: This code will be executed, only if the following flags have been set: + // - backend flag: `ImGuiBackendFlags_HasMouseCursors` - enabled + // - config flag: `ImGuiConfigFlags_NoMouseCursorChange` - disabled + +#ifndef QT_NO_CURSOR + if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) + return; + + const ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); + if (io.MouseDrawCursor || (imgui_cursor == ImGuiMouseCursor_None)) + { + // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor + m_window->setCursorShape(Qt::CursorShape::BlankCursor); + } + else + { + // Show OS mouse cursor + + // Translate `ImGuiMouseCursor` into `Qt::CursorShape` and show it, if we can + const auto cursor_it = CURSOR_MAP.constFind( imgui_cursor ); + if(cursor_it != CURSOR_MAP.constEnd()) // `Qt::CursorShape` found for `ImGuiMouseCursor` + { + const Qt::CursorShape qt_cursor_shape = *(cursor_it); + m_window->setCursorShape(qt_cursor_shape); + } + else // shape NOT found - use default + { + m_window->setCursorShape(Qt::CursorShape::ArrowCursor); + } + } +#else + Q_UNUSED(io); +#endif +} + +void ImGuiRenderer::setCursorPos(const ImGuiIO& io) +{ + // NOTE: This code will be executed, only if the following flags have been set: + // - backend flag: `ImGuiBackendFlags_HasSetMousePos` - enabled + // - config flag: `ImGuiConfigFlags_NavEnableSetMousePos` - enabled + +#ifndef QT_NO_CURSOR + if(io.WantSetMousePos) { + m_window->setCursorPos({(int)io.MousePos.x, (int)io.MousePos.y}); + } +#else + Q_UNUSED(io); +#endif +} + +bool ImGuiRenderer::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_window->object()) { + switch (event->type()) { + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + this->onMousePressedChange(static_cast(event)); + break; + case QEvent::Wheel: + this->onWheel(static_cast(event)); + break; + case QEvent::KeyPress: + case QEvent::KeyRelease: + this->onKeyPressRelease(static_cast(event)); + break; + default: + break; + } + } + return QObject::eventFilter(watched, event); +} + +ImGuiRenderer* ImGuiRenderer::instance() { + static ImGuiRenderer* instance = nullptr; + if (!instance) { + instance = new ImGuiRenderer(); + } + return instance; +} + +} // namespace QtImGui diff --git a/GUI/qtimgui/src/ImGuiRenderer.h b/GUI/qtimgui/src/ImGuiRenderer.h new file mode 100644 index 0000000..8afd804 --- /dev/null +++ b/GUI/qtimgui/src/ImGuiRenderer.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include + +class QMouseEvent; +class QWheelEvent; +class QKeyEvent; + +namespace QtImGui { + +class WindowWrapper { +public: + virtual ~WindowWrapper() {} + virtual void installEventFilter(QObject *object) = 0; + virtual QSize size() const = 0; + virtual qreal devicePixelRatio() const = 0; + virtual bool isActive() const = 0; + virtual QPoint mapFromGlobal(const QPoint &p) const = 0; + virtual QObject* object() = 0; + + virtual void setCursorShape(Qt::CursorShape shape) = 0; + virtual void setCursorPos(const QPoint& local_pos) = 0; +}; + +class ImGuiRenderer : public QObject, QOpenGLExtraFunctions { + Q_OBJECT +public: + void initialize(WindowWrapper *window); + void newFrame(); + void render(); + bool eventFilter(QObject *watched, QEvent *event); + + static ImGuiRenderer *instance(); + +public: + ImGuiRenderer(); + ~ImGuiRenderer(); + +private: + void onMousePressedChange(QMouseEvent *event); + void onWheel(QWheelEvent *event); + void onKeyPressRelease(QKeyEvent *event); + + void updateCursorShape(const ImGuiIO &io); + void setCursorPos(const ImGuiIO &io); + + void renderDrawList(ImDrawData *draw_data); + bool createFontsTexture(); + bool createDeviceObjects(); + + std::unique_ptr m_window; + double g_Time = 0.0f; + bool g_MousePressed[3] = { false, false, false }; + // NOTE(TrainUtils): these must be initialized. They are copied straight + // into io.MouseWheel/MouseWheelH on the first frame, and a garbage value + // there is read as a scroll gesture -- which, with imgui-node-editor, + // leaves the canvas at a random zoom before the user has touched anything. + float g_MouseWheel = 0.0f; + float g_MouseWheelH = 0.0f; + GLuint g_FontTexture = 0; + int g_ShaderHandle = 0, g_VertHandle = 0, g_FragHandle = 0; + int g_AttribLocationTex = 0, g_AttribLocationProjMtx = 0; + int g_AttribLocationPosition = 0, g_AttribLocationUV = 0, g_AttribLocationColor = 0; + unsigned int g_VboHandle = 0, g_VaoHandle = 0, g_ElementsHandle = 0; + + ImGuiContext* g_ctx = nullptr; +}; + +} // namespace QtImGui diff --git a/GUI/qtimgui/src/QtImGui.cpp b/GUI/qtimgui/src/QtImGui.cpp new file mode 100644 index 0000000..ee1256d --- /dev/null +++ b/GUI/qtimgui/src/QtImGui.cpp @@ -0,0 +1,191 @@ +#include "QtImGui.h" + +#include "ImGuiRenderer.h" +#include +#ifdef QT_WIDGETS_LIB +#include +#endif + +namespace QtImGui { + +class QWindowWrapper : public WindowWrapper +{ +public: + QWindowWrapper(ImGuiRenderer* r) + : r(r) + {} + ~QWindowWrapper() { + if (r && (r != ImGuiRenderer::instance())) { + delete r; + } + } + +public: + void newFrame() { r->newFrame(); } + + void render() { r->render(); } +private: + ImGuiRenderer* r; +}; + +#ifdef QT_WIDGETS_LIB + +namespace { + +class QWidgetWindowWrapper : public QWindowWrapper { +public: + QWidgetWindowWrapper(QWidget *w, ImGuiRenderer* r) + : QWindowWrapper(r), w(w) + {} + void installEventFilter(QObject *object) override { + return w->installEventFilter(object); + } + QSize size() const override { + return w->size(); + } + qreal devicePixelRatio() const override { + return w->devicePixelRatioF(); + } + bool isActive() const override { + return w->isActiveWindow(); + } + QPoint mapFromGlobal(const QPoint &p) const override { + return w->mapFromGlobal(p); + } + QObject* object() override { + return w; + } + + void setCursorShape(Qt::CursorShape shape) override + { + #ifndef QT_NO_CURSOR + w->setCursor(shape); + #else + Q_UNUSED(shape); + #endif + } + + void setCursorPos(const QPoint& local_pos) override + { + #ifndef QT_NO_CURSOR + // Convert position from widget-space into screen-space + const QPoint global_pos = w->mapToGlobal(local_pos); + + QCursor cursor = w->cursor(); + cursor.setPos(global_pos); + w->setCursor(cursor); + #else + Q_UNUSED(local_pos); + #endif + } + +private: + QWidget *w; +}; + +} // namespace + +RenderRef initialize(QWidget *window, bool defaultRender) { + if (defaultRender) { + auto* wrapper = new QWidgetWindowWrapper(window, ImGuiRenderer::instance()); + ImGuiRenderer::instance()->initialize(wrapper); + return reinterpret_cast(dynamic_cast(wrapper)); + } else { + auto* render = new ImGuiRenderer(); + auto* wrapper = new QWidgetWindowWrapper(window, render); + render->initialize(wrapper); + return reinterpret_cast(dynamic_cast(wrapper)); + } +} + +#endif // QT_WIDGETS_LIB + +namespace { + +class QWindowWindowWrapper : public QWindowWrapper { +public: + QWindowWindowWrapper(QWindow *w, ImGuiRenderer* r) + : QWindowWrapper(r), w(w) + {} + void installEventFilter(QObject *object) override { + return w->installEventFilter(object); + } + QSize size() const override { + return w->size(); + } + qreal devicePixelRatio() const override { + return w->devicePixelRatio(); + } + bool isActive() const override { + return w->isActive(); + } + QPoint mapFromGlobal(const QPoint &p) const override { + return w->mapFromGlobal(p); + } + QObject* object() override { + return w; + } + + void setCursorShape(Qt::CursorShape shape) override + { + #ifndef QT_NO_CURSOR + w->setCursor(shape); + #else + Q_UNUSED(shape); + #endif + } + + void setCursorPos(const QPoint& local_pos) override + { + #ifndef QT_NO_CURSOR + // Convert position from window-space into screen-space + const QPoint global_pos = w->mapToGlobal(local_pos); + + QCursor cursor = w->cursor(); + cursor.setPos(global_pos); + w->setCursor(cursor); + #else + Q_UNUSED(local_pos); + #endif + } + +private: + QWindow *w; +}; + +} // namespace + +RenderRef initialize(QWindow* window, bool defaultRender) { + if (defaultRender) { + auto* wrapper = new QWindowWindowWrapper(window, ImGuiRenderer::instance()); + ImGuiRenderer::instance()->initialize(wrapper); + return reinterpret_cast(dynamic_cast(wrapper)); + } + else { + auto* render = new ImGuiRenderer(); + auto* wrapper = new QWindowWindowWrapper(window, render); + render->initialize(wrapper); + return reinterpret_cast(dynamic_cast(wrapper)); + } +} + +void newFrame(RenderRef ref) { + if (!ref) { + ImGuiRenderer::instance()->newFrame(); + } else { + auto wrapper = reinterpret_cast(ref); + wrapper->newFrame(); + } +} + +void render(RenderRef ref) +{ + if (!ref) { + ImGuiRenderer::instance()->render(); + } else { + auto wrapper = reinterpret_cast(ref); + wrapper->render(); + } +} + +} // namespace QtImGui diff --git a/GUI/qtimgui/src/QtImGui.h b/GUI/qtimgui/src/QtImGui.h new file mode 100644 index 0000000..110c00c --- /dev/null +++ b/GUI/qtimgui/src/QtImGui.h @@ -0,0 +1,18 @@ +#pragma once + +class QWidget; +class QWindow; + +namespace QtImGui { + +typedef void* RenderRef; + +#ifdef QT_WIDGETS_LIB +RenderRef initialize(QWidget *window, bool defaultRender = true); +#endif + +RenderRef initialize(QWindow *window, bool defaultRender = true); +void newFrame(RenderRef ref = nullptr); +void render(RenderRef ref = nullptr); + +} diff --git a/README.md b/README.md index f66d213..00a5b8f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,55 @@ Configuration files for vcpkg are available, you can try those. cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=/home/robert/vcpkg/scripts/buildsystems/vcpkg.cmake -DBUILD_GUI=ON ``` +### Known issue: vcpkg manifest install fails on libpq + +A full vcpkg manifest install (letting CMake run it automatically off `vcpkg.json`) can end up +trying to build `libpq` from source -- pulled in transitively by Qt's SQL support -- which fails +during `configure` with: + +``` +configure: error: +When cross-compiling, either use the option --with-system-tzdata to use +existing time-zone data, or set the environment variable ZIC to a zic +program to use during the build. +``` + +This is unrelated to anything in this repo and isn't fixed here. If your build otherwise has +`Qt6`, `fmt`, `log4cxx` and `qt-advanced-docking-system` available already (e.g. installed under +`/usr/local` rather than through vcpkg), the only packages you actually need vcpkg for are +`imgui` and `imgui-node-editor` (used by the GUI's track panel). Install just those two into +whichever `vcpkg_installed` directory your build points at (check `VCPKG_INSTALLED_DIR` in that +build's `CMakeCache.txt` if you're not sure, e.g. Qt Creator gives each kit's build directory its +own `/vcpkg_installed`), then reconfigure with manifest install turned off so CMake +doesn't try -- and fail -- to redo it itself: + +```bash +mkdir -p /tmp/imgui-scratch-manifest +cat > /tmp/imgui-scratch-manifest/vcpkg.json <<'EOF' +{ + "name": "imgui-scratch", + "version-string": "0.1", + "dependencies": ["imgui", "imgui-node-editor"] +} +EOF +cp vcpkg-configuration.json /tmp/imgui-scratch-manifest/ # keeps the same pinned versions + +/path/to/vcpkg/vcpkg install \ + --x-manifest-root=/tmp/imgui-scratch-manifest \ + --x-install-root=/vcpkg_installed \ + --triplet=x64-linux + +cmake -B -S . \ + -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake \ + -DBUILD_GUI=ON \ + -DVCPKG_INSTALLED_DIR=/vcpkg_installed \ + -DVCPKG_MANIFEST_INSTALL=OFF +``` + +For a Qt Creator-managed build this last `cmake` step already happens for you on reconfigure -- +just run the `vcpkg install` step above pointed at that kit's `/vcpkg_installed` and +reconfigure the project. + ## License: GPL v2 ONLY diff --git a/vcpkg.json b/vcpkg.json index a9d2f15..03387d6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -15,6 +15,8 @@ ] }, "qt-advanced-docking-system", - "qtserialport" + "qtserialport", + "imgui", + "imgui-node-editor" ] }