Skip to content

Commit a09c9c0

Browse files
committed
feat: [WIP] Form.TagPicker support
1 parent 9c7d53c commit a09c9c0

17 files changed

Lines changed: 670 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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,12 @@
6868
export CXX=${pkgs.gcc15}/bin/g++
6969
export CMAKE_C_COMPILER=$CC
7070
export CMAKE_CXX_COMPILER=$CXX
71+
72+
export QML2_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
73+
export QML_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
7174
'';
72-
};
73-
}
74-
);
75+
};
76+
});
7577
overlays.default = final: prev: {
7678
vicinae = final.callPackage ./nix/vicinae.nix { };
7779
mkVicinaeExtension = prev.callPackage ./nix/mkVicinaeExtension.nix { };

src/server/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,8 @@ set(SRCS
500500
src/qml/missing-preference-view-host.cpp
501501
src/qml/extension-form-model.hpp
502502
src/qml/extension-form-model.cpp
503+
src/qml/form-tag-picker-suggestions-model.hpp
504+
src/qml/form-tag-picker-suggestions-model.cpp
503505
src/qml/clipboard-history-model.hpp
504506
src/qml/clipboard-history-model.cpp
505507
src/qml/clipboard-history-view-host.hpp
@@ -719,6 +721,7 @@ set(VICINAE_QML_FILES
719721
src/qml/qml/FormTextArea.qml
720722
src/qml/qml/FormCheckbox.qml
721723
src/qml/qml/FormSeparator.qml
724+
src/qml/qml/FormTagPicker.qml
722725
src/qml/qml/SnippetFormView.qml
723726
src/qml/qml/AliasFormView.qml
724727
src/qml/qml/ShortcutFormView.qml
@@ -772,6 +775,7 @@ set(VICINAE_QML_FILES
772775
src/qml/qml/MissingPreferenceView.qml
773776
src/qml/qml/OAuthOverlayView.qml
774777
src/qml/qml/HudWindow.qml
778+
src/qml/qml/TagPickerTag.qml
775779
)
776780

777781
if(CMAKE_BUILD_TYPE MATCHES "Debug")

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: 73 additions & 5 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

@@ -99,13 +109,38 @@ void ExtensionFormModel::setFilePaths(int index, const QVariantList &paths) {
99109
if (item.onChange) { m_notify(*item.onChange, QJsonArray{arr}); }
100110
}
101111

112+
void ExtensionFormModel::setPickedItems(int index, const QVariantList &pickedItems) {
113+
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
114+
auto &item = m_items[index];
115+
if (item.type != FormItemData::Type::TagPicker) return;
116+
117+
QJsonArray arr;
118+
for (const auto &t : pickedItems) {
119+
arr.append(t.toString());
120+
}
121+
122+
item.userValue = arr;
123+
item.hasUserValue = true;
124+
syncTagPickerSuggestionsModel(item);
125+
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole});
126+
127+
if (item.onChange) { m_notify(*item.onChange, QJsonArray{arr}); }
128+
}
129+
102130
void ExtensionFormModel::dropdownSearchTextChanged(int index, const QString &text) {
103131
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
104132
const auto &item = m_items[index];
105133
auto handler = item.fieldData.value("onSearchTextChange").toString();
106134
if (!handler.isEmpty()) { m_notify(handler, QJsonArray{text}); }
107135
}
108136

137+
void ExtensionFormModel::tagPickerSearchTextChanged(int index, const QString &text) {
138+
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
139+
auto &item = m_items[index];
140+
if (item.type != FormItemData::Type::TagPicker || !item.tagPickerSuggestionsModel) return;
141+
item.tagPickerSuggestionsModel->setFilter(text);
142+
}
143+
109144
std::optional<FileChooserOptions> ExtensionFormModel::filePickerOptions(int index) const {
110145
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return std::nullopt;
111146
const auto &item = m_items[index];
@@ -146,6 +181,7 @@ void ExtensionFormModel::setFormData(const FormModel &model) {
146181
if (it != savedValues.end()) {
147182
newData.userValue = it->second;
148183
newData.hasUserValue = true;
184+
syncTagPickerSuggestionsModel(newData);
149185
}
150186
}
151187
m_items[i] = std::move(newData);
@@ -162,6 +198,7 @@ void ExtensionFormModel::setFormData(const FormModel &model) {
162198
if (it != savedValues.end()) {
163199
newData.userValue = it->second;
164200
newData.hasUserValue = true;
201+
syncTagPickerSuggestionsModel(newData);
165202
}
166203
}
167204
m_items.push_back(std::move(newData));
@@ -207,13 +244,14 @@ ExtensionFormModel::FormItemData ExtensionFormModel::createItem(const FormModel:
207244
data.onChange = field->onChange;
208245
data.onBlur = field->onBlur;
209246
data.onFocus = field->onFocus;
210-
data.fieldData = buildFieldData(*field);
211247

212248
if (field->value) {
213249
data.modelValue = *field->value;
214250
} else if (field->defaultValue) {
215251
data.modelValue = *field->defaultValue;
216252
}
253+
254+
updateFieldData(data, *field);
217255
} else if (auto *desc = std::get_if<FormModel::Description>(&item)) {
218256
data.type = FormItemData::Type::Description;
219257
data.label = desc->title.value_or(QString());
@@ -235,12 +273,13 @@ void ExtensionFormModel::updateItem(FormItemData &existing, const FormModel::Ite
235273
existing.onChange = field->onChange;
236274
existing.onBlur = field->onBlur;
237275
existing.onFocus = field->onFocus;
238-
existing.fieldData = buildFieldData(*field);
239276

240277
if (field->value) {
241278
existing.modelValue = *field->value;
242279
existing.hasUserValue = false;
243280
}
281+
282+
updateFieldData(existing, *field);
244283
} else if (auto *desc = std::get_if<FormModel::Description>(&newItem)) {
245284
existing.label = desc->title.value_or(QString());
246285
existing.fieldData = {{"text", desc->text}};
@@ -255,10 +294,11 @@ ExtensionFormModel::FormItemData::Type ExtensionFormModel::fieldType(const FormM
255294
if (dynamic_cast<const FormModel::TextAreaField *>(&field)) return FormItemData::Type::TextArea;
256295
if (dynamic_cast<const FormModel::FilePickerField *>(&field)) return FormItemData::Type::FilePicker;
257296
if (dynamic_cast<const FormModel::DatePickerField *>(&field)) return FormItemData::Type::DatePicker;
297+
if (dynamic_cast<const FormModel::TagPickerField *>(&field)) return FormItemData::Type::TagPicker;
258298
return FormItemData::Type::Text;
259299
}
260300

261-
QVariantMap ExtensionFormModel::buildFieldData(const FormModel::IField &field) {
301+
void ExtensionFormModel::updateFieldData(FormItemData &item, const FormModel::IField &field) const {
262302
QVariantMap data;
263303

264304
if (auto *f = dynamic_cast<const FormModel::CheckboxField *>(&field)) {
@@ -280,9 +320,35 @@ QVariantMap ExtensionFormModel::buildFieldData(const FormModel::IField &field) {
280320
if (f->min) data["min"] = *f->min;
281321
if (f->max) data["max"] = *f->max;
282322
data["includeTime"] = f->type.value_or("date") == "dateTime";
323+
} else if (auto *f = dynamic_cast<const FormModel::TagPickerField *>(&field)) {
324+
data["items"] = qml::convertTagPickerItems(f->m_items);
325+
if (!item.tagPickerSuggestionsModel) {
326+
item.tagPickerSuggestionsModel = std::make_unique<FormTagPickerSuggestionsModel>();
327+
}
328+
item.tagPickerSuggestionsModel->setSourceItems(f->m_items);
329+
syncTagPickerSuggestionsModel(item);
330+
data["suggestionsModel"] =
331+
QVariant::fromValue(static_cast<QObject *>(item.tagPickerSuggestionsModel.get()));
283332
}
284333

285-
return data;
334+
item.fieldData = std::move(data);
335+
}
336+
337+
void ExtensionFormModel::syncTagPickerSuggestionsModel(FormItemData &item) const {
338+
if (!item.tagPickerSuggestionsModel) return;
339+
item.tagPickerSuggestionsModel->setPickedItems(tagPickerPickedItems(item.effectiveValue()));
340+
}
341+
342+
QStringList ExtensionFormModel::tagPickerPickedItems(const QJsonValue &value) {
343+
QStringList result;
344+
if (!value.isArray()) return result;
345+
346+
auto const arr = value.toArray();
347+
result.reserve(arr.size());
348+
for (const auto &entry : arr) {
349+
if (entry.isString()) { result.append(entry.toString()); }
350+
}
351+
return result;
286352
}
287353

288354
QString ExtensionFormModel::FormItemData::typeString() const {
@@ -305,6 +371,8 @@ QString ExtensionFormModel::FormItemData::typeString() const {
305371
return QStringLiteral("description");
306372
case Type::Separator:
307373
return QStringLiteral("separator");
374+
case Type::TagPicker:
375+
return QStringLiteral("tagpicker");
308376
}
309377
return QStringLiteral("text");
310378
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#pragma once
22
#include "extend/form-model.hpp"
3+
#include "form-tag-picker-suggestions-model.hpp"
34
#include "services/file-chooser/abstract-file-chooser.hpp"
45
#include <QAbstractListModel>
56
#include <QJsonValue>
67
#include <QVariantMap>
78
#include <expected>
89
#include <functional>
910
#include <optional>
11+
#include <qcontainerfwd.h>
12+
#include <qtmetamacros.h>
1013
#include <vector>
1114

1215
class ExtensionFormModel : public QAbstractListModel {
@@ -40,7 +43,9 @@ class ExtensionFormModel : public QAbstractListModel {
4043
Q_INVOKABLE void fieldFocused(int index);
4144
Q_INVOKABLE void fieldBlurred(int index);
4245
Q_INVOKABLE void setFilePaths(int index, const QVariantList &paths);
46+
Q_INVOKABLE void setPickedItems(int index, const QVariantList &pickedItems);
4347
Q_INVOKABLE void dropdownSearchTextChanged(int index, const QString &text);
48+
Q_INVOKABLE void tagPickerSearchTextChanged(int index, const QString &text);
4449

4550
std::optional<FileChooserOptions> filePickerOptions(int index) const;
4651
bool isExtensionControlled(int index) const;
@@ -58,6 +63,7 @@ class ExtensionFormModel : public QAbstractListModel {
5863
TextArea,
5964
FilePicker,
6065
DatePicker,
66+
TagPicker,
6167
Description,
6268
Separator
6369
};
@@ -79,6 +85,7 @@ class ExtensionFormModel : public QAbstractListModel {
7985
std::optional<EventHandler> onBlur;
8086
std::optional<EventHandler> onFocus;
8187

88+
std::unique_ptr<FormTagPickerSuggestionsModel> tagPickerSuggestionsModel;
8289
QVariantMap fieldData;
8390

8491
QJsonValue effectiveValue() const { return hasUserValue ? userValue : modelValue; }
@@ -88,9 +95,11 @@ class ExtensionFormModel : public QAbstractListModel {
8895

8996
FormItemData createItem(const FormModel::Item &item) const;
9097
void updateItem(FormItemData &existing, const FormModel::Item &newItem);
98+
void updateFieldData(FormItemData &item, const FormModel::IField &field) const;
99+
void syncTagPickerSuggestionsModel(FormItemData &item) const;
100+
static QStringList tagPickerPickedItems(const QJsonValue &value);
91101

92102
static FormItemData::Type fieldType(const FormModel::IField &field);
93-
static QVariantMap buildFieldData(const FormModel::IField &field);
94103

95104
NotifyFn m_notify;
96105
std::vector<FormItemData> m_items;

0 commit comments

Comments
 (0)