Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ __cmake*
*.tgz

/qt6
**/.qmlls.ini

/AppDir
*AppImage
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
cmake_minimum_required(VERSION 3.16)
set(PROJECT_NAME vicinae)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(QT_QML_GENERATE_QMLLS_INI ON)
project(${PROJECT_NAME} VERSION 1.0.0 LANGUAGES C CXX)

set(LIB_NAMESPACE "vicinae")
Expand Down
8 changes: 5 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,12 @@
export CXX=${pkgs.gcc15}/bin/g++
export CMAKE_C_COMPILER=$CC
export CMAKE_CXX_COMPILER=$CXX

export QML2_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
export QML_IMPORT_PATH=${pkgs.qt6.qtdeclarative}/lib/qt-6/qml
'';
};
}
);
};
});
overlays.default = final: prev: {
vicinae = final.callPackage ./nix/vicinae.nix { };
mkVicinaeExtension = prev.callPackage ./nix/mkVicinaeExtension.nix { };
Expand Down
4 changes: 4 additions & 0 deletions src/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ set(SRCS
src/qml/missing-preference-view-host.cpp
src/qml/extension-form-model.hpp
src/qml/extension-form-model.cpp
src/qml/form-tag-picker-suggestions-model.hpp
src/qml/form-tag-picker-suggestions-model.cpp
src/qml/clipboard-history-model.hpp
src/qml/clipboard-history-model.cpp
src/qml/clipboard-history-view-host.hpp
Expand Down Expand Up @@ -719,6 +721,7 @@ set(VICINAE_QML_FILES
src/qml/qml/FormTextArea.qml
src/qml/qml/FormCheckbox.qml
src/qml/qml/FormSeparator.qml
src/qml/qml/FormTagPicker.qml
src/qml/qml/SnippetFormView.qml
src/qml/qml/AliasFormView.qml
src/qml/qml/ShortcutFormView.qml
Expand Down Expand Up @@ -772,6 +775,7 @@ set(VICINAE_QML_FILES
src/qml/qml/MissingPreferenceView.qml
src/qml/qml/OAuthOverlayView.qml
src/qml/qml/HudWindow.qml
src/qml/qml/TagPickerTag.qml
)

if(CMAKE_BUILD_TYPE MATCHES "Debug")
Expand Down
9 changes: 9 additions & 0 deletions src/server/include/extend/form-model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "extend/action-model.hpp"
#include "extend/list-model.hpp"
#include "extend/model.hpp"
#include "extend/tag-model.hpp"
#include <memory>
#include <qcoreevent.h>
#include <qjsonobject.h>
Expand Down Expand Up @@ -97,6 +98,14 @@ struct FormModel {
public:
DatePickerField(const FieldBase &base) : IField(base) {}
};

struct TagPickerField : public IField {
std::vector<TagPickerItemModel> m_items;

public:
TagPickerField(const FieldBase &base) : IField(base) {}
};

struct InvalidField : public FieldBase {};

struct Separator {};
Expand Down
17 changes: 17 additions & 0 deletions src/server/include/extend/tag-model.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include "extend/color-model.hpp"
#include "extend/image-model.hpp"
#include "lib/fuzzy/fuzzy-searchable.hpp"
#include <qjsonobject.h>
#include <qstring.h>

Expand All @@ -24,3 +25,19 @@ class TagListParser {

TagListModel parse(const QJsonObject &instance);
};

struct TagPickerItemModel {
QString title;
QString value;
std::optional<ImageLikeModel> icon;

public:
static TagPickerItemModel fromJson(const QJsonObject &instance);
};

template <> struct fuzzy::FuzzySearchable<TagPickerItemModel> {
static int score(const TagPickerItemModel &item, std::string_view query) {
auto name = item.title.toStdString();
return fuzzy::scoreWeighted({{name, 1.0}}, query);
}
};
15 changes: 12 additions & 3 deletions src/server/src/extend/form-model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

#include <algorithm>

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

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

model.items.emplace_back(filePicker);
} else if (*it == "tag-picker-field") {
auto tagPicker = std::make_shared<TagPickerField>(base);
tagPicker->m_items.reserve(children.size());

for (const auto &child : children) {
tagPicker->m_items.emplace_back(TagPickerItemModel::fromJson(child.toObject()));
}

model.items.emplace_back(tagPicker);
}
} else {
qWarning() << "Unknown form children of type" << type;
Expand Down
15 changes: 15 additions & 0 deletions src/server/src/extend/tag-list.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#include "extend/color-model.hpp"
#include "extend/image-model.hpp"
#include "extend/tag-model.hpp"
#include <exception>
#include <qjsonarray.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <stdexcept>

TagListParser::TagListParser() = default;

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

return model;
}

TagPickerItemModel TagPickerItemModel::fromJson(const QJsonObject &instance) {
TagPickerItemModel model;
auto props = instance.value("props").toObject();

model.title = props.value("title").toString();
model.value = props.value("value").toString();

if (props.contains("icon")) { model.icon = ImageModelParser().parse(props.value("icon")); }

return model;
}
78 changes: 73 additions & 5 deletions src/server/src/qml/extension-form-model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#include "view-utils.hpp"
#include <QJsonArray>
#include <QJsonObject>
#include <QJSValue>
#include <qcontainerfwd.h>
#include <qstringliteral.h>
#include <utility>

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

item.userValue = QJsonValue::fromVariant(value);
// TODO: find out why Form.TagPicker tags would break without that
QVariant normalizedValue = value;

if (value.metaType().id() == qMetaTypeId<QJSValue>()) {
normalizedValue = value.value<QJSValue>().toVariant();
}

item.userValue = QJsonValue::fromVariant(normalizedValue);
item.hasUserValue = true;
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole});

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

void ExtensionFormModel::setPickedItems(int index, const QVariantList &pickedItems) {
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
auto &item = m_items[index];
if (item.type != FormItemData::Type::TagPicker) return;

QJsonArray arr;
for (const auto &t : pickedItems) {
arr.append(t.toString());
}

item.userValue = arr;
item.hasUserValue = true;
syncTagPickerSuggestionsModel(item);
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole});

if (item.onChange) { m_notify(*item.onChange, QJsonArray{arr}); }
}

void ExtensionFormModel::dropdownSearchTextChanged(int index, const QString &text) {
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
const auto &item = m_items[index];
auto handler = item.fieldData.value("onSearchTextChange").toString();
if (!handler.isEmpty()) { m_notify(handler, QJsonArray{text}); }
}

void ExtensionFormModel::tagPickerSearchTextChanged(int index, const QString &text) {
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return;
auto &item = m_items[index];
if (item.type != FormItemData::Type::TagPicker || !item.tagPickerSuggestionsModel) return;
item.tagPickerSuggestionsModel->setFilter(text);
}

std::optional<FileChooserOptions> ExtensionFormModel::filePickerOptions(int index) const {
if (index < 0 || std::cmp_greater_equal(index, m_items.size())) return std::nullopt;
const auto &item = m_items[index];
Expand Down Expand Up @@ -146,6 +181,7 @@ void ExtensionFormModel::setFormData(const FormModel &model) {
if (it != savedValues.end()) {
newData.userValue = it->second;
newData.hasUserValue = true;
syncTagPickerSuggestionsModel(newData);
}
}
m_items[i] = std::move(newData);
Expand All @@ -162,6 +198,7 @@ void ExtensionFormModel::setFormData(const FormModel &model) {
if (it != savedValues.end()) {
newData.userValue = it->second;
newData.hasUserValue = true;
syncTagPickerSuggestionsModel(newData);
}
}
m_items.push_back(std::move(newData));
Expand Down Expand Up @@ -207,13 +244,14 @@ ExtensionFormModel::FormItemData ExtensionFormModel::createItem(const FormModel:
data.onChange = field->onChange;
data.onBlur = field->onBlur;
data.onFocus = field->onFocus;
data.fieldData = buildFieldData(*field);

if (field->value) {
data.modelValue = *field->value;
} else if (field->defaultValue) {
data.modelValue = *field->defaultValue;
}

updateFieldData(data, *field);
} else if (auto *desc = std::get_if<FormModel::Description>(&item)) {
data.type = FormItemData::Type::Description;
data.label = desc->title.value_or(QString());
Expand All @@ -235,12 +273,13 @@ void ExtensionFormModel::updateItem(FormItemData &existing, const FormModel::Ite
existing.onChange = field->onChange;
existing.onBlur = field->onBlur;
existing.onFocus = field->onFocus;
existing.fieldData = buildFieldData(*field);

if (field->value) {
existing.modelValue = *field->value;
existing.hasUserValue = false;
}

updateFieldData(existing, *field);
} else if (auto *desc = std::get_if<FormModel::Description>(&newItem)) {
existing.label = desc->title.value_or(QString());
existing.fieldData = {{"text", desc->text}};
Expand All @@ -255,10 +294,11 @@ ExtensionFormModel::FormItemData::Type ExtensionFormModel::fieldType(const FormM
if (dynamic_cast<const FormModel::TextAreaField *>(&field)) return FormItemData::Type::TextArea;
if (dynamic_cast<const FormModel::FilePickerField *>(&field)) return FormItemData::Type::FilePicker;
if (dynamic_cast<const FormModel::DatePickerField *>(&field)) return FormItemData::Type::DatePicker;
if (dynamic_cast<const FormModel::TagPickerField *>(&field)) return FormItemData::Type::TagPicker;
return FormItemData::Type::Text;
}

QVariantMap ExtensionFormModel::buildFieldData(const FormModel::IField &field) {
void ExtensionFormModel::updateFieldData(FormItemData &item, const FormModel::IField &field) const {
QVariantMap data;

if (auto *f = dynamic_cast<const FormModel::CheckboxField *>(&field)) {
Expand All @@ -280,9 +320,35 @@ QVariantMap ExtensionFormModel::buildFieldData(const FormModel::IField &field) {
if (f->min) data["min"] = *f->min;
if (f->max) data["max"] = *f->max;
data["includeTime"] = f->type.value_or("date") == "dateTime";
} else if (auto *f = dynamic_cast<const FormModel::TagPickerField *>(&field)) {
data["items"] = qml::convertTagPickerItems(f->m_items);
if (!item.tagPickerSuggestionsModel) {
item.tagPickerSuggestionsModel = std::make_unique<FormTagPickerSuggestionsModel>();
}
item.tagPickerSuggestionsModel->setSourceItems(f->m_items);
syncTagPickerSuggestionsModel(item);
data["suggestionsModel"] =
QVariant::fromValue(static_cast<QObject *>(item.tagPickerSuggestionsModel.get()));
}

return data;
item.fieldData = std::move(data);
}

void ExtensionFormModel::syncTagPickerSuggestionsModel(FormItemData &item) const {
if (!item.tagPickerSuggestionsModel) return;
item.tagPickerSuggestionsModel->setPickedItems(tagPickerPickedItems(item.effectiveValue()));
}

QStringList ExtensionFormModel::tagPickerPickedItems(const QJsonValue &value) {
QStringList result;
if (!value.isArray()) return result;

auto const arr = value.toArray();
result.reserve(arr.size());
for (const auto &entry : arr) {
if (entry.isString()) { result.append(entry.toString()); }
}
return result;
}

QString ExtensionFormModel::FormItemData::typeString() const {
Expand All @@ -305,6 +371,8 @@ QString ExtensionFormModel::FormItemData::typeString() const {
return QStringLiteral("description");
case Type::Separator:
return QStringLiteral("separator");
case Type::TagPicker:
return QStringLiteral("tagpicker");
}
return QStringLiteral("text");
}
11 changes: 10 additions & 1 deletion src/server/src/qml/extension-form-model.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#pragma once
#include "extend/form-model.hpp"
#include "form-tag-picker-suggestions-model.hpp"
#include "services/file-chooser/abstract-file-chooser.hpp"
#include <QAbstractListModel>
#include <QJsonValue>
#include <QVariantMap>
#include <expected>
#include <functional>
#include <optional>
#include <qcontainerfwd.h>
#include <qtmetamacros.h>
#include <vector>

class ExtensionFormModel : public QAbstractListModel {
Expand Down Expand Up @@ -40,7 +43,9 @@ class ExtensionFormModel : public QAbstractListModel {
Q_INVOKABLE void fieldFocused(int index);
Q_INVOKABLE void fieldBlurred(int index);
Q_INVOKABLE void setFilePaths(int index, const QVariantList &paths);
Q_INVOKABLE void setPickedItems(int index, const QVariantList &pickedItems);
Q_INVOKABLE void dropdownSearchTextChanged(int index, const QString &text);
Q_INVOKABLE void tagPickerSearchTextChanged(int index, const QString &text);

std::optional<FileChooserOptions> filePickerOptions(int index) const;
bool isExtensionControlled(int index) const;
Expand All @@ -58,6 +63,7 @@ class ExtensionFormModel : public QAbstractListModel {
TextArea,
FilePicker,
DatePicker,
TagPicker,
Description,
Separator
};
Expand All @@ -79,6 +85,7 @@ class ExtensionFormModel : public QAbstractListModel {
std::optional<EventHandler> onBlur;
std::optional<EventHandler> onFocus;

std::unique_ptr<FormTagPickerSuggestionsModel> tagPickerSuggestionsModel;
QVariantMap fieldData;

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

FormItemData createItem(const FormModel::Item &item) const;
void updateItem(FormItemData &existing, const FormModel::Item &newItem);
void updateFieldData(FormItemData &item, const FormModel::IField &field) const;
void syncTagPickerSuggestionsModel(FormItemData &item) const;
static QStringList tagPickerPickedItems(const QJsonValue &value);

static FormItemData::Type fieldType(const FormModel::IField &field);
static QVariantMap buildFieldData(const FormModel::IField &field);

NotifyFn m_notify;
std::vector<FormItemData> m_items;
Expand Down
Loading
Loading