diff --git a/CMakeLists.txt b/CMakeLists.txt index 32ec263..d20aa52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ include(toml) add_subdirectory(IDHAN) add_subdirectory(IDHANServer) add_subdirectory(IDHANClient) +add_subdirectory(tools/TagEditor) add_subdirectory(docs) diff --git a/tools/TagEditor/CMakeLists.txt b/tools/TagEditor/CMakeLists.txt new file mode 100644 index 0000000..76e0554 --- /dev/null +++ b/tools/TagEditor/CMakeLists.txt @@ -0,0 +1,11 @@ + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Network Concurrent Widgets) +AddFGLExecutable(TagEditor ${CMAKE_CURRENT_SOURCE_DIR}/src) + +file(GLOB_RECURSE UI_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.ui") +target_sources(TagEditor PRIVATE ${UI_FILES}) + +target_link_libraries(TagEditor PUBLIC Qt6::Core Qt6::Widgets Qt6::Concurrent IDHANClient) diff --git a/tools/TagEditor/src/GraphWidget.cpp b/tools/TagEditor/src/GraphWidget.cpp new file mode 100644 index 0000000..c780e2f --- /dev/null +++ b/tools/TagEditor/src/GraphWidget.cpp @@ -0,0 +1,74 @@ +// +// Created by kj16609 on 5/2/25. +// +#include "GraphWidget.hpp" + +#include + +#include "TagNode.hpp" + +GraphWidget::GraphWidget( QWidget* parent ) : QGraphicsView( parent ) +{ + QGraphicsScene* scene { new QGraphicsScene( this ) }; + scene->setItemIndexMethod( QGraphicsScene::NoIndex ); + scene->setSceneRect( -200, -200, 400, 400 ); + setScene( scene ); + setCacheMode( CacheBackground ); + setViewportUpdateMode( BoundingRectViewportUpdate ); + setRenderHint( QPainter::Antialiasing ); + setTransformationAnchor( AnchorUnderMouse ); + setDragMode( ScrollHandDrag ); +} + +void GraphWidget::itemMoved() +{ + using namespace std::chrono_literals; + if ( !timer.isActive() ) + { + timer.start( 1000ms / 25, this ); + } +} + +void GraphWidget::setRootNode( TagNode* node ) +{ + m_tag_nodes.clear(); + scene()->clear(); + + scene()->addItem( node ); + m_tag_nodes.insert_or_assign( node->tagID(), node ); + + node->spawnRelationships(); +} + +void GraphWidget::addRelationship( TagNode* left, TagNode* right, TagRelationship::RelationshipType relationship ) +{ + this->scene()->addItem( new TagRelationship( left, right, relationship ) ); + + itemMoved(); +} + +void GraphWidget::drawBackground( QPainter* painter, const QRectF& rect ) +{ + QGraphicsView::drawBackground( painter, rect ); +} + +void GraphWidget::timerEvent( [[maybe_unused]] QTimerEvent* event ) +{ + const QList< QGraphicsItem* > items { scene()->items() }; + for ( QGraphicsItem* item : items ) + { + TagNode* node { qgraphicsitem_cast< TagNode* >( item ) }; + + if ( node ) node->calculateForces(); + } + + bool items_moved { false }; + + for ( const auto item : items ) + { + TagNode* node { qgraphicsitem_cast< TagNode* >( item ) }; + if ( node ) items_moved |= node->advancePosition(); + } + + if ( !items_moved ) timer.stop(); +} \ No newline at end of file diff --git a/tools/TagEditor/src/GraphWidget.hpp b/tools/TagEditor/src/GraphWidget.hpp new file mode 100644 index 0000000..52f9041 --- /dev/null +++ b/tools/TagEditor/src/GraphWidget.hpp @@ -0,0 +1,43 @@ +// +// Created by kj16609 on 5/2/25. +// +#pragma once +#include +#include + +#include + +#include "IDHANTypes.hpp" +#include "TagRelationship.hpp" + +class TagNode; + +class GraphWidget final : public QGraphicsView +{ + Q_OBJECT + + public: + + GraphWidget( QWidget* parent = nullptr ); + + void itemMoved(); + + std::unordered_map< idhan::TagID, TagNode* > m_tag_nodes {}; + QBasicTimer timer; + + public slots: + // void zoomIn(); + // void zoomOut(); + + public: + + void setRootNode( TagNode* node ); + void addRelationship( TagNode* left, TagNode* right, TagRelationship::RelationshipType relationship ); + + protected: + + void drawBackground( QPainter* painter, const QRectF& rect ) override; + + private slots: + void timerEvent( QTimerEvent* event ) override; +}; diff --git a/tools/TagEditor/src/TagNode.cpp b/tools/TagEditor/src/TagNode.cpp new file mode 100644 index 0000000..b1dc6f7 --- /dev/null +++ b/tools/TagEditor/src/TagNode.cpp @@ -0,0 +1,346 @@ +// +// Created by kj16609 on 5/2/25. +// +#include "TagNode.hpp" + +#include + +#include + +#include "TagRelationship.hpp" + +TagNode::TagNode( GraphWidget* widget, const idhan::TagID tag_id ) : + m_id( tag_id ), + tag_text( "loading..." ), + graph( widget ) +{ + FGL_ASSERT( graph, "Graph was nullptr!" ); + + setFlag( ItemIsMovable ); + setFlag( ItemSendsGeometryChanges ); + setCacheMode( DeviceCoordinateCache ); + setZValue( -1 ); + + connect( this, &TagNode::triggerSpawnRelated, this, &TagNode::getRelatedInfo ); + connect( this, &TagNode::triggerGatherName, this, &TagNode::gatherName ); + connect( + &m_relationshipinfo_watcher, &decltype( m_relationshipinfo_watcher )::finished, this, &TagNode::spawnRelated ); + + // spawnRelationships(); + emit triggerGatherName(); +} + +void TagNode::spawnRelationships() +{ + if ( m_relationships_spawned ) return; + m_relationships_spawned = true; + emit triggerSpawnRelated(); +} + +void TagNode::addRelationship( TagRelationship* relationship ) +{ + m_relationships.push_back( relationship ); +} + +double modifier( const double x, const double a ) +{ + constexpr double clamp { 50.0 }; + constexpr double div { 30.0 }; + return std::clamp< double >( std::pow( x - a, 5.0 ) / div, -clamp, clamp ); +} + +void TagNode::calculateForces() +{ + QVector2D force_direction { 0, 0 }; + qreal largest_dist { 0.0f }; + + FGL_ASSERT( graph, "Graph was nullptr!" ); + + const QList< QGraphicsItem* > items = graph->scene()->items(); + + const auto cur_pos { this->pos() }; + if ( std::isnan( cur_pos.x() ) || std::isnan( cur_pos.y() ) ) + { + new_pos = { 0.0f, 0.0f }; + return; + } + + /* + for ( const QGraphicsItem* item : items ) + { + const TagRelationship* relationship { qgraphicsitem_cast< const TagRelationship* >( item ) }; + if ( !relationship ) continue; + + // the left node is, children, older_siblings, and aliased_tags + if ( relationship->leftNode() != this ) continue; + + // Children are pulled toward parents, + // Older siblings are pulled toward younger siblings (TOOD: Reverse this) + // Aliased tags are pulled closer to their alias + + const TagNode* other { relationship->rightNode() }; + } + */ + + // Push nodes away from each other + for ( const QGraphicsItem* item : items ) + { + const TagNode* other = qgraphicsitem_cast< const TagNode* >( item ); + if ( !other || other == this ) continue; + + const QVector2D vec_to_target { other->pos() - pos() }; + QVector2D vec_to_target_normalized { vec_to_target.normalized() }; + qreal distance = vec_to_target.length(); + + bool has_relationship { false }; + for ( const auto& relationship : std::as_const( m_relationships ) ) + { + if ( relationship->rightNode() == other ) + { + has_relationship = true; + break; + } + } + + // We have a relationship with this node, So we should only apply relationship weights. + if ( has_relationship ) continue; + + if ( std::isnan( distance ) || std::isinf( distance ) ) continue; + + if ( distance < 1.0f ) + { + // If the nodes are right on top of eachother, move them. + new_pos = pos() + QPointF( rand() % 50, rand() % 50 ); + return; + } + + constexpr double range { 80 }; // leeway around the target + constexpr double target { 300 }; // target + constexpr double offset { target / range }; + + //remap the distance from [-200,200] (-offset,offset) to [-1,1] + const double normalized_distance = ( distance / range ); + + const auto mod { modifier( normalized_distance, offset ) }; + + if ( mod > 0.0f ) continue; + + vec_to_target_normalized *= ( mod ); + + force_direction += vec_to_target_normalized; + } + /* + // Add constant force pulling toward center + constexpr double center_pull { 0.5 }; + QVector2D to_center { -pos() }; + if ( to_center.length() > 1.0 ) + { + to_center.normalize(); + force_direction += to_center * center_pull; + } + */ + + for ( const QGraphicsItem* item : items ) + { + const TagRelationship* relationship { qgraphicsitem_cast< const TagRelationship* >( item ) }; + if ( !relationship ) continue; + + // the left node is, children, older_siblings, and aliased_tags + if ( relationship->leftNode() != this ) continue; + + // Children are pulled toward parents, + // Older siblings are pulled toward younger siblings (TOOD: Reverse this) + // Aliased tags are pulled closer to their alias + + const TagNode* other { relationship->rightNode() }; + + QVector2D target_vector { other->pos() - pos() }; + const auto distance { target_vector.length() }; + + target_vector.normalize(); + + constexpr double range { 80 }; // leeway around the target + constexpr double target { 300 }; // target + constexpr double offset { target / range }; + const double normalized_distance { distance / range }; + + const auto mod { modifier( normalized_distance, offset ) }; + + // Pull toward parent, older sibling, alias + force_direction += target_vector * mod; + } + + auto xvel { force_direction.x() }; + auto yvel { force_direction.y() }; + + // Limit maximum velocity + // if ( qAbs( xvel ) < 0.01 ) xvel = 0; + // if ( qAbs( yvel ) < 0.01 ) yvel = 0; + + new_pos = pos() + QPointF( xvel, yvel ); +} + +bool TagNode::advancePosition() +{ + if ( new_pos == pos() ) return false; + + setPos( new_pos ); + return true; +} + +const QFont FONT { "Arial", 12 }; + +QRectF TagNode::boundingRect() const +{ + // The bounding rect for a tag node should be the text of the tag + const QFontMetrics metrics( FONT ); + QRectF bounds { metrics.boundingRect( tag_text ) }; + + // Add some padding around the text + bounds.adjust( -10, -10, 10, 10 ); + return bounds; +} + +QPainterPath TagNode::shape() const +{ + return QGraphicsItem::shape(); +} + +void TagNode::paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget ) +{ + QRectF bounds = boundingRect(); + + // Draw rectangle + painter->setPen( QPen( Qt::black, 2 ) ); + painter->setBrush( QBrush( Qt::white ) ); + painter->drawRect( bounds ); + + // Draw text + painter->setPen( Qt::black ); + painter->setFont( FONT ); + painter->drawText( bounds, Qt::AlignCenter, tag_text ); +} + +TagNode* checkNodes( std::unordered_map< idhan::TagID, TagNode* >& nodes, const idhan::TagID& id, GraphWidget* graph ) +{ + if ( auto itter = nodes.find( id ); itter != nodes.end() ) return itter->second; + + // Node doesn't exist. + idhan::logging::info( "Spawning node {}", id ); + TagNode* other { new TagNode( graph, id ) }; + nodes.insert_or_assign( id, other ); + graph->scene()->addItem( other ); + + return other; +} + +void TagNode::spawnRelated() +{ + const auto& future { m_relationshipinfo_watcher.future() }; + + if ( future.isFinished() ) + { + const idhan::TagRelationshipInfo& info { future.result() }; + + const auto& [ aliased, aliases, parents, children, older_siblings, younger_siblings ] = info; + + auto& existing_nodes { graph->m_tag_nodes }; + + for ( const auto& aliased_id : aliased ) + { + auto* node { checkNodes( existing_nodes, aliased_id, graph ) }; + graph->addRelationship( node, this, TagRelationship::Aliased ); + node->spawnRelationships(); + } + + for ( const auto& alias : aliases ) + { + auto* node { checkNodes( existing_nodes, alias, graph ) }; + graph->addRelationship( this, node, TagRelationship::Aliased ); + node->spawnRelationships(); + } + + /* + for ( const auto& parent_id : parents ) + { + auto* node { checkNodes( existing_nodes, parent_id, graph ) }; + graph->addRelationship( node, this, TagRelationship::ParentChild ); + // node->spawnRelationships(); + } + + for ( const auto& child_id : children ) + { + auto* node { checkNodes( existing_nodes, child_id, graph ) }; + graph->addRelationship( this, node, TagRelationship::ParentChild ); + // node->spawnRelationships(); + } + + for ( const auto& older_sibling_id : older_siblings ) + { + auto* node { checkNodes( existing_nodes, older_sibling_id, graph ) }; + graph->addRelationship( node, this, TagRelationship::Sibling ); + // node->spawnRelationships(); + } + + for ( const auto& younger_sibling_id : younger_siblings ) + { + auto* node { checkNodes( existing_nodes, younger_sibling_id, graph ) }; + graph->addRelationship( this, node, TagRelationship::Sibling ); + // node->spawnRelationships(); + } + */ + + // add node to graph + } + else + { + throw std::runtime_error( "Huh?" ); + } +} + +void TagNode::getRelatedInfo() +{ + const auto m_domain { 5 }; + auto future { idhan::IDHANClient::instance().getTagRelationships( m_id, m_domain ) }; + + m_relationshipinfo_watcher.setFuture( future ); +} + +void TagNode::gatherName() +{ + m_taginfo_watcher.setFuture( idhan::IDHANClient::instance().getTagInfo( m_id ) ); + + connect( + &m_taginfo_watcher, + &decltype( m_taginfo_watcher )::finished, + this, + &TagNode::gotName, + Qt::SingleShotConnection ); +} + +void TagNode::gotName() +{ + const auto result { m_taginfo_watcher.future().result() }; + this->tag_text = QString::number( m_id ) + ": " + result.toQString(); + + update(); +} + +QVariant TagNode::itemChange( GraphicsItemChange change, const QVariant& value ) +{ + switch ( change ) + { + case ItemPositionHasChanged: + { + for ( const auto relationship : std::as_const( m_relationships ) ) + { + relationship->adjust(); + } + graph->itemMoved(); + } + default: + break; + } + + return QGraphicsItem::itemChange( change, value ); +} diff --git a/tools/TagEditor/src/TagNode.hpp b/tools/TagEditor/src/TagNode.hpp new file mode 100644 index 0000000..438eb1b --- /dev/null +++ b/tools/TagEditor/src/TagNode.hpp @@ -0,0 +1,78 @@ +// +// Created by kj16609 on 5/2/25. +// +#pragma once + +#include + +#include "GraphWidget.hpp" +#include "TagRelationship.hpp" +#include "idhan/IDHANClient.hpp" + +class TagNode final : public QObject, public QGraphicsItem +{ + Q_OBJECT + Q_INTERFACES( QGraphicsItem ) + + idhan::TagID m_id; + QString tag_text; + QList< TagRelationship* > m_relationships {}; + QFutureWatcher< idhan::TagRelationshipInfo > m_relationshipinfo_watcher; + QFutureWatcher< idhan::TagInfo > m_taginfo_watcher; + bool m_relationships_spawned { false }; + + std::vector< TagNode* > left_siblings {}; + std::vector< TagNode* > right_siblings {}; + + std::vector< TagNode* > children {}; + std::vector< TagNode* > parents {}; + + std::vector< TagNode* > aliased_tags {}; + std::vector< TagNode* > ideal_tags {}; + + public: + + TagNode( GraphWidget* widget, idhan::TagID tag_id ); + void spawnRelationships(); + + void addRelationship( TagRelationship* relationship ); + + QList< TagRelationship* > edges() const { return m_relationships; } + + enum + { + Type = UserType + 1 + }; + + int type() const override { return Type; } + + void calculateForces(); + bool advancePosition(); + + QRectF boundingRect() const override; + QPainterPath shape() const override; + void paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget ) override; + + virtual ~TagNode() {} + + idhan::TagID tagID() const { return m_id; } + + signals: + void triggerSpawnRelated(); + void triggerGatherName(); + + private slots: + void spawnRelated(); + void getRelatedInfo(); + void gatherName(); + void gotName(); + + protected: + + QVariant itemChange( GraphicsItemChange change, const QVariant& value ) override; + + private: + + QPointF new_pos; + GraphWidget* graph; +}; diff --git a/tools/TagEditor/src/TagRelationship.cpp b/tools/TagEditor/src/TagRelationship.cpp new file mode 100644 index 0000000..ca697de --- /dev/null +++ b/tools/TagEditor/src/TagRelationship.cpp @@ -0,0 +1,151 @@ +// +// Created by kj16609 on 5/3/25. +// +#include "TagRelationship.hpp" + +#include "TagNode.hpp" + +TagRelationship::TagRelationship( TagNode* left, TagNode* right, const RelationshipType relationship ) : + m_relationship( relationship ), + m_left( left ), + m_right( right ) +{ + setZValue( -1 ); // Draw behind nodes + m_left->addRelationship( this ); + m_right->addRelationship( this ); + adjust(); +} + +QPointF intersects( const QRectF& rect, const QLineF& line ) +{ + const auto top_left { rect.topLeft() }; + const auto bottom_right { rect.bottomRight() }; + + const auto top_right { QPointF( bottom_right.x(), top_left.y() ) }; + const auto bottom_left { QPointF( top_left.x(), bottom_right.y() ) }; + + const QLineF top_line { top_left, top_right }; + const QLineF bottom_line { bottom_left, bottom_right }; + const QLineF left_line { top_left, bottom_left }; + const QLineF right_line { top_right, bottom_right }; + + QPointF closest_intersection { line.p2() }; + QPointF intersection { line.p2() }; + + const auto isCloser = [ &line, &closest_intersection ]( QPointF point ) + { + const QLineF line_to_point { line.p1(), point }; + const QLineF line_to_prev_point { line.p1(), closest_intersection }; + + return line_to_point.length() < line_to_prev_point.length(); + }; + + if ( line.intersects( top_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) ) + closest_intersection = intersection; + + if ( line.intersects( bottom_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) ) + closest_intersection = intersection; + + if ( line.intersects( left_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) ) + closest_intersection = intersection; + + if ( line.intersects( right_line, &intersection ) == QLineF::BoundedIntersection && isCloser( intersection ) ) + closest_intersection = intersection; + + return closest_intersection; +} + +void TagRelationship::adjust() +{ + const QLineF line { mapFromItem( m_left, 0, 0 ), mapFromItem( m_right, 0, 0 ) }; + const qreal length { line.length() }; + + const auto left_bounds { m_left->sceneBoundingRect() }; + const auto right_bounds { m_right->sceneBoundingRect() }; + + prepareGeometryChange(); + + if ( length > qreal( 20.0 ) ) + { + QPointF edge_offset { ( line.dx() * 10 ) / length, ( line.dy() * 10 ) / length }; + // source_point = line.p1() + edge_offset; + // target_point = line.p2() - edge_offset; + + // source_point = intersects( left_bounds, line ); + source_point = intersects( left_bounds, line ); + target_point = intersects( right_bounds, line ); + } + else + { + source_point = line.p1(); + target_point = line.p1(); + } +} + +QRectF TagRelationship::boundingRect() const +{ + constexpr qreal pen_width { 1.0 }; + const qreal extra { ( pen_width + arrow_size ) / 2.0f }; + + return QRectF( source_point, QSizeF( target_point.x() - source_point.x(), target_point.y() - source_point.y() ) ) + .normalized() + .adjusted( -extra, -extra, extra, extra ); +} + +void TagRelationship::paint( QPainter* painter, const QStyleOptionGraphicsItem*, QWidget* ) +{ + static constexpr qreal arrow_size { 10.0 }; + static constexpr qreal node_offset { 10.0 }; + QLineF line { source_point, target_point }; + if ( qFuzzyCompare( line.length(), qreal( 0.0 ) ) ) return; + + const QPen alias_pen { Qt::white, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin }; + const QPen parent_pen { Qt::blue, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin }; + const QPen sibling_pen { Qt::red, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin }; + const QPen outline_pen { Qt::black, 3, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin }; + + // Calculate offset target point + const double angle { std::atan2( -line.dy(), line.dx() ) }; + const QPointF offset_target { target_point + - QPointF( std::cos( angle ) * node_offset, -std::sin( angle ) * node_offset ) }; + QLineF offset_line { source_point, offset_target }; + + // Draw black outline + painter->setPen( outline_pen ); + painter->drawLine( offset_line ); + + switch ( m_relationship ) + { + case ParentChild: + painter->setPen( parent_pen ); + break; + case Sibling: + painter->setPen( sibling_pen ); + break; + default: + [[fallthrough]] + case Aliased: + painter->setPen( alias_pen ); + break; + } + + painter->drawLine( offset_line ); + + QPointF sourceArrowP1 { source_point + + QPointF( sin( angle + M_PI / 3 ) * arrow_size, cos( angle + M_PI / 3 ) * arrow_size ) }; + QPointF sourceArrowP2 { + source_point + + QPointF( sin( angle + M_PI - M_PI / 3 ) * arrow_size, cos( angle + M_PI - M_PI / 3 ) * arrow_size ) + }; + + QPointF destArrowP1 { target_point + + QPointF( sin( angle - M_PI / 3 ) * arrow_size, cos( angle - M_PI / 3 ) * arrow_size ) }; + QPointF destArrowP2 { + target_point + + QPointF( sin( angle - M_PI + M_PI / 3 ) * arrow_size, cos( angle - M_PI + M_PI / 3 ) * arrow_size ) + }; + + painter->setBrush( Qt::black ); + // painter->drawPolygon( QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2 ); + painter->drawPolygon( QPolygonF() << line.p2() << destArrowP1 << destArrowP2 ); +} \ No newline at end of file diff --git a/tools/TagEditor/src/TagRelationship.hpp b/tools/TagEditor/src/TagRelationship.hpp new file mode 100644 index 0000000..a5942e2 --- /dev/null +++ b/tools/TagEditor/src/TagRelationship.hpp @@ -0,0 +1,68 @@ +// +// Created by kj16609 on 5/3/25. +// +#pragma once +#include + +class TagNode; + +class TagRelationship : public QObject, public QGraphicsItem +{ + Q_OBJECT + Q_INTERFACES( QGraphicsItem ) + + public: + + enum class RelationshipType + { + ParentChild, + Sibling, + Aliased + } m_relationship; + + using enum RelationshipType; + + private: + + // Parent/OlderSibling/AliasedTag + union + { + TagNode* m_left; + TagNode* child; + TagNode* older_sibling; + TagNode* aliased_tag; + }; + + // Child/YoungerSibling/IdealTag + union + { + TagNode* m_right; + TagNode* parent; + TagNode* younger_sibling; + TagNode* ideal_tag; + }; + + QPointF source_point; + + QPointF target_point; + qreal arrow_size { 10 }; + + public: + + TagRelationship( TagNode* left, TagNode* right, RelationshipType relationship ); + + TagNode* leftNode() const { return m_left; } + + TagNode* rightNode() const { return m_right; } + + void adjust(); + QRectF boundingRect() const override; + void paint( QPainter* painter, const QStyleOptionGraphicsItem*, QWidget* ) override; + + enum + { + Type = UserType + 2 + }; + + int type() const override { return Type; } +}; diff --git a/tools/TagEditor/src/main.cpp b/tools/TagEditor/src/main.cpp new file mode 100644 index 0000000..4693cc2 --- /dev/null +++ b/tools/TagEditor/src/main.cpp @@ -0,0 +1,18 @@ +// +// Created by kj16609 on 5/2/25. +// + +#include + +#include "ui/MainWindow.hpp" + +int main( int argc, char** argv ) +{ + QApplication app { argc, argv }; + + MainWindow window; + + window.show(); + + app.exec(); +} diff --git a/tools/TagEditor/src/ui/MainWindow.cpp b/tools/TagEditor/src/ui/MainWindow.cpp new file mode 100644 index 0000000..e1dad5b --- /dev/null +++ b/tools/TagEditor/src/ui/MainWindow.cpp @@ -0,0 +1,89 @@ +// +// Created by kj16609 on 5/2/25. +// +// You may need to build the project (run Qt uic code generator) to get "ui_MainWindow.h" resolved + +#include "MainWindow.hpp" + +#include +#include +#include + +#include "TagNode.hpp" +#include "ui_MainWindow.h" + +MainWindow::MainWindow( QWidget* parent ) : QMainWindow( parent ), ui( new Ui::MainWindow ) +{ + ui->setupUi( this ); + + // TagNode* node { new TagNode( ui->graphicsView ) }; + + // ui->graphicsView->scene()->addItem( node ); + ui->tagSearch->setCompleter( &m_completer ); + m_completer.setCaseSensitivity( Qt::CaseInsensitive ); + m_completer.setCompletionMode( QCompleter::UnfilteredPopupCompletion ); + m_completer.setModel( m_model.get() ); + + connect( + &m_autocomplete_watcher, + &QFutureWatcher< std::vector< std::pair< idhan::TagID, std::string > > >::finished, + this, + &MainWindow::autocompleteFinished ); + + connect( + &m_completer, QOverload< const QString& >::of( &QCompleter::activated ), ui->tagSearch, &QLineEdit::setText ); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::on_tagSearch_textChanged( const QString& text ) +{ + const auto tags { m_client.autocompleteTag( text ) }; + + m_autocomplete_watcher.setFuture( tags ); + + idhan::logging::debug( "Searching for tags matching \"{}\"", text.toStdString() ); +} + +void MainWindow::on_tagSearch_returnPressed() +{ + const auto tag_name { ui->tagSearch->text() }; + + idhan::logging::debug( "Searching for tag: {}", tag_name.toStdString() ); + + const auto itter { tag_map.find( tag_name.toStdString() ) }; + + if ( itter != tag_map.end() ) + { + auto* root { new TagNode( ui->graphicsView, itter->second ) }; + + ui->graphicsView->setRootNode( root ); + } + else + { + ui->tagSearch->setText( "" ); + } +} + +void MainWindow::autocompleteFinished() +{ + idhan::logging::debug( "Autocomplete finished" ); + + QStringList list {}; + + const auto& future { m_autocomplete_watcher.future() }; + + for ( const auto& tag : future.result() ) + { + const auto [ tag_id, string ] = tag; + list.append( QString::fromStdString( string ) ); + tag_map.emplace( string, tag_id ); + } + + idhan::logging::debug( "Got {} tags", future.result().size() ); + + m_model->setStringList( list ); +} diff --git a/tools/TagEditor/src/ui/MainWindow.hpp b/tools/TagEditor/src/ui/MainWindow.hpp new file mode 100644 index 0000000..db07583 --- /dev/null +++ b/tools/TagEditor/src/ui/MainWindow.hpp @@ -0,0 +1,54 @@ +// +// Created by kj16609 on 5/2/25. +// +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP + +#include +#include +#include +#include + +#include "NET_CONSTANTS.hpp" +#include "idhan/IDHANClient.hpp" + +class QStringListModel; +QT_BEGIN_NAMESPACE + +namespace Ui +{ +class MainWindow; +} + +QT_END_NAMESPACE + +class MainWindow final : public QMainWindow +{ + Q_OBJECT + + public: + + idhan::IDHANClientConfig m_client_config { "localhost", idhan::IDHAN_DEFAULT_PORT, "Tag Editor", false }; + + idhan::IDHANClient m_client { m_client_config }; + + QCompleter m_completer { this }; + std::unordered_map< std::string, idhan::TagID > tag_map {}; + std::unique_ptr< QStringListModel > m_model { new QStringListModel() }; + + QFutureWatcher< std::vector< std::pair< idhan::TagID, std::string > > > m_autocomplete_watcher {}; + + explicit MainWindow( QWidget* parent = nullptr ); + ~MainWindow() override; + + public slots: + void on_tagSearch_textChanged( const QString& text ); + void on_tagSearch_returnPressed(); + void autocompleteFinished(); + + private: + + Ui::MainWindow* ui; +}; + +#endif //MAINWINDOW_HPP diff --git a/tools/TagEditor/src/ui/MainWindow.ui b/tools/TagEditor/src/ui/MainWindow.ui new file mode 100644 index 0000000..358d630 --- /dev/null +++ b/tools/TagEditor/src/ui/MainWindow.ui @@ -0,0 +1,47 @@ + + + MainWindow + + + + 0 + 0 + 1486 + 947 + + + + MainWindow + + + + + + + + + + + + + + + 0 + 0 + 1486 + 30 + + + + + + + + GraphWidget + QGraphicsView +
GraphWidget.hpp
+
+
+ + +