├── src ├── contents │ ├── logic │ │ ├── qmlLinker.cpp │ │ ├── view.h │ │ ├── mdHandler.h │ │ ├── qmlLinker.h │ │ ├── mdHandler.cpp │ │ └── view.cpp │ ├── ui │ │ ├── pages │ │ │ ├── AboutPage.qml │ │ │ ├── MainPage.qml │ │ │ └── SettingsPage.qml │ │ ├── dialogs │ │ │ ├── FolderPickerDialog.qml │ │ │ ├── DeleteConfirmationDialog.qml │ │ │ ├── FilePickerDialog.qml │ │ │ ├── colorDialog │ │ │ │ ├── ColorDialog.qml │ │ │ │ ├── LightnessSlider.qml │ │ │ │ ├── ColorPicker.qml │ │ │ │ └── HSPicker.qml │ │ │ ├── URLDialog.qml │ │ │ ├── NamingErrorDialog.qml │ │ │ ├── FontPickerDialog.qml │ │ │ ├── ToDoDialog.qml │ │ │ ├── NamingDialog.qml │ │ │ ├── StorageDialog.qml │ │ │ ├── ImagePickerDialog.qml │ │ │ └── TableMakerDialog.qml │ │ ├── textEditor │ │ │ ├── BottomToolBar.qml │ │ │ ├── TextEditor.qml │ │ │ ├── EditorView.qml │ │ │ ├── TextDisplay.qml │ │ │ └── TextToolBar.qml │ │ ├── main.qml │ │ ├── sideBar │ │ │ ├── SubEntryColumn.qml │ │ │ ├── TreeView.qml │ │ │ ├── Sidebar.qml │ │ │ ├── TreeNode.qml │ │ │ └── ActionBar.qml │ │ └── todoEditor │ │ │ └── ToDoView.qml │ └── resources │ │ ├── 3rdparty │ │ ├── MARKED-LICENSE.txt │ │ └── markdown.css │ │ ├── index.html │ │ └── qwebchannel.js ├── Messages.sh ├── kleverconfig.kcfgc ├── logic │ ├── documentHandler.h │ ├── todoHandler.h │ ├── kleverUtility.h │ ├── storageHandler.h │ ├── todoHandler.cpp │ ├── documentHandler.cpp │ ├── kleverUtility.cpp │ └── storageHandler.cpp ├── app.h ├── app.cpp ├── CMakeLists.txt ├── resources.qrc ├── kleverconfig.kcfg └── main.cpp ├── LICENSES ├── FSFAP.txt ├── LicenseRef-KDE-Accepted-LGPL.txt ├── BSD-3-Clause.txt ├── CC0-1.0.txt ├── LGPL-3.0-only.txt └── GPL-2.0-or-later.txt ├── .gitignore ├── org.kde.klever.desktop ├── README.md ├── org.kde.klever.json ├── org.kde.klever.metainfo.xml └── CMakeLists.txt /src/contents/logic/qmlLinker.cpp: -------------------------------------------------------------------------------- 1 | #include "qmlLinker.h" 2 | 3 | QmlLinker::QmlLinker(QObject *parent) 4 | : QObject(parent) 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/Messages.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # SPDX-FileCopyrightText: 2022 Carl Schwan 3 | # SPDX-License-Identifier: CC0-1.0 4 | $XGETTEXT `find -name \*.cpp -o -name \*.qml` -o $podir/klevernotes.pot 5 | -------------------------------------------------------------------------------- /LICENSES/FSFAP.txt: -------------------------------------------------------------------------------- 1 | Copying and distribution of this file, with or without modification, are permitted 2 | in any medium without royalty provided the copyright notice and this notice 3 | are preserved. This file is offered as-is, without any warranty. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Louis Schul 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | build*/ 5 | android*/ 6 | .cache 7 | *.kate-swp 8 | *.kdev4 9 | .directory 10 | CMakeCache.txt 11 | CMakeFiles 12 | CMakeLists.txt.user 13 | .clang-format 14 | -------------------------------------------------------------------------------- /src/kleverconfig.kcfgc: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | # SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | File=kleverconfig.kcfg 5 | ClassName=KleverConfig 6 | Mutators=true 7 | DefaultValueGetters=true 8 | GenerateProperties=true 9 | ParentInConstructor=true 10 | Singleton=true 11 | -------------------------------------------------------------------------------- /src/contents/ui/pages/AboutPage.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import org.kde.Klever 1.0 10 | 11 | Kirigami.AboutPage { 12 | aboutData: About 13 | } 14 | -------------------------------------------------------------------------------- /src/logic/documentHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | class DocumentHandler : public QObject 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | explicit DocumentHandler(QObject *parent = nullptr); 14 | 15 | Q_INVOKABLE QString readNote(const QString &path) const; 16 | Q_INVOKABLE void writeNote(const QString ¬e, const QString &path); 17 | }; 18 | -------------------------------------------------------------------------------- /org.kde.klever.desktop: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC0-1.0 2 | # SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | [Desktop Entry] 5 | Name=KleverNotes 6 | Comment=KleverNotes Kirigami Application 7 | Version=1.0 8 | Exec=klevernotes 9 | Icon=applications-development 10 | Type=Application 11 | Terminal=false 12 | # Add an actual main category here (and possibly applicable additional ones) 13 | # https://specifications.freedesktop.org/menu-spec/latest/apa.html#main-category-registry 14 | Categories=Qt;KDE; 15 | -------------------------------------------------------------------------------- /src/logic/todoHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class TodoHandler : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit TodoHandler(QObject *parent = nullptr); 16 | 17 | Q_INVOKABLE QJsonObject readTodos(const QString &path) const; 18 | Q_INVOKABLE void writeTodos(const QJsonObject &todos, const QString &path); 19 | }; 20 | -------------------------------------------------------------------------------- /src/contents/logic/view.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class View : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit View(QObject *parent = nullptr); 16 | 17 | signals: 18 | void hierarchySent(QJsonObject); 19 | 20 | public slots: 21 | QJsonObject getHierarchy(QString path, int lvl); 22 | void hierarchySupplier(QString path, int lvl); 23 | }; 24 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/FolderPickerDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import Qt.labs.platform 1.1 6 | import QtQuick.Dialogs 1.3 7 | 8 | FolderDialog{ 9 | id:folderDialog 10 | 11 | folder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0] 12 | options: FolderDialog.ShowDirsOnly 13 | 14 | property QtObject parent; 15 | property string toChangeProperty; 16 | 17 | onAccepted: { 18 | parent.folder = currentFolder 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | class QQuickWindow; 9 | 10 | class App : public QObject 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | // Restore current window geometry 16 | Q_INVOKABLE void restoreWindowGeometry(QQuickWindow *window, const QString &group = QStringLiteral("main")) const; 17 | // Save current window geometry 18 | Q_INVOKABLE void saveWindowGeometry(QQuickWindow *window, const QString &group = QStringLiteral("main")) const; 19 | }; 20 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/DeleteConfirmationDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | 7 | 8 | Kirigami.PromptDialog { 9 | property string useCase 10 | readonly property var useCaseTrad: { 11 | "Category": i18n("category"), 12 | "Group": i18n("group"), 13 | "Note": i18n("note") 14 | } 15 | 16 | subtitle: "Are you sure you want to delete this "+useCaseTrad[useCase]+"?" 17 | standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 18 | } 19 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/FilePickerDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.3 as Controls 6 | import QtQuick.Dialogs 1.3 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | FileDialog { 10 | id: fileDialog 11 | 12 | title: i18n("Image picker") 13 | selectExisting: true 14 | selectMultiple: false 15 | nameFilters: [ "Image files (*.jpeg *.jpg *.png)" ] 16 | 17 | property QtObject caller 18 | 19 | onAccepted: { 20 | caller.path = fileDialog.fileUrl 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/contents/logic/mdHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | class MDHandler : public QObject 10 | { 11 | Q_OBJECT 12 | 13 | public: 14 | explicit MDHandler(QObject *parent = nullptr); 15 | 16 | public slots: 17 | QJsonArray getLines(QString text); 18 | // Would love to use a pair here, but QML doesn't like it :/ 19 | QStringList getPositionLineInfo(QJsonArray lines, int position); 20 | QJsonObject getInstructions(QString selectedText, QStringList charsList, bool checkLineEnd); 21 | }; 22 | -------------------------------------------------------------------------------- /src/logic/kleverUtility.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | class KleverUtility : public QObject 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | explicit KleverUtility(QObject *parent = nullptr); 14 | 15 | public slots: 16 | QString getName(const QString &path); 17 | QString getPath(const QUrl &url); 18 | bool exists(const QString &path); 19 | void create(const QString &path); 20 | QString getImageStoragingPath(const QString ¬eImagesStoringPath, const QString &wantedName, int iteration = 0); 21 | bool isEmptyDir(const QString &path); 22 | }; 23 | -------------------------------------------------------------------------------- /LICENSES/LicenseRef-KDE-Accepted-LGPL.txt: -------------------------------------------------------------------------------- 1 | This library is free software; you can redistribute it and/or 2 | modify it under the terms of the GNU Lesser General Public 3 | License as published by the Free Software Foundation; either 4 | version 3 of the license or (at your option) any later version 5 | that is accepted by the membership of KDE e.V. (or its successor 6 | approved by the membership of KDE e.V.), which shall act as a 7 | proxy as defined in Section 6 of version 3 of the license. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/colorDialog/ColorDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Layouts 1.15 6 | import QtQuick.Controls 2.15 as Controls 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | Kirigami.Dialog { 10 | title: i18n("Color Picker") 11 | padding: 0 12 | preferredWidth: Kirigami.Units.gridUnit * 16 13 | preferredHeight : Kirigami.Units.gridUnit * 16 14 | 15 | standardButtons: Kirigami.Dialog.Apply | Kirigami.Dialog.Cancel 16 | 17 | property alias selectedColor: picker.selectedColor 18 | 19 | ColorPicker{ 20 | id:picker 21 | anchors.fill:parent 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/URLDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | import QtQuick.Controls 2.15 as Controls 7 | 8 | Kirigami.PromptDialog { 9 | id: textPromptDialog 10 | 11 | title: i18n("Choose an URL") 12 | 13 | property QtObject caller 14 | 15 | Controls.TextField { 16 | id: urlField 17 | 18 | Keys.onPressed: if ((event.key === Qt.Key_Return) || (event.key=== Qt.Key_Enter)) textPromptDialog.accept() 19 | } 20 | 21 | standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 22 | 23 | onAccepted: { 24 | caller.path = urlField.text 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # KleverNotes 7 | 8 | 9 | A convergent markdown note taking application. 10 | 11 | 12 | ## Dependencies 13 | * Kirigami 14 | * Qt Quick Controls 15 | 16 | ## Installing 17 | 1. Install the required packages, which you can find [on this page](https://develop.kde.org/docs/getting-started/kirigami/introduction-getting_started/) 18 | 2. Download or clone the repo 19 | 3. Inside the directory run : 20 | 21 | ```sh 22 | mkdir build 23 | cd build 24 | /usr/bin/cmake -Wno-dev -G Ninja ../ 25 | /usr/bin/ninja 26 | ``` 27 | 28 | `/usr/bin/cmake` is the default path to cmake, replace it by yours if it's different. 29 | 30 | Same goes for `/usr/bin/ninja`. 31 | -------------------------------------------------------------------------------- /org.kde.klever.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "org.kde.klevernotes", 3 | "runtime": "org.kde.Platform", 4 | "runtime-version": "5.15", 5 | "sdk": "org.kde.Sdk", 6 | "command": "klevernotes", 7 | "tags": ["nightly"], 8 | "desktop-file-name-suffix": " (Nightly)", 9 | "finish-args": [ 10 | "--share=ipc", 11 | "--share=network", 12 | "--socket=x11", 13 | "--socket=wayland", 14 | "--device=dri", 15 | "--filesystem=home" 16 | ], 17 | "separate-locales": false, 18 | 19 | "modules": [ 20 | { 21 | "name": "klevernotes", 22 | "buildsystem": "cmake-ninja", 23 | "builddir": true, 24 | "sources": [ { "type": "dir", "path": ".", "skip": [".git"] } ] 25 | } 26 | ] 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/contents/ui/textEditor/BottomToolBar.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.15 7 | import QtQuick.Dialogs 1.1 8 | import org.kde.kirigami 2.19 as Kirigami 9 | 10 | Kirigami.NavigationTabBar { 11 | id: tool 12 | 13 | property bool showNoteEditor: true 14 | 15 | actions: [ 16 | Kirigami.Action { 17 | iconName: "document-edit" 18 | text: i18n("Note") 19 | 20 | onTriggered: showNoteEditor = true 21 | }, 22 | Kirigami.Action { 23 | iconName: "dino-double-tick-symbolic" 24 | text: i18n("TODO") 25 | 26 | onTriggered: showNoteEditor = false 27 | } 28 | ] 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/logic/storageHandler.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | #pragma once 4 | 5 | #include "kleverUtility.h" 6 | #include 7 | #include 8 | #include 9 | 10 | class StorageHandler : public QObject 11 | { 12 | Q_OBJECT 13 | KleverUtility util; 14 | 15 | public: 16 | explicit StorageHandler(QObject *parent = nullptr); 17 | 18 | signals: 19 | void storageUpdated(); 20 | 21 | public slots: 22 | void makeStorage(QString storagePath); 23 | void makeCategory(QString storagePath, QString categoryName); 24 | void makeGroup(QString categoryPath, QString groupName); 25 | void makeNote(QString groupPath, QString noteName); 26 | 27 | bool rename(QString oldPath, QString newPath); 28 | 29 | void remove(const QString &path); 30 | 31 | void slotResult(KJob *job); 32 | }; 33 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/NamingErrorDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | 7 | Kirigami.PromptDialog { 8 | title: "Klever Notes Storage" 9 | 10 | // Dirty workaround to prevent weird height 11 | height: header.height + footer.height + topPadding + bottomPadding + mainItem.height 12 | 13 | showCloseButton: false 14 | closePolicy: Controls.Popup.NoAutoClose 15 | standardButtons: Kirigami.Dialog.Ok 16 | 17 | property string useCase 18 | property QtObject nameField 19 | 20 | readonly property var useCaseTrad: { 21 | "Category": i18n("category"), 22 | "Group": i18n("group"), 23 | "Note": i18n("note") 24 | } 25 | 26 | subtitle: i18n("This "+useCaseTrad[useCase]+" already exist.\nPlease choose another name for it.\n") 27 | 28 | onAccepted: nameField.selectAll() 29 | } 30 | -------------------------------------------------------------------------------- /src/contents/logic/qmlLinker.h: -------------------------------------------------------------------------------- 1 | #ifndef QMLLINKER_H 2 | #define QMLLINKER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class QmlLinker : public QObject 9 | { 10 | Q_OBJECT 11 | Q_PROPERTY(QString text MEMBER changedText NOTIFY textChanged FINAL) 12 | Q_PROPERTY(QVariantMap css MEMBER changedCss NOTIFY cssChanged FINAL) 13 | Q_PROPERTY(QString homePath MEMBER changedHomePath NOTIFY homePathChanged FINAL) 14 | Q_PROPERTY(QString notePath MEMBER changedNotePath NOTIFY notePathChanged FINAL) 15 | 16 | public: 17 | explicit QmlLinker(QObject *parent = nullptr); 18 | 19 | signals: 20 | void textChanged(const QString &text); 21 | void cssChanged(const QVariantMap &css); 22 | void homePathChanged(const QString &homePath); 23 | void notePathChanged(const QString ¬ePath); 24 | 25 | private: 26 | QString changedText; 27 | QVariantMap changedCss; 28 | QString changedHomePath; 29 | QString changedNotePath; 30 | }; 31 | 32 | #endif // QMLLINKER_H 33 | -------------------------------------------------------------------------------- /src/logic/todoHandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | #include "todoHandler.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | TodoHandler::TodoHandler(QObject *parent) 11 | : QObject(parent) 12 | { 13 | } 14 | 15 | QJsonObject TodoHandler::readTodos(const QString &path) const 16 | { 17 | QFile file(path); 18 | 19 | QJsonObject todos; 20 | if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { 21 | todos = QJsonDocument::fromJson(file.readAll()).object(); 22 | file.close(); 23 | } 24 | return todos; 25 | } 26 | 27 | void TodoHandler::writeTodos(const QJsonObject &todos, const QString &path) 28 | { 29 | QJsonDocument doc = QJsonDocument(todos); 30 | 31 | QFile file(path); 32 | if (file.open(QIODevice::WriteOnly)) { 33 | file.write(doc.toJson()); 34 | file.close(); 35 | return; 36 | } 37 | qDebug() << "Error !!"; 38 | } 39 | -------------------------------------------------------------------------------- /org.kde.klever.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | org.kde.klevernotes 8 | KleverNotes Kirigami Application 9 | A short summary describing what this software is about 10 | A permissive license for this metadata, e.g. "FSFAP" + Update the SPDX tags above! 11 | The license of this software as SPDX string, e.g. "GPL-2.0-or-later" 12 | The software vendor name, e.g. "ACME Corporation" 13 | 14 |

Multiple paragraphs of long description, describing this software component.

15 |

You can also use ordered and unordered lists:

16 |
    17 |
  • Feature 1
  • 18 |
  • Feature 2
  • 19 |
20 |

Keep in mind to XML-escape characters, and that this is not HTML markup.

21 |
22 |
23 | -------------------------------------------------------------------------------- /src/app.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #include "app.h" 5 | #include 6 | #include 7 | #include 8 | 9 | void App::restoreWindowGeometry(QQuickWindow *window, const QString &group) const 10 | { 11 | KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); 12 | KConfigGroup windowGroup(&dataResource, QStringLiteral("Window-") + group); 13 | KWindowConfig::restoreWindowSize(window, windowGroup); 14 | KWindowConfig::restoreWindowPosition(window, windowGroup); 15 | } 16 | 17 | void App::saveWindowGeometry(QQuickWindow *window, const QString &group) const 18 | { 19 | KConfig dataResource(QStringLiteral("data"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); 20 | KConfigGroup windowGroup(&dataResource, QStringLiteral("Window-") + group); 21 | KWindowConfig::saveWindowPosition(window, windowGroup); 22 | KWindowConfig::saveWindowSize(window, windowGroup); 23 | dataResource.sync(); 24 | } 25 | -------------------------------------------------------------------------------- /src/logic/documentHandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | #include "documentHandler.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | DocumentHandler::DocumentHandler(QObject *parent) 11 | : QObject(parent) 12 | { 13 | } 14 | 15 | QString DocumentHandler::readNote(const QString &path) const 16 | { 17 | QFile file(path); 18 | 19 | QString line; 20 | if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { 21 | QTextStream stream(&file); 22 | while (!stream.atEnd()) { 23 | line.append(stream.readLine() + "\n"); 24 | } 25 | line.remove(line.length() - 1, 2); 26 | } 27 | file.close(); 28 | return line; 29 | } 30 | 31 | void DocumentHandler::writeNote(const QString ¬e, const QString &path) 32 | { 33 | QFile file(path); 34 | if (file.open(QIODevice::WriteOnly)) { 35 | QTextStream stream(&file); 36 | stream << note << Qt::endl; 37 | } 38 | file.close(); 39 | } 40 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | add_executable(klevernotes 5 | main.cpp 6 | app.cpp 7 | 8 | logic/documentHandler.cpp 9 | logic/kleverUtility.cpp 10 | logic/storageHandler.cpp 11 | logic/todoHandler.cpp 12 | 13 | contents/logic/mdHandler.cpp 14 | contents/logic/qmlLinker.cpp 15 | contents/logic/view.cpp 16 | 17 | resources.qrc) 18 | 19 | find_package(KF5KIO) 20 | 21 | target_link_libraries(klevernotes 22 | Qt5::Core 23 | Qt5::Gui 24 | Qt5::Qml 25 | Qt5::Quick 26 | Qt5::QuickControls2 27 | Qt5::Svg 28 | KF5::I18n 29 | KF5::CoreAddons 30 | KF5::ConfigCore 31 | KF5::ConfigGui 32 | KF5::KIOCore 33 | Qt::WebChannel 34 | Qt::WebEngine 35 | Qt::WebEngineWidgets) 36 | 37 | if (ANDROID) 38 | kirigami_package_breeze_icons(ICONS 39 | list-add 40 | help-about 41 | application-exit 42 | applications-graphics 43 | ) 44 | endif() 45 | 46 | kconfig_add_kcfg_files(klevernotes GENERATE_MOC kleverconfig.kcfgc) 47 | install(TARGETS klevernotes ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) 48 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/FontPickerDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import org.kde.kirigami 2.19 as Kirigami 7 | 8 | Kirigami.Dialog { 9 | id: scrollableDialog 10 | title: i18n("Font selector") 11 | 12 | property string currentFamily 13 | property string checkedFamily 14 | 15 | standardButtons: Kirigami.Dialog.Apply | Kirigami.Dialog.Cancel 16 | 17 | ListView { 18 | id: listView 19 | implicitWidth: Kirigami.Units.gridUnit * 16 20 | implicitHeight: Kirigami.Units.gridUnit * 16 21 | 22 | model: Qt.fontFamilies() 23 | 24 | delegate: Controls.RadioDelegate { 25 | topPadding: Kirigami.Units.smallSpacing * 2 26 | bottomPadding: Kirigami.Units.smallSpacing * 2 27 | implicitWidth: listView.width 28 | text: modelData 29 | font.family: modelData 30 | checked: currentFamily == modelData 31 | onCheckedChanged: if (checked) scrollableDialog.checkedFamily = modelData 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/contents/resources/3rdparty/MARKED-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/contents/ui/textEditor/TextEditor.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | 5 | import QtQuick 2.15 6 | import QtQuick.Controls 2.2 7 | import org.kde.Klever 1.0 8 | 9 | ScrollView { 10 | id: view 11 | 12 | property string path 13 | property string text: textArea.text 14 | property bool modified : false 15 | readonly property TextArea textArea: textArea 16 | 17 | onPathChanged: { 18 | textArea.tempBuff = true ; 19 | textArea.text = DocumentHandler.readNote(path) ; 20 | modified = false ; 21 | textArea.tempBuff = false 22 | } 23 | 24 | TextArea{ 25 | id: textArea 26 | 27 | persistentSelection: true 28 | wrapMode: TextEdit.Wrap 29 | 30 | property bool tempBuff 31 | 32 | onTextChanged : if (!tempBuff) modified = true 33 | } 34 | 35 | Timer { 36 | id: noteSaverTimer 37 | 38 | interval: 10000 39 | onTriggered: saveNote(textArea.text, view.path) 40 | } 41 | 42 | function saveNote (text, path) { 43 | if (modified) { 44 | DocumentHandler.writeNote(text, path) 45 | modified = false 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/contents/resources/3rdparty/markdown.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bodyColor: ; 3 | --font: ; 4 | --textColor: ; 5 | --titleColor: ; 6 | --linkColor: ; 7 | --visitedLinkColor: ; 8 | --codeColor: ; 9 | } 10 | 11 | body{ 12 | color: var(--textColor); 13 | background-color: var(--bodyColor); 14 | 15 | font-size: 16px; 16 | word-wrap: break-word; 17 | } 18 | 19 | h1, h2, h3, h4 { 20 | color: var(--titleColor); 21 | font-weight: 400; 22 | } 23 | 24 | a { 25 | color: var(--linkColor); 26 | margin: 0; 27 | padding: 0; 28 | vertical-align: baseline; 29 | } 30 | a:visited { 31 | color: var(--visitedLinkColor); 32 | } 33 | 34 | ol, ul { 35 | padding: 10px; 36 | } 37 | 38 | ol li, ul li { 39 | margin: 5px; 40 | } 41 | 42 | pre { 43 | background: var(--codeColor); 44 | border: 1px solid var(--linkColor); 45 | border-radius: 4px; 46 | } 47 | 48 | code { 49 | background: var(--codeColor); 50 | } 51 | 52 | pre code { 53 | border-radius: 4px; 54 | } 55 | 56 | blockquote { 57 | border-left:.3em solid var(--titleColor); 58 | color: var(--titleColor); 59 | padding-left: 5px; 60 | margin-left:0; 61 | font-style: italic; 62 | } 63 | 64 | hr { 65 | width: 98%; 66 | text-align: left; 67 | margin: 0 auto 0 0; 68 | color: var(--titleColor); 69 | } 70 | 71 | img { 72 | max-width: 100%; 73 | max-height: 100%; 74 | } 75 | 76 | table, th, td { 77 | border: 1px solid; 78 | border-collapse: collapse; 79 | } 80 | 81 | table { 82 | width: 100% 83 | } 84 | -------------------------------------------------------------------------------- /src/logic/kleverUtility.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #include "kleverUtility.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | KleverUtility::KleverUtility(QObject *parent) 11 | : QObject(parent) 12 | { 13 | } 14 | 15 | QString KleverUtility::getName(const QString &path) 16 | { 17 | return QDir(path).dirName(); 18 | } 19 | 20 | QString KleverUtility::getPath(const QUrl &url) 21 | { 22 | return url.toLocalFile(); 23 | } 24 | 25 | bool KleverUtility::exists(const QString &url) 26 | { 27 | return QFile().exists(url); 28 | } 29 | 30 | void KleverUtility::create(const QString &path) 31 | { 32 | if (!exists(path)) { 33 | QDir().mkpath(path); 34 | } 35 | } 36 | 37 | QString KleverUtility::getImageStoragingPath(const QString ¬eImagesStoringPath, const QString &wantedName, int iteration) 38 | { 39 | create(noteImagesStoringPath); 40 | 41 | QString imagePath = noteImagesStoringPath + wantedName; 42 | if (iteration != 0) 43 | imagePath += "(" + QString::number(iteration) + ")"; 44 | imagePath += ".png"; 45 | 46 | if (exists(imagePath)) { 47 | return getImageStoragingPath(noteImagesStoringPath, wantedName, iteration + 1); 48 | } 49 | qDebug() << imagePath + " C++"; 50 | return imagePath; 51 | } 52 | 53 | bool KleverUtility::isEmptyDir(const QString &path) 54 | { 55 | return !exists(path) || QDir(path).isEmpty(); 56 | } 57 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 26 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/colorDialog/LightnessSlider.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.11 5 | 6 | Item{ 7 | id: lightSlider 8 | property int cursorHeight: 7 9 | property real l 10 | Rectangle { 11 | y: 10 12 | height: parent.height - 20 13 | width:parent.width 14 | 15 | 16 | gradient: Gradient { 17 | GradientStop { position: 0.0; color: "#FFFFFFFF" } 18 | GradientStop { position: 1.0; color: "#FF000000" } 19 | } 20 | 21 | Item { 22 | id: pickerCursor 23 | width: parent.width 24 | Rectangle { 25 | x: -2; y: -height*0.5 26 | width: parent.width + 4; height: cursorHeight 27 | border.color: (lightSlider.l > 50) ? "black" : "white" 28 | border.width: 2 29 | color: "transparent" 30 | } 31 | } 32 | 33 | MouseArea{ 34 | id:mouseArea 35 | anchors.fill:parent 36 | 37 | function handleMouse(mouse) { 38 | if (mouse.buttons & Qt.LeftButton) { 39 | pickerCursor.y = Math.max(0, Math.min(height, mouse.y)) 40 | 41 | lightSlider.l = ((height-pickerCursor.y)/height) 42 | } 43 | } 44 | onPositionChanged: { 45 | handleMouse(mouse) 46 | } 47 | onPressed: handleMouse(mouse) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/ToDoDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Layouts 1.15 6 | import org.kde.kirigami 2.19 as Kirigami 7 | import QtQuick.Controls 2.15 as Controls 8 | 9 | Kirigami.PromptDialog { 10 | id: promptDialog 11 | 12 | title: i18n("Add Todo") 13 | 14 | property int callerModelIndex: -1 15 | property alias name: nameField.text 16 | property alias description: descriptionField.text 17 | 18 | ColumnLayout { 19 | Kirigami.Heading { 20 | text: i18nc("@label:textbox", "Title:") 21 | level: 2 22 | 23 | Layout.fillWidth: true 24 | } 25 | 26 | Controls.TextField { 27 | id: nameField 28 | 29 | placeholderText: i18n("Todo title (required)") 30 | onAccepted: descriptionField.forceActiveFocus() 31 | 32 | Layout.fillWidth: true 33 | } 34 | 35 | Kirigami.Heading { 36 | text: i18n("Description:") 37 | level: 2 38 | 39 | Layout.fillWidth: true 40 | } 41 | 42 | Controls.ScrollView { 43 | Layout.fillWidth: true 44 | Layout.preferredHeight: Kirigami.Units.gridUnit * 4 45 | 46 | Controls.TextArea { 47 | id: descriptionField 48 | 49 | placeholderText: i18n("Optional") 50 | wrapMode: TextEdit.WrapAnywhere 51 | } 52 | } 53 | } 54 | 55 | standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 56 | } 57 | 58 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | cmake_minimum_required(VERSION 3.16) 5 | 6 | project(klevernotes VERSION 0.1) 7 | 8 | include(FeatureSummary) 9 | 10 | set(QT5_MIN_VERSION 5.15) 11 | set(KF5_MIN_VERSION 5.84) 12 | 13 | find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) 14 | 15 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) 16 | 17 | include(KDEInstallDirs) 18 | include(KDECMakeSettings) 19 | include(KDECompilerSettings NO_POLICY_SCOPE) 20 | include(ECMSetupVersion) 21 | include(ECMGenerateHeaders) 22 | include(KDEClangFormat) 23 | include(KDEGitCommitHooks) 24 | 25 | ecm_setup_version(${PROJECT_VERSION} 26 | VARIABLE_PREFIX KLEVER 27 | VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/src/version-klevernotes.h" 28 | ) 29 | 30 | find_package(Qt5 ${QT5_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Svg WebEngineWidgets WebEngine) 31 | find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Kirigami2 CoreAddons Config I18n) 32 | 33 | if (ANDROID) 34 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle) 35 | endif() 36 | 37 | add_subdirectory(src) 38 | 39 | ki18n_install(po) 40 | 41 | install(PROGRAMS org.kde.klever.desktop DESTINATION ${KDE_INSTALL_APPDIR}) 42 | install(FILES org.kde.klever.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) 43 | 44 | feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) 45 | 46 | file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES src/*.cpp src/*.h) 47 | kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) 48 | kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) 49 | -------------------------------------------------------------------------------- /src/logic/storageHandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #include "storageHandler.h" 5 | #include "kleverUtility.h" 6 | #include 7 | #include 8 | #include 9 | 10 | StorageHandler::StorageHandler(QObject *parent) 11 | : QObject(parent) 12 | { 13 | } 14 | 15 | void StorageHandler::makeNote(QString groupPath, QString noteName) 16 | { 17 | QDir noteDir; 18 | QString notePath = groupPath.append("/" + noteName); 19 | QFile note(notePath + "/note.md"); 20 | QFile todo(notePath + "/todo.json"); 21 | 22 | util.create(notePath); 23 | note.open(QIODevice::ReadWrite); 24 | note.close(); 25 | todo.open(QIODevice::ReadWrite); 26 | todo.close(); 27 | } 28 | 29 | void StorageHandler::makeGroup(QString categoryPath, QString groupName) 30 | { 31 | QString groupPath = categoryPath.append("/" + groupName); 32 | util.create(groupPath); 33 | } 34 | 35 | void StorageHandler::makeCategory(QString storagePath, QString categoryName) 36 | { 37 | QString categoryPath = storagePath.append("/" + categoryName); 38 | makeGroup(categoryPath, ".BaseGroup"); 39 | } 40 | 41 | void StorageHandler::makeStorage(QString storagePath) 42 | { 43 | makeCategory(storagePath, "/.BaseCategory"); 44 | } 45 | 46 | bool StorageHandler::rename(QString oldPath, QString newPath) 47 | { 48 | QDir dir; 49 | return dir.rename(oldPath, newPath); 50 | } 51 | 52 | void StorageHandler::remove(const QString &path) 53 | { 54 | auto *job = KIO::trash(QUrl::fromLocalFile(path)); 55 | job->start(); 56 | connect(job, &KJob::result, this, &StorageHandler::slotResult); 57 | } 58 | 59 | void StorageHandler::slotResult(KJob *job) 60 | { 61 | if (!job->error()) { 62 | emit storageUpdated(); 63 | return; 64 | } 65 | qDebug() << job->errorString(); 66 | } 67 | -------------------------------------------------------------------------------- /src/contents/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/colorDialog/ColorPicker.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | /* 5 | * THIS COMPONENT AND INNER COMPONENTS ARE LOOSELY BASED ON : 6 | * https://github.com/rshest/qml-colorpicker/tree/master/colorpicker 7 | */ 8 | import QtQuick 2.11 9 | import QtQuick.Layouts 1.1 10 | import QtQuick.Controls 2.4 11 | 12 | Rectangle { 13 | id: colorPicker 14 | color: "transparent" 15 | 16 | property color selectedColor 17 | property bool initital: false 18 | 19 | onSelectedColorChanged: if (!visible) { 20 | hsPicker.h = selectedColor.hslHue 21 | hsPicker.s = selectedColor.hslSaturation 22 | lightnessSlider.l = selectedColor.hslLightness 23 | } 24 | 25 | ColumnLayout{ 26 | anchors.fill:parent 27 | spacing:0 28 | RowLayout { 29 | id: picker 30 | 31 | width: parent.width 32 | height:parent.height-15 33 | 34 | // hue and saturation 35 | HSPicker { 36 | id: hsPicker 37 | Layout.fillWidth:true 38 | Layout.fillHeight:true 39 | 40 | onSChanged: _hsla(h,s,lightnessSlider.v) 41 | onHChanged: _hsla(h,s,lightnessSlider.v) 42 | } 43 | 44 | // lightness picking slider 45 | LightnessSlider { 46 | id: lightnessSlider 47 | width: 12 48 | Layout.rightMargin: 10 49 | Layout.fillHeight:true 50 | 51 | onLChanged: _hsla(hsPicker.h,hsPicker.s,l) 52 | } 53 | } 54 | Rectangle{ 55 | id:select 56 | Layout.preferredWidth: hsPicker.width - 20 57 | height: 15 58 | Layout.leftMargin: 10 59 | Layout.bottomMargin: 10 60 | } 61 | } 62 | 63 | // creates color value from hue, saturation, lightness 64 | function _hsla(h, s, l) { 65 | var c = Qt.hsla(h, s, l, 1) 66 | select.color = c 67 | 68 | if (visible) selectedColor = c 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/NamingDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | import QtQuick.Controls 2.15 as Controls 7 | 8 | import org.kde.Klever 1.0 9 | 10 | Kirigami.PromptDialog { 11 | id: textPromptDialog 12 | 13 | title: "Choose a name" 14 | 15 | property string useCase 16 | property alias shownName: nameField.text 17 | property string realName 18 | property string parentPath 19 | property bool newItem 20 | property bool sideBarAction 21 | property QtObject callingAction 22 | 23 | readonly property QtObject nameField : nameField 24 | 25 | 26 | function throwError(){ 27 | let component = Qt.createComponent("qrc:/contents/ui/dialogs/NamingErrorDialog.qml") 28 | 29 | if (component.status == Component.Ready) { 30 | var dialog = component.createObject(textPromptDialog); 31 | dialog.useCase = useCase 32 | dialog.nameField = nameField 33 | dialog.open() 34 | } 35 | } 36 | 37 | function checkName(){ 38 | // The user just pressed apply without renaming the object 39 | if (nameField.text === shownName && !textPromptDialog.newItem) return true 40 | 41 | let exist = false 42 | if (sideBarAction) { 43 | const newPath = parentPath+"/"+nameField.text 44 | exist = KleverUtility.exists(newPath) 45 | } 46 | 47 | const isDisplay = (nameField.text === Config.categoryDisplayName) 48 | 49 | return !(exist || isDisplay) 50 | } 51 | 52 | 53 | Controls.TextField { 54 | id:nameField 55 | 56 | text: shownName 57 | leftPadding: Kirigami.Units.largeSpacing 58 | rightPadding: Kirigami.Units.largeSpacing 59 | 60 | onSelectedTextChanged: nameField.forceActiveFocus() 61 | 62 | Keys.onPressed: if ((event.key === Qt.Key_Return) || (event.key=== Qt.Key_Enter)) textPromptDialog.applied() 63 | } 64 | 65 | standardButtons: Kirigami.Dialog.Apply | Kirigami.Dialog.Cancel 66 | 67 | onApplied: { 68 | if (checkName()){ 69 | textPromptDialog.close() 70 | callingAction.name = nameField.text 71 | return 72 | } 73 | throwError() 74 | nameField.selectAll() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/resources.qrc: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | contents/ui/main.qml 8 | 9 | contents/ui/dialogs/DeleteConfirmationDialog.qml 10 | contents/ui/dialogs/FilePickerDialog.qml 11 | contents/ui/dialogs/FolderPickerDialog.qml 12 | contents/ui/dialogs/FontPickerDialog.qml 13 | contents/ui/dialogs/ImagePickerDialog.qml 14 | contents/ui/dialogs/NamingDialog.qml 15 | contents/ui/dialogs/NamingErrorDialog.qml 16 | contents/ui/dialogs/StorageDialog.qml 17 | contents/ui/dialogs/TableMakerDialog.qml 18 | contents/ui/dialogs/ToDoDialog.qml 19 | contents/ui/dialogs/URLDialog.qml 20 | 21 | contents/ui/dialogs/colorDialog/ColorDialog.qml 22 | contents/ui/dialogs/colorDialog/ColorPicker.qml 23 | contents/ui/dialogs/colorDialog/HSPicker.qml 24 | contents/ui/dialogs/colorDialog/LightnessSlider.qml 25 | 26 | contents/ui/pages/AboutPage.qml 27 | contents/ui/pages/MainPage.qml 28 | contents/ui/pages/SettingsPage.qml 29 | 30 | contents/ui/sideBar/ActionBar.qml 31 | contents/ui/sideBar/Sidebar.qml 32 | contents/ui/sideBar/SubEntryColumn.qml 33 | contents/ui/sideBar/TreeNode.qml 34 | contents/ui/sideBar/TreeView.qml 35 | 36 | contents/ui/textEditor/BottomToolBar.qml 37 | contents/ui/textEditor/EditorView.qml 38 | contents/ui/textEditor/TextDisplay.qml 39 | contents/ui/textEditor/TextEditor.qml 40 | contents/ui/textEditor/TextToolBar.qml 41 | 42 | contents/ui/todoEditor/ToDoView.qml 43 | 44 | contents/resources/index.html 45 | contents/resources/qwebchannel.js 46 | contents/resources/3rdparty/marked.js 47 | contents/resources/3rdparty/markdown.css 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/contents/ui/main.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import org.kde.Klever 1.0 10 | import "qrc:/contents/ui/sideBar" 11 | 12 | Kirigami.ApplicationWindow { 13 | id: root 14 | 15 | title: i18n("KleverNotes") 16 | 17 | minimumWidth: Kirigami.Units.gridUnit * 25 18 | minimumHeight: Kirigami.Units.gridUnit * 30 19 | 20 | onClosing: { 21 | App.saveWindowGeometry(root) ; 22 | const mainPage = pageStack.get(0) 23 | const editor = mainPage.editorView.editor 24 | editor.saveNote(editor.text, editor.path) 25 | } 26 | 27 | onWidthChanged: saveWindowGeometryTimer.restart() 28 | onHeightChanged: saveWindowGeometryTimer.restart() 29 | onXChanged: saveWindowGeometryTimer.restart() 30 | onYChanged: saveWindowGeometryTimer.restart() 31 | 32 | pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn 33 | 34 | Kirigami.PagePool {id: pagePool} 35 | 36 | globalDrawer: Sidebar{} 37 | 38 | function getPage(name) { 39 | switch (name) { 40 | case "Main": return pagePool.loadPage("qrc:contents/ui/pages/MainPage.qml"); 41 | case "Settings": return pagePool.loadPage("qrc:contents/ui/pages/SettingsPage.qml"); 42 | case "About": return pagePool.loadPage("qrc:contents/ui/pages/AboutPage.qml"); 43 | } 44 | } 45 | 46 | function switchToPage(pageName) { 47 | const page = getPage(pageName) 48 | 49 | pageStack.push(page); 50 | } 51 | 52 | pageStack.onCurrentItemChanged: if (isMainPage() && pageStack.depth > 1) pageStack.pop(1) 53 | 54 | function isMainPage(){ 55 | return pageStack.currentItem == getPage("Main") 56 | } 57 | 58 | // This timer allows to batch update the window size change to reduce 59 | // the io load and also work around the fact that x/y/width/height are 60 | // changed when loading the page and overwrite the saved geometry from 61 | // the previous session. 62 | Timer { 63 | id: saveWindowGeometryTimer 64 | 65 | interval: 1000 66 | onTriggered: App.saveWindowGeometry(root) 67 | } 68 | 69 | Component.onCompleted: { 70 | App.restoreWindowGeometry(root) 71 | switchToPage('Main') 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/contents/logic/mdHandler.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #include "mdHandler.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | MDHandler::MDHandler(QObject *parent) 11 | : QObject(parent) 12 | { 13 | } 14 | 15 | QJsonArray MDHandler::getLines(QString text) 16 | { 17 | return QJsonArray::fromStringList(text.split('\n')); 18 | } 19 | 20 | QStringList MDHandler::getPositionLineInfo(QJsonArray lines, int position) 21 | { 22 | QStringList info; 23 | 24 | int textLength = 0; 25 | 26 | int lineIndex = 0; 27 | QString cursorLine = lines[0].toString(); 28 | int inLineCursorPosition = position; 29 | for (int i = 0; i < lines.size(); i++) { 30 | QString line = lines[i].toString(); 31 | cursorLine = line; 32 | lineIndex = i; 33 | int nextTextLength = textLength + line.length(); 34 | if (nextTextLength >= position) 35 | break; 36 | textLength = nextTextLength + 1; 37 | inLineCursorPosition = position - textLength; 38 | } 39 | info.append(QString::number(lineIndex)); 40 | info.append(cursorLine); 41 | info.append(QString::number(inLineCursorPosition)); 42 | return info; 43 | } 44 | 45 | QJsonObject MDHandler::getInstructions(QString selectedText, QStringList charsList, bool checkLineEnd) 46 | { 47 | QJsonObject final = QJsonObject(); 48 | 49 | QJsonArray selectedLines = getLines(selectedText); 50 | // if (selectedLines.isEmpty()) {selectedLines.append("");}; 51 | final["lines"] = selectedLines; 52 | 53 | QJsonArray instructions; 54 | bool applyToAll = false; 55 | foreach (const QJsonValue &lineRef, selectedLines) { 56 | QString line = lineRef.toString(); 57 | instructions.append("remove"); 58 | if (line.isEmpty() && selectedLines.size() > 1) { 59 | instructions[instructions.size() - 1] = "none"; 60 | continue; 61 | } 62 | bool skip = false; 63 | foreach (const QString &chars, charsList) { 64 | if (line.startsWith(chars)) { 65 | skip = true; 66 | if (checkLineEnd && !line.endsWith(chars)) 67 | skip = false; 68 | break; 69 | } 70 | } 71 | if (skip) 72 | continue; 73 | applyToAll = true; 74 | instructions[instructions.size() - 1] = "apply"; 75 | } 76 | 77 | if (applyToAll) { 78 | for (int i = 0; i < instructions.count(); ++i) { 79 | if (instructions[i] == "remove") { 80 | instructions[i] = "none"; 81 | } 82 | } 83 | } 84 | final["instructions"] = instructions; 85 | 86 | return final; 87 | } 88 | -------------------------------------------------------------------------------- /src/contents/ui/textEditor/EditorView.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | GridLayout{ 10 | id: root 11 | 12 | readonly property TextEditor editor: editor 13 | required property string path 14 | 15 | property list actions: [ 16 | Kirigami.Action { 17 | id: editorToggler 18 | checkable: true 19 | icon.name: checked ? "text-flow-into-frame" : "text-unflow" 20 | checked: true 21 | onCheckedChanged: if (!checked && !viewToggler.checked) checked = true 22 | }, 23 | Kirigami.Action { 24 | id: viewToggler 25 | checkable: true 26 | icon.name: checked ? "quickview" : "view-hidden" 27 | checked: applicationWindow().wideScreen 28 | onCheckedChanged: if (!checked && !editorToggler.checked) checked = true 29 | } 30 | ] 31 | 32 | rows: 4 33 | columns: 2 34 | 35 | TextToolBar { 36 | id: toolbar 37 | 38 | textArea: root.editor 39 | notePath: root.path 40 | 41 | Layout.preferredHeight: childrenRect.height 42 | Layout.column: 0 43 | Layout.columnSpan: 2 44 | } 45 | 46 | // This item can be seen as useless but it prevent a weird bug with the height not being adjusted 47 | Item { 48 | Layout.fillWidth: true 49 | Layout.fillHeight: true 50 | Layout.row: 3 51 | Layout.column: 0 52 | Layout.columnSpan: 2 53 | 54 | GridLayout { 55 | anchors.fill:parent 56 | 57 | columns: parent.width > Kirigami.Units.gridUnit * 30 ? 2 : 1 58 | rows: columns > 1 ? 1 : 2 59 | 60 | TextEditor{ 61 | id: editor 62 | 63 | visible: editorToggler.checked 64 | 65 | path: root.path 66 | 67 | Layout.fillWidth:true 68 | Layout.fillHeight:true 69 | Layout.preferredHeight: parent.columns === 2 ? parent.height : parent.height/2 70 | Layout.preferredWidth: parent.columns === 2 ? parent.width/2 : parent.width 71 | } 72 | 73 | TextDisplay{ 74 | id: display 75 | 76 | visible: viewToggler.checked 77 | 78 | text: editor.text 79 | path: root.path.replace("note.md", "") 80 | 81 | Layout.fillWidth:true 82 | Layout.fillHeight:true 83 | Layout.preferredHeight: parent.columns === 2 ? parent.height : parent.height/2 84 | Layout.preferredWidth: parent.columns === 2 ? parent.width/2 : parent.width 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/contents/logic/view.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | #include "view.h" 5 | #include "kleverconfig.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | View::View(QObject *parent) 18 | : QObject(parent) 19 | { 20 | } 21 | 22 | QJsonObject View::getHierarchy(QString path, int lvl) 23 | { 24 | QJsonObject final = QJsonObject(); 25 | QFileInfo testingFile(path); 26 | final["name"] = QJsonValue(testingFile.fileName()); 27 | 28 | final["displayedName"] = (final["name"] == ".BaseCategory") ? QJsonValue(KleverConfig::categoryDisplayName()) : QJsonValue(final["name"]); 29 | 30 | final["path"] = QJsonValue(testingFile.filePath()); 31 | final["lvl"] = QJsonValue(lvl); 32 | 33 | switch (lvl) { 34 | case 0: 35 | final["useCase"] = QJsonValue("Category"); 36 | break; 37 | case 1: 38 | final["useCase"] = QJsonValue("Group"); 39 | break; 40 | case 2: 41 | final["useCase"] = QJsonValue("Note"); 42 | break; 43 | default: 44 | final["useCase"] = QJsonValue("Root"); 45 | break; 46 | } 47 | 48 | QJsonArray content; 49 | 50 | if (testingFile.isDir()) { 51 | QDir dir(path); 52 | 53 | QFileInfoList fileList = dir.entryInfoList(QDir::Filter::NoDotAndDotDot | QDir::Filter::AllEntries | QDir::Filter::AccessMask); 54 | 55 | foreach (const QFileInfo &file, fileList) { 56 | QString nextPath = file.filePath(); 57 | QString name = file.fileName(); 58 | 59 | if (name == ".directory") 60 | continue; 61 | 62 | if (lvl + 1 < 3) { 63 | QJsonObject entryInfo = getHierarchy(nextPath, lvl + 1); 64 | // A switch could have been nice here but you can't do that with string ;'( 65 | if (name == ".BaseGroup") { 66 | QJsonArray groupContent = entryInfo["content"].toArray(); 67 | 68 | for (int i = groupContent.size(); i-- > 0;) { 69 | QJsonObject object = groupContent[i].toObject(); 70 | object["lvl"] = QJsonValue(object["lvl"].toInt() - 1); 71 | content.insert(0, QJsonValue(object)); 72 | } 73 | } else if (name == ".BaseCategory") { 74 | content.insert(0, QJsonValue(entryInfo)); 75 | } else { 76 | content.append(QJsonValue(entryInfo)); 77 | } 78 | } 79 | } 80 | } 81 | 82 | final["content"] = QJsonValue(content); 83 | 84 | return final; 85 | } 86 | 87 | void View::hierarchySupplier(QString path, int lvl) 88 | { 89 | QJsonObject hierarchy = getHierarchy(path, lvl); 90 | hierarchySent(hierarchy); 91 | } 92 | -------------------------------------------------------------------------------- /src/kleverconfig.kcfg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 12 | 13 | 14 | None 15 | 16 | 17 | 18 | Notes 19 | 20 | 21 | 22 | 23 | New Category 24 | 25 | 26 | 27 | New Group 28 | 29 | 30 | 31 | New Note 32 | 33 | 34 | 35 | 36 | None 37 | 38 | 39 | 40 | None 41 | 42 | 43 | 44 | None 45 | 46 | 47 | 48 | None 49 | 50 | 51 | 52 | None 53 | 54 | 55 | 56 | None 57 | 58 | 59 | 60 | None 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/contents/ui/pages/MainPage.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Layouts 1.15 6 | import QtQuick.Controls 2.15 as QQC2 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import "qrc:/contents/ui/textEditor" 10 | import "qrc:/contents/ui/todoEditor" 11 | 12 | import org.kde.Klever 1.0 13 | 14 | Kirigami.Page { 15 | id: root 16 | 17 | property QtObject currentlySelected 18 | property QtObject editorView: editorLoader.item 19 | property QtObject todoView: todoLoader.item 20 | readonly property bool hasNote: currentlySelected && currentlySelected.useCase === "Note" 21 | 22 | title: hasNote ? currentlySelected.displayedName : i18n("Welcome") 23 | 24 | actions.contextualActions: hasNote 25 | ? editorView.visible 26 | ? editorView.actions 27 | : todoView.actions 28 | : [] 29 | 30 | onCurrentlySelectedChanged: if (root.hasNote) { 31 | const editor = editorView.editor 32 | const oldPath = editorView.path 33 | const text = editor.text 34 | editor.saveNote(text, oldPath) 35 | } 36 | 37 | Loader { 38 | id: editorLoader 39 | 40 | sourceComponent: EditorView { 41 | path: currentlySelected.path + "/note.md" 42 | visible: bottomToolBar.showNoteEditor 43 | } 44 | active: root.hasNote 45 | anchors.fill: parent 46 | } 47 | 48 | Loader { 49 | id: todoLoader 50 | 51 | sourceComponent: ToDoView { 52 | path: currentlySelected.path + "/todo.json" 53 | visible: !bottomToolBar.showNoteEditor 54 | } 55 | active: root.hasNote 56 | anchors.fill: parent 57 | } 58 | 59 | 60 | Kirigami.Card { 61 | id: placeHolder 62 | 63 | visible: !root.hasNote 64 | 65 | anchors.fill: parent 66 | 67 | ColumnLayout { 68 | anchors.fill: parent 69 | Kirigami.Theme.colorSet: Kirigami.Theme.View 70 | Kirigami.Theme.inherit: false 71 | 72 | QQC2.Label { 73 | text: i18n("Welcome to KleverNotes!") 74 | wrapMode: Text.WordWrap 75 | horizontalAlignment: Text.AlignHCenter 76 | font.pointSize: 24 77 | 78 | Layout.margins: Kirigami.Units.largeSpacing * 2 79 | Layout.fillWidth: true 80 | } 81 | 82 | QQC2.Label { 83 | text: i18n("Create or select a note to start working !") 84 | wrapMode: Text.WordWrap 85 | horizontalAlignment: Text.AlignHCenter 86 | font.pointSize: 12 87 | 88 | Layout.margins: Kirigami.Units.largeSpacing * 2 89 | Layout.fillWidth: true 90 | } 91 | } 92 | } 93 | 94 | footer: BottomToolBar{ 95 | id: bottomToolBar 96 | 97 | visible: root.hasNote 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/contents/ui/sideBar/SubEntryColumn.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | 7 | Column{ 8 | id: subEntryColumn 9 | 10 | width: parent.width 11 | height: childrenRect.height * opacity 12 | 13 | property var entries: [] 14 | property int delimiter: 1 15 | 16 | Behavior on visible { NumberAnimation { duration: Kirigami.Units.shortDuration } } 17 | Behavior on height { NumberAnimation { duration: Kirigami.Units.veryShortDuration } } 18 | 19 | 20 | function reorderEntries(useCase) { 21 | let firstHalf = subEntryColumn.entries.slice(0,subEntryColumn.delimiter) 22 | let secondHalf = subEntryColumn.entries.slice(subEntryColumn.delimiter,subEntryColumn.entries.length-1) 23 | 24 | if (useCase === "Note"){ 25 | firstHalf.push(subEntryColumn.entries[subEntryColumn.entries.length-1]) 26 | firstHalf.sort((a,b) => a.localeCompare(b)) 27 | } 28 | else{ 29 | secondHalf.push(subEntryColumn.entries[subEntryColumn.entries.length-1]) 30 | secondHalf.sort((a,b) => a.localeCompare(b)) 31 | } 32 | const ordered = firstHalf.concat(secondHalf) 33 | 34 | return ordered 35 | } 36 | 37 | function reorderSubEntries(useCase) { 38 | const reorderedEntries = reorderEntries(useCase) 39 | 40 | if (useCase === "Note") subEntryColumn.delimiter += 1 41 | 42 | let newChildren = new Array(children.length).fill(0) 43 | for(var i = entries.length-1 ; i >= 0 ; i--){ 44 | const entry = entries[i] 45 | const reorderIndex = reorderedEntries.indexOf(entry) 46 | 47 | let child = children[i] 48 | 49 | newChildren[reorderIndex] = child 50 | } 51 | 52 | subEntryColumn.entries = reorderedEntries 53 | subEntryColumn.children = newChildren 54 | } 55 | 56 | 57 | function addRow(data,forcedLvl,reorder){ 58 | subEntryColumn.entries.push(data.displayedName) 59 | 60 | let component = Qt.createComponent("qrc:/contents/ui/sideBar/TreeNode.qml") 61 | if (component.status == Component.Ready) { 62 | let row = component.createObject(subEntryColumn) 63 | row.useCase = data.useCase 64 | row.parentRow = subEntryColumn.parent 65 | row.name = data.name 66 | row.displayedName = data.displayedName 67 | row.lvl = (!forcedLvl || forcedLvl === Infinity) ? data.lvl : forcedLvl 68 | row.expanded = tree.currentlySelected.path.includes(row.path) 69 | 70 | // This property will be usefull to reorder the category while keeping notes on top when we add an elem 71 | if (row.useCase === "Note" && subEntryColumn.parent.useCase === "Category" && !reorder) subEntryColumn.delimiter += 1 72 | 73 | if (data.content.length > 0) { 74 | const newHolder = row.subEntryColumn 75 | newHolder.addRows(data) 76 | } 77 | if (reorder) { 78 | reorderSubEntries(row.useCase) 79 | } 80 | } 81 | } 82 | 83 | function addRows(hierarchy) { 84 | const content = hierarchy.content 85 | content.forEach(data => { 86 | subEntryColumn.addRow(data) 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/contents/ui/sideBar/TreeView.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import org.kde.kirigami 2.19 as Kirigami 6 | import QtQuick.Controls 2.15 as Controls 7 | import QtQuick.Layouts 1.3 8 | 9 | import org.kde.Klever 1.0 10 | 11 | import "qrc:/contents/ui/dialogs" 12 | 13 | Controls.ScrollView { 14 | id: tree 15 | 16 | width: parent.width 17 | 18 | property bool init: false 19 | property QtObject currentlySelected: subEntryColumn.children[0] 20 | property var hierarchyAsker: [] 21 | 22 | readonly property QtObject subEntryColumn: subEntryColumn 23 | readonly property QtObject deleteConfirmationDialog: deleteConfirmationDialog 24 | 25 | onCurrentlySelectedChanged: { 26 | const mainWindow = applicationWindow() 27 | if(mainWindow.isMainPage()) { 28 | const mainPage = mainWindow.pageStack.currentItem 29 | mainPage.currentlySelected = currentlySelected 30 | } 31 | } 32 | 33 | DeleteConfirmationDialog { 34 | id: deleteConfirmationDialog 35 | 36 | useCase: tree.currentlySelected.useCase 37 | 38 | onAccepted: { 39 | StorageHandler.remove(tree.currentlySelected.path) 40 | } 41 | } 42 | 43 | SubEntryColumn { 44 | id: subEntryColumn 45 | 46 | delimiter : 1 47 | } 48 | 49 | Connections { 50 | target: StorageHandler 51 | 52 | function onStorageUpdated() { 53 | const holder = currentlySelected.parent 54 | const childrenList = holder.visibleChildren 55 | 56 | let nextSelected 57 | for (var childIdx = 0; childIdx < childrenList.length; childIdx++) { 58 | if (currentlySelected == childrenList[childIdx]){ 59 | holder.entries.splice(childIdx, 1) 60 | 61 | if (childIdx-1 >= 0) nextSelected = childrenList[childIdx-1] 62 | else if (childIdx+1 != childrenList.length) nextSelected = childrenList[childIdx+1] 63 | else nextSelected = holder.parent 64 | break 65 | } 66 | } 67 | if (currentlySelected.useCase === "Note") holder.delimiter -= 1 68 | currentlySelected.destroy() 69 | currentlySelected = nextSelected 70 | } 71 | } 72 | 73 | Connections { 74 | target: View 75 | 76 | function onHierarchySent(hierarchy) { 77 | if (tree.hierarchyAsker.length != 0) { 78 | const askerInfo = tree.hierarchyAsker.shift() 79 | 80 | const caller = askerInfo[0] 81 | if (caller == tree) { 82 | tree.subEntryColumn.addRows(hierarchy) 83 | return 84 | } 85 | 86 | const forcedLvl = askerInfo[1] 87 | const reorder = askerInfo[2] 88 | caller.addRow(hierarchy,forcedLvl,askerInfo) 89 | } 90 | } 91 | } 92 | 93 | onVisibleChanged: { 94 | if (visible && !init) { 95 | tree.init = true 96 | const caller = tree 97 | tree.hierarchyAsker.push([caller]) 98 | View.hierarchySupplier(Config.storagePath,-1) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/contents/ui/sideBar/Sidebar.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | /* 5 | * BASED ON : 6 | * https://invent.kde.org/graphics/koko/-/blob/master/src/qml/Sidebar.qml 7 | * 2017 Atul Sharma 8 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 9 | */ 10 | 11 | import QtQuick 2.15 12 | import QtQuick.Layouts 1.3 13 | import QtQuick.Controls 2.15 as Controls 14 | import org.kde.kirigami 2.5 as Kirigami 15 | 16 | import org.kde.Klever 1.0 17 | 18 | 19 | 20 | Kirigami.OverlayDrawer { 21 | id:drawer 22 | 23 | edge: Qt.application.layoutDirection == Qt.RightToLeft ? Qt.RightEdge : Qt.LeftEdge 24 | handleClosedIcon.source: null 25 | handleOpenIcon.source: null 26 | handleVisible: applicationWindow().isMainPage() && modal && pageStack.layers.depth < 2 27 | 28 | // Autohiding behavior 29 | modal: !applicationWindow().isMainPage() || !root.wideScreen 30 | onEnabledChanged: drawerOpen = enabled && !modal 31 | onModalChanged: drawerOpen = !modal && pageStack.layers.depth < 2 32 | // Prevent it to being close while in wideScreen 33 | closePolicy: Controls.Popup.CloseOnReleaseOutside 34 | 35 | leftPadding: 0 36 | rightPadding: 0 37 | topPadding: 0 38 | bottomPadding: 0 39 | 40 | property bool storageExist: Config.storagePath !== "None" 41 | 42 | contentItem: ColumnLayout { 43 | id: column 44 | // FIXME: Dirty workaround for 385992 45 | implicitWidth: Kirigami.Units.gridUnit * 14 46 | 47 | ActionBar{ 48 | id: action 49 | 50 | treeView: treeview 51 | 52 | Layout.fillWidth: true 53 | Layout.preferredHeight: pageStack.globalToolBar.preferredHeight 54 | } 55 | 56 | TreeView{ 57 | id: treeview 58 | 59 | visible: storageExist 60 | 61 | Layout.fillHeight: true 62 | Layout.alignment:Qt.AlignTop 63 | } 64 | 65 | Item{ 66 | id: dummyPlaceHolder 67 | 68 | Layout.fillHeight: !storageExist 69 | Layout.alignment:Qt.AlignTop 70 | } 71 | 72 | 73 | Controls.ToolSeparator { 74 | orientation: Qt.Horizontal 75 | 76 | Layout.topMargin: -1; 77 | Layout.fillWidth: true 78 | Layout.alignment:Qt.AlignBottom 79 | } 80 | 81 | Kirigami.BasicListItem { 82 | text: i18n("Settings") 83 | icon: "settings-configure" 84 | 85 | Layout.alignment:Qt.AlignBottom 86 | 87 | onClicked: applicationWindow().switchToPage('Settings') 88 | } 89 | 90 | Kirigami.BasicListItem { 91 | text: i18n("About Klever") 92 | icon: "help-about" 93 | 94 | onClicked: {drawerOpen = false,applicationWindow().switchToPage('About')} 95 | } 96 | 97 | } 98 | 99 | Component.onCompleted: { 100 | if (!storageExist){ 101 | let component = Qt.createComponent("qrc:/contents/ui/dialogs/StorageDialog.qml") 102 | 103 | if (component.status == Component.Ready) { 104 | var dialog = component.createObject(applicationWindow()); 105 | dialog.open() 106 | } 107 | } 108 | } 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/colorDialog/HSPicker.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.11 5 | 6 | Item { 7 | id: root 8 | 9 | property int cursorRadius : 10 10 | property real h 11 | property real s 12 | 13 | Rectangle { 14 | // This way the middle of the cursor can trully be in a corner 15 | x : cursorRadius 16 | y : cursorRadius + parent.height - 2 * cursorRadius 17 | width: parent.height - 2 * cursorRadius 18 | height: parent.width - 2 * cursorRadius 19 | rotation: -90 20 | transformOrigin: Item.TopLeft 21 | gradient: Gradient { 22 | GradientStop { position: 0.0; color: "#FF0000" } 23 | GradientStop { position: 0.16; color: "#FFFF00" } 24 | GradientStop { position: 0.33; color: "#00FF00" } 25 | GradientStop { position: 0.5; color: "#00FFFF" } 26 | GradientStop { position: 0.76; color: "#0000FF" } 27 | GradientStop { position: 0.85; color: "#FF00FF" } 28 | GradientStop { position: 1.0; color: "#FF0000" } 29 | } 30 | } 31 | 32 | Rectangle { 33 | id:bouderies 34 | x: cursorRadius 35 | y: cursorRadius 36 | width: parent.width - 2 * cursorRadius 37 | height: parent.height - 2 * cursorRadius 38 | gradient: Gradient { 39 | GradientStop { position: 1.0; color: "#FFFFFFFF" } 40 | GradientStop { position: 0.0; color: "#00000000" } 41 | } 42 | } 43 | 44 | Rectangle{ 45 | id:pickerCursor 46 | visible: !mouseArea.containsPress 47 | color:"transparent" 48 | width: cursorRadius*2 49 | height: cursorRadius*2 50 | x: root.h * bouderies.width 51 | y: (root.s * bouderies.height * -1) + bouderies.height 52 | 53 | Rectangle{ 54 | id:north 55 | color:"black" 56 | x: cursorRadius-1 57 | anchors.top: parent.top 58 | height: cursorRadius 59 | width:2 60 | } 61 | 62 | Rectangle{ 63 | id:east 64 | color:"black" 65 | y:cursorRadius-1 66 | anchors.right: parent.right 67 | height: 2 68 | width:cursorRadius 69 | } 70 | 71 | Rectangle{ 72 | id:south 73 | color:"black" 74 | x:cursorRadius-1 75 | anchors.bottom: parent.bottom 76 | height: cursorRadius 77 | width:2 78 | } 79 | 80 | Rectangle{ 81 | id:west 82 | color:"black" 83 | y:cursorRadius-1 84 | anchors.left: parent.left 85 | height: 2 86 | width:cursorRadius 87 | } 88 | 89 | } 90 | 91 | MouseArea { 92 | id:mouseArea 93 | anchors.fill: bouderies 94 | 95 | cursorShape: (containsPress) ? Qt.CrossCursor : Qt.ArrowCursor 96 | 97 | function handleMouse(mouse) { 98 | if (mouse.buttons & Qt.LeftButton) { 99 | pickerCursor.x = Math.max(0, Math.min(bouderies.width, mouse.x)); 100 | pickerCursor.y = Math.max(0, Math.min(bouderies.height, mouse.y)); 101 | 102 | root.h = (pickerCursor.x/bouderies.width) 103 | root.s = ((bouderies.height-pickerCursor.y)/bouderies.height) 104 | } 105 | } 106 | onPositionChanged: handleMouse(mouse) 107 | onPressed: handleMouse(mouse) 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/StorageDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.3 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import org.kde.Klever 1.0 10 | 11 | Kirigami.Dialog { 12 | id: setupPopup 13 | 14 | width: Kirigami.Units.gridUnit * 22 15 | 16 | closePolicy: Controls.Popup.NoAutoClose 17 | standardButtons: Kirigami.Dialog.NoButton 18 | 19 | property string subtitle: i18n("It looks like this is your first time using this app!\n\nPlease choose a location for your future KleverNotes storage or select an existing one.") 20 | property bool firstSetup: true 21 | property string folder 22 | property string userChoice 23 | showCloseButton: !firstSetup 24 | 25 | ColumnLayout{ 26 | width: parent.width 27 | 28 | Controls.Label { 29 | text: subtitle 30 | wrapMode: Text.Wrap 31 | 32 | Layout.fillWidth: true 33 | Layout.margins: Kirigami.Units.smallSpacing 34 | } 35 | 36 | RowLayout { 37 | Layout.fillWidth: true 38 | Layout.fillHeight: true 39 | 40 | Controls.Button{ 41 | icon.name: "folder-sync" 42 | text: i18n("Existing storage") 43 | 44 | Controls.ToolTip.delay: Kirigami.Units.toolTipDelay 45 | Controls.ToolTip.visible: hovered 46 | Controls.ToolTip.text: i18n("Change the storage path") 47 | 48 | Layout.fillWidth: true 49 | Layout.margins: Kirigami.Units.smallSpacing 50 | 51 | onClicked: { 52 | setupPopup.userChoice = "Existing storage chosen at " 53 | getFolder() 54 | } 55 | } 56 | 57 | Controls.Button{ 58 | icon.name: "folder-new" 59 | text: i18n("Create storage") 60 | 61 | Controls.ToolTip.delay: Kirigami.Units.toolTipDelay 62 | Controls.ToolTip.visible: hovered 63 | Controls.ToolTip.text: i18n("Create a new storage") 64 | 65 | Layout.fillWidth: true 66 | Layout.margins: Kirigami.Units.smallSpacing 67 | 68 | onClicked: { 69 | setupPopup.userChoice = "Storage created at " 70 | getFolder() 71 | } 72 | } 73 | } 74 | } 75 | 76 | function getFolder() { 77 | let component = Qt.createComponent("qrc:/contents/ui/dialogs/FolderPickerDialog.qml") 78 | 79 | if (component.status == Component.Ready) { 80 | var dialog = component.createObject(setupPopup); 81 | dialog.parent = setupPopup 82 | dialog.open() 83 | } 84 | } 85 | 86 | onFolderChanged: { 87 | let folderPath = KleverUtility.getPath(setupPopup.folder) 88 | if (userChoice === "Storage created at "){ 89 | folderPath = folderPath.concat("/klevernotes") 90 | StorageHandler.makeStorage(folderPath) 91 | } 92 | 93 | var pathEnd = folderPath.substring(folderPath.length,folderPath.length-11) 94 | 95 | if (pathEnd.toLowerCase() !== "klevernotes"){ 96 | subtitle = i18n("It looks like the selected folder is not a KleverNotes storage.\n\nPlease choose a location for your future KleverNotes storage or select an existing one.") 97 | return 98 | } 99 | Config.storagePath = folderPath 100 | 101 | const fullNotification = setupPopup.userChoice+folderPath 102 | 103 | showPassiveNotification(fullNotification); 104 | setupPopup.close(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/contents/ui/textEditor/TextDisplay.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | // This is based on https://github.com/CrazyCxl/markdown-editor 5 | // SPDX-License-Identifier: GPL-3.0 6 | 7 | 8 | import QtQuick 2.2 9 | import QtQuick.Controls 2.2 10 | import QtWebChannel 1.0 11 | import QtWebEngine 1.6 12 | import QtQuick.Layouts 1.15 13 | import org.kde.kirigami 2.19 as Kirigami 14 | import Qt.labs.platform 1.1 15 | 16 | import qtMdEditor 1.0 as QtMdEditor 17 | import org.kde.Klever 1.0 18 | 19 | RowLayout { 20 | id: root 21 | 22 | required property string path 23 | required property string text 24 | 25 | property var defaultCSS: { 26 | '--bodyColor': Config.viewBodyColor !== "None" ? Config.viewBodyColor : Kirigami.Theme.backgroundColor, 27 | '--font': Config.viewFont !== "None" ? Config.viewFont : Kirigami.Theme.defaultFont, 28 | '--textColor': Config.viewTextColor !== "None" ? Config.viewTextColor : Kirigami.Theme.textColor, 29 | '--titleColor': Config.viewTitleColor !== "None" ? Config.viewTitleColor : Kirigami.Theme.disabledTextColor, 30 | '--linkColor': Config.viewLinkColor !== "None" ? Config.viewLinkColor : Kirigami.Theme.linkColor, 31 | '--visitedLinkColor': Config.viewVisitedLinkColor !== "None" ? Config.viewVisitedLinkColor : Kirigami.Theme.visitedLinkColor, 32 | '--codeColor': Config.viewCodeColor !== "None" ? Config.viewCodeColor : Kirigami.Theme.alternateBackgroundColor, 33 | } 34 | 35 | spacing:0 36 | 37 | onDefaultCSSChanged: if(web_view.loadProgress === 100) changeStyle() 38 | 39 | Kirigami.Theme.colorSet: Kirigami.Theme.View 40 | Kirigami.Theme.inherit: false 41 | 42 | Kirigami.Card{ 43 | id:background 44 | 45 | Layout.fillWidth: true 46 | Layout.fillHeight: true 47 | 48 | WebEngineView{ 49 | id:web_view 50 | 51 | onJavaScriptConsoleMessage: console.error('WEB:', message, lineNumber, sourceID) 52 | 53 | settings.showScrollBars:false 54 | 55 | width: background.width - 4 56 | height:background.height - 4 57 | x: 2 58 | y: 2 59 | url: "qrc:/index.html" 60 | focus: true 61 | 62 | webChannel: WebChannel{ 63 | registeredObjects: [editorLink, cssLink, homePathPasser, notePathPasser] 64 | } 65 | 66 | onLoadProgressChanged: if (loadProgress === 100) { 67 | changeStyle() 68 | } 69 | 70 | QtMdEditor.QmlLinker{ 71 | id: editorLink 72 | text: root.text 73 | WebChannel.id: "linkToEditor" 74 | } 75 | 76 | QtMdEditor.QmlLinker{ 77 | id: cssLink 78 | css: {"key":"value"} 79 | WebChannel.id: "linkToCss" 80 | } 81 | 82 | QtMdEditor.QmlLinker{ 83 | id: homePathPasser 84 | homePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0].substring("file://".length) 85 | WebChannel.id: "homePathPasser" 86 | } 87 | 88 | QtMdEditor.QmlLinker{ 89 | id: notePathPasser 90 | notePath: root.path 91 | WebChannel.id: "notePathPasser" 92 | } 93 | } 94 | 95 | MouseArea{ 96 | anchors.fill:web_view 97 | enabled:true 98 | 99 | onWheel:{ 100 | (wheel.angleDelta.y > 0) 101 | ? vbar.decrease() 102 | : vbar.increase() 103 | } 104 | 105 | // Use to make some test 106 | 107 | onClicked: web_view.runJavaScript("document.body.innerHTML",function(result) { console.log(result); }); 108 | 109 | } 110 | } 111 | 112 | ScrollBar { 113 | id: vbar 114 | 115 | hoverEnabled: true 116 | active: hovered || pressed 117 | orientation: Qt.Vertical 118 | size: background.height / web_view.contentsSize.height 119 | stepSize: 0.03 120 | snapMode: ScrollBar.SnapOnRelease 121 | onPositionChanged: { 122 | let scrollY = web_view.contentsSize.height*vbar.position + 5 123 | web_view.runJavaScript("window.scrollTo(0,"+scrollY+")") 124 | } 125 | 126 | Layout.row:0 127 | Layout.column:1 128 | Layout.fillHeight: true 129 | } 130 | 131 | function changeStyle() { 132 | cssLink.css = defaultCSS 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/contents/ui/sideBar/TreeNode.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | /* 5 | * LOOSELY BASED ON : 6 | * https://gist.github.com/anonymous/85b53283c66395f98302 7 | */ 8 | import QtQuick 2.15 9 | import org.kde.kirigami 2.19 as Kirigami 10 | import QtQuick.Controls 2.15 as Controls 11 | import QtQuick.Layouts 1.3 12 | 13 | import org.kde.Klever 1.0 14 | 15 | 16 | Column { 17 | id: infoRow 18 | 19 | height: childrenRect.height 20 | 21 | property alias displayedName: textDisplay.text 22 | 23 | // Not super clean but this make it easier to handle rename 24 | property string parentPath 25 | property QtObject parentRow 26 | onParentRowChanged: { 27 | width = parentRow.width 28 | 29 | // The parentRow is possibly the subEntryColumn, so we need to acces is parent 30 | const parPath = parentRow.path 31 | 32 | // The path is undifined if the parentRow is the treeview 33 | parentPath = (parPath != undefined) 34 | ? (useCase === "Note" && parentRow.useCase === "Category") 35 | ? parPath+"/.BaseGroup" 36 | : parPath 37 | : Config.storagePath 38 | } 39 | property string name 40 | property bool expanded 41 | property string useCase 42 | property int lvl 43 | 44 | readonly property string path: parentPath+"/"+name 45 | readonly property QtObject mouseArea: controlRoot 46 | readonly property QtObject textDisplay: textDisplay 47 | readonly property QtObject subEntryColumn: subEntryColumn 48 | 49 | Rectangle{ 50 | id:visualRow 51 | 52 | width: infoRow.width 53 | height: textDisplay.height 54 | visible: true 55 | 56 | color: (tree.currentlySelected === infoRow) 57 | ? Kirigami.Theme.highlightColor 58 | : "transparent" 59 | 60 | Kirigami.Icon { 61 | id: carot 62 | 63 | x: 20 * infoRow.lvl 64 | width: Kirigami.Units.iconSizes.small 65 | height: textDisplay.height 66 | source: infoRow.expanded ? "go-down-symbolic" : "go-next-symbolic" 67 | isMask: true 68 | color: !(tree.currentlySelected === infoRow) 69 | ?(controlRoot.hovered) 70 | ? Kirigami.Theme.highlightColor 71 | : Kirigami.Theme.textColor 72 | : Kirigami.Theme.textColor 73 | 74 | Behavior on color { 75 | ColorAnimation { 76 | duration: Kirigami.Units.shortDuration; 77 | easing.type: Easing.InOutQuad 78 | } 79 | } 80 | visible: infoRow.useCase != "Note" 81 | } 82 | 83 | 84 | Text { 85 | id: textDisplay 86 | 87 | x: carot.x + 20 88 | height: 25 89 | width: infoRow.width-x 90 | font.pointSize: 12 91 | 92 | color: !(tree.currentlySelected === infoRow) 93 | ?(controlRoot.hovered) 94 | ? Kirigami.Theme.highlightColor 95 | : Kirigami.Theme.textColor 96 | : Kirigami.Theme.textColor 97 | 98 | elide: Text.ElideRight 99 | } 100 | 101 | MouseArea { 102 | id: controlRoot 103 | 104 | anchors.fill: parent 105 | 106 | hoverEnabled: true 107 | enabled: true 108 | acceptedButtons: Qt.LeftButton | Qt.RightButton 109 | 110 | property bool hovered : false 111 | 112 | onEntered: hovered = true 113 | onExited: hovered = false 114 | onClicked: { 115 | tree.currentlySelected = infoRow 116 | visualRow.forceActiveFocus() 117 | 118 | if (mouse.button === Qt.RightButton && 119 | infoRow.path !== Config.storagePath+"/.BaseCategory") contextMenu.popup() 120 | 121 | else if (infoRow.useCase !== "Note") infoRow.expanded = !infoRow.expanded; 122 | } 123 | 124 | Controls.Menu { 125 | id: contextMenu 126 | 127 | Controls.MenuItem { 128 | icon.name: "user-trash-symbolic" 129 | text: i18n("Delete") 130 | 131 | onTriggered: tree.deleteConfirmationDialog.open() 132 | } 133 | } 134 | } 135 | // For futur keyboard nav 136 | // Keys.onPressed: console.log("hey") 137 | } 138 | 139 | 140 | SubEntryColumn { 141 | id: subEntryColumn 142 | 143 | visible: opacity > 0 144 | opacity: infoRow.expanded ? 1 : 0 145 | delimiter: 0 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-License-Identifier: GPL-2.0-or-later 3 | SPDX-FileCopyrightText: 2022 Louis Schul 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "app.h" 13 | #include 14 | #include 15 | #include 16 | 17 | #include "contents/logic/mdHandler.h" 18 | #include "contents/logic/qmlLinker.h" 19 | #include "contents/logic/view.h" 20 | #include "kleverconfig.h" 21 | #include "logic/documentHandler.h" 22 | #include "logic/kleverUtility.h" 23 | #include "logic/storageHandler.h" 24 | #include "logic/todoHandler.h" 25 | 26 | Q_DECL_EXPORT int main(int argc, char *argv[]) 27 | { 28 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 29 | QtWebEngine::initialize(); 30 | QApplication app(argc, argv); 31 | QCoreApplication::setOrganizationName(QStringLiteral("KDE")); 32 | QCoreApplication::setApplicationName(QStringLiteral("KleverNotes")); 33 | 34 | KLocalizedString::setApplicationDomain("klevernotes"); 35 | 36 | KAboutData aboutData( 37 | // The program name used internally. 38 | QStringLiteral("KleverNotes"), 39 | // A displayable program name string. 40 | i18nc("@title", "KleverNotes"), 41 | // The program version string. 42 | QStringLiteral("1.0"), 43 | // Short description of what the app does. 44 | i18n("Application Description"), 45 | // The license this code is released under. 46 | KAboutLicense::GPL, 47 | // Copyright Statement. 48 | i18n("(c) 2022-2023")); 49 | aboutData.addAuthor(i18nc("@info:credit", "Louis Schul"), 50 | // i18nc("@info:credit", "Author Role"), 51 | QStringLiteral("schul9louis@outlook.fr") 52 | // QStringLiteral("https://yourwebsite.com") 53 | ); 54 | aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); 55 | KAboutData::setApplicationData(aboutData); 56 | 57 | QQmlApplicationEngine engine; 58 | 59 | auto config = KleverConfig::self(); 60 | QObject::connect(config, &KleverConfig::storagePathChanged, config, &KleverConfig::save); 61 | QObject::connect(config, &KleverConfig::defaultCategoryNameChanged, config, &KleverConfig::save); 62 | QObject::connect(config, &KleverConfig::defaultGroupNameChanged, config, &KleverConfig::save); 63 | QObject::connect(config, &KleverConfig::defaultNoteNameChanged, config, &KleverConfig::save); 64 | QObject::connect(config, &KleverConfig::categoryDisplayNameChanged, config, &KleverConfig::save); 65 | QObject::connect(config, &KleverConfig::viewBodyColorChanged, config, &KleverConfig::save); 66 | QObject::connect(config, &KleverConfig::viewTextColorChanged, config, &KleverConfig::save); 67 | QObject::connect(config, &KleverConfig::viewTitleColorChanged, config, &KleverConfig::save); 68 | QObject::connect(config, &KleverConfig::viewLinkColorChanged, config, &KleverConfig::save); 69 | QObject::connect(config, &KleverConfig::viewVisitedLinkColorChanged, config, &KleverConfig::save); 70 | QObject::connect(config, &KleverConfig::viewCodeColorChanged, config, &KleverConfig::save); 71 | 72 | qmlRegisterType("qtMdEditor", 1, 0, "QmlLinker"); 73 | 74 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "Config", config); 75 | 76 | qmlRegisterSingletonType("org.kde.Klever", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue { 77 | return engine->toScriptValue(KAboutData::applicationData()); 78 | }); 79 | 80 | App application; 81 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "App", &application); 82 | 83 | StorageHandler storageHandler; 84 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "StorageHandler", &storageHandler); 85 | 86 | TodoHandler todoHandler; 87 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "TodoHandler", &todoHandler); 88 | 89 | KleverUtility kleverUtility; 90 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "KleverUtility", &kleverUtility); 91 | 92 | DocumentHandler documentHandler; 93 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "DocumentHandler", &documentHandler); 94 | 95 | MDHandler mdHandler; 96 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "MDHandler", &mdHandler); 97 | 98 | View view; 99 | qmlRegisterSingletonInstance("org.kde.Klever", 1, 0, "View", &view); 100 | 101 | engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); 102 | engine.load(QUrl(QStringLiteral("qrc:/contents/ui/main.qml"))); 103 | 104 | if (engine.rootObjects().isEmpty()) { 105 | return -1; 106 | } 107 | 108 | return app.exec(); 109 | } 110 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/contents/ui/todoEditor/ToDoView.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import org.kde.Klever 1.0 10 | 11 | import "qrc:/contents/ui/dialogs" 12 | 13 | ColumnLayout { 14 | id: root 15 | 16 | property list actions: [ 17 | Kirigami.Action { 18 | id: clearTodosAction 19 | 20 | icon.name: "edit-clear-history" 21 | text: i18n("Clear checked") 22 | 23 | onTriggered: clearCheckedTodos() 24 | } 25 | ] 26 | property string path 27 | property int alreadySavedCount: 0 28 | 29 | onPathChanged: { 30 | todoModel.clear() 31 | setTodos() 32 | } 33 | 34 | ToDoDialog { 35 | id: todoDialog 36 | 37 | onAccepted: { 38 | if (name.length > 0) { 39 | if (callerModelIndex < 0) { 40 | todoModel.append({ 41 | "todoTitle": name, 42 | "todoDesc": description.trim(), 43 | "todoChecked": false 44 | }) 45 | } else { 46 | todoModel.setProperty(callerModelIndex, "todoTitle", name) 47 | todoModel.setProperty(callerModelIndex, "todoDesc", description) 48 | } 49 | } 50 | saveTodos() 51 | name = "" 52 | description = "" 53 | callerModelIndex = -1 54 | } 55 | } 56 | 57 | Kirigami.Card { 58 | Layout.fillWidth: true 59 | Layout.fillHeight: true 60 | 61 | Kirigami.CardsListView { 62 | id: layout 63 | 64 | anchors.fill: parent 65 | 66 | model: todoModel 67 | delegate: todoDelegate 68 | } 69 | 70 | ListModel { 71 | id: todoModel 72 | } 73 | 74 | Component { 75 | id: todoDelegate 76 | Kirigami.AbstractCard { 77 | id: card 78 | 79 | width: parent.width 80 | 81 | contentItem: Item { 82 | id: holder 83 | implicitWidth: parent.width 84 | implicitHeight: Kirigami.Units.iconSizes.large 85 | 86 | RowLayout { 87 | id: delegateLayout 88 | 89 | anchors.fill: parent 90 | 91 | Controls.CheckBox { 92 | id: check 93 | 94 | checked: todoChecked 95 | onCheckedChanged: { 96 | todoModel.setProperty(index, "todoChecked", checked) 97 | root.saveTodos() 98 | } 99 | Layout.alignment: Qt.AlignVCenter 100 | } 101 | 102 | ColumnLayout { 103 | Layout.fillWidth: true 104 | Layout.fillHeight: true 105 | 106 | Kirigami.Heading { 107 | id: displayTitle 108 | 109 | text: title 110 | level: 2 111 | elide: Text.ElideRight 112 | 113 | Layout.fillWidth: true 114 | Layout.rightMargin: Kirigami.Units.smallSpacing 115 | } 116 | Kirigami.Separator { 117 | visible: descriptionLabel.text.length > 0 118 | 119 | Layout.fillWidth: true 120 | Layout.rightMargin: Kirigami.Units.smallSpacing 121 | } 122 | 123 | ColumnLayout { 124 | id: expendable 125 | 126 | Layout.fillWidth: true 127 | Layout.fillHeight: true 128 | Layout.margins: 0 129 | spacing: 0 130 | 131 | Controls.Label { 132 | id: descriptionLabel 133 | 134 | text: desc 135 | wrapMode: Text.WordWrap 136 | elide: Text.ElideRight 137 | 138 | Layout.fillWidth: true 139 | Layout.fillHeight: true 140 | Layout.margins: 0 141 | } 142 | } 143 | } 144 | 145 | Controls.Button { 146 | id: editButton 147 | 148 | icon.name: "document-edit" 149 | icon.height: Kirigami.Units.iconSizes.small 150 | 151 | Layout.alignment: Qt.AlignVCenter | Qt.AlignRight 152 | 153 | onClicked: { 154 | todoDialog.callerModelIndex = index 155 | todoDialog.name = displayTitle.text 156 | todoDialog.description = descriptionLabel.text 157 | todoDialog.open() 158 | } 159 | } 160 | } 161 | } 162 | 163 | footer: Controls.Button{ 164 | id: expend 165 | 166 | height: Kirigami.Units.iconSizes.small * 2 167 | 168 | icon.name: descriptionLabel.implicitHeight > expendable.height ? "expand" : "collapse" 169 | icon.height: Kirigami.Units.iconSizes.small 170 | 171 | visible: (descriptionLabel.implicitHeight - 10 > expendable.height || 172 | holder.implicitHeight > Kirigami.Units.iconSizes.large) 173 | 174 | onClicked: holder.implicitHeight = 175 | holder.implicitHeight == Kirigami.Units.iconSizes.large 176 | ? delegateLayout.implicitHeight 177 | : Kirigami.Units.iconSizes.large 178 | } 179 | } 180 | } 181 | } 182 | 183 | Controls.Button { 184 | icon.name: "list-add-symbolic" 185 | icon.width: Kirigami.Units.iconSizes.medium 186 | icon.height: Kirigami.Units.iconSizes.medium 187 | 188 | Layout.fillWidth: true 189 | 190 | onClicked: todoDialog.open() 191 | } 192 | 193 | function clearCheckedTodos() { 194 | for (var idx = todoModel.count-1 ; idx >= 0 ; idx--){ 195 | const model = todoModel.get(idx) 196 | if (model.todoChecked) { 197 | todoModel.remove(idx,1) 198 | } 199 | } 200 | saveTodos() 201 | } 202 | 203 | function setTodos() { 204 | const todos = TodoHandler.readTodos(root.path).todos 205 | root.alreadySavedCount = todos.length 206 | 207 | todos.forEach(todo => todoModel.append(todo)) 208 | } 209 | 210 | function saveTodos() { 211 | let json = {"todos":[]} 212 | 213 | for (var idx = 0 ; idx < todoModel.count ; idx++){ 214 | const model = todoModel.get(idx) 215 | json.todos.push(model) 216 | } 217 | TodoHandler.writeTodos(json, root.path) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /LICENSES/LGPL-3.0-only.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | This version of the GNU Lesser General Public License incorporates the terms 11 | and conditions of version 3 of the GNU General Public License, supplemented 12 | by the additional permissions listed below. 13 | 14 | 0. Additional Definitions. 15 | 16 | 17 | 18 | As used herein, "this License" refers to version 3 of the GNU Lesser General 19 | Public License, and the "GNU GPL" refers to version 3 of the GNU General Public 20 | License. 21 | 22 | 23 | 24 | "The Library" refers to a covered work governed by this License, other than 25 | an Application or a Combined Work as defined below. 26 | 27 | 28 | 29 | An "Application" is any work that makes use of an interface provided by the 30 | Library, but which is not otherwise based on the Library. Defining a subclass 31 | of a class defined by the Library is deemed a mode of using an interface provided 32 | by the Library. 33 | 34 | 35 | 36 | A "Combined Work" is a work produced by combining or linking an Application 37 | with the Library. The particular version of the Library with which the Combined 38 | Work was made is also called the "Linked Version". 39 | 40 | 41 | 42 | The "Minimal Corresponding Source" for a Combined Work means the Corresponding 43 | Source for the Combined Work, excluding any source code for portions of the 44 | Combined Work that, considered in isolation, are based on the Application, 45 | and not on the Linked Version. 46 | 47 | 48 | 49 | The "Corresponding Application Code" for a Combined Work means the object 50 | code and/or source code for the Application, including any data and utility 51 | programs needed for reproducing the Combined Work from the Application, but 52 | excluding the System Libraries of the Combined Work. 53 | 54 | 1. Exception to Section 3 of the GNU GPL. 55 | 56 | You may convey a covered work under sections 3 and 4 of this License without 57 | being bound by section 3 of the GNU GPL. 58 | 59 | 2. Conveying Modified Versions. 60 | 61 | If you modify a copy of the Library, and, in your modifications, a facility 62 | refers to a function or data to be supplied by an Application that uses the 63 | facility (other than as an argument passed when the facility is invoked), 64 | then you may convey a copy of the modified version: 65 | 66 | a) under this License, provided that you make a good faith effort to ensure 67 | that, in the event an Application does not supply the function or data, the 68 | facility still operates, and performs whatever part of its purpose remains 69 | meaningful, or 70 | 71 | b) under the GNU GPL, with none of the additional permissions of this License 72 | applicable to that copy. 73 | 74 | 3. Object Code Incorporating Material from Library Header Files. 75 | 76 | The object code form of an Application may incorporate material from a header 77 | file that is part of the Library. You may convey such object code under terms 78 | of your choice, provided that, if the incorporated material is not limited 79 | to numerical parameters, data structure layouts and accessors, or small macros, 80 | inline functions and templates (ten or fewer lines in length), you do both 81 | of the following: 82 | 83 | a) Give prominent notice with each copy of the object code that the Library 84 | is used in it and that the Library and its use are covered by this License. 85 | 86 | b) Accompany the object code with a copy of the GNU GPL and this license document. 87 | 88 | 4. Combined Works. 89 | 90 | You may convey a Combined Work under terms of your choice that, taken together, 91 | effectively do not restrict modification of the portions of the Library contained 92 | in the Combined Work and reverse engineering for debugging such modifications, 93 | if you also do each of the following: 94 | 95 | a) Give prominent notice with each copy of the Combined Work that the Library 96 | is used in it and that the Library and its use are covered by this License. 97 | 98 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 99 | document. 100 | 101 | c) For a Combined Work that displays copyright notices during execution, include 102 | the copyright notice for the Library among these notices, as well as a reference 103 | directing the user to the copies of the GNU GPL and this license document. 104 | 105 | d) Do one of the following: 106 | 107 | 0) Convey the Minimal Corresponding Source under the terms of this License, 108 | and the Corresponding Application Code in a form suitable for, and under terms 109 | that permit, the user to recombine or relink the Application with a modified 110 | version of the Linked Version to produce a modified Combined Work, in the 111 | manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 112 | 113 | 1) Use a suitable shared library mechanism for linking with the Library. A 114 | suitable mechanism is one that (a) uses at run time a copy of the Library 115 | already present on the user's computer system, and (b) will operate properly 116 | with a modified version of the Library that is interface-compatible with the 117 | Linked Version. 118 | 119 | e) Provide Installation Information, but only if you would otherwise be required 120 | to provide such information under section 6 of the GNU GPL, and only to the 121 | extent that such information is necessary to install and execute a modified 122 | version of the Combined Work produced by recombining or relinking the Application 123 | with a modified version of the Linked Version. (If you use option 4d0, the 124 | Installation Information must accompany the Minimal Corresponding Source and 125 | Corresponding Application Code. If you use option 4d1, you must provide the 126 | Installation Information in the manner specified by section 6 of the GNU GPL 127 | for conveying Corresponding Source.) 128 | 129 | 5. Combined Libraries. 130 | 131 | You may place library facilities that are a work based on the Library side 132 | by side in a single library together with other library facilities that are 133 | not Applications and are not covered by this License, and convey such a combined 134 | library under terms of your choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based on the 137 | Library, uncombined with any other library facilities, conveyed under the 138 | terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it is a work 141 | based on the Library, and explaining where to find the accompanying uncombined 142 | form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions of the 147 | GNU Lesser General Public License from time to time. Such new versions will 148 | be similar in spirit to the present version, but may differ in detail to address 149 | new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the Library as you 152 | received it specifies that a certain numbered version of the GNU Lesser General 153 | Public License "or any later version" applies to it, you have the option of 154 | following the terms and conditions either of that published version or of 155 | any later version published by the Free Software Foundation. If the Library 156 | as you received it does not specify a version number of the GNU Lesser General 157 | Public License, you may choose any version of the GNU Lesser General Public 158 | License ever published by the Free Software Foundation. 159 | 160 | If the Library as you received it specifies that a proxy can decide whether 161 | future versions of the GNU Lesser General Public License shall apply, that 162 | proxy's public statement of acceptance of any version is permanent authorization 163 | for you to choose that version for the Library. 164 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/ImagePickerDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.3 as Controls 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | import Qt.labs.platform 1.1 9 | 10 | import org.kde.Klever 1.0 11 | 12 | import "qrc:/contents/ui/dialogs" 13 | 14 | 15 | Kirigami.Dialog { 16 | id: imagePickerDialog 17 | 18 | title: i18n("Image selector") 19 | 20 | height: header.height + footer.height + topPadding + bottomPadding + mainItem.height 21 | 22 | property string noteImagesStoringPath 23 | 24 | property bool storedImagesExist: !KleverUtility.isEmptyDir(noteImagesStoringPath) 25 | property string path: "" 26 | property alias imageName: nameTextField.text 27 | property bool storedImageChoosen: false 28 | 29 | readonly property QtObject imageObject: displayImage 30 | readonly property bool imageLoaded: displayImage.visible 31 | readonly property bool storeImage: storeCheckbox.checked && !storedImageChoosen 32 | 33 | onPathChanged: displayImage.source = path 34 | 35 | FilePickerDialog { 36 | id: filePickerDialog 37 | 38 | caller: imagePickerDialog 39 | } 40 | 41 | URLDialog { 42 | id: urlDialog 43 | 44 | caller: imagePickerDialog 45 | } 46 | 47 | Column { 48 | id: mainItem 49 | 50 | padding: Kirigami.Units.largeSpacing 51 | spacing: Kirigami.Units.largeSpacing 52 | 53 | Layout.alignment: Qt.AlignTop 54 | Row { 55 | id: buttonHolder 56 | 57 | property int visibleChildrenCount: (storageButton.visible) ? 3 : 2 58 | 59 | spacing: Kirigami.Units.largeSpacing 60 | 61 | Controls.ToolButton { 62 | id: internetButton 63 | 64 | icon.name: "internet-amarok" 65 | icon.width: icon.height 66 | icon.height: imageHolder.visible 67 | ? internetButton.height 68 | : Kirigami.Units.iconSizes.large * (5-buttonHolder.visibleChildrenCount) 69 | 70 | text: i18n("Web image") 71 | 72 | display: imageHolder.visible 73 | ? Controls.AbstractButton.TextBesideIcon 74 | : Controls.AbstractButton.TextUnderIcon 75 | 76 | width: Kirigami.Units.iconSizes.huge * (5-buttonHolder.visibleChildrenCount) 77 | 78 | height: imageHolder.visible 79 | ? Kirigami.Units.iconSizes.medium 80 | : width 81 | 82 | onClicked: { 83 | storedImageChoosen = false 84 | urlDialog.open() 85 | } 86 | } 87 | 88 | Kirigami.Separator{ 89 | height: internetButton.height 90 | } 91 | 92 | Controls.ToolButton { 93 | id: localButton 94 | 95 | icon.name: "document-open-folder" 96 | icon.height: internetButton.icon.height 97 | icon.width: icon.height 98 | 99 | text: i18n("Local image") 100 | 101 | display: internetButton.display 102 | width: internetButton.width 103 | height: internetButton.height 104 | 105 | onClicked: { 106 | storedImageChoosen = false 107 | filePickerDialog.folder = StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] 108 | filePickerDialog.open() 109 | } 110 | } 111 | 112 | Kirigami.Separator{ 113 | height: internetButton.height 114 | visible: imagePickerDialog.storedImagesExist 115 | } 116 | 117 | Controls.ToolButton { 118 | id: storageButton 119 | 120 | icon.name: "dblatex" 121 | icon.width: icon.height 122 | icon.height: internetButton.icon.height 123 | 124 | text: i18n("Stored image") 125 | 126 | display: internetButton.display 127 | width: internetButton.width 128 | height: internetButton.height 129 | 130 | visible: imagePickerDialog.storedImagesExist 131 | 132 | onClicked: { 133 | storedImageChoosen = true 134 | filePickerDialog.folder = "file://"+imagePickerDialog.noteImagesStoringPath 135 | filePickerDialog.open() 136 | } 137 | } 138 | } 139 | 140 | Kirigami.Separator{ 141 | visible: imageHolder.visible 142 | width: internetButton.width * buttonHolder.visibleChildrenCount 143 | anchors.horizontalCenter: parent.horizontalCenter 144 | } 145 | 146 | 147 | Item { 148 | id: imageHolder 149 | 150 | visible: path != "" 151 | height: Kirigami.Units.iconSizes.huge * 3 152 | width: internetButton.width * buttonHolder.visibleChildrenCount 153 | Image { 154 | id: displayImage 155 | 156 | source: path 157 | fillMode: Image.PreserveAspectFit 158 | visible: displayImage.status == Image.Ready 159 | 160 | property int idealWidth 161 | property int idealHeight 162 | 163 | anchors.horizontalCenter: parent.horizontalCenter 164 | 165 | onStatusChanged: if (status == Image.Ready){ 166 | // If the image is placed inside the note folder, we want it to be max 1024x1024 167 | if (Math.max(implicitWidth,implicitHeight,1024) == 1024) { 168 | idealWidth = implicitWidth 169 | idealHeight = implicitHeight 170 | } else { 171 | let divider = (implicitHeight > implicitWidth) 172 | ? implicitHeight/1024 173 | : implicitWidth/1024 174 | 175 | idealWidth = Math.round(implicitWidth/divider) 176 | idealHeight = Math.round(implicitHeight/divider) 177 | } 178 | height = Kirigami.Units.iconSizes.huge * 3 179 | } 180 | } 181 | 182 | Text { 183 | Kirigami.Theme.colorSet: Kirigami.Theme.View 184 | Kirigami.Theme.inherit: false 185 | color: Kirigami.Theme.textColor 186 | 187 | text: i18n("It seems that the image you select doesn't exist or is not supported.") 188 | wrapMode: Text.Wrap 189 | visible: !displayImage.visible 190 | 191 | anchors.fill: parent 192 | anchors.horizontalCenter: parent.horizontalCenter 193 | } 194 | } 195 | 196 | Kirigami.Separator{ 197 | width: internetButton.width * buttonHolder.visibleChildrenCount 198 | anchors.horizontalCenter: parent.horizontalCenter 199 | } 200 | 201 | RowLayout { 202 | width: internetButton.width * buttonHolder.visibleChildrenCount 203 | anchors.horizontalCenter: parent.horizontalCenter 204 | 205 | spacing: Kirigami.Units.smallSpacing 206 | Controls.Label { 207 | text: i18n("Image text: ") 208 | } 209 | 210 | Controls.TextField { 211 | id: nameTextField 212 | 213 | focus: true 214 | Layout.fillWidth: true 215 | } 216 | 217 | Layout.margins: Kirigami.Units.largeSpacing 218 | } 219 | 220 | Controls.CheckBox { 221 | id: storeCheckbox 222 | 223 | checked: true 224 | text: i18n("Place this image inside the note folder") 225 | width: internetButton.width * buttonHolder.visibleChildrenCount 226 | visible: displayImage.visible && !storedImageChoosen 227 | 228 | anchors.horizontalCenter: parent.horizontalCenter 229 | } 230 | } 231 | 232 | standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 233 | 234 | onOpened: {path = "" ; imageName = ""; displayImage.height = undefined ;} 235 | } 236 | 237 | -------------------------------------------------------------------------------- /src/contents/ui/sideBar/ActionBar.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 6 | import QtQuick.Layouts 1.15 7 | import QtQuick.Dialogs 1.1 8 | import org.kde.kirigami 2.19 as Kirigami 9 | 10 | import org.kde.Klever 1.0 11 | 12 | import "qrc:/contents/ui/dialogs" 13 | 14 | ToolBar { 15 | id: mainToolBar 16 | 17 | property QtObject treeView: undefined 18 | readonly property QtObject infoRow: treeView.currentlySelected 19 | 20 | function getName(useCase,shownName,realName,parentPath,callingAction,newItem){ 21 | namingDialog.useCase = useCase 22 | namingDialog.shownName = shownName 23 | namingDialog.realName = realName 24 | namingDialog.parentPath = parentPath 25 | namingDialog.callingAction = callingAction 26 | namingDialog.newItem = newItem 27 | namingDialog.open() 28 | namingDialog.nameField.selectAll() 29 | namingDialog.nameField.forceActiveFocus() 30 | } 31 | 32 | function makeRow(subEntryColumn,useCase,creatingPath,name,forcedLvl){ 33 | const newPath = creatingPath+"/"+name 34 | let lvl 35 | switch(useCase) { 36 | case "Category": 37 | StorageHandler.makeCategory(creatingPath,name) 38 | lvl = 0 39 | break 40 | case "Group": 41 | StorageHandler.makeGroup(creatingPath,name) 42 | lvl = 1 43 | break 44 | case "Note": 45 | StorageHandler.makeNote(creatingPath,name) 46 | lvl = 2 47 | break 48 | } 49 | const caller = subEntryColumn 50 | 51 | forcedLvl = (forcedLvl) ? forcedLvl : Infinity 52 | 53 | treeView.hierarchyAsker.push([caller,forcedLvl,true]) 54 | View.hierarchySupplier(newPath,lvl) 55 | } 56 | 57 | NamingDialog { 58 | id: namingDialog 59 | 60 | sideBarAction: true 61 | } 62 | 63 | Kirigami.Action { 64 | id: createCategoryAction 65 | 66 | icon.name: "journal-new" 67 | 68 | property bool isActive : false 69 | property string name 70 | 71 | onNameChanged: { 72 | if (isActive) { 73 | console.log(treeView.subEntryColumn) 74 | makeRow(treeView.subEntryColumn,"Category",Config.storagePath,name) 75 | isActive = false 76 | name = "" 77 | } 78 | } 79 | 80 | onTriggered: { 81 | isActive = true 82 | const objectName = Config.defaultCategoryName 83 | mainToolBar.getName("Category",objectName,objectName,Config.storagePath,createCategoryAction,true) 84 | } 85 | } 86 | Kirigami.Action { 87 | id: createGroupAction 88 | 89 | icon.name: "folder-new" 90 | 91 | property bool isActive : false 92 | property string categoryPath 93 | property QtObject subEntryColumn 94 | property string name 95 | 96 | onNameChanged: { 97 | if (isActive) { 98 | makeRow(subEntryColumn,"Group",categoryPath,name) 99 | isActive = false 100 | name = "" 101 | } 102 | } 103 | 104 | onTriggered: { 105 | isActive = true 106 | const parentRow = infoRow.parentRow 107 | 108 | switch(infoRow.useCase) { 109 | case "Category": 110 | categoryPath = infoRow.path 111 | subEntryColumn = infoRow.subEntryColumn 112 | break; 113 | case "Group": 114 | categoryPath = infoRow.parentPath 115 | subEntryColumn = parentRow.subEntryColumn 116 | break; 117 | case "Note": 118 | // A note can be inside a group or a category 119 | if (parentRow.useCase == "Group"){ 120 | categoryPath = parentRow.parentPath 121 | subEntryColumn = parentRow.parentRow.subEntryColumn 122 | } 123 | else { 124 | categoryPath = parentRow.path 125 | subEntryColumn = parentRow.subEntryColumn 126 | } 127 | break; 128 | } 129 | 130 | const objectName = Config.defaultGroupName 131 | mainToolBar.getName("Group",objectName,objectName,categoryPath,createGroupAction,true) 132 | } 133 | } 134 | 135 | Kirigami.Action { 136 | id: createNoteAction 137 | 138 | icon.name: "document-new" 139 | 140 | property bool isActive : false 141 | property string groupPath 142 | property QtObject subEntryColumn 143 | property string name 144 | property int forcedLvl 145 | 146 | onNameChanged: { 147 | if (isActive) { 148 | makeRow(subEntryColumn,"Note",groupPath,name,forcedLvl) 149 | isActive = false 150 | name = "" 151 | forcedLvl = Infinity 152 | } 153 | } 154 | 155 | onTriggered: { 156 | isActive = true 157 | switch(infoRow.useCase) { 158 | case "Category": 159 | groupPath = infoRow.path+"/.BaseGroup" 160 | subEntryColumn = infoRow.subEntryColumn 161 | forcedLvl = 1 162 | break; 163 | case "Group": 164 | groupPath = infoRow.path 165 | subEntryColumn = infoRow.subEntryColumn 166 | break; 167 | case "Note": 168 | groupPath = infoRow.parentPath 169 | if (groupPath.endsWith("/.BaseGroup")) forcedLvl = 1 170 | subEntryColumn = infoRow.parentRow.subEntryColumn 171 | break; 172 | } 173 | 174 | const objectName = Config.defaultNoteName 175 | console.log(objectName) 176 | mainToolBar.getName("Note",objectName,objectName,groupPath,createNoteAction,true) 177 | } 178 | } 179 | 180 | Kirigami.Action{ 181 | id: renameAction 182 | 183 | icon.name: "edit-rename" 184 | 185 | property bool isActive : false 186 | property string name : infoRow.textDisplay.text 187 | 188 | onNameChanged: { 189 | if (isActive) { 190 | infoRow.displayedName = name 191 | if (infoRow.name == ".BaseCategory") { 192 | Config.categoryDisplayName = name 193 | } 194 | else { 195 | const oldPath = infoRow.path 196 | const newPath = infoRow.parentPath+"/"+name 197 | StorageHandler.rename(oldPath,newPath) 198 | } 199 | isActive = false 200 | } 201 | } 202 | 203 | onTriggered: { 204 | isActive = true 205 | mainToolBar.getName(infoRow.useCase,name,infoRow.name,infoRow.parentPath,renameAction,false) 206 | } 207 | } 208 | 209 | 210 | RowLayout { 211 | anchors.fill: parent 212 | spacing: 0 213 | 214 | ToolButton { 215 | action: createCategoryAction 216 | ToolTip.delay: Kirigami.Units.toolTipDelay 217 | ToolTip.visible: hovered 218 | ToolTip.text: i18n("Create a new category") 219 | } 220 | 221 | ToolButton { 222 | action: createGroupAction 223 | ToolTip.delay: Kirigami.Units.toolTipDelay 224 | ToolTip.visible: hovered 225 | ToolTip.text: i18n("Create a new group") 226 | } 227 | 228 | ToolButton { 229 | action: createNoteAction 230 | ToolTip.delay: Kirigami.Units.toolTipDelay 231 | ToolTip.visible: hovered 232 | ToolTip.text: i18n("Create a new note") 233 | } 234 | 235 | ToolButton { 236 | action: renameAction 237 | ToolTip.delay: Kirigami.Units.toolTipDelay 238 | ToolTip.visible: hovered 239 | ToolTip.text: i18n("Rename") 240 | } 241 | 242 | Item { Layout.fillWidth: true} 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/contents/ui/dialogs/TableMakerDialog.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Layouts 1.15 6 | import QtQuick.Controls 2.15 as Controls 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | Kirigami.Dialog { 10 | id: scrollableDialog 11 | 12 | readonly property QtObject listView: listView 13 | readonly property int rowCount: rowSpin.value 14 | readonly property int columnCount: columnSpin.value 15 | 16 | title: i18n("Select Number") 17 | 18 | width: Kirigami.Units.gridUnit * 18 19 | 20 | Column { 21 | width: parent.width 22 | spacing: Kirigami.Units.largeSpacing 23 | RowLayout { 24 | width: parent.width 25 | height: 50 26 | Text { 27 | text: i18n("Row") 28 | horizontalAlignment: Text.AlignHCenter 29 | color: Kirigami.Theme.textColor 30 | 31 | Layout.alignment: Qt.AlignHCenter 32 | Layout.leftMargin: Kirigami.Units.largeSpacing 33 | } 34 | Controls.SpinBox{ 35 | id: rowSpin 36 | 37 | from: 1 38 | 39 | Layout.fillWidth: true 40 | Layout.alignment: Qt.AlignHCenter 41 | } 42 | 43 | Text { 44 | text: i18n("Column") 45 | horizontalAlignment: Text.AlignHCenter 46 | color: Kirigami.Theme.textColor 47 | 48 | Layout.alignment: Qt.AlignHCenter 49 | } 50 | Controls.SpinBox{ 51 | id: columnSpin 52 | 53 | from: 1 54 | 55 | Layout.fillWidth: true 56 | Layout.alignment: Qt.AlignHCenter 57 | Layout.rightMargin: Kirigami.Units.largeSpacing 58 | } 59 | 60 | Layout.margins: Kirigami.Units.largeSpacing 61 | } 62 | 63 | Item { 64 | implicitWidth: Kirigami.Units.gridUnit * 15 65 | implicitHeight: 30 66 | anchors.horizontalCenter: parent.horizontalCenter 67 | RowLayout { 68 | anchors.fill: parent 69 | 70 | Item { 71 | height: 20 72 | Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 73 | Layout.margins: 0 74 | } 75 | 76 | Kirigami.Icon { 77 | source: "format-justify-left" 78 | 79 | Layout.alignment: Qt.AlignHCenter 80 | Layout.fillWidth: true 81 | Layout.fillHeight: true 82 | Layout.margins: 0 83 | } 84 | Kirigami.Icon { 85 | source: "format-justify-center" 86 | 87 | Layout.alignment: Qt.AlignHCenter 88 | Layout.fillWidth: true 89 | Layout.fillHeight: true 90 | Layout.margins: 0 91 | } 92 | Kirigami.Icon { 93 | source: "format-justify-right" 94 | 95 | Layout.alignment: Qt.AlignHCenter 96 | Layout.fillWidth: true 97 | Layout.fillHeight: true 98 | Layout.margins: 0 99 | } 100 | } 101 | } 102 | 103 | 104 | Item { 105 | implicitWidth: Kirigami.Units.gridUnit * 18 106 | implicitHeight: Kirigami.Units.gridUnit * 6 107 | anchors.horizontalCenter: parent.horizontalCenter 108 | ListView { 109 | id: listView 110 | 111 | implicitWidth: Kirigami.Units.gridUnit * 15 112 | implicitHeight: Kirigami.Units.gridUnit * 4 113 | 114 | anchors.horizontalCenter: parent.horizontalCenter 115 | anchors.verticalCenter: parent.verticalCenter 116 | 117 | Controls.ScrollBar.vertical: Controls.ScrollBar { 118 | active: true 119 | } 120 | 121 | 122 | model: columnSpin.value 123 | delegate: Item { 124 | height: 30 125 | width: listView.width 126 | 127 | RowLayout { 128 | anchors.fill: parent 129 | spacing: 0 130 | 131 | Controls.ButtonGroup { 132 | id: radioGroup 133 | } 134 | 135 | Text { 136 | text: modelData+1 137 | Layout.preferredWidth: Kirigami.Units.gridUnit * 1 138 | color: Kirigami.Theme.textColor 139 | } 140 | 141 | Controls.RadioButton { 142 | id: control 143 | 144 | property string align: "left" 145 | 146 | indicator.x: control.width/2 147 | 148 | checked: checkAllLeft.checked 149 | Controls.ButtonGroup.group: radioGroup 150 | 151 | Layout.fillWidth: true 152 | Layout.margins: 0 153 | 154 | background: Rectangle { 155 | color: "transparent" 156 | } 157 | } 158 | 159 | Controls.RadioButton { 160 | property string align: "center" 161 | 162 | indicator.x: control.width/2 163 | 164 | checked: checkAllCenter.checked 165 | Controls.ButtonGroup.group: radioGroup 166 | 167 | Layout.fillWidth: true 168 | Layout.margins: 0 169 | 170 | 171 | background: Rectangle { 172 | color: "transparent" 173 | } 174 | } 175 | 176 | Controls.RadioButton { 177 | property string align: "right" 178 | 179 | indicator.x: control.width/2 180 | 181 | checked: checkAllRight.checked 182 | Controls.ButtonGroup.group: radioGroup 183 | 184 | Layout.fillWidth: true 185 | Layout.margins: 0 186 | 187 | background: Rectangle { 188 | color: "transparent" 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | Item { 197 | implicitWidth: Kirigami.Units.gridUnit * 15 198 | implicitHeight: 30 199 | anchors.horizontalCenter: parent.horizontalCenter 200 | Controls.ButtonGroup { id: alignGroup } 201 | RowLayout { 202 | anchors.fill: parent 203 | 204 | Text { 205 | text: i18n("Align all:") 206 | 207 | color: Kirigami.Theme.textColor 208 | 209 | Layout.preferredWidth: Kirigami.Units.gridUnit * 1 210 | Layout.margins: 0 211 | } 212 | 213 | Controls.RadioButton { 214 | id: checkAllLeft 215 | 216 | indicator.x: checkAllLeft.width/2.1 217 | 218 | checked: true 219 | 220 | Layout.fillWidth: true 221 | Layout.fillHeight: true 222 | Layout.margins: 0 223 | } 224 | 225 | Controls.RadioButton { 226 | id: checkAllCenter 227 | 228 | indicator.x: checkAllCenter.width/2.1 229 | 230 | Layout.fillWidth: true 231 | Layout.fillHeight: true 232 | Layout.margins: 0 233 | } 234 | 235 | Controls.RadioButton { 236 | id: checkAllRight 237 | 238 | indicator.x: checkAllRight.width/2.1 239 | 240 | Layout.fillWidth: true 241 | Layout.fillHeight: true 242 | Layout.margins: 0 243 | } 244 | } 245 | } 246 | } 247 | 248 | standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel 249 | 250 | onOpened: { 251 | columnSpin.value = 1 252 | rowSpin.value = 1 253 | checkAllLeft.checked = true 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/contents/ui/textEditor/TextToolBar.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2022 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 6 | import QtQuick.Layouts 1.15 7 | import org.kde.kirigami 2.19 as Kirigami 8 | import QtQuick.Dialogs 1.3 9 | 10 | import org.kde.Klever 1.0 11 | 12 | import "qrc:/contents/ui/dialogs" 13 | 14 | Kirigami.Card { 15 | id: toolbarHolder 16 | 17 | required property TextEditor textArea 18 | required property string notePath 19 | 20 | // This 'replicate' the DefaultCardBackground and just change the background color 21 | //(https://api.kde.org/frameworks/kirigami/html/DefaultCardBackground_8qml_source.html) 22 | background: Kirigami.ShadowedRectangle{ 23 | Kirigami.Theme.colorSet: Kirigami.Theme.Header 24 | Kirigami.Theme.inherit: false 25 | color: Kirigami.Theme.backgroundColor 26 | radius: Kirigami.Units.smallSpacing 27 | } 28 | 29 | ImagePickerDialog { 30 | id: imagePickerDialog 31 | 32 | noteImagesStoringPath: toolbarHolder.notePath + "Images/" 33 | 34 | onAccepted: if (imageLoaded) { 35 | let modifiedPath = path 36 | 37 | let useLocalImage = storedImageChoosen 38 | if (storeImage && !storedImageChoosen){ 39 | let wantedImageName = imageName 40 | if (imageName == ""){ 41 | const fileName = KleverUtility.getName(path) 42 | wantedImageName = fileName.substring(0,fileName.lastIndexOf(".")) 43 | } 44 | 45 | // We can't asign the result to modifiedPath and use it to saveToFile or it won't work ! 46 | const validPath = KleverUtility.getImageStoragingPath(noteImagesStoringPath, wantedImageName) 47 | modifiedPath = validPath 48 | 49 | imageObject.grabToImage(function(result) { 50 | result.saveToFile(validPath) 51 | },Qt.size(imageObject.idealWidth,imageObject.idealHeight)); 52 | 53 | useLocalImage = true 54 | 55 | storedImagesExist = true 56 | } 57 | 58 | if (modifiedPath.startsWith("file://")) modifiedPath = modifiedPath.replace("file://","") 59 | 60 | if (useLocalImage) modifiedPath = "./"+modifiedPath.replace(toolbarHolder.notePath,"") 61 | 62 | if (modifiedPath.startsWith("/home/")) { 63 | // Get the first "/" after the /home/username 64 | modifiedPath = modifiedPath.replace("/home/","") 65 | const idx = modifiedPath.indexOf("/") 66 | modifiedPath = "~" + modifiedPath.substring(idx) 67 | } 68 | 69 | let imageString = `![${imageName}](${modifiedPath}) ` 70 | toolbarHolder.textArea.insert(toolbarHolder.textArea.cursorPosition, imageString) 71 | } 72 | } 73 | 74 | TableMakerDialog { 75 | id: tableMakerDialog 76 | 77 | onAccepted: { 78 | const alignPattern = {"left":":------","center":":------:","right":"------:"} 79 | const cells = "|"+" |".repeat(tableMakerDialog.columnCount)+"\n" 80 | const headers = "|"+(i18n("Header")+"|").repeat(tableMakerDialog.columnCount)+"\n" 81 | 82 | let columnsAlignments = "|" 83 | 84 | for(var childIdx = 0; childIdx < tableMakerDialog.listView.count ; childIdx++) { 85 | const columnAlignment = tableMakerDialog.listView.itemAtIndex(childIdx).children[0].data[0].checkedButton.align 86 | 87 | columnsAlignments = columnsAlignments.concat(alignPattern[columnAlignment],"|") 88 | } 89 | columnsAlignments +="\n" 90 | 91 | const result = "\n"+headers+columnsAlignments+cells.repeat(tableMakerDialog.rowCount-1) 92 | 93 | toolbarHolder.textArea.insert(toolbarHolder.textArea.cursorPosition, result) 94 | } 95 | } 96 | 97 | Kirigami.ActionToolBar { 98 | id: mainToolBar 99 | 100 | function applyInstructions(selectionEnd,info,specialChars,multiPlaceApply){ 101 | const instructions = info.instructions 102 | const lines = info.lines 103 | let end = selectionEnd 104 | let applied = false 105 | 106 | for (var i = lines.length-1 ; i >= 0; i--){ 107 | const line = lines[i] 108 | const instruction = instructions[i] 109 | 110 | const start = end-line.length 111 | switch(instruction) { 112 | case "apply": 113 | if (multiPlaceApply) toolbarHolder.textArea.insert(end,specialChars) 114 | toolbarHolder.textArea.insert(start,specialChars) 115 | 116 | applied = true 117 | break; 118 | case "remove": 119 | if (multiPlaceApply) toolbarHolder.textArea.remove(end-specialChars.length,end) 120 | toolbarHolder.textArea.remove(start,start+specialChars.length) 121 | break; 122 | default: 123 | break 124 | } 125 | end = start-1 126 | } 127 | if (applied) toolbarHolder.textArea.select(toolbarHolder.textArea.selectionStart-specialChars.length,toolbarHolder.textArea.selectionEnd) 128 | } 129 | 130 | function handleAction(selectionStart,selectionEnd,specialChars,multiPlaceApply) { 131 | const selectedText = textArea.getText(selectionStart,selectionEnd) 132 | const info = MDHandler.getInstructions(selectedText,specialChars,multiPlaceApply) 133 | 134 | const appliedSpecialChars = specialChars[0] 135 | mainToolBar.applyInstructions(selectionEnd,info,appliedSpecialChars,multiPlaceApply) 136 | } 137 | 138 | function getLinesBlock(selectionStart,selectionEnd) { 139 | const startingText = textArea.getText(0,textArea.selectionStart) 140 | const endingText = textArea.getText(textArea.selectionEnd,textArea.text.length) 141 | 142 | 143 | const startBlockIndex = startingText.lastIndexOf('\n')+1 144 | 145 | return [startBlockIndex,textArea.selectionEnd] 146 | } 147 | 148 | actions: [ 149 | Kirigami.Action { 150 | text: "𝐇" 151 | 152 | Kirigami.Action { 153 | text: "𝐇𝟏" 154 | onTriggered: { 155 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 156 | 157 | mainToolBar.handleAction(selectionStart, selectionEnd,["# "],false) 158 | } 159 | } 160 | Kirigami.Action { 161 | text: "𝐇𝟐" 162 | onTriggered: { 163 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 164 | 165 | mainToolBar.handleAction(selectionStart, selectionEnd,["## "],false) 166 | } 167 | } 168 | Kirigami.Action { 169 | text: "𝐇𝟑" 170 | onTriggered: { 171 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 172 | 173 | mainToolBar.handleAction(selectionStart, selectionEnd,["### "],false) 174 | } 175 | } 176 | Kirigami.Action { 177 | text: "𝐇𝟒" 178 | onTriggered: { 179 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 180 | 181 | mainToolBar.handleAction(selectionStart, selectionEnd,["#### "],false) 182 | } 183 | } 184 | Kirigami.Action { 185 | text: "𝐇𝟓" 186 | onTriggered: { 187 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 188 | 189 | mainToolBar.handleAction(selectionStart, selectionEnd,["###### "],false) 190 | } 191 | } 192 | Kirigami.Action { 193 | text: "𝐇𝟔" 194 | onTriggered: { 195 | const [selectionStart, selectionEnd] = mainToolBar.getLinesBlock(textArea.selectionStart,textArea.selectionEnd); 196 | 197 | mainToolBar.handleAction(selectionStart, selectionEnd,["####### "],false) 198 | } 199 | } 200 | 201 | }, 202 | Kirigami.Action { 203 | id: boldAction 204 | icon.name: "format-text-bold" 205 | onTriggered: mainToolBar.handleAction(textArea.selectionStart,textArea.selectionEnd,["**","__"],true) 206 | }, 207 | Kirigami.Action { 208 | id: italicAction 209 | icon.name: "format-text-italic" 210 | onTriggered: mainToolBar.handleAction(textArea.selectionStart,textArea.selectionEnd,["_","*"],true) 211 | }, 212 | Kirigami.Action { 213 | id: strikethroughAction 214 | icon.name: "format-text-strikethrough" 215 | onTriggered: mainToolBar.handleAction(textArea.selectionStart,textArea.selectionEnd,["~~"],true) 216 | }, 217 | Kirigami.Action { 218 | id: codeBlockAction 219 | icon.name: "format-text-code" 220 | onTriggered: mainToolBar.handleAction(textArea.selectionStart,textArea.selectionEnd,["\n```\n"],true) 221 | }, 222 | Kirigami.Action { 223 | id: quoteAction 224 | icon.name: "format-text-blockquote" 225 | onTriggered: mainToolBar.handleAction(textArea.selectionStart,textArea.selectionEnd,["> "],false) 226 | }, 227 | Kirigami.Action { 228 | id: imageAction 229 | icon.name: "insert-image" 230 | onTriggered: imagePickerDialog.open() 231 | }, 232 | Kirigami.Action { 233 | id: tableAction 234 | icon.name: "insert-table" 235 | onTriggered: tableMakerDialog.open() 236 | } 237 | ] 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/contents/ui/pages/SettingsPage.qml: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | // SPDX-FileCopyrightText: 2023 Louis Schul 3 | 4 | import QtQuick 2.15 5 | import QtQuick.Controls 2.15 as Controls 6 | import QtQuick.Layouts 1.3 7 | import org.kde.kirigami 2.19 as Kirigami 8 | 9 | import org.kde.Klever 1.0 10 | 11 | import "qrc:/contents/ui/dialogs" 12 | import "qrc:/contents/ui/dialogs/colorDialog" 13 | 14 | Kirigami.ScrollablePage { 15 | id:settingsPage 16 | 17 | title: i18nc("@title:window", "Settings") 18 | 19 | function updateName(shownName,callingAction){ 20 | namingDialog.shownName = shownName 21 | namingDialog.callingAction = callingAction 22 | namingDialog.open() 23 | namingDialog.nameField.selectAll() 24 | namingDialog.nameField.forceActiveFocus() 25 | } 26 | 27 | function updateColor(button, selectedColor) { 28 | switch(button.parent.name) { 29 | case "background": 30 | Config.viewBodyColor = selectedColor 31 | break; 32 | case "text": 33 | Config.viewTextColor = selectedColor 34 | break; 35 | case "title": 36 | Config.viewTitleColor = selectedColor 37 | break; 38 | case "link": 39 | Config.viewLinkColor = selectedColor 40 | break; 41 | case "visitedLink": 42 | Config.viewVisitedLinkColor = selectedColor 43 | break; 44 | case "code": 45 | Config.viewCodeColor = selectedColor 46 | break; 47 | } 48 | } 49 | 50 | StorageDialog { 51 | id: storageDialog 52 | 53 | subtitle: i18n("Please choose a location for your future KleverNotes storage or select an existing one.\n") 54 | firstSetup: false 55 | } 56 | 57 | NamingDialog { 58 | id: namingDialog 59 | 60 | sideBarAction: false 61 | useCase: "" 62 | parentPath: "" 63 | realName: shownName 64 | newItem: false 65 | } 66 | 67 | ColorDialog { 68 | id: colorPicker 69 | 70 | property QtObject caller 71 | 72 | selectedColor: caller.color 73 | 74 | onApplied: { 75 | if (selectedColor != caller.color) updateColor(caller, selectedColor) 76 | colorPicker.close() 77 | } 78 | } 79 | 80 | FontPickerDialog{ 81 | id: fontDialog 82 | 83 | onApplied: { 84 | Config.viewFont = checkedFamily 85 | fontDialog.close() 86 | } 87 | } 88 | 89 | Kirigami.FormLayout { 90 | Kirigami.Separator { 91 | Kirigami.FormData.label: i18n("General") 92 | Kirigami.FormData.isSection: true 93 | } 94 | 95 | Row { 96 | Kirigami.FormData.label: i18n("Storage path:") 97 | 98 | Controls.TextField { 99 | text: Config.storagePath 100 | readOnly: true 101 | } 102 | 103 | Controls.Button{ 104 | text: i18n("Change storage path") 105 | onClicked: storageDialog.open() 106 | } 107 | } 108 | 109 | Item { 110 | Kirigami.FormData.isSection: true 111 | } 112 | 113 | Controls.TextField { 114 | id: newCategoryField 115 | 116 | Kirigami.FormData.label: i18n("New Category name:") 117 | 118 | readOnly: true 119 | text: Config.defaultCategoryName 120 | 121 | property bool isActive: false 122 | property string name 123 | 124 | onNameChanged: { 125 | if (isActive) { 126 | Config.defaultCategoryName = name 127 | isActive = false 128 | name = "" 129 | } 130 | } 131 | 132 | MouseArea { 133 | anchors.fill: parent 134 | onClicked: { 135 | newCategoryField.isActive = true 136 | updateName(newCategoryField.text, newCategoryField) 137 | } 138 | } 139 | } 140 | 141 | Controls.TextField { 142 | id: newGroupField 143 | 144 | Kirigami.FormData.label: i18n("New Group name:") 145 | 146 | readOnly: true 147 | text: Config.defaultGroupName 148 | 149 | property bool isActive: false 150 | property string name 151 | 152 | onNameChanged: { 153 | if (isActive) { 154 | Config.defaultCategoryName = name 155 | isActive = false 156 | name = "" 157 | } 158 | } 159 | 160 | MouseArea { 161 | anchors.fill: parent 162 | onClicked: { 163 | newGroupField.isActive = true 164 | updateName(newGroupField.text, newGroupField) 165 | } 166 | } 167 | } 168 | 169 | Controls.TextField { 170 | id: newNoteField 171 | 172 | Kirigami.FormData.label: i18n("New Note name:") 173 | 174 | readOnly: true 175 | text: Config.defaultNoteName 176 | 177 | property bool isActive: false 178 | property string name 179 | 180 | onNameChanged: { 181 | if (isActive) { 182 | Config.defaultCategoryName = name 183 | isActive = false 184 | name = "" 185 | } 186 | } 187 | 188 | MouseArea { 189 | anchors.fill: parent 190 | onClicked: { 191 | newNoteField.isActive = newNoteField.true 192 | updateName(newNoteField.text, newNoteField) 193 | } 194 | } 195 | } 196 | 197 | Kirigami.Separator { 198 | Kirigami.FormData.label: i18n("Note display") 199 | Kirigami.FormData.isSection: true 200 | } 201 | 202 | // We need to put the button in a raw to prevent a weird display bug and align the label and button correctly 203 | Row { 204 | Kirigami.FormData.label: i18n("Background color:") 205 | 206 | property string name: "background" 207 | 208 | Controls.Button { 209 | id: backgroundButton 210 | 211 | width: Kirigami.Units.largeSpacing * 20 212 | 213 | anchors.bottom: resetBackground.bottom 214 | anchors.top: resetBackground.top 215 | anchors.margins: Kirigami.Units.smallSpacing 216 | 217 | 218 | Kirigami.Theme.colorSet: Kirigami.Theme.View 219 | Kirigami.Theme.inherit: false 220 | 221 | property string color: (Config.viewBodyColor !== "None") ? Config.viewBodyColor : Kirigami.Theme.backgroundColor 222 | 223 | background: Rectangle { 224 | color: backgroundButton.color 225 | radius: Kirigami.Units.smallSpacing 226 | } 227 | 228 | onClicked: { 229 | colorPicker.caller = backgroundButton 230 | colorPicker.open() 231 | } 232 | } 233 | 234 | Controls.Button { 235 | id: resetBackground 236 | icon.name: "edit-undo" 237 | 238 | onClicked: updateColor(resetBackground, "None") 239 | } 240 | } 241 | 242 | Row { 243 | Kirigami.FormData.label: i18n("Text color:") 244 | 245 | property string name: "text" 246 | 247 | Controls.Button { 248 | id: textButton 249 | 250 | width: Kirigami.Units.largeSpacing * 20 251 | 252 | anchors.bottom: resetText.bottom 253 | anchors.top: resetText.top 254 | anchors.margins: Kirigami.Units.smallSpacing 255 | 256 | Kirigami.Theme.colorSet: Kirigami.Theme.View 257 | Kirigami.Theme.inherit: false 258 | 259 | property string color: (Config.viewTextColor !== "None") ? Config.viewTextColor : Kirigami.Theme.textColor 260 | 261 | background: Rectangle { 262 | color: textButton.color 263 | radius: Kirigami.Units.smallSpacing 264 | } 265 | 266 | onClicked: { 267 | colorPicker.caller = textButton 268 | colorPicker.open() 269 | } 270 | } 271 | 272 | Controls.Button { 273 | id: resetText 274 | icon.name: "edit-undo" 275 | 276 | onClicked: updateColor(resetText, "None") 277 | } 278 | } 279 | 280 | Row { 281 | Kirigami.FormData.label: i18n("Title color:") 282 | 283 | property string name: "title" 284 | 285 | Controls.Button { 286 | id: titleButton 287 | 288 | width: Kirigami.Units.largeSpacing * 20 289 | 290 | anchors.bottom: resetTitle.bottom 291 | anchors.top: resetTitle.top 292 | anchors.margins: Kirigami.Units.smallSpacing 293 | 294 | Kirigami.Theme.colorSet: Kirigami.Theme.View 295 | Kirigami.Theme.inherit: false 296 | 297 | property string color: (Config.viewTitleColor !== "None") ? Config.viewTitleColor : Kirigami.Theme.disabledTextColor 298 | 299 | background: Rectangle { 300 | color: titleButton.color 301 | radius: Kirigami.Units.smallSpacing 302 | } 303 | 304 | onClicked: { 305 | colorPicker.caller = titleButton 306 | colorPicker.open() 307 | } 308 | } 309 | 310 | Controls.Button { 311 | id: resetTitle 312 | icon.name: "edit-undo" 313 | 314 | onClicked: updateColor(resetTitle, "None") 315 | } 316 | } 317 | 318 | Row { 319 | Kirigami.FormData.label: i18n("Link color:") 320 | 321 | property string name: "link" 322 | 323 | Controls.Button { 324 | id: linkButton 325 | 326 | width: Kirigami.Units.largeSpacing * 20 327 | 328 | anchors.bottom: resetLink.bottom 329 | anchors.top: resetLink.top 330 | anchors.margins: Kirigami.Units.smallSpacing 331 | 332 | Kirigami.Theme.colorSet: Kirigami.Theme.View 333 | Kirigami.Theme.inherit: false 334 | 335 | property string color: (Config.viewLinkColor !== "None") ? Config.viewLinkColor : Kirigami.Theme.linkColor 336 | 337 | background: Rectangle { 338 | color: linkButton.color 339 | radius: Kirigami.Units.smallSpacing 340 | } 341 | 342 | onClicked: { 343 | colorPicker.caller = linkButton 344 | colorPicker.open() 345 | } 346 | } 347 | 348 | Controls.Button { 349 | id: resetLink 350 | icon.name: "edit-undo" 351 | 352 | onClicked: updateColor(resetLink, "None") 353 | } 354 | } 355 | 356 | Row { 357 | Kirigami.FormData.label: i18n("Visited Link color:") 358 | 359 | property string name: "visitedLink" 360 | 361 | Controls.Button { 362 | id: visitedLinkButton 363 | 364 | width: Kirigami.Units.largeSpacing * 20 365 | 366 | anchors.bottom: resetVisitiedLink.bottom 367 | anchors.top: resetVisitiedLink.top 368 | anchors.margins: Kirigami.Units.smallSpacing 369 | 370 | Kirigami.Theme.colorSet: Kirigami.Theme.View 371 | Kirigami.Theme.inherit: false 372 | 373 | property string color: (Config.viewVisitedLinkColor !== "None") ? Config.viewVisitedLinkColor : Kirigami.Theme.visitedLinkColor 374 | 375 | background: Rectangle { 376 | color: visitedLinkButton.color 377 | radius: Kirigami.Units.smallSpacing 378 | } 379 | onClicked: { 380 | colorPicker.caller = visitedLinkButton 381 | colorPicker.open() 382 | } 383 | } 384 | 385 | Controls.Button { 386 | id: resetVisitiedLink 387 | icon.name: "edit-undo" 388 | 389 | onClicked: updateColor(resetVisitiedLink, "None") 390 | } 391 | } 392 | 393 | Row { 394 | Kirigami.FormData.label: i18n("Code color:") 395 | 396 | property string name: "code" 397 | 398 | Controls.Button { 399 | id: codeButton 400 | 401 | width: Kirigami.Units.largeSpacing * 20 402 | 403 | anchors.bottom: resetCode.bottom 404 | anchors.top: resetCode.top 405 | anchors.margins: Kirigami.Units.smallSpacing 406 | 407 | Kirigami.Theme.colorSet: Kirigami.Theme.View 408 | Kirigami.Theme.inherit: false 409 | 410 | property string color: (Config.viewCodeColor !== "None") ? Config.viewCodeColor : Kirigami.Theme.alternateBackgroundColor 411 | 412 | background: Rectangle { 413 | color: codeButton.color 414 | radius: Kirigami.Units.smallSpacing 415 | } 416 | 417 | onClicked: { 418 | colorPicker.caller = codeButton 419 | colorPicker.open() 420 | } 421 | } 422 | 423 | Controls.Button { 424 | id: resetCode 425 | icon.name: "edit-undo" 426 | 427 | onClicked: updateColor(resetCode, "None") 428 | } 429 | } 430 | 431 | Controls.TextField { 432 | id: fontDisplay 433 | 434 | Kirigami.FormData.label: i18n("Font:") 435 | 436 | Kirigami.Theme.colorSet: Kirigami.Theme.View 437 | Kirigami.Theme.inherit: false 438 | 439 | readOnly: true 440 | text: (Config.viewFont !== "None") ? Config.viewFont : Kirigami.Theme.defaultFont.family 441 | font.family: text 442 | 443 | MouseArea { 444 | anchors.fill: parent 445 | onClicked: { 446 | fontDialog.currentFamily = parent.text 447 | fontDialog.open() 448 | } 449 | } 450 | } 451 | } 452 | } 453 | 454 | 455 | -------------------------------------------------------------------------------- /src/contents/resources/qwebchannel.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2016 The Qt Company Ltd. 4 | ** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff 5 | ** Contact: https://www.qt.io/licensing/ 6 | ** 7 | ** This file is part of the QtWebChannel module of the Qt Toolkit. 8 | ** 9 | ** $QT_BEGIN_LICENSE:BSD$ 10 | ** Commercial License Usage 11 | ** Licensees holding valid commercial Qt licenses may use this file in 12 | ** accordance with the commercial license agreement provided with the 13 | ** Software or, alternatively, in accordance with the terms contained in 14 | ** a written agreement between you and The Qt Company. For licensing terms 15 | ** and conditions see https://www.qt.io/terms-conditions. For further 16 | ** information use the contact form at https://www.qt.io/contact-us. 17 | ** 18 | ** BSD License Usage 19 | ** Alternatively, you may use this file under the terms of the BSD license 20 | ** as follows: 21 | ** 22 | ** "Redistribution and use in source and binary forms, with or without 23 | ** modification, are permitted provided that the following conditions are 24 | ** met: 25 | ** * Redistributions of source code must retain the above copyright 26 | ** notice, this list of conditions and the following disclaimer. 27 | ** * Redistributions in binary form must reproduce the above copyright 28 | ** notice, this list of conditions and the following disclaimer in 29 | ** the documentation and/or other materials provided with the 30 | ** distribution. 31 | ** * Neither the name of The Qt Company Ltd nor the names of its 32 | ** contributors may be used to endorse or promote products derived 33 | ** from this software without specific prior written permission. 34 | ** 35 | ** 36 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 37 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 38 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 39 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 40 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 41 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 42 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 43 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 44 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 45 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 46 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 47 | ** 48 | ** $QT_END_LICENSE$ 49 | ** 50 | ****************************************************************************/ 51 | 52 | "use strict"; 53 | 54 | var QWebChannelMessageTypes = { 55 | signal: 1, 56 | propertyUpdate: 2, 57 | init: 3, 58 | idle: 4, 59 | debug: 5, 60 | invokeMethod: 6, 61 | connectToSignal: 7, 62 | disconnectFromSignal: 8, 63 | setProperty: 9, 64 | response: 10, 65 | }; 66 | 67 | var QWebChannel = function(transport, initCallback) 68 | { 69 | if (typeof transport !== "object" || typeof transport.send !== "function") { 70 | console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + 71 | " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); 72 | return; 73 | } 74 | 75 | var channel = this; 76 | this.transport = transport; 77 | 78 | this.send = function(data) 79 | { 80 | if (typeof(data) !== "string") { 81 | data = JSON.stringify(data); 82 | } 83 | channel.transport.send(data); 84 | } 85 | 86 | this.transport.onmessage = function(message) 87 | { 88 | var data = message.data; 89 | if (typeof data === "string") { 90 | data = JSON.parse(data); 91 | } 92 | switch (data.type) { 93 | case QWebChannelMessageTypes.signal: 94 | channel.handleSignal(data); 95 | break; 96 | case QWebChannelMessageTypes.response: 97 | channel.handleResponse(data); 98 | break; 99 | case QWebChannelMessageTypes.propertyUpdate: 100 | channel.handlePropertyUpdate(data); 101 | break; 102 | default: 103 | console.error("invalid message received:", message.data); 104 | break; 105 | } 106 | } 107 | 108 | this.execCallbacks = {}; 109 | this.execId = 0; 110 | this.exec = function(data, callback) 111 | { 112 | if (!callback) { 113 | // if no callback is given, send directly 114 | channel.send(data); 115 | return; 116 | } 117 | if (channel.execId === Number.MAX_VALUE) { 118 | // wrap 119 | channel.execId = Number.MIN_VALUE; 120 | } 121 | if (data.hasOwnProperty("id")) { 122 | console.error("Cannot exec message with property id: " + JSON.stringify(data)); 123 | return; 124 | } 125 | data.id = channel.execId++; 126 | channel.execCallbacks[data.id] = callback; 127 | channel.send(data); 128 | }; 129 | 130 | this.objects = {}; 131 | 132 | this.handleSignal = function(message) 133 | { 134 | var object = channel.objects[message.object]; 135 | if (object) { 136 | object.signalEmitted(message.signal, message.args); 137 | } else { 138 | console.warn("Unhandled signal: " + message.object + "::" + message.signal); 139 | } 140 | } 141 | 142 | this.handleResponse = function(message) 143 | { 144 | if (!message.hasOwnProperty("id")) { 145 | console.error("Invalid response message received: ", JSON.stringify(message)); 146 | return; 147 | } 148 | channel.execCallbacks[message.id](message.data); 149 | delete channel.execCallbacks[message.id]; 150 | } 151 | 152 | this.handlePropertyUpdate = function(message) 153 | { 154 | for (var i in message.data) { 155 | var data = message.data[i]; 156 | var object = channel.objects[data.object]; 157 | if (object) { 158 | object.propertyUpdate(data.signals, data.properties); 159 | } else { 160 | console.warn("Unhandled property update: " + data.object + "::" + data.signal); 161 | } 162 | } 163 | channel.exec({type: QWebChannelMessageTypes.idle}); 164 | } 165 | 166 | this.debug = function(message) 167 | { 168 | channel.send({type: QWebChannelMessageTypes.debug, data: message}); 169 | }; 170 | 171 | channel.exec({type: QWebChannelMessageTypes.init}, function(data) { 172 | for (var objectName in data) { 173 | var object = new QObject(objectName, data[objectName], channel); 174 | } 175 | // now unwrap properties, which might reference other registered objects 176 | for (var objectName in channel.objects) { 177 | channel.objects[objectName].unwrapProperties(); 178 | } 179 | if (initCallback) { 180 | initCallback(channel); 181 | } 182 | channel.exec({type: QWebChannelMessageTypes.idle}); 183 | }); 184 | }; 185 | 186 | function QObject(name, data, webChannel) 187 | { 188 | this.__id__ = name; 189 | webChannel.objects[name] = this; 190 | 191 | // List of callbacks that get invoked upon signal emission 192 | this.__objectSignals__ = {}; 193 | 194 | // Cache of all properties, updated when a notify signal is emitted 195 | this.__propertyCache__ = {}; 196 | 197 | var object = this; 198 | 199 | // ---------------------------------------------------------------------- 200 | 201 | this.unwrapQObject = function(response) 202 | { 203 | if (response instanceof Array) { 204 | // support list of objects 205 | var ret = new Array(response.length); 206 | for (var i = 0; i < response.length; ++i) { 207 | ret[i] = object.unwrapQObject(response[i]); 208 | } 209 | return ret; 210 | } 211 | if (!response 212 | || !response["__QObject*__"] 213 | || response.id === undefined) { 214 | return response; 215 | } 216 | 217 | var objectId = response.id; 218 | if (webChannel.objects[objectId]) 219 | return webChannel.objects[objectId]; 220 | 221 | if (!response.data) { 222 | console.error("Cannot unwrap unknown QObject " + objectId + " without data."); 223 | return; 224 | } 225 | 226 | var qObject = new QObject( objectId, response.data, webChannel ); 227 | qObject.destroyed.connect(function() { 228 | if (webChannel.objects[objectId] === qObject) { 229 | delete webChannel.objects[objectId]; 230 | // reset the now deleted QObject to an empty {} object 231 | // just assigning {} though would not have the desired effect, but the 232 | // below also ensures all external references will see the empty map 233 | // NOTE: this detour is necessary to workaround QTBUG-40021 234 | var propertyNames = []; 235 | for (var propertyName in qObject) { 236 | propertyNames.push(propertyName); 237 | } 238 | for (var idx in propertyNames) { 239 | delete qObject[propertyNames[idx]]; 240 | } 241 | } 242 | }); 243 | // here we are already initialized, and thus must directly unwrap the properties 244 | qObject.unwrapProperties(); 245 | return qObject; 246 | } 247 | 248 | this.unwrapProperties = function() 249 | { 250 | for (var propertyIdx in object.__propertyCache__) { 251 | object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); 252 | } 253 | } 254 | 255 | function addSignal(signalData, isPropertyNotifySignal) 256 | { 257 | var signalName = signalData[0]; 258 | var signalIndex = signalData[1]; 259 | object[signalName] = { 260 | connect: function(callback) { 261 | if (typeof(callback) !== "function") { 262 | console.error("Bad callback given to connect to signal " + signalName); 263 | return; 264 | } 265 | 266 | object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; 267 | object.__objectSignals__[signalIndex].push(callback); 268 | 269 | if (!isPropertyNotifySignal && signalName !== "destroyed") { 270 | // only required for "pure" signals, handled separately for properties in propertyUpdate 271 | // also note that we always get notified about the destroyed signal 272 | webChannel.exec({ 273 | type: QWebChannelMessageTypes.connectToSignal, 274 | object: object.__id__, 275 | signal: signalIndex 276 | }); 277 | } 278 | }, 279 | disconnect: function(callback) { 280 | if (typeof(callback) !== "function") { 281 | console.error("Bad callback given to disconnect from signal " + signalName); 282 | return; 283 | } 284 | object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; 285 | var idx = object.__objectSignals__[signalIndex].indexOf(callback); 286 | if (idx === -1) { 287 | console.error("Cannot find connection of signal " + signalName + " to " + callback.name); 288 | return; 289 | } 290 | object.__objectSignals__[signalIndex].splice(idx, 1); 291 | if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { 292 | // only required for "pure" signals, handled separately for properties in propertyUpdate 293 | webChannel.exec({ 294 | type: QWebChannelMessageTypes.disconnectFromSignal, 295 | object: object.__id__, 296 | signal: signalIndex 297 | }); 298 | } 299 | } 300 | }; 301 | } 302 | 303 | /** 304 | * Invokes all callbacks for the given signalname. Also works for property notify callbacks. 305 | */ 306 | function invokeSignalCallbacks(signalName, signalArgs) 307 | { 308 | var connections = object.__objectSignals__[signalName]; 309 | if (connections) { 310 | connections.forEach(function(callback) { 311 | callback.apply(callback, signalArgs); 312 | }); 313 | } 314 | } 315 | 316 | this.propertyUpdate = function(signals, propertyMap) 317 | { 318 | // update property cache 319 | for (var propertyIndex in propertyMap) { 320 | var propertyValue = propertyMap[propertyIndex]; 321 | object.__propertyCache__[propertyIndex] = propertyValue; 322 | } 323 | 324 | for (var signalName in signals) { 325 | // Invoke all callbacks, as signalEmitted() does not. This ensures the 326 | // property cache is updated before the callbacks are invoked. 327 | invokeSignalCallbacks(signalName, signals[signalName]); 328 | } 329 | } 330 | 331 | this.signalEmitted = function(signalName, signalArgs) 332 | { 333 | invokeSignalCallbacks(signalName, signalArgs); 334 | } 335 | 336 | function addMethod(methodData) 337 | { 338 | var methodName = methodData[0]; 339 | var methodIdx = methodData[1]; 340 | object[methodName] = function() { 341 | var args = []; 342 | var callback; 343 | for (var i = 0; i < arguments.length; ++i) { 344 | if (typeof arguments[i] === "function") 345 | callback = arguments[i]; 346 | else 347 | args.push(arguments[i]); 348 | } 349 | 350 | webChannel.exec({ 351 | "type": QWebChannelMessageTypes.invokeMethod, 352 | "object": object.__id__, 353 | "method": methodIdx, 354 | "args": args 355 | }, function(response) { 356 | if (response !== undefined) { 357 | var result = object.unwrapQObject(response); 358 | if (callback) { 359 | (callback)(result); 360 | } 361 | } 362 | }); 363 | }; 364 | } 365 | 366 | function bindGetterSetter(propertyInfo) 367 | { 368 | var propertyIndex = propertyInfo[0]; 369 | var propertyName = propertyInfo[1]; 370 | var notifySignalData = propertyInfo[2]; 371 | // initialize property cache with current value 372 | // NOTE: if this is an object, it is not directly unwrapped as it might 373 | // reference other QObject that we do not know yet 374 | object.__propertyCache__[propertyIndex] = propertyInfo[3]; 375 | 376 | if (notifySignalData) { 377 | if (notifySignalData[0] === 1) { 378 | // signal name is optimized away, reconstruct the actual name 379 | notifySignalData[0] = propertyName + "Changed"; 380 | } 381 | addSignal(notifySignalData, true); 382 | } 383 | 384 | Object.defineProperty(object, propertyName, { 385 | configurable: true, 386 | get: function () { 387 | var propertyValue = object.__propertyCache__[propertyIndex]; 388 | if (propertyValue === undefined) { 389 | // This shouldn't happen 390 | console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); 391 | } 392 | 393 | return propertyValue; 394 | }, 395 | set: function(value) { 396 | if (value === undefined) { 397 | console.warn("Property setter for " + propertyName + " called with undefined value!"); 398 | return; 399 | } 400 | object.__propertyCache__[propertyIndex] = value; 401 | webChannel.exec({ 402 | "type": QWebChannelMessageTypes.setProperty, 403 | "object": object.__id__, 404 | "property": propertyIndex, 405 | "value": value 406 | }); 407 | } 408 | }); 409 | 410 | } 411 | 412 | // ---------------------------------------------------------------------- 413 | 414 | data.methods.forEach(addMethod); 415 | 416 | data.properties.forEach(bindGetterSetter); 417 | 418 | data.signals.forEach(function(signal) { addSignal(signal, false); }); 419 | 420 | for (var name in data.enums) { 421 | object[name] = data.enums[name]; 422 | } 423 | } 424 | 425 | //required for use with nodejs 426 | if (typeof module === 'object') { 427 | module.exports = { 428 | QWebChannel: QWebChannel 429 | }; 430 | } 431 | -------------------------------------------------------------------------------- /LICENSES/GPL-2.0-or-later.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your freedom to share 13 | and change it. By contrast, the GNU General Public License is intended to 14 | guarantee your freedom to share and change free software--to make sure the 15 | software is free for all its users. This General Public License applies to 16 | most of the Free Software Foundation's software and to any other program whose 17 | authors commit to using it. (Some other Free Software Foundation software 18 | is covered by the GNU Lesser General Public License instead.) You can apply 19 | it to your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not price. Our 22 | General Public Licenses are designed to make sure that you have the freedom 23 | to distribute copies of free software (and charge for this service if you 24 | wish), that you receive source code or can get it if you want it, that you 25 | can change the software or use pieces of it in new free programs; and that 26 | you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid anyone to 29 | deny you these rights or to ask you to surrender the rights. These restrictions 30 | translate to certain responsibilities for you if you distribute copies of 31 | the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether gratis or 34 | for a fee, you must give the recipients all the rights that you have. You 35 | must make sure that they, too, receive or can get the source code. And you 36 | must show them these terms so they know their rights. 37 | 38 | We protect your rights with two steps: (1) copyright the software, and (2) 39 | offer you this license which gives you legal permission to copy, distribute 40 | and/or modify the software. 41 | 42 | Also, for each author's protection and ours, we want to make certain that 43 | everyone understands that there is no warranty for this free software. If 44 | the software is modified by someone else and passed on, we want its recipients 45 | to know that what they have is not the original, so that any problems introduced 46 | by others will not reflect on the original authors' reputations. 47 | 48 | Finally, any free program is threatened constantly by software patents. We 49 | wish to avoid the danger that redistributors of a free program will individually 50 | obtain patent licenses, in effect making the program proprietary. To prevent 51 | this, we have made it clear that any patent must be licensed for everyone's 52 | free use or not licensed at all. 53 | 54 | The precise terms and conditions for copying, distribution and modification 55 | follow. 56 | 57 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 58 | 59 | 0. This License applies to any program or other work which contains a notice 60 | placed by the copyright holder saying it may be distributed under the terms 61 | of this General Public License. The "Program", below, refers to any such program 62 | or work, and a "work based on the Program" means either the Program or any 63 | derivative work under copyright law: that is to say, a work containing the 64 | Program or a portion of it, either verbatim or with modifications and/or translated 65 | into another language. (Hereinafter, translation is included without limitation 66 | in the term "modification".) Each licensee is addressed as "you". 67 | 68 | Activities other than copying, distribution and modification are not covered 69 | by this License; they are outside its scope. The act of running the Program 70 | is not restricted, and the output from the Program is covered only if its 71 | contents constitute a work based on the Program (independent of having been 72 | made by running the Program). Whether that is true depends on what the Program 73 | does. 74 | 75 | 1. You may copy and distribute verbatim copies of the Program's source code 76 | as you receive it, in any medium, provided that you conspicuously and appropriately 77 | publish on each copy an appropriate copyright notice and disclaimer of warranty; 78 | keep intact all the notices that refer to this License and to the absence 79 | of any warranty; and give any other recipients of the Program a copy of this 80 | License along with the Program. 81 | 82 | You may charge a fee for the physical act of transferring a copy, and you 83 | may at your option offer warranty protection in exchange for a fee. 84 | 85 | 2. You may modify your copy or copies of the Program or any portion of it, 86 | thus forming a work based on the Program, and copy and distribute such modifications 87 | or work under the terms of Section 1 above, provided that you also meet all 88 | of these conditions: 89 | 90 | a) You must cause the modified files to carry prominent notices stating that 91 | you changed the files and the date of any change. 92 | 93 | b) You must cause any work that you distribute or publish, that in whole or 94 | in part contains or is derived from the Program or any part thereof, to be 95 | licensed as a whole at no charge to all third parties under the terms of this 96 | License. 97 | 98 | c) If the modified program normally reads commands interactively when run, 99 | you must cause it, when started running for such interactive use in the most 100 | ordinary way, to print or display an announcement including an appropriate 101 | copyright notice and a notice that there is no warranty (or else, saying that 102 | you provide a warranty) and that users may redistribute the program under 103 | these conditions, and telling the user how to view a copy of this License. 104 | (Exception: if the Program itself is interactive but does not normally print 105 | such an announcement, your work based on the Program is not required to print 106 | an announcement.) 107 | 108 | These requirements apply to the modified work as a whole. If identifiable 109 | sections of that work are not derived from the Program, and can be reasonably 110 | considered independent and separate works in themselves, then this License, 111 | and its terms, do not apply to those sections when you distribute them as 112 | separate works. But when you distribute the same sections as part of a whole 113 | which is a work based on the Program, the distribution of the whole must be 114 | on the terms of this License, whose permissions for other licensees extend 115 | to the entire whole, and thus to each and every part regardless of who wrote 116 | it. 117 | 118 | Thus, it is not the intent of this section to claim rights or contest your 119 | rights to work written entirely by you; rather, the intent is to exercise 120 | the right to control the distribution of derivative or collective works based 121 | on the Program. 122 | 123 | In addition, mere aggregation of another work not based on the Program with 124 | the Program (or with a work based on the Program) on a volume of a storage 125 | or distribution medium does not bring the other work under the scope of this 126 | License. 127 | 128 | 3. You may copy and distribute the Program (or a work based on it, under Section 129 | 2) in object code or executable form under the terms of Sections 1 and 2 above 130 | provided that you also do one of the following: 131 | 132 | a) Accompany it with the complete corresponding machine-readable source code, 133 | which must be distributed under the terms of Sections 1 and 2 above on a medium 134 | customarily used for software interchange; or, 135 | 136 | b) Accompany it with a written offer, valid for at least three years, to give 137 | any third party, for a charge no more than your cost of physically performing 138 | source distribution, a complete machine-readable copy of the corresponding 139 | source code, to be distributed under the terms of Sections 1 and 2 above on 140 | a medium customarily used for software interchange; or, 141 | 142 | c) Accompany it with the information you received as to the offer to distribute 143 | corresponding source code. (This alternative is allowed only for noncommercial 144 | distribution and only if you received the program in object code or executable 145 | form with such an offer, in accord with Subsection b above.) 146 | 147 | The source code for a work means the preferred form of the work for making 148 | modifications to it. For an executable work, complete source code means all 149 | the source code for all modules it contains, plus any associated interface 150 | definition files, plus the scripts used to control compilation and installation 151 | of the executable. However, as a special exception, the source code distributed 152 | need not include anything that is normally distributed (in either source or 153 | binary form) with the major components (compiler, kernel, and so on) of the 154 | operating system on which the executable runs, unless that component itself 155 | accompanies the executable. 156 | 157 | If distribution of executable or object code is made by offering access to 158 | copy from a designated place, then offering equivalent access to copy the 159 | source code from the same place counts as distribution of the source code, 160 | even though third parties are not compelled to copy the source along with 161 | the object code. 162 | 163 | 4. You may not copy, modify, sublicense, or distribute the Program except 164 | as expressly provided under this License. Any attempt otherwise to copy, modify, 165 | sublicense or distribute the Program is void, and will automatically terminate 166 | your rights under this License. However, parties who have received copies, 167 | or rights, from you under this License will not have their licenses terminated 168 | so long as such parties remain in full compliance. 169 | 170 | 5. You are not required to accept this License, since you have not signed 171 | it. However, nothing else grants you permission to modify or distribute the 172 | Program or its derivative works. These actions are prohibited by law if you 173 | do not accept this License. Therefore, by modifying or distributing the Program 174 | (or any work based on the Program), you indicate your acceptance of this License 175 | to do so, and all its terms and conditions for copying, distributing or modifying 176 | the Program or works based on it. 177 | 178 | 6. Each time you redistribute the Program (or any work based on the Program), 179 | the recipient automatically receives a license from the original licensor 180 | to copy, distribute or modify the Program subject to these terms and conditions. 181 | You may not impose any further restrictions on the recipients' exercise of 182 | the rights granted herein. You are not responsible for enforcing compliance 183 | by third parties to this License. 184 | 185 | 7. If, as a consequence of a court judgment or allegation of patent infringement 186 | or for any other reason (not limited to patent issues), conditions are imposed 187 | on you (whether by court order, agreement or otherwise) that contradict the 188 | conditions of this License, they do not excuse you from the conditions of 189 | this License. If you cannot distribute so as to satisfy simultaneously your 190 | obligations under this License and any other pertinent obligations, then as 191 | a consequence you may not distribute the Program at all. For example, if a 192 | patent license would not permit royalty-free redistribution of the Program 193 | by all those who receive copies directly or indirectly through you, then the 194 | only way you could satisfy both it and this License would be to refrain entirely 195 | from distribution of the Program. 196 | 197 | If any portion of this section is held invalid or unenforceable under any 198 | particular circumstance, the balance of the section is intended to apply and 199 | the section as a whole is intended to apply in other circumstances. 200 | 201 | It is not the purpose of this section to induce you to infringe any patents 202 | or other property right claims or to contest validity of any such claims; 203 | this section has the sole purpose of protecting the integrity of the free 204 | software distribution system, which is implemented by public license practices. 205 | Many people have made generous contributions to the wide range of software 206 | distributed through that system in reliance on consistent application of that 207 | system; it is up to the author/donor to decide if he or she is willing to 208 | distribute software through any other system and a licensee cannot impose 209 | that choice. 210 | 211 | This section is intended to make thoroughly clear what is believed to be a 212 | consequence of the rest of this License. 213 | 214 | 8. If the distribution and/or use of the Program is restricted in certain 215 | countries either by patents or by copyrighted interfaces, the original copyright 216 | holder who places the Program under this License may add an explicit geographical 217 | distribution limitation excluding those countries, so that distribution is 218 | permitted only in or among countries not thus excluded. In such case, this 219 | License incorporates the limitation as if written in the body of this License. 220 | 221 | 9. The Free Software Foundation may publish revised and/or new versions of 222 | the General Public License from time to time. Such new versions will be similar 223 | in spirit to the present version, but may differ in detail to address new 224 | problems or concerns. 225 | 226 | Each version is given a distinguishing version number. If the Program specifies 227 | a version number of this License which applies to it and "any later version", 228 | you have the option of following the terms and conditions either of that version 229 | or of any later version published by the Free Software Foundation. If the 230 | Program does not specify a version number of this License, you may choose 231 | any version ever published by the Free Software Foundation. 232 | 233 | 10. If you wish to incorporate parts of the Program into other free programs 234 | whose distribution conditions are different, write to the author to ask for 235 | permission. For software which is copyrighted by the Free Software Foundation, 236 | write to the Free Software Foundation; we sometimes make exceptions for this. 237 | Our decision will be guided by the two goals of preserving the free status 238 | of all derivatives of our free software and of promoting the sharing and reuse 239 | of software generally. 240 | 241 | NO WARRANTY 242 | 243 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR 244 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 245 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 246 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 247 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 248 | FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE 249 | OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME 250 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 251 | 252 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 253 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 254 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 255 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE 256 | OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA 257 | OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES 258 | OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH 259 | HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 260 | 261 | END OF TERMS AND CONDITIONS 262 | 263 | How to Apply These Terms to Your New Programs 264 | 265 | If you develop a new program, and you want it to be of the greatest possible 266 | use to the public, the best way to achieve this is to make it free software 267 | which everyone can redistribute and change under these terms. 268 | 269 | To do so, attach the following notices to the program. It is safest to attach 270 | them to the start of each source file to most effectively convey the exclusion 271 | of warranty; and each file should have at least the "copyright" line and a 272 | pointer to where the full notice is found. 273 | 274 | one line to give the program's name and an idea of what it does. Copyright 275 | (C) yyyy name of author 276 | 277 | This program is free software; you can redistribute it and/or modify it under 278 | the terms of the GNU General Public License as published by the Free Software 279 | Foundation; either version 2 of the License, or (at your option) any later 280 | version. 281 | 282 | This program is distributed in the hope that it will be useful, but WITHOUT 283 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 284 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 285 | 286 | You should have received a copy of the GNU General Public License along with 287 | this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 288 | Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how 289 | to contact you by electronic and paper mail. 290 | 291 | If the program is interactive, make it output a short notice like this when 292 | it starts in an interactive mode: 293 | 294 | Gnomovision version 69, Copyright (C) year name of author Gnomovision comes 295 | with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, 296 | and you are welcome to redistribute it under certain conditions; type `show 297 | c' for details. 298 | 299 | The hypothetical commands `show w' and `show c' should show the appropriate 300 | parts of the General Public License. Of course, the commands you use may be 301 | called something other than `show w' and `show c'; they could even be mouse-clicks 302 | or menu items--whatever suits your program. 303 | 304 | You should also get your employer (if you work as a programmer) or your school, 305 | if any, to sign a "copyright disclaimer" for the program, if necessary. Here 306 | is a sample; alter the names: 307 | 308 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' 309 | (which makes passes at compilers) written by James Hacker. 310 | 311 | signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice 312 | --------------------------------------------------------------------------------