Skip to content

Commit 7104486

Browse files
committed
feat: [WIP] Form.TagPicker support
1 parent 47aaa94 commit 7104486

19 files changed

Lines changed: 576 additions & 16 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ __cmake*
1616
*.tgz
1717

1818
/qt6
19+
**/.qmlls.ini
1920

2021
/AppDir
2122
*AppImage

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
cmake_minimum_required(VERSION 3.16)
22
set(PROJECT_NAME vicinae)
33
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
4+
set(QT_QML_GENERATE_QMLLS_INI ON)
45
project(${PROJECT_NAME} VERSION 1.0.0 LANGUAGES C CXX)
56

67
set(LIB_NAMESPACE "vicinae")

flake.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
export CXX=${pkgs.gcc15}/bin/g++
6464
export CMAKE_C_COMPILER=$CC
6565
export CMAKE_CXX_COMPILER=$CXX
66+
67+
export QML2_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
68+
export QML_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
6669
'';
6770
};
6871
});

src/server/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ set(SRCS
504504
src/qml/extension-grid-model.hpp
505505
src/qml/extension-grid-model.cpp
506506
src/qml/source-blend-rect.hpp
507+
src/qml/tag-search.hpp
507508
src/qml/image-url.hpp
508509
src/qml/image-url.cpp
509510
src/qml/image-source.hpp
@@ -704,6 +705,7 @@ set(VICINAE_QML_FILES
704705
src/qml/qml/FormTextArea.qml
705706
src/qml/qml/FormCheckbox.qml
706707
src/qml/qml/FormSeparator.qml
708+
src/qml/qml/FormTagPicker.qml
707709
src/qml/qml/SnippetFormView.qml
708710
src/qml/qml/AliasFormView.qml
709711
src/qml/qml/ShortcutFormView.qml
@@ -754,6 +756,7 @@ set(VICINAE_QML_FILES
754756
src/qml/qml/MissingPreferenceView.qml
755757
src/qml/qml/OAuthOverlayView.qml
756758
src/qml/qml/HudWindow.qml
759+
src/qml/qml/TagPickerTag.qml
757760
)
758761

759762
# Strip directory prefix so resources are qrc:/Vicinae/Foo.qml

src/server/include/extend/form-model.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "extend/action-model.hpp"
33
#include "extend/list-model.hpp"
44
#include "extend/model.hpp"
5+
#include "extend/tag-model.hpp"
56
#include <memory>
67
#include <qcoreevent.h>
78
#include <qjsonobject.h>
@@ -97,6 +98,14 @@ struct FormModel {
9798
public:
9899
DatePickerField(const FieldBase &base) : IField(base) {}
99100
};
101+
102+
struct TagPickerField : public IField {
103+
std::vector<TagPickerItemModel> m_items;
104+
105+
public:
106+
TagPickerField(const FieldBase &base) : IField(base) {}
107+
};
108+
100109
struct InvalidField : public FieldBase {};
101110

102111
struct Separator {};

src/server/include/extend/tag-model.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include "extend/color-model.hpp"
33
#include "extend/image-model.hpp"
4+
#include "lib/fuzzy/fuzzy-searchable.hpp"
45
#include <qjsonobject.h>
56
#include <qstring.h>
67

@@ -24,3 +25,19 @@ class TagListParser {
2425

2526
TagListModel parse(const QJsonObject &instance);
2627
};
28+
29+
struct TagPickerItemModel {
30+
QString title;
31+
QString value;
32+
std::optional<ImageLikeModel> icon;
33+
34+
public:
35+
static TagPickerItemModel fromJson(const QJsonObject &instance);
36+
};
37+
38+
template <> struct fuzzy::FuzzySearchable<TagPickerItemModel> {
39+
static int score(const TagPickerItemModel &item, std::string_view query) {
40+
auto name = item.title.toStdString();
41+
return fuzzy::scoreWeighted({{name, 1.0}}, query);
42+
}
43+
};

src/server/src/extend/form-model.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
#include <algorithm>
55

6-
const static std::vector<QString> fieldTypes = {"dropdown-field", "password-field", "text-field",
7-
"checkbox-field", "date-picker-field", "text-area-field",
8-
"file-picker-field"};
6+
const static std::vector<QString> fieldTypes = {"dropdown-field", "password-field", "text-field",
7+
"checkbox-field", "date-picker-field", "text-area-field",
8+
"file-picker-field", "tag-picker-field"};
99

1010
FormModel FormModel::fromJson(const QJsonObject &json) {
1111
FormModel model;
@@ -123,6 +123,15 @@ FormModel FormModel::fromJson(const QJsonObject &json) {
123123
filePicker->showHiddenFiles = props.value("showHiddenFiles").toBool();
124124

125125
model.items.emplace_back(filePicker);
126+
} else if (*it == "tag-picker-field") {
127+
auto tagPicker = std::make_shared<TagPickerField>(base);
128+
tagPicker->m_items.reserve(children.size());
129+
130+
for (const auto &child : children) {
131+
tagPicker->m_items.emplace_back(TagPickerItemModel::fromJson(child.toObject()));
132+
}
133+
134+
model.items.emplace_back(tagPicker);
126135
}
127136
} else {
128137
qWarning() << "Unknown form children of type" << type;

src/server/src/extend/tag-list.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#include "extend/color-model.hpp"
22
#include "extend/image-model.hpp"
33
#include "extend/tag-model.hpp"
4+
#include <exception>
45
#include <qjsonarray.h>
56
#include <qjsonobject.h>
7+
#include <qlogging.h>
8+
#include <stdexcept>
69

710
TagListParser::TagListParser() = default;
811

@@ -32,3 +35,15 @@ TagListModel TagListParser::parse(const QJsonObject &instance) {
3235

3336
return model;
3437
}
38+
39+
TagPickerItemModel TagPickerItemModel::fromJson(const QJsonObject &instance) {
40+
TagPickerItemModel model;
41+
auto props = instance.value("props").toObject();
42+
43+
model.title = props.value("title").toString();
44+
model.value = props.value("value").toString();
45+
46+
if (props.contains("icon")) { model.icon = ImageModelParser().parse(props.value("icon")); }
47+
48+
return model;
49+
}

src/server/src/qml/extension-form-model.cpp

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#include "view-utils.hpp"
33
#include <QJsonArray>
44
#include <QJsonObject>
5+
#include <QJSValue>
6+
#include <qcontainerfwd.h>
7+
#include <qstringliteral.h>
58
#include <utility>
69

710
ExtensionFormModel::ExtensionFormModel(NotifyFn notify, QObject *parent)
@@ -54,7 +57,14 @@ void ExtensionFormModel::setFieldValue(int index, const QVariant &value) {
5457
auto &item = m_items[index];
5558
if (!item.isField()) return;
5659

57-
item.userValue = QJsonValue::fromVariant(value);
60+
// TODO: find out why Form.TagPicker tags would break without that
61+
QVariant normalizedValue = value;
62+
63+
if (value.metaType().id() == qMetaTypeId<QJSValue>()) {
64+
normalizedValue = value.value<QJSValue>().toVariant();
65+
}
66+
67+
item.userValue = QJsonValue::fromVariant(normalizedValue);
5868
item.hasUserValue = true;
5969
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole});
6070

@@ -90,6 +100,23 @@ void ExtensionFormModel::setFilePaths(int index, const QVariantList &paths) {
90100
if (item.onChange) { m_notify(*item.onChange, QJsonArray{arr}); }
91101
}
92102

103+
void ExtensionFormModel::setPickedItems(int index, const QVariantList &pickedItems) {
104+
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
105+
auto &item = m_items[index];
106+
if (item.type != FormItemData::Type::TagPicker) return;
107+
108+
QJsonArray arr;
109+
for (const auto &t : pickedItems) {
110+
arr.append(t.toString());
111+
}
112+
113+
item.userValue = arr;
114+
item.hasUserValue = true;
115+
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole});
116+
117+
if (item.onChange) { m_notify(*item.onChange, QJsonArray{arr}); }
118+
}
119+
93120
void ExtensionFormModel::dropdownSearchTextChanged(int index, const QString &text) {
94121
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
95122
const auto &item = m_items[index];
@@ -187,10 +214,16 @@ ExtensionFormModel::FormItemData ExtensionFormModel::createItem(const FormModel:
187214
data.onFocus = field->onFocus;
188215
data.fieldData = buildFieldData(*field);
189216

190-
if (field->value) {
217+
if (field->value && !(data.type == FormItemData::Type::TagPicker && field->value->isNull())) {
191218
data.modelValue = *field->value;
192219
} else if (field->defaultValue) {
193220
data.modelValue = *field->defaultValue;
221+
} else if (data.type == FormItemData::Type::Dropdown) {
222+
if (auto *df = dynamic_cast<const FormModel::DropdownField *>(field.get())) {
223+
if (auto first = qml::firstDropdownItemValue(df->m_items)) { data.modelValue = *first; }
224+
}
225+
} else if (data.type == FormItemData::Type::TagPicker) {
226+
data.modelValue = QJsonArray{};
194227
}
195228
} else if (auto *desc = std::get_if<FormModel::Description>(&item)) {
196229
data.type = FormItemData::Type::Description;
@@ -216,8 +249,11 @@ void ExtensionFormModel::updateItem(FormItemData &existing, const FormModel::Ite
216249
existing.fieldData = buildFieldData(*field);
217250

218251
if (field->value) {
219-
existing.modelValue = *field->value;
220-
existing.hasUserValue = false;
252+
bool const isNullTagValue = existing.type == FormItemData::Type::TagPicker && field->value->isNull();
253+
if (!isNullTagValue) {
254+
existing.modelValue = *field->value;
255+
existing.hasUserValue = false;
256+
}
221257
}
222258
} else if (auto *desc = std::get_if<FormModel::Description>(&newItem)) {
223259
existing.label = desc->title.value_or(QString());
@@ -233,6 +269,7 @@ ExtensionFormModel::FormItemData::Type ExtensionFormModel::fieldType(const FormM
233269
if (dynamic_cast<const FormModel::TextAreaField *>(&field)) return FormItemData::Type::TextArea;
234270
if (dynamic_cast<const FormModel::FilePickerField *>(&field)) return FormItemData::Type::FilePicker;
235271
if (dynamic_cast<const FormModel::DatePickerField *>(&field)) return FormItemData::Type::DatePicker;
272+
if (dynamic_cast<const FormModel::TagPickerField *>(&field)) return FormItemData::Type::TagPicker;
236273
return FormItemData::Type::Text;
237274
}
238275

@@ -257,6 +294,8 @@ QVariantMap ExtensionFormModel::buildFieldData(const FormModel::IField &field) {
257294
if (f->min) data["min"] = *f->min;
258295
if (f->max) data["max"] = *f->max;
259296
data["includeTime"] = f->type.value_or("date") == "dateTime";
297+
} else if (auto *f = dynamic_cast<const FormModel::TagPickerField *>(&field)) {
298+
data["items"] = qml::convertTagPickerItems(f->m_items);
260299
}
261300

262301
return data;
@@ -282,6 +321,8 @@ QString ExtensionFormModel::FormItemData::typeString() const {
282321
return QStringLiteral("description");
283322
case Type::Separator:
284323
return QStringLiteral("separator");
324+
case Type::TagPicker:
325+
return QStringLiteral("tagpicker");
285326
}
286327
return QStringLiteral("text");
287328
}

src/server/src/qml/extension-form-model.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include <QVariantMap>
66
#include <expected>
77
#include <functional>
8+
#include <qcontainerfwd.h>
9+
#include <qtmetamacros.h>
810
#include <vector>
911

1012
class ExtensionFormModel : public QAbstractListModel {
@@ -38,6 +40,7 @@ class ExtensionFormModel : public QAbstractListModel {
3840
Q_INVOKABLE void fieldFocused(int index);
3941
Q_INVOKABLE void fieldBlurred(int index);
4042
Q_INVOKABLE void setFilePaths(int index, const QVariantList &paths);
43+
Q_INVOKABLE void setPickedItems(int index, const QVariantList &pickedItems);
4144
Q_INVOKABLE void dropdownSearchTextChanged(int index, const QString &text);
4245

4346
void setFormData(const FormModel &model);
@@ -53,6 +56,7 @@ class ExtensionFormModel : public QAbstractListModel {
5356
TextArea,
5457
FilePicker,
5558
DatePicker,
59+
TagPicker,
5660
Description,
5761
Separator
5862
};

0 commit comments

Comments
 (0)