diff --git a/CMakeLists.txt b/CMakeLists.txt index ca6b079..28175a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,67 +1,67 @@ cmake_minimum_required(VERSION 3.0) set(NOTA_VERSION 1.0.0) project(nota VERSION ${NOTA_VERSION}) find_package(ECM 1.7.0 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${ECM_MODULE_PATH}) find_package(MauiKit REQUIRED) -find_package(Qt5 REQUIRED NO_MODULE COMPONENTS Qml Quick Sql Svg QuickControls2 Widgets Xml) +find_package(Qt5 REQUIRED NO_MODULE COMPONENTS Qml Sql Svg) find_package(KF5 ${KF5_VERSION} REQUIRED COMPONENTS I18n Notifications Config KIO Attica SyntaxHighlighting) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(ECMInstallIcons) include(ECMAddAppIcon) include(ECMSetupVersion) include(FeatureSummary) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTORCC ON) set(nota_SRCS src/main.cpp src/models/documentsmodel.cpp src/models/editormodel.cpp ) set(nota_HDRS src/models/documentsmodel.h src/models/editormodel.h ) set(nota_ASSETS src/qml.qrc src/assets/img_assets.qrc ) add_executable(nota ${nota_SRCS} ${nota_HDRS} ${nota_ASSETS} ) ecm_setup_version(${NOTA_VERSION} VARIABLE_PREFIX NOTA VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/nota_version.h" ) if (ANDROID) find_package(Qt5 REQUIRED COMPONENTS AndroidExtras WebView) target_link_libraries(nota MauiKit Qt5::AndroidExtras) else() find_package(Qt5 REQUIRED COMPONENTS WebEngine) endif() -target_link_libraries(nota MauiKit Qt5::Sql Qt5::Qml Qt5::Widgets Qt5::Svg Qt5::QuickControls2 KF5::ConfigCore KF5::Notifications KF5::KIOCore KF5::I18n KF5::Attica) +target_link_libraries(nota MauiKit Qt5::Sql Qt5::Qml Qt5::Svg KF5::ConfigCore KF5::Notifications KF5::KIOCore KF5::I18n KF5::Attica) install(TARGETS nota ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org.kde.nota.desktop DESTINATION ${XDG_APPS_INSTALL_DIR}) #TODO: port to ecm_install_icons() # install(FILES assets/pix.svg DESTINATION ${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps) # install(FILES org.kde.pix.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/Editor.qml b/src/Editor.qml index 1720ccd..86b18b7 100644 --- a/src/Editor.qml +++ b/src/Editor.qml @@ -1,45 +1,45 @@ import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 import org.kde.mauikit 1.0 as Maui import org.kde.kirigami 2.7 as Kirigami Maui.Editor { height: _editorListView.height width: _editorListView.width footBar.visible: false headBar.rightContent: [ ToolButton { id: saveBtn icon.name: "document-save" - onClicked: saveFile(document.fileUrl) + onClicked: saveFile(document.fileUrl, _tabBar.currentIndex) }, ToolButton { icon.name: "document-save-as" text: qsTr("Save as...") - onClicked: saveFile() + onClicked: saveFile("", _tabBar.currentIndex) } ] - function saveFile(path) + function saveFile(path, index) { if (path && Maui.FM.fileExists(path)) { document.saveAs(path); } else { fileDialog.mode = fileDialog.modes.SAVE; fileDialog.settings.singleSelection = true fileDialog.show(function (paths) { document.saveAs(paths[0]); - _editorList.update(_tabBar.currentIndex, paths[0]); + _editorList.update(index, paths[0]); }); } } } diff --git a/src/main.qml b/src/main.qml index 8dc3614..3fc6964 100644 --- a/src/main.qml +++ b/src/main.qml @@ -1,505 +1,545 @@ import QtQuick 2.9 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.7 as Kirigami import org.kde.mauikit 1.0 as Maui import org.maui.nota 1.0 as Nota import QtQuick.Window 2.0 - +import QtQml.Models 2.3 import "views" Maui.ApplicationWindow { id: root title: qsTr("Nota") // property bool terminalVisible: Maui.FM.loadSettings("TERMINAL", "MAINVIEW", false) == "true" // property alias terminal : terminalLoader.item property var views : ({editor: 0, documents: 1, recent: 2}) Maui.App.iconName: "qrc:/img/nota.svg" Maui.App.description: qsTr("Nota is a simple text editor for Plasma Mobile, GNU/Linux distros and Android") rightIcon.visible: false // mainMenu: [ // MenuItem // { // text: qsTr("Show terminal") // checkable: true // checked: terminal.visible // onTriggered: // { // terminalVisible = !terminalVisible // Maui.FM.saveSettings("TERMINAL",terminalVisible, "MAINVIEW") // } // } // ] + ObjectModel + { + id: _documentModel + } + onClosing: { - var files = [] + _unsavedFilesModel.clear() for(var i = 0; i<_editorListView.count; i++) { - const doc = _editorListView.itemAtIndex(i) + const doc = _documentModel.get(i) if(doc.document.modified) - files.push({'file': _editorModel.get(i), 'index': i}) + _unsavedFilesModel.append({'file': _editorModel.get(i), 'documentIndex': i}) } - if(files.length > 0 && !_unsavedDialog.discard) + if(_unsavedFilesModel.count > 0 && !_unsavedDialog.discard) { close.accepted = false - _unsavedDialog.files = files _unsavedDialog.open() }else close.accepted = true } Maui.Dialog { id: _unsavedDialog - property var files : [] property bool discard : false acceptButton.visible: false page.title: qsTr("Un saved files") - + headBar.visible: true maxHeight: 500 maxWidth: 400 page.padding: Maui.Style.space.big ListView { id: _unsavedFilesListView anchors.fill: parent spacing: Maui.Style.space.medium - model: _unsavedDialog.files + model: ListModel + { + id: _unsavedFilesModel + } + + onCountChanged: if(count === 0) _unsavedDialog.close() + clip: true delegate : Maui.ItemDelegate { + id: _unsavedFileDelegate + property int index_ : index width: parent.width height: Maui.Style.rowHeight * 1.2 RowLayout { anchors.fill: parent Maui.ListItemTemplate { Layout.fillHeight: true Layout.fillWidth: true - label1.text: modelData.file.label - label2.text: modelData.file.path - iconSource: modelData.file.icon + label1.text: model.file.label + label2.text: model.file.path + iconSource: model.file.icon iconSizeHint: Maui.Style.iconSizes.big } Row { Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: implicitWidth Button { text: qsTr("Save") - onClicked: Object.call(_editorListView.itemAtIndex(modelData.index).saveFile(modelData.file.path)) + onClicked: + { + _documentModel.get(model.documentIndex).saveFile(model.file.path, model.documentIndex) +// closeTab(model.index) + _unsavedFilesModel.remove(_unsavedFileDelegate.index_) + } + } + + Button + { + text: qsTr("Discard") + onClicked: + { + closeTab(model.documentIndex) + _unsavedFilesModel.remove(_unsavedFileDelegate.index_) + } } } } } } rejectButton.text: qsTr("Discard") onRejected: { discard = true root.close() } } Maui.FileDialog { id: fileDialog settings.onlyDirs: false settings.filterType: Maui.FMList.TEXT settings.sortBy: Maui.FMList.MODIFIED mode: modes.OPEN } Maui.FloatingButton { id: _overlayButton z: 999 anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Maui.Style.toolBarHeight anchors.bottomMargin: Maui.Style.toolBarHeight height: Maui.Style.toolBarHeight width: height icon.name: "document-new" icon.color: Kirigami.Theme.highlightedTextColor onClicked: openTab("") Maui.Badge { iconName: "list-add" anchors { horizontalCenter: parent.right verticalCenter: parent.top } onClicked: _newDocumentMenu.open() } Maui.Dialog { id: _newDocumentMenu maxHeight: 300 maxWidth: 400 defaultButtons: false footBar.middleContent: Button { text: qsTr("Add new template file") } ColumnLayout { anchors.fill: parent anchors.margins: Maui.Style.space.big spacing: Maui.Style.space.big Maui.ItemDelegate { Layout.fillWidth: true Layout.fillHeight: true Maui.ListItemTemplate { anchors.fill:parent iconSizeHint: Math.min(height, Maui.Style.iconSizes.big) iconSource: "text-x-generic" label1.text: qsTr("Text file") label2.text: qsTr("Simple text file with syntax highlighting") } onClicked: { openTab("") _newDocumentMenu.close() } } Maui.ItemDelegate { Layout.fillWidth: true Layout.fillHeight: true Maui.ListItemTemplate { anchors.fill:parent iconSizeHint: Math.min(height, Maui.Style.iconSizes.big) iconSource: "text-enriched" label1.text: qsTr("Rich text file") label2.text: qsTr("With support for basic text format editing") } } Maui.ItemDelegate { Layout.fillWidth: true Layout.fillHeight: true Maui.ListItemTemplate { anchors.fill:parent iconSizeHint: Math.min(height, Maui.Style.iconSizes.big) iconSource: "text-html" label1.text: qsTr("HTML text file") label2.text: qsTr("Text file with HTML markup support") } } } } } headBar.rightContent: [ ToolButton { icon.name: "document-open" onClicked: { fileDialog.mode = fileDialog.modes.OPEN fileDialog.settings.onlyDirs = false fileDialog.settings.singleSelection = false fileDialog.show(function (paths) { for(var i in paths) openTab(paths[i]) }); } } ] headBar.middleContent: Maui.ActionGroup { id: _actionGroup currentIndex: _swipeView.currentIndex Layout.fillHeight: true width: implicitWidth Action { text: qsTr("Editor") icon.name: "document-edit" } Action { text: qsTr("Documents") icon.name: "view-pim-journal" // to do } Action { text: qsTr("Recent") icon.name: "view-media-recent" // to do } } sideBar: Maui.AbstractSideBar { id : _drawer focus: true - width: visible ? Math.min(Kirigami.Units.gridUnit * (Kirigami.Settings.isMobile? 14 : 18), root.width) : 0 + width: visible ? Math.min(Kirigami.Units.gridUnit * (Kirigami.Settings.isMobile? 14 : 16), root.width) : 0 modal: !isWide closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent dragMargin: Maui.Style.space.big Maui.Page { anchors.fill: parent headBar.middleContent: ComboBox { Layout.fillWidth: true z : _drawer.z + 9999 model: Maui.BaseModel { list: Maui.PlacesList { groups: [ Maui.FMList.PLACES_PATH, Maui.FMList.DRIVES_PATH, Maui.FMList.TAGS_PATH] } } textRole: "label" onActivated: { currentIndex = index browserView.openFolder(model.list.get(index).path) } } Maui.FileBrowser { id: browserView anchors.fill: parent headBar.position: ToolBar.Footer headBar.visible: true viewType : Maui.FMList.LIST_VIEW settings.filterType: Maui.FMList.TEXT headBar.rightLayout.visible: false headBar.rightLayout.width: 0 onItemClicked: { var item = currentFMList.get(index) if(item.isdir == "true") openFolder(item.path) else root.openTab(item.path) } } } } Maui.BaseModel { id: _editorModel list: Nota.Editor { id: _editorList } } SwipeView { id: _swipeView anchors.fill: parent currentIndex: _actionGroup.currentIndex onCurrentItemChanged: currentItem.forceActiveFocus() onCurrentIndexChanged: _actionGroup.currentIndex = currentIndex ColumnLayout { id: editorView spacing: 0 Maui.TabBar { id: _tabBar visible: _editorListView.count > 1 Layout.fillWidth: true Layout.preferredHeight: _tabBar.implicitHeight position: TabBar.Header currentIndex : _editorListView.currentIndex // Keys.onPressed: // { // if(event.key == Qt.Key_Return) // { // _browserList.currentIndex = currentIndex // control.currentPath = tabsObjectModel.get(currentIndex).path // } // } Repeater { id: _repeater model: _editorModel Maui.TabButton { id: _tabButton + readonly property int index_ : index implicitHeight: _tabBar.implicitHeight implicitWidth: Math.max(_tabBar.width / _repeater.count, 120) checked: index === _tabBar.currentIndex text: model.label onClicked: _editorListView.currentIndex = index onCloseClicked: { - if( _editorListView.itemAtIndex(_tabButton.index).document.modified) + console.log("CLOSING EDITOR AT", _tabButton.index_) + if( _documentModel.get(_tabButton.index_).document.modified) _saveDialog.open() else - _editorList.remove(index) + closeTab(_tabButton.index_) } Maui.Dialog { id: _saveDialog page.padding: Maui.Style.space.huge - title: _editorModel.get(_tabButton.index).path - message: qsTr("This file has been modified, you can save now your changes or discard them") + title: qsTr("Save file") + message: qsTr(String("This file has been modified, you can save your changes now or discard them.\n")) + _editorModel.get(_tabButton.index).path acceptButton.text: qsTr("Save") rejectButton.text: qsTr("Discard") onAccepted: { - _editorListView.itemAtIndex(_tabButton.index).saveFile(_editorModel.get(_tabButton.index).path) + _documentModel.get(_tabButton.index_).saveFile(_editorModel.get(_tabButton.index_).path, _tabButton.index_) _saveDialog.close() } onRejected: { - _editorList.remove(_tabButton.index) _saveDialog.close() + _editorList.remove(_tabButton.index_) } } } } } // Kirigami.Separator // { // color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.7)) // Layout.fillWidth: true // Layout.preferredHeight: 1 // visible: _tabBar.visible // } ListView { id: _editorListView Layout.fillHeight: true Layout.fillWidth: true orientation: ListView.Horizontal - model: _editorModel + model: _documentModel snapMode: ListView.SnapOneItem spacing: 0 interactive: Kirigami.Settings.isMobile && count > 1 highlightFollowsCurrentItem: true highlightMoveDuration: 0 onMovementEnded: currentIndex = indexAt(contentX, contentY) - + cacheBuffer: count Maui.Holder { id: _holder visible: !_editorListView.count emoji: "qrc:/img/document-edit.svg" emojiSize: Maui.Style.iconSizes.huge isMask: true onActionTriggered: openTab("") title: qsTr("Create a new document") body: qsTr("You can create a new document by clicking the New File button, or here.
Alternative you can open existing files from the left places sidebar or by clicking the Open button") } - delegate: Editor - { - Component.onCompleted: fileUrl = model.path - } +// delegate: Editor +// { +// Component.onCompleted: fileUrl = model.path +// } } // Loader // { // id: terminalLoader // visible: terminalVisible // focus: true // Layout.fillWidth: true // Layout.alignment: Qt.AlignBottom // Layout.minimumHeight: 100 // Layout.maximumHeight: 200 // // anchors.bottom: parent.bottom // // anchors.top: handle.bottom // source: !isMobile ? "Terminal.qml" : undefined // } } DocumentsView { id: _documentsView } RecentView { id:_recentView } } - function openTab(path) { if(!_editorList.append(path)) return ; - _editorListView.currentIndex = _editorListView.count - 1 + var component = Qt.createComponent("Editor.qml"); + if (component.status === Component.Ready) + { + _documentModel.append(component.createObject(_documentModel)); + + _editorListView.currentIndex = _documentModel.count - 1 + _documentModel.get(_documentModel.count - 1).fileUrl = path + + if(path && Maui.FM.fileExists(path)) + browserView.openFolder(Maui.FM.fileDir(path)) + } + } - if(path && Maui.FM.fileExists(path)) - browserView.openFolder(Maui.FM.fileDir(path)) + function closeTab(index) + { + _documentModel.remove(index) + _editorList.remove(index) } } diff --git a/src/models/documentsmodel.cpp b/src/models/documentsmodel.cpp index bd34c4a..d61ccd3 100644 --- a/src/models/documentsmodel.cpp +++ b/src/models/documentsmodel.cpp @@ -1,70 +1,70 @@ #include "documentsmodel.h" #include Q_DECLARE_METATYPE (FMH::MODEL_LIST) Q_DECLARE_METATYPE (FMH::MODEL) DocumentsModel::DocumentsModel(QObject * parent) : MauiList (parent) { qRegisterMetaType("MODEL_LIST"); qRegisterMetaType("MODEL"); - FileLoader *loader = new FileLoader; + FilesFetcher *loader = new FilesFetcher; loader->moveToThread(&m_worker); connect(&m_worker, &QThread::finished, loader, &QObject::deleteLater); - connect(this, &DocumentsModel::start, loader, &FileLoader::fetch); - connect(loader, &FileLoader::itemReady, this, &DocumentsModel::append); + connect(this, &DocumentsModel::start, loader, &FilesFetcher::fetch); + connect(loader, &FilesFetcher::itemReady, this, &DocumentsModel::append); m_worker.start(); } DocumentsModel::~DocumentsModel() { m_worker.quit(); m_worker.wait(); } void DocumentsModel::setList(const FMH::MODEL_LIST &list) { emit this->preListChanged (); this->m_list = list; emit this->postListChanged (); } void DocumentsModel::append(const FMH::MODEL &item) { emit this->preItemAppended (); this->m_list << item; emit this->postItemAppended (); } -void FileLoader::fetch(const QList & urls) +void FilesFetcher::fetch(const QList & urls) { FMH::MODEL_LIST res; for(const auto &url : urls) { QDirIterator it(url.toLocalFile(), FMH::FILTER_LIST[FMH::FILTER_TYPE::TEXT], QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories); while (it.hasNext()) { const auto item = FMH::getFileInfoModel (QUrl::fromLocalFile (it.next ())); res << item; emit this->itemReady (item); } } // emit this->resultReady(res); } FMH::MODEL_LIST DocumentsModel::items() const { return this->m_list; } void DocumentsModel::componentComplete() { emit this->start({FMH::DocumentsPath, FMH::DownloadsPath, FMH::DesktopPath}); } diff --git a/src/models/documentsmodel.h b/src/models/documentsmodel.h index c149a63..390d1ea 100644 --- a/src/models/documentsmodel.h +++ b/src/models/documentsmodel.h @@ -1,50 +1,50 @@ #ifndef DOCUMENTSMODEL_H #define DOCUMENTSMODEL_H #include #include #ifdef STATIC_MAUIKIT #include "fmh.h" #include "mauilist.h" #else #include #include #endif -class FileLoader : public QObject +class FilesFetcher : public QObject { Q_OBJECT public slots: void fetch(const QList &urls); signals: void resultReady(FMH::MODEL_LIST items); void itemReady(FMH::MODEL item); }; class DocumentsModel : public MauiList { Q_OBJECT QThread m_worker; public: DocumentsModel(QObject *parent = nullptr); ~DocumentsModel() override; FMH::MODEL_LIST items() const override final; void componentComplete() override final; private: void setList(const FMH::MODEL_LIST &list); void append(const FMH::MODEL &item); FMH::MODEL_LIST m_list; signals: void start(QList urls); }; #endif // DOCUMENTSMODEL_H diff --git a/src/views/DocumentsView.qml b/src/views/DocumentsView.qml index 92aeb54..7aa4ee0 100644 --- a/src/views/DocumentsView.qml +++ b/src/views/DocumentsView.qml @@ -1,51 +1,59 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.6 as Kirigami import org.kde.mauikit 1.0 as Maui import org.maui.nota 1.0 as Nota Maui.Page { id: control property alias model : _documentsModel property alias list : _documentsList headBar.middleContent: Maui.TextField { Layout.fillWidth: true placeholderText: qsTr("Filter...") - onAccepted: _gridView.model.filter = text - onCleared: _gridView.model.filter = text + onAccepted: _listView.model.filter = text + onCleared: _listView.model.filter = text } Maui.ListBrowser { - id: _gridView + id: _listView anchors.fill: parent model: Maui.BaseModel { id: _documentsModel list: Nota.Documents { id: _documentsList } } - delegate: Maui.ListBrowserDelegate + delegate: Maui.ItemDelegate { height: Maui.Style.rowHeight *2 - width: parent.width - label1.text: model.label - label2.text: model.path + width: _listView.width padding: Maui.Style.space.medium + + Maui.ListItemTemplate + { + anchors.fill: parent + label1.text: model.label + label2.text: model.path + iconSource: model.icon + iconSizeHint: Maui.Style.iconSizes.big + } + onClicked: { - root.openTab(_gridView.model.get(index).path) + root.openTab(_listView.model.get(index).path) _actionGroup.currentIndex = views.editor } } } }