diff --git a/mix/FileIo.cpp b/mix/FileIo.cpp index 449512975..9c8f0c95b 100644 --- a/mix/FileIo.cpp +++ b/mix/FileIo.cpp @@ -20,6 +20,7 @@ * Ethereum IDE client. */ +#include #include #include #include @@ -40,7 +41,6 @@ using namespace dev; using namespace dev::crypto; using namespace dev::mix; - void FileIo::openFileBrowser(QString const& _dir) { QDesktopServices::openUrl(QUrl(_dir)); @@ -87,7 +87,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 +97,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 +194,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..53b66b307 100644 --- a/mix/FileIo.h +++ b/mix/FileIo.h @@ -22,6 +22,7 @@ #pragma once +#include #include #include @@ -39,8 +40,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(): m_watcher(new QFileSystemWatcher(this)) { connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &FileIo::fileChanged); } /// 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 +59,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..390670c8b 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,6 +53,9 @@ Item { breakpointsChanged(document.documentId); }); editor.setText(data, document.syntaxMode); + editor.onIsCleanChanged.connect(function(){ + isCleanChanged(editor.isClean, document.documentId); + }); } function getEditor(documentId) { @@ -91,6 +96,12 @@ Item { editor.toggleBreakpoint(); } + function resetEditStatus() { + var editor = getEditor(currentDocumentId); + if (editor) + editor.changeGenerator(); + } + Component.onCompleted: projectModel.codeEditor = codeEditorView; Connections { @@ -100,8 +111,12 @@ Item { } onProjectSaving: { for (var i = 0; i < editorListModel.count; i++) + { fileIo.writeFile(editorListModel.get(i).path, editors.itemAt(i).item.getText()); + resetEditStatus(); + } } + onProjectClosed: { for (var i = 0; i < editorListModel.count; i++) { editors.itemAt(i).visible = false; @@ -109,6 +124,31 @@ Item { editorListModel.clear(); currentDocumentId = ""; } + + onDocumentSaving: { + for (var i = 0; i < editorListModel.count; i++) + { + if (editorListModel.get(i).path === document) + { + fileIo.writeFile(document, editors.itemAt(i).item.getText()); + resetEditStatus(); + 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); + } } Repeater { @@ -121,10 +161,18 @@ 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 (visible && changed) + { + changed = false; + messageDialog.item = loader.item; + messageDialog.doc = editorListModel.get(index); + messageDialog.open(); + } } Component.onCompleted: { loadIfNotLoaded() @@ -133,6 +181,21 @@ Item { doLoadDocument(loader.item, editorListModel.get(index)) } + Connections + { + target: projectModel + onDocumentChanged: { + if (currentDocumentId == documentId) + { + messageDialog.item = loader.item; + messageDialog.doc = editorListModel.get(index); + messageDialog.open(); + } + else + changed = true; + } + } + function loadIfNotLoaded () { 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 081e0cd95..60d583128 100644 --- a/mix/qml/MainContent.qml +++ b/mix/qml/MainContent.qml @@ -179,6 +179,10 @@ Rectangle { width: 350 Layout.minimumWidth: 250 Layout.fillHeight: true + Connections { + target: projectModel.codeEditor + + } } Rectangle { id: contentView 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 d3b4070aa..b98fb2395 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 document) signal documentOpened(var document) signal documentRemoved(var documentId) signal documentUpdated(var documentId) //renamed @@ -21,10 +22,12 @@ 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" @@ -41,6 +44,7 @@ Item { //interface function saveAll() { ProjectModelCode.saveAll(); } + function saveCurrentDocument() { ProjectModelCode.saveCurrentDocument(); } function createProject() { ProjectModelCode.createProject(); } function closeProject() { ProjectModelCode.closeProject(); } function saveProject() { ProjectModelCode.saveProject(); } @@ -69,6 +73,13 @@ Item { } } + Connections { + target: codeEditor + onIsCleanChanged: { + isCleanChanged(isClean, documentId); + } + } + NewProjectDialog { id: newProjectDialog visible: false @@ -79,6 +90,18 @@ Item { } } + Connections + { + target: fileIo + property bool saving: false + onFileChanged: + { + fileIo.watchFileChanged(_filePath); + if (_filePath.indexOf(currentDocumentId, _filePath.length - currentDocumentId.length)) + documentChanged(_filePath); + } + } + MessageDialog { id: saveMessageDialog title: qsTr("Project") diff --git a/mix/qml/TransactionLog.qml b/mix/qml/TransactionLog.qml index 8e25b0812..7d30356e1 100644 --- a/mix/qml/TransactionLog.qml +++ b/mix/qml/TransactionLog.qml @@ -30,12 +30,21 @@ 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 (compilationStatus.compilationComplete && codeModel.hasContract && !clientModel.running) projectModel.stateListModel.debugDefaultState(); } onProjectClosed: @@ -44,6 +53,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..f6081d9cf 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 changeGenerator() { + editorBrowser.runJavaScript("changeGeneration()", function(result) {}); + } + Connections { target: appContext onClipboardChanged: syncClipboard() @@ -75,6 +80,7 @@ Item { runJavaScript("getTextChanged()", function(result) { }); pollTimer.running = true; syncClipboard(); + parent.changeGenerator(); } } @@ -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 6f03088a4..fc279626c 100644 --- a/mix/qml/WebPreview.qml +++ b/mix/qml/WebPreview.qml @@ -86,8 +86,7 @@ Item { Connections { target: projectModel - //onProjectSaved : reloadOnSave(); - //onDocumentSaved: reloadOnSave(); + onDocumentAdded: { var document = projectModel.getDocument(documentId) if (document.isHtml) @@ -115,6 +114,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 d308119ae..c8045803e 100644 --- a/mix/qml/js/ProjectModel.js +++ b/mix/qml/js/ProjectModel.js @@ -25,6 +25,15 @@ Qt.include("TransactionHelper.js") var htmlTemplate = "\n\n\n\n\n\n\n"; var contractTemplate = "contract Contract {\n}\n"; +function saveCurrentDocument() +{ + documentSaving(projectPath + currentDocumentId); + var doc = projectListModel.get(getDocumentIndex(currentDocumentId)); + if (doc.isContract) + contractSaved(currentDocumentId); + documentSaved(currentDocumentId); +} + function saveAll() { saveProject(); } @@ -55,7 +64,7 @@ function saveProject() { deploymentDir: projectModel.deploymentDir }; for (var i = 0; i < projectListModel.count; i++) - projectData.files.push(projectListModel.get(i).fileName) + projectData.files.push(projectListModel.get(i).fileName); projectSaving(projectData); var json = JSON.stringify(projectData, null, "\t"); var projectFile = projectPath + projectFileName; @@ -105,7 +114,6 @@ function loadProject(path) { contractSources[doc.documentId] = fileIo.readFile(doc.path); } codeModel.reset(contractSources); - } function addFile(fileName) { @@ -132,6 +140,7 @@ function addFile(fileName) { }; projectListModel.append(docData); + fileIo.watchFileChanged(p); return docData.documentId; } @@ -184,6 +193,7 @@ function doCloseProject() { projectListModel.clear(); projectPath = ""; currentDocumentId = ""; + fileIo.stopFilesWatcher(); projectClosed(); } @@ -247,6 +257,7 @@ function removeDocument(documentId) { var document = projectListModel.get(i); if (!document.isContract) { projectListModel.remove(i); + fileIo.stopWatching(projectPath+ documentId); documentRemoved(documentId); } } diff --git a/mix/qml/main.qml b/mix/qml/main.qml index 75a913155..e4db8c437 100644 --- a/mix/qml/main.qml +++ b/mix/qml/main.qml @@ -281,7 +281,7 @@ ApplicationWindow { text: qsTr("Save All") shortcut: "Ctrl+S" enabled: !projectModel.isEmpty - onTriggered: projectModel.saveAll(); + onTriggered: projectModel.saveCurrentDocument(); } Action {