From 89517870e965781c4b1dbb209215d1ac0c9d9b3c Mon Sep 17 00:00:00 2001 From: Tobias Kohn Date: Sat, 30 May 2020 22:42:47 +0200 Subject: [PATCH] Working on a new file management system --- CHANGELOG.md | 7 +- build.sbt | 2 +- src/main/resources/themes/chrome.css | 37 ++++ src/main/resources/themes/cobalt.css | 37 ++++ src/main/resources/themes/github.css | 37 ++++ src/main/resources/themes/idle.css | 37 ++++ src/main/resources/themes/jem.css | 37 ++++ src/main/resources/themes/monokai.css | 37 ++++ src/main/resources/themes/python-keywords.css | 71 ------- src/main/resources/themes/textmate.css | 37 ++++ src/main/resources/themes/tigerjython.css | 37 ++++ src/main/resources/themes/tigerjython2.css | 37 ++++ src/main/resources/themes/twilight.css | 37 ++++ .../ConfigValue.scala | 2 +- .../{config => configparser}/Parser.scala | 4 +- .../{config => configparser}/Token.scala | 6 +- .../TokenSource.scala | 2 +- .../tigerjython/core/Configuration.scala | 2 +- .../scala/tigerjython/core/Preferences.scala | 105 +++++----- src/main/scala/tigerjython/core/Utils.scala | 2 +- .../scala/tigerjython/files/Document.scala | 192 ++++++++++++++++++ .../scala/tigerjython/files/Documents.scala | 83 ++++++++ .../scala/tigerjython/jython/Builtins.java | 21 +- .../tigerjython/jython/JythonBuiltins.scala | 6 +- .../jython/TigerJythonBuiltins.scala | 56 +++++ .../tigerjython/ui/DefaultTabManager.scala | 60 ++++-- src/main/scala/tigerjython/ui/TabFrame.scala | 2 + .../scala/tigerjython/ui/TabManager.scala | 9 + .../ui/TigerJythonApplication.scala | 17 +- src/main/scala/tigerjython/ui/UIString.scala | 2 +- src/main/scala/tigerjython/ui/ZoomMixin.scala | 2 + .../ui/editor/DefaultOutputPane.scala | 5 + .../tigerjython/ui/editor/EditorTab.scala | 121 +++++------ .../tigerjython/ui/editor/OutputPane.scala | 2 + .../tigerjython/ui/editor/PythonEditor.scala | 15 +- .../ui/editor/PythonEditorTab.scala | 7 + .../tigerjython/ui/tabs/DocumentItem.scala | 60 ++++++ .../tigerjython/ui/tabs/NewDocumentItem.scala | 24 +++ .../ui/tabs/OpenDocumentItem.scala | 45 ++++ .../tigerjython/ui/tabs/OpenDocumentTab.scala | 39 ++++ .../utils/PrefBooleanProperty.scala | 26 +++ .../utils/PrefDoubleProperty.scala | 24 +++ .../utils/PrefIntegerProperty.scala | 24 +++ .../utils/PrefStringProperty.scala | 24 +++ 44 files changed, 1213 insertions(+), 226 deletions(-) delete mode 100644 src/main/resources/themes/python-keywords.css rename src/main/scala/tigerjython/{config => configparser}/ConfigValue.scala (96%) rename src/main/scala/tigerjython/{config => configparser}/Parser.scala (98%) rename src/main/scala/tigerjython/{config => configparser}/Token.scala (89%) rename src/main/scala/tigerjython/{config => configparser}/TokenSource.scala (99%) create mode 100644 src/main/scala/tigerjython/files/Document.scala create mode 100644 src/main/scala/tigerjython/files/Documents.scala create mode 100644 src/main/scala/tigerjython/jython/TigerJythonBuiltins.scala create mode 100644 src/main/scala/tigerjython/ui/tabs/DocumentItem.scala create mode 100644 src/main/scala/tigerjython/ui/tabs/NewDocumentItem.scala create mode 100644 src/main/scala/tigerjython/ui/tabs/OpenDocumentItem.scala create mode 100644 src/main/scala/tigerjython/ui/tabs/OpenDocumentTab.scala create mode 100644 src/main/scala/tigerjython/utils/PrefBooleanProperty.scala create mode 100644 src/main/scala/tigerjython/utils/PrefDoubleProperty.scala create mode 100644 src/main/scala/tigerjython/utils/PrefIntegerProperty.scala create mode 100644 src/main/scala/tigerjython/utils/PrefStringProperty.scala diff --git a/CHANGELOG.md b/CHANGELOG.md index c05594a..c46b78b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Changelog -### 3.0 Early Access Preview 4 (... 2020) +### 3.0 Early Access Preview 5 (30 May 2020) Implemented: - Support for `repeat` loops; - TigerJython specific libraries (only as part of the release, no sources); +- New file management that allows reopening previously edited files; +- "+"/"Add" tab to create a new document or reopen a previous one; + +Big fixes: +- Error messages on JRE 8 are displayed too large; ### 3.0 Early Access Preview 3 (25 May 2020) diff --git a/build.sbt b/build.sbt index 3ee5498..078490c 100644 --- a/build.sbt +++ b/build.sbt @@ -54,7 +54,7 @@ val buildDate = "%d %s %d".format( val buildTag = "-SNAPSHOT" -val buildVersion = "ea+04" +val buildVersion = "ea+05" // This is needed to run/test the project without having to restart SBT afterwards fork in run := true diff --git a/src/main/resources/themes/chrome.css b/src/main/resources/themes/chrome.css index aa68cec..8466f11 100644 --- a/src/main/resources/themes/chrome.css +++ b/src/main/resources/themes/chrome.css @@ -23,6 +23,43 @@ -fx-text-fill: maroon; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #ebebeb; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/cobalt.css b/src/main/resources/themes/cobalt.css index 6709193..db29999 100644 --- a/src/main/resources/themes/cobalt.css +++ b/src/main/resources/themes/cobalt.css @@ -33,6 +33,43 @@ -fx-background-color: #002240; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #011e3a; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/github.css b/src/main/resources/themes/github.css index 8921dea..5325e82 100644 --- a/src/main/resources/themes/github.css +++ b/src/main/resources/themes/github.css @@ -23,6 +23,43 @@ -fx-text-fill: maroon; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #e8e8e8; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/idle.css b/src/main/resources/themes/idle.css index 9529186..d8d8605 100644 --- a/src/main/resources/themes/idle.css +++ b/src/main/resources/themes/idle.css @@ -23,6 +23,43 @@ -fx-text-fill: maroon; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #e8e8e8; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/jem.css b/src/main/resources/themes/jem.css index b1d12ea..a8102f7 100644 --- a/src/main/resources/themes/jem.css +++ b/src/main/resources/themes/jem.css @@ -13,6 +13,43 @@ -fx-padding: 5; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #FFDDAA; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/monokai.css b/src/main/resources/themes/monokai.css index b472cf6..d00db4a 100644 --- a/src/main/resources/themes/monokai.css +++ b/src/main/resources/themes/monokai.css @@ -33,6 +33,43 @@ -fx-background-color: #202020; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #2F3129; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/python-keywords.css b/src/main/resources/themes/python-keywords.css deleted file mode 100644 index 5db75b4..0000000 --- a/src/main/resources/themes/python-keywords.css +++ /dev/null @@ -1,71 +0,0 @@ -.lineno { - -fx-fill: gold; - -fx-background-color: -fx-fill; - -fx-min-width: 40; - -fx-pref-width: 40; - -fx-font-style: italic; - -fx-text-fill: #966; -} - -.error-label { - -fx-background-color: mistyrose; - -fx-border-style: solid; - -fx-border-color: maroon; - -fx-padding: 5; -} - -.output-pane { - -fx-font-family: monospace; - -fx-text-fill: navy; -} - -.problems-pane { - -fx-font-family: monospace; - -fx-text-fill: maroon; -} - -.run-triangle { - -fx-fill: green; -} - -.stop-square { - -fx-fill: maroon; -} - -.keyword { - -fx-fill: purple; - -fx-font-weight: bold; -} -.semicolon { - -fx-font-weight: bold; -} -.paren { - -fx-fill: firebrick; - -fx-font-weight: bold; -} -.bracket { - -fx-fill: darkgreen; - -fx-font-weight: bold; -} -.brace { - -fx-fill: teal; - -fx-font-weight: bold; -} -.string { - -fx-fill: blue; -} - -.comment { - -fx-fill: cadetblue; -} - -.active-var { - -fx-fill: red; -} -.active-var:hover { - -fx-underline: true; -} - -.paragraph-box:has-caret { - -fx-background-color: #f2f9fc; -} \ No newline at end of file diff --git a/src/main/resources/themes/textmate.css b/src/main/resources/themes/textmate.css index 3f2521f..0bf84de 100644 --- a/src/main/resources/themes/textmate.css +++ b/src/main/resources/themes/textmate.css @@ -23,6 +23,43 @@ -fx-text-fill: maroon; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #f0f0f0; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/tigerjython.css b/src/main/resources/themes/tigerjython.css index 21917a8..df14561 100644 --- a/src/main/resources/themes/tigerjython.css +++ b/src/main/resources/themes/tigerjython.css @@ -13,6 +13,43 @@ -fx-padding: 5; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: gold; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/tigerjython2.css b/src/main/resources/themes/tigerjython2.css index 884ceea..09670aa 100644 --- a/src/main/resources/themes/tigerjython2.css +++ b/src/main/resources/themes/tigerjython2.css @@ -13,6 +13,43 @@ -fx-padding: 5; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #FFDDAA; -fx-background-color: -fx-fill; diff --git a/src/main/resources/themes/twilight.css b/src/main/resources/themes/twilight.css index c17cb7c..df964a6 100644 --- a/src/main/resources/themes/twilight.css +++ b/src/main/resources/themes/twilight.css @@ -33,6 +33,43 @@ -fx-background-color: #141414; } +.document-item { + -fx-padding: 10; +} + +.document-item:hover { + -fx-background-color: skyblue; + -fx-border-style: solid; + -fx-border-color: navy; +} + +.document-item .title { + -fx-font-size: 150%; + -fx-font-weight: bold; +} + +.document-item .paper { + -fx-fill: silver; +} + +.document-item .description { + -fx-font-family: monospaced; + -fx-font-size: 90%; + -fx-text-fill: gray; +} + +.document-item:hover .title { + -fx-text-fill: blue; +} + +.document-item:hover .description { + -fx-text-fill: navy; +} + +.document-item:hover .paper { + -fx-fill: white; +} + .lineno { -fx-fill: #232323; -fx-background-color: -fx-fill; diff --git a/src/main/scala/tigerjython/config/ConfigValue.scala b/src/main/scala/tigerjython/configparser/ConfigValue.scala similarity index 96% rename from src/main/scala/tigerjython/config/ConfigValue.scala rename to src/main/scala/tigerjython/configparser/ConfigValue.scala index 52d1f7e..c9964e5 100644 --- a/src/main/scala/tigerjython/config/ConfigValue.scala +++ b/src/main/scala/tigerjython/configparser/ConfigValue.scala @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package tigerjython.config +package tigerjython.configparser /** * The `ConfigValue` is a convenient way to read config values as a specific type. diff --git a/src/main/scala/tigerjython/config/Parser.scala b/src/main/scala/tigerjython/configparser/Parser.scala similarity index 98% rename from src/main/scala/tigerjython/config/Parser.scala rename to src/main/scala/tigerjython/configparser/Parser.scala index 7a7c2c2..1f60f9f 100644 --- a/src/main/scala/tigerjython/config/Parser.scala +++ b/src/main/scala/tigerjython/configparser/Parser.scala @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package tigerjython.config +package tigerjython.configparser /** * The parser takes a text as input and returns a map that maps qualified identifiers to values. @@ -152,7 +152,7 @@ object Parser { val res = getClass.getClassLoader.getResourceAsStream("resources/" + name) if (res != null) { val source = scala.io.Source.fromInputStream(res)("utf-8") - tigerjython.config.Parser(source.getLines()) + tigerjython.configparser.Parser(source.getLines()) } else null } diff --git a/src/main/scala/tigerjython/config/Token.scala b/src/main/scala/tigerjython/configparser/Token.scala similarity index 89% rename from src/main/scala/tigerjython/config/Token.scala rename to src/main/scala/tigerjython/configparser/Token.scala index 6bc6fc8..0e7778f 100644 --- a/src/main/scala/tigerjython/config/Token.scala +++ b/src/main/scala/tigerjython/configparser/Token.scala @@ -5,17 +5,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package tigerjython.config +package tigerjython.configparser /** * An individual token in the config's source. * * @author Tobias Kohn */ -private[config] +private[configparser] sealed abstract class Token -private[config] +private[configparser] object Token { object ASSIGNMENT extends Token diff --git a/src/main/scala/tigerjython/config/TokenSource.scala b/src/main/scala/tigerjython/configparser/TokenSource.scala similarity index 99% rename from src/main/scala/tigerjython/config/TokenSource.scala rename to src/main/scala/tigerjython/configparser/TokenSource.scala index 12bde98..fb9e0c6 100644 --- a/src/main/scala/tigerjython/config/TokenSource.scala +++ b/src/main/scala/tigerjython/configparser/TokenSource.scala @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package tigerjython.config +package tigerjython.configparser /** * The `TokenSource` is a `Token`-iterator that takes a text as a char-sequence as input and splits it up into tokens. diff --git a/src/main/scala/tigerjython/core/Configuration.scala b/src/main/scala/tigerjython/core/Configuration.scala index f16ece2..0471e8f 100644 --- a/src/main/scala/tigerjython/core/Configuration.scala +++ b/src/main/scala/tigerjython/core/Configuration.scala @@ -10,7 +10,7 @@ package tigerjython.core import java.net.URI import java.nio.file.{Path, Paths} -import tigerjython.config.Parser +import tigerjython.configparser.Parser /** * In contrast to the `Preferences`, which are basically entirely user-controlled, the configurations are rather diff --git a/src/main/scala/tigerjython/core/Preferences.scala b/src/main/scala/tigerjython/core/Preferences.scala index bc04169..8bd8d81 100644 --- a/src/main/scala/tigerjython/core/Preferences.scala +++ b/src/main/scala/tigerjython/core/Preferences.scala @@ -7,13 +7,13 @@ */ package tigerjython.core -import java.lang -import java.util.Locale +import java.util.{Base64, Locale, Random} import java.util.prefs.{Preferences => JPreferences} import javafx.beans.property._ import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.scene.text.Text +import tigerjython.utils._ /** * The global preferences keep track of values used to customise the application. By binding to the respective @@ -39,45 +39,6 @@ object Preferences { protected val preferences: JPreferences = JPreferences.userNodeForPackage(getClass) - // We define our own flavour of preferences, each with the ability to interact with the persistent storage. - - protected class PrefBooleanProperty(val name: String, default: Boolean = false) extends - SimpleBooleanProperty(preferences.getBoolean(name, default)) { - - addListener(new ChangeListener[lang.Boolean] { - override def changed(observableValue: ObservableValue[_ <: lang.Boolean], oldValue: lang.Boolean, - newValue: lang.Boolean): Unit = - preferences.putBoolean(name, newValue) - }) - } - - protected class PrefDoubleProperty(val name: String, default: Double = 0.0) extends - SimpleDoubleProperty(preferences.getDouble(name, default)) { - - addListener(new ChangeListener[Number] { - override def changed(observableValue: ObservableValue[_ <: Number], oldValue: Number, newValue: Number): Unit = - preferences.putDouble(name, newValue.doubleValue()) - }) - } - - protected class PrefIntegerProperty(val name: String, default: Int = 0) extends - SimpleIntegerProperty(preferences.getInt(name, default)) { - - addListener(new ChangeListener[Number] { - override def changed(observableValue: ObservableValue[_ <: Number], oldValue: Number, newValue: Number): Unit = - preferences.putInt(name, newValue.intValue()) - }) - } - - protected class PrefStringProperty(val name: String, default: String = null) extends - SimpleStringProperty(preferences.get(name, default)) { - - addListener(new ChangeListener[String] { - override def changed(observableValue: ObservableValue[_ <: String], oldValue: String, newValue: String): Unit = - preferences.put(name, newValue) - }) - } - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Private methods used to obtain some system standard values @@ -99,29 +60,31 @@ object Preferences { // UI Preferences - val fontFamily: StringProperty = new PrefStringProperty("editor.font-family", "monospace") + val fontFamily: StringProperty = new PrefStringProperty(preferences, "editor.font-family", "monospace") - val fontSize: DoubleProperty = new PrefDoubleProperty("editor.font-size", getDefaultFontSize) + val fontSize: DoubleProperty = new PrefDoubleProperty(preferences, "editor.font-size", getDefaultFontSize) - val globalZoom: DoubleProperty = new PrefDoubleProperty("global.zoom", 1.0) + val globalZoom: DoubleProperty = new PrefDoubleProperty(preferences, "global.zoom", 1.0) - val language: StringProperty = new PrefStringProperty("language", getDefaultLanguage) + val language: StringProperty = new PrefStringProperty(preferences, "language", getDefaultLanguage) val languageCode: StringProperty = new SimpleStringProperty() - val tabWidth: IntegerProperty = new PrefIntegerProperty("tabWidth", 4) + val tabWidth: IntegerProperty = new PrefIntegerProperty(preferences, "tabWidth", 4) - val theme: StringProperty = new PrefStringProperty("editor.theme", getDefaultTheme) + val theme: StringProperty = new PrefStringProperty(preferences, "editor.theme", getDefaultTheme) - val windowHeight: DoubleProperty = new PrefDoubleProperty("window.height", 600) + val windowHeight: DoubleProperty = new PrefDoubleProperty(preferences, "window.height", 600) - val windowWidth: DoubleProperty = new PrefDoubleProperty("window.width", 800) + val windowWidth: DoubleProperty = new PrefDoubleProperty(preferences, "window.width", 800) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// { val code = Configuration.getLanguageCode(language.get) languageCode.setValue(code) + if (preferences.get("user-number", null) == null) + preferences.put("user-number", generateUserNumber) } language.addListener(new ChangeListener[String] { @@ -131,23 +94,53 @@ object Preferences { } }) + /** + * Creates a new quasi-unique random string as a pseudo-user-id. This is only created the first time TigerJython is + * run on a specific machine and cannot be modified afterwards. This user-number is used for purposes of research, + * where you might want to collect a user's edits anonymously (without revealing the actual identity). However, the + * user-number here is generated irrespective of whether it is actually used or not. It might just sit there dormant + * in the preferences, making sure that opting in or out of such research programmes does not generate a new number. + * + * The generated user number is a modified base64 encoded ASCII string, representing (pseudo-)random bytes. By + * replacing `/` by `-`, it can also be used as a file-name. Furthermore note that each string starts with the + * same letter. This allows to change the scheme of the user numbers to be changed later on, where the initial + * letter will be modified accordingly, allowing a server to easily detect the used scheme. + * + * Finally note that there is, in principle, a chance that more than one user might have the same user numbers, but + * since it is used merely to differentiate between users taking part in a study and not for actual identification, + * this risk is acceptable. + */ + private def generateUserNumber: String = { + val data = new Array[Byte](48) + new Random().nextBytes(data) + val result = "A" + Base64.getEncoder.encodeToString(data).replace('/', '-') + println(result) + result + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // General and Python-Related Preferences - val checkSyntax: BooleanProperty = new PrefBooleanProperty("syntaxcheck", true) + val checkSyntax: BooleanProperty = new PrefBooleanProperty(preferences, "syntaxcheck", true) + + val checkUpdates: BooleanProperty = new PrefBooleanProperty(preferences, "check-updates", true) - val checkUpdates: BooleanProperty = new PrefBooleanProperty("check-updates", true) + val pythonInterpreter: StringProperty = new PrefStringProperty(preferences, "python.interpreter") - val pythonInterpreter: StringProperty = new PrefStringProperty("python.interpreter") + val repeatLoop: BooleanProperty = new PrefBooleanProperty(preferences, "repeat-loop", false) - val repeatLoop: BooleanProperty = new PrefBooleanProperty("repeat-loop", false) + val sendStatistics: BooleanProperty = new PrefBooleanProperty(preferences, "send-statistics") - val sendStatistics: BooleanProperty = new PrefBooleanProperty("send-statistics") + val syntaxCheckIsStrict: BooleanProperty = + new PrefBooleanProperty(preferences, "syntaxcheck-strict", true) - val syntaxCheckIsStrict: BooleanProperty = new PrefBooleanProperty("syntaxcheck-strict", true) + val syntaxCheckRejectDeadCode: BooleanProperty = + new PrefBooleanProperty(preferences, "syntaxcheck-deadcode", false) - val syntaxCheckRejectDeadCode: BooleanProperty = new PrefBooleanProperty("syntaxcheck-deadcode", false) + val userNumber: ReadOnlyStringProperty = new PrefStringProperty(preferences, "user-number") //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + } diff --git a/src/main/scala/tigerjython/core/Utils.scala b/src/main/scala/tigerjython/core/Utils.scala index 9e8fa0e..686a033 100644 --- a/src/main/scala/tigerjython/core/Utils.scala +++ b/src/main/scala/tigerjython/core/Utils.scala @@ -7,7 +7,7 @@ */ package tigerjython.core -import tigerjython.config._ +import tigerjython.configparser._ /** * Various small utility and helper functions. diff --git a/src/main/scala/tigerjython/files/Document.scala b/src/main/scala/tigerjython/files/Document.scala new file mode 100644 index 0000000..027f9a4 --- /dev/null +++ b/src/main/scala/tigerjython/files/Document.scala @@ -0,0 +1,192 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.files + +import java.io.{FileWriter, PrintWriter} +import java.nio.file.{Path, Paths} +import java.text.DateFormat +import java.util.Date +import java.util.prefs.{Preferences => JPreferences} + +import javafx.beans.property._ +import tigerjython.execute.PythonCodeTranslator +import tigerjython.ui.{TabFrame, TigerJythonApplication} +import tigerjython.utils._ + +/** + * In order to store (meta-)properties about the document being edited (such as the position of the caret, the modules + * used, etc.), we use this `Document` class. It allows us to restore a complete edit session later on. + * + * @author Tobias Kohn + */ +class Document(protected val prefNode: JPreferences) { + + private var _tempFile: java.io.File = _ + + var frame: TabFrame = _ + + // We ensure that the preferences contain some basic fields such as the date of its creation and the date when it + // has been modified last + { + val now = DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date()) + if (prefNode.get("created", null) == null) + prefNode.put("created", now) + if (prefNode.get("last-modified", null) == null) + prefNode.put("last-modified", now) + } + + val caretPosition: IntegerProperty = new PrefIntegerProperty(prefNode, "caret-pos", 0) + + def close(): Unit = { + prefNode.putBoolean("open", false) + caretPosition.unbind() + frame = null + } + + val description: StringProperty = new PrefStringProperty(prefNode, "description", "") + + def exists: Boolean = + path match { + case Some(p) => + p.toFile.exists() + case _ => + false + } + + def file: java.io.File = + path match { + case Some(p) => + p.toFile + case _ => + null + } + + def getDefaultFileSuffix: String = ".py" + + /** + * Creates a temporary file for execution, even if the document does otherwise not have an actual file backing it + * up. + */ + def getExecutableFile: java.io.File = { + var result = file + if (result == null) { + if (_tempFile == null) { + _tempFile = java.io.File.createTempFile(name.getValue + " (", ")" + getDefaultFileSuffix) + _tempFile.deleteOnExit() + } + result = _tempFile + } + val text = + PythonCodeTranslator.translate(this.text.get) match { + case Some(text) => + text + case None => + this.text.get + } + synchronized { + val writer = new FileWriter(result) + val printer = new PrintWriter(writer) + printer.print(text) + printer.close() + } + result + } + + val imports: StringProperty = new PrefStringProperty(prefNode, "imports", "") + + def index: Int = prefNode.getInt("index", 0) + + def isOpen: Boolean = prefNode.getBoolean("open", false) + + def isPath(p: Path): Boolean = + path match { + case Some(myPath) => + myPath.compareTo(p) == 0 + case None => + false + } + + /** + * Returns the date the document has last been modified. + */ + def lastModified: Date = + DateFormat.getDateInstance(DateFormat.MEDIUM).parse(prefNode.get("last-modified", null)) + + /** + * Returns the date the document has last been modified. + * + * This method returns the string + */ + def lastModifiedString: String = + prefNode.get("last-modified", "?") + + def load(): String = synchronized { + val f = file + if (f != null && f.exists()) { + val source = scala.io.Source.fromFile(file) + val result = source.getLines.mkString("\n") + this.text.setValue(result) + result + } else + this.text.get() + } + + /** + * Call `modified` to mark the document as having been modified today. + */ + def modified(): Unit = + prefNode.put("last-modified", DateFormat.getDateInstance(DateFormat.MEDIUM).format(new Date())) + + val name: StringProperty = new PrefStringProperty(prefNode, "name") + + def open(f: TabFrame): Unit = { + prefNode.putBoolean("open", true) + frame = f + } + + def path: Option[Path] = { + val s = pathString.get + if (s != null && s != "") + Some(Paths.get(s)) + else + None + } + + val pathString: ReadOnlyStringProperty = new PrefStringProperty(prefNode, "path") + + def save(text: String, caretPos: Int): Unit = synchronized { + val f = file + if (f != null) { + val writer = new FileWriter(file) + val printer = new PrintWriter(writer) + printer.print(text) + printer.close() + } + caretPosition.setValue(caretPos) + this.text.setValue(text) + modified() + setDescriptionFromText(text) + } + + private def setDescriptionFromText(text: String): Unit = + description.setValue(text.split('\n').take(3).mkString("\n")) + + def setPath(path: Path): Unit = { + pathString.asInstanceOf[PrefStringProperty].setValue(path.toAbsolutePath.toString) + val n = path.getFileName.toString + if (n.toLowerCase.endsWith(".py")) + name.setValue(n.dropRight(3)) + else + name.setValue(n) + } + + def show(): Unit = + TigerJythonApplication.tabManager.openDocument(this) + + val text: StringProperty = new PrefStringProperty(prefNode, "text") +} diff --git a/src/main/scala/tigerjython/files/Documents.scala b/src/main/scala/tigerjython/files/Documents.scala new file mode 100644 index 0000000..6f23979 --- /dev/null +++ b/src/main/scala/tigerjython/files/Documents.scala @@ -0,0 +1,83 @@ +package tigerjython.files + +import java.nio.file.Path +import java.util.prefs.{Preferences => JPreferences} +import tigerjython.ui.{TigerJythonApplication, editor} + +/** + * This is the companion object to `Document`s. In maintains a list of documents ever edited, where to find them as + * well as the position of the caret within it, etc. + * + * @author Tobias Kohn + */ +object Documents { + + protected lazy val preferences: JPreferences = JPreferences.userNodeForPackage(getClass) + + private val documents = collection.mutable.ArrayBuffer[Document]() + + private var currentIndex: Int = 0 + + def apply(path: Path): Document = + Documents.getDocumentForPath(path) + + def apply(path: java.io.File): Document = + Documents.getDocumentForPath(path.toPath) + + def createDocument(): Document = { + val node = preferences.node(createDocumentName) + node.putInt("index", currentIndex) + val result = new Document(node) + documents += result + result + } + + private def createDocumentName: String = { + currentIndex += 1 + val name = currentIndex.toHexString.toLowerCase + preferences.putInt("index", currentIndex) + val prefix = + if (name.length < 4) + 4 - name.length + else + name.length % 2 + "file_x%s%s".format("0" * prefix, name) + } + + def getDocumentForPath(path: Path): Document = + if (path != null) { + for (doc <- documents) + if (doc.isPath(path)) + return doc + val result = createDocument() + result.setPath(path) + result + } else + createDocument() + + def getListOfDocuments: Array[Document] = { + documents.toArray + } + + /** + * Reads all the documents from the preferences. + */ + def initialize(): Unit = { + currentIndex = preferences.getInt("index", currentIndex) + val openDocuments = collection.mutable.ArrayBuffer[Document]() + for (childName <- preferences.childrenNames()) { + val doc = new Document(preferences.node(childName)) + if (doc.exists) { + documents += doc + if (doc.isOpen) + openDocuments += doc + } + } + for (doc <- openDocuments) { + val tab = editor.PythonEditorTab(doc) + TigerJythonApplication.tabManager.addTab(tab) + } + if (openDocuments.isEmpty) + TigerJythonApplication.tabManager.addTab(editor.PythonEditorTab()) + } +} diff --git a/src/main/scala/tigerjython/jython/Builtins.java b/src/main/scala/tigerjython/jython/Builtins.java index dff4b81..c9276d6 100644 --- a/src/main/scala/tigerjython/jython/Builtins.java +++ b/src/main/scala/tigerjython/jython/Builtins.java @@ -8,8 +8,8 @@ package tigerjython.jython; import javax.swing.JOptionPane; -import org.python.core.Py; -import org.python.core.PyObject; + +import org.python.core.*; /** * Since Scala has no static methods, we need to define the static methods for built-in replacements inside a @@ -47,6 +47,19 @@ public static PyObject input(String prompt) { return Py.None; } + public static PyObject isInteger(PyObject obj) { + if (obj instanceof PyInteger || obj instanceof PyLong) + return Py.True; + if (obj instanceof PyFloat) { + double f = obj.asDouble(); + if ((Math.floor(f) == f) && !Double.isInfinite(f)) + return Py.True; + else + return Py.False; + } else + return Py.False; + } + public static PyObject msgDlg(PyObject message) { JOptionPane.showMessageDialog(null, message); return Py.None; @@ -59,4 +72,8 @@ public static java.awt.Color makeColor(String value) { int b = (int)Math.round(color.getBlue() * 255); return new java.awt.Color(r, g, b); } + + public static PyObject getTigerJythonFlag(String name) { + return TigerJythonBuiltins.getTigerJythonSeting(name); + } } diff --git a/src/main/scala/tigerjython/jython/JythonBuiltins.scala b/src/main/scala/tigerjython/jython/JythonBuiltins.scala index ef86a00..a95ce2a 100644 --- a/src/main/scala/tigerjython/jython/JythonBuiltins.scala +++ b/src/main/scala/tigerjython/jython/JythonBuiltins.scala @@ -22,7 +22,11 @@ object JythonBuiltins { * This is the list of all names of methods to be redefined. */ val builtinNames: Array[String] = Array( - "input", "msgDlg", "raw_input", "makeColor" + "getTigerJythonFlag", + "input", //"inputFloat", + //"inputInt", "inputString", + "isInteger", + "makeColor", "msgDlg", "raw_input", ) /** diff --git a/src/main/scala/tigerjython/jython/TigerJythonBuiltins.scala b/src/main/scala/tigerjython/jython/TigerJythonBuiltins.scala new file mode 100644 index 0000000..5c3320b --- /dev/null +++ b/src/main/scala/tigerjython/jython/TigerJythonBuiltins.scala @@ -0,0 +1,56 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.jython + +import org.python.core.{Py, PyObject} +import tigerjython.core.{BuildInfo, Configuration, Preferences} + +/** + * + * + * @author Tobias Kohn + */ +object TigerJythonBuiltins { + + /** + * Returns preference values for specific settings. + * + * Many of the settings here reflect the history of TigerJython and are due to a large body of additional libraries. + * + * If no value can be returned (either because of a missing setting, or because the key is undefined), `Py.None` is + * returned instead. + * + * @param name The name/key of the setting to query. + * @return Either the value as stored in the settings/preferences, or `Py.None`. + */ + def getTigerJythonSeting(name: String): PyObject = + name.toLowerCase match { + case "aplu.device.ip" => + Py.None + case "gpanel.windowsize" => + Py.None + case "gturtle.hideonstart" => + Py.None + case "gturtle.playground.height" => + Py.None + case "gturtle.playground.width" => + Py.None + case "gturtle.windowsize" | "gturtle.window.size" | "gturtle.playgroundsize" | "gturtle.playground.size" => + Py.None + case "jar" => + Py.newString(Configuration.sourcePath.getPath) + case "lang" | "language" | "tigerjython.language" => + Py.newString(Preferences.languageCode.get) + case "path" => + Py.newString(Configuration.sourcePath.getPath) + case "tigerjython.version" => + Py.newString(BuildInfo.Version) + case _ => + Py.None + } +} diff --git a/src/main/scala/tigerjython/ui/DefaultTabManager.scala b/src/main/scala/tigerjython/ui/DefaultTabManager.scala index d0bff9c..3435c89 100644 --- a/src/main/scala/tigerjython/ui/DefaultTabManager.scala +++ b/src/main/scala/tigerjython/ui/DefaultTabManager.scala @@ -10,6 +10,9 @@ package tigerjython.ui import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.scene.Node import javafx.scene.control.{Tab, TabPane} +import tigerjython.files.Document +import tigerjython.ui.editor.{EditorTab, PythonEditorTab} +import tigerjython.ui.tabs.OpenDocumentTab import scala.jdk.CollectionConverters._ @@ -33,14 +36,29 @@ class DefaultTabManager extends TabPane with TabManager { newTab.getContent match { case frame: TabFrame => frame.focusChanged(true) + case _ => } } }) + { + val frame = OpenDocumentTab() + val t = new Tab() + t.setContent(frame) + t.setClosable(false) + t.textProperty().bind(frame.caption) + getTabs.add(t) + } + def addTab(frame: TabFrame): Option[TabFrame] = if (frame != null) { val tab = createTab(frame) - getTabs.add(tab) + val tabList = getTabs + val len = tabList.size() + if (len > 0) + tabList.add(len-1, tab) + else + getTabs.add(tab) if (frame.manager != null) {} frame.manager = this getSelectionModel.select(tab) @@ -52,6 +70,7 @@ class DefaultTabManager extends TabPane with TabManager { val result = new Tab() result.setContent(frame) result.textProperty.bind(frame.caption) + result.setOnCloseRequest({ _ => frame.onClose() }) result } @@ -99,30 +118,37 @@ class DefaultTabManager extends TabPane with TabManager { case None => } + def openDocument(document: Document): Unit = { + if (document.frame == null) { + val f = PythonEditorTab(document) + showOrAdd(f) + } else + showOrAdd(document.frame) + } + + def saveAll(): Boolean = { + for (t <- getTabs.asScala) + t match { + case tab: Tab => + tab.getContent match { + case editorTab: EditorTab => + editorTab.autoSave() + case _ => + } + case _ => + } + true + } + def showOrAdd(frame: TabFrame): Unit = { for (t <- getTabs.asScala) t match { case tab: Tab if tab.getContent eq frame => getSelectionModel.select(tab) tab.getContent.setVisible(true) + return case _ => } addTab(frame) } - - /*def tabChanged(sender: TabFrame): Unit = { - } if (sender != null && sender.manager == this) { - for (t <- getTabs.asScala) - t match { - case tab: Tab if tab.getContent == sender => - if (Platform.isFxApplicationThread) - tab.setText(sender.caption) - else - Platform.runLater(() => { - tab.setText(sender.caption) - }) - case _ => - } - } - }*/ } diff --git a/src/main/scala/tigerjython/ui/TabFrame.scala b/src/main/scala/tigerjython/ui/TabFrame.scala index c5f294c..894da54 100644 --- a/src/main/scala/tigerjython/ui/TabFrame.scala +++ b/src/main/scala/tigerjython/ui/TabFrame.scala @@ -33,4 +33,6 @@ trait TabFrame extends Region { private[ui] def manager_=(m: TabManager): Unit = _manager = m + + def onClose(): Unit = {} } diff --git a/src/main/scala/tigerjython/ui/TabManager.scala b/src/main/scala/tigerjython/ui/TabManager.scala index 555a867..ba702c5 100644 --- a/src/main/scala/tigerjython/ui/TabManager.scala +++ b/src/main/scala/tigerjython/ui/TabManager.scala @@ -8,6 +8,7 @@ package tigerjython.ui import javafx.scene.Node +import tigerjython.files.Document /** * The `TabManager` is responsible for managing the different tabs in the main window. It has a default implementation @@ -24,6 +25,8 @@ trait TabManager { */ def addTab(frame: TabFrame): Option[TabFrame] + def saveAll(): Boolean + /** * * @param frame @@ -49,6 +52,12 @@ trait TabManager { */ def focusChanged(receivingNode: Node): Unit + /** + * + * @param document + */ + def openDocument(document: Document): Unit + /** * * @param frame diff --git a/src/main/scala/tigerjython/ui/TigerJythonApplication.scala b/src/main/scala/tigerjython/ui/TigerJythonApplication.scala index f868295..aed8959 100644 --- a/src/main/scala/tigerjython/ui/TigerJythonApplication.scala +++ b/src/main/scala/tigerjython/ui/TigerJythonApplication.scala @@ -14,6 +14,7 @@ import javafx.scene.control._ import javafx.scene.layout.BorderPane import javafx.stage.Stage import tigerjython.core.{BuildInfo, Preferences} +import tigerjython.files.Documents import tigerjython.plugins.{MainWindow, PluginsManager} /** @@ -25,18 +26,22 @@ object TigerJythonApplication { private var _mainStage: Stage = _ + private var _mainWindow: MainWindow = _ + private var _scene: Scene = _ + private var _tabManager: TabManager = _ + private var _zoomingPane: ZoomingPane = _ def mainScene: Scene = _scene def mainStage: Stage = _mainStage - private var _mainWindow: MainWindow = _ - def mainWindow: MainWindow = _mainWindow + def tabManager: TabManager = _tabManager + /** * Launches the IDE. * @@ -49,7 +54,7 @@ object TigerJythonApplication { class TigerJythonApplication extends Application { - import TigerJythonApplication.{_mainStage, _mainWindow, _scene, _zoomingPane} + import TigerJythonApplication.{_mainStage, _mainWindow, _scene, _tabManager, _zoomingPane} lazy val menuManager: MenuManager = new DefaultMenuManager(this) @@ -74,7 +79,6 @@ class TigerJythonApplication extends Application { root.setCenter(centre) _zoomingPane = centre centre.zoomFactorProperty.bind(Preferences.globalZoom) - tabManager.addTab(editor.PythonEditorTab()) val scene = new Scene(root) scene.getStylesheets.add("themes/%s.css".format(Preferences.theme.get())) @@ -93,12 +97,14 @@ class TigerJythonApplication extends Application { } }) primaryStage.setScene(scene) - primaryStage.setTitle(BuildInfo.Name) + primaryStage.setTitle(BuildInfo.Name + " " + BuildInfo.fullVersion) primaryStage.setOnCloseRequest(_ => handleCloseRequest()) primaryStage.show() _mainStage = primaryStage _scene = scene _mainWindow = new MainWindow(menuManager, tabManager) + _tabManager = tabManager + Documents.initialize() Platform.runLater(() => { PluginsManager.initialize() }) @@ -121,6 +127,7 @@ class TigerJythonApplication extends Application { } def handleCloseRequest(): Unit = { + tabManager.saveAll() stop() Platform.exit() sys.exit() diff --git a/src/main/scala/tigerjython/ui/UIString.scala b/src/main/scala/tigerjython/ui/UIString.scala index d03c428..6817996 100644 --- a/src/main/scala/tigerjython/ui/UIString.scala +++ b/src/main/scala/tigerjython/ui/UIString.scala @@ -9,7 +9,7 @@ package tigerjython.ui import javafx.application.Platform import javafx.beans.property.StringProperty -import tigerjython.config.{ConfigValue, Parser} +import tigerjython.configparser.{ConfigValue, Parser} /** * A `UIString` holds a string value that depends on the user's locale and language. It is loaded at runtime from an diff --git a/src/main/scala/tigerjython/ui/ZoomMixin.scala b/src/main/scala/tigerjython/ui/ZoomMixin.scala index b096dce..1d5b3ce 100644 --- a/src/main/scala/tigerjython/ui/ZoomMixin.scala +++ b/src/main/scala/tigerjython/ui/ZoomMixin.scala @@ -63,6 +63,8 @@ trait ZoomMixin { self: Node => } } + def getScaledFontSize: Double = Preferences.fontSize.get * zoomFactors(zoomIndex) + def getZoom: Double = zoomProperty.getValue def setZoom(factor: Double): Unit = diff --git a/src/main/scala/tigerjython/ui/editor/DefaultOutputPane.scala b/src/main/scala/tigerjython/ui/editor/DefaultOutputPane.scala index af2b085..1ce8bb7 100644 --- a/src/main/scala/tigerjython/ui/editor/DefaultOutputPane.scala +++ b/src/main/scala/tigerjython/ui/editor/DefaultOutputPane.scala @@ -67,6 +67,11 @@ class DefaultOutputPane(val editorTab: EditorTab, val captionID: String) extends def getContentText: String = textArea.getText() + def isEmpty: Boolean = { + val text = textArea.getText + text == null || text.length == 0 + } + /** * Set the `onKeyPress` handler to enable keyboard input. */ diff --git a/src/main/scala/tigerjython/ui/editor/EditorTab.scala b/src/main/scala/tigerjython/ui/editor/EditorTab.scala index 2b42601..f396e95 100644 --- a/src/main/scala/tigerjython/ui/editor/EditorTab.scala +++ b/src/main/scala/tigerjython/ui/editor/EditorTab.scala @@ -7,8 +7,6 @@ */ package tigerjython.ui.editor -import java.io.{FileWriter, PrintWriter} - import javafx.application.Platform import javafx.beans.value.{ChangeListener, ObservableValue} import javafx.geometry.Orientation @@ -20,7 +18,8 @@ import javafx.stage.Popup import org.fxmisc.flowless.VirtualizedScrollPane import org.fxmisc.richtext.CodeArea import tigerjython.errorhandling._ -import tigerjython.execute.{PythonCodeTranslator, PythonExecutor} +import tigerjython.execute.PythonExecutor +import tigerjython.files.{Document, Documents} import tigerjython.plugins.EventManager import tigerjython.ui.{TabFrame, TigerJythonApplication, ZoomMixin} @@ -44,13 +43,18 @@ abstract class EditorTab extends TabFrame { protected val splitPane: SplitPane = new SplitPane() protected val topToolBar: Node = createTopToolBar - protected var file: java.io.File = _ + protected var document: Document = _ protected var executor: PythonExecutor = _ - private var _execFile: java.io.File = _ private var _running: Boolean = false protected var errorLabel: Node = _ + protected def file: java.io.File = + if (document != null) + document.file + else + null + private object CaretChangeListener extends ChangeListener[Integer] { override def changed(observableValue: ObservableValue[_ <: Integer], t: Integer, t1: Integer): Unit = @@ -97,7 +101,7 @@ abstract class EditorTab extends TabFrame { result.getStyleClass.add("error-label") editor match { case zoomMixin: ZoomMixin => - result.setStyle("-fx-font-size: %g%%;".format(zoomMixin.getZoom * 100)) + result.setStyle("-fx-font-size: %g;".format(zoomMixin.getScaledFontSize)) case _ => } result @@ -153,6 +157,18 @@ abstract class EditorTab extends TabFrame { editor.caretPositionProperty().addListener(CaretChangeListener) editor.textProperty().addListener(TextChangeListener) } + if (errorPane.isEmpty) { + if (line >= 0) { + val pos = + if (column > 0) + "%s.%s".format(line, column) + else + line.toString + errorPane.append("[%s] %s".format(pos, msg)) + } else + errorPane.append(msg) + } + }) } @@ -173,33 +189,19 @@ abstract class EditorTab extends TabFrame { editor.getCaretPosition def getExecutableFile: java.io.File = - _execFile + if (document != null) + document.getExecutableFile + else + null def getFile: java.io.File = file - def getDefaultFileSuffix: String = ".py" - def getSelectedText: String = editor.getSelectedText def getText: String = editor.getText - def hasExecutableFile: Boolean = - if (file != null) { - _execFile = file - saveExecutable() - true - } else if (!isEmpty) { - if (_execFile == null) { - _execFile = java.io.File.createTempFile(caption.getValue + " (", ")" + getDefaultFileSuffix) - _execFile.deleteOnExit() - } - saveExecutable() - true - } else - false - def handleError(errorText: String): Unit = { infoPane.getSelectionModel.select(1) val (line, filename, msg) = PythonRuntimeErrors.generateMessage(errorText) @@ -222,12 +224,26 @@ abstract class EditorTab extends TabFrame { def isRunning: Boolean = _running - def loadFile(file: java.io.File): Unit = { - if (file.canWrite) - this.file = file - val source = scala.io.Source.fromFile(file) - editor.replaceText(source.getLines.mkString("\n")) - caption.setValue(file.getName) + def loadFile(file: java.io.File): Unit = + loadDocument(Documents(file)) + + def loadDocument(document: Document): Unit = + if (document != null) { + this.document = document + val text = document.load() + Platform.runLater(() => { + editor.replaceText(text) + editor.moveTo(document.caretPosition.get min text.length) + }) + document.open(this) + caption.setValue(document.name.get) + } + + override def onClose(): Unit = { + if (document != null) + document.close() + if (isRunning) + stop() } def run(): Unit = { @@ -250,7 +266,8 @@ abstract class EditorTab extends TabFrame { } protected def _run(): Unit = - if (hasExecutableFile) { + { + save() // Execute the code val executor = PythonExecutor(this) if (executor != null) @@ -260,37 +277,21 @@ abstract class EditorTab extends TabFrame { } def save(): Unit = - if (file != null) synchronized { - val writer = new FileWriter(file) - val printer = new PrintWriter(writer) - printer.print(editor.getText()) - printer.close() - } - - protected def saveExecutable(): Unit = { - val editorText = editor.getText() - val text = - PythonCodeTranslator.translate(editorText) match { - case Some(text) => - text - case None => - editorText - } - synchronized { - val writer = new FileWriter(_execFile) - val printer = new PrintWriter(writer) - printer.print(text) - printer.close() + if (document != null) + document.save(editor.getText, editor.getCaretPosition) + + def setDocument(document: Document): Unit = + if (document != null) { + if (this.document != null) + this.document.close() + this.document = document + document.open(this) + caption.setValue(document.name.get) + document.save(editor.getText, editor.getCaretPosition) } - } - def setFile(file: java.io.File): Unit = { - this.file = file - if (file != null) { - caption.setValue(file.getName) - save() - } - } + def setFile(file: java.io.File): Unit = + setDocument(Documents(file)) def setSelectedText(s: String): Unit = editor.replaceSelection(s) diff --git a/src/main/scala/tigerjython/ui/editor/OutputPane.scala b/src/main/scala/tigerjython/ui/editor/OutputPane.scala index c58d72d..35fd34f 100644 --- a/src/main/scala/tigerjython/ui/editor/OutputPane.scala +++ b/src/main/scala/tigerjython/ui/editor/OutputPane.scala @@ -25,4 +25,6 @@ trait OutputPane extends Tab { def clear(): Unit def getContentText: String + + def isEmpty: Boolean } diff --git a/src/main/scala/tigerjython/ui/editor/PythonEditor.scala b/src/main/scala/tigerjython/ui/editor/PythonEditor.scala index 2c3290a..c3fa292 100644 --- a/src/main/scala/tigerjython/ui/editor/PythonEditor.scala +++ b/src/main/scala/tigerjython/ui/editor/PythonEditor.scala @@ -157,7 +157,11 @@ class PythonEditor extends CodeArea with ZoomMixin { } private def computeHighlighting(text: String): StyleSpans[java.util.Collection[String]] = { - val matcher = PythonEditor.PATTERN.matcher(text) + val matcher = + if (Preferences.repeatLoop.get) + PythonEditor.PATTERN.matcher(text) + else + PythonEditor.PATTERN_PLAIN.matcher(text) val spansBuilder = new StyleSpansBuilder[java.util.Collection[String]]() var lastKwEnd = 0 while (matcher.find()) { @@ -186,7 +190,8 @@ object PythonEditor { ) private val COMMENT_PATTERN = "#[^\\n]*" - private val KEYWORD_PATTERN = "\\b(" + KEYWORDS.mkString("|") + ")\\b" + private val KEYWORD_PATTERN = "\\b(" + KEYWORDS.mkString("|") + "|repeat)\\b" + private val KEYWORD_PATTERN_PLAIN = "\\b(" + KEYWORDS.mkString("|") + ")\\b" private val STRING_PATTERN = "\\\"([^\\\"\\\\\\\\]|\\\\\\\\.)*\\\"" private lazy val PATTERN = Pattern.compile( @@ -194,4 +199,10 @@ object PythonEditor { "|(?" + STRING_PATTERN + ")" + "|(?" + COMMENT_PATTERN + ")" ) + + private lazy val PATTERN_PLAIN = Pattern.compile( + "(?" + KEYWORD_PATTERN_PLAIN + ")" + + "|(?" + STRING_PATTERN + ")" + + "|(?" + COMMENT_PATTERN + ")" + ) } \ No newline at end of file diff --git a/src/main/scala/tigerjython/ui/editor/PythonEditorTab.scala b/src/main/scala/tigerjython/ui/editor/PythonEditorTab.scala index 7ce85a5..6fcdf47 100644 --- a/src/main/scala/tigerjython/ui/editor/PythonEditorTab.scala +++ b/src/main/scala/tigerjython/ui/editor/PythonEditorTab.scala @@ -8,6 +8,7 @@ package tigerjython.ui.editor import org.fxmisc.richtext._ +import tigerjython.files.Document /** * This is a specialisation of the more general `EditorTab` to use the Python-editor. @@ -37,4 +38,10 @@ object PythonEditorTab { } def apply(): PythonEditorTab = new PythonEditorTab() + + def apply(document: Document): PythonEditorTab = { + val result = new PythonEditorTab() + result.loadDocument(document) + result + } } diff --git a/src/main/scala/tigerjython/ui/tabs/DocumentItem.scala b/src/main/scala/tigerjython/ui/tabs/DocumentItem.scala new file mode 100644 index 0000000..b490608 --- /dev/null +++ b/src/main/scala/tigerjython/ui/tabs/DocumentItem.scala @@ -0,0 +1,60 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.ui.tabs + +import javafx.scene.{Group, Node} +import javafx.scene.control.Label +import javafx.scene.input.MouseEvent +import javafx.scene.layout.{BorderPane, VBox} +import javafx.scene.paint.Color +import javafx.scene.shape.Rectangle + +/** + * @author Tobias Kohn + */ +abstract class DocumentItem extends BorderPane { + + protected val icon: Node = createIcon() + protected val titleLabel = new Label("Hello World!") + protected val descriptionLabel = new Label() + + { + this.getStyleClass.add("document-item") + titleLabel.getStyleClass.add("title") + descriptionLabel.getStyleClass.add("description") + if (icon != null) + icon.getStyleClass.add("icon") + val contents = new VBox() + contents.getChildren.addAll( + titleLabel, descriptionLabel + ) + setCenter(contents) + if (icon != null) + setLeft(icon) + + setMinHeight(60) + } + + addEventFilter(MouseEvent.MOUSE_CLICKED, (event: MouseEvent) => { + onClicked() + }) + + protected def createIcon(): Node = { + val result = new Rectangle(42, 59) + result.setStroke(Color.BLACK) + result.getStyleClass.add("paper") + val outline = new Rectangle(60, 59) + outline.setFill(Color.TRANSPARENT) + val g = new Group() + g.getChildren.add(outline) + g.getChildren.add(result) + g + } + + def onClicked(): Unit +} diff --git a/src/main/scala/tigerjython/ui/tabs/NewDocumentItem.scala b/src/main/scala/tigerjython/ui/tabs/NewDocumentItem.scala new file mode 100644 index 0000000..908eaad --- /dev/null +++ b/src/main/scala/tigerjython/ui/tabs/NewDocumentItem.scala @@ -0,0 +1,24 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.ui.tabs + +import tigerjython.ui.{TigerJythonApplication, editor} + +/** + * @author Tobias Kohn + */ +class NewDocumentItem extends DocumentItem { + + { + titleLabel.setText("New Document") + descriptionLabel.setText("Create a new empty document") + } + + def onClicked(): Unit = + TigerJythonApplication.tabManager.addTab(editor.PythonEditorTab()) +} diff --git a/src/main/scala/tigerjython/ui/tabs/OpenDocumentItem.scala b/src/main/scala/tigerjython/ui/tabs/OpenDocumentItem.scala new file mode 100644 index 0000000..d0fcbe2 --- /dev/null +++ b/src/main/scala/tigerjython/ui/tabs/OpenDocumentItem.scala @@ -0,0 +1,45 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.ui.tabs + +import javafx.scene.{Group, Node} +import javafx.scene.paint.Color +import javafx.scene.shape.{Line, Rectangle} +import tigerjython.files.Document + +/** + * @author Tobias Kohn + */ +class OpenDocumentItem(val document: Document) extends DocumentItem { + + { + titleLabel.textProperty().bind(document.name) + descriptionLabel.textProperty().bind(document.description) + } + + override protected def createIcon(): Node = { + val result = new Rectangle(42, 59) + result.setStroke(Color.BLACK) + result.getStyleClass.add("paper") + val outline = new Rectangle(60, 59) + outline.setFill(Color.TRANSPARENT) + val g = new Group() + g.getChildren.addAll( + outline, result + ) + for (i <- 0 to 13) { + val l = new Line(10, 10 + 3 * i, 32, 10 + 3 * i) + l.setStroke(Color.GRAY) + g.getChildren.add(l) + } + g + } + + def onClicked(): Unit = + document.show() +} \ No newline at end of file diff --git a/src/main/scala/tigerjython/ui/tabs/OpenDocumentTab.scala b/src/main/scala/tigerjython/ui/tabs/OpenDocumentTab.scala new file mode 100644 index 0000000..e11b05c --- /dev/null +++ b/src/main/scala/tigerjython/ui/tabs/OpenDocumentTab.scala @@ -0,0 +1,39 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.ui.tabs + +import javafx.scene.layout.VBox +import tigerjython.files.Documents +import tigerjython.ui.TabFrame + +/** + * @author Tobias Kohn + */ +class OpenDocumentTab protected () extends TabFrame { + + caption.setValue("+") + + override def focusChanged(receiveFocus: Boolean): Unit = + if (receiveFocus) + update() + + def update(): Unit = { + getChildren.clear() + val box = new VBox() + box.getChildren.add(new NewDocumentItem()) + for (doc <- Documents.getListOfDocuments) + box.getChildren.add(new OpenDocumentItem(doc)) + getChildren.add(box) + } +} +object OpenDocumentTab { + + private lazy val _openDocTab: OpenDocumentTab = new OpenDocumentTab() + + def apply(): OpenDocumentTab = _openDocTab +} diff --git a/src/main/scala/tigerjython/utils/PrefBooleanProperty.scala b/src/main/scala/tigerjython/utils/PrefBooleanProperty.scala new file mode 100644 index 0000000..6c60adb --- /dev/null +++ b/src/main/scala/tigerjython/utils/PrefBooleanProperty.scala @@ -0,0 +1,26 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.utils + +import java.lang +import java.util.prefs.{Preferences => JPreferences} +import javafx.beans.property._ +import javafx.beans.value.{ChangeListener, ObservableValue} + +/** + * @author Tobias Kohn + */ +class PrefBooleanProperty(protected val preferences: JPreferences, val name: String, default: Boolean = false) extends + SimpleBooleanProperty(preferences.getBoolean(name, default)) { + + addListener(new ChangeListener[lang.Boolean] { + override def changed(observableValue: ObservableValue[_ <: lang.Boolean], oldValue: lang.Boolean, + newValue: lang.Boolean): Unit = + preferences.putBoolean(name, newValue) + }) +} diff --git a/src/main/scala/tigerjython/utils/PrefDoubleProperty.scala b/src/main/scala/tigerjython/utils/PrefDoubleProperty.scala new file mode 100644 index 0000000..060dbcb --- /dev/null +++ b/src/main/scala/tigerjython/utils/PrefDoubleProperty.scala @@ -0,0 +1,24 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.utils + +import java.util.prefs.{Preferences => JPreferences} +import javafx.beans.property._ +import javafx.beans.value.{ChangeListener, ObservableValue} + +/** + * @author Tobias Kohn + */ +class PrefDoubleProperty(protected val preferences: JPreferences, val name: String, default: Double = 0.0) extends + SimpleDoubleProperty(preferences.getDouble(name, default)) { + + addListener(new ChangeListener[Number] { + override def changed(observableValue: ObservableValue[_ <: Number], oldValue: Number, newValue: Number): Unit = + preferences.putDouble(name, newValue.doubleValue()) + }) +} diff --git a/src/main/scala/tigerjython/utils/PrefIntegerProperty.scala b/src/main/scala/tigerjython/utils/PrefIntegerProperty.scala new file mode 100644 index 0000000..ca9c430 --- /dev/null +++ b/src/main/scala/tigerjython/utils/PrefIntegerProperty.scala @@ -0,0 +1,24 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.utils + +import java.util.prefs.{Preferences => JPreferences} +import javafx.beans.property._ +import javafx.beans.value.{ChangeListener, ObservableValue} + +/** + * @author Tobias Kohn + */ +class PrefIntegerProperty(protected val preferences: JPreferences, val name: String, default: Int = 0) extends + SimpleIntegerProperty(preferences.getInt(name, default)) { + + addListener(new ChangeListener[Number] { + override def changed(observableValue: ObservableValue[_ <: Number], oldValue: Number, newValue: Number): Unit = + preferences.putInt(name, newValue.intValue()) + }) +} \ No newline at end of file diff --git a/src/main/scala/tigerjython/utils/PrefStringProperty.scala b/src/main/scala/tigerjython/utils/PrefStringProperty.scala new file mode 100644 index 0000000..e746f10 --- /dev/null +++ b/src/main/scala/tigerjython/utils/PrefStringProperty.scala @@ -0,0 +1,24 @@ +/* + * This file is part of the 'TigerJython' project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package tigerjython.utils + +import java.util.prefs.{Preferences => JPreferences} +import javafx.beans.property._ +import javafx.beans.value.{ChangeListener, ObservableValue} + +/** + * @author Tobias Kohn + */ +class PrefStringProperty(protected val preferences: JPreferences, val name: String, default: String = null) extends + SimpleStringProperty(preferences.get(name, default)) { + + addListener(new ChangeListener[String] { + override def changed(observableValue: ObservableValue[_ <: String], oldValue: String, newValue: String): Unit = + preferences.put(name, newValue) + }) +}