diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a0be26ea..977c6e3d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,11 +61,14 @@ set(HEIMER_LIB_SRC dialogs/defaults_tab.cpp dialogs/editing_tab.cpp dialogs/effects_tab.cpp + dialogs/image_file_dialog.cpp + dialogs/image_file_display.cpp dialogs/layout_optimization_dialog.cpp dialogs/png_export_dialog.cpp dialogs/scene_color_dialog.cpp dialogs/settings_dialog.cpp dialogs/settings_tab_base.cpp + dialogs/special_content_dialog.cpp dialogs/spinner_dialog.cpp dialogs/svg_export_dialog.cpp dialogs/whats_new_dialog.cpp @@ -98,6 +101,11 @@ set(HEIMER_LIB_SRC scene_items/scene_item_base.cpp scene_items/text_edit.cpp + special_content/special_content_button.cpp + special_content/special_content_model.hpp + special_content/special_content_tray.cpp + special_content/special_content_tray_model.hpp + widgets/font_button.cpp widgets/status_label.cpp ) diff --git a/src/application.cpp b/src/application.cpp index 2a57e1e0..3b487fd1 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -33,15 +33,16 @@ #include "core/user_exception.hpp" #include "core/version_checker.hpp" +#include "dialogs/image_file_dialog.hpp" #include "dialogs/layout_optimization_dialog.hpp" #include "dialogs/png_export_dialog.hpp" #include "dialogs/scene_color_dialog.hpp" +#include "dialogs/special_content_dialog.hpp" #include "dialogs/svg_export_dialog.hpp" #include "argengine.hpp" #include "simple_logger.hpp" -#include #include #include #include @@ -274,6 +275,9 @@ void Application::runState(StateMachine::State state) case StateMachine::State::ShowNodeColorDialog: showNodeColorDialog(); break; + case StateMachine::State::ShowSpecialContentDialog: + showSpecialContentDialog(); + break; case StateMachine::State::ShowTextColorDialog: showTextColorDialog(); break; @@ -426,6 +430,15 @@ void Application::showNodeColorDialog() emit actionTriggered(StateMachine::Action::NodeColorChanged); } +void Application::showSpecialContentDialog() +{ + Dialogs::SpecialContentDialog dialog(m_mainWindow.get()); + if (dialog.exec() == QDialog::Accepted) { + m_mediator->performNodeAction({ NodeAction::Type::AddSpecialContent, dialog.specialContentModel() }); + } + emit actionTriggered(StateMachine::Action::SpecialContentAdded); +} + void Application::showTextColorDialog() { if (Dialogs::SceneColorDialog(Dialogs::ColorDialog::Role::Text, m_mediator).exec() != QDialog::Accepted) { @@ -437,16 +450,9 @@ void Application::showTextColorDialog() void Application::showImageFileDialog() { - const auto path = Settings::V1::loadRecentImagePath(); - const auto extensions = "(*.jpg *.jpeg *.JPG *.JPEG *.png *.PNG)"; - const auto fileName = QFileDialog::getOpenFileName( - m_mainWindow.get(), tr("Open an image"), path, tr("Image Files") + " " + extensions); - - if (QImage image; image.load(fileName)) { - m_mediator->performNodeAction({ NodeAction::Type::AttachImage, image, fileName }); - Settings::V1::saveRecentImagePath(fileName); - } else if (fileName != "") { - QMessageBox::critical(m_mainWindow.get(), tr("Load image"), tr("Failed to load image '") + fileName + "'"); + Dialogs::ImageFileDialog dialog(m_mainWindow.get()); + if (const auto image = dialog.loadImage(); image.has_value()) { + m_mediator->performNodeAction({ NodeAction::Type::AttachImage, image->image(), image->path().c_str() }); } } diff --git a/src/application.hpp b/src/application.hpp index c20985e7..7cf807c4 100644 --- a/src/application.hpp +++ b/src/application.hpp @@ -88,6 +88,8 @@ public slots: void showPngExportDialog(); + void showSpecialContentDialog(); + void showSvgExportDialog(); void showTextColorDialog(); diff --git a/src/constants.hpp b/src/constants.hpp index 032b432d..8aeecddc 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -211,6 +211,8 @@ static const int MIN_HEIGHT = 75; static const int MIN_WIDTH = 200; +static const int SPECIAL_CONTENT_TRAY_HEIGHT = 20; + static const QColor TEXT_EDIT_BACKGROUND_COLOR_DARK { 0x00, 0x00, 0x00, 0x10 }; static const QColor TEXT_EDIT_BACKGROUND_COLOR_LIGHT { 0xff, 0xff, 0xff, 0x10 }; diff --git a/src/dialogs/defaults_tab.cpp b/src/dialogs/defaults_tab.cpp index be7c16e0..89893314 100644 --- a/src/dialogs/defaults_tab.cpp +++ b/src/dialogs/defaults_tab.cpp @@ -211,8 +211,7 @@ void DefaultsTab::createTextWidgets(QVBoxLayout & mainLayout) void DefaultsTab::setActiveDefaults() { if (const auto defaultArrowStyle = settingsProxy().edgeArrowMode(); m_edgeArrowStyleRadioMap.count(defaultArrowStyle)) { - const auto radio = m_edgeArrowStyleRadioMap[defaultArrowStyle]; - radio->setChecked(true); + m_edgeArrowStyleRadioMap.at(defaultArrowStyle)->setChecked(true); } else { juzzlin::L().error() << "Invalid arrow style: " << static_cast(defaultArrowStyle); } diff --git a/src/dialogs/image_file_dialog.cpp b/src/dialogs/image_file_dialog.cpp new file mode 100644 index 00000000..f30b9e89 --- /dev/null +++ b/src/dialogs/image_file_dialog.cpp @@ -0,0 +1,53 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#include "image_file_dialog.hpp" + +#include "../core/settings.hpp" + +#include +#include + +namespace Dialogs { + +ImageFileDialog::ImageFileDialog(QWidget * parent) + : QFileDialog(parent) +{ + setNameFilter("(*.jpg *.jpeg *.JPG *.JPEG *.png *.PNG)"); + setViewMode(QFileDialog::Detail); +} + +std::optional ImageFileDialog::loadImage() +{ + const auto path = Settings::V1::loadRecentImagePath(); + setHistory({ path }); + setDirectory(QFileInfo(path).path()); + + if (exec()) { + if (auto fileNames = selectedFiles(); !fileNames.isEmpty()) { + const auto fileName = fileNames.at(0); + if (QImage image; image.load(fileName)) { + Settings::V1::saveRecentImagePath(fileName); + return { { image, fileName.toStdString() } }; + } else if (fileName != "") { + QMessageBox::critical(this, tr("Load image"), tr("Failed to load image '") + fileName + "'"); + } + } + } + + return {}; +} + +} // namespace Dialogs diff --git a/src/dialogs/image_file_dialog.hpp b/src/dialogs/image_file_dialog.hpp new file mode 100644 index 00000000..81541a02 --- /dev/null +++ b/src/dialogs/image_file_dialog.hpp @@ -0,0 +1,39 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef IMAGE_FILE_DIALOG_HPP +#define IMAGE_FILE_DIALOG_HPP + +#include "../image.hpp" + +#include + +#include + +namespace Dialogs { + +class ImageFileDialog : QFileDialog +{ + Q_OBJECT + +public: + ImageFileDialog(QWidget * parent = nullptr); + + std::optional loadImage(); +}; + +} // namespace Dialogs + +#endif // IMAGE_FILE_DIALOG_HPP diff --git a/src/dialogs/image_file_display.cpp b/src/dialogs/image_file_display.cpp new file mode 100644 index 00000000..f70181fd --- /dev/null +++ b/src/dialogs/image_file_display.cpp @@ -0,0 +1,103 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#include "image_file_display.hpp" + +#include +#include +#include +#include + +namespace Dialogs { + +ImageFileDisplay::ImageFileDisplay(QWidget * parent) + : QWidget(parent) +{ + setAttribute(Qt::WA_Hover, true); +} + +bool ImageFileDisplay::event(QEvent * event) +{ + switch (event->type()) { + case QEvent::HoverEnter: + if (m_image.has_value()) { + setCursor(Qt::PointingHandCursor); + } + return true; + case QEvent::HoverLeave: + unsetCursor(); + return true; + default: + break; + } + return QWidget::event(event); +} + +QSize ImageFileDisplay::sizeHint() const +{ + return { 400, 300 }; +} + +QSize ImageFileDisplay::minimumSizeHint() const +{ + return { 40, 30 }; +} + +void ImageFileDisplay::mousePressEvent(QMouseEvent * event) +{ + if (m_image.has_value()) { + QDesktopServices::openUrl(QUrl(QString("file://") + m_image->path().c_str())); + } + + QWidget::mousePressEvent(event); +} + +void ImageFileDisplay::paintEvent(QPaintEvent * event) +{ + Q_UNUSED(event) + + QPainter painter(this); + painter.setPen(palette().dark().color()); + painter.setBrush(Qt::NoBrush); + painter.drawRect(QRect(0, 0, width() - 1, height() - 1)); + + if (m_image.has_value()) { + const auto imageWidth = m_image->image().width(); + const auto imageHeight = m_image->image().height(); + const auto margin = 1; + if (imageWidth > imageHeight) { + auto targetRect = QRect(margin, margin, this->width() - margin * 2, (this->width() - margin * 2) * imageHeight / imageWidth); + const auto marginTop = ((this->height() - 2) - targetRect.height()) / 2; + targetRect.moveTo(targetRect.x(), targetRect.y() + marginTop); + painter.drawImage(targetRect, m_image->image()); + } else { + auto targetRect = QRect(margin, margin, this->height() - 2 * imageWidth / imageHeight, this->height() - margin * 2); + const auto marginLeft = ((this->width() - margin * 2) - targetRect.width()) / 2; + targetRect.moveTo(targetRect.x() + marginLeft, targetRect.y()); + painter.drawImage(targetRect, m_image->image()); + } + } +} + +void ImageFileDisplay::setImage(const Image & image) +{ + m_image = image; + + setToolTip(tr("Click to open the image in browser")); + + update(); +} + +} // namespace Dialogs diff --git a/src/dialogs/image_file_display.hpp b/src/dialogs/image_file_display.hpp new file mode 100644 index 00000000..e39842fd --- /dev/null +++ b/src/dialogs/image_file_display.hpp @@ -0,0 +1,52 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef IMAGE_FILE_DISPLAY_HPP +#define IMAGE_FILE_DISPLAY_HPP + +#include + +#include + +#include "../image.hpp" + +namespace Dialogs { + +class ImageFileDisplay : public QWidget +{ + Q_OBJECT + +public: + explicit ImageFileDisplay(QWidget * parent = nullptr); + + bool event(QEvent * event) override; + + QSize sizeHint() const override; + + QSize minimumSizeHint() const override; + + void mousePressEvent(QMouseEvent * event) override; + + void setImage(const Image & image); + +protected: + void paintEvent(QPaintEvent * event) override; + + std::optional m_image; +}; + +} // namespace Dialogs + +#endif // IMAGE_FILE_DISPLAY_HPP diff --git a/src/dialogs/special_content_dialog.cpp b/src/dialogs/special_content_dialog.cpp new file mode 100644 index 00000000..07f60d78 --- /dev/null +++ b/src/dialogs/special_content_dialog.cpp @@ -0,0 +1,97 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#include "special_content_dialog.hpp" + +#include "image_file_dialog.hpp" +#include "image_file_display.hpp" +#include "widget_factory.hpp" + +#include "../mediator.hpp" + +#include +#include +#include +#include +#include +#include + +namespace Dialogs { + +bool isValidUrl(QString url) +{ + return !url.isEmpty() && QUrl(url).isValid(); +} + +SpecialContentDialog::SpecialContentDialog(QWidget * parent) + : QDialog(parent) + , m_urlEdit(new QLineEdit(this)) +{ + setWindowTitle(tr("Add special content")); + setMinimumWidth(640); + + initWidgets(); +} + +SpecialContent::SpecialContentModel SpecialContentDialog::specialContentModel() const +{ + return m_specialContentModel; +} + +void SpecialContentDialog::initWidgets() +{ + const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::accepted, this, [=]() { + if (isValidUrl(m_urlEdit->text())) { + m_specialContentModel.url = m_urlEdit->text(); + } + QDialog::accept(); + }); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + const auto mainLayout = new QVBoxLayout; + initWebLinkWidgets(*mainLayout, *buttonBox); + initImageWidgets(*mainLayout); + + mainLayout->addWidget(buttonBox); + setLayout(mainLayout); +} + +void SpecialContentDialog::initWebLinkWidgets(QVBoxLayout & mainLayout, QDialogButtonBox & buttonBox) +{ + const auto webLinkGroup = WidgetFactory::buildGroupBoxWithVLayout(tr("Web link"), mainLayout); + connect(m_urlEdit, &QLineEdit::textChanged, this, [&buttonBox](QString text) { + buttonBox.button(QDialogButtonBox::Ok)->setEnabled(isValidUrl(text) || text.isEmpty()); + }); + webLinkGroup.second->addWidget(m_urlEdit); +} + +void SpecialContentDialog::initImageWidgets(QVBoxLayout & mainLayout) +{ + const auto imageGroup = WidgetFactory::buildGroupBoxWithVLayout(tr("Image file"), mainLayout); + const auto imageButton = new QPushButton(tr("Select image file")); + imageGroup.second->addWidget(imageButton); + const auto imageFileDisplay = new ImageFileDisplay; + imageGroup.second->addWidget(imageFileDisplay); + connect(imageButton, &QPushButton::clicked, this, [=] { + ImageFileDialog dialog(this); + if (const auto image = dialog.loadImage(); image.has_value()) { + imageFileDisplay->setImage(image.value()); + m_specialContentModel.image = image; + } + }); +} + +} // namespace Dialogs diff --git a/src/dialogs/special_content_dialog.hpp b/src/dialogs/special_content_dialog.hpp new file mode 100644 index 00000000..8435e1d8 --- /dev/null +++ b/src/dialogs/special_content_dialog.hpp @@ -0,0 +1,54 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef SPECIAL_CONTENT_DIALOG_HPP +#define SPECIAL_CONTENT_DIALOG_HPP + +#include + +#include + +#include "../special_content/special_content_model.hpp" + +class QDialogButtonBox; +class QLineEdit; +class QVBoxLayout; + +namespace Dialogs { + +class SpecialContentDialog : public QDialog +{ + Q_OBJECT + +public: + SpecialContentDialog(QWidget * parent); + + SpecialContent::SpecialContentModel specialContentModel() const; + +private: + void initWidgets(); + + void initWebLinkWidgets(QVBoxLayout & mainLayout, QDialogButtonBox & buttonBox); + + void initImageWidgets(QVBoxLayout & mainLayout); + + QLineEdit * m_urlEdit; + + SpecialContent::SpecialContentModel m_specialContentModel; +}; + +} // namespace Dialogs + +#endif // SPECIAL_CONTENT_DIALOG_HPP diff --git a/src/editor_data.cpp b/src/editor_data.cpp index 8aac1a77..c91b9572 100644 --- a/src/editor_data.cpp +++ b/src/editor_data.cpp @@ -17,6 +17,7 @@ #include "editor_scene.hpp" #include "constants.hpp" +#include "image_manager.hpp" #include "recent_files_manager.hpp" #include "selection_group.hpp" @@ -32,6 +33,8 @@ #include "scene_items/edge.hpp" #include "scene_items/node.hpp" +#include "special_content/special_content_model.hpp" + #include "simple_logger.hpp" using juzzlin::L; @@ -59,6 +62,13 @@ void EditorData::addNodeToSelectionGroup(NodeR node, bool isImplicit) m_selectionGroup->addNode(node, isImplicit); } +void EditorData::addSpecialContentForSelectedNodes(const SpecialContent::SpecialContentModel & specialContentModel) +{ + for (auto && node : m_selectionGroup->nodes()) { + node->addSpecialContent(specialContentModel); + } +} + void EditorData::requestAutosave(AutosaveContext context, bool async) { const auto doRequestAutosave = [this](bool async) { diff --git a/src/editor_data.hpp b/src/editor_data.hpp index dcb60c45..6ea736fd 100644 --- a/src/editor_data.hpp +++ b/src/editor_data.hpp @@ -47,6 +47,10 @@ class Node; class NodeBase; } // namespace SceneItems +namespace SpecialContent { +class SpecialContentModel; +} + namespace IO { class AlzFileIO; } // namespace IO @@ -64,6 +68,8 @@ class EditorData : public QObject void addNodeToSelectionGroup(NodeR node, bool isImplicit = false); + void addSpecialContentForSelectedNodes(const SpecialContent::SpecialContentModel & specialContentModel); + //! \return true if at least one selected node pair can be connected. bool areSelectedNodesConnectable() const; diff --git a/src/image.cpp b/src/image.cpp index d195b7cc..a5ec6be2 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -15,6 +15,8 @@ #include "image.hpp" +#include + Image::Image() { } @@ -22,7 +24,20 @@ Image::Image() Image::Image(QImage image, std::string path) : m_image(image) , m_path(path) + , m_hash(QCryptographicHash::hash(QByteArray::fromRawData(reinterpret_cast(image.bits()), +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + image.sizeInBytes()), +#else + image.byteCount()), +#endif + QCryptographicHash::Md5)) +{ +} + +bool Image::isSimilar(const Image & other) const { + return !m_image.isNull() && !other.image().isNull() && // + m_image.size() == other.image().size() && !m_hash.isEmpty() && !other.m_hash.isEmpty() && m_hash == other.m_hash; } QImage Image::image() const @@ -44,3 +59,8 @@ void Image::setId(size_t id) { m_id = id; } + +QByteArray Image::hash() const +{ + return m_hash; +} diff --git a/src/image.hpp b/src/image.hpp index 706504f8..361c739a 100644 --- a/src/image.hpp +++ b/src/image.hpp @@ -35,12 +35,18 @@ class Image void setId(size_t id); + bool isSimilar(const Image & other) const; + + QByteArray hash() const; + private: QImage m_image; std::string m_path; size_t m_id = 0; + + QByteArray m_hash; }; #endif // IMAGE_HPP diff --git a/src/image_manager.cpp b/src/image_manager.cpp index e8e539f3..dd86f7ae 100644 --- a/src/image_manager.cpp +++ b/src/image_manager.cpp @@ -41,6 +41,14 @@ ImageManager & ImageManager::operator=(const ImageManager & other) size_t ImageManager::addImage(const Image & image) { + if (const auto iter = std::find_if(m_images.begin(), m_images.end(), [image](const auto & imagePair) { + return imagePair.second.isSimilar(image); + }); + iter != m_images.end()) { + juzzlin::L().debug() << "Similar image already added, id=" << iter->second.id(); + return iter->second.id(); + } + const auto id = ++m_count; m_images[id] = image; m_images[id].setId(id); diff --git a/src/mediator.cpp b/src/mediator.cpp index a8c0c3d6..e07256a2 100644 --- a/src/mediator.cpp +++ b/src/mediator.cpp @@ -527,6 +527,17 @@ void Mediator::performNodeAction(const NodeAction & action) switch (action.type) { case NodeAction::Type::None: break; + case NodeAction::Type::AddSpecialContent: { + if (m_editorData->selectionGroupSize()) { + saveUndoPoint(); + auto specialContentModelCopy = action.specialContentModel; + if (specialContentModelCopy.image.has_value()) { + const auto imageId = m_editorData->mindMapData()->imageManager().addImage(specialContentModelCopy.image.value()); + specialContentModelCopy.image->setId(imageId); + } + m_editorData->addSpecialContentForSelectedNodes(specialContentModelCopy); + } + } break; case NodeAction::Type::AttachImage: { const Image image { action.image, action.fileName.toStdString() }; const auto id = m_editorData->mindMapData()->imageManager().addImage(image); @@ -592,6 +603,8 @@ void Mediator::performNodeAction(const NodeAction & action) bool Mediator::openMindMap(QString fileName) { + juzzlin::L().info() << "Loading '" << fileName.toStdString() << "'"; + try { juzzlin::L().info() << "Loading '" << fileName.toStdString() << "'"; Core::SingleInstanceContainer::instance().progressManager().setEnabled(true); diff --git a/src/menus/main_context_menu.cpp b/src/menus/main_context_menu.cpp index f82b90f3..566eb37e 100644 --- a/src/menus/main_context_menu.cpp +++ b/src/menus/main_context_menu.cpp @@ -123,6 +123,13 @@ MainContextMenu::MainContextMenu(QWidget * parent, Mediator & mediator, Grid & g m_mainContextMenuActions[Mode::Node].push_back(attachImageAction); + const auto addSpecialContentAction(new QAction(tr("Add special content..."), this)); + connect(addSpecialContentAction, &QAction::triggered, this, [this] { + emit actionTriggered(StateMachine::Action::SpecialContentAdditionRequested); + }); + + m_mainContextMenuActions[Mode::Node].push_back(addSpecialContentAction); + m_removeImageAction = new QAction(tr("Remove attached image"), this); connect(m_removeImageAction, &QAction::triggered, this, [this] { m_mediator.performNodeAction({ NodeAction::Type::RemoveAttachedImage }); @@ -148,11 +155,13 @@ MainContextMenu::MainContextMenu(QWidget * parent, Mediator & mediator, Grid & g colorMenu->addAction(setNodeColorAction); addSeparator(); colorMenu->addAction(setNodeTextColorAction); - + addSeparator(); addAction(deleteNodeAction); addSeparator(); addAction(attachImageAction); addAction(m_removeImageAction); + addSeparator(); + addAction(addSpecialContentAction); } void MainContextMenu::setMode(const Mode & mode) diff --git a/src/node_action.hpp b/src/node_action.hpp index fdc006df..ad0a9b3f 100644 --- a/src/node_action.hpp +++ b/src/node_action.hpp @@ -19,11 +19,16 @@ #include #include +#include + +#include "special_content/special_content_model.hpp" + struct NodeAction { enum class Type { None, + AddSpecialContent, AttachImage, ConnectSelected, Copy, @@ -55,6 +60,12 @@ struct NodeAction { } + NodeAction(Type type, SpecialContent::SpecialContentModel specialContentModel) + : type(type) + , specialContentModel(specialContentModel) + { + } + Type type = Type::None; QColor color = Qt::white; @@ -62,6 +73,8 @@ struct NodeAction QImage image; QString fileName; + + SpecialContent::SpecialContentModel specialContentModel; }; #endif // NODE_ACTION_HPP diff --git a/src/scene_items/node.cpp b/src/scene_items/node.cpp index 530e06f0..c149330b 100644 --- a/src/scene_items/node.cpp +++ b/src/scene_items/node.cpp @@ -30,6 +30,9 @@ #include "../core/single_instance_container.hpp" #include "../core/test_mode.hpp" +#include "../special_content/special_content_model.hpp" +#include "../special_content/special_content_tray.hpp" + #include "simple_logger.hpp" #include @@ -55,6 +58,7 @@ Node::Node() : m_settingsProxy(Core::SingleInstanceContainer::instance().settingsProxy()) , m_nodeModel(std::make_unique(m_settingsProxy.nodeColor(), m_settingsProxy.nodeTextColor())) , m_textEdit(new TextEdit(this)) + , m_specialContentTray(new SpecialContent::SpecialContentTray(*this)) { setAcceptHoverEvents(true); @@ -131,6 +135,24 @@ void Node::removeGraphicsEdge(EdgeR edge) } } +void Node::addSpecialContent(const SpecialContent::SpecialContentModel & specialContentModel) +{ + if (specialContentModel.hasData()) { + m_nodeModel->specialContentModels.push_back(specialContentModel); + setShowSpecialContentTray(true); + // Handle image + if (specialContentModel.image.has_value()) { + juzzlin::L().info() << "Adding special content image id=" << specialContentModel.image.value().id() << " to node " << index(); + } + // Handle web link + if (specialContentModel.url.has_value()) { + juzzlin::L().info() << "Adding special content url='" << specialContentModel.url.value().toStdString() << "' to node " << index(); + } + } else { + juzzlin::L().warning() << "Special content has no data for node " << index(); + } +} + void Node::removeFromScene() { hide(); @@ -155,12 +177,20 @@ void Node::adjustSize() prepareGeometryChange(); const auto margin = Constants::Node::MARGIN * 2; - const auto newSize = QSize { - std::max(Constants::Node::MIN_WIDTH, static_cast(m_textEdit->boundingRect().width() + margin)), - std::max(Constants::Node::MIN_HEIGHT, static_cast(m_textEdit->boundingRect().height() + margin)) - }; - m_nodeModel->size = newSize; + if (m_showSpecialContentTray) { + const auto newSize = QSize { + std::max(Constants::Node::MIN_WIDTH, static_cast(m_textEdit->boundingRect().width() + margin)), + std::max(Constants::Node::MIN_HEIGHT, static_cast(m_textEdit->boundingRect().height() + margin + Constants::Node::SPECIAL_CONTENT_TRAY_HEIGHT)) + }; + m_nodeModel->size = newSize; + } else { + const auto newSize = QSize { + std::max(Constants::Node::MIN_WIDTH, static_cast(m_textEdit->boundingRect().width() + margin)), + std::max(Constants::Node::MIN_HEIGHT, static_cast(m_textEdit->boundingRect().height() + margin)) + }; + m_nodeModel->size = newSize; + } createEdgePoints(); @@ -175,8 +205,7 @@ void Node::adjustSize() QRectF Node::boundingRect() const { - const auto size = m_nodeModel->size; - return { -size.width() / 2, -size.height() / 2, size.width(), size.height() }; + return { -size().width() / 2, -size().height() / 2, size().width(), size().height() }; } void Node::changeFont(const QFont & font) @@ -456,6 +485,13 @@ void Node::setShadowEffect(const ShadowEffectParams & params) update(); } +void Node::setShowSpecialContentTray(bool showSpecialContentTray) +{ + m_showSpecialContentTray = showSpecialContentTray; + + adjustSize(); +} + void Node::setTextInputActive(bool active) { m_textEdit->setActive(active); diff --git a/src/scene_items/node.hpp b/src/scene_items/node.hpp index f87acca2..58cc37f4 100644 --- a/src/scene_items/node.hpp +++ b/src/scene_items/node.hpp @@ -41,6 +41,11 @@ namespace Core { class SettingsProxy; } +namespace SpecialContent { +class SpecialContentModel; +class SpecialContentTray; +} + namespace SceneItems { struct NodeModel; @@ -61,6 +66,8 @@ class Node : public SceneItemBase void addGraphicsEdge(EdgeR edge); + void addSpecialContent(const SpecialContent::SpecialContentModel & specialContentModel); + void adjustSize(); void applyImage(const Image & image); @@ -113,6 +120,8 @@ class Node : public SceneItemBase void setShadowEffect(const ShadowEffectParams & params); + void setShowSpecialContentTray(bool showSpecialContentTray); + void setSize(const QSizeF & size); void setText(const QString & text); @@ -181,10 +190,14 @@ class Node : public SceneItemBase TextEdit * m_textEdit; + SpecialContent::SpecialContentTray * m_specialContentTray; + QPointF m_currentMousePos; QPixmap m_pixmap; + bool m_showSpecialContentTray = false; + static NodeP m_lastHoveredNode; }; diff --git a/src/scene_items/node_model.hpp b/src/scene_items/node_model.hpp index 6bc1c40f..6e2e8a76 100644 --- a/src/scene_items/node_model.hpp +++ b/src/scene_items/node_model.hpp @@ -21,6 +21,10 @@ #include #include +#include + +#include "special_content/special_content_model.hpp" + namespace SceneItems { struct NodeModel @@ -44,6 +48,8 @@ struct NodeModel QColor textColor; QString text; + + std::vector specialContentModels; }; } // namespace SceneItems diff --git a/src/scene_items/scene_item_base.hpp b/src/scene_items/scene_item_base.hpp index e87c2fab..4f51d9a6 100644 --- a/src/scene_items/scene_item_base.hpp +++ b/src/scene_items/scene_item_base.hpp @@ -31,7 +31,7 @@ class SceneItemBase : public QObject, public QGraphicsItem public: SceneItemBase(); - virtual ~SceneItemBase(); + virtual ~SceneItemBase() override; virtual void hideWithAnimation(); diff --git a/src/special_content/special_content_button.cpp b/src/special_content/special_content_button.cpp new file mode 100644 index 00000000..24d6a2e3 --- /dev/null +++ b/src/special_content/special_content_button.cpp @@ -0,0 +1,44 @@ +// This file is part of Heimer. +// Copyright (C) 2023 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#include "special_content_button.hpp" + +namespace SpecialContent { + +SpecialContentButton::SpecialContentButton(Role role) + : m_role(role) +{ +} + +QRectF SpecialContentButton::boundingRect() const +{ + return { -size().width() / 2, -size().height() / 2, size().width(), size().height() }; +} + +void SpecialContentButton::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) +{ +} + +const QSizeF & SpecialContentButton::size() const +{ + return m_size; +} + +void SpecialContentButton::setSize(const QSizeF & newSize) +{ + m_size = newSize; +} + +} // namespace SpecialContent diff --git a/src/special_content/special_content_button.hpp b/src/special_content/special_content_button.hpp new file mode 100644 index 00000000..47a9c7c5 --- /dev/null +++ b/src/special_content/special_content_button.hpp @@ -0,0 +1,52 @@ +// This file is part of Heimer. +// Copyright (C) 2023 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef SPECIAL_CONTENT_BUTTON_HPP +#define SPECIAL_CONTENT_BUTTON_HPP + +#include + +namespace SpecialContent { + +class SpecialContentButton : public QGraphicsItem +{ +public: + enum class Role + { + Image, + WebLink + }; + + SpecialContentButton(Role role); + + QRectF + boundingRect() const override; + + const QSizeF & size() const; + + void setSize(const QSizeF & newSize); + +protected: + void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = nullptr) override; + +private: + Role m_role; + + QSizeF m_size; +}; + +} // namespace SpecialContent + +#endif // SPECIAL_CONTENT_BUTTON_HPP diff --git a/src/special_content/special_content_model.hpp b/src/special_content/special_content_model.hpp new file mode 100644 index 00000000..501af226 --- /dev/null +++ b/src/special_content/special_content_model.hpp @@ -0,0 +1,41 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef SPECIAL_CONTENT_MODEL_HPP +#define SPECIAL_CONTENT_MODEL_HPP + +#include + +#include + +#include "../image.hpp" + +namespace SpecialContent { + +struct SpecialContentModel +{ + bool hasData() const + { + return url.has_value() || image.has_value(); + } + + std::optional url; + + std::optional image; +}; + +} // namespace SpecialContent + +#endif // SPECIAL_CONTENT_MODEL_HPP diff --git a/src/special_content/special_content_tray.cpp b/src/special_content/special_content_tray.cpp new file mode 100644 index 00000000..eaafdfed --- /dev/null +++ b/src/special_content/special_content_tray.cpp @@ -0,0 +1,44 @@ +// This file is part of Heimer. +// Copyright (C) 2023 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#include "special_content_tray.hpp" + +namespace SpecialContent { + +SpecialContentTray::SpecialContentTray(NodeR parentNode) + : m_parentNode(parentNode) +{ +} + +QRectF SpecialContentTray::boundingRect() const +{ + return { -size().width() / 2, -size().height() / 2, size().width(), size().height() }; +} + +void SpecialContentTray::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) +{ +} + +const QSizeF & SpecialContentTray::size() const +{ + return m_size; +} + +void SpecialContentTray::setSize(const QSizeF & newSize) +{ + m_size = newSize; +} + +} // namespace SpecialContent diff --git a/src/special_content/special_content_tray.hpp b/src/special_content/special_content_tray.hpp new file mode 100644 index 00000000..2a2f9796 --- /dev/null +++ b/src/special_content/special_content_tray.hpp @@ -0,0 +1,47 @@ +// This file is part of Heimer. +// Copyright (C) 2023 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef SPECIAL_CONTENT_TRAY_HPP +#define SPECIAL_CONTENT_TRAY_HPP + +#include + +#include "../types.hpp" + +namespace SpecialContent { + +class SpecialContentTray : public QGraphicsItem +{ +public: + SpecialContentTray(NodeR parentNode); + + QRectF boundingRect() const override; + + const QSizeF & size() const; + + void setSize(const QSizeF & newSize); + +protected: + void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget = nullptr) override; + +private: + NodeR m_parentNode; + + QSizeF m_size; +}; + +} // namespace SpecialContent + +#endif // SPECIAL_CONTENT_TRAY_HPP diff --git a/src/special_content/special_content_tray_model.hpp b/src/special_content/special_content_tray_model.hpp new file mode 100644 index 00000000..a5fdc88f --- /dev/null +++ b/src/special_content/special_content_tray_model.hpp @@ -0,0 +1,23 @@ +// This file is part of Heimer. +// Copyright (C) 2022 Jussi Lind +// +// Heimer is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// Heimer is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Heimer. If not, see . + +#ifndef SPECIAL_CONTENT_TRAY_MODEL_HPP +#define SPECIAL_CONTENT_TRAY_MODEL_HPP + +namespace SpecialContent { + +} // namespace SpecialContent + +#endif // SPECIAL_CONTENT_TRAY_MODEL_HPP diff --git a/src/state_machine.cpp b/src/state_machine.cpp index e02a8ab2..0a9e0460 100644 --- a/src/state_machine.cpp +++ b/src/state_machine.cpp @@ -106,6 +106,7 @@ void StateMachine::calculateState(StateMachine::Action action) case Action::OpeningMindMapCanceled: case Action::OpeningMindMapFailed: case Action::PngExported: + case Action::SpecialContentAdded: case Action::SvgExported: case Action::TextColorChanged: m_quitType = QuitType::None; @@ -143,6 +144,10 @@ void StateMachine::calculateState(StateMachine::Action action) m_state = State::ShowSaveAsDialog; break; + case Action::SpecialContentAdditionRequested: + m_state = State::ShowSpecialContentDialog; + break; + case Action::SvgExportSelected: m_state = State::ShowSvgExportDialog; break; diff --git a/src/state_machine.hpp b/src/state_machine.hpp index e42ef5d1..269f1369 100644 --- a/src/state_machine.hpp +++ b/src/state_machine.hpp @@ -47,6 +47,7 @@ class StateMachine : public QObject ShowOpenDialog, ShowPngExportDialog, ShowSaveAsDialog, + ShowSpecialContentDialog, ShowSvgExportDialog, ShowTextColorDialog, TryCloseWindow @@ -89,6 +90,8 @@ class StateMachine : public QObject RedoSelected, SaveAsSelected, SaveSelected, + SpecialContentAdded, + SpecialContentAdditionRequested, SvgExported, SvgExportSelected, TextColorChanged, diff --git a/src/unit_tests/alz_file_io_test/alz_file_io_test.cpp b/src/unit_tests/alz_file_io_test/alz_file_io_test.cpp index be5a0d26..75e51992 100644 --- a/src/unit_tests/alz_file_io_test/alz_file_io_test.cpp +++ b/src/unit_tests/alz_file_io_test/alz_file_io_test.cpp @@ -339,6 +339,40 @@ void AlzFileIOTest::testGraph_NodeDeletion() QCOMPARE(edges.size(), static_cast(1)); } +void AlzFileIOTest::testSimilarImages_notSimilar() +{ + Image image1; + Image image2; + + QCOMPARE(image1.isSimilar(image2), false); + + const auto outData = std::make_shared(); + const auto id1 = outData->imageManager().addImage(image1); + const auto id2 = outData->imageManager().addImage(image2); + + QCOMPARE(id1 == id2, false); +} + +void AlzFileIOTest::testSimilarImages_similar() +{ + QImage qImage1({ 4, 4 }, QImage::Format_RGB555); + qImage1.fill(0); + Image image1(qImage1, "qImage1"); + + QImage qImage2({ 4, 4 }, QImage::Format_RGB555); + qImage2.fill(0); + Image image2(qImage2, "qImage2"); + + QCOMPARE(image1.hash(), image2.hash()); + QCOMPARE(image1.isSimilar(image2), true); + + const auto outData = std::make_shared(); + const auto id1 = outData->imageManager().addImage(image1); + const auto id2 = outData->imageManager().addImage(image2); + + QCOMPARE(id1, id2); +} + void AlzFileIOTest::testGraph_SingleEdge() { const auto outData = std::make_shared(); diff --git a/src/unit_tests/alz_file_io_test/alz_file_io_test.hpp b/src/unit_tests/alz_file_io_test/alz_file_io_test.hpp index d2066414..baffad9c 100644 --- a/src/unit_tests/alz_file_io_test/alz_file_io_test.hpp +++ b/src/unit_tests/alz_file_io_test/alz_file_io_test.hpp @@ -83,6 +83,10 @@ private slots: void testNotUsedImages(); + void testSimilarImages_notSimilar(); + + void testSimilarImages_similar(); + void testUsedImages(); void testV1_ArrowSize();