diff --git a/mix/FileIo.cpp b/mix/FileIo.cpp index bf0351ee3..58668cad6 100644 --- a/mix/FileIo.cpp +++ b/mix/FileIo.cpp @@ -20,6 +20,7 @@ * Ethereum IDE client. */ +#include #include #include #include @@ -40,6 +41,10 @@ using namespace dev; using namespace dev::crypto; using namespace dev::mix; +FileIo::FileIo(): m_watcher(new QFileSystemWatcher(this)) +{ + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &FileIo::fileChanged); +} void FileIo::openFileBrowser(QString const& _dir) { @@ -87,7 +92,9 @@ QString FileIo::readFile(QString const& _url) void FileIo::writeFile(QString const& _url, QString const& _data) { - QFile file(pathFromUrl(_url)); + QString path = pathFromUrl(_url); + m_watcher->removePath(path); + QFile file(path); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream stream(&file); @@ -95,6 +102,7 @@ void FileIo::writeFile(QString const& _url, QString const& _data) } else error(tr("Error writing file %1").arg(_url)); + m_watcher->addPath(path); } void FileIo::copyFile(QString const& _sourceUrl, QString const& _destUrl) @@ -191,3 +199,12 @@ QStringList FileIo::makePackage(QString const& _deploymentFolder) return ret; } +void FileIo::watchFileChanged(QString const& _path) +{ + m_watcher->addPath(pathFromUrl(_path)); +} + +void FileIo::stopWatching(QString const& _path) +{ + m_watcher->removePath(pathFromUrl(_path)); +} diff --git a/mix/FileIo.h b/mix/FileIo.h index 400995435..33c2bd5fd 100644 --- a/mix/FileIo.h +++ b/mix/FileIo.h @@ -25,6 +25,8 @@ #include #include +class QFileSystemWatcher; + namespace dev { namespace mix @@ -39,8 +41,11 @@ class FileIo: public QObject signals: /// Signalled in case of IO error void error(QString const& _errorText); + /// Signnalled when a file is changed. + void fileChanged(QString const& _filePath); public: + FileIo(); /// Create a directory if it does not exist. Signals on failure. Q_INVOKABLE void makeDir(QString const& _url); /// Read file contents to a string. Signals on failure. @@ -55,12 +60,17 @@ public: Q_INVOKABLE bool fileExists(QString const& _url); /// Compress a folder, @returns sha3 of the compressed file. Q_INVOKABLE QStringList makePackage(QString const& _deploymentFolder); - /// Open a file browser + /// Open a file browser. Q_INVOKABLE void openFileBrowser(QString const& _dir); + /// Listen for files change in @arg _path. + Q_INVOKABLE void watchFileChanged(QString const& _path); + /// Stop Listenning for files change in @arg _path. + Q_INVOKABLE void stopWatching(QString const& _path); private: QString getHomePath() const; QString pathFromUrl(QString const& _url); + QFileSystemWatcher* m_watcher; }; } diff --git a/mix/qml/CodeEditorView.qml b/mix/qml/CodeEditorView.qml index 13ce1e220..0410382a5 100644 --- a/mix/qml/CodeEditorView.qml +++ b/mix/qml/CodeEditorView.qml @@ -2,12 +2,14 @@ import QtQuick 2.0 import QtQuick.Window 2.0 import QtQuick.Layouts 1.0 import QtQuick.Controls 1.0 +import QtQuick.Dialogs 1.1 Item { id: codeEditorView property string currentDocumentId: "" signal documentEdit(string documentId) signal breakpointsChanged(string documentId) + signal isCleanChanged(var isClean, string documentId) function getDocumentText(documentId) { for (var i = 0; i < editorListModel.count; i++) { @@ -51,12 +53,17 @@ Item { breakpointsChanged(document.documentId); }); editor.setText(data, document.syntaxMode); + editor.onIsCleanChanged.connect(function() { + isCleanChanged(editor.isClean, document.documentId); + }); } function getEditor(documentId) { for (var i = 0; i < editorListModel.count; i++) + { if (editorListModel.get(i).documentId === documentId) return editors.itemAt(i).item; + } return null; } @@ -91,6 +98,12 @@ Item { editor.toggleBreakpoint(); } + function resetEditStatus(docId) { + var editor = getEditor(docId); + if (editor) + editor.changeGeneration(); + } + Component.onCompleted: projectModel.codeEditor = codeEditorView; Connections { @@ -98,17 +111,65 @@ Item { onDocumentOpened: { openDocument(document); } + onProjectSaving: { for (var i = 0; i < editorListModel.count; i++) - fileIo.writeFile(editorListModel.get(i).path, editors.itemAt(i).item.getText()); + { + var doc = editorListModel.get(i); + fileIo.writeFile(doc.path, editors.itemAt(i).item.getText()); + } + } + + onProjectSaved: { + if (projectModel.appIsClosing) + return; + for (var i = 0; i < editorListModel.count; i++) + { + var doc = editorListModel.get(i); + resetEditStatus(doc.documentId); + } } + onProjectClosed: { - for (var i = 0; i < editorListModel.count; i++) { + for (var i = 0; i < editorListModel.count; i++) editors.itemAt(i).visible = false; - } editorListModel.clear(); currentDocumentId = ""; } + + onDocumentSaved: { + resetEditStatus(documentId); + } + + onContractSaved: { + resetEditStatus(documentId); + } + + onDocumentSaving: { + for (var i = 0; i < editorListModel.count; i++) + { + var doc = editorListModel.get(i); + if (doc.path === document.path) + { + fileIo.writeFile(document.path, editors.itemAt(i).item.getText()); + break; + } + } + } + } + + MessageDialog + { + id: messageDialog + title: qsTr("File Changed") + text: qsTr("This file has been changed outside of the editor. Do you want to reload it?") + standardButtons: StandardButton.Yes | StandardButton.No + property variant item + property variant doc + onYes: { + doLoadDocument(item, doc); + resetEditStatus(doc.documentId); + } } Repeater { @@ -121,10 +182,20 @@ Item { anchors.fill: parent source: "CodeEditor.qml" visible: (index >= 0 && index < editorListModel.count && currentDocumentId === editorListModel.get(index).documentId) + property bool changed: false onVisibleChanged: { loadIfNotLoaded() if (visible && item) + { loader.item.setFocus(); + if (changed) + { + changed = false; + messageDialog.item = loader.item; + messageDialog.doc = editorListModel.get(index); + messageDialog.open(); + } + } } Component.onCompleted: { loadIfNotLoaded() @@ -133,8 +204,39 @@ Item { doLoadDocument(loader.item, editorListModel.get(index)) } + Connections + { + target: projectModel + onDocumentChanged: { + if (!item) + return; + var current = editorListModel.get(index); + if (documentId === current.documentId) + { + if (currentDocumentId === current.documentId) + { + messageDialog.item = loader.item; + messageDialog.doc = editorListModel.get(index); + messageDialog.open(); + } + else + changed = true + } + } + + onDocumentUpdated: { + var document = projectModel.getDocument(documentId); + for (var i = 0; i < editorListModel.count; i++) + if (editorListModel.get(i).documentId === documentId) + { + editorListModel.set(i, document); + break; + } + } + } + function loadIfNotLoaded () { - if(visible && !active) { + if (visible && !active) { active = true; } } diff --git a/mix/qml/FilesSection.qml b/mix/qml/FilesSection.qml index cc5a67741..d9f664894 100644 --- a/mix/qml/FilesSection.qml +++ b/mix/qml/FilesSection.qml @@ -141,34 +141,53 @@ Rectangle color: isSelected ? ProjectFilesStyle.documentsList.highlightColor : "transparent" property bool isSelected property bool renameMode - Text { - id: nameText - height: parent.height - visible: !renameMode - color: rootItem.isSelected ? ProjectFilesStyle.documentsList.selectedColor : ProjectFilesStyle.documentsList.color - text: name; - font.family: fileNameFont.name - font.pointSize: ProjectFilesStyle.documentsList.fontSize + + Row { + spacing: 3 anchors.verticalCenter: parent.verticalCenter - verticalAlignment: Text.AlignVCenter + anchors.fill: parent anchors.left: parent.left anchors.leftMargin: ProjectFilesStyle.general.leftMargin + 2 - width: parent.width - Connections - { - target: selManager - onSelected: { - if (groupName != sectionName) - rootItem.isSelected = false; - else if (doc === documentId) - rootItem.isSelected = true; - else - rootItem.isSelected = false; + Text { + id: nameText + height: parent.height + visible: !renameMode + color: rootItem.isSelected ? ProjectFilesStyle.documentsList.selectedColor : ProjectFilesStyle.documentsList.color + text: name; + font.family: fileNameFont.name + font.pointSize: ProjectFilesStyle.documentsList.fontSize + verticalAlignment: Text.AlignVCenter + + Connections + { + target: selManager + onSelected: { + if (groupName != sectionName) + rootItem.isSelected = false; + else if (doc === documentId) + rootItem.isSelected = true; + else + rootItem.isSelected = false; - if (rootItem.isSelected && section.state === "hidden") - section.state = ""; + if (rootItem.isSelected && section.state === "hidden") + section.state = ""; + } + onIsCleanChanged: { + if (groupName === sectionName && doc === documentId) + editStatusLabel.visible = !isClean; + } } } + + DefaultLabel { + id: editStatusLabel + visible: false + color: rootItem.isSelected ? ProjectFilesStyle.documentsList.selectedColor : ProjectFilesStyle.documentsList.color + verticalAlignment: Text.AlignVCenter + text: "*" + width: 10 + height: parent.height + } } TextInput { diff --git a/mix/qml/MainContent.qml b/mix/qml/MainContent.qml index 7ac751a79..c9495b81f 100644 --- a/mix/qml/MainContent.qml +++ b/mix/qml/MainContent.qml @@ -173,6 +173,9 @@ Rectangle { width: 350 Layout.minimumWidth: 250 Layout.fillHeight: true + Connections { + target: projectModel.codeEditor + } } Rectangle { diff --git a/mix/qml/ProjectList.qml b/mix/qml/ProjectList.qml index 18e7a0d5b..f3964f094 100644 --- a/mix/qml/ProjectList.qml +++ b/mix/qml/ProjectList.qml @@ -67,8 +67,6 @@ Item { color: ProjectFilesStyle.documentsList.background } - - Rectangle { Layout.fillWidth: true @@ -83,6 +81,7 @@ Item { Repeater { model: [qsTr("Contracts"), qsTr("Javascript"), qsTr("Web Pages"), qsTr("Styles"), qsTr("Images"), qsTr("Misc")]; signal selected(string doc, string groupName) + signal isCleanChanged(string doc, string groupName, var isClean) property int incr: -1; id: sectionRepeater FilesSection @@ -145,6 +144,17 @@ Item { } } + onIsCleanChanged: { + for (var si = 0; si < sectionModel.count; si++) { + var document = sectionModel.get(si); + if (documentId === document.documentId && document.groupName === modelData) + { + selManager.isCleanChanged(documentId, modelData, isClean); + break; + } + } + } + onDocumentOpened: { if (document.groupName === modelData) sectionRepeater.selected(document.documentId, modelData); diff --git a/mix/qml/ProjectModel.qml b/mix/qml/ProjectModel.qml index cca2c834b..a47d77ddb 100644 --- a/mix/qml/ProjectModel.qml +++ b/mix/qml/ProjectModel.qml @@ -7,12 +7,13 @@ import Qt.labs.settings 1.0 import "js/ProjectModel.js" as ProjectModelCode Item { - id: projectModel signal projectClosed signal projectLoading(var projectData) signal projectLoaded() + signal documentSaving(var document) + signal documentChanged(var documentId) signal documentOpened(var document) signal documentRemoved(var documentId) signal documentUpdated(var documentId) //renamed @@ -21,15 +22,17 @@ Item { signal projectSaved() signal newProject(var projectData) signal documentSaved(var documentId) + signal contractSaved(var documentId) signal deploymentStarted() signal deploymentStepChanged(string message) signal deploymentComplete() signal deploymentError(string error) + signal isCleanChanged(var isClean, string documentId) property bool isEmpty: (projectPath === "") readonly property string projectFileName: ".mix" - property bool haveUnsavedChanges: false + property bool appIsClosing: false property string projectPath: "" property string projectTitle: "" property string currentDocumentId: "" @@ -38,11 +41,13 @@ Item { property var listModel: projectListModel property var stateListModel: projectStateListModel.model property CodeEditorView codeEditor: null + property var unsavedFiles: [] //interface function saveAll() { ProjectModelCode.saveAll(); } + function saveCurrentDocument() { ProjectModelCode.saveCurrentDocument(); } function createProject() { ProjectModelCode.createProject(); } - function closeProject() { ProjectModelCode.closeProject(); } + function closeProject(callBack) { ProjectModelCode.closeProject(callBack); } function saveProject() { ProjectModelCode.saveProject(); } function loadProject(path) { ProjectModelCode.loadProject(path); } function newHtmlFile() { ProjectModelCode.newHtmlFile(); } @@ -69,6 +74,20 @@ Item { } } + Connections { + target: codeEditor + onIsCleanChanged: { + for (var i in unsavedFiles) + { + if (unsavedFiles[i] === documentId && isClean) + unsavedFiles.splice(i, 1); + } + if (!isClean) + unsavedFiles.push(documentId); + isCleanChanged(isClean, documentId); + } + } + NewProjectDialog { id: newProjectDialog visible: false @@ -79,18 +98,36 @@ Item { } } + Connections + { + target: fileIo + property bool saving: false + onFileChanged: + { + fileIo.watchFileChanged(_filePath); + var documentId = ProjectModelCode.getDocumentByPath(_filePath); + documentChanged(documentId); + } + } + MessageDialog { id: saveMessageDialog title: qsTr("Project") - text: qsTr("Do you want to save changes?") - standardButtons: StandardButton.Ok | StandardButton.Cancel + text: qsTr("Some files require to be saved. Do you want to save changes?"); + standardButtons: StandardButton.Yes | StandardButton.No | StandardButton.Cancel icon: StandardIcon.Question - onAccepted: { + property var callBack; + onYes: { projectModel.saveAll(); ProjectModelCode.doCloseProject(); + if (callBack) + callBack(); } - onRejected: { + onRejected: {} + onNo: { ProjectModelCode.doCloseProject(); + if (callBack) + callBack(); } } @@ -135,6 +172,7 @@ Item { target: projectModel onProjectClosed: { projectSettings.lastProjectPath = ""; + projectPath = ""; } } diff --git a/mix/qml/StatusPane.qml b/mix/qml/StatusPane.qml index 20b30ce3b..6d4b5e7e1 100644 --- a/mix/qml/StatusPane.qml +++ b/mix/qml/StatusPane.qml @@ -77,11 +77,15 @@ Rectangle { function format(_message) { var formatted = _message.match(/(?:)/); + if (formatted === null) + formatted = _message.match(/(?:)/); if (formatted.length > 1) - formatted = formatted[1] + ": "; + formatted = formatted[1]; else return _message; var exceptionInfos = _message.match(/(?:tag_)(.+)/g); + if (exceptionInfos !== null && exceptionInfos.length > 0) + formatted += ": " for (var k in exceptionInfos) formatted += " " + exceptionInfos[k].replace("*]", "").replace("tag_", "").replace("=", ""); return formatted; diff --git a/mix/qml/TransactionLog.qml b/mix/qml/TransactionLog.qml index 8e25b0812..86d48a829 100644 --- a/mix/qml/TransactionLog.qml +++ b/mix/qml/TransactionLog.qml @@ -30,12 +30,23 @@ Item { anchors.fill: parent RowLayout { + Connections + { + id: compilationStatus + target: codeModel + property bool compilationComplete: false + onCompilationComplete: compilationComplete = true + onCompilationError: compilationComplete = false + } + Connections { target: projectModel onProjectSaved: { - if (codeModel.hasContract && !clientModel.running) + if (projectModel.appIsClosing) + return; + if (compilationStatus.compilationComplete && codeModel.hasContract && !clientModel.running) projectModel.stateListModel.debugDefaultState(); } onProjectClosed: @@ -44,6 +55,10 @@ Item { transactionModel.clear(); callModel.clear(); } + onContractSaved: { + if (compilationStatus.compilationComplete && codeModel.hasContract && !clientModel.running) + projectModel.stateListModel.debugDefaultState(); + } } ComboBox { diff --git a/mix/qml/WebCodeEditor.qml b/mix/qml/WebCodeEditor.qml index 165bad9ee..97cbcd30c 100644 --- a/mix/qml/WebCodeEditor.qml +++ b/mix/qml/WebCodeEditor.qml @@ -6,8 +6,9 @@ import QtWebEngine 1.0 import QtWebEngine.experimental 1.0 Item { - signal editorTextChanged; - signal breakpointsChanged; + signal editorTextChanged + signal breakpointsChanged + property bool isClean: true property string currentText: "" property string currentMode: "" property bool initialized: false @@ -50,6 +51,10 @@ Item { editorBrowser.runJavaScript("toggleBreakpoint()"); } + function changeGeneration() { + editorBrowser.runJavaScript("changeGeneration()", function(result) {}); + } + Connections { target: appContext onClipboardChanged: syncClipboard() @@ -75,6 +80,7 @@ Item { runJavaScript("getTextChanged()", function(result) { }); pollTimer.running = true; syncClipboard(); + parent.changeGeneration(); } } @@ -103,7 +109,9 @@ Item { }); } }); - + editorBrowser.runJavaScript("isClean()", function(result) { + isClean = result; + }); } } } diff --git a/mix/qml/WebPreview.qml b/mix/qml/WebPreview.qml index 34dc7c698..bff4be66f 100644 --- a/mix/qml/WebPreview.qml +++ b/mix/qml/WebPreview.qml @@ -87,8 +87,7 @@ Item { Connections { target: projectModel - //onProjectSaved : reloadOnSave(); - //onDocumentSaved: reloadOnSave(); + onDocumentAdded: { var document = projectModel.getDocument(documentId) if (document.isHtml) @@ -99,7 +98,13 @@ Item { } onDocumentUpdated: { - updateDocument(documentId, function(i) { pageListModel.set(i, projectModel.getDocument(documentId)) } ) + var document = projectModel.getDocument(documentId); + for (var i = 0; i < pageListModel.count; i++) + if (pageListModel.get(i).documentId === documentId) + { + pageListModel.set(i, document); + break; + } } onProjectLoading: { @@ -116,6 +121,12 @@ Item { } } + onDocumentSaved: + { + if (!projectModel.getDocument(documentId).isContract) + reloadOnSave(); + } + onProjectClosed: { pageListModel.clear(); } diff --git a/mix/qml/html/codeeditor.js b/mix/qml/html/codeeditor.js index e9958c685..30c70867b 100644 --- a/mix/qml/html/codeeditor.js +++ b/mix/qml/html/codeeditor.js @@ -18,7 +18,6 @@ editor.breakpointsChangeRegistered = false; editor.on("change", function(eMirror, object) { editor.changeRegistered = true; - }); var mac = /Mac/.test(navigator.platform); @@ -110,3 +109,14 @@ highlightExecution = function(start, end) { executionMark.clear(); executionMark = editor.markText(editor.posFromIndex(start), editor.posFromIndex(end), { className: "CodeMirror-exechighlight" }); } + +var changeId; +changeGeneration = function() +{ + changeId = editor.changeGeneration(true); +} + +isClean = function() +{ + return editor.isClean(changeId); +} diff --git a/mix/qml/js/ProjectModel.js b/mix/qml/js/ProjectModel.js index 413e61b85..e969ea46a 100644 --- a/mix/qml/js/ProjectModel.js +++ b/mix/qml/js/ProjectModel.js @@ -25,6 +25,16 @@ Qt.include("TransactionHelper.js") var htmlTemplate = "\n\n\n\n\n\n\n"; var contractTemplate = "contract Contract {\n}\n"; +function saveCurrentDocument() +{ + var doc = projectListModel.get(getDocumentIndex(currentDocumentId)); + documentSaving(doc); + if (doc.isContract) + contractSaved(currentDocumentId); + else + documentSaved(currentDocumentId); +} + function saveAll() { saveProject(); } @@ -33,16 +43,35 @@ function createProject() { newProjectDialog.open(); } -function closeProject() { +function closeProject(callBack) { if (!isEmpty) { - if (haveUnsavedChanges) + if (unsavedFiles.length > 0) + { + saveMessageDialog.callBack = callBack; saveMessageDialog.open(); + } else + { doCloseProject(); + if (callBack) + callBack(); + } } } function saveProject() { + if (!isEmpty) { + var projectData = saveProjectFile(); + if (projectData !== null) + { + projectSaving(projectData); + projectSaved(); + } + } +} + +function saveProjectFile() +{ if (!isEmpty) { var projectData = { files: [], @@ -55,13 +84,14 @@ function saveProject() { deploymentDir: projectModel.deploymentDir }; for (var i = 0; i < projectListModel.count; i++) - projectData.files.push(projectListModel.get(i).fileName) - projectSaving(projectData); + projectData.files.push(projectListModel.get(i).fileName); + var json = JSON.stringify(projectData, null, "\t"); var projectFile = projectPath + projectFileName; fileIo.writeFile(projectFile, json); - projectSaved(); + return projectData; } + return null; } function loadProject(path) { @@ -131,6 +161,8 @@ function addFile(fileName) { }; projectListModel.append(docData); + saveProjectFile(); + fileIo.watchFileChanged(p); return docData.documentId; } @@ -143,6 +175,17 @@ function getDocumentIndex(documentId) return -1; } +function getDocumentByPath(_path) +{ + for (var i = 0; i < projectListModel.count; i++) + { + var doc = projectListModel.get(i); + if (doc.path.indexOf(_path) !== -1) + return doc.documentId; + } + return null; +} + function openDocument(documentId) { if (documentId !== currentDocumentId) { documentOpened(projectListModel.get(getDocumentIndex(documentId))); @@ -226,12 +269,16 @@ function renameDocument(documentId, newName) { var i = getDocumentIndex(documentId); var document = projectListModel.get(i); if (!document.isContract) { + fileIo.stopWatching(document.path); var sourcePath = document.path; var destPath = projectPath + newName; fileIo.moveFile(sourcePath, destPath); document.path = destPath; document.name = newName; + document.fileName = newName; projectListModel.set(i, document); + fileIo.watchFileChanged(destPath); + saveProjectFile(); documentUpdated(documentId); } } diff --git a/mix/qml/main.qml b/mix/qml/main.qml index 75a913155..0c21e6513 100644 --- a/mix/qml/main.qml +++ b/mix/qml/main.qml @@ -17,6 +17,25 @@ ApplicationWindow { minimumHeight: 300 title: qsTr("Mix") + Connections + { + target: mainApplication + onClosing: + { + mainApplication.close(); + close.accepted = false; + } + } + + function close() + { + projectModel.appIsClosing = true; + if (projectModel.projectPath !== "") + projectModel.closeProject(function() { Qt.quit(); }) + else + Qt.quit(); + } + menuBar: MenuBar { Menu { title: qsTr("File") @@ -24,6 +43,7 @@ ApplicationWindow { MenuItem { action: openProjectAction } MenuSeparator {} MenuItem { action: saveAllFilesAction } + MenuItem { action: saveCurrentDocument } MenuSeparator {} MenuItem { action: addExistingFileAction } MenuItem { action: addNewJsFileAction } @@ -92,7 +112,10 @@ ApplicationWindow { id: exitAppAction text: qsTr("Exit") shortcut: "Ctrl+Q" - onTriggered: Qt.quit(); + onTriggered: + { + mainApplication.close(); + } } Action { @@ -279,11 +302,19 @@ ApplicationWindow { Action { id: saveAllFilesAction text: qsTr("Save All") - shortcut: "Ctrl+S" + shortcut: "Ctrl+Shift+A" enabled: !projectModel.isEmpty onTriggered: projectModel.saveAll(); } + Action { + id: saveCurrentDocument + text: qsTr("Save Current Document") + shortcut: "Ctrl+S" + enabled: !projectModel.isEmpty + onTriggered: projectModel.saveCurrentDocument(); + } + Action { id: closeProjectAction text: qsTr("Close Project")