Add save+open to notebook, allow multiple windows, add translations, add welcome window
diff --git a/ide/Cell.cpp b/ide/Cell.cpp
index d00a219..85d20a3 100644
--- a/ide/Cell.cpp
+++ b/ide/Cell.cpp
@@ -90,6 +90,14 @@
return nullptr;
}
+void Cell::fromJson(QJsonObject json)
+{
+ setCode(json["code"].toString());
+ setResult(json["result"].toString());
+ setStatus(json["status"].toInt());
+ setResultType(json["resultType"].toInt());
+}
+
QJsonObject Cell::toJson() const
{
QJsonObject object;
diff --git a/ide/Cell.h b/ide/Cell.h
index 55da010..350874c 100644
--- a/ide/Cell.h
+++ b/ide/Cell.h
@@ -38,6 +38,7 @@
Q_INVOKABLE static Cell *cellFromUuid(QUuid uuid);
+ Q_INVOKABLE void fromJson(QJsonObject json);
Q_INVOKABLE QJsonObject toJson() const;
enum Status
diff --git a/ide/IdeMain.cpp b/ide/IdeMain.cpp
index caa0c85..ff8025f 100644
--- a/ide/IdeMain.cpp
+++ b/ide/IdeMain.cpp
@@ -2,15 +2,12 @@
#include <QQmlApplicationEngine>
#include <QQuickStyle>
+#include <QTranslator>
#include "CellModel.h"
int ideMain(Application *app)
{
-#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
- QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
-#endif
-
QQmlApplicationEngine engine;
QQuickStyle::setStyle("Material");
@@ -18,6 +15,10 @@
qRegisterMetaType<CellModel>();
qRegisterMetaType<CellModel *>();
+ QTranslator translator;
+ qInfo() << "loading translations" << translator.load(QLocale(), "refal", "_", ":/ts/", ".qm");
+ app->installTranslator(&translator);
+
const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
app, [url](QObject *obj, const QUrl &objUrl)
diff --git a/ide/Notebook.cpp b/ide/Notebook.cpp
index 8d0e8ee..36cc0a3 100644
--- a/ide/Notebook.cpp
+++ b/ide/Notebook.cpp
@@ -5,6 +5,7 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QFileDialog>
+#include <QDebug>
// TODO: avoid potential race condition if Cell is deleted, pass by value with same UUID instead
@@ -57,6 +58,47 @@
_rt->unqueueCell(Cell::cellFromUuid(uuid));
}
+void Notebook::fromJson(QJsonDocument doc)
+{
+ QJsonObject nb = doc.object();
+ QJsonArray cellArray = nb["cells"].toArray();
+
+ for (const QJsonValueRef &cell : cellArray)
+ {
+ cellModel()->insertCellBefore(cellModel()->rowCount());
+ _cells.last()->fromJson(cell.toObject());
+ }
+}
+
+void Notebook::open(QString path)
+{
+ QFile file(path);
+
+ if (file.exists())
+ {
+ file.open(QFile::ReadOnly);
+
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
+
+ if (error.error == QJsonParseError::NoError)
+ {
+ fromJson(doc);
+ }
+ else
+ {
+ qWarning() << error.errorString();
+ }
+
+ file.close();
+ setSavePath(path);
+ }
+ else
+ {
+ qWarning() << "File does not exist" << path;
+ }
+}
+
QJsonDocument Notebook::toJson() const
{
QJsonObject nb;
diff --git a/ide/Notebook.h b/ide/Notebook.h
index dda07bc..6a6541d 100644
--- a/ide/Notebook.h
+++ b/ide/Notebook.h
@@ -26,6 +26,9 @@
Q_INVOKABLE void runCell(QUuid uuid);
Q_INVOKABLE void quitCell(QUuid uuid);
+ Q_INVOKABLE void fromJson(QJsonDocument doc);
+ Q_INVOKABLE void open(QString path);
+
Q_INVOKABLE QJsonDocument toJson() const;
Q_INVOKABLE void save();
diff --git a/ide/icons/book.svg b/ide/icons/book.svg
new file mode 100644
index 0000000..24d24e2
--- /dev/null
+++ b/ide/icons/book.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.125 1.875C15.484 1.88633 13.5207 2.25 12.1 2.87852C11.0398 3.34727 10.625 3.70195 10.625 4.90352V17.5C12.2488 16.0352 13.6898 15.625 19.375 15.625V1.875H18.125ZM1.875 1.875C4.51602 1.88633 6.4793 2.25 7.9 2.87852C8.96016 3.34727 9.375 3.70195 9.375 4.90352V17.5C7.75117 16.0352 6.31016 15.625 0.625 15.625V1.875H1.875Z" fill="#C8C8C8"/>
+</svg>
diff --git a/ide/icons/document.svg b/ide/icons/document.svg
new file mode 100644
index 0000000..918c5ad
--- /dev/null
+++ b/ide/icons/document.svg
@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.375 8.4375V1.25H3.59375C3.46943 1.25 3.3502 1.29939 3.26229 1.38729C3.17439 1.4752 3.125 1.59443 3.125 1.71875V18.2812C3.125 18.4056 3.17439 18.5248 3.26229 18.6127C3.3502 18.7006 3.46943 18.75 3.59375 18.75H16.4062C16.5306 18.75 16.6498 18.7006 16.7377 18.6127C16.8256 18.5248 16.875 18.4056 16.875 18.2812V8.75H9.6875C9.60462 8.75 9.52513 8.71708 9.46653 8.65847C9.40792 8.59987 9.375 8.52038 9.375 8.4375Z" fill="#C8C8C8"/>
+<path d="M10.625 1.62852V7.34375C10.625 7.38519 10.6415 7.42493 10.6708 7.45423C10.7001 7.48354 10.7398 7.5 10.7813 7.5H16.4965C16.5119 7.50001 16.527 7.49545 16.5399 7.48689C16.5527 7.47833 16.5627 7.46616 16.5687 7.45191C16.5746 7.43766 16.5762 7.42197 16.5732 7.40682C16.5702 7.39167 16.5629 7.37774 16.552 7.3668L10.7582 1.57305C10.7473 1.56217 10.7333 1.55477 10.7182 1.5518C10.703 1.54882 10.6873 1.5504 10.6731 1.55633C10.6588 1.56227 10.6467 1.57229 10.6381 1.58514C10.6295 1.59799 10.625 1.61308 10.625 1.62852V1.62852Z" fill="#C8C8C8"/>
+</svg>
diff --git a/ide/qml/NbWindow.qml b/ide/qml/NbWindow.qml
new file mode 100644
index 0000000..5710c9e
--- /dev/null
+++ b/ide/qml/NbWindow.qml
@@ -0,0 +1,213 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.15
+import QtQuick.Controls.Material 2.0
+import QtQuick.Layouts 1.3
+
+import sh.swisschili.REFAL 1.0
+
+ApplicationWindow {
+ id: root
+ width: 800
+ height: 800
+ title: "Refal Notebook — " + notebook.savePath
+ visible: true
+
+ Material.theme: Material.Light
+ Material.accent: Material.Orange
+
+ minimumWidth: column.implicitWidth
+
+ required property ApplicationWindow welcomeWindow
+
+ function openNotebook(path) {
+ notebook.open(path);
+ }
+
+ menuBar: MenuBar {
+ Menu {
+ title: qsTr("&File")
+
+ Action {
+ text: qsTr("&New")
+
+ onTriggered: {
+ welcomeWindow.openNotebook();
+ }
+ }
+
+ Action {
+ text: qsTr("&Save")
+ shortcut: "Ctrl+s"
+
+ onTriggered: {
+ notebook.save()
+ }
+ }
+
+ Action {
+ text: qsTr("&Open")
+ shortcut: "Ctrl+o"
+
+ onTriggered: {
+ notebook.open();
+ }
+ }
+ }
+
+ Menu {
+ title: qsTr("&View")
+
+ Action {
+ text: qsTr("&Welcome Window")
+ checkable: true
+
+ checked: welcomeWindow.visible
+
+ onTriggered: {
+ welcomeWindow.toggleVisible()
+ }
+ }
+
+ Action {
+ id: varInspector
+ text: qsTr("&Variable Inspector")
+
+ checkable: true
+ }
+ }
+
+ Menu {
+ title: qsTr("&Runtime")
+
+ Action {
+ text: qsTr("Run &Selected Cell")
+ shortcut: "Ctrl+Return"
+
+ onTriggered: {
+ if (codeEditor.currentItem !== null) {
+ notebook.runCell(codeEditor.currentItem.uuid)
+ }
+ }
+ }
+
+ Action {
+ text: qsTr("Run &All")
+ }
+
+ Action {
+ text: qsTr("&Reset Runtime State")
+ }
+ }
+ }
+
+ Notebook {
+ id: notebook
+
+ onSaveError: (message) =>
+ {
+ console.error(message)
+ }
+ }
+
+ ColumnLayout {
+ id: column
+ anchors.fill: parent
+
+ SplitView {
+ id: split
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ orientation: Qt.Horizontal
+
+ ListView {
+ id: codeEditor
+ SplitView.fillWidth: true
+ SplitView.minimumWidth: 400
+ model: notebook.cellModel
+ clip: true
+
+ spacing: 5
+
+ header: ColumnLayout {
+ width: codeEditor.width
+
+ Pane {
+ Layout.bottomMargin: 0
+
+ ColumnLayout {
+ Label {
+ font.pointSize: 18
+ text: qsTr("Notebook")
+ }
+
+ Label {
+ visible: codeEditor.count === 0
+
+ text: qsTr("Looks like you haven't created any cells yet. Click the + button below to create one.")
+ }
+ }
+ }
+
+ InsertRow {
+ onInsertClicked: notebook.cellModel.insertCellBefore(0)
+ }
+
+ Item {
+ height: 5 // JANK!
+ }
+ }
+
+ delegate: NotebookCell {
+ id: notebookCell
+
+ required property var model
+ required property var index
+ required property var uuid
+
+ width: codeEditor.width
+
+ code: model.code
+ result: model.result.trim()
+ status: model.status
+ resultType: model.resultType
+ cellActive: codeEditor.currentIndex === index
+
+ onCodeEditingFinished: code => model.code = code
+
+ onInsertBelowClicked: {
+ console.info(index);
+ notebook.cellModel.insertCellBefore(index + 1);
+ }
+
+ onRunClicked: {
+ notebook.runCell(uuid)
+ }
+
+ onCellFocused: {
+ codeEditor.currentIndex = index
+ }
+
+ onDeleteClicked: {
+ notebook.cellModel.deleteCellAt(index)
+ }
+
+ onCellUnfocused: {
+ codeEditor.currentIndex = -1
+ }
+ }
+ }
+
+ Item {
+ id: variables
+ SplitView.minimumWidth: 240
+
+ visible: varInspector.checked
+
+ Label {
+ anchors.centerIn: parent
+ text: qsTr("Variables")
+ }
+ }
+ }
+ }
+}
diff --git a/ide/qml/NotebookCell.qml b/ide/qml/NotebookCell.qml
index 24eaf17..7653dd7 100644
--- a/ide/qml/NotebookCell.qml
+++ b/ide/qml/NotebookCell.qml
@@ -90,7 +90,7 @@
selectByMouse: true
wrapMode: TextEdit.WrapAtWordBoundaryOrAnywhere
- placeholderText: "Write some code..."
+ placeholderText: qsTr("Write some code...")
Keys.onTabPressed: {
var pos = cursorPosition + 4
@@ -134,7 +134,7 @@
MenuItem {
icon.source: "qrc:///icons/trash.svg"
icon.color: Material.color(Material.Red)
- text: "Delete"
+ text: qsTr("Delete")
onClicked: root.deleteClicked()
}
diff --git a/ide/qml/RecentNotebook.qml b/ide/qml/RecentNotebook.qml
new file mode 100644
index 0000000..d0b1120
--- /dev/null
+++ b/ide/qml/RecentNotebook.qml
@@ -0,0 +1,39 @@
+import QtQuick 2.0
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.0
+
+RowLayout {
+ id: root
+
+ property string name: "Hello.refnb"
+ property alias containsMouse: mouseArea.containsMouse
+ signal clicked()
+
+ Image {
+ id: nbIcon
+ width: 100
+ height: 100
+ source: "qrc:///icons/document.svg"
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Label {
+ text: name
+ Layout.fillWidth: true
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onClicked: root.clicked()
+ }
+ }
+}
+
+/*##^##
+Designer {
+ D{i:0;autoSize:true;height:24;width:300}
+}
+##^##*/
diff --git a/ide/qml/Tip.qml b/ide/qml/Tip.qml
new file mode 100644
index 0000000..ba6084b
--- /dev/null
+++ b/ide/qml/Tip.qml
@@ -0,0 +1,33 @@
+import QtQuick 2.0
+import QtQuick.Layouts 1.12
+import QtQuick.Controls 2.0
+
+RowLayout {
+ id: root
+
+ property string title: "title"
+ property string url: "https://swisschili.sh"
+ property alias containsMouse: mouseArea.containsMouse
+
+ Image {
+ id: nbIcon
+ width: 100
+ height: 100
+ source: "qrc:///icons/book.svg"
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Label {
+ text: title
+ Layout.fillWidth: true
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onClicked: Qt.openUrlExternally(url)
+ }
+ }
+}
diff --git a/ide/qml/main.qml b/ide/qml/main.qml
index ab86efd..23f8926 100644
--- a/ide/qml/main.qml
+++ b/ide/qml/main.qml
@@ -1,178 +1,186 @@
-import QtQuick 2.5
+import QtQuick 2.0
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.0
-import QtQuick.Layouts 1.3
-
-import sh.swisschili.REFAL 1.0
+import QtQuick.Layouts 1.11
ApplicationWindow {
id: root
- width: 1080
- height: 720
- title: "Refal Notebook -- " + notebook.savePath
- visible: true
+
+ title: "REFAL Studio"
+
+ width: 680
+ height: 360
+
+ minimumWidth: 680
+ minimumHeight: 360
Material.theme: Material.Light
Material.accent: Material.Orange
- menuBar: MenuBar {
- Menu {
- title: qsTr("&File")
+ visible: true
- Action {
- text: "&Save"
- shortcut: "Ctrl+s"
+ function openNotebook(path=null) {
+ let NbWindow = Qt.createComponent("qrc:///qml/NbWindow.qml");
+ let window = NbWindow.createObject(null, {welcomeWindow: root});
- onTriggered: {
- notebook.save()
- }
- }
- }
-
- Menu {
- title: qsTr("&Runtime")
-
- Action {
- text: qsTr("Run &Selected Cell")
- shortcut: "Ctrl+Return"
-
- onTriggered: {
- if (codeEditor.currentItem !== null) {
- notebook.runCell(codeEditor.currentItem.uuid)
- }
- }
- }
- }
- }
-
- Notebook {
- id: notebook
-
- onSaveError: (message) =>
+ if (path !== null)
{
- console.error(message)
+ window.openNotebook(path)
}
}
- ColumnLayout {
- id: column
- anchors.fill: parent
+ function toggleVisible() {
+ if (visible)
+ hide();
+ else
+ show();
+ }
- TabBar {
- id: bar
+ Label {
+ id: textRefal
+ text: qsTr("REFAL")
+ anchors.left: parent.left
+ anchors.top: parent.top
+ font.pixelSize: 36
+ anchors.leftMargin: 36
+ anchors.topMargin: 29
+ font.weight: Font.Black
+ font.bold: true
+ font.italic: false
+ }
- Layout.fillWidth: true
+ Label {
+ id: textStudio
+ y: 29
+ text: qsTr("Studio")
+ anchors.verticalCenter: textRefal.verticalCenter
+ anchors.left: textRefal.right
+ font.pixelSize: 36
+ anchors.leftMargin: 6
+ font.bold: false
+ font.italic: false
+ font.weight: Font.Medium
+ color: Material.color(Material.Orange)
+ }
- TabButton {
- text: "Notebook"
- width: implicitWidth
+ Rectangle {
+ id: sepRect
+ x: 364
+ width: 1
+ color: "#cecece"
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 14
+ anchors.topMargin: 78
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Flickable {
+ id: notebooksFlick
+ anchors.left: textRefal.left
+ anchors.right: sepRect.left
+ anchors.top: textRefal.bottom
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: -8
+ anchors.rightMargin: 16
+ anchors.bottomMargin: 16
+ anchors.topMargin: 6
+ clip: true
+ flickableDirection: Flickable.VerticalFlick
+
+ ColumnLayout {
+ id: notebooksCol
+ spacing: 12
+
+ RowLayout {
+ id: nbBtnsRow
+ spacing: 12
+ Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+
+ Button {
+ id: newNotebookBtn
+ text: qsTr("New Notebook")
+ font.bold: true
+ highlighted: true
+
+ onClicked: {
+ root.openNotebook();
+ }
+ }
+
+ Button {
+ id: openNotebookBtn
+ text: qsTr("Open Existing")
+ }
}
- TabButton {
- text: "Another Notebook"
- width: implicitWidth
+ Repeater {
+ id: notebooksList
+
+ Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+ Layout.fillWidth: true
+ Layout.fillHeight: false
+
+ model: [
+ // "~/Documents/Hello.refnb", "~/Downloads/stuff/Goodbye.refnb", "/home/ch/dev/REFAL/build/test.refnb"
+ ]
+
+ delegate: RecentNotebook {
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+
+ name: modelData.split("/").pop()
+
+ ToolTip.text: modelData
+ ToolTip.visible: containsMouse
+ ToolTip.delay: 1000
+
+ onClicked: root.openNotebook(modelData)
+ }
}
- TabButton {
- text: "Testing"
- width: implicitWidth
+ Label {
+ Layout.leftMargin: 8
+ Layout.rightMargin: 8
+
+ visible: notebooksList.count == 0
+ text: qsTr("Your recent notebooks will appear here")
}
}
+ }
- SplitView {
- id: split
- Layout.fillHeight: true
+ ListView {
+ id: tipsList
+ anchors.left: sepRect.right
+ anchors.right: parent.right
+ anchors.top: textRefal.bottom
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 16
+ anchors.topMargin: 6
+ anchors.leftMargin: 16
+ anchors.rightMargin: 36
+ spacing: 12
+
+ model: [
+ {url: "https://wiki.swisschili.sh/wiki/REFAL", title: "REFAL Studio Wiki"},
+ {url: "https://wiki.swisschili.sh/wiki/REFAL/Cookbook", title: "REFAL Cookbook"},
+ {url: "http://refal.botik.ru/book/html/", title: "REFAL-5 Programming Guide (en)"},
+ {url: "http://refal.net/rf5_frm.htm", title: "REFAL-5 Programming Guide (ru)"}
+ ]
+
+ delegate: Tip {
Layout.fillWidth: true
- orientation: Qt.Horizontal
- ListView {
- id: codeEditor
- SplitView.fillWidth: true
- SplitView.minimumWidth: 400
- model: notebook.cellModel
- clip: true
+ url: modelData.url
+ title: modelData.title
- spacing: 5
-
- header: ColumnLayout {
- width: codeEditor.width
-
- Pane {
- Layout.bottomMargin: 0
-
- ColumnLayout {
- Label {
- font.pointSize: 18
- text: "Notebook"
- }
-
- Label {
- visible: codeEditor.count === 0
-
- text: "Looks like you haven't created any cells yet. Click the + button below to create one."
- }
- }
- }
-
- InsertRow {
- onInsertClicked: notebook.cellModel.insertCellBefore(0)
- }
-
- Item {
- height: 5 // JANK!
- }
- }
-
- delegate: NotebookCell {
- id: notebookCell
-
- required property var model
- required property var index
- required property var uuid
-
- width: codeEditor.width
-
- code: model.code
- result: model.result.trim()
- status: model.status
- resultType: model.resultType
- cellActive: codeEditor.currentIndex === index
-
- onCodeEditingFinished: code => model.code = code
-
- onInsertBelowClicked: {
- console.info(index);
- notebook.cellModel.insertCellBefore(index + 1);
- }
-
- onRunClicked: {
- console.info("Cell run clicked")
- notebook.runCell(uuid)
- }
-
- onCellFocused: {
- codeEditor.currentIndex = index
- }
-
- onDeleteClicked: {
- notebook.cellModel.deleteCellAt(index)
- }
-
- onCellUnfocused: {
- codeEditor.currentIndex = -1
- }
- }
- }
-
- Item {
- id: variables
- SplitView.minimumWidth: 240
-
- Label {
- anchors.centerIn: parent
- text: "Vars"
- }
- }
+ ToolTip.text: modelData.url
+ ToolTip.visible: containsMouse
+ ToolTip.delay: 1000
}
}
}
diff --git a/ide/resources.qrc b/ide/resources.qrc
index c4b120e..f76afd3 100644
--- a/ide/resources.qrc
+++ b/ide/resources.qrc
@@ -1,6 +1,6 @@
<RCC>
<qresource prefix="/">
- <file>qml/main.qml</file>
+ <file>qml/NbWindow.qml</file>
<file>qml/NotebookCell.qml</file>
<file>icons/play-circle.svg</file>
<file>icons/add.svg</file>
@@ -10,5 +10,10 @@
<file>icons/menu.svg</file>
<file>qml/Constants.qml</file>
<file>qml/qmldir</file>
+ <file>qml/main.qml</file>
+ <file>icons/book.svg</file>
+ <file>icons/document.svg</file>
+ <file>qml/RecentNotebook.qml</file>
+ <file>qml/Tip.qml</file>
</qresource>
</RCC>