diff --git a/code/nel/tools/3d/mesh_editor/icons.qrc b/code/nel/tools/3d/mesh_editor/icons.qrc new file mode 100644 index 000000000..31a9f782d --- /dev/null +++ b/code/nel/tools/3d/mesh_editor/icons.qrc @@ -0,0 +1,7 @@ + + + icons/cross-circle.png + icons/exclamation.png + icons/information-white.png + + diff --git a/code/nel/tools/3d/mesh_editor/icons/cross-circle.png b/code/nel/tools/3d/mesh_editor/icons/cross-circle.png new file mode 100644 index 000000000..d3b37afb8 Binary files /dev/null and b/code/nel/tools/3d/mesh_editor/icons/cross-circle.png differ diff --git a/code/nel/tools/3d/mesh_editor/icons/exclamation.png b/code/nel/tools/3d/mesh_editor/icons/exclamation.png new file mode 100644 index 000000000..21fd8f78a Binary files /dev/null and b/code/nel/tools/3d/mesh_editor/icons/exclamation.png differ diff --git a/code/nel/tools/3d/mesh_editor/icons/information-white.png b/code/nel/tools/3d/mesh_editor/icons/information-white.png new file mode 100644 index 000000000..70052384a Binary files /dev/null and b/code/nel/tools/3d/mesh_editor/icons/information-white.png differ diff --git a/code/nel/tools/3d/mesh_editor/main_window.cpp b/code/nel/tools/3d/mesh_editor/main_window.cpp index f6d6260d4..f8d3bb701 100644 --- a/code/nel/tools/3d/mesh_editor/main_window.cpp +++ b/code/nel/tools/3d/mesh_editor/main_window.cpp @@ -40,6 +40,7 @@ // Project includes #include "../shared_widgets/command_log.h" +#include "../shared_widgets/error_list.h" #include "graphics_viewport.h" #include "graphics_config.h" @@ -54,6 +55,7 @@ CMainWindow::CMainWindow(QWidget *parent, Qt::WindowFlags flags) m_IsSoundInitialized(false), m_IsSoundEnabled(false), m_Timer(NULL), m_GraphicsViewport(NULL), m_CommandLog(NULL), m_CommandLogDock(NULL), + m_ErrorList(NULL), m_ErrorListDock(NULL), m_GraphicsConfig(NULL), m_GraphicsConfigScroll(NULL), m_GraphicsConfigDock(NULL), m_FileMenu(NULL), m_EditMenu(NULL), m_ViewportMenu(NULL), m_WidgetsMenu(NULL), m_HelpMenu(NULL), m_FileToolBar(NULL), m_EditToolBar(NULL), @@ -341,6 +343,17 @@ void CMainWindow::createDockWindows() m_WidgetsMenu->addAction(m_CommandLogDock->toggleViewAction()); } + // ErrorList (Error List) + { + m_ErrorListDock = new QDockWidget(this); + m_ErrorListDock->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); + m_ErrorList = new CErrorList(m_ErrorListDock); + m_ErrorListDock->setWidget(m_ErrorList); + addDockWidget(Qt::BottomDockWidgetArea, m_ErrorListDock); + m_WidgetsMenu->addAction(m_ErrorListDock->toggleViewAction()); + tabifyDockWidget(m_CommandLogDock, m_ErrorListDock); + } + // GraphicsConfig (Graphics Configuration) { m_GraphicsConfigDock = new QDockWidget(this); @@ -381,6 +394,7 @@ void CMainWindow::createDockWindows() void CMainWindow::translateDockWindows() { m_CommandLogDock->setWindowTitle(tr("Console")); + m_ErrorListDock->setWindowTitle(tr("Error List")); m_GraphicsConfigDock->setWindowTitle(tr("Graphics Configuration")); m_AssetTreeDock->setWindowTitle(tr("Asset Database")); } diff --git a/code/nel/tools/3d/mesh_editor/main_window.h b/code/nel/tools/3d/mesh_editor/main_window.h index 74ecae5a1..36ba41eb1 100644 --- a/code/nel/tools/3d/mesh_editor/main_window.h +++ b/code/nel/tools/3d/mesh_editor/main_window.h @@ -63,6 +63,7 @@ namespace NLSOUND { namespace NLQT { class CCommandLogDisplayer; + class CErrorList; } class CGraphicsViewport; @@ -136,6 +137,9 @@ private: NLQT::CCommandLogDisplayer *m_CommandLog; QDockWidget *m_CommandLogDock; + NLQT::CErrorList *m_ErrorList; + QDockWidget *m_ErrorListDock; + CGraphicsConfig *m_GraphicsConfig; QScrollArea *m_GraphicsConfigScroll; QDockWidget *m_GraphicsConfigDock; diff --git a/code/nel/tools/3d/shared_widgets/error_list.cpp b/code/nel/tools/3d/shared_widgets/error_list.cpp new file mode 100644 index 000000000..caf270992 --- /dev/null +++ b/code/nel/tools/3d/shared_widgets/error_list.cpp @@ -0,0 +1,477 @@ +/* + +Copyright (C) 2015 by authors +Author: Jan Boon +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +// Source: https://github.com/kaetemi/errorlist + +#include "error_list.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NLQT { + +#define DATAROLE_TYPE (Qt::UserRole + 0) +#define DATAROLE_TIMESTAMP (Qt::UserRole + 1) +#define DATAROLE_GROUP (Qt::UserRole + 2) +#define DATAROLE_MESSAGE (Qt::UserRole + 3) +#define DATAROLE_LINE (Qt::UserRole + 4) +#define DATAROLE_USERDATA (Qt::UserRole + 5) +#define DATAROLE_COLLAPSED (Qt::UserRole + 6) + +class CErrorListItem : public QListWidgetItem +{ +public: + CErrorListItem(const QIcon &icon, const QString &message) : QListWidgetItem(icon, message) + { + + } + + virtual bool operator<(const QListWidgetItem &other) const Q_DECL_OVERRIDE + { + time_t this_timestamp = data(DATAROLE_TIMESTAMP).toInt(); + time_t other_timestamp = other.data(DATAROLE_TIMESTAMP).toInt(); + return this_timestamp < other_timestamp; + } +}; + +const char *iconNames[] = { + ":/icons/cross-circle.png", + ":/icons/exclamation.png", + ":/icons/information-white.png" +}; + +const char *filterText[] = { + "Errors", + "Warnings", + "Messages" +}; + +CErrorList::CErrorList(QWidget *parent) : QWidget(parent), m_Collapse(true), m_CollapseBtn(NULL), m_Message(NULL) +{ + m_Filter[0] = true; + m_Filter[1] = true; + m_Filter[2] = true; + m_FilterBtn[0] = NULL; + m_FilterBtn[1] = NULL; + m_FilterBtn[2] = NULL; + m_FilterCounts[0] = 0; + m_FilterCounts[1] = 0; + m_FilterCounts[2] = 0; + + QVBoxLayout *layout = new QVBoxLayout(this); + + QHBoxLayout *buttons = new QHBoxLayout(this); + + QPushButton *clearBtn = new QPushButton(this); + clearBtn->setText(tr("Clear")); + connect(clearBtn, SIGNAL(clicked()), this, SLOT(clear())); + buttons->addWidget(clearBtn); + + QPushButton *collapseBtn = new QPushButton(this); + m_CollapseBtn = collapseBtn; + collapseBtn->setText(tr("Collapse")); + collapseBtn->setCheckable(true); + collapseBtn->setChecked(m_Collapse); + connect(collapseBtn, SIGNAL(toggled(bool)), this, SLOT(collapse(bool))); + buttons->addWidget(collapseBtn); + + buttons->addStretch(); + + QPushButton *errorBtn = new QPushButton(this); + m_FilterBtn[0] = errorBtn; + errorBtn->setIcon(QIcon(iconNames[0])); + updateFilterCount(0); + errorBtn->setCheckable(true); + errorBtn->setChecked(m_Filter[0]); + connect(errorBtn, SIGNAL(toggled(bool)), this, SLOT(filterError(bool))); + buttons->addWidget(errorBtn); + + QPushButton *warningBtn = new QPushButton(this); + m_FilterBtn[1] = warningBtn; + warningBtn->setIcon(QIcon(iconNames[1])); + updateFilterCount(1); + warningBtn->setCheckable(true); + warningBtn->setChecked(m_Filter[1]); + connect(warningBtn, SIGNAL(toggled(bool)), this, SLOT(filterWarning(bool))); + buttons->addWidget(warningBtn); + + QPushButton *messageBtn = new QPushButton(this); + m_FilterBtn[2] = messageBtn; + messageBtn->setIcon(QIcon(iconNames[2])); + updateFilterCount(2); + messageBtn->setCheckable(true); + messageBtn->setChecked(m_Filter[2]); + connect(messageBtn, SIGNAL(toggled(bool)), this, SLOT(filterMessage(bool))); + buttons->addWidget(messageBtn); + + layout->addLayout(buttons); + + QSplitter *splitter = new QSplitter(Qt::Vertical, this); + + m_List = new QListWidget(this); + m_List->setTextElideMode(Qt::ElideRight); + m_List->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + // m_List->setSortingEnabled(true); + connect(m_List, &QListWidget::itemClicked, this, &CErrorList::listItemClicked); + connect(m_List, &QListWidget::itemDoubleClicked, this, &CErrorList::listItemDoubleClicked); + splitter->addWidget(m_List); + + QTextEdit *messageText = new QTextEdit(this); + m_Message = messageText; + messageText->setReadOnly(true); + messageText->setVisible(false); + messageText->setAcceptRichText(true); + splitter->addWidget(messageText); + + layout->addWidget(splitter); + + setLayout(layout); +} + +CErrorList::~CErrorList() +{ + +} + +void CErrorList::clear() +{ + m_CurrentMessage = NULL; + m_Message->setVisible(false); + m_CollapseItems.clear(); + m_List->clear(); + m_FilterCounts[0] = 0; + m_FilterCounts[1] = 0; + m_FilterCounts[2] = 0; + updateFilterCount(0); + updateFilterCount(1); + updateFilterCount(2); +} + +void CErrorList::clear(const QString &group) +{ + for (int i = 0; i < m_List->count(); ++i) + { + QListWidgetItem *item = m_List->item(i); + if (item->data(DATAROLE_GROUP).toString() == group) + { + if (m_CurrentMessage == item) + { + m_CurrentMessage = NULL; + m_Message->setVisible(false); + } + if (m_Collapse) + { + QString cs = getCollapseString(item); + std::map::iterator it + = m_CollapseItems.find(cs); + if (it != m_CollapseItems.end()) + m_CollapseItems.erase(it); + } + --m_FilterCounts[(int)item->data(DATAROLE_TYPE).toInt()]; + delete item; + --i; + } + } + updateFilterCount(0); + updateFilterCount(1); + updateFilterCount(2); +} + +void CErrorList::collapse(bool c) +{ + if (c != m_Collapse) + { + m_Collapse = c; + m_CollapseBtn->setChecked(c); + + if (c) + { + for (int i = 0; i < m_List->count(); ++i) + { + QListWidgetItem *item = m_List->item(i); + QString cs = getCollapseString(item); + QListWidgetItem *ref = m_CollapseItems[cs]; + + if (ref != NULL) + { + ref->setData(DATAROLE_COLLAPSED, ref->data(DATAROLE_COLLAPSED).toInt() + 1); + updateCollapseText(ref); + item->setData(DATAROLE_COLLAPSED, 0); + item->setHidden(true); + if (m_CurrentMessage == item) + { + m_CurrentMessage = NULL; + m_Message->setHidden(true); + } + } + else + { + m_CollapseItems[cs] = item; + } + } + } + else + { + m_CollapseItems.clear(); + for (int i = 0; i < m_List->count(); ++i) + { + int cc = m_List->item(i)->data(DATAROLE_COLLAPSED).toInt(); + if (cc > 1) + { + m_List->item(i)->setText(m_List->item(i)->data(DATAROLE_LINE).toString()); + } + if (cc == 0) + { + bool hide = !m_Filter[m_List->item(i)->data(DATAROLE_TYPE).toInt()]; + m_List->item(i)->setHidden(hide); + } + if (cc != 1) + { + m_List->item(i)->setData(DATAROLE_COLLAPSED, 1); + } + } + } + } +} + +void CErrorList::filter(ErrorType type, bool f) +{ + if (f != m_Filter[(int)type]) + { + m_Filter[(int)type] = f; + m_FilterBtn[(int)type]->setChecked(f); + + for (int i = 0; i < m_List->count(); ++i) + { + if (m_List->item(i)->data(DATAROLE_TYPE).toInt() == (int)type) + { + bool hide = + m_List->item(i)->data(DATAROLE_COLLAPSED).toInt() == 0 + || !f; + m_List->item(i)->setHidden(hide); + if (hide && m_CurrentMessage == m_List->item(i)) + { + m_CurrentMessage = NULL; + m_Message->setHidden(true); + } + } + } + } +} + +void CErrorList::updateFilterCount(int filter) +{ + m_FilterBtn[filter]->setText(QString(" ") + QString::number(m_FilterCounts[filter]) + " " + filterText[filter]); +} + +void CErrorList::filterError(bool f) +{ + filter(Error, f); +} + +void CErrorList::filterWarning(bool f) +{ + filter(Warning, f); +} + +void CErrorList::filterMessage(bool f) +{ + filter(Message, f); +} + +void CErrorList::markClear(const QString &group) +{ + for (int i = 0; i < m_List->count(); ++i) + { + QListWidgetItem *item = m_List->item(i); + if (item->data(DATAROLE_GROUP).toString() == group) + { + QString cs = getCollapseString(item); + m_MarkedClear.insert(cs); + } + } +} + +void CErrorList::clearMarked() +{ + for (int i = 0; i < m_List->count(); ++i) + { + QListWidgetItem *item = m_List->item(i); + QString cs = getCollapseString(item); + if (m_MarkedClear.find(cs) != m_MarkedClear.end()) + { + if (m_CurrentMessage == item) + { + m_CurrentMessage = NULL; + m_Message->setVisible(false); + } + if (m_Collapse) + { + QString cs = getCollapseString(item); + std::map::iterator it + = m_CollapseItems.find(cs); + if (it != m_CollapseItems.end()) + m_CollapseItems.erase(it); + } + --m_FilterCounts[(int)item->data(DATAROLE_TYPE).toInt()]; + delete item; + --i; + } + } +} + +void CErrorList::listItemClicked(QListWidgetItem *item) +{ + if (item != m_CurrentMessage) + { + if (item->isSelected()) + { + QString text = item->data(DATAROLE_MESSAGE).toString(); + m_Message->setHtml(text); + m_Message->setVisible(true); + } + else + { + m_CurrentMessage = NULL; + m_Message->setVisible(false); + } + } +} + +void CErrorList::listItemDoubleClicked(QListWidgetItem *item) +{ + emit request(item->data(DATAROLE_GROUP).toString(), item->data(DATAROLE_USERDATA).toMap()); +} + +QString CErrorList::getCollapseString(QListWidgetItem *item) +{ + return item->data(DATAROLE_GROUP).toString() + " / " + item->data(DATAROLE_LINE).toString(); +} + +void CErrorList::updateCollapseText(QListWidgetItem *item) +{ + item->setText(QString("(" + QString::number(item->data(DATAROLE_COLLAPSED).toInt()) + "x) " + item->data(DATAROLE_LINE).toString()));// .replace('\n', ' ').replace('\r', ' '))); +} + +void CErrorList::add(ErrorType type, const QString &group, time_t timestamp, const QString &message, const QMap &userData) +{ + QTextDocument doc; + doc.setHtml(message); + QString line = doc.toPlainText().replace('\n', ' ').replace('\r', ' '); + + CErrorListItem *item = new CErrorListItem(QIcon(iconNames[(int)type]), line); + item->setData(DATAROLE_TYPE, (int)type); + item->setData(DATAROLE_TIMESTAMP, timestamp); + item->setData(DATAROLE_GROUP, group); + item->setData(DATAROLE_MESSAGE, message); + item->setData(DATAROLE_LINE, line); + item->setData(DATAROLE_USERDATA, userData); + + QString cs = getCollapseString(item); + if (m_MarkedClear.find(cs) != m_MarkedClear.end()) + { + m_MarkedClear.erase(cs); + return; + } + + bool hide = m_Collapse; + if (hide) + { + QListWidgetItem *ref = m_CollapseItems[cs]; + hide = ref != NULL; + if (hide) + { + ref->setData(DATAROLE_COLLAPSED, ref->data(DATAROLE_COLLAPSED).toInt() + 1); + updateCollapseText(ref); + item->setData(DATAROLE_COLLAPSED, 0); + } + else + { + m_CollapseItems[cs] = item; + } + } + if (!hide) + { + item->setData(DATAROLE_COLLAPSED, 1); + hide = !m_Filter[(int)type]; + } + + m_List->addItem(item); + item->setHidden(hide); + ++m_FilterCounts[(int)type]; + updateFilterCount((int)type); +} + +void CErrorList::update(const QString &group, const QString &message) +{ + // NOTE: This does not play well with collapse, so you should only have one message present in the category + // Automatically adds a message if no message exists in the category + for (int i = 0; i < m_List->count(); ++i) + { + QListWidgetItem *item = m_List->item(i); + if (item->data(DATAROLE_GROUP).toString() == group) + { + QTextDocument doc; + doc.setHtml(message); + QString line = doc.toPlainText().replace('\n', ' ').replace('\r', ' '); + item->setData(DATAROLE_MESSAGE, message); + item->setData(DATAROLE_LINE, line); + item->setText(line); + return; + } + } + add(Message, group, message); +} + +void CErrorList::add(ErrorType type, const QString &group, time_t timestamp, const QString &message) +{ + QMap nullMap; + add(type, group, timestamp, message, nullMap); +} + +void CErrorList::add(ErrorType type, const QString &group, const QString &message, const QMap &userData) +{ + add(type, group, QDateTime::currentDateTime().toTime_t(), message, userData); +} + +void CErrorList::add(ErrorType type, const QString &group, const QString &message) +{ + QMap nullMap; + add(type, group, message, nullMap); +} + +} + +/* end of file */ \ No newline at end of file diff --git a/code/nel/tools/3d/shared_widgets/error_list.h b/code/nel/tools/3d/shared_widgets/error_list.h new file mode 100644 index 000000000..f90f52db5 --- /dev/null +++ b/code/nel/tools/3d/shared_widgets/error_list.h @@ -0,0 +1,118 @@ +/* + +Copyright (C) 2015 by authors +Author: Jan Boon +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +// Source: https://github.com/kaetemi/errorlist + +#ifndef ERRORLIST_H +#define ERRORLIST_H + +#define ERRORLIST_EXPORT + +#include +#include + +#include +#include +#include +#include + +class QListWidgetItem; +class QListWidget; +class QPushButton; +class QTextEdit; + +namespace NLQT { + +class ERRORLIST_EXPORT CErrorList : public QWidget +{ + Q_OBJECT + +public: + enum ErrorType + { + Error, + Warning, + Message + }; + +public: + CErrorList(QWidget *parent); + virtual ~CErrorList(); + + void add(ErrorType type, const QString &group, time_t timestamp, const QString &message, const QMap &userData); + void add(ErrorType type, const QString &group, time_t timestamp, const QString &message); + void add(ErrorType type, const QString &group, const QString &message, const QMap &userData); + void add(ErrorType type, const QString &group, const QString &message); + + void update(const QString &group, const QString &message); + + void markClear(const QString &group); + void clearMarked(); + + void clear(const QString &group); + void filter(ErrorType type, bool f); + +signals: + void request(const QString &group, const QMap &userData); + +public slots: + void clear(); + void collapse(bool c); + void filterError(bool f); + void filterWarning(bool f); + void filterMessage(bool f); + +private slots: + void listItemClicked(QListWidgetItem *item); + void listItemDoubleClicked(QListWidgetItem *item); + +private: + void updateFilterCount(int filter); + static QString getCollapseString(QListWidgetItem *item); + static void updateCollapseText(QListWidgetItem *item); + +private: + QListWidget *m_List; + std::map m_CollapseItems; + bool m_Collapse; + QPushButton *m_CollapseBtn; + bool m_Filter[3]; + QPushButton *m_FilterBtn[3]; + int m_FilterCounts[3]; + QTextEdit *m_Message; + QListWidgetItem *m_CurrentMessage; + std::set m_MarkedClear; + +}; /* class CErrorList */ + +} + +#endif /* ERRORLIST_H */ + +/* end of file */