Revised Tagging interface (#23)
parent
cc17b794ee
commit
7b5d4baffd
11
ashirt.pro
11
ashirt.pro
|
@ -26,9 +26,12 @@ SOURCES += \
|
|||
src/components/error_view/errorview.cpp \
|
||||
src/components/evidence_editor/evidenceeditor.cpp \
|
||||
src/components/evidencepreview.cpp \
|
||||
src/components/flow_layout/flowlayout.cpp \
|
||||
src/components/loading/qprogressindicator.cpp \
|
||||
src/components/loading_button/loadingbutton.cpp \
|
||||
src/components/tag_editor/tageditor.cpp \
|
||||
src/components/tagging/tageditor.cpp \
|
||||
src/components/tagging/tagview.cpp \
|
||||
src/components/tagging/tagwidget.cpp \
|
||||
src/db/databaseconnection.cpp \
|
||||
src/forms/evidence_filter/evidencefilter.cpp \
|
||||
src/forms/evidence_filter/evidencefilterform.cpp \
|
||||
|
@ -56,9 +59,13 @@ HEADERS += \
|
|||
src/components/evidence_editor/evidenceeditor.h \
|
||||
src/components/evidence_editor/saveevidenceresponse.h \
|
||||
src/components/evidencepreview.h \
|
||||
src/components/flow_layout/flowlayout.h \
|
||||
src/components/loading/qprogressindicator.h \
|
||||
src/components/loading_button/loadingbutton.h \
|
||||
src/components/tag_editor/tageditor.h \
|
||||
src/components/tagging/tageditor.h \
|
||||
src/components/tagging/tagginglineediteventfilter.h \
|
||||
src/components/tagging/tagview.h \
|
||||
src/components/tagging/tagwidget.h \
|
||||
src/db/databaseconnection.h \
|
||||
src/exceptions/databaseerr.h \
|
||||
src/exceptions/fileerror.h \
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#include <QSettings>
|
||||
#include <QString>
|
||||
|
||||
#include "models/tag.h"
|
||||
|
||||
// AppSettings is a singleton construct for accessing the application's settings. This is different
|
||||
// from configuration, as it represents the application's state, rather than how the application
|
||||
// communicates.
|
||||
|
@ -29,6 +31,7 @@ class AppSettings : public QObject {
|
|||
|
||||
const char *opSlugSetting = "operation/slug";
|
||||
const char *opNameSetting = "operation/name";
|
||||
const char *lastUsedTagsSetting = "gather/tags";
|
||||
|
||||
AppSettings() : QObject(nullptr) {}
|
||||
|
||||
|
@ -51,5 +54,13 @@ class AppSettings : public QObject {
|
|||
}
|
||||
QString operationSlug() { return settings.value(opSlugSetting).toString(); }
|
||||
QString operationName() { return settings.value(opNameSetting).toString(); }
|
||||
|
||||
void setLastUsedTags(std::vector<model::Tag> lastTags) {
|
||||
settings.setValue(lastUsedTagsSetting, QVariant::fromValue(lastTags));
|
||||
}
|
||||
std::vector<model::Tag> getLastUsedTags() {
|
||||
auto val = settings.value(lastUsedTagsSetting);
|
||||
return qvariant_cast<std::vector<model::Tag>>(val);
|
||||
}
|
||||
};
|
||||
#endif // APPSETTINGS_H
|
||||
|
|
|
@ -91,7 +91,7 @@ model::Evidence EvidenceEditor::encodeEvidence() {
|
|||
void EvidenceEditor::setEnabled(bool enable) {
|
||||
// if the product is enabled, then we can edit, hence it's not readonly
|
||||
descriptionTextBox->setReadOnly(!enable);
|
||||
tagEditor->setEnabled(enable);
|
||||
tagEditor->setReadonly(!enable);
|
||||
if (loadedPreview != nullptr) {
|
||||
loadedPreview->setReadonly(!enable);
|
||||
}
|
||||
|
@ -124,13 +124,7 @@ void EvidenceEditor::loadData() {
|
|||
loadedPreview->setReadonly(readonly);
|
||||
|
||||
// get all remote tags (for op)
|
||||
std::vector<qint64> initialTagIDs;
|
||||
initialTagIDs.reserve(originalEvidenceData.tags.size());
|
||||
for (const model::Tag &tag : originalEvidenceData.tags) {
|
||||
initialTagIDs.push_back(tag.serverTagId);
|
||||
}
|
||||
|
||||
tagEditor->loadTags(operationSlug, initialTagIDs);
|
||||
tagEditor->loadTags(operationSlug, originalEvidenceData.tags);
|
||||
}
|
||||
catch (QSqlError &e) {
|
||||
loadedPreview = new ErrorView("Unable to load evidence: " + e.text(), this);
|
||||
|
@ -157,12 +151,7 @@ void EvidenceEditor::clearEditor() {
|
|||
}
|
||||
|
||||
void EvidenceEditor::onTagsLoaded(bool success) {
|
||||
if (!success) {
|
||||
tagEditor->setEnabled(false);
|
||||
}
|
||||
else {
|
||||
tagEditor->setEnabled(!readonly);
|
||||
}
|
||||
tagEditor->setReadonly(!success || readonly);
|
||||
emit onWidgetReady();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,14 +9,15 @@
|
|||
#include <QString>
|
||||
#include <QTextEdit>
|
||||
#include <QWidget>
|
||||
#include <QSplitter>
|
||||
|
||||
#include "components/evidencepreview.h"
|
||||
#include "components/tag_editor/tageditor.h"
|
||||
#include "db/databaseconnection.h"
|
||||
#include "deleteevidenceresponse.h"
|
||||
#include "saveevidenceresponse.h"
|
||||
|
||||
#include <QSplitter>
|
||||
#include "components/tagging/tageditor.h"
|
||||
|
||||
|
||||
class EvidenceEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the examples of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:BSD$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** BSD License Usage
|
||||
** Alternatively, you may use this file under the terms of the BSD license
|
||||
** as follows:
|
||||
**
|
||||
** "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.
|
||||
** * Neither the name of The Qt Company Ltd nor the names of its
|
||||
** contributors may be used to endorse or promote products derived
|
||||
** from this software without specific prior written permission.
|
||||
**
|
||||
**
|
||||
** 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
|
||||
** OWNER 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."
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "flowlayout.h"
|
||||
|
||||
#include <QtWidgets>
|
||||
|
||||
// Local Edits:
|
||||
// 1. Formatting
|
||||
|
||||
FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
|
||||
: QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing)
|
||||
{
|
||||
setContentsMargins(margin, margin, margin, margin);
|
||||
}
|
||||
|
||||
FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing)
|
||||
: m_hSpace(hSpacing), m_vSpace(vSpacing)
|
||||
{
|
||||
setContentsMargins(margin, margin, margin, margin);
|
||||
}
|
||||
|
||||
FlowLayout::~FlowLayout() {
|
||||
QLayoutItem *item;
|
||||
while ((item = takeAt(0)))
|
||||
delete item;
|
||||
}
|
||||
|
||||
void FlowLayout::addItem(QLayoutItem *item) {
|
||||
itemList.append(item);
|
||||
}
|
||||
|
||||
int FlowLayout::horizontalSpacing() const {
|
||||
return (m_hSpace >= 0) ? m_hSpace : smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
|
||||
}
|
||||
|
||||
int FlowLayout::verticalSpacing() const {
|
||||
return (m_vSpace >= 0) ? m_vSpace : smartSpacing(QStyle::PM_LayoutVerticalSpacing);
|
||||
}
|
||||
|
||||
int FlowLayout::count() const {
|
||||
return itemList.size();
|
||||
}
|
||||
|
||||
QLayoutItem *FlowLayout::itemAt(int index) const {
|
||||
return itemList.value(index);
|
||||
}
|
||||
|
||||
QLayoutItem *FlowLayout::takeAt(int index) {
|
||||
if (index >= 0 && index < itemList.size()) {
|
||||
return itemList.takeAt(index);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Qt::Orientations FlowLayout::expandingDirections() const { return 0; }
|
||||
|
||||
bool FlowLayout::hasHeightForWidth() const { return true; }
|
||||
|
||||
int FlowLayout::heightForWidth(int width) const {
|
||||
return doLayout(QRect(0, 0, width, 0), true);
|
||||
}
|
||||
|
||||
void FlowLayout::setGeometry(const QRect &rect) {
|
||||
QLayout::setGeometry(rect);
|
||||
doLayout(rect, false);
|
||||
}
|
||||
|
||||
QSize FlowLayout::sizeHint() const {
|
||||
return minimumSize();
|
||||
}
|
||||
|
||||
QSize FlowLayout::minimumSize() const {
|
||||
QSize size;
|
||||
for (const QLayoutItem *item : qAsConst(itemList))
|
||||
size = size.expandedTo(item->minimumSize());
|
||||
|
||||
const QMargins margins = contentsMargins();
|
||||
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
|
||||
return size;
|
||||
}
|
||||
|
||||
int FlowLayout::doLayout(const QRect &rect, bool testOnly) const {
|
||||
int left, top, right, bottom;
|
||||
getContentsMargins(&left, &top, &right, &bottom);
|
||||
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
|
||||
int x = effectiveRect.x();
|
||||
int y = effectiveRect.y();
|
||||
int lineHeight = 0;
|
||||
|
||||
for (QLayoutItem *item : qAsConst(itemList)) {
|
||||
const QWidget *wid = item->widget();
|
||||
int spaceX = horizontalSpacing();
|
||||
if (spaceX == -1) {
|
||||
spaceX = wid->style()->layoutSpacing(
|
||||
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
|
||||
}
|
||||
int spaceY = verticalSpacing();
|
||||
if (spaceY == -1) {
|
||||
spaceY = wid->style()->layoutSpacing(
|
||||
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
|
||||
}
|
||||
|
||||
int nextX = x + item->sizeHint().width() + spaceX;
|
||||
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
|
||||
x = effectiveRect.x();
|
||||
y = y + lineHeight + spaceY;
|
||||
nextX = x + item->sizeHint().width() + spaceX;
|
||||
lineHeight = 0;
|
||||
}
|
||||
|
||||
if (!testOnly) {
|
||||
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
|
||||
}
|
||||
|
||||
x = nextX;
|
||||
lineHeight = qMax(lineHeight, item->sizeHint().height());
|
||||
}
|
||||
return y + lineHeight - rect.y() + bottom;
|
||||
}
|
||||
|
||||
int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const {
|
||||
QObject *parent = this->parent();
|
||||
if (!parent) {
|
||||
return -1;
|
||||
}
|
||||
else if (parent->isWidgetType()) {
|
||||
auto pw = static_cast<QWidget *>(parent);
|
||||
return pw->style()->pixelMetric(pm, nullptr, pw);
|
||||
}
|
||||
return static_cast<QLayout *>(parent)->spacing();
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the examples of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:BSD$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** BSD License Usage
|
||||
** Alternatively, you may use this file under the terms of the BSD license
|
||||
** as follows:
|
||||
**
|
||||
** "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.
|
||||
** * Neither the name of The Qt Company Ltd nor the names of its
|
||||
** contributors may be used to endorse or promote products derived
|
||||
** from this software without specific prior written permission.
|
||||
**
|
||||
**
|
||||
** 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
|
||||
** OWNER 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."
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef FLOWLAYOUT_H
|
||||
#define FLOWLAYOUT_H
|
||||
|
||||
#include <QLayout>
|
||||
#include <QRect>
|
||||
#include <QStyle>
|
||||
|
||||
class FlowLayout : public QLayout
|
||||
{
|
||||
public:
|
||||
explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
|
||||
explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
|
||||
~FlowLayout();
|
||||
|
||||
void addItem(QLayoutItem *item) override;
|
||||
int horizontalSpacing() const;
|
||||
int verticalSpacing() const;
|
||||
Qt::Orientations expandingDirections() const override;
|
||||
bool hasHeightForWidth() const override;
|
||||
int heightForWidth(int) const override;
|
||||
int count() const override;
|
||||
QLayoutItem *itemAt(int index) const override;
|
||||
QSize minimumSize() const override;
|
||||
void setGeometry(const QRect &rect) override;
|
||||
QSize sizeHint() const override;
|
||||
QLayoutItem *takeAt(int index) override;
|
||||
|
||||
private:
|
||||
int doLayout(const QRect &rect, bool testOnly) const;
|
||||
int smartSpacing(QStyle::PixelMetric pm) const;
|
||||
|
||||
QList<QLayoutItem *> itemList;
|
||||
int m_hSpace;
|
||||
int m_vSpace;
|
||||
};
|
||||
|
||||
#endif // FLOWLAYOUT_H
|
|
@ -1,240 +0,0 @@
|
|||
#include "tageditor.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "dtos/tag.h"
|
||||
#include "helpers/netman.h"
|
||||
#include "helpers/stopreply.h"
|
||||
|
||||
TagEditor::TagEditor(QWidget *parent) : QWidget(parent) {
|
||||
buildUi();
|
||||
couldNotCreateTagMsg = new QErrorMessage(this);
|
||||
|
||||
wireUi();
|
||||
}
|
||||
|
||||
TagEditor::~TagEditor() {
|
||||
delete _withTagsLabel;
|
||||
delete createTagTextBox;
|
||||
delete includedTagsListBox;
|
||||
delete allTagsListBox;
|
||||
delete includeTagButton;
|
||||
delete excludeTagButton;
|
||||
delete couldNotCreateTagMsg;
|
||||
delete createTagButton;
|
||||
delete gridLayout;
|
||||
|
||||
stopReply(&getTagsReply);
|
||||
stopReply(&createTagReply);
|
||||
}
|
||||
|
||||
void TagEditor::buildUi() {
|
||||
gridLayout = new QGridLayout(this);
|
||||
gridLayout->setMargin(0);
|
||||
|
||||
_withTagsLabel = new QLabel("With Tags...", this);
|
||||
includedTagsListBox = new QListWidget(this);
|
||||
allTagsListBox = new QListWidget(this);
|
||||
includeTagButton = new QPushButton("<<", this);
|
||||
excludeTagButton = new QPushButton(">>", this);
|
||||
createTagTextBox = new QLineEdit();
|
||||
createTagButton = new LoadingButton("Create Tag", this);
|
||||
minorErrorLabel = new QLabel(this);
|
||||
|
||||
// Layout
|
||||
/* 0 1 2 3
|
||||
+----------+-----------+----------+---------+
|
||||
0 | With Tags label |
|
||||
+----------+-----------+----------+---------+
|
||||
1 | Include | << btn | All |
|
||||
+ +-----------+ + +
|
||||
2 | Tag List | >> Btn | Tags List |
|
||||
+----------+-----------+----------+---------+
|
||||
3 | err Lab | <empty> | Add TB | add btn |
|
||||
+----------+-----------+----------+---------+
|
||||
*/
|
||||
|
||||
// row 0
|
||||
gridLayout->addWidget(_withTagsLabel, 0, 0, 1, gridLayout->columnCount());
|
||||
|
||||
// row 1
|
||||
gridLayout->addWidget(includedTagsListBox, 1, 0, 2, 1);
|
||||
gridLayout->addWidget(includeTagButton, 1, 1);
|
||||
gridLayout->addWidget(allTagsListBox, 1, 2, 2, 2);
|
||||
|
||||
// row 2
|
||||
gridLayout->addWidget(excludeTagButton, 2, 1);
|
||||
|
||||
// row 3
|
||||
gridLayout->addWidget(minorErrorLabel, 3, 0);
|
||||
gridLayout->addWidget(createTagTextBox, 3, 2);
|
||||
gridLayout->addWidget(createTagButton, 3, 3);
|
||||
|
||||
this->setLayout(gridLayout);
|
||||
}
|
||||
|
||||
void TagEditor::wireUi() {
|
||||
auto btnClicked = &QPushButton::clicked;
|
||||
connect(createTagButton, btnClicked, this, &TagEditor::createTagButtonClicked);
|
||||
connect(includeTagButton, btnClicked, this, &TagEditor::includeTagButtonClicked);
|
||||
connect(excludeTagButton, btnClicked, this, &TagEditor::excludeTagButtonClicked);
|
||||
connect(createTagTextBox, &QLineEdit::returnPressed, this, &TagEditor::createTagButtonClicked);
|
||||
|
||||
allTagsListBox->setSortingEnabled(true);
|
||||
}
|
||||
|
||||
void TagEditor::clear() {
|
||||
stopReply(&getTagsReply);
|
||||
stopReply(&createTagReply);
|
||||
createTagTextBox->setText("");
|
||||
allTagsListBox->clear();
|
||||
includedTagsListBox->clear();
|
||||
minorErrorLabel->setText("");
|
||||
}
|
||||
|
||||
void TagEditor::setEnabled(bool enable) {
|
||||
createTagButton->setEnabled(enable);
|
||||
includeTagButton->setEnabled(enable);
|
||||
excludeTagButton->setEnabled(enable);
|
||||
createTagTextBox->setEnabled(enable);
|
||||
}
|
||||
|
||||
void TagEditor::loadTags(const QString& operationSlug, std::vector<qint64> initialTagIDs) {
|
||||
this->operationSlug = operationSlug;
|
||||
|
||||
includedTagIds.clear();
|
||||
for (auto tagID : initialTagIDs) {
|
||||
includedTagIds.insert(tagID);
|
||||
}
|
||||
|
||||
getTagsReply = NetMan::getInstance().getOperationTags(operationSlug);
|
||||
connect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
|
||||
}
|
||||
|
||||
void TagEditor::refreshTagBoxes() {
|
||||
allTagsListBox->clear();
|
||||
includedTagsListBox->clear();
|
||||
|
||||
for (const auto &itr : knownTags) {
|
||||
QString tagText = itr.first;
|
||||
qint64 tagId = itr.second;
|
||||
int itemCount = includedTagIds.count(tagId);
|
||||
|
||||
if (itemCount == 1) {
|
||||
includedTagsListBox->addItem(tagText);
|
||||
}
|
||||
else {
|
||||
allTagsListBox->addItem(tagText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<model::Tag> TagEditor::getIncludedTags() {
|
||||
std::vector<model::Tag> rtn;
|
||||
rtn.reserve(includedTagIds.size());
|
||||
|
||||
// Construct a reverse map to find tag names.
|
||||
// slightly inefficient way to do this, but much easier to code against.
|
||||
std::unordered_map<qint64, QString> reversedMap;
|
||||
for (const auto &entry : knownTags) {
|
||||
reversedMap.insert(std::pair<qint64, QString>(entry.second, entry.first));
|
||||
}
|
||||
|
||||
for (const qint64 &tagID : includedTagIds) {
|
||||
try {
|
||||
auto tagName = reversedMap.at(tagID);
|
||||
model::Tag tag(tagID, tagName);
|
||||
rtn.push_back(tag);
|
||||
}
|
||||
catch (std::out_of_range &e) {
|
||||
} // drop any tag ids we can't find (doesn't exist on the server, and will fail anyway)
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
void TagEditor::createTagButtonClicked() {
|
||||
auto newText = createTagTextBox->text().trimmed();
|
||||
if (newText == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
createTagButton->startAnimation();
|
||||
createTagButton->setEnabled(false);
|
||||
|
||||
dto::Tag newTag(newText, randomColor());
|
||||
|
||||
createTagReply = NetMan::getInstance().createTag(newTag, operationSlug);
|
||||
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
||||
}
|
||||
|
||||
void TagEditor::includeTagButtonClicked() {
|
||||
auto items = allTagsListBox->selectedItems();
|
||||
for (auto item : items) {
|
||||
includedTagsListBox->addItem(item->text());
|
||||
allTagsListBox->takeItem(allTagsListBox->row(item));
|
||||
includedTagIds.insert(knownTags[item->text()]);
|
||||
}
|
||||
}
|
||||
|
||||
void TagEditor::excludeTagButtonClicked() {
|
||||
auto items = includedTagsListBox->selectedItems();
|
||||
for (auto item : items) {
|
||||
allTagsListBox->addItem(item->text());
|
||||
includedTagsListBox->takeItem(includedTagsListBox->row(item));
|
||||
includedTagIds.erase(knownTags[item->text()]);
|
||||
}
|
||||
}
|
||||
|
||||
void TagEditor::onGetTagsComplete() {
|
||||
bool isValid;
|
||||
auto data = NetMan::extractResponse(getTagsReply, isValid);
|
||||
if (isValid) {
|
||||
std::vector<dto::Tag> tags = dto::Tag::parseDataAsList(data);
|
||||
knownTags.clear();
|
||||
|
||||
for (const dto::Tag &tag : tags) {
|
||||
knownTags.insert(std::pair<QString, qint64>(tag.name, tag.id));
|
||||
}
|
||||
refreshTagBoxes();
|
||||
}
|
||||
else {
|
||||
minorErrorLabel->setText(tr("Unable to fetch tags. Please check your connection."));
|
||||
includeTagButton->setEnabled(false);
|
||||
}
|
||||
|
||||
disconnect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
|
||||
tidyReply(&getTagsReply);
|
||||
emit tagsLoaded(isValid);
|
||||
}
|
||||
|
||||
void TagEditor::onCreateTagComplete() {
|
||||
bool isValid;
|
||||
auto data = NetMan::extractResponse(createTagReply, isValid);
|
||||
if (isValid) {
|
||||
auto newTag = dto::Tag::parseData(data);
|
||||
knownTags.insert(std::pair<QString, qint64>(newTag.name, newTag.id));
|
||||
includedTagIds.insert(newTag.id);
|
||||
createTagTextBox->setText("");
|
||||
refreshTagBoxes();
|
||||
}
|
||||
else {
|
||||
couldNotCreateTagMsg->showMessage(
|
||||
"Could not create tag. Please check your connection and try again.");
|
||||
}
|
||||
disconnect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
||||
tidyReply(&createTagReply);
|
||||
createTagButton->stopAnimation();
|
||||
createTagButton->setEnabled(true);
|
||||
}
|
||||
|
||||
QString TagEditor::randomColor() {
|
||||
// Note: this should match the frontend's color palette (naming)
|
||||
static std::vector<QString> colors = {
|
||||
"blue", "yellow", "green", "indigo", "orange",
|
||||
"lightBlue", "lightYellow", "lightGreen", "lightIndigo", "lightOrange",
|
||||
"pink", "red", "teal", "vermilion", "violet",
|
||||
"lightPink", "lightRed", "lightTeal", "lightVermilion", "lightViolet"};
|
||||
auto index = QRandomGenerator::global()->bounded(int(colors.size()));
|
||||
return colors.at(index);
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
#ifndef TAGEDITOR_H
|
||||
#define TAGEDITOR_H
|
||||
|
||||
#include <QErrorMessage>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QNetworkReply>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "components/loading_button/loadingbutton.h"
|
||||
#include "models/tag.h"
|
||||
|
||||
class TagEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TagEditor(QWidget* parent = nullptr);
|
||||
~TagEditor();
|
||||
|
||||
public:
|
||||
void setEnabled(bool enable);
|
||||
void loadTags(const QString& operationSlug, std::vector<qint64> initialTagIDs);
|
||||
void clear();
|
||||
std::vector<model::Tag> getIncludedTags();
|
||||
|
||||
signals:
|
||||
void tagsLoaded(bool success);
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void wireUi();
|
||||
void refreshTagBoxes();
|
||||
QString randomColor();
|
||||
|
||||
private slots:
|
||||
void createTagButtonClicked();
|
||||
void includeTagButtonClicked();
|
||||
void excludeTagButtonClicked();
|
||||
|
||||
void onGetTagsComplete();
|
||||
void onCreateTagComplete();
|
||||
|
||||
private:
|
||||
QString operationSlug;
|
||||
std::unordered_map<QString, qint64> knownTags;
|
||||
std::unordered_set<qint64> includedTagIds;
|
||||
|
||||
// ui Components
|
||||
QGridLayout* gridLayout;
|
||||
QLabel* _withTagsLabel;
|
||||
LoadingButton* createTagButton;
|
||||
QLineEdit* createTagTextBox;
|
||||
QListWidget* includedTagsListBox;
|
||||
QListWidget* allTagsListBox;
|
||||
QPushButton* includeTagButton;
|
||||
QPushButton* excludeTagButton;
|
||||
QLabel* minorErrorLabel;
|
||||
|
||||
QNetworkReply* getTagsReply = nullptr;
|
||||
QNetworkReply* createTagReply = nullptr;
|
||||
QErrorMessage* couldNotCreateTagMsg = nullptr;
|
||||
};
|
||||
|
||||
#endif // TAGEDITOR_H
|
|
@ -0,0 +1,228 @@
|
|||
#include "components/tagging/tageditor.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QLineEdit>
|
||||
#include <QStringListModel>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
|
||||
#include "helpers/netman.h"
|
||||
#include "helpers/stopreply.h"
|
||||
|
||||
TagEditor::TagEditor(QWidget *parent) : QWidget(parent) {
|
||||
buildUi();
|
||||
|
||||
wireUi();
|
||||
}
|
||||
|
||||
TagEditor::~TagEditor() {
|
||||
delete couldNotCreateTagMsg;
|
||||
delete loading;
|
||||
delete tagCompleteTextBox;
|
||||
delete errorLabel;
|
||||
delete tagView;
|
||||
delete gridLayout;
|
||||
delete completer;
|
||||
|
||||
stopReply(&getTagsReply);
|
||||
stopReply(&createTagReply);
|
||||
}
|
||||
|
||||
void TagEditor::buildUi() {
|
||||
gridLayout = new QGridLayout(this);
|
||||
gridLayout->setMargin(0);
|
||||
|
||||
couldNotCreateTagMsg = new QErrorMessage(this);
|
||||
|
||||
tagView = new TagView(this);
|
||||
errorLabel = new QLabel(this);
|
||||
loading = new QProgressIndicator(this);
|
||||
|
||||
completer = new QCompleter(this);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
|
||||
tagCompleteTextBox = new QLineEdit(this);
|
||||
tagCompleteTextBox->setPlaceholderText("Add Tags...");
|
||||
tagCompleteTextBox->installEventFilter(&filter);
|
||||
tagCompleteTextBox->setCompleter(completer);
|
||||
tagCompleteTextBox->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed));
|
||||
|
||||
// Layout
|
||||
/* 0 1 2
|
||||
+----------------+-------------+--------------+
|
||||
0 | |
|
||||
| Flow Tag List |
|
||||
| |
|
||||
+----------------+-------------+--------------+
|
||||
1 | errLbl/Loading | <None> | [Add Tag TB] |
|
||||
+----------------+-------------+--------------+
|
||||
*/
|
||||
|
||||
// row 0
|
||||
gridLayout->addWidget(tagView, 0, 0, 1, 3);
|
||||
|
||||
// row 1
|
||||
gridLayout->addWidget(errorLabel, 1, 0);
|
||||
gridLayout->addWidget(loading, 1, 0);
|
||||
gridLayout->addWidget(tagCompleteTextBox, 1, 2);
|
||||
|
||||
this->setLayout(gridLayout);
|
||||
}
|
||||
|
||||
void TagEditor::wireUi() {
|
||||
connect(tagCompleteTextBox, &QLineEdit::returnPressed, this, &TagEditor::tagEditReturnPressed);
|
||||
connect(&filter, &TaggingLineEditEventFilter::upPressed, this, &TagEditor::showCompleter);
|
||||
connect(&filter, &TaggingLineEditEventFilter::downPressed, this, &TagEditor::showCompleter);
|
||||
connect(&filter, &TaggingLineEditEventFilter::completePressed, this, &TagEditor::showCompleter);
|
||||
connect(&filter, &TaggingLineEditEventFilter::leftMouseClickPressed, this, &TagEditor::showCompleter);
|
||||
|
||||
connect(completer, QOverload<const QString &>::of(&QCompleter::activated), this,
|
||||
&TagEditor::completerActivated);
|
||||
|
||||
connect(tagCompleteTextBox, &QLineEdit::textChanged, [this](const QString &text) {
|
||||
if (text.isEmpty()) {
|
||||
tagCompleteTextBox->completer()->setCompletionPrefix("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TagEditor::completerActivated(const QString &text) {
|
||||
tagTextEntered(text);
|
||||
QTimer::singleShot(0, tagCompleteTextBox, &QLineEdit::clear);
|
||||
}
|
||||
|
||||
void TagEditor::tagEditReturnPressed() {
|
||||
if (completer->popup()->isVisible()) {
|
||||
return;
|
||||
}
|
||||
tagTextEntered(tagCompleteTextBox->text().trimmed());
|
||||
}
|
||||
|
||||
void TagEditor::tagTextEntered(QString text) {
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto foundTag = tagMap.find(standardizeTagKey(text));
|
||||
if (foundTag == tagMap.end()) {
|
||||
createTag(text);
|
||||
}
|
||||
else {
|
||||
dto::Tag data = foundTag->second;
|
||||
tagView->contains(data) ? tagView->remove(data) : tagView->addTag(data);
|
||||
}
|
||||
|
||||
tagCompleteTextBox->setText("");
|
||||
tagCompleteTextBox->completer()->setCompletionPrefix("");
|
||||
}
|
||||
|
||||
void TagEditor::updateCompleterModel() {
|
||||
tagNames.sort(Qt::CaseInsensitive);
|
||||
// no need to delete previous model -- handled by qcompleter
|
||||
completer->setModel(new QStringListModel(tagNames, completer));
|
||||
completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel);
|
||||
}
|
||||
|
||||
void TagEditor::clear() {
|
||||
stopReply(&getTagsReply);
|
||||
stopReply(&createTagReply);
|
||||
tagCompleteTextBox->clear();
|
||||
errorLabel->setText("");
|
||||
tagView->clear();
|
||||
}
|
||||
|
||||
void TagEditor::loadTags(const QString &operationSlug, std::vector<model::Tag> initialTags) {
|
||||
this->operationSlug = operationSlug;
|
||||
this->initialTags = initialTags;
|
||||
|
||||
getTagsReply = NetMan::getInstance().getOperationTags(operationSlug);
|
||||
connect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
|
||||
}
|
||||
|
||||
void TagEditor::onGetTagsComplete() {
|
||||
bool isValid;
|
||||
auto data = NetMan::extractResponse(getTagsReply, isValid);
|
||||
tagMap.clear();
|
||||
tagNames.clear();
|
||||
if (isValid) {
|
||||
std::vector<dto::Tag> tags = dto::Tag::parseDataAsList(data);
|
||||
for (auto tag : tags) {
|
||||
addTag(tag);
|
||||
|
||||
auto itr = std::find_if(initialTags.begin(), initialTags.end(), [tag](model::Tag modelTag) {
|
||||
return modelTag.serverTagId == tag.id;
|
||||
});
|
||||
if (itr != initialTags.end()) {
|
||||
tagView->addTag(tag);
|
||||
}
|
||||
}
|
||||
updateCompleterModel();
|
||||
}
|
||||
else {
|
||||
errorLabel->setText(
|
||||
tr("Unable to fetch tags."
|
||||
" Please check your connection."
|
||||
" (Tags names and colors may be incorrect)"));
|
||||
tagCompleteTextBox->setEnabled(false);
|
||||
for (auto tag : initialTags) {
|
||||
tagView->addTag(dto::Tag::fromModelTag(tag, TagWidget::randomColor()));
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
|
||||
tidyReply(&getTagsReply);
|
||||
emit tagsLoaded(isValid);
|
||||
}
|
||||
|
||||
void TagEditor::createTag(QString tagName) {
|
||||
auto newText = tagName.trimmed();
|
||||
if (newText == "") {
|
||||
return;
|
||||
}
|
||||
errorLabel->setText("");
|
||||
loading->startAnimation();
|
||||
tagCompleteTextBox->setEnabled(false);
|
||||
|
||||
dto::Tag newTag(newText, TagWidget::randomColor());
|
||||
createTagReply = NetMan::getInstance().createTag(newTag, operationSlug);
|
||||
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
||||
}
|
||||
|
||||
void TagEditor::onCreateTagComplete() {
|
||||
bool isValid;
|
||||
auto data = NetMan::extractResponse(createTagReply, isValid);
|
||||
if (isValid) {
|
||||
auto newTag = dto::Tag::parseData(data);
|
||||
addTag(newTag);
|
||||
tagView->addTag(newTag);
|
||||
updateCompleterModel();
|
||||
}
|
||||
else {
|
||||
couldNotCreateTagMsg->showMessage(
|
||||
"Could not create tag."
|
||||
" Please check your connection and try again.");
|
||||
}
|
||||
disconnect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
|
||||
tidyReply(&createTagReply);
|
||||
loading->stopAnimation();
|
||||
tagCompleteTextBox->setEnabled(true);
|
||||
tagCompleteTextBox->setFocus();
|
||||
}
|
||||
|
||||
void TagEditor::addTag(dto::Tag tag) {
|
||||
tagNames << tag.name;
|
||||
tagMap.emplace(standardizeTagKey(tag.name), tag);
|
||||
}
|
||||
|
||||
QString TagEditor::standardizeTagKey(const QString& tagName) {
|
||||
return tagName.trimmed().toLower();
|
||||
}
|
||||
|
||||
void TagEditor::setReadonly(bool readonly) {
|
||||
tagCompleteTextBox->setEnabled(!readonly);
|
||||
tagCompleteTextBox->setVisible(!readonly);
|
||||
|
||||
tagView->setReadonly(readonly);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
#ifndef TAGEDITOR_H
|
||||
#define TAGEDITOR_H
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QErrorMessage>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QNetworkReply>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
#include "components/loading/qprogressindicator.h"
|
||||
#include "components/loading_button/loadingbutton.h"
|
||||
#include "components/tagging/tagview.h"
|
||||
#include "components/tagging/tagwidget.h"
|
||||
#include "components/tagging/tagginglineediteventfilter.h"
|
||||
#include "models/tag.h"
|
||||
|
||||
class TagEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TagEditor(QWidget* parent = nullptr);
|
||||
~TagEditor();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void wireUi();
|
||||
|
||||
void createTag(QString tagName);
|
||||
void updateCompleterModel();
|
||||
void tagTextEntered(QString text);
|
||||
inline void showCompleter() { completer->complete(); }
|
||||
void addTag(dto::Tag tag);
|
||||
QString standardizeTagKey(const QString &tagName);
|
||||
|
||||
private slots:
|
||||
void onGetTagsComplete();
|
||||
void onCreateTagComplete();
|
||||
void tagEditReturnPressed();
|
||||
void completerActivated(const QString& text);
|
||||
|
||||
public:
|
||||
void clear();
|
||||
void setReadonly(bool readonly);
|
||||
void loadTags(const QString& operationSlug, std::vector<model::Tag> initialTagIDs);
|
||||
inline std::vector<model::Tag> getIncludedTags() { return tagView->getIncludedTags(); }
|
||||
|
||||
signals:
|
||||
void tagsLoaded(bool isValid);
|
||||
|
||||
private:
|
||||
QString operationSlug = "";
|
||||
std::vector<model::Tag> initialTags;
|
||||
|
||||
QNetworkReply* getTagsReply = nullptr;
|
||||
QNetworkReply* createTagReply = nullptr;
|
||||
|
||||
QErrorMessage* couldNotCreateTagMsg = nullptr;
|
||||
|
||||
TaggingLineEditEventFilter filter;
|
||||
QCompleter* completer;
|
||||
QStringList tagNames;
|
||||
std::unordered_map<QString, dto::Tag> tagMap;
|
||||
|
||||
// Ui Elements
|
||||
QGridLayout* gridLayout = nullptr;
|
||||
QLineEdit* tagCompleteTextBox = nullptr;
|
||||
QProgressIndicator* loading = nullptr;
|
||||
QLabel* errorLabel = nullptr;
|
||||
TagView* tagView = nullptr;
|
||||
};
|
||||
|
||||
#endif // TAGEDITOR_H
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef UPDOWNKEYFILTER_H
|
||||
#define UPDOWNKEYFILTER_H
|
||||
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QWidget>
|
||||
#include <iostream>
|
||||
|
||||
class TaggingLineEditEventFilter : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TaggingLineEditEventFilter(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
signals:
|
||||
void upPressed();
|
||||
void downPressed();
|
||||
void completePressed();
|
||||
void leftMouseClickPressed();
|
||||
|
||||
private:
|
||||
bool matchesKey(QKeyEvent *ke, QKeySequence keyCombo) {
|
||||
// with help from https://forum.qt.io/topic/73408/qt-reading-key-sequences-from-key-event/3
|
||||
|
||||
QString modifier;
|
||||
QString key;
|
||||
|
||||
if (ke->modifiers() & Qt::ShiftModifier) modifier += "Shift+";
|
||||
if (ke->modifiers() & Qt::ControlModifier) modifier += "Ctrl+";
|
||||
if (ke->modifiers() & Qt::AltModifier) modifier += "Alt+";
|
||||
if (ke->modifiers() & Qt::MetaModifier) modifier += "Meta+";
|
||||
|
||||
key = QKeySequence(ke->key()).toString();
|
||||
|
||||
QKeySequence ks(modifier + key);
|
||||
return ks[0] == keyCombo[0];
|
||||
}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *object, QEvent *event) {
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
QKeyEvent *ke = static_cast<QKeyEvent *>(event);
|
||||
|
||||
if (matchesKey(ke, QKeySequence(Qt::CTRL + Qt::Key_Space))) {
|
||||
emit completePressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ke->key() == Qt::Key_Up) {
|
||||
emit upPressed();
|
||||
return true;
|
||||
}
|
||||
if (ke->key() == Qt::Key_Down) {
|
||||
emit downPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (event->type() == QEvent::MouseButtonRelease) {
|
||||
auto mouseEvt = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvt->button() == Qt::LeftButton) {
|
||||
emit leftMouseClickPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(object, event);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // UPDOWNKEYFILTER_H
|
|
@ -0,0 +1,97 @@
|
|||
#include "tagview.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
TagView::TagView(QWidget *parent) : QWidget(parent) {
|
||||
buildUi();
|
||||
wireUi();
|
||||
}
|
||||
|
||||
TagView::~TagView() {
|
||||
clear();
|
||||
delete layout;
|
||||
delete tagGroupBox;
|
||||
delete mainLayout;
|
||||
}
|
||||
|
||||
void TagView::buildUi() {
|
||||
mainLayout = new QHBoxLayout(this);
|
||||
mainLayout->setMargin(0);
|
||||
|
||||
tagGroupBox = new QGroupBox("Tags", this);
|
||||
layout = new FlowLayout();
|
||||
tagGroupBox->setLayout(layout);
|
||||
|
||||
mainLayout->addWidget(tagGroupBox);
|
||||
|
||||
setLayout(mainLayout);
|
||||
}
|
||||
|
||||
void TagView::wireUi() {
|
||||
|
||||
}
|
||||
|
||||
void TagView::addTag(dto::Tag tag) {
|
||||
TagWidget* widget = new TagWidget(tag, readonly, this);
|
||||
includedTags.push_back(widget);
|
||||
layout->addWidget(widget);
|
||||
connect(widget, &TagWidget::removePressed, [this, widget](){
|
||||
removeWidget(widget);
|
||||
});
|
||||
}
|
||||
|
||||
bool TagView::contains(dto::Tag tag) {
|
||||
for (const auto &widget : includedTags) {
|
||||
if (widget->getTag().id == tag.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TagView::removeWidget(TagWidget* tagWidget) {
|
||||
// remove from view
|
||||
tagWidget->hide();
|
||||
layout->removeWidget(tagWidget);
|
||||
|
||||
// remove from includedTags
|
||||
auto itr = std::find(includedTags.begin(), includedTags.end(), tagWidget);
|
||||
if(itr != includedTags.cend()) {
|
||||
auto last = includedTags.end()-1;
|
||||
std::iter_swap(itr, last);
|
||||
includedTags.pop_back();
|
||||
}
|
||||
delete tagWidget;
|
||||
}
|
||||
|
||||
void TagView::remove(dto::Tag tag) {
|
||||
auto itr = std::find_if(includedTags.begin(), includedTags.end(), [tag](TagWidget* item){
|
||||
return item->getTag().id == tag.id;
|
||||
});
|
||||
removeWidget(includedTags.at(itr - includedTags.begin()));
|
||||
}
|
||||
|
||||
void TagView::clear() {
|
||||
for(auto widget : includedTags) {
|
||||
layout->removeWidget(widget);
|
||||
delete widget;
|
||||
}
|
||||
includedTags.clear();
|
||||
}
|
||||
|
||||
std::vector<model::Tag> TagView::getIncludedTags() {
|
||||
std::vector<model::Tag> rtn;
|
||||
|
||||
for (const auto &widget : includedTags) {
|
||||
dto::Tag tag = widget->getTag();
|
||||
rtn.push_back(model::Tag(tag.id, tag.name));
|
||||
}
|
||||
|
||||
return rtn;
|
||||
}
|
||||
|
||||
void TagView::setReadonly(bool readonly) {
|
||||
for(auto widget : includedTags) {
|
||||
widget->setReadOnly(readonly);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
#ifndef TAGVIEW_H
|
||||
#define TAGVIEW_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QGroupBox>
|
||||
|
||||
#include "components/tagging/tagwidget.h"
|
||||
#include "components/flow_layout/flowlayout.h"
|
||||
#include "models/tag.h"
|
||||
|
||||
class TagView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TagView(QWidget *parent = nullptr);
|
||||
~TagView();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void wireUi();
|
||||
|
||||
private slots:
|
||||
void removeWidget(TagWidget* tag);
|
||||
|
||||
public:
|
||||
void addTag(dto::Tag tag);
|
||||
std::vector<model::Tag> getIncludedTags();
|
||||
void setReadonly(bool readonly);
|
||||
bool contains(dto::Tag tag);
|
||||
void remove(dto::Tag tag);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
bool readonly = false;
|
||||
|
||||
// UI Components
|
||||
QHBoxLayout* mainLayout = nullptr;
|
||||
FlowLayout* layout = nullptr;
|
||||
QGroupBox* tagGroupBox = nullptr;
|
||||
std::vector<TagWidget*> includedTags;
|
||||
};
|
||||
|
||||
#endif // TAGVIEW_H
|
|
@ -0,0 +1,108 @@
|
|||
#include "tagwidget.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <iostream>
|
||||
|
||||
TagWidget::TagWidget(dto::Tag tag, bool readonly, QWidget *parent) : QLabel(parent) {
|
||||
this->tag = tag;
|
||||
this->readonly = readonly;
|
||||
buildUi();
|
||||
}
|
||||
|
||||
void TagWidget::buildUi() {
|
||||
buildTag();
|
||||
}
|
||||
|
||||
void TagWidget::wireUi(){
|
||||
|
||||
}
|
||||
|
||||
void TagWidget::setReadOnly(bool readonly) {
|
||||
this->readonly = readonly;
|
||||
buildTag();
|
||||
}
|
||||
|
||||
void TagWidget::mouseReleaseEvent(QMouseEvent* evt) {
|
||||
const int x = evt->x();
|
||||
const int y = evt->y();
|
||||
|
||||
if (removeArea.contains(x, y)) {
|
||||
emit removePressed();
|
||||
}
|
||||
else if (labelArea.contains(x, y)) {
|
||||
emit labelPressed();
|
||||
}
|
||||
}
|
||||
|
||||
void TagWidget::buildTag() {
|
||||
QFont labelFont;
|
||||
#ifdef Q_OS_MACOS
|
||||
labelFont = QFont("Sans", 14);
|
||||
#else
|
||||
labelFont = QFont("Sans", 12);
|
||||
#endif
|
||||
|
||||
// Calculate the positions of everything
|
||||
QFontMetrics metric(labelFont);
|
||||
QSize labelSize = metric.size(Qt::TextSingleLine, tag.name);
|
||||
QSize removeSize = metric.size(Qt::TextSingleLine, removeSymbol);
|
||||
|
||||
int labelWidth = labelSize.width();
|
||||
int innerTagHeight = std::max(labelSize.height(), removeSize.height()); //tag height without the buffer
|
||||
|
||||
int tagHeightBuffer = 12; // space around the top/bottom (real size is half as much)
|
||||
int tagWidthBuffer = 12; // space around the outer left/right edges (real size is half as much)
|
||||
int innerBuffer = 6; // space between each segment
|
||||
|
||||
int labelLeftOffset = tagWidthBuffer/2;
|
||||
int labelTopOffset = ((innerTagHeight - labelSize.height())/2) + (tagHeightBuffer/2);
|
||||
|
||||
int removeLeftOffset = labelLeftOffset + labelWidth + innerBuffer;
|
||||
int removeTopOffset = ((innerTagHeight - removeSize.height())/2) + (tagHeightBuffer/2);
|
||||
|
||||
int fullTagWidth = labelWidth + tagWidthBuffer;
|
||||
int fullTagHeight = innerTagHeight + tagHeightBuffer;
|
||||
|
||||
// set bounds for mouse release event
|
||||
labelArea = QRectF(0, 0, removeLeftOffset, fullTagHeight);
|
||||
removeArea = QRectF(-1, -1, 0, 0); // set to dummy value in case we don't have a remove area
|
||||
|
||||
if (!readonly) {
|
||||
fullTagWidth += removeSize.width() + innerBuffer;
|
||||
removeArea = QRectF(removeLeftOffset, 0, fullTagWidth - removeLeftOffset, fullTagHeight);
|
||||
}
|
||||
|
||||
const qreal dpr = this->devicePixelRatio();
|
||||
|
||||
// prep the image
|
||||
QPixmap pixmap = QPixmap(fullTagWidth * dpr, fullTagHeight * dpr);
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QColor bgColor = colorMap[tag.colorName];
|
||||
QPainter painter(&pixmap);
|
||||
// these actually are used and removing them makes the edges/text slightly less sharp
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform);
|
||||
|
||||
// draw container
|
||||
painter.setBrush(bgColor);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(QRectF(0, 0, (pixmap.width() / dpr)-1, (pixmap.height() / dpr)-1), 6, 6);
|
||||
|
||||
// set up font drawing
|
||||
auto fontColor = fontColorForBgColor(bgColor);
|
||||
painter.setFont(labelFont);
|
||||
painter.setPen(fontColor);
|
||||
|
||||
// draw label
|
||||
painter.drawText(QRectF(QPointF(labelLeftOffset, labelTopOffset), labelSize), Qt::AlignCenter, tag.name);
|
||||
|
||||
// draw remove (if needed)
|
||||
if(!readonly) {
|
||||
painter.drawText(QRectF(QPointF(removeLeftOffset, removeTopOffset), removeSize), Qt::AlignCenter, removeSymbol);
|
||||
}
|
||||
painter.end();
|
||||
|
||||
setPixmap(pixmap);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
#ifndef TAGWIDGET_H
|
||||
#define TAGWIDGET_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QLabel>
|
||||
#include <QWidget>
|
||||
#include <QRandomGenerator>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "dtos/tag.h"
|
||||
|
||||
static std::unordered_map<QString, QColor> colorMap{
|
||||
// matches colors defined on front end
|
||||
{"blue", QColor(0x0E5A8A)},
|
||||
{"yellow", QColor(0xA67908)},
|
||||
{"green", QColor(0x0A6640)},
|
||||
{"indigo", QColor(0x5642A6)},
|
||||
{"orange", QColor(0xA66321)},
|
||||
{"pink", QColor(0xA82255)},
|
||||
{"red", QColor(0xA82A2A)},
|
||||
{"teal", QColor(0x008075)},
|
||||
{"vermilion", QColor(0x9E2B0E)},
|
||||
{"violet", QColor(0x5C255C)},
|
||||
{"lightBlue", QColor(0x48AFF0)},
|
||||
{"lightYellow", QColor(0xFFC940)},
|
||||
{"lightGreen", QColor(0x3DCC91)},
|
||||
{"lightIndigo", QColor(0xAD99FF)},
|
||||
{"lightOrange", QColor(0xFFB366)},
|
||||
{"lightPink", QColor(0xFF66A1)},
|
||||
{"lightRed", QColor(0xFF7373)},
|
||||
{"lightTeal", QColor(0x2EE6D6)},
|
||||
{"lightVermilion", QColor(0xFF6E4A)},
|
||||
{"lightViolet", QColor(0xC274C2)},
|
||||
};
|
||||
|
||||
static std::vector<QString> allColorsNames = []()->std::vector<QString>{
|
||||
std::vector<QString> colors;
|
||||
for (auto kv : colorMap) {
|
||||
colors.push_back(kv.first);
|
||||
}
|
||||
return colors;
|
||||
}();
|
||||
|
||||
class TagWidget : public QLabel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TagWidget(dto::Tag tag, bool readonly, QWidget* parent = nullptr);
|
||||
~TagWidget() = default;
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void wireUi();
|
||||
void buildTag();
|
||||
void setImage(QImage img);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent* ev) override;
|
||||
|
||||
public:
|
||||
inline dto::Tag getTag(){return tag;};
|
||||
|
||||
void setReadOnly(bool readonly);
|
||||
inline bool isReadOnly(){return readonly;}
|
||||
|
||||
static QString randomColor() {
|
||||
// Note: this should match the frontend's color palette (naming)
|
||||
auto index = QRandomGenerator::global()->bounded(int(allColorsNames.size()));
|
||||
return allColorsNames.at(index);
|
||||
}
|
||||
|
||||
static QColor fontColorForBgColor(QColor bg) {
|
||||
long yiq = ((bg.red() * 299) + (bg.green() * 587) + (bg.blue() * 114)) / 1000;
|
||||
return (yiq < 128 ? Qt::white : Qt::black);
|
||||
}
|
||||
|
||||
signals:
|
||||
void removePressed();
|
||||
void labelPressed();
|
||||
|
||||
private:
|
||||
bool readonly = false;
|
||||
dto::Tag tag;
|
||||
|
||||
const QString removeSymbol = QString::fromUtf8("\u2718");
|
||||
|
||||
QRectF labelArea;
|
||||
QRectF removeArea;
|
||||
int tagWidth;
|
||||
int tagHeight;
|
||||
};
|
||||
|
||||
#endif // TAGWIDGET_H
|
|
@ -8,20 +8,21 @@
|
|||
#include <vector>
|
||||
|
||||
#include "helpers/jsonhelpers.h"
|
||||
#include "models/tag.h"
|
||||
|
||||
namespace dto {
|
||||
class Tag {
|
||||
public:
|
||||
Tag() {}
|
||||
Tag() = default;
|
||||
~Tag() = default;
|
||||
Tag(const Tag &) = default;
|
||||
|
||||
Tag(QString name, QString colorName) {
|
||||
this->name = name;
|
||||
this->colorName = colorName;
|
||||
}
|
||||
|
||||
qint64 id;
|
||||
QString colorName;
|
||||
QString name;
|
||||
|
||||
public:
|
||||
static Tag parseData(QByteArray data) { return parseJSONItem<Tag>(data, Tag::fromJson); }
|
||||
|
||||
static std::vector<Tag> parseDataAsList(QByteArray data) {
|
||||
|
@ -35,6 +36,13 @@ class Tag {
|
|||
return QJsonDocument(obj).toJson();
|
||||
}
|
||||
|
||||
static Tag fromModelTag(model::Tag tag, QString colorName) {
|
||||
Tag t;
|
||||
t.name = tag.tagName;
|
||||
t.colorName = colorName;
|
||||
return t;
|
||||
}
|
||||
|
||||
private:
|
||||
// provides a Tag from a given QJsonObject
|
||||
static Tag fromJson(QJsonObject obj) {
|
||||
|
@ -45,7 +53,13 @@ class Tag {
|
|||
|
||||
return t;
|
||||
}
|
||||
|
||||
public:
|
||||
qint64 id;
|
||||
QString colorName;
|
||||
QString name;
|
||||
|
||||
};
|
||||
} // namespace dto
|
||||
|
||||
Q_DECLARE_METATYPE(dto::Tag);
|
||||
#endif // DTO_TAG_H
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <QAction>
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QNetworkReply>
|
||||
#include <QTableWidget>
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
#include "getinfo.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QKeySequence>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "appsettings.h"
|
||||
#include "components/evidence_editor/evidenceeditor.h"
|
||||
|
@ -54,6 +54,12 @@ void GetInfo::wireUi() {
|
|||
connect(closeWindowAction, &QAction::triggered, this, &GetInfo::deleteButtonClicked);
|
||||
}
|
||||
|
||||
void GetInfo::showEvent(QShowEvent* evt) {
|
||||
QDialog::showEvent(evt);
|
||||
setFocus(); // giving the form focus, to prevent retaining focus on the submit button when
|
||||
// closing the window
|
||||
}
|
||||
|
||||
bool GetInfo::saveData() {
|
||||
auto saveResponse = evidenceEditor->saveEvidence();
|
||||
if (!saveResponse.actionSucceeded) {
|
||||
|
@ -139,6 +145,7 @@ void GetInfo::onUploadComplete() {
|
|||
else {
|
||||
try {
|
||||
db->updateEvidenceSubmitted(this->evidenceID);
|
||||
emit evidenceSubmitted(db->getEvidenceDetails(this->evidenceID));
|
||||
this->close();
|
||||
}
|
||||
catch (QSqlError& e) {
|
||||
|
|
|
@ -30,12 +30,18 @@ class GetInfo : public QDialog {
|
|||
bool saveData();
|
||||
void setActionButtonsEnabled(bool enabled);
|
||||
|
||||
void showEvent(QShowEvent *evt) override;
|
||||
|
||||
private slots:
|
||||
void submitButtonClicked();
|
||||
void deleteButtonClicked();
|
||||
|
||||
void onUploadComplete();
|
||||
|
||||
public:
|
||||
signals:
|
||||
void evidenceSubmitted(model::Evidence evidence);
|
||||
|
||||
private:
|
||||
Ui::GetInfo *ui;
|
||||
DatabaseConnection *db;
|
||||
|
|
34
src/main.cpp
34
src/main.cpp
|
@ -19,6 +19,38 @@ void handleCLI(std::vector<std::string> args);
|
|||
#include "exceptions/fileerror.h"
|
||||
#include "traymanager.h"
|
||||
|
||||
QDataStream& operator<<(QDataStream& out, const model::Tag& v) {
|
||||
out << v.tagName << v.id << v.serverTagId;
|
||||
return out;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& in, model::Tag& v) {
|
||||
in >> v.tagName;
|
||||
in >> v.id;
|
||||
in >> v.serverTagId;
|
||||
return in;
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream& out, const std::vector<model::Tag>& v) {
|
||||
out << int(v.size());
|
||||
for (auto tag : v) {
|
||||
out << tag;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream& in, std::vector<model::Tag>& v) {
|
||||
int qty;
|
||||
in >> qty;
|
||||
v.reserve(qty);
|
||||
for(int i = 0; i < qty; i++) {
|
||||
model::Tag t;
|
||||
in >> t;
|
||||
v.push_back(t);
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
Q_INIT_RESOURCE(res_icons);
|
||||
Q_INIT_RESOURCE(res_migrations);
|
||||
|
@ -58,6 +90,8 @@ int main(int argc, char* argv[]) {
|
|||
|
||||
int rtn;
|
||||
try {
|
||||
qRegisterMetaTypeStreamOperators<model::Tag>("Tag");
|
||||
qRegisterMetaTypeStreamOperators<std::vector<model::Tag>>("TagVector");
|
||||
QApplication app(argc, argv);
|
||||
|
||||
if (!QSystemTrayIcon::isSystemTrayAvailable()) {
|
||||
|
|
|
@ -5,20 +5,29 @@
|
|||
#define MODEL_TAG_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QDataStream>
|
||||
|
||||
namespace model {
|
||||
class Tag {
|
||||
public:
|
||||
Tag() = default;
|
||||
~Tag() = default;
|
||||
Tag(const Tag &) = default;
|
||||
|
||||
Tag(qint64 id, qint64 tagId, QString name) : Tag(tagId, name) { this->id = id; }
|
||||
Tag(qint64 tagId, QString name) {
|
||||
this->serverTagId = tagId;
|
||||
this->tagName = name;
|
||||
}
|
||||
|
||||
public:
|
||||
qint64 id;
|
||||
qint64 serverTagId;
|
||||
QString tagName;
|
||||
};
|
||||
} // namespace model
|
||||
|
||||
Q_DECLARE_METATYPE(model::Tag);
|
||||
Q_DECLARE_METATYPE(std::vector<model::Tag>);
|
||||
#endif // MODEL_TAG_H
|
||||
|
|
|
@ -175,15 +175,36 @@ void TrayManager::createActions() {
|
|||
chooseOpSubmenu->addSeparator();
|
||||
}
|
||||
|
||||
void TrayManager::spawnGetInfoWindow(qint64 evidenceID) {
|
||||
auto getInfoWindow = new GetInfo(db, evidenceID, this);
|
||||
connect(getInfoWindow, &GetInfo::evidenceSubmitted, [](model::Evidence evi){
|
||||
AppSettings::getInstance().setLastUsedTags(evi.tags);
|
||||
});
|
||||
getInfoWindow->show();
|
||||
}
|
||||
|
||||
qint64 TrayManager::createNewEvidence(QString filepath, QString evidenceType) {
|
||||
AppSettings& inst = AppSettings::getInstance();
|
||||
auto evidenceID = db->createEvidence(filepath, inst.operationSlug(), evidenceType);
|
||||
auto tags = inst.getLastUsedTags();
|
||||
if (tags.size() > 0) {
|
||||
db->setEvidenceTags(tags, evidenceID);
|
||||
}
|
||||
return evidenceID;
|
||||
}
|
||||
|
||||
void TrayManager::onCodeblockCapture() {
|
||||
QString clipboardContent = ClipboardHelper::readPlaintext();
|
||||
if (clipboardContent != "") {
|
||||
Codeblock evidence(clipboardContent);
|
||||
Codeblock::saveCodeblock(evidence);
|
||||
auto evidenceID = db->createEvidence(evidence.filePath(),
|
||||
AppSettings::getInstance().operationSlug(), "codeblock");
|
||||
auto getInfoWindow = new GetInfo(db, evidenceID, this);
|
||||
getInfoWindow->show();
|
||||
try {
|
||||
auto evidenceID = createNewEvidence(evidence.filePath(), "codeblock");
|
||||
spawnGetInfoWindow(evidenceID);
|
||||
}
|
||||
catch (QSqlError& e) {
|
||||
std::cout << "could not write to the database: " << e.text().toStdString() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,11 +228,9 @@ void TrayManager::createTrayMenu() {
|
|||
}
|
||||
|
||||
void TrayManager::onScreenshotCaptured(const QString& path) {
|
||||
std::cout << "Captured screenshot to file: " << path.toStdString() << std::endl;
|
||||
try {
|
||||
auto evidenceID = db->createEvidence(path, AppSettings::getInstance().operationSlug(), "image");
|
||||
auto getInfoWindow = new GetInfo(db, evidenceID, this);
|
||||
getInfoWindow->show();
|
||||
auto evidenceID = createNewEvidence(path, "image");
|
||||
spawnGetInfoWindow(evidenceID);
|
||||
}
|
||||
catch (QSqlError& e) {
|
||||
std::cout << "could not write to the database: " << e.text().toStdString() << std::endl;
|
||||
|
@ -245,6 +264,7 @@ void TrayManager::onOperationListUpdated(bool success,
|
|||
}
|
||||
|
||||
connect(newAction, &QAction::triggered, [this, newAction, op] {
|
||||
AppSettings::getInstance().setLastUsedTags(std::vector<model::Tag>{}); // clear last used tags
|
||||
AppSettings::getInstance().setOperationDetails(op.slug, op.name);
|
||||
if (selectedAction != nullptr) {
|
||||
selectedAction->setChecked(false);
|
||||
|
|
|
@ -54,7 +54,10 @@ class TrayManager : public QDialog {
|
|||
void createActions();
|
||||
void createTrayMenu();
|
||||
void wireUi();
|
||||
qint64 createNewEvidence(QString filepath, QString evidenceType);
|
||||
void spawnGetInfoWindow(qint64 evidenceID);
|
||||
|
||||
private:
|
||||
QAction *quitAction;
|
||||
QAction *showSettingsAction;
|
||||
QAction *currentOperationMenuAction;
|
||||
|
|
Loading…
Reference in New Issue