feat(Storybook): read Figma links directly from pages
This commit is contained in:
parent
158bb87b4a
commit
853641fb89
|
@ -45,11 +45,9 @@ set(PROJECT_LIB "${PROJECT_NAME}Lib")
|
|||
add_library(${PROJECT_LIB}
|
||||
cachecleaner.cpp cachecleaner.h
|
||||
directorieswatcher.cpp directorieswatcher.h
|
||||
figmadecoratormodel.cpp figmadecoratormodel.h
|
||||
figmaio.cpp figmaio.h
|
||||
figmalinks.cpp figmalinks.h
|
||||
figmalinksmodel.cpp figmalinksmodel.h
|
||||
figmalinkssource.cpp figmalinkssource.h
|
||||
modelutils.cpp modelutils.h
|
||||
pagesmodel.h pagesmodel.cpp
|
||||
sectionsdecoratormodel.cpp sectionsdecoratormodel.h
|
||||
|
@ -99,10 +97,6 @@ add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp)
|
|||
target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
|
||||
add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest)
|
||||
|
||||
add_executable(FigmaDecoratorModelTest tests/tst_FigmaDecoratorModel.cpp)
|
||||
target_link_libraries(FigmaDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
|
||||
add_test(NAME FigmaModelTest COMMAND FigmaModelTest)
|
||||
|
||||
add_executable(QmlTests
|
||||
qmlTests/main.cpp
|
||||
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
#include "figmadecoratormodel.h"
|
||||
|
||||
#include "figmalinks.h"
|
||||
#include "figmalinksmodel.h"
|
||||
#include "modelutils.h"
|
||||
|
||||
FigmaDecoratorModel::FigmaDecoratorModel(QObject *parent)
|
||||
: QIdentityProxyModel{parent}
|
||||
{
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FigmaDecoratorModel::roleNames() const
|
||||
{
|
||||
auto roles = QIdentityProxyModel::roleNames();
|
||||
roles.insert(FigmaRole, QByteArrayLiteral("figma"));
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
QVariant FigmaDecoratorModel::data(const QModelIndex &proxyIndex, int role) const
|
||||
{
|
||||
if (!checkIndex(proxyIndex, CheckIndexOption::IndexIsValid))
|
||||
return {};
|
||||
|
||||
if (role == FigmaRole) {
|
||||
static FigmaLinksModel empty({});
|
||||
|
||||
if (!m_titleRole)
|
||||
return QVariant::fromValue(&empty);
|
||||
|
||||
const auto title = data(proxyIndex, m_titleRole.value()).toString();
|
||||
auto it = m_submodels.find(title);
|
||||
|
||||
if (it == m_submodels.end()) {
|
||||
QStringList links;
|
||||
|
||||
if (m_figmaLinks)
|
||||
links = m_figmaLinks->getLinksMap().value(title, {});
|
||||
|
||||
auto linksModel = new FigmaLinksModel(
|
||||
links, const_cast<FigmaDecoratorModel*>(this));
|
||||
it = m_submodels.insert(title, linksModel);
|
||||
}
|
||||
|
||||
return QVariant::fromValue(it.value());
|
||||
}
|
||||
|
||||
return QIdentityProxyModel::data(proxyIndex, role);
|
||||
}
|
||||
|
||||
FigmaLinks* FigmaDecoratorModel::getFigmaLinks() const
|
||||
{
|
||||
return m_figmaLinks;
|
||||
}
|
||||
|
||||
void FigmaDecoratorModel::setFigmaLinks(FigmaLinks *figmaLinks)
|
||||
{
|
||||
if (figmaLinks == m_figmaLinks)
|
||||
return;
|
||||
|
||||
m_figmaLinks = figmaLinks;
|
||||
const auto& linksMap = m_figmaLinks
|
||||
? m_figmaLinks->getLinksMap()
|
||||
: QMap<QString, QStringList>{};
|
||||
|
||||
auto linksIt = linksMap.constBegin();
|
||||
while (linksIt != linksMap.constEnd()) {
|
||||
if (m_submodels.contains(linksIt.key()))
|
||||
m_submodels.value(linksIt.key())->setContent(linksIt.value());
|
||||
++linksIt;
|
||||
}
|
||||
|
||||
auto submodelsIt = m_submodels.constBegin();
|
||||
while (submodelsIt != m_submodels.constEnd()) {
|
||||
if (!linksMap.contains(submodelsIt.key()))
|
||||
submodelsIt.value()->setContent({});
|
||||
++submodelsIt;
|
||||
}
|
||||
|
||||
emit figmaLinksChanged();
|
||||
}
|
||||
|
||||
void FigmaDecoratorModel::setSourceModel(QAbstractItemModel *sourceModel)
|
||||
{
|
||||
qDeleteAll(m_submodels);
|
||||
m_submodels.clear();
|
||||
|
||||
m_titleRole = ModelUtils::findRole(QByteArrayLiteral("title"), sourceModel);
|
||||
|
||||
if(!m_titleRole)
|
||||
qWarning("The source model is missing title role!");
|
||||
|
||||
QIdentityProxyModel::setSourceModel(sourceModel);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QIdentityProxyModel>
|
||||
#include <optional>
|
||||
|
||||
class FigmaLinks;
|
||||
class FigmaLinksModel;
|
||||
|
||||
class FigmaDecoratorModel : public QIdentityProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks
|
||||
WRITE setFigmaLinks NOTIFY figmaLinksChanged)
|
||||
public:
|
||||
static constexpr auto FigmaRole = Qt::UserRole + 100;
|
||||
|
||||
explicit FigmaDecoratorModel(QObject *parent = nullptr);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QVariant data(const QModelIndex &proxyIndex, int role) const override;
|
||||
|
||||
FigmaLinks* getFigmaLinks() const;
|
||||
void setFigmaLinks(FigmaLinks *figmaLinks);
|
||||
|
||||
void setSourceModel(QAbstractItemModel *sourceModel) override;
|
||||
|
||||
signals:
|
||||
void figmaLinksChanged();
|
||||
|
||||
private:
|
||||
std::optional<int> m_titleRole;
|
||||
FigmaLinks* m_figmaLinks = nullptr;
|
||||
mutable QMap<QString, FigmaLinksModel*> m_submodels;
|
||||
};
|
|
@ -1,139 +0,0 @@
|
|||
#include "figmalinkssource.h"
|
||||
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "figmaio.h"
|
||||
#include "figmalinks.h"
|
||||
|
||||
FigmaLinksSource::FigmaLinksSource(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
connect(&m_watcher, &QFileSystemWatcher::fileChanged,
|
||||
this, [this](const QString &path) {
|
||||
this->readFile();
|
||||
|
||||
if (!this->m_watcher.files().contains(path))
|
||||
this->m_watcher.addPath(path);
|
||||
});
|
||||
}
|
||||
|
||||
const QUrl& FigmaLinksSource::getFilePath() const
|
||||
{
|
||||
return m_filePath;
|
||||
}
|
||||
|
||||
void FigmaLinksSource::setFilePath(const QUrl& path)
|
||||
{
|
||||
if (path == m_filePath)
|
||||
return;
|
||||
|
||||
m_filePath = path;
|
||||
readFile();
|
||||
setupWatcher();
|
||||
emit filePathChanged();
|
||||
}
|
||||
|
||||
FigmaLinks* FigmaLinksSource::getFigmaLinks() const
|
||||
{
|
||||
return m_figmaLinks;
|
||||
}
|
||||
|
||||
void FigmaLinksSource::remove(const QString &key, const QList<int> &indexes)
|
||||
{
|
||||
if (m_filePath.isEmpty()) {
|
||||
qWarning("FigmaLinksSource::remove - file path is not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, QStringList> linksMap;
|
||||
|
||||
if (m_figmaLinks)
|
||||
linksMap = m_figmaLinks->getLinksMap();
|
||||
|
||||
auto it = linksMap.find(key);
|
||||
|
||||
if (it == linksMap.end()) {
|
||||
qWarning("FigmaLinksSource::remove - provided key doesn't exist!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (indexes.isEmpty())
|
||||
return;
|
||||
|
||||
auto indexesSorted = indexes;
|
||||
std::sort(indexesSorted.begin(), indexesSorted.end());
|
||||
|
||||
if (std::adjacent_find(indexesSorted.cbegin(), indexesSorted.cend())
|
||||
!= indexesSorted.cend()) {
|
||||
qWarning("FigmaLinksSource::remove - provided indexes list contains duplicates!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& linksList = it.value();
|
||||
|
||||
if (indexesSorted.first() < 0 || indexesSorted.last() >= linksList.size()) {
|
||||
qWarning("FigmaLinksSource::remove - at least one provided index is out of range!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (linksList.size() == indexesSorted.size()) {
|
||||
linksMap.erase(it);
|
||||
} else {
|
||||
std::for_each(std::crbegin(indexesSorted), std::crend(indexesSorted),
|
||||
[&linksList](int idx) {
|
||||
linksList.removeAt(idx);
|
||||
});
|
||||
}
|
||||
|
||||
FigmaIO::write(m_filePath.path(), linksMap);
|
||||
}
|
||||
|
||||
void FigmaLinksSource::append(const QString &key, const QList<QString> &links)
|
||||
{
|
||||
QMap<QString, QStringList> linksMap;
|
||||
|
||||
|
||||
if (m_filePath.isEmpty()) {
|
||||
qWarning("FigmaLinksSource::append - file path is not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_figmaLinks)
|
||||
linksMap = m_figmaLinks->getLinksMap();
|
||||
|
||||
linksMap[key].append(links);
|
||||
|
||||
FigmaIO::write(m_filePath.path(), linksMap);
|
||||
}
|
||||
|
||||
void FigmaLinksSource::updateFigmaLinks(const QMap<QString, QStringList>& map)
|
||||
{
|
||||
FigmaLinks *mapping = new FigmaLinks(map, this);
|
||||
|
||||
if (m_figmaLinks && qjsEngine(m_figmaLinks)) {
|
||||
m_figmaLinks->setParent(nullptr);
|
||||
QQmlEngine::setObjectOwnership(m_figmaLinks, QQmlEngine::JavaScriptOwnership);
|
||||
}
|
||||
|
||||
m_figmaLinks = mapping;
|
||||
emit figmaLinksChanged();
|
||||
}
|
||||
|
||||
void FigmaLinksSource::readFile()
|
||||
{
|
||||
QMap<QString, QStringList> figmaLinks = FigmaIO::read(m_filePath.path());
|
||||
updateFigmaLinks(figmaLinks);
|
||||
}
|
||||
|
||||
void FigmaLinksSource::setupWatcher()
|
||||
{
|
||||
auto currentlyWatched = m_watcher.files();
|
||||
|
||||
if (!currentlyWatched.isEmpty())
|
||||
m_watcher.removePaths(currentlyWatched);
|
||||
|
||||
if (m_filePath.isEmpty())
|
||||
return;
|
||||
|
||||
m_watcher.addPath(m_filePath.path());
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QUrl>
|
||||
|
||||
class FigmaLinks;
|
||||
|
||||
class FigmaLinksSource : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUrl filePath READ getFilePath WRITE setFilePath NOTIFY filePathChanged)
|
||||
Q_PROPERTY(FigmaLinks* figmaLinks READ getFigmaLinks NOTIFY figmaLinksChanged)
|
||||
|
||||
public:
|
||||
explicit FigmaLinksSource(QObject *parent = nullptr);
|
||||
|
||||
const QUrl& getFilePath() const;
|
||||
void setFilePath(const QUrl& path);
|
||||
FigmaLinks* getFigmaLinks() const;
|
||||
|
||||
Q_INVOKABLE void remove(const QString &key, const QList<int> &indexes);
|
||||
Q_INVOKABLE void append(const QString &key, const QList<QString> &links);
|
||||
|
||||
signals:
|
||||
void filePathChanged();
|
||||
void figmaLinksChanged();
|
||||
|
||||
private:
|
||||
void updateFigmaLinks(const QMap<QString, QStringList>& map);
|
||||
void readFile();
|
||||
void setupWatcher();
|
||||
|
||||
FigmaLinks *m_figmaLinks = nullptr;
|
||||
QUrl m_filePath;
|
||||
QFileSystemWatcher m_watcher;
|
||||
};
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
#include "cachecleaner.h"
|
||||
#include "directorieswatcher.h"
|
||||
#include "figmadecoratormodel.h"
|
||||
#include "figmalinks.h"
|
||||
#include "figmalinkssource.h"
|
||||
#include "pagesmodel.h"
|
||||
#include "sectionsdecoratormodel.h"
|
||||
|
||||
|
@ -46,8 +44,6 @@ int main(int argc, char *argv[])
|
|||
engine.rootContext()->setContextProperty(
|
||||
"pagesFolder", QML_IMPORT_ROOT + QStringLiteral("/pages"));
|
||||
|
||||
qmlRegisterType<FigmaDecoratorModel>("Storybook", 1, 0, "FigmaDecoratorModel");
|
||||
qmlRegisterType<FigmaLinksSource>("Storybook", 1, 0, "FigmaLinksSource");
|
||||
qmlRegisterType<PagesModelInitialized>("Storybook", 1, 0, "PagesModel");
|
||||
qmlRegisterType<SectionsDecoratorModel>("Storybook", 1, 0, "SectionsDecoratorModel");
|
||||
qmlRegisterUncreatableType<FigmaLinks>("Storybook", 1, 0, "FigmaLinks", {});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
|
@ -78,19 +78,6 @@ ApplicationWindow {
|
|||
id: pagesModel
|
||||
}
|
||||
|
||||
FigmaLinksSource {
|
||||
id: figmaLinksSource
|
||||
|
||||
filePath: "figma.json"
|
||||
}
|
||||
|
||||
FigmaDecoratorModel {
|
||||
id: figmaModel
|
||||
|
||||
sourceModel: pagesModel
|
||||
figmaLinks: figmaLinksSource.figmaLinks
|
||||
}
|
||||
|
||||
HotReloader {
|
||||
id: reloader
|
||||
|
||||
|
@ -212,7 +199,7 @@ ApplicationWindow {
|
|||
id: currentPageModelItem
|
||||
|
||||
model: SingleItemProxyModel {
|
||||
sourceModel: figmaModel
|
||||
sourceModel: pagesModel
|
||||
roleName: "title"
|
||||
value: root.currentPage
|
||||
}
|
||||
|
@ -320,9 +307,6 @@ Tips:
|
|||
figmaLinksCache: figmaImageLinksCache
|
||||
}
|
||||
|
||||
onRemoveLinksRequested: figmaLinksSource.remove(pageTitle, indexes)
|
||||
onAppendLinksRequested: figmaLinksSource.append(pageTitle, links)
|
||||
|
||||
onClosing: Qt.callLater(destroy)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
|
||||
import Storybook 1.0
|
||||
|
||||
|
@ -50,63 +50,13 @@ Pane {
|
|||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
ImagesGridView {
|
||||
id: grid
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
CheckBox {
|
||||
id: selectableCheckBox
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
text: "selectable"
|
||||
}
|
||||
|
||||
ToolSeparator {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
id: selectionText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
property string selectionAsString: ""
|
||||
|
||||
text: `selected indexes: [${selectionAsString}]`
|
||||
|
||||
Connections {
|
||||
target: grid.selection
|
||||
|
||||
function onSelectionChanged() {
|
||||
const indexes = grid.selection.selectedIndexes
|
||||
const rows = indexes.map(idx => idx.row)
|
||||
|
||||
selectionText.selectionAsString = rows.join(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Clear selection"
|
||||
|
||||
onClicked: grid.selection.clear()
|
||||
}
|
||||
}
|
||||
|
||||
ImagesGridView {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
selectable: selectableCheckBox.checked
|
||||
clip: true
|
||||
model: imagesModel
|
||||
}
|
||||
clip: true
|
||||
model: imagesModel
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include <QFileSystemWatcher>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
const auto categoryUncategorized QStringLiteral("Uncategorized");
|
||||
}
|
||||
|
@ -15,6 +17,10 @@ PagesModel::PagesModel(const QString &path, QObject *parent)
|
|||
m_items = load();
|
||||
readMetadata(m_items);
|
||||
|
||||
for (const auto& item : qAsConst(m_items)) {
|
||||
setFigmaLinks(item.title, item.figmaLinks);
|
||||
}
|
||||
|
||||
fsWatcher->addPath(path);
|
||||
|
||||
connect(fsWatcher, &QFileSystemWatcher::directoryChanged,
|
||||
|
@ -62,6 +68,21 @@ void PagesModel::readMetadata(PagesModelItem& item) {
|
|||
? categoryMatch.captured(2).trimmed() : categoryUncategorized;
|
||||
|
||||
item.category = category;
|
||||
|
||||
static QRegularExpression figmaRegex(
|
||||
"^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$",
|
||||
QRegularExpression::MultilineOption);
|
||||
|
||||
QRegularExpressionMatchIterator i = figmaRegex.globalMatch(content);
|
||||
QStringList links;
|
||||
|
||||
while (i.hasNext()) {
|
||||
QRegularExpressionMatch match = i.next();
|
||||
QString link = match.captured(2);
|
||||
links << link;
|
||||
}
|
||||
|
||||
item.figmaLinks = links;
|
||||
}
|
||||
|
||||
void PagesModel::readMetadata(QList<PagesModelItem> &items) {
|
||||
|
@ -129,6 +150,7 @@ void PagesModel::reload() {
|
|||
auto index = std::distance(m_items.begin(), it);
|
||||
const auto& previous = *it;
|
||||
readMetadata(item);
|
||||
setFigmaLinks(item.title, item.figmaLinks);
|
||||
|
||||
if (previous.category != item.category) {
|
||||
// For simplicity category change is handled by removing and
|
||||
|
@ -149,7 +171,8 @@ QHash<int, QByteArray> PagesModel::roleNames() const
|
|||
{
|
||||
static const QHash<int,QByteArray> roles {
|
||||
{ TitleRole, QByteArrayLiteral("title") },
|
||||
{ CategoryRole, QByteArrayLiteral("category") }
|
||||
{ CategoryRole, QByteArrayLiteral("category") },
|
||||
{ FigmaRole, QByteArrayLiteral("figma") }
|
||||
};
|
||||
|
||||
return roles;
|
||||
|
@ -167,8 +190,28 @@ QVariant PagesModel::data(const QModelIndex &index, int role) const
|
|||
|
||||
if (role == TitleRole)
|
||||
return m_items.at(index.row()).title;
|
||||
|
||||
if (role == CategoryRole)
|
||||
return m_items.at(index.row()).category;
|
||||
|
||||
if (role == FigmaRole) {
|
||||
auto title = m_items.at(index.row()).title;
|
||||
auto it = m_figmaSubmodels.find(title);
|
||||
assert(it != m_figmaSubmodels.end());
|
||||
|
||||
return QVariant::fromValue(it.value());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void PagesModel::setFigmaLinks(const QString& title, const QStringList& links)
|
||||
{
|
||||
auto it = m_figmaSubmodels.find(title);
|
||||
|
||||
if (it == m_figmaSubmodels.end()) {
|
||||
m_figmaSubmodels.insert(title, new FigmaLinksModel(links, this));
|
||||
} else {
|
||||
it.value()->setContent(links);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "figmalinksmodel.h"
|
||||
|
||||
class QFileSystemWatcher;
|
||||
|
||||
struct PagesModelItem {
|
||||
|
@ -10,6 +12,7 @@ struct PagesModelItem {
|
|||
QDateTime lastModified;
|
||||
QString title;
|
||||
QString category;
|
||||
QStringList figmaLinks;
|
||||
};
|
||||
|
||||
class PagesModel : public QAbstractListModel
|
||||
|
@ -20,7 +23,8 @@ public:
|
|||
|
||||
enum Roles {
|
||||
TitleRole = Qt::UserRole + 1,
|
||||
CategoryRole
|
||||
CategoryRole,
|
||||
FigmaRole
|
||||
};
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
@ -34,7 +38,10 @@ private:
|
|||
static void readMetadata(PagesModelItem &item);
|
||||
static void readMetadata(QList<PagesModelItem> &items);
|
||||
|
||||
void setFigmaLinks(const QString& title, const QStringList& links);
|
||||
|
||||
QString m_path;
|
||||
QList<PagesModelItem> m_items;
|
||||
QMap<QString, FigmaLinksModel*> m_figmaSubmodels;
|
||||
QFileSystemWatcher* fsWatcher;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick 2.15
|
||||
|
||||
ListModel {
|
||||
id: root
|
||||
|
||||
/* required */ property FigmaLinksCache figmaLinksCache
|
||||
required property FigmaLinksCache figmaLinksCache
|
||||
property alias sourceModel: d.model
|
||||
|
||||
readonly property Instantiator _d: Instantiator {
|
||||
|
@ -29,7 +29,7 @@ ListModel {
|
|||
d.idCounter++
|
||||
|
||||
figmaLinksCache.getImageUrl(model.link, link => {
|
||||
if (delegate)
|
||||
if (delegate && link !== null)
|
||||
root.setProperty(model.index, "imageLink", link)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import QtQml 2.14
|
||||
import QtQml 2.15
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
|
|
@ -14,10 +14,6 @@ ApplicationWindow {
|
|||
signal removeLinksRequested(var indexes)
|
||||
signal appendLinksRequested(var links)
|
||||
|
||||
readonly property var urlRegex:
|
||||
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/
|
||||
|
||||
|
||||
SwipeView {
|
||||
id: topSwipeView
|
||||
|
||||
|
@ -40,37 +36,6 @@ ApplicationWindow {
|
|||
topSwipeView.incrementCurrentIndex()
|
||||
}
|
||||
}
|
||||
|
||||
footer: ToolBar {
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Button {
|
||||
id: removeButton
|
||||
|
||||
readonly property int selectionCount:
|
||||
grid.selection.selectedIndexes.length
|
||||
|
||||
text: "Remove selected"
|
||||
+ (enabled ? ` (${selectionCount})` : "")
|
||||
enabled: grid.selection.hasSelection
|
||||
|
||||
onClicked: removeConfirmDialog.open()
|
||||
}
|
||||
|
||||
ToolSeparator {}
|
||||
|
||||
Button {
|
||||
text: "Add new links"
|
||||
|
||||
onClicked: addNewLinksDialog.open()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -106,81 +71,4 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: removeConfirmDialog
|
||||
|
||||
readonly property var selected: grid.selection.selectedIndexes
|
||||
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
title: "Links removal"
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
Label {
|
||||
text: "Are you sure that you want to remove "
|
||||
+ removeButton.selectionCount + " link(s)?"
|
||||
}
|
||||
|
||||
onAccepted: root.removeLinksRequested(selected.map(idx => idx.row))
|
||||
onSelectedChanged: close()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: addNewLinksDialog
|
||||
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
title: "Add new Figma links"
|
||||
standardButtons: Dialog.Save | Dialog.Cancel
|
||||
|
||||
width: parent.width * 0.8
|
||||
height: parent.height * 0.4
|
||||
|
||||
GroupBox {
|
||||
anchors.fill: parent
|
||||
|
||||
title: "Figma links, 1 per line"
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
contentHeight: linksTextEdit.implicitHeight
|
||||
contentWidth: linksTextEdit.implicitWidth
|
||||
|
||||
TextEdit {
|
||||
id: linksTextEdit
|
||||
|
||||
property var links: []
|
||||
|
||||
width: scrollView.width
|
||||
height: scrollView.height
|
||||
|
||||
font.pixelSize: 13
|
||||
selectByMouse: true
|
||||
|
||||
onTextChanged: {
|
||||
const allLines = text.split("\n")
|
||||
const nonEmptyLines = allLines.filter(
|
||||
line => line.trim().length > 0)
|
||||
const trimmed = nonEmptyLines.map(line => line.trim())
|
||||
|
||||
links = trimmed.every(line => root.urlRegex.test(line))
|
||||
? trimmed : []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: Qt.callLater(linksTextEdit.clear)
|
||||
onAccepted: root.appendLinksRequested(linksTextEdit.links)
|
||||
|
||||
Component.onCompleted: {
|
||||
standardButton(Dialog.Save).enabled
|
||||
= Qt.binding(() => linksTextEdit.links.length > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQml.Models 2.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQml.Models 2.15
|
||||
|
||||
GridView {
|
||||
id: root
|
||||
|
@ -10,15 +10,6 @@ GridView {
|
|||
|
||||
signal clicked(int index)
|
||||
|
||||
property bool selectable: true
|
||||
readonly property alias selection: selectionModel
|
||||
|
||||
ItemSelectionModel {
|
||||
id: selectionModel
|
||||
|
||||
model: root.model
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
width: root.cellWidth
|
||||
height: root.cellHeight
|
||||
|
@ -60,21 +51,6 @@ GridView {
|
|||
ToolTip.visible: hovered
|
||||
ToolTip.text: model.rawLink
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
visible: root.selectable
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
|
||||
checked: {
|
||||
selectionModel.selection
|
||||
return selectionModel.isSelected(root.model.index(index, 0))
|
||||
}
|
||||
|
||||
onToggled: selectionModel.select(root.model.index(index, 0),
|
||||
ItemSelectionModel.Toggle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,341 +0,0 @@
|
|||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QTemporaryFile>
|
||||
#include <QStringListModel>
|
||||
|
||||
#include "figmadecoratormodel.h"
|
||||
#include "figmaio.h"
|
||||
#include "figmalinks.h"
|
||||
#include "figmalinksmodel.h"
|
||||
#include "figmalinkssource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
auto constexpr sampleJson1 = R"(
|
||||
{
|
||||
"Component_1": [
|
||||
"link_1", "link_2"
|
||||
],
|
||||
"Component_2": [
|
||||
"link_3", "link_4"
|
||||
]
|
||||
}
|
||||
)";
|
||||
|
||||
auto constexpr sampleJson2 = R"(
|
||||
{
|
||||
"Component_1": [
|
||||
"link_1"
|
||||
],
|
||||
"Component_2": [
|
||||
"link_3", "link_5"
|
||||
]
|
||||
}
|
||||
)";
|
||||
|
||||
class TestSourceModel : public QAbstractListModel {
|
||||
|
||||
public:
|
||||
static constexpr auto TitleRole = 0;
|
||||
|
||||
TestSourceModel(int count = 1) : m_count(count) {}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
|
||||
return m_count;
|
||||
}
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override {
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
return QString("title_%1").arg(index.row());
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(TitleRole, QByteArrayLiteral("title"));
|
||||
return roles;
|
||||
}
|
||||
|
||||
int m_count;
|
||||
};
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
class FigmaDecoratorModelTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void figmaIOTest() {
|
||||
QTemporaryFile file;
|
||||
QVERIFY(file.open());
|
||||
file.close();
|
||||
|
||||
const auto readEmpty = FigmaIO::read(file.fileName());
|
||||
QCOMPARE(readEmpty, {});
|
||||
|
||||
const QMap<QString, QStringList> content = {
|
||||
{ "k_1", { "l_1", "l_2"}},
|
||||
{ "k_2", { "l_3", "l_4"}}
|
||||
};
|
||||
|
||||
FigmaIO::write(file.fileName(), content);
|
||||
const auto readContent = FigmaIO::read(file.fileName());
|
||||
|
||||
QCOMPARE(readContent, content);
|
||||
}
|
||||
|
||||
void readingFigmaFileTest() {
|
||||
FigmaLinksSource figmaLinksSource;
|
||||
|
||||
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
|
||||
|
||||
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
|
||||
|
||||
QTemporaryFile file;
|
||||
if (file.open()) {
|
||||
QTextStream stream(&file);
|
||||
stream << sampleJson1;
|
||||
}
|
||||
|
||||
figmaLinksSource.setFilePath(file.fileName());
|
||||
|
||||
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
|
||||
|
||||
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
|
||||
|
||||
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
|
||||
{{"Component_1"}, {"link_1", "link_2"}},
|
||||
{{"Component_2"}, {"link_3", "link_4"}}}));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
QTemporaryFile file2;
|
||||
if (file2.open()) {
|
||||
QTextStream stream(&file2);
|
||||
stream << sampleJson2;
|
||||
}
|
||||
|
||||
figmaLinksSource.setFilePath(file2.fileName());
|
||||
|
||||
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
|
||||
|
||||
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
|
||||
|
||||
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
|
||||
{{"Component_1"}, {"link_1"}},
|
||||
{{"Component_2"}, {"link_3", "link_5"}}}));
|
||||
QCOMPARE(spy.count(), 2);
|
||||
}
|
||||
|
||||
void readingAfterFigmaFileChangedTest() {
|
||||
|
||||
FigmaLinksSource figmaLinksSource;
|
||||
|
||||
QSignalSpy spy(&figmaLinksSource, &FigmaLinksSource::figmaLinksChanged);
|
||||
|
||||
QCOMPARE(figmaLinksSource.getFigmaLinks(), nullptr);
|
||||
|
||||
QTemporaryFile file;
|
||||
if (file.open()) {
|
||||
QTextStream stream(&file);
|
||||
stream << sampleJson1;
|
||||
}
|
||||
|
||||
figmaLinksSource.setFilePath(file.fileName());
|
||||
|
||||
QVERIFY(figmaLinksSource.getFigmaLinks() != nullptr);
|
||||
|
||||
const FigmaLinks *links = figmaLinksSource.getFigmaLinks();
|
||||
|
||||
QCOMPARE(links->getLinksMap(), (QMap<QString, QStringList> {
|
||||
{{"Component_1"}, {"link_1", "link_2"}},
|
||||
{{"Component_2"}, {"link_3", "link_4"}}}));
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
if (QFile f(file.fileName());
|
||||
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
QTextStream stream(&f);
|
||||
stream << sampleJson2;
|
||||
}
|
||||
|
||||
QVERIFY(spy.wait());
|
||||
QCOMPARE(spy.count(), 2);
|
||||
|
||||
const FigmaLinks *links2 = figmaLinksSource.getFigmaLinks();
|
||||
|
||||
QCOMPARE(links2->getLinksMap(), (QMap<QString, QStringList> {
|
||||
{{"Component_1"}, {"link_1"}},
|
||||
{{"Component_2"}, {"link_3", "link_5"}}}));
|
||||
|
||||
|
||||
if (QFile f(file.fileName());
|
||||
f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||
QTextStream stream(&f);
|
||||
stream << sampleJson1;
|
||||
}
|
||||
|
||||
QVERIFY(spy.wait());
|
||||
QCOMPARE(spy.count(), 3);
|
||||
|
||||
const FigmaLinks *links3 = figmaLinksSource.getFigmaLinks();
|
||||
|
||||
QCOMPARE(links3->getLinksMap(), (QMap<QString, QStringList> {
|
||||
{{"Component_1"}, {"link_1", "link_2"}},
|
||||
{{"Component_2"}, {"link_3", "link_4"}}}));
|
||||
}
|
||||
|
||||
void emptyFigmaModelTest() {
|
||||
FigmaDecoratorModel model;
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
|
||||
QCOMPARE(model.roleNames().value(
|
||||
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
|
||||
}
|
||||
|
||||
void figmaModelWithoutSourceModel() {
|
||||
FigmaLinks links({
|
||||
{{"Component_1"}, {"link_1", "link_2"}},
|
||||
{{"Component_2"}, {"link_3", "link_4"}}
|
||||
});
|
||||
|
||||
FigmaDecoratorModel model;
|
||||
model.setFigmaLinks(&links);
|
||||
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
|
||||
QCOMPARE(model.roleNames().value(
|
||||
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
|
||||
}
|
||||
|
||||
void figmaModelWithIncompatibleSourceModel() {
|
||||
QStringListModel stringsModel({"s1", "s2"});
|
||||
FigmaDecoratorModel model;
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg,
|
||||
"The source model is missing title role!");
|
||||
model.setSourceModel(&stringsModel);
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
|
||||
QCOMPARE(model.roleNames().value(
|
||||
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
|
||||
|
||||
QCOMPARE(model.data(
|
||||
model.index(0, 0),
|
||||
FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>()->rowCount(), 0);
|
||||
}
|
||||
|
||||
void figmaModelWithoutLinksTest() {
|
||||
TestSourceModel sourceModel;
|
||||
FigmaDecoratorModel model;
|
||||
|
||||
model.setSourceModel(&sourceModel);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.roleNames().count(), 2);
|
||||
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
|
||||
QCOMPARE(model.roleNames().value(
|
||||
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
|
||||
|
||||
QCOMPARE(model.data(
|
||||
model.index(0, 0),
|
||||
FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>()->rowCount(), 0);
|
||||
}
|
||||
|
||||
void figmaModelTest() {
|
||||
TestSourceModel sourceModel{2};
|
||||
FigmaLinks links({
|
||||
{{"title_0"}, {"link_1", "link_2"}},
|
||||
{{"title_x"}, {"link_3", "link_4"}}
|
||||
});
|
||||
|
||||
FigmaDecoratorModel model;
|
||||
|
||||
QSignalSpy spy(&model, &FigmaDecoratorModel::dataChanged);
|
||||
|
||||
model.setSourceModel(&sourceModel);
|
||||
|
||||
QCOMPARE(spy.size(), 0);
|
||||
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QCOMPARE(model.roleNames().count(), 2);
|
||||
QVERIFY(model.roleNames().contains(FigmaDecoratorModel::FigmaRole));
|
||||
QVERIFY(model.roleNames().contains(TestSourceModel::TitleRole));
|
||||
|
||||
QCOMPARE(model.roleNames().value(
|
||||
FigmaDecoratorModel::FigmaRole), QStringLiteral("figma"));
|
||||
QCOMPARE(model.roleNames().value(
|
||||
TestSourceModel::TitleRole), QStringLiteral("title"));
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0),
|
||||
TestSourceModel::TitleRole).toString(), "title_0");
|
||||
QCOMPARE(model.data(model.index(1, 0),
|
||||
TestSourceModel::TitleRole).toString(), "title_1");
|
||||
|
||||
auto figmaLinksModel1 = model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>();
|
||||
|
||||
QVERIFY(figmaLinksModel1 != nullptr);
|
||||
QCOMPARE(figmaLinksModel1->rowCount(), 0);
|
||||
|
||||
auto figmaLinksModel2 = model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>();
|
||||
|
||||
QVERIFY(figmaLinksModel2 != nullptr);
|
||||
QCOMPARE(figmaLinksModel2->rowCount(), 0);
|
||||
|
||||
QSignalSpy linksModelspy1(figmaLinksModel1,
|
||||
&QAbstractItemModel::modelReset);
|
||||
QSignalSpy linksModelspy2(figmaLinksModel2,
|
||||
&QAbstractItemModel::modelReset);
|
||||
|
||||
model.setFigmaLinks(&links);
|
||||
|
||||
QCOMPARE(spy.size(), 0);
|
||||
|
||||
QCOMPARE(linksModelspy1.size(), 1);
|
||||
QCOMPARE(linksModelspy2.size(), 0);
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>(), figmaLinksModel1);
|
||||
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>(), figmaLinksModel2);
|
||||
|
||||
QCOMPARE(figmaLinksModel1->rowCount(), 2);
|
||||
QCOMPARE(figmaLinksModel2->rowCount(), 0);
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0),
|
||||
TestSourceModel::TitleRole).toString(), "title_0");
|
||||
QCOMPARE(model.data(model.index(1, 0),
|
||||
TestSourceModel::TitleRole).toString(), "title_1");
|
||||
|
||||
QCOMPARE(figmaLinksModel1->roleNames().size(), 1);
|
||||
QCOMPARE(figmaLinksModel2->roleNames().size(), 1);
|
||||
|
||||
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(0, 0),
|
||||
FigmaLinksModel::LinkRole).toString(), "link_1");
|
||||
QCOMPARE(figmaLinksModel1->data(figmaLinksModel1->index(1, 0),
|
||||
FigmaLinksModel::LinkRole).toString(), "link_2");
|
||||
|
||||
model.setFigmaLinks(nullptr);
|
||||
|
||||
QCOMPARE(spy.size(), 0);
|
||||
QCOMPARE(linksModelspy1.size(), 2);
|
||||
QCOMPARE(linksModelspy2.size(), 0);
|
||||
|
||||
QCOMPARE(model.data(model.index(0, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>(), figmaLinksModel1);
|
||||
QCOMPARE(model.data(model.index(1, 0), FigmaDecoratorModel::FigmaRole)
|
||||
.value<QAbstractItemModel*>(), figmaLinksModel2);
|
||||
|
||||
QCOMPARE(figmaLinksModel1->rowCount(), 0);
|
||||
QCOMPARE(figmaLinksModel2->rowCount(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(FigmaDecoratorModelTest)
|
||||
#include "tst_FigmaDecoratorModel.moc"
|
Loading…
Reference in New Issue