| 2 | 2 | *.bin |
| 3 | 3 | *.exe |
| 4 | /*.jar | |
| 4 | 5 | build |
| 5 | 6 | .gradle |
| 6 | 7 | contacted.csv |
| 7 | 8 | video |
| 8 | 9 | .settings |
| 9 | 10 | .classpath |
| 11 | .idea | |
| 10 | 12 |
| 1 | workspace.xml | |
| 2 | 1 |
| 1 | <component name="ProjectCodeStyleConfiguration"> | |
| 2 | <state> | |
| 3 | <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> | |
| 4 | </state> | |
| 5 | </component> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="CompilerConfiguration"> | |
| 4 | <bytecodeTargetLevel target="14" /> | |
| 5 | </component> | |
| 6 | </project> | |
| 1 |
| 1 | <component name="ProjectDictionaryState"> | |
| 2 | <dictionary name="jarvisd"> | |
| 3 | <words> | |
| 4 | <w>blockquotes</w> | |
| 5 | <w>sigil</w> | |
| 6 | <w>transcoded</w> | |
| 7 | </words> | |
| 8 | </dictionary> | |
| 9 | </component> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="GradleMigrationSettings" migrationVersion="1" /> | |
| 4 | <component name="GradleSettings"> | |
| 5 | <option name="linkedExternalProjectsSettings"> | |
| 6 | <GradleProjectSettings> | |
| 7 | <option name="distributionType" value="LOCAL" /> | |
| 8 | <option name="externalProjectPath" value="$PROJECT_DIR$" /> | |
| 9 | <option name="gradleHome" value="/opt/gradle" /> | |
| 10 | <option name="gradleJvm" value="#JAVA_HOME" /> | |
| 11 | <option name="modules"> | |
| 12 | <set> | |
| 13 | <option value="$PROJECT_DIR$" /> | |
| 14 | </set> | |
| 15 | </option> | |
| 16 | </GradleProjectSettings> | |
| 17 | </option> | |
| 18 | </component> | |
| 19 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="RemoteRepositoriesConfiguration"> | |
| 4 | <remote-repository> | |
| 5 | <option name="id" value="central" /> | |
| 6 | <option name="name" value="Maven Central repository" /> | |
| 7 | <option name="url" value="https://repo1.maven.org/maven2" /> | |
| 8 | </remote-repository> | |
| 9 | <remote-repository> | |
| 10 | <option name="id" value="jboss.community" /> | |
| 11 | <option name="name" value="JBoss Community repository" /> | |
| 12 | <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> | |
| 13 | </remote-repository> | |
| 14 | <remote-repository> | |
| 15 | <option name="id" value="MavenRepo" /> | |
| 16 | <option name="name" value="MavenRepo" /> | |
| 17 | <option name="url" value="https://repo.maven.apache.org/maven2/" /> | |
| 18 | </remote-repository> | |
| 19 | <remote-repository> | |
| 20 | <option name="id" value="maven" /> | |
| 21 | <option name="name" value="maven" /> | |
| 22 | <option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" /> | |
| 23 | </remote-repository> | |
| 24 | <remote-repository> | |
| 25 | <option name="id" value="BintrayJCenter" /> | |
| 26 | <option name="name" value="BintrayJCenter" /> | |
| 27 | <option name="url" value="https://jcenter.bintray.com/" /> | |
| 28 | </remote-repository> | |
| 29 | <remote-repository> | |
| 30 | <option name="id" value="maven2" /> | |
| 31 | <option name="name" value="maven2" /> | |
| 32 | <option name="url" value="https://nexus.bedatadriven.com/content/groups/public" /> | |
| 33 | </remote-repository> | |
| 34 | </component> | |
| 35 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="ExternalStorageConfigurationManager" enabled="true" /> | |
| 4 | <component name="ProjectRootManager" version="2" languageLevel="JDK_14" default="true" project-jdk-name="14" project-jdk-type="JavaSDK"> | |
| 5 | <output url="file://$PROJECT_DIR$/build" /> | |
| 6 | </component> | |
| 7 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="RGraphicsSettings"> | |
| 4 | <option name="height" value="1600" /> | |
| 5 | <option name="resolution" value="112" /> | |
| 6 | <option name="version" value="1" /> | |
| 7 | <option name="width" value="2560" /> | |
| 8 | </component> | |
| 9 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="RSettings"> | |
| 4 | <option name="interpreterPath" value="/usr/bin/R" /> | |
| 5 | </component> | |
| 6 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="RPackageService"> | |
| 4 | <option name="enabledRepositoryUrls"> | |
| 5 | <list> | |
| 6 | <option value="@CRAN@" /> | |
| 7 | </list> | |
| 8 | </option> | |
| 9 | </component> | |
| 10 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <module type="JAVA_MODULE" version="4" /> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="Palette2"> | |
| 4 | <group name="Swing"> | |
| 5 | <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 6 | <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> | |
| 7 | </item> | |
| 8 | <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 9 | <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> | |
| 10 | </item> | |
| 11 | <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 12 | <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> | |
| 13 | </item> | |
| 14 | <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true"> | |
| 15 | <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> | |
| 16 | </item> | |
| 17 | <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 18 | <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> | |
| 19 | <initial-values> | |
| 20 | <property name="text" value="Button" /> | |
| 21 | </initial-values> | |
| 22 | </item> | |
| 23 | <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 24 | <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> | |
| 25 | <initial-values> | |
| 26 | <property name="text" value="RadioButton" /> | |
| 27 | </initial-values> | |
| 28 | </item> | |
| 29 | <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 30 | <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> | |
| 31 | <initial-values> | |
| 32 | <property name="text" value="CheckBox" /> | |
| 33 | </initial-values> | |
| 34 | </item> | |
| 35 | <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 36 | <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> | |
| 37 | <initial-values> | |
| 38 | <property name="text" value="Label" /> | |
| 39 | </initial-values> | |
| 40 | </item> | |
| 41 | <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 42 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
| 43 | <preferred-size width="150" height="-1" /> | |
| 44 | </default-constraints> | |
| 45 | </item> | |
| 46 | <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 47 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
| 48 | <preferred-size width="150" height="-1" /> | |
| 49 | </default-constraints> | |
| 50 | </item> | |
| 51 | <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 52 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
| 53 | <preferred-size width="150" height="-1" /> | |
| 54 | </default-constraints> | |
| 55 | </item> | |
| 56 | <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 57 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
| 58 | <preferred-size width="150" height="50" /> | |
| 59 | </default-constraints> | |
| 60 | </item> | |
| 61 | <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 62 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
| 63 | <preferred-size width="150" height="50" /> | |
| 64 | </default-constraints> | |
| 65 | </item> | |
| 66 | <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 67 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
| 68 | <preferred-size width="150" height="50" /> | |
| 69 | </default-constraints> | |
| 70 | </item> | |
| 71 | <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 72 | <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> | |
| 73 | </item> | |
| 74 | <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 75 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
| 76 | <preferred-size width="150" height="50" /> | |
| 77 | </default-constraints> | |
| 78 | </item> | |
| 79 | <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 80 | <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> | |
| 81 | <preferred-size width="150" height="50" /> | |
| 82 | </default-constraints> | |
| 83 | </item> | |
| 84 | <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 85 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
| 86 | <preferred-size width="150" height="50" /> | |
| 87 | </default-constraints> | |
| 88 | </item> | |
| 89 | <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 90 | <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> | |
| 91 | <preferred-size width="200" height="200" /> | |
| 92 | </default-constraints> | |
| 93 | </item> | |
| 94 | <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 95 | <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> | |
| 96 | <preferred-size width="200" height="200" /> | |
| 97 | </default-constraints> | |
| 98 | </item> | |
| 99 | <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
| 100 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> | |
| 101 | </item> | |
| 102 | <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 103 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> | |
| 104 | </item> | |
| 105 | <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 106 | <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> | |
| 107 | </item> | |
| 108 | <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 109 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> | |
| 110 | </item> | |
| 111 | <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 112 | <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> | |
| 113 | <preferred-size width="-1" height="20" /> | |
| 114 | </default-constraints> | |
| 115 | </item> | |
| 116 | <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
| 117 | <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> | |
| 118 | </item> | |
| 119 | <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
| 120 | <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> | |
| 121 | </item> | |
| 122 | </group> | |
| 123 | </component> | |
| 124 | </project> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <project version="4"> | |
| 3 | <component name="VcsDirectoryMappings"> | |
| 4 | <mapping directory="$PROJECT_DIR$" vcs="Git" /> | |
| 5 | </component> | |
| 6 | </project> | |
| 1 |
| 1 | language: java | |
| 2 | ||
| 3 | jdk: | |
| 4 | - oraclejdk8 | |
| 5 | ||
| 6 | # enable Java 8u45+, see https://github.com/travis-ci/travis-ci/issues/4042 | |
| 7 | addons: | |
| 8 | apt: | |
| 9 | packages: | |
| 10 | - oracle-java8-installer | |
| 11 | os: | |
| 12 | - linux | |
| 13 | ||
| 14 | # run in container | |
| 15 | sudo: false | |
| 16 | 1 |
| 17 | 17 | ### Windows |
| 18 | 18 | |
| 19 | On Windows, double-click the application to start. You will have to give the application permission to run. | |
| 19 | Double-click the application to start; give the application permission to run. | |
| 20 | 20 | |
| 21 | When upgrading to a new version, delete the following directory; | |
| 21 | When upgrading to a new version, delete the following directory: | |
| 22 | 22 | |
| 23 | 23 | C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe |
| 24 | 24 | |
| 25 | 25 | ### Linux |
| 26 | 26 | |
| 27 | On Linux, run `chmod +x keenwrite.bin` then `./keenwrite.bin`. | |
| 27 | Execute the following commands in a terminal: | |
| 28 | ||
| 29 | ``` bash | |
| 30 | chmod +x keenwrite.bin | |
| 31 | ./keenwrite.bin | |
| 32 | ``` | |
| 28 | 33 | |
| 29 | 34 | ### Other |
| 30 | 35 | |
| 31 | On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run: | |
| 36 | Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run: | |
| 32 | 37 | |
| 33 | 38 | ``` bash |
| 34 | 39 | java -jar keenwrite.jar |
| 35 | 40 | ``` |
| 36 | 41 | |
| 37 | 42 | ## Features |
| 43 | ||
| 44 | The application offers: | |
| 38 | 45 | |
| 39 | 46 | * User-defined interpolated strings |
| 40 | * Real-time preview with variable substitution | |
| 41 | 47 | * Auto-complete variable names based on variable values |
| 42 | * XML document transformation using XSLT3 or older | |
| 43 | * Platform independent (Windows, Linux, MacOS) | |
| 44 | * Spellcheck while typing | |
| 45 | * Write mathematical formulas using a subset of TeX | |
| 48 | * Real-time spell check | |
| 49 | * Real-time rendering of math using TeX notation | |
| 50 | * Diagrams: Mermaid, GraphViz, UML, sequence, timing, DITAA, and more! | |
| 46 | 51 | * R integration |
| 52 | * XML transformation using XSLT3 or older | |
| 53 | * Customizable GUI having detachable tabs | |
| 54 | * Platform independent (Windows, Linux, MacOS) | |
| 47 | 55 | |
| 48 | 56 | ## Usage |
| ... | ||
| 58 | 66 | |
| 59 | 67 | This software is licensed under the [BSD 2-Clause License](LICENSE.md) and |
| 60 | derived heavily from [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md). | |
| 68 | based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md). | |
| 61 | 69 | |
| 62 | 70 | |
| 1 | #  | |
| 2 | ||
| 3 | 智能写入是一个文本编辑器,它使用插值字符串引用外部定义的值。 | |
| 4 | ||
| 5 | ## 下载 | |
| 6 | ||
| 7 | 下载以下版本之一: | |
| 8 | ||
| 9 | * [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe) | |
| 10 | * [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin) | |
| 11 | * [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar) | |
| 12 | ||
| 13 | ## 跑 | |
| 14 | ||
| 15 | 在第一次运行期间,应用程序将自身解压到本地目录中。随后的启动会更快。 | |
| 16 | ||
| 17 | ### Windows | |
| 18 | ||
| 19 | 双击应用程序以启动。您必须授予应用程序运行权限。 | |
| 20 | ||
| 21 | 升级时,删除以下目录: | |
| 22 | ||
| 23 | C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe | |
| 24 | ||
| 25 | ### Linux | |
| 26 | ||
| 27 | 执行以下命令: | |
| 28 | ||
| 29 | ``` bash | |
| 30 | chmod +x keenwrite.bin | |
| 31 | ./keenwrite.bin | |
| 32 | ``` | |
| 33 | ||
| 34 | ### Other | |
| 35 | ||
| 36 | Download and install a full version of [OpenJDK 15](https://bell-sw.com/pages/downloads/?version=java-15#mn) that includes JavaFX module support, then run: | |
| 37 | ||
| 38 | ``` bash | |
| 39 | java -jar keenwrite.jar | |
| 40 | ``` | |
| 41 | ||
| 42 | ## 特征 | |
| 43 | ||
| 44 | * 用户定义的插值字符串 | |
| 45 | * 带变量替换的实时预览 | |
| 46 | * 基于变量值自动完成变量名 | |
| 47 | * 使用XSLT3或更早版本的XML文档转换 | |
| 48 | * 独立于操作系统 | |
| 49 | * 打字时拼写检查 | |
| 50 | * 使用TeX的子集编写数学公式 | |
| 51 | * 嵌入R语句 | |
| 52 | ||
| 53 | ## 软件使用 | |
| 54 | ||
| 55 | See the [detailed documentation](docs/README.md) for information about | |
| 56 | using the application. | |
| 57 | ||
| 58 | ## 截图 | |
| 59 | ||
| 60 |  | |
| 61 | ||
| 62 | ## 软件许可证 | |
| 63 | ||
| 64 | This software is licensed under the [BSD 2-Clause License](LICENSE.md) and | |
| 65 | based on [Markdown-Writer-FX](licenses/MARKDOWN-WRITER-FX.md). | |
| 66 | ||
| 1 | 67 |
| 30 | 30 | |
| 31 | 31 | javafx { |
| 32 | version = "14" | |
| 32 | version = "15" | |
| 33 | 33 | modules = ['javafx.controls', 'javafx.swing'] |
| 34 | 34 | configuration = 'compileOnly' |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | dependencies { |
| 38 | def v_junit = '5.4.2' | |
| 38 | def v_junit = '5.5.1' | |
| 39 | 39 | def v_flexmark = '0.62.2' |
| 40 | def v_jackson = '2.11.2' | |
| 40 | def v_jackson = '2.12.0' | |
| 41 | 41 | def v_batik = '1.13' |
| 42 | 42 | |
| 43 | 43 | // JavaFX |
| 44 | implementation 'org.reactfx:reactfx:1.4.1' | |
| 45 | implementation 'org.controlsfx:controlsfx:11.0.2' | |
| 44 | implementation 'org.controlsfx:controlsfx:11.0.3' | |
| 46 | 45 | implementation 'org.fxmisc.richtext:richtextfx:0.10.5' |
| 47 | 46 | implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' |
| 48 | 47 | implementation 'com.miglayout:miglayout-javafx:5.2' |
| 49 | implementation('com.dlsc.preferencesfx:preferencesfx-core:11.6.0') { | |
| 48 | implementation('com.dlsc.preferencesfx:preferencesfx-core:11.7.0') { | |
| 50 | 49 | exclude group: 'org.openjfx' |
| 51 | 50 | } |
| ... | ||
| 70 | 69 | implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}" |
| 71 | 70 | implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}" |
| 72 | implementation 'org.yaml:snakeyaml:1.26' | |
| 71 | implementation 'org.yaml:snakeyaml:1.27' | |
| 73 | 72 | |
| 74 | 73 | // XML and XSL |
| ... | ||
| 99 | 98 | implementation "org.apache.xmlgraphics:batik-util:${v_batik}" |
| 100 | 99 | implementation "org.apache.xmlgraphics:batik-xml:${v_batik}" |
| 101 | ||
| 102 | // Spelling, TeX | |
| 103 | implementation fileTree(include: ['**/*.jar'], dir: 'libs') | |
| 104 | 100 | |
| 105 | 101 | // Misc. |
| 106 | 102 | implementation 'org.ahocorasick:ahocorasick:0.4.0' |
| 107 | 103 | implementation 'org.apache.commons:commons-configuration2:2.7' |
| 108 | 104 | implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3' |
| 109 | 105 | implementation 'javax.validation:validation-api:2.0.1.Final' |
| 106 | ||
| 107 | // Configuration | |
| 108 | implementation 'org.apache.commons:commons-configuration2:2.7' | |
| 109 | implementation 'commons-beanutils:commons-beanutils:1.9.4' | |
| 110 | ||
| 111 | // Spelling, TeX, Docking | |
| 112 | implementation fileTree(include: ['**/*.jar'], dir: 'libs') | |
| 110 | 113 | |
| 111 | 114 | def fx = ['controls', 'graphics', 'fxml', 'swing'] |
| ... | ||
| 119 | 122 | } |
| 120 | 123 | |
| 124 | testImplementation "org.junit.jupiter:junit-jupiter-engine:${v_junit}" | |
| 121 | 125 | testImplementation "org.junit.jupiter:junit-jupiter-api:${v_junit}" |
| 122 | testRuntime "org.junit.jupiter:junit-jupiter-engine:${v_junit}" | |
| 126 | testImplementation "org.testfx:testfx-junit5:4.0.16-alpha" | |
| 123 | 127 | } |
| 124 | 128 | |
| 1 | *Song of the Yellow Bird*: | |
| 2 | ||
| 3 | 翩翩黃鳥, | |
| 4 | 雌雄相依。 | |
| 5 | 念我之獨, | |
| 6 | 誰其與歸? | |
| 7 | ||
| 8 | English translation: | |
| 9 | ||
| 10 | Orioles fly smoothly | |
| 11 | Female and male cuddle close together | |
| 12 | Thinking of my loneliness | |
| 13 | Whom shall I go with? | |
| 14 | ||
| 15 | Fonts: | |
| 16 | ||
| 17 | * Regular: 활판 인쇄술 | |
| 18 | * Bold: **활판 인쇄술** | |
| 19 | * Monospace: `활판 인쇄술` | |
| 20 | * Monospace bold: **`활판 인쇄술`** | |
| 21 | * Math: $E=mc^2$ | |
| 22 | ||
| 1 | 23 |
| 1 | ||
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no" ?> | |
| 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |
| 3 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve"> | |
| 4 | <desc>Created with Fabric.js 3.6.3</desc> | |
| 5 | <defs> | |
| 6 | </defs> | |
| 7 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo" > | |
| 8 | <rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" paint-order="stroke" x="-325" y="-260" rx="0" ry="0" width="650" height="520" /> | |
| 9 | </g> | |
| 10 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo" > | |
| 11 | <g style="" paint-order="stroke" > | |
| 12 | <g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)" > | |
| 13 | <linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)" x1="0" y1="0" x2="1" y2="0"> | |
| 14 | <stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/> | |
| 15 | <stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/> | |
| 16 | </linearGradient> | |
| 17 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" /> | |
| 18 | </g> | |
| 19 | <g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)" > | |
| 20 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" /> | |
| 21 | </g> | |
| 22 | <g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)" > | |
| 23 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" /> | |
| 24 | </g> | |
| 25 | <g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)" > | |
| 26 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" /> | |
| 27 | </g> | |
| 28 | <g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)" > | |
| 29 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" /> | |
| 30 | </g> | |
| 31 | <g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)" > | |
| 32 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" /> | |
| 33 | </g> | |
| 34 | <g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)" > | |
| 35 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" /> | |
| 36 | </g> | |
| 37 | <g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)" > | |
| 38 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" /> | |
| 39 | </g> | |
| 40 | </g> | |
| 41 | </g> | |
| 42 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path" > | |
| 43 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" /> | |
| 44 | </g> | |
| 45 | </svg> |
| 1 | ||
| 1 | <svg height="197.4767" viewBox="0 0 695.99768 197.4767" width="695.99768" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(-8.7796153 42.985832 -42.985832 -8.7796153 810.33577 828.59028)" gradientUnits="userSpaceOnUse" x1=".152358" x2=".968809" y1="-.044912" y2="-.049471"><stop offset="0" stop-color="#ec706a"/><stop offset="1" stop-color="#ecd980"/></linearGradient><g transform="translate(-295.50101 -692.52836)"><path d="m793.12811 845.45734c-1.09438 20.55837 6.93804 24.54772 6.93804 24.54772s.98325-13.16026 6.76656-11.6325c4.96369 1.30552 2.67983 10.4134 2.67983 10.4134s26.21535-34.03672 1.372-40.63137-5.51534-1.89773-10.40994.92679c-3.58074 2.06734-6.82887 6.66097-7.34649 16.37596" fill="url(#a)"/><path d="m826.30436 831.16428-10.99206-16.95952 1.75995-6.49966 10.01483 2.71233z" fill="#126d95"/><path d="m828.56081 804.89512-.91739 3.38458-9.99361-2.70665.91739-3.38458z" fill="#126d95"/><g fill="#51a9cf"><path d="m834.01973 741.0381c-1.68105.0185-3.22054 1.13771-3.68367 2.84981-.56186 2.07405.665 4.21099 2.73743 4.77241l-13.96475 51.52944-9.99361-2.70665c8.36013-31.46487 4.99411-51.98144 4.99411-51.98144 14.99782-11.92097 23.67-25.56577 27.63101-32.97331z"/><path d="m818.56767 802.18881-.9174 3.38458-10.03996-2.72957.91314-3.37522z"/><path d="m817.07405 807.70594-1.75995 6.49966-18.03534 9.08805 9.78412-18.31044z"/></g><path d="m836.1981 741.64919 7.72577-28.52932c-.3195 8.40427.28451 24.55036 7.21678 42.41047 0 0-11.89603 16.50235-21.99788 47.3763l-10.03442-2.71758 13.96533-51.5284c2.08221.56405 4.21039-.66603 4.77182-2.73844.45427-1.67248-.26571-3.38317-1.64739-4.27302" fill="#126d95"/></g><text transform="translate(-295.73751 -689.6407)"/><g style="font-style:italic;font-weight:800;font-size:133.333;font-family:Merriweather Sans;letter-spacing:0;word-spacing:0;fill:#51a9cf"><text x="16.133343" y="130.6234"><tspan x="16.133343" y="130.6234">KeenWr</tspan></text><text x="552.53137" y="130.6234"><tspan x="552.53137" y="130.6234">te</tspan></text></g></svg> |
| 1 | ||
| 1 | <svg height="197.4767" viewBox="0 0 493.25561 197.4767" width="493.25562" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(-8.7796153 42.985832 -42.985832 -8.7796153 810.33577 828.59028)" gradientUnits="userSpaceOnUse" x1=".152358" x2=".968809" y1="-.044912" y2="-.049471"><stop offset="0" stop-color="#ec706a"/><stop offset="1" stop-color="#ecd980"/></linearGradient><text transform="translate(-312.52749 -472.07353)"/><text fill="#51a9cf" font-family="'Noto Serif CJK SC'" font-size="35.1025" letter-spacing="0" transform="matrix(3.7983969 0 0 3.7983969 -330.7653 961.00598)" word-spacing="0"><tspan x="91.011719" y="-209.05206"><tspan x="91.011719" y="-209.05206">智能写<tspan fill="#51a9cf"/></tspan></tspan><tspan x="91.011719" y="-165.17393"/></text><g transform="translate(-377.88503 -692.52836)"><path d="m793.12811 845.45734c-1.09438 20.55837 6.93804 24.54772 6.93804 24.54772s.98325-13.16026 6.76656-11.6325c4.96369 1.30552 2.67983 10.4134 2.67983 10.4134s26.21535-34.03672 1.372-40.63137-5.51534-1.89773-10.40994.92679c-3.58074 2.06734-6.82887 6.66097-7.34649 16.37596" fill="url(#a)"/><path d="m826.30436 831.16428-10.99206-16.95952 1.75995-6.49966 10.01483 2.71233z" fill="#126d95"/><path d="m828.56081 804.89512-.91739 3.38458-9.99361-2.70665.91739-3.38458z" fill="#126d95"/><g fill="#51a9cf"><path d="m834.01973 741.0381c-1.68105.0185-3.22054 1.13771-3.68367 2.84981-.56186 2.07405.665 4.21099 2.73743 4.77241l-13.96475 51.52944-9.99361-2.70665c8.36013-31.46487 4.99411-51.98144 4.99411-51.98144 14.99782-11.92097 23.67-25.56577 27.63101-32.97331z"/><path d="m818.56767 802.18881-.9174 3.38458-10.03996-2.72957.91314-3.37522z"/><path d="m817.07405 807.70594-1.75995 6.49966-18.03534 9.08805 9.78412-18.31044z"/></g><path d="m836.1981 741.64919 7.72577-28.52932c-.3195 8.40427.28451 24.55036 7.21678 42.41047 0 0-11.89603 16.50235-21.99788 47.3763l-10.03442-2.71758 13.96533-51.5284c2.08221.56405 4.21039-.66603 4.77182-2.73844.45427-1.67248-.26571-3.38317-1.64739-4.27302" fill="#126d95"/></g></svg> |
| 15 | 15 | ARG_JAVA_OS="linux" |
| 16 | 16 | ARG_JAVA_ARCH="amd64" |
| 17 | ARG_JAVA_VERSION="14.0.2" | |
| 18 | ARG_JAVA_UPDATE="13" | |
| 17 | ARG_JAVA_VERSION="15.0.1" | |
| 18 | ARG_JAVA_UPDATE="9" | |
| 19 | 19 | ARG_JAVA_DIR="java" |
| 20 | 20 |
| 1 | 1 | |
| 2 | Apache License | |
| 3 | Version 2.0, January 2004 | |
| 4 | http://www.apache.org/licenses/ | |
| 5 | ||
| 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 7 | ||
| 8 | 1. Definitions. | |
| 9 | ||
| 10 | "License" shall mean the terms and conditions for use, reproduction, | |
| 11 | and distribution as defined by Sections 1 through 9 of this document. | |
| 12 | ||
| 13 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 14 | the copyright owner that is granting the License. | |
| 15 | ||
| 16 | "Legal Entity" shall mean the union of the acting entity and all | |
| 17 | other entities that control, are controlled by, or are under common | |
| 18 | control with that entity. For the purposes of this definition, | |
| 19 | "control" means (i) the power, direct or indirect, to cause the | |
| 20 | direction or management of such entity, whether by contract or | |
| 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 22 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 23 | ||
| 24 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 25 | exercising permissions granted by this License. | |
| 26 | ||
| 27 | "Source" form shall mean the preferred form for making modifications, | |
| 28 | including but not limited to software source code, documentation | |
| 29 | source, and configuration files. | |
| 30 | ||
| 31 | "Object" form shall mean any form resulting from mechanical | |
| 32 | transformation or translation of a Source form, including but | |
| 33 | not limited to compiled object code, generated documentation, | |
| 34 | and conversions to other media types. | |
| 35 | ||
| 36 | "Work" shall mean the work of authorship, whether in Source or | |
| 37 | Object form, made available under the License, as indicated by a | |
| 38 | copyright notice that is included in or attached to the work | |
| 39 | (an example is provided in the Appendix below). | |
| 40 | ||
| 41 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 42 | form, that is based on (or derived from) the Work and for which the | |
| 43 | editorial revisions, annotations, elaborations, or other modifications | |
| 44 | represent, as a whole, an original work of authorship. For the purposes | |
| 45 | of this License, Derivative Works shall not include works that remain | |
| 46 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 47 | the Work and Derivative Works thereof. | |
| 48 | ||
| 49 | "Contribution" shall mean any work of authorship, including | |
| 50 | the original version of the Work and any modifications or additions | |
| 51 | to that Work or Derivative Works thereof, that is intentionally | |
| 52 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 53 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 54 | the copyright owner. For the purposes of this definition, "submitted" | |
| 55 | means any form of electronic, verbal, or written communication sent | |
| 56 | to the Licensor or its representatives, including but not limited to | |
| 57 | communication on electronic mailing lists, source code control systems, | |
| 58 | and issue tracking systems that are managed by, or on behalf of, the | |
| 59 | Licensor for the purpose of discussing and improving the Work, but | |
| 60 | excluding communication that is conspicuously marked or otherwise | |
| 61 | designated in writing by the copyright owner as "Not a Contribution." | |
| 62 | ||
| 63 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 64 | on behalf of whom a Contribution has been received by Licensor and | |
| 65 | subsequently incorporated within the Work. | |
| 66 | ||
| 67 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 68 | this License, each Contributor hereby grants to You a perpetual, | |
| 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 70 | copyright license to reproduce, prepare Derivative Works of, | |
| 71 | publicly display, publicly perform, sublicense, and distribute the | |
| 72 | Work and such Derivative Works in Source or Object form. | |
| 73 | ||
| 74 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 75 | this License, each Contributor hereby grants to You a perpetual, | |
| 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 77 | (except as stated in this section) patent license to make, have made, | |
| 78 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 79 | where such license applies only to those patent claims licensable | |
| 80 | by such Contributor that are necessarily infringed by their | |
| 81 | Contribution(s) alone or by combination of their Contribution(s) | |
| 82 | with the Work to which such Contribution(s) was submitted. If You | |
| 83 | institute patent litigation against any entity (including a | |
| 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 85 | or a Contribution incorporated within the Work constitutes direct | |
| 86 | or contributory patent infringement, then any patent licenses | |
| 87 | granted to You under this License for that Work shall terminate | |
| 88 | as of the date such litigation is filed. | |
| 89 | ||
| 90 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 91 | Work or Derivative Works thereof in any medium, with or without | |
| 92 | modifications, and in Source or Object form, provided that You | |
| 93 | meet the following conditions: | |
| 94 | ||
| 95 | (a) You must give any other recipients of the Work or | |
| 96 | Derivative Works a copy of this License; and | |
| 97 | ||
| 98 | (b) You must cause any modified files to carry prominent notices | |
| 99 | stating that You changed the files; and | |
| 100 | ||
| 101 | (c) You must retain, in the Source form of any Derivative Works | |
| 102 | that You distribute, all copyright, patent, trademark, and | |
| 103 | attribution notices from the Source form of the Work, | |
| 104 | excluding those notices that do not pertain to any part of | |
| 105 | the Derivative Works; and | |
| 106 | ||
| 107 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 108 | distribution, then any Derivative Works that You distribute must | |
| 109 | include a readable copy of the attribution notices contained | |
| 110 | within such NOTICE file, excluding those notices that do not | |
| 111 | pertain to any part of the Derivative Works, in at least one | |
| 112 | of the following places: within a NOTICE text file distributed | |
| 113 | as part of the Derivative Works; within the Source form or | |
| 114 | documentation, if provided along with the Derivative Works; or, | |
| 115 | within a display generated by the Derivative Works, if and | |
| 116 | wherever such third-party notices normally appear. The contents | |
| 117 | of the NOTICE file are for informational purposes only and | |
| 118 | do not modify the License. You may add Your own attribution | |
| 119 | notices within Derivative Works that You distribute, alongside | |
| 120 | or as an addendum to the NOTICE text from the Work, provided | |
| 121 | that such additional attribution notices cannot be construed | |
| 122 | as modifying the License. | |
| 123 | ||
| 124 | You may add Your own copyright statement to Your modifications and | |
| 125 | may provide additional or different license terms and conditions | |
| 126 | for use, reproduction, or distribution of Your modifications, or | |
| 127 | for any such Derivative Works as a whole, provided Your use, | |
| 128 | reproduction, and distribution of the Work otherwise complies with | |
| 129 | the conditions stated in this License. | |
| 130 | ||
| 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 132 | any Contribution intentionally submitted for inclusion in the Work | |
| 133 | by You to the Licensor shall be under the terms and conditions of | |
| 134 | this License, without any additional terms or conditions. | |
| 135 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 136 | the terms of any separate license agreement you may have executed | |
| 137 | with Licensor regarding such Contributions. | |
| 138 | ||
| 139 | 6. Trademarks. This License does not grant permission to use the trade | |
| 140 | names, trademarks, service marks, or product names of the Licensor, | |
| 141 | except as required for reasonable and customary use in describing the | |
| 142 | origin of the Work and reproducing the content of the NOTICE file. | |
| 143 | ||
| 144 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 145 | agreed to in writing, Licensor provides the Work (and each | |
| 146 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 148 | implied, including, without limitation, any warranties or conditions | |
| 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 150 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 151 | appropriateness of using or redistributing the Work and assume any | |
| 152 | risks associated with Your exercise of permissions under this License. | |
| 153 | ||
| 154 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 155 | whether in tort (including negligence), contract, or otherwise, | |
| 156 | unless required by applicable law (such as deliberate and grossly | |
| 157 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 158 | liable to You for damages, including any direct, indirect, special, | |
| 159 | incidental, or consequential damages of any character arising as a | |
| 160 | result of this License or out of the use or inability to use the | |
| 161 | Work (including but not limited to damages for loss of goodwill, | |
| 162 | work stoppage, computer failure or malfunction, or any and all | |
| 163 | other commercial damages or losses), even if such Contributor | |
| 164 | has been advised of the possibility of such damages. | |
| 165 | ||
| 166 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 167 | the Work or Derivative Works thereof, You may choose to offer, | |
| 168 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 169 | or other liability obligations and/or rights consistent with this | |
| 170 | License. However, in accepting such obligations, You may act only | |
| 171 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 172 | of any other Contributor, and only if You agree to indemnify, | |
| 173 | defend, and hold each Contributor harmless for any liability | |
| 174 | incurred by, or claims asserted against, such Contributor by reason | |
| 175 | of your accepting any such warranty or additional liability. | |
| 176 | ||
| 177 | END OF TERMS AND CONDITIONS | |
| 178 | ||
| 179 | APPENDIX: How to apply the Apache License to your work. | |
| 180 | ||
| 181 | To apply the Apache License to your work, attach the following | |
| 182 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 183 | replaced with your own identifying information. (Don't include | |
| 184 | the brackets!) The text should be enclosed in the appropriate | |
| 185 | comment syntax for the file format. We also recommend that a | |
| 186 | file or class name and description of purpose be included on the | |
| 187 | same "printed page" as the copyright notice for easier | |
| 188 | identification within third-party archives. | |
| 189 | ||
| 190 | Copyright [yyyy] [name of copyright owner] | |
| 191 | ||
| 192 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 193 | you may not use this file except in compliance with the License. | |
| 194 | You may obtain a copy of the License at | |
| 195 | ||
| 196 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 197 | ||
| 198 | Unless required by applicable law or agreed to in writing, software | |
| 199 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 201 | See the License for the specific language governing permissions and | |
| 202 | limitations under the License. | |
| 203 |
| 1 | Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode) | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | This license is copied below, and is also available with a FAQ at: | |
| 5 | http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 94 | 1 |
| 1 | URL: https://github.com/googlefonts/noto-cjk | |
| 2 | ||
| 3 | Version: 1.002 or later | |
| 4 | ||
| 5 | License: SIL Open Font License v1.1 | |
| 6 | ||
| 7 | License File: LICENSE | |
| 8 | ||
| 9 | Note: prior releases of the CJK fonts were issued under the Apache 2 | |
| 10 | license. This was changed to the SIL OFL v1.1 starting with Version 1.002. | |
| 11 | ||
| 12 | Description: | |
| 13 | Noto CJK fonts, supporting Simplified Chinese, Traditional Chinese, | |
| 14 | Japanese, and Korean. The supported scripts are Han, Hiragana, Katakana, | |
| 15 | Hangul, and Bopomofo. Latin, Greek, Cyrillic, and various symbols are also | |
| 16 | supported for compatibility with CJK standards. | |
| 17 | ||
| 18 | The fonts in this directory are developed by Google and Adobe and are | |
| 19 | released as open source under the Apache license version 2.0. The copyright | |
| 20 | is held by Adobe, while the trademarks on the names are held by Google. | |
| 21 | ||
| 22 | A README-formats file has been added explaining the different formats | |
| 23 | provided and their features and limitations. | |
| 1 | 24 |
| 1 | 1 | |
| 2 | Apache License | |
| 3 | Version 2.0, January 2004 | |
| 4 | http://www.apache.org/licenses/ | |
| 5 | ||
| 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 7 | ||
| 8 | 1. Definitions. | |
| 9 | ||
| 10 | "License" shall mean the terms and conditions for use, reproduction, | |
| 11 | and distribution as defined by Sections 1 through 9 of this document. | |
| 12 | ||
| 13 | "Licensor" shall mean the copyright owner or entity authorized by | |
| 14 | the copyright owner that is granting the License. | |
| 15 | ||
| 16 | "Legal Entity" shall mean the union of the acting entity and all | |
| 17 | other entities that control, are controlled by, or are under common | |
| 18 | control with that entity. For the purposes of this definition, | |
| 19 | "control" means (i) the power, direct or indirect, to cause the | |
| 20 | direction or management of such entity, whether by contract or | |
| 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| 22 | outstanding shares, or (iii) beneficial ownership of such entity. | |
| 23 | ||
| 24 | "You" (or "Your") shall mean an individual or Legal Entity | |
| 25 | exercising permissions granted by this License. | |
| 26 | ||
| 27 | "Source" form shall mean the preferred form for making modifications, | |
| 28 | including but not limited to software source code, documentation | |
| 29 | source, and configuration files. | |
| 30 | ||
| 31 | "Object" form shall mean any form resulting from mechanical | |
| 32 | transformation or translation of a Source form, including but | |
| 33 | not limited to compiled object code, generated documentation, | |
| 34 | and conversions to other media types. | |
| 35 | ||
| 36 | "Work" shall mean the work of authorship, whether in Source or | |
| 37 | Object form, made available under the License, as indicated by a | |
| 38 | copyright notice that is included in or attached to the work | |
| 39 | (an example is provided in the Appendix below). | |
| 40 | ||
| 41 | "Derivative Works" shall mean any work, whether in Source or Object | |
| 42 | form, that is based on (or derived from) the Work and for which the | |
| 43 | editorial revisions, annotations, elaborations, or other modifications | |
| 44 | represent, as a whole, an original work of authorship. For the purposes | |
| 45 | of this License, Derivative Works shall not include works that remain | |
| 46 | separable from, or merely link (or bind by name) to the interfaces of, | |
| 47 | the Work and Derivative Works thereof. | |
| 48 | ||
| 49 | "Contribution" shall mean any work of authorship, including | |
| 50 | the original version of the Work and any modifications or additions | |
| 51 | to that Work or Derivative Works thereof, that is intentionally | |
| 52 | submitted to Licensor for inclusion in the Work by the copyright owner | |
| 53 | or by an individual or Legal Entity authorized to submit on behalf of | |
| 54 | the copyright owner. For the purposes of this definition, "submitted" | |
| 55 | means any form of electronic, verbal, or written communication sent | |
| 56 | to the Licensor or its representatives, including but not limited to | |
| 57 | communication on electronic mailing lists, source code control systems, | |
| 58 | and issue tracking systems that are managed by, or on behalf of, the | |
| 59 | Licensor for the purpose of discussing and improving the Work, but | |
| 60 | excluding communication that is conspicuously marked or otherwise | |
| 61 | designated in writing by the copyright owner as "Not a Contribution." | |
| 62 | ||
| 63 | "Contributor" shall mean Licensor and any individual or Legal Entity | |
| 64 | on behalf of whom a Contribution has been received by Licensor and | |
| 65 | subsequently incorporated within the Work. | |
| 66 | ||
| 67 | 2. Grant of Copyright License. Subject to the terms and conditions of | |
| 68 | this License, each Contributor hereby grants to You a perpetual, | |
| 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 70 | copyright license to reproduce, prepare Derivative Works of, | |
| 71 | publicly display, publicly perform, sublicense, and distribute the | |
| 72 | Work and such Derivative Works in Source or Object form. | |
| 73 | ||
| 74 | 3. Grant of Patent License. Subject to the terms and conditions of | |
| 75 | this License, each Contributor hereby grants to You a perpetual, | |
| 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| 77 | (except as stated in this section) patent license to make, have made, | |
| 78 | use, offer to sell, sell, import, and otherwise transfer the Work, | |
| 79 | where such license applies only to those patent claims licensable | |
| 80 | by such Contributor that are necessarily infringed by their | |
| 81 | Contribution(s) alone or by combination of their Contribution(s) | |
| 82 | with the Work to which such Contribution(s) was submitted. If You | |
| 83 | institute patent litigation against any entity (including a | |
| 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| 85 | or a Contribution incorporated within the Work constitutes direct | |
| 86 | or contributory patent infringement, then any patent licenses | |
| 87 | granted to You under this License for that Work shall terminate | |
| 88 | as of the date such litigation is filed. | |
| 89 | ||
| 90 | 4. Redistribution. You may reproduce and distribute copies of the | |
| 91 | Work or Derivative Works thereof in any medium, with or without | |
| 92 | modifications, and in Source or Object form, provided that You | |
| 93 | meet the following conditions: | |
| 94 | ||
| 95 | (a) You must give any other recipients of the Work or | |
| 96 | Derivative Works a copy of this License; and | |
| 97 | ||
| 98 | (b) You must cause any modified files to carry prominent notices | |
| 99 | stating that You changed the files; and | |
| 100 | ||
| 101 | (c) You must retain, in the Source form of any Derivative Works | |
| 102 | that You distribute, all copyright, patent, trademark, and | |
| 103 | attribution notices from the Source form of the Work, | |
| 104 | excluding those notices that do not pertain to any part of | |
| 105 | the Derivative Works; and | |
| 106 | ||
| 107 | (d) If the Work includes a "NOTICE" text file as part of its | |
| 108 | distribution, then any Derivative Works that You distribute must | |
| 109 | include a readable copy of the attribution notices contained | |
| 110 | within such NOTICE file, excluding those notices that do not | |
| 111 | pertain to any part of the Derivative Works, in at least one | |
| 112 | of the following places: within a NOTICE text file distributed | |
| 113 | as part of the Derivative Works; within the Source form or | |
| 114 | documentation, if provided along with the Derivative Works; or, | |
| 115 | within a display generated by the Derivative Works, if and | |
| 116 | wherever such third-party notices normally appear. The contents | |
| 117 | of the NOTICE file are for informational purposes only and | |
| 118 | do not modify the License. You may add Your own attribution | |
| 119 | notices within Derivative Works that You distribute, alongside | |
| 120 | or as an addendum to the NOTICE text from the Work, provided | |
| 121 | that such additional attribution notices cannot be construed | |
| 122 | as modifying the License. | |
| 123 | ||
| 124 | You may add Your own copyright statement to Your modifications and | |
| 125 | may provide additional or different license terms and conditions | |
| 126 | for use, reproduction, or distribution of Your modifications, or | |
| 127 | for any such Derivative Works as a whole, provided Your use, | |
| 128 | reproduction, and distribution of the Work otherwise complies with | |
| 129 | the conditions stated in this License. | |
| 130 | ||
| 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| 132 | any Contribution intentionally submitted for inclusion in the Work | |
| 133 | by You to the Licensor shall be under the terms and conditions of | |
| 134 | this License, without any additional terms or conditions. | |
| 135 | Notwithstanding the above, nothing herein shall supersede or modify | |
| 136 | the terms of any separate license agreement you may have executed | |
| 137 | with Licensor regarding such Contributions. | |
| 138 | ||
| 139 | 6. Trademarks. This License does not grant permission to use the trade | |
| 140 | names, trademarks, service marks, or product names of the Licensor, | |
| 141 | except as required for reasonable and customary use in describing the | |
| 142 | origin of the Work and reproducing the content of the NOTICE file. | |
| 143 | ||
| 144 | 7. Disclaimer of Warranty. Unless required by applicable law or | |
| 145 | agreed to in writing, Licensor provides the Work (and each | |
| 146 | Contributor provides its Contributions) on an "AS IS" BASIS, | |
| 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| 148 | implied, including, without limitation, any warranties or conditions | |
| 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| 150 | PARTICULAR PURPOSE. You are solely responsible for determining the | |
| 151 | appropriateness of using or redistributing the Work and assume any | |
| 152 | risks associated with Your exercise of permissions under this License. | |
| 153 | ||
| 154 | 8. Limitation of Liability. In no event and under no legal theory, | |
| 155 | whether in tort (including negligence), contract, or otherwise, | |
| 156 | unless required by applicable law (such as deliberate and grossly | |
| 157 | negligent acts) or agreed to in writing, shall any Contributor be | |
| 158 | liable to You for damages, including any direct, indirect, special, | |
| 159 | incidental, or consequential damages of any character arising as a | |
| 160 | result of this License or out of the use or inability to use the | |
| 161 | Work (including but not limited to damages for loss of goodwill, | |
| 162 | work stoppage, computer failure or malfunction, or any and all | |
| 163 | other commercial damages or losses), even if such Contributor | |
| 164 | has been advised of the possibility of such damages. | |
| 165 | ||
| 166 | 9. Accepting Warranty or Additional Liability. While redistributing | |
| 167 | the Work or Derivative Works thereof, You may choose to offer, | |
| 168 | and charge a fee for, acceptance of support, warranty, indemnity, | |
| 169 | or other liability obligations and/or rights consistent with this | |
| 170 | License. However, in accepting such obligations, You may act only | |
| 171 | on Your own behalf and on Your sole responsibility, not on behalf | |
| 172 | of any other Contributor, and only if You agree to indemnify, | |
| 173 | defend, and hold each Contributor harmless for any liability | |
| 174 | incurred by, or claims asserted against, such Contributor by reason | |
| 175 | of your accepting any such warranty or additional liability. | |
| 176 | ||
| 177 | END OF TERMS AND CONDITIONS | |
| 178 | ||
| 179 | APPENDIX: How to apply the Apache License to your work. | |
| 180 | ||
| 181 | To apply the Apache License to your work, attach the following | |
| 182 | boilerplate notice, with the fields enclosed by brackets "[]" | |
| 183 | replaced with your own identifying information. (Don't include | |
| 184 | the brackets!) The text should be enclosed in the appropriate | |
| 185 | comment syntax for the file format. We also recommend that a | |
| 186 | file or class name and description of purpose be included on the | |
| 187 | same "printed page" as the copyright notice for easier | |
| 188 | identification within third-party archives. | |
| 189 | ||
| 190 | Copyright [yyyy] [name of copyright owner] | |
| 191 | ||
| 192 | Licensed under the Apache License, Version 2.0 (the "License"); | |
| 193 | you may not use this file except in compliance with the License. | |
| 194 | You may obtain a copy of the License at | |
| 195 | ||
| 196 | http://www.apache.org/licenses/LICENSE-2.0 | |
| 197 | ||
| 198 | Unless required by applicable law or agreed to in writing, software | |
| 199 | distributed under the License is distributed on an "AS IS" BASIS, | |
| 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 201 | See the License for the specific language governing permissions and | |
| 202 | limitations under the License. | |
| 203 |
| 1 | Copyright 2018 The Noto Project Authors (https://github.com/googlei18n/noto-fonts) | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, | |
| 4 | Version 1.1. | |
| 5 | ||
| 6 | This license is copied below, and is also available with a FAQ at: | |
| 7 | http://scripts.sil.org/OFL | |
| 8 | ||
| 9 | ----------------------------------------------------------- | |
| 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 11 | ----------------------------------------------------------- | |
| 12 | ||
| 13 | PREAMBLE | |
| 14 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 15 | development of collaborative font projects, to support the font | |
| 16 | creation efforts of academic and linguistic communities, and to | |
| 17 | provide a free and open framework in which fonts may be shared and | |
| 18 | improved in partnership with others. | |
| 19 | ||
| 20 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 21 | redistributed freely as long as they are not sold by themselves. The | |
| 22 | fonts, including any derivative works, can be bundled, embedded, | |
| 23 | redistributed and/or sold with any software provided that any reserved | |
| 24 | names are not used by derivative works. The fonts and derivatives, | |
| 25 | however, cannot be released under any other type of license. The | |
| 26 | requirement for fonts to remain under this license does not apply to | |
| 27 | any document created using the fonts or their derivatives. | |
| 28 | ||
| 29 | DEFINITIONS | |
| 30 | "Font Software" refers to the set of files released by the Copyright | |
| 31 | Holder(s) under this license and clearly marked as such. This may | |
| 32 | include source files, build scripts and documentation. | |
| 33 | ||
| 34 | "Reserved Font Name" refers to any names specified as such after the | |
| 35 | copyright statement(s). | |
| 36 | ||
| 37 | "Original Version" refers to the collection of Font Software | |
| 38 | components as distributed by the Copyright Holder(s). | |
| 39 | ||
| 40 | "Modified Version" refers to any derivative made by adding to, | |
| 41 | deleting, or substituting -- in part or in whole -- any of the | |
| 42 | components of the Original Version, by changing formats or by porting | |
| 43 | the Font Software to a new environment. | |
| 44 | ||
| 45 | "Author" refers to any designer, engineer, programmer, technical | |
| 46 | writer or other person who contributed to the Font Software. | |
| 47 | ||
| 48 | PERMISSION & CONDITIONS | |
| 49 | Permission is hereby granted, free of charge, to any person obtaining | |
| 50 | a copy of the Font Software, to use, study, copy, merge, embed, | |
| 51 | modify, redistribute, and sell modified and unmodified copies of the | |
| 52 | Font Software, subject to the following conditions: | |
| 53 | ||
| 54 | 1) Neither the Font Software nor any of its individual components, in | |
| 55 | Original or Modified Versions, may be sold by itself. | |
| 56 | ||
| 57 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 58 | redistributed and/or sold with any software, provided that each copy | |
| 59 | contains the above copyright notice and this license. These can be | |
| 60 | included either as stand-alone text files, human-readable headers or | |
| 61 | in the appropriate machine-readable metadata fields within text or | |
| 62 | binary files as long as those fields can be easily viewed by the user. | |
| 63 | ||
| 64 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 65 | Name(s) unless explicit written permission is granted by the | |
| 66 | corresponding Copyright Holder. This restriction only applies to the | |
| 67 | primary font name as presented to the users. | |
| 68 | ||
| 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 70 | Software shall not be used to promote, endorse or advertise any | |
| 71 | Modified Version, except to acknowledge the contribution(s) of the | |
| 72 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 73 | permission. | |
| 74 | ||
| 75 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 76 | must be distributed entirely under this license, and must not be | |
| 77 | distributed under any other license. The requirement for fonts to | |
| 78 | remain under this license does not apply to any document created using | |
| 79 | the Font Software. | |
| 80 | ||
| 81 | TERMINATION | |
| 82 | This license becomes null and void if any of the above conditions are | |
| 83 | not met. | |
| 84 | ||
| 85 | DISCLAIMER | |
| 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 94 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 1 | 95 |
| 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | This license is copied below, and is also available with a FAQ at: | |
| 5 | http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 1 | 94 |
| 1 | Copyright 2014-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | ||
| 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 1 | 94 |
| 1 | Copyright 2017 The Vollkorn Project Authors (https://github.com/FAlthausen/Vollkorn-Typeface) | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | This license is copied below, and is also available with a FAQ at: | |
| 5 | http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 94 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 3 | ||
| 4 | import com.keenwrite.io.FileType; | |
| 29 | 5 | |
| 30 | 6 | import java.nio.file.Path; |
| 31 | 7 | |
| 32 | 8 | import static com.keenwrite.Constants.GLOB_PREFIX_FILE; |
| 33 | import static com.keenwrite.Constants.SETTINGS; | |
| 34 | import static com.keenwrite.FileType.UNKNOWN; | |
| 9 | import static com.keenwrite.Constants.sSettings; | |
| 10 | import static com.keenwrite.io.FileType.UNKNOWN; | |
| 35 | 11 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; |
| 36 | 12 | |
| ... | ||
| 51 | 27 | */ |
| 52 | 28 | public static FileType lookup( final Path path ) { |
| 29 | assert path != null; | |
| 30 | ||
| 53 | 31 | return lookup( path, GLOB_PREFIX_FILE ); |
| 54 | 32 | } |
| ... | ||
| 65 | 43 | assert prefix != null; |
| 66 | 44 | |
| 67 | final var keys = SETTINGS.getKeys( prefix ); | |
| 45 | final var keys = sSettings.getKeys( prefix ); | |
| 68 | 46 | |
| 69 | 47 | var found = false; |
| 70 | 48 | var fileType = UNKNOWN; |
| 71 | 49 | |
| 72 | 50 | while( keys.hasNext() && !found ) { |
| 73 | 51 | final var key = keys.next(); |
| 74 | final var patterns = SETTINGS.getStringSettingList( key ); | |
| 52 | final var patterns = sSettings.getStringSettingList( key ); | |
| 75 | 53 | final var predicate = createFileTypePredicate( patterns ); |
| 76 | 54 | |
| 77 | 55 | if( found = predicate.test( path.toFile() ) ) { |
| 78 | // Remove the EXTENSIONS_PREFIX to get the filename extension mapped | |
| 56 | // Remove the EXTENSIONS_PREFIX to get the file name extension mapped | |
| 79 | 57 | // to a standard name (as defined in the settings.properties file). |
| 80 | final String suffix = key.replace( prefix + ".", "" ); | |
| 58 | final String suffix = key.replace( prefix + '.', "" ); | |
| 81 | 59 | fileType = FileType.from( suffix ); |
| 82 | 60 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| 30 | import java.io.IOException; | |
| 31 | 4 | import java.util.Properties; |
| 32 | 5 | |
| ... | ||
| 48 | 21 | Constants.class.getResourceAsStream( "/bootstrap.properties" ) ) { |
| 49 | 22 | BOOTSTRAP.load( stream ); |
| 50 | } catch( final IOException ignored ) { | |
| 23 | } catch( final Exception ignored ) { | |
| 51 | 24 | // Bootstrap properties cannot be found, throw in the towel. |
| 52 | 25 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.service.Settings; |
| 31 | 5 | import javafx.scene.image.Image; |
| 32 | 6 | |
| 7 | import java.io.File; | |
| 8 | import java.nio.charset.Charset; | |
| 33 | 9 | import java.nio.file.Path; |
| 34 | import java.nio.file.Paths; | |
| 10 | import java.util.ArrayList; | |
| 11 | import java.util.List; | |
| 12 | import java.util.Locale; | |
| 35 | 13 | |
| 36 | 14 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; |
| 15 | import static com.keenwrite.preferences.LocaleScripts.withScript; | |
| 16 | import static java.io.File.separator; | |
| 37 | 17 | import static java.lang.String.format; |
| 18 | import static java.lang.System.getProperty; | |
| 38 | 19 | |
| 39 | 20 | /** |
| ... | ||
| 51 | 32 | */ |
| 52 | 33 | public static final String PATH_PROPERTIES_SETTINGS = |
| 53 | format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE ); | |
| 34 | format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE ); | |
| 54 | 35 | |
| 55 | 36 | /** |
| 56 | 37 | * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}. |
| 57 | 38 | */ |
| 58 | public static final Settings SETTINGS = Services.load( Settings.class ); | |
| 39 | public static final Settings sSettings = Services.load( Settings.class ); | |
| 59 | 40 | |
| 60 | public static final String DEFINITION_NAME = "variables.yaml"; | |
| 41 | public static final double WINDOW_X_DEFAULT = 0; | |
| 42 | public static final double WINDOW_Y_DEFAULT = 0; | |
| 43 | public static final double WINDOW_W_DEFAULT = 1200; | |
| 44 | public static final double WINDOW_H_DEFAULT = 800; | |
| 45 | ||
| 46 | public static final File DOCUMENT_DEFAULT = getFile( "document" ); | |
| 47 | public static final File DEFINITION_DEFAULT = getFile( "definition" ); | |
| 61 | 48 | |
| 62 | 49 | public static final String APP_BUNDLE_NAME = get( "application.messages" ); |
| 63 | 50 | |
| 64 | // Prevent double events when updating files on Linux (save and timestamp). | |
| 51 | /** | |
| 52 | * Prevent double events when updating files on Linux (save and timestamp). | |
| 53 | */ | |
| 65 | 54 | public static final int APP_WATCHDOG_TIMEOUT = get( |
| 66 | "application.watchdog.timeout", 200 ); | |
| 55 | "application.watchdog.timeout", 200 ); | |
| 67 | 56 | |
| 68 | public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" ); | |
| 69 | 57 | public static final String STYLESHEET_MARKDOWN = get( |
| 70 | "file.stylesheet.markdown" ); | |
| 58 | "file.stylesheet.markdown" ); | |
| 59 | public static final String STYLESHEET_MARKDOWN_LOCALE = | |
| 60 | "file.stylesheet.markdown.locale"; | |
| 71 | 61 | public static final String STYLESHEET_PREVIEW = get( |
| 72 | "file.stylesheet.preview" ); | |
| 62 | "file.stylesheet.preview" ); | |
| 63 | public static final String STYLESHEET_PREVIEW_LOCALE = | |
| 64 | "file.stylesheet.preview.locale"; | |
| 65 | public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" ); | |
| 73 | 66 | |
| 74 | public static final String FILE_LOGO_16 = get( "file.logo.16" ); | |
| 75 | public static final String FILE_LOGO_32 = get( "file.logo.32" ); | |
| 76 | public static final String FILE_LOGO_128 = get( "file.logo.128" ); | |
| 77 | public static final String FILE_LOGO_256 = get( "file.logo.256" ); | |
| 78 | public static final String FILE_LOGO_512 = get( "file.logo.512" ); | |
| 67 | public static final List<Image> LOGOS = createImages( | |
| 68 | "file.logo.16", | |
| 69 | "file.logo.32", | |
| 70 | "file.logo.128", | |
| 71 | "file.logo.256", | |
| 72 | "file.logo.512" | |
| 73 | ); | |
| 79 | 74 | |
| 80 | public static final Image ICON_DIALOG = new Image( FILE_LOGO_32 ); | |
| 75 | public static final Image ICON_DIALOG = LOGOS.get( 1 ); | |
| 81 | 76 | |
| 82 | public static final String PREFS_ROOT = get( "preferences.root" ); | |
| 83 | public static final String PREFS_STATE = get( "preferences.root.state" ); | |
| 77 | public static final String FILE_PREFERENCES = getPreferencesFilename(); | |
| 84 | 78 | |
| 85 | 79 | /** |
| 86 | * Refer to filename extension settings in the configuration file. Do not | |
| 87 | * terminate these prefixes with a period. | |
| 80 | * Refer to file name extension settings in the configuration file. Do not | |
| 81 | * terminate with a period. | |
| 88 | 82 | */ |
| 89 | 83 | public static final String GLOB_PREFIX_FILE = "file.ext"; |
| 90 | public static final String GLOB_PREFIX_DEFINITION = | |
| 91 | "definition." + GLOB_PREFIX_FILE; | |
| 92 | 84 | |
| 93 | 85 | /** |
| ... | ||
| 103 | 95 | public static final String STATUS_PARSE_ERROR = "Main.status.error.parse"; |
| 104 | 96 | public static final String STATUS_DEFINITION_BLANK = |
| 105 | "Main.status.error.def.blank"; | |
| 97 | "Main.status.error.def.blank"; | |
| 106 | 98 | public static final String STATUS_DEFINITION_EMPTY = |
| 107 | "Main.status.error.def.empty"; | |
| 99 | "Main.status.error.def.empty"; | |
| 108 | 100 | |
| 109 | 101 | /** |
| 110 | 102 | * One parameter: the word under the cursor that could not be found. |
| 111 | 103 | */ |
| 112 | 104 | public static final String STATUS_DEFINITION_MISSING = |
| 113 | "Main.status.error.def.missing"; | |
| 105 | "Main.status.error.def.missing"; | |
| 114 | 106 | |
| 115 | 107 | /** |
| 116 | 108 | * Used when creating flat maps relating to resolved variables. |
| 117 | 109 | */ |
| 118 | public static final int DEFAULT_MAP_SIZE = 64; | |
| 110 | public static final int MAP_SIZE_DEFAULT = 128; | |
| 119 | 111 | |
| 120 | 112 | /** |
| 121 | 113 | * Default image extension order to use when scanning. |
| 122 | 114 | */ |
| 123 | 115 | public static final String PERSIST_IMAGES_DEFAULT = |
| 124 | get( "file.ext.image.order" ); | |
| 116 | get( "file.ext.image.order" ); | |
| 125 | 117 | |
| 126 | 118 | /** |
| 127 | 119 | * Default working directory to use for R startup script. |
| 128 | 120 | */ |
| 129 | public static final String USER_DIRECTORY = System.getProperty( "user.dir" ); | |
| 121 | public static final File USER_DIRECTORY = | |
| 122 | new File( System.getProperty( "user.dir" ) ); | |
| 130 | 123 | |
| 131 | 124 | /** |
| 132 | 125 | * Default path to use for an untitled (pathless) file. |
| 133 | 126 | */ |
| 134 | public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY ); | |
| 127 | public static final Path DEFAULT_DIRECTORY = USER_DIRECTORY.toPath(); | |
| 128 | ||
| 129 | /** | |
| 130 | * Default character set to use when reading/writing files. | |
| 131 | */ | |
| 132 | public static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); | |
| 135 | 133 | |
| 136 | 134 | /** |
| ... | ||
| 169 | 167 | * Default text editor font size, in points. |
| 170 | 168 | */ |
| 171 | public static final float FONT_SIZE_EDITOR = 12f; | |
| 169 | public static final float FONT_SIZE_EDITOR_DEFAULT = 12f; | |
| 170 | ||
| 171 | /** | |
| 172 | * Default preview font size, in points. | |
| 173 | */ | |
| 174 | public static final float FONT_SIZE_PREVIEW_DEFAULT = 13f; | |
| 175 | ||
| 176 | /** | |
| 177 | * Default locale for font loading, including ISO 15924 alpha-4 script code. | |
| 178 | */ | |
| 179 | public static final Locale LOCALE_DEFAULT = withScript( Locale.getDefault() ); | |
| 172 | 180 | |
| 173 | 181 | /** |
| 174 | 182 | * Default identifier to use for synchronized scrolling. |
| 175 | 183 | */ |
| 176 | public static String CARET_ID = "caret"; | |
| 184 | public static final String CARET_ID = "caret"; | |
| 177 | 185 | |
| 178 | 186 | /** |
| 179 | 187 | * Prevent instantiation. |
| 180 | 188 | */ |
| 181 | 189 | private Constants() { |
| 182 | 190 | } |
| 183 | 191 | |
| 184 | 192 | private static String get( final String key ) { |
| 185 | return SETTINGS.getSetting( key, "" ); | |
| 193 | return sSettings.getSetting( key, "" ); | |
| 186 | 194 | } |
| 187 | 195 | |
| 188 | @SuppressWarnings("SameParameterValue") | |
| 196 | @SuppressWarnings( "SameParameterValue" ) | |
| 189 | 197 | private static int get( final String key, final int defaultValue ) { |
| 190 | return SETTINGS.getSetting( key, defaultValue ); | |
| 198 | return sSettings.getSetting( key, defaultValue ); | |
| 199 | } | |
| 200 | ||
| 201 | /** | |
| 202 | * Returns a default {@link File} instance based on the given key suffix. | |
| 203 | * | |
| 204 | * @param suffix Appended to {@code "file.default."}. | |
| 205 | * @return A new {@link File} instance that references the settings file name. | |
| 206 | */ | |
| 207 | private static File getFile( final String suffix ) { | |
| 208 | return new File( get( "file.default." + suffix ) ); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Returns the equivalent of {@code $HOME/.filename.xml}. | |
| 213 | */ | |
| 214 | private static String getPreferencesFilename() { | |
| 215 | return format( | |
| 216 | "%s%s.%s.xml", | |
| 217 | getProperty( "user.home" ), | |
| 218 | separator, | |
| 219 | APP_TITLE_LOWERCASE | |
| 220 | ); | |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Converts the given file names to images, such as application icons. | |
| 225 | * | |
| 226 | * @param keys The file names to convert to images. | |
| 227 | * @return The images loaded from the file name references. | |
| 228 | */ | |
| 229 | private static List<Image> createImages( final String... keys ) { | |
| 230 | final List<Image> images = new ArrayList<>( keys.length ); | |
| 231 | ||
| 232 | for( final var key : keys ) { | |
| 233 | images.add( new Image( get( key ) ) ); | |
| 234 | } | |
| 235 | ||
| 236 | return images; | |
| 191 | 237 | } |
| 192 | 238 | } |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite; | |
| 3 | ||
| 4 | import com.keenwrite.editors.TextDefinition; | |
| 5 | import com.keenwrite.editors.TextEditor; | |
| 6 | import com.keenwrite.editors.definition.DefinitionTreeItem; | |
| 7 | import com.keenwrite.sigils.SigilOperator; | |
| 8 | ||
| 9 | import static com.keenwrite.Constants.*; | |
| 10 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 11 | ||
| 12 | /** | |
| 13 | * Provides the logic for injecting variable names within the editor. | |
| 14 | */ | |
| 15 | public final class DefinitionNameInjector { | |
| 16 | ||
| 17 | /** | |
| 18 | * Prevent instantiation. | |
| 19 | */ | |
| 20 | private DefinitionNameInjector() { | |
| 21 | } | |
| 22 | ||
| 23 | /** | |
| 24 | * Find a node that matches the current word and substitute the definition | |
| 25 | * reference. | |
| 26 | */ | |
| 27 | public static void autoinsert( | |
| 28 | final TextEditor editor, | |
| 29 | final TextDefinition definitions, | |
| 30 | final SigilOperator operator ) { | |
| 31 | try { | |
| 32 | if( definitions.isEmpty() ) { | |
| 33 | clue( STATUS_DEFINITION_EMPTY ); | |
| 34 | } | |
| 35 | else { | |
| 36 | final var indexes = editor.getCaretWord(); | |
| 37 | final var word = editor.getText( indexes ); | |
| 38 | ||
| 39 | if( word.isBlank() ) { | |
| 40 | clue( STATUS_DEFINITION_BLANK ); | |
| 41 | } | |
| 42 | else { | |
| 43 | final var leaf = findLeaf( definitions, word ); | |
| 44 | ||
| 45 | if( leaf == null ) { | |
| 46 | clue( STATUS_DEFINITION_MISSING, word ); | |
| 47 | } | |
| 48 | else { | |
| 49 | editor.replaceText( indexes, operator.entoken( leaf.toPath() ) ); | |
| 50 | definitions.expand( leaf ); | |
| 51 | } | |
| 52 | } | |
| 53 | } | |
| 54 | } catch( final Exception ignored ) { | |
| 55 | clue( STATUS_DEFINITION_BLANK ); | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Looks for the given word, matching first by exact, next by a starts-with | |
| 61 | * condition with diacritics replaced, then by containment. | |
| 62 | * | |
| 63 | * @param word Match the word by: exact, beginning, containment, or other. | |
| 64 | */ | |
| 65 | @SuppressWarnings("ConstantConditions") | |
| 66 | private static DefinitionTreeItem<String> findLeaf( | |
| 67 | final TextDefinition definition, final String word ) { | |
| 68 | assert word != null; | |
| 69 | ||
| 70 | DefinitionTreeItem<String> leaf = null; | |
| 71 | ||
| 72 | leaf = leaf == null ? definition.findLeafExact( word ) : leaf; | |
| 73 | leaf = leaf == null ? definition.findLeafStartsWith( word ) : leaf; | |
| 74 | leaf = leaf == null ? definition.findLeafContains( word ) : leaf; | |
| 75 | leaf = leaf == null ? definition.findLeafContainsNoCase( word ) : leaf; | |
| 76 | ||
| 77 | return leaf; | |
| 78 | } | |
| 79 | } | |
| 1 | 80 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| 30 | 4 | import java.io.File; |
| 5 | import java.nio.file.Path; | |
| 31 | 6 | |
| 32 | 7 | import static org.apache.commons.io.FilenameUtils.removeExtension; |
| ... | ||
| 69 | 44 | |
| 70 | 45 | /** |
| 71 | * Returns the given file renamed with the extension that matches this | |
| 72 | * {@link ExportFormat} extension. | |
| 46 | * Returns the given {@link File} with its extension replaced by one that | |
| 47 | * matches this {@link ExportFormat} extension. | |
| 73 | 48 | * |
| 74 | * @param file The file to rename. | |
| 75 | * @return The renamed version of the given file. | |
| 49 | * @param file The file to perform an extension swap. | |
| 50 | * @return The given file with its extension replaced. | |
| 76 | 51 | */ |
| 77 | 52 | public File toExportFilename( final File file ) { |
| 78 | 53 | return new File( removeExtension( file.getName() ) + mExtension ); |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Delegates to {@link #toExportFilename(File)} after converting the given | |
| 58 | * {@link Path} to an instance of {@link File}. | |
| 59 | * | |
| 60 | * @param path The {@link Path} to convert to a {@link File}. | |
| 61 | * @return The given path with its extension replaced. | |
| 62 | */ | |
| 63 | public File toExportFilename( final Path path ) { | |
| 64 | return toExportFilename( path.toFile() ); | |
| 79 | 65 | } |
| 80 | 66 | } |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * Redistribution and use in source and binary forms, with or without | |
| 5 | * modification, are permitted provided that the following conditions are met: | |
| 6 | * | |
| 7 | * o Redistributions of source code must retain the above copyright | |
| 8 | * notice, this list of conditions and the following disclaimer. | |
| 9 | * | |
| 10 | * o Redistributions in binary form must reproduce the above copyright | |
| 11 | * notice, this list of conditions and the following disclaimer in the | |
| 12 | * documentation and/or other materials provided with the distribution. | |
| 13 | * | |
| 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 15 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 17 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 18 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 19 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 20 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 21 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 22 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 25 | */ | |
| 26 | package com.keenwrite; | |
| 27 | ||
| 28 | import com.keenwrite.editors.EditorPane; | |
| 29 | import com.keenwrite.editors.markdown.MarkdownEditorPane; | |
| 30 | import com.keenwrite.processors.Processor; | |
| 31 | import com.keenwrite.processors.markdown.CaretPosition; | |
| 32 | import com.keenwrite.service.events.Notification; | |
| 33 | import com.keenwrite.service.events.Notifier; | |
| 34 | import javafx.beans.binding.Bindings; | |
| 35 | import javafx.beans.property.BooleanProperty; | |
| 36 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 37 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 38 | import javafx.beans.property.SimpleBooleanProperty; | |
| 39 | import javafx.beans.value.ChangeListener; | |
| 40 | import javafx.event.Event; | |
| 41 | import javafx.event.EventHandler; | |
| 42 | import javafx.event.EventType; | |
| 43 | import javafx.scene.Scene; | |
| 44 | import javafx.scene.control.Tab; | |
| 45 | import javafx.scene.control.Tooltip; | |
| 46 | import javafx.scene.text.Text; | |
| 47 | import javafx.stage.Window; | |
| 48 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 49 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 50 | import org.fxmisc.undo.UndoManager; | |
| 51 | import org.jetbrains.annotations.NotNull; | |
| 52 | import org.mozilla.universalchardet.UniversalDetector; | |
| 53 | ||
| 54 | import java.io.File; | |
| 55 | import java.nio.charset.Charset; | |
| 56 | import java.nio.file.Files; | |
| 57 | import java.nio.file.Path; | |
| 58 | ||
| 59 | import static com.keenwrite.Messages.get; | |
| 60 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 61 | import static com.keenwrite.StatusBarNotifier.getNotifier; | |
| 62 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 63 | import static java.util.Locale.ENGLISH; | |
| 64 | import static javafx.application.Platform.runLater; | |
| 65 | ||
| 66 | /** | |
| 67 | * Editor for a single file. | |
| 68 | */ | |
| 69 | public final class FileEditorTab extends Tab { | |
| 70 | ||
| 71 | private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane(); | |
| 72 | ||
| 73 | private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper(); | |
| 74 | private final BooleanProperty canUndo = new SimpleBooleanProperty(); | |
| 75 | private final BooleanProperty canRedo = new SimpleBooleanProperty(); | |
| 76 | ||
| 77 | /** | |
| 78 | * Character encoding used by the file (or default encoding if none found). | |
| 79 | */ | |
| 80 | private Charset mEncoding = UTF_8; | |
| 81 | ||
| 82 | /** | |
| 83 | * File to load into the editor. | |
| 84 | */ | |
| 85 | private Path mPath; | |
| 86 | ||
| 87 | /** | |
| 88 | * Dynamically updated position of the caret within the text editor. | |
| 89 | */ | |
| 90 | private final CaretPosition mCaretPosition; | |
| 91 | ||
| 92 | public FileEditorTab( final Path path ) { | |
| 93 | setPath( path ); | |
| 94 | ||
| 95 | mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() ); | |
| 96 | ||
| 97 | setOnSelectionChanged( e -> { | |
| 98 | if( isSelected() ) { | |
| 99 | runLater( this::activated ); | |
| 100 | requestFocus(); | |
| 101 | } | |
| 102 | } ); | |
| 103 | ||
| 104 | mCaretPosition = createCaretPosition( getEditor() ); | |
| 105 | } | |
| 106 | ||
| 107 | private CaretPosition createCaretPosition( | |
| 108 | final StyleClassedTextArea editor ) { | |
| 109 | final var propParaIndex = editor.currentParagraphProperty(); | |
| 110 | final var propParagraphs = editor.getParagraphs(); | |
| 111 | final var propParaOffset = editor.caretColumnProperty(); | |
| 112 | final var propTextOffset = editor.caretPositionProperty(); | |
| 113 | ||
| 114 | return CaretPosition | |
| 115 | .builder() | |
| 116 | .with( CaretPosition.Mutator::setParagraph, propParaIndex ) | |
| 117 | .with( CaretPosition.Mutator::setParagraphs, propParagraphs ) | |
| 118 | .with( CaretPosition.Mutator::setParaOffset, propParaOffset ) | |
| 119 | .with( CaretPosition.Mutator::setTextOffset, propTextOffset ) | |
| 120 | .build(); | |
| 121 | } | |
| 122 | ||
| 123 | private void updateTab() { | |
| 124 | setText( getTabTitle() ); | |
| 125 | setGraphic( getModifiedMark() ); | |
| 126 | setTooltip( getTabTooltip() ); | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * Returns the base filename (without the directory names). | |
| 131 | * | |
| 132 | * @return The untitled text if the path hasn't been set. | |
| 133 | */ | |
| 134 | private String getTabTitle() { | |
| 135 | return getPath().getFileName().toString(); | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Returns the full filename represented by the path. | |
| 140 | * | |
| 141 | * @return The untitled text if the path hasn't been set. | |
| 142 | */ | |
| 143 | private Tooltip getTabTooltip() { | |
| 144 | final Path filePath = getPath(); | |
| 145 | return new Tooltip( filePath == null ? "" : filePath.toString() ); | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Returns a marker to indicate whether the file has been modified. | |
| 150 | * | |
| 151 | * @return "*" when the file has changed; otherwise null. | |
| 152 | */ | |
| 153 | private Text getModifiedMark() { | |
| 154 | return isModified() ? new Text( "*" ) : null; | |
| 155 | } | |
| 156 | ||
| 157 | /** | |
| 158 | * Called when the user switches tab. | |
| 159 | */ | |
| 160 | private void activated() { | |
| 161 | // Tab is closed or no longer active. | |
| 162 | if( getTabPane() == null || !isSelected() ) { | |
| 163 | return; | |
| 164 | } | |
| 165 | ||
| 166 | // If the tab is devoid of content, load it. | |
| 167 | if( getContent() == null ) { | |
| 168 | readFile(); | |
| 169 | initLayout(); | |
| 170 | initUndoManager(); | |
| 171 | } | |
| 172 | } | |
| 173 | ||
| 174 | private void initLayout() { | |
| 175 | setContent( getScrollPane() ); | |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * Tracks undo requests, but can only be called <em>after</em> load. | |
| 180 | */ | |
| 181 | private void initUndoManager() { | |
| 182 | final UndoManager<?> undoManager = getUndoManager(); | |
| 183 | undoManager.forgetHistory(); | |
| 184 | ||
| 185 | // Bind the editor undo manager to the properties. | |
| 186 | mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) ); | |
| 187 | canUndo.bind( undoManager.undoAvailableProperty() ); | |
| 188 | canRedo.bind( undoManager.redoAvailableProperty() ); | |
| 189 | } | |
| 190 | ||
| 191 | private void requestFocus() { | |
| 192 | getEditorPane().requestFocus(); | |
| 193 | } | |
| 194 | ||
| 195 | /** | |
| 196 | * Searches from the caret position forward for the given string. | |
| 197 | * | |
| 198 | * @param needle The text string to match. | |
| 199 | */ | |
| 200 | public void searchNext( final String needle ) { | |
| 201 | final String haystack = getEditorText(); | |
| 202 | int index = haystack.indexOf( needle, getCaretTextOffset() ); | |
| 203 | ||
| 204 | // Wrap around. | |
| 205 | if( index == -1 ) { | |
| 206 | index = haystack.indexOf( needle ); | |
| 207 | } | |
| 208 | ||
| 209 | if( index >= 0 ) { | |
| 210 | setCaretTextOffset( index ); | |
| 211 | getEditor().selectRange( index, index + needle.length() ); | |
| 212 | } | |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Gets a reference to the scroll pane that houses the editor. | |
| 217 | * | |
| 218 | * @return The editor's scroll pane, containing a vertical scrollbar. | |
| 219 | */ | |
| 220 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 221 | return getEditorPane().getScrollPane(); | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Returns an instance of {@link CaretPosition} that contains information | |
| 226 | * about the caret, including the offset into the text, the paragraph into | |
| 227 | * the text, maximum number of paragraphs, and more. This allows the main | |
| 228 | * application and the {@link Processor} instances to get the current | |
| 229 | * caret position. | |
| 230 | * | |
| 231 | * @return The current values for the caret's position within the editor. | |
| 232 | */ | |
| 233 | public CaretPosition getCaretPosition() { | |
| 234 | return mCaretPosition; | |
| 235 | } | |
| 236 | ||
| 237 | /** | |
| 238 | * Returns the index into the text where the caret blinks happily away. | |
| 239 | * | |
| 240 | * @return A number from 0 to the editor's document text length. | |
| 241 | */ | |
| 242 | private int getCaretTextOffset() { | |
| 243 | return getEditor().getCaretPosition(); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Moves the caret to a given offset. | |
| 248 | * | |
| 249 | * @param offset The new caret offset. | |
| 250 | */ | |
| 251 | private void setCaretTextOffset( final int offset ) { | |
| 252 | getEditor().moveTo( offset ); | |
| 253 | getEditor().requestFollowCaret(); | |
| 254 | } | |
| 255 | ||
| 256 | /** | |
| 257 | * Returns the text area associated with this tab. | |
| 258 | * | |
| 259 | * @return A text editor. | |
| 260 | */ | |
| 261 | private StyleClassedTextArea getEditor() { | |
| 262 | return getEditorPane().getEditor(); | |
| 263 | } | |
| 264 | ||
| 265 | /** | |
| 266 | * Returns true if the given path exactly matches this tab's path. | |
| 267 | * | |
| 268 | * @param check The path to compare against. | |
| 269 | * @return true The paths are the same. | |
| 270 | */ | |
| 271 | public boolean isPath( final Path check ) { | |
| 272 | final Path filePath = getPath(); | |
| 273 | ||
| 274 | return filePath != null && filePath.equals( check ); | |
| 275 | } | |
| 276 | ||
| 277 | /** | |
| 278 | * Reads the entire file contents from the path associated with this tab. | |
| 279 | */ | |
| 280 | private void readFile() { | |
| 281 | final Path path = getPath(); | |
| 282 | final File file = path.toFile(); | |
| 283 | ||
| 284 | try { | |
| 285 | if( file.exists() ) { | |
| 286 | if( file.canWrite() && file.canRead() ) { | |
| 287 | final EditorPane pane = getEditorPane(); | |
| 288 | pane.setText( asString( Files.readAllBytes( path ) ) ); | |
| 289 | pane.scrollToTop(); | |
| 290 | } | |
| 291 | else { | |
| 292 | final String msg = get( "FileEditor.loadFailed.reason.permissions" ); | |
| 293 | clue( "FileEditor.loadFailed.message", file.toString(), msg ); | |
| 294 | } | |
| 295 | } | |
| 296 | } catch( final Exception ex ) { | |
| 297 | clue( ex ); | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | /** | |
| 302 | * Saves the entire file contents from the path associated with this tab. | |
| 303 | * | |
| 304 | * @return true The file has been saved. | |
| 305 | */ | |
| 306 | public boolean save() { | |
| 307 | try { | |
| 308 | final EditorPane editor = getEditorPane(); | |
| 309 | Files.write( getPath(), asBytes( editor.getText() ) ); | |
| 310 | editor.getUndoManager().mark(); | |
| 311 | return true; | |
| 312 | } catch( final Exception ex ) { | |
| 313 | return popupAlert( | |
| 314 | "FileEditor.saveFailed.title", | |
| 315 | "FileEditor.saveFailed.message", | |
| 316 | ex | |
| 317 | ); | |
| 318 | } | |
| 319 | } | |
| 320 | ||
| 321 | /** | |
| 322 | * Creates an alert dialog and waits for it to close. | |
| 323 | * | |
| 324 | * @param titleKey Resource bundle key for the alert dialog title. | |
| 325 | * @param messageKey Resource bundle key for the alert dialog message. | |
| 326 | * @param e The unexpected happening. | |
| 327 | * @return false | |
| 328 | */ | |
| 329 | @SuppressWarnings("SameParameterValue") | |
| 330 | private boolean popupAlert( | |
| 331 | final String titleKey, final String messageKey, final Exception e ) { | |
| 332 | final Notifier service = getNotifier(); | |
| 333 | final Path filePath = getPath(); | |
| 334 | ||
| 335 | final Notification message = service.createNotification( | |
| 336 | get( titleKey ), | |
| 337 | get( messageKey ), | |
| 338 | filePath == null ? "" : filePath, | |
| 339 | e.getMessage() | |
| 340 | ); | |
| 341 | ||
| 342 | try { | |
| 343 | service.createError( getWindow(), message ).showAndWait(); | |
| 344 | } catch( final Exception ex ) { | |
| 345 | clue( ex ); | |
| 346 | } | |
| 347 | ||
| 348 | return false; | |
| 349 | } | |
| 350 | ||
| 351 | private Window getWindow() { | |
| 352 | final Scene scene = getEditorPane().getScene(); | |
| 353 | ||
| 354 | if( scene == null ) { | |
| 355 | throw new UnsupportedOperationException( "No scene window available" ); | |
| 356 | } | |
| 357 | ||
| 358 | return scene.getWindow(); | |
| 359 | } | |
| 360 | ||
| 361 | /** | |
| 362 | * Returns a best guess at the file encoding. If the encoding could not be | |
| 363 | * detected, this will return the default charset for the JVM. | |
| 364 | * | |
| 365 | * @param bytes The bytes to perform character encoding detection. | |
| 366 | * @return The character encoding. | |
| 367 | */ | |
| 368 | private Charset detectEncoding( final byte[] bytes ) { | |
| 369 | final var detector = new UniversalDetector( null ); | |
| 370 | detector.handleData( bytes, 0, bytes.length ); | |
| 371 | detector.dataEnd(); | |
| 372 | ||
| 373 | final String charset = detector.getDetectedCharset(); | |
| 374 | ||
| 375 | return charset == null | |
| 376 | ? Charset.defaultCharset() | |
| 377 | : Charset.forName( charset.toUpperCase( ENGLISH ) ); | |
| 378 | } | |
| 379 | ||
| 380 | /** | |
| 381 | * Converts the given string to an array of bytes using the encoding that was | |
| 382 | * originally detected (if any) and associated with this file. | |
| 383 | * | |
| 384 | * @param text The text to convert into the original file encoding. | |
| 385 | * @return A series of bytes ready for writing to a file. | |
| 386 | */ | |
| 387 | private byte[] asBytes( final String text ) { | |
| 388 | return text.getBytes( getEncoding() ); | |
| 389 | } | |
| 390 | ||
| 391 | /** | |
| 392 | * Converts the given bytes into a Java String. This will call setEncoding | |
| 393 | * with the encoding detected by the CharsetDetector. | |
| 394 | * | |
| 395 | * @param text The text of unknown character encoding. | |
| 396 | * @return The text, in its auto-detected encoding, as a String. | |
| 397 | */ | |
| 398 | private String asString( final byte[] text ) { | |
| 399 | setEncoding( detectEncoding( text ) ); | |
| 400 | return new String( text, getEncoding() ); | |
| 401 | } | |
| 402 | ||
| 403 | /** | |
| 404 | * Returns the path to the file being edited in this tab. | |
| 405 | * | |
| 406 | * @return A non-null instance. | |
| 407 | */ | |
| 408 | public Path getPath() { | |
| 409 | return mPath; | |
| 410 | } | |
| 411 | ||
| 412 | /** | |
| 413 | * Sets the path to a file for editing and then updates the tab with the | |
| 414 | * file contents. | |
| 415 | * | |
| 416 | * @param path A non-null instance. | |
| 417 | */ | |
| 418 | public void setPath( final Path path ) { | |
| 419 | assert path != null; | |
| 420 | mPath = path; | |
| 421 | ||
| 422 | updateTab(); | |
| 423 | } | |
| 424 | ||
| 425 | public boolean isModified() { | |
| 426 | return mModified.get(); | |
| 427 | } | |
| 428 | ||
| 429 | ReadOnlyBooleanProperty modifiedProperty() { | |
| 430 | return mModified.getReadOnlyProperty(); | |
| 431 | } | |
| 432 | ||
| 433 | BooleanProperty canUndoProperty() { | |
| 434 | return this.canUndo; | |
| 435 | } | |
| 436 | ||
| 437 | BooleanProperty canRedoProperty() { | |
| 438 | return this.canRedo; | |
| 439 | } | |
| 440 | ||
| 441 | private UndoManager<?> getUndoManager() { | |
| 442 | return getEditorPane().getUndoManager(); | |
| 443 | } | |
| 444 | ||
| 445 | /** | |
| 446 | * Forwards to the editor pane's listeners for text change events. | |
| 447 | * | |
| 448 | * @param listener The listener to notify when the text changes. | |
| 449 | */ | |
| 450 | public void addTextChangeListener( final ChangeListener<String> listener ) { | |
| 451 | getEditorPane().addTextChangeListener( listener ); | |
| 452 | } | |
| 453 | ||
| 454 | /** | |
| 455 | * Forwards to the editor pane's listeners for caret change events. | |
| 456 | * | |
| 457 | * @param listener Notified when the caret position changes. | |
| 458 | */ | |
| 459 | public void addCaretPositionListener( | |
| 460 | final ChangeListener<? super Integer> listener ) { | |
| 461 | getEditorPane().addCaretPositionListener( listener ); | |
| 462 | } | |
| 463 | ||
| 464 | public <T extends Event> void addEventFilter( | |
| 465 | final EventType<T> eventType, | |
| 466 | final EventHandler<? super T> eventFilter ) { | |
| 467 | getEditor().addEventFilter( eventType, eventFilter ); | |
| 468 | } | |
| 469 | ||
| 470 | /** | |
| 471 | * Forwards the request to the editor pane. | |
| 472 | * | |
| 473 | * @return The text to process. | |
| 474 | */ | |
| 475 | public String getEditorText() { | |
| 476 | return getEditorPane().getText(); | |
| 477 | } | |
| 478 | ||
| 479 | /** | |
| 480 | * Returns the editor pane, or creates one if it doesn't yet exist. | |
| 481 | * | |
| 482 | * @return The editor pane, never null. | |
| 483 | */ | |
| 484 | @NotNull | |
| 485 | public MarkdownEditorPane getEditorPane() { | |
| 486 | return mEditorPane; | |
| 487 | } | |
| 488 | ||
| 489 | /** | |
| 490 | * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been | |
| 491 | * determined. | |
| 492 | * | |
| 493 | * @return The file encoding or UTF-8 if unknown. | |
| 494 | */ | |
| 495 | private Charset getEncoding() { | |
| 496 | return mEncoding; | |
| 497 | } | |
| 498 | ||
| 499 | private void setEncoding( final Charset encoding ) { | |
| 500 | assert encoding != null; | |
| 501 | mEncoding = encoding; | |
| 502 | } | |
| 503 | ||
| 504 | /** | |
| 505 | * Returns the tab title, without any modified indicators. | |
| 506 | * | |
| 507 | * @return The tab title. | |
| 508 | */ | |
| 509 | @Override | |
| 510 | public String toString() { | |
| 511 | return getTabTitle(); | |
| 512 | } | |
| 513 | } | |
| 514 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.service.Options; | |
| 31 | import com.keenwrite.service.Settings; | |
| 32 | import com.keenwrite.service.events.Notification; | |
| 33 | import com.keenwrite.service.events.Notifier; | |
| 34 | import com.keenwrite.util.Utils; | |
| 35 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 36 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 37 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 38 | import javafx.beans.property.ReadOnlyObjectWrapper; | |
| 39 | import javafx.beans.value.ChangeListener; | |
| 40 | import javafx.collections.ListChangeListener; | |
| 41 | import javafx.collections.ObservableList; | |
| 42 | import javafx.event.Event; | |
| 43 | import javafx.scene.control.Alert; | |
| 44 | import javafx.scene.control.ButtonType; | |
| 45 | import javafx.scene.control.Tab; | |
| 46 | import javafx.scene.control.TabPane; | |
| 47 | import javafx.stage.FileChooser; | |
| 48 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 49 | import javafx.stage.Window; | |
| 50 | ||
| 51 | import java.io.File; | |
| 52 | import java.nio.file.Path; | |
| 53 | import java.util.ArrayList; | |
| 54 | import java.util.List; | |
| 55 | import java.util.Optional; | |
| 56 | import java.util.concurrent.atomic.AtomicReference; | |
| 57 | import java.util.prefs.Preferences; | |
| 58 | import java.util.stream.Collectors; | |
| 59 | ||
| 60 | import static com.keenwrite.Constants.GLOB_PREFIX_FILE; | |
| 61 | import static com.keenwrite.Constants.SETTINGS; | |
| 62 | import static com.keenwrite.FileType.*; | |
| 63 | import static com.keenwrite.Messages.get; | |
| 64 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; | |
| 65 | import static com.keenwrite.service.events.Notifier.YES; | |
| 66 | ||
| 67 | /** | |
| 68 | * Tab pane for file editors. | |
| 69 | */ | |
| 70 | public final class FileEditorTabPane extends TabPane { | |
| 71 | ||
| 72 | private static final String FILTER_EXTENSION_TITLES = | |
| 73 | "Dialog.file.choose.filter"; | |
| 74 | ||
| 75 | private static final Options sOptions = Services.load( Options.class ); | |
| 76 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 77 | ||
| 78 | private final ReadOnlyObjectWrapper<Path> mOpenDefinition = | |
| 79 | new ReadOnlyObjectWrapper<>(); | |
| 80 | private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor = | |
| 81 | new ReadOnlyObjectWrapper<>(); | |
| 82 | private final ReadOnlyBooleanWrapper mAnyFileEditorModified = | |
| 83 | new ReadOnlyBooleanWrapper(); | |
| 84 | private final ChangeListener<Integer> mCaretPositionListener; | |
| 85 | ||
| 86 | /** | |
| 87 | * Constructs a new file editor tab pane. | |
| 88 | * | |
| 89 | * @param caretPositionListener Listens for changes to caret position so | |
| 90 | * that the status bar can update. | |
| 91 | */ | |
| 92 | public FileEditorTabPane( | |
| 93 | final ChangeListener<Integer> caretPositionListener ) { | |
| 94 | final ObservableList<Tab> tabs = getTabs(); | |
| 95 | ||
| 96 | setFocusTraversable( false ); | |
| 97 | setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | |
| 98 | ||
| 99 | addTabSelectionListener( | |
| 100 | ( tabPane, oldTab, newTab ) -> { | |
| 101 | if( newTab != null ) { | |
| 102 | mActiveFileEditor.set( (FileEditorTab) newTab ); | |
| 103 | } | |
| 104 | } | |
| 105 | ); | |
| 106 | ||
| 107 | final ChangeListener<Boolean> modifiedListener = | |
| 108 | ( observable, oldValue, newValue ) -> { | |
| 109 | for( final Tab tab : tabs ) { | |
| 110 | if( ((FileEditorTab) tab).isModified() ) { | |
| 111 | mAnyFileEditorModified.set( true ); | |
| 112 | break; | |
| 113 | } | |
| 114 | } | |
| 115 | }; | |
| 116 | ||
| 117 | tabs.addListener( | |
| 118 | (ListChangeListener<Tab>) change -> { | |
| 119 | while( change.next() ) { | |
| 120 | if( change.wasAdded() ) { | |
| 121 | change.getAddedSubList().forEach( | |
| 122 | ( tab ) -> { | |
| 123 | final var fet = (FileEditorTab) tab; | |
| 124 | fet.modifiedProperty().addListener( modifiedListener ); | |
| 125 | } ); | |
| 126 | } | |
| 127 | else if( change.wasRemoved() ) { | |
| 128 | change.getRemoved().forEach( | |
| 129 | ( tab ) -> { | |
| 130 | final var fet = (FileEditorTab) tab; | |
| 131 | fet.modifiedProperty().removeListener( modifiedListener ); | |
| 132 | } | |
| 133 | ); | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | // Changes in the tabs may also change anyFileEditorModified property | |
| 138 | // (e.g. closed modified file) | |
| 139 | modifiedListener.changed( null, null, null ); | |
| 140 | } | |
| 141 | ); | |
| 142 | ||
| 143 | mCaretPositionListener = caretPositionListener; | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Allows observers to be notified when the current file editor tab changes. | |
| 148 | * | |
| 149 | * @param listener The listener to notify of tab change events. | |
| 150 | */ | |
| 151 | public void addTabSelectionListener( final ChangeListener<Tab> listener ) { | |
| 152 | // Observe the tab so that when a new tab is opened or selected, | |
| 153 | // a notification is kicked off. | |
| 154 | getSelectionModel().selectedItemProperty().addListener( listener ); | |
| 155 | } | |
| 156 | ||
| 157 | /** | |
| 158 | * Returns the tab that has keyboard focus. | |
| 159 | * | |
| 160 | * @return A non-null instance. | |
| 161 | */ | |
| 162 | public FileEditorTab getActiveFileEditor() { | |
| 163 | return mActiveFileEditor.get(); | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns the property corresponding to the tab that has focus. | |
| 168 | * | |
| 169 | * @return A non-null instance. | |
| 170 | */ | |
| 171 | public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | |
| 172 | return mActiveFileEditor.getReadOnlyProperty(); | |
| 173 | } | |
| 174 | ||
| 175 | /** | |
| 176 | * Property that can answer whether the text has been modified. | |
| 177 | * | |
| 178 | * @return A non-null instance, true meaning the content has not been saved. | |
| 179 | */ | |
| 180 | ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | |
| 181 | return mAnyFileEditorModified.getReadOnlyProperty(); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Creates a new editor instance from the given path. | |
| 186 | * | |
| 187 | * @param path The file to open. | |
| 188 | * @return A non-null instance. | |
| 189 | */ | |
| 190 | private FileEditorTab createFileEditor( final Path path ) { | |
| 191 | assert path != null; | |
| 192 | ||
| 193 | final FileEditorTab tab = new FileEditorTab( path ); | |
| 194 | ||
| 195 | tab.setOnCloseRequest( e -> { | |
| 196 | if( !canCloseEditor( tab ) ) { | |
| 197 | e.consume(); | |
| 198 | } | |
| 199 | else if( isActiveFileEditor( tab ) ) { | |
| 200 | // Prevent prompting the user to save when there are no file editor | |
| 201 | // tabs open. | |
| 202 | mActiveFileEditor.set( null ); | |
| 203 | } | |
| 204 | } ); | |
| 205 | ||
| 206 | tab.addCaretPositionListener( mCaretPositionListener ); | |
| 207 | ||
| 208 | return tab; | |
| 209 | } | |
| 210 | ||
| 211 | private boolean isActiveFileEditor( final FileEditorTab tab ) { | |
| 212 | return getActiveFileEditor() == tab; | |
| 213 | } | |
| 214 | ||
| 215 | private Path getDefaultPath() { | |
| 216 | final String filename = getDefaultFilename(); | |
| 217 | return (new File( filename )).toPath(); | |
| 218 | } | |
| 219 | ||
| 220 | private String getDefaultFilename() { | |
| 221 | return getSettings().getSetting( "file.default", "untitled.md" ); | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Called to add a new {@link FileEditorTab} to the tab pane. | |
| 226 | */ | |
| 227 | void newEditor() { | |
| 228 | final FileEditorTab tab = createFileEditor( getDefaultPath() ); | |
| 229 | ||
| 230 | getTabs().add( tab ); | |
| 231 | getSelectionModel().select( tab ); | |
| 232 | } | |
| 233 | ||
| 234 | void openFileDialog() { | |
| 235 | final FileChooser dialog = createFileChooser( | |
| 236 | "Dialog.file.choose.open.title" ); | |
| 237 | final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | |
| 238 | ||
| 239 | if( files != null ) { | |
| 240 | openFiles( files ); | |
| 241 | } | |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Opens the files into new editors, unless one of those files was a | |
| 246 | * definition file. The definition file is loaded into the definition pane, | |
| 247 | * but only the first one selected (multiple definition files will result in a | |
| 248 | * warning). | |
| 249 | * | |
| 250 | * @param files The list of non-definition files that the were requested to | |
| 251 | * open. | |
| 252 | */ | |
| 253 | private void openFiles( final List<File> files ) { | |
| 254 | final List<String> extensions = | |
| 255 | createExtensionFilter( DEFINITION ).getExtensions(); | |
| 256 | final var predicate = createFileTypePredicate( extensions ); | |
| 257 | ||
| 258 | // The user might have opened multiple definitions files. These will | |
| 259 | // be discarded from the text editable files. | |
| 260 | final var definitions | |
| 261 | = files.stream().filter( predicate ).collect( Collectors.toList() ); | |
| 262 | ||
| 263 | // Create a modifiable list to remove any definition files that were | |
| 264 | // opened. | |
| 265 | final var editors = new ArrayList<>( files ); | |
| 266 | ||
| 267 | if( !editors.isEmpty() ) { | |
| 268 | saveLastDirectory( editors.get( 0 ) ); | |
| 269 | } | |
| 270 | ||
| 271 | editors.removeAll( definitions ); | |
| 272 | ||
| 273 | // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | |
| 274 | if( !editors.isEmpty() ) { | |
| 275 | openEditors( editors, 0 ); | |
| 276 | } | |
| 277 | ||
| 278 | if( !definitions.isEmpty() ) { | |
| 279 | openDefinition( definitions.get( 0 ) ); | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | private void openEditors( final List<File> files, final int activeIndex ) { | |
| 284 | final int fileTally = files.size(); | |
| 285 | final List<Tab> tabs = getTabs(); | |
| 286 | ||
| 287 | // Close single unmodified "Untitled" tab. | |
| 288 | if( tabs.size() == 1 ) { | |
| 289 | final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 )); | |
| 290 | ||
| 291 | if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | |
| 292 | closeEditor( fileEditor, false ); | |
| 293 | } | |
| 294 | } | |
| 295 | ||
| 296 | for( int i = 0; i < fileTally; i++ ) { | |
| 297 | final Path path = files.get( i ).toPath(); | |
| 298 | ||
| 299 | FileEditorTab fileEditorTab = findEditor( path ); | |
| 300 | ||
| 301 | // Only open new files. | |
| 302 | if( fileEditorTab == null ) { | |
| 303 | fileEditorTab = createFileEditor( path ); | |
| 304 | getTabs().add( fileEditorTab ); | |
| 305 | } | |
| 306 | ||
| 307 | // Select the first file in the list. | |
| 308 | if( i == activeIndex ) { | |
| 309 | getSelectionModel().select( fileEditorTab ); | |
| 310 | } | |
| 311 | } | |
| 312 | } | |
| 313 | ||
| 314 | /** | |
| 315 | * Returns a property that changes when a new definition file is opened. | |
| 316 | * | |
| 317 | * @return The path to a definition file that was opened. | |
| 318 | */ | |
| 319 | public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | |
| 320 | return getOnOpenDefinitionFile().getReadOnlyProperty(); | |
| 321 | } | |
| 322 | ||
| 323 | private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | |
| 324 | return mOpenDefinition; | |
| 325 | } | |
| 326 | ||
| 327 | /** | |
| 328 | * Called when the user has opened a definition file (using the file open | |
| 329 | * dialog box). This will replace the current set of definitions for the | |
| 330 | * active tab. | |
| 331 | * | |
| 332 | * @param definition The file to open. | |
| 333 | */ | |
| 334 | private void openDefinition( final File definition ) { | |
| 335 | // TODO: Prevent reading this file twice when a new text document is opened. | |
| 336 | // (might be a matter of checking the value first). | |
| 337 | getOnOpenDefinitionFile().set( definition.toPath() ); | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Called when the contents of the editor are to be saved. | |
| 342 | * | |
| 343 | * @param tab The tab containing content to save. | |
| 344 | * @return true The contents were saved (or needn't be saved). | |
| 345 | */ | |
| 346 | public boolean saveEditor( final FileEditorTab tab ) { | |
| 347 | if( tab == null || !tab.isModified() ) { | |
| 348 | return true; | |
| 349 | } | |
| 350 | ||
| 351 | return tab.getPath() == null ? saveEditorAs( tab ) : tab.save(); | |
| 352 | } | |
| 353 | ||
| 354 | /** | |
| 355 | * Opens the Save As dialog for the user to save the content under a new | |
| 356 | * path. | |
| 357 | * | |
| 358 | * @param tab The tab with contents to save. | |
| 359 | * @return true The contents were saved, or the tab was null. | |
| 360 | */ | |
| 361 | public boolean saveEditorAs( final FileEditorTab tab ) { | |
| 362 | if( tab == null ) { | |
| 363 | return true; | |
| 364 | } | |
| 365 | ||
| 366 | getSelectionModel().select( tab ); | |
| 367 | ||
| 368 | final FileChooser chooser = createFileChooser( | |
| 369 | "Dialog.file.choose.save.title" ); | |
| 370 | final File file = chooser.showSaveDialog( getWindow() ); | |
| 371 | if( file == null ) { | |
| 372 | return false; | |
| 373 | } | |
| 374 | ||
| 375 | saveLastDirectory( file ); | |
| 376 | tab.setPath( file.toPath() ); | |
| 377 | ||
| 378 | return tab.save(); | |
| 379 | } | |
| 380 | ||
| 381 | void saveAllEditors() { | |
| 382 | for( final FileEditorTab fileEditor : getAllEditors() ) { | |
| 383 | saveEditor( fileEditor ); | |
| 384 | } | |
| 385 | } | |
| 386 | ||
| 387 | /** | |
| 388 | * Answers whether the file has had modifications. | |
| 389 | * | |
| 390 | * @param tab THe tab to check for modifications. | |
| 391 | * @return false The file is unmodified. | |
| 392 | */ | |
| 393 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") | |
| 394 | boolean canCloseEditor( final FileEditorTab tab ) { | |
| 395 | final AtomicReference<Boolean> canClose = new AtomicReference<>(); | |
| 396 | canClose.set( true ); | |
| 397 | ||
| 398 | if( tab.isModified() ) { | |
| 399 | final Notification message = getNotifyService().createNotification( | |
| 400 | Messages.get( "Alert.file.close.title" ), | |
| 401 | Messages.get( "Alert.file.close.text" ), | |
| 402 | tab.getText() | |
| 403 | ); | |
| 404 | ||
| 405 | final Alert confirmSave = getNotifyService().createConfirmation( | |
| 406 | getWindow(), message ); | |
| 407 | ||
| 408 | final Optional<ButtonType> buttonType = confirmSave.showAndWait(); | |
| 409 | ||
| 410 | buttonType.ifPresent( | |
| 411 | save -> canClose.set( | |
| 412 | save == YES ? saveEditor( tab ) : save == ButtonType.NO | |
| 413 | ) | |
| 414 | ); | |
| 415 | } | |
| 416 | ||
| 417 | return canClose.get(); | |
| 418 | } | |
| 419 | ||
| 420 | boolean closeEditor( final FileEditorTab tab, final boolean save ) { | |
| 421 | if( tab == null ) { | |
| 422 | return true; | |
| 423 | } | |
| 424 | ||
| 425 | if( save ) { | |
| 426 | Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | |
| 427 | Event.fireEvent( tab, event ); | |
| 428 | ||
| 429 | if( event.isConsumed() ) { | |
| 430 | return false; | |
| 431 | } | |
| 432 | } | |
| 433 | ||
| 434 | getTabs().remove( tab ); | |
| 435 | ||
| 436 | if( tab.getOnClosed() != null ) { | |
| 437 | Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | |
| 438 | } | |
| 439 | ||
| 440 | return true; | |
| 441 | } | |
| 442 | ||
| 443 | boolean closeAllEditors() { | |
| 444 | final FileEditorTab[] allEditors = getAllEditors(); | |
| 445 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 446 | ||
| 447 | // try to save active tab first because in case the user decides to cancel, | |
| 448 | // then it stays active | |
| 449 | if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | |
| 450 | return false; | |
| 451 | } | |
| 452 | ||
| 453 | // This should be called any time a tab changes. | |
| 454 | persistPreferences(); | |
| 455 | ||
| 456 | // save modified tabs | |
| 457 | for( int i = 0; i < allEditors.length; i++ ) { | |
| 458 | final FileEditorTab fileEditor = allEditors[ i ]; | |
| 459 | ||
| 460 | if( fileEditor == activeEditor ) { | |
| 461 | continue; | |
| 462 | } | |
| 463 | ||
| 464 | if( fileEditor.isModified() ) { | |
| 465 | // activate the modified tab to make its modified content visible to | |
| 466 | // the user | |
| 467 | getSelectionModel().select( i ); | |
| 468 | ||
| 469 | if( !canCloseEditor( fileEditor ) ) { | |
| 470 | return false; | |
| 471 | } | |
| 472 | } | |
| 473 | } | |
| 474 | ||
| 475 | // Close all tabs. | |
| 476 | for( final FileEditorTab fileEditor : allEditors ) { | |
| 477 | if( !closeEditor( fileEditor, false ) ) { | |
| 478 | return false; | |
| 479 | } | |
| 480 | } | |
| 481 | ||
| 482 | return getTabs().isEmpty(); | |
| 483 | } | |
| 484 | ||
| 485 | private FileEditorTab[] getAllEditors() { | |
| 486 | final ObservableList<Tab> tabs = getTabs(); | |
| 487 | final int length = tabs.size(); | |
| 488 | final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | |
| 489 | ||
| 490 | for( int i = 0; i < length; i++ ) { | |
| 491 | allEditors[ i ] = (FileEditorTab) tabs.get( i ); | |
| 492 | } | |
| 493 | ||
| 494 | return allEditors; | |
| 495 | } | |
| 496 | ||
| 497 | /** | |
| 498 | * Returns the file editor tab that has the given path. | |
| 499 | * | |
| 500 | * @return null No file editor tab for the given path was found. | |
| 501 | */ | |
| 502 | private FileEditorTab findEditor( final Path path ) { | |
| 503 | for( final Tab tab : getTabs() ) { | |
| 504 | final FileEditorTab fileEditor = (FileEditorTab) tab; | |
| 505 | ||
| 506 | if( fileEditor.isPath( path ) ) { | |
| 507 | return fileEditor; | |
| 508 | } | |
| 509 | } | |
| 510 | ||
| 511 | return null; | |
| 512 | } | |
| 513 | ||
| 514 | /** | |
| 515 | * Opens a new {@link FileChooser} at the previously selected directory. | |
| 516 | * | |
| 517 | * @param key Message key from resource bundle. | |
| 518 | * @return {@link FileChooser} GUI allowing the user to pick a file. | |
| 519 | */ | |
| 520 | private FileChooser createFileChooser( final String key ) { | |
| 521 | final FileChooser fileChooser = new FileChooser(); | |
| 522 | ||
| 523 | fileChooser.setTitle( get( key ) ); | |
| 524 | fileChooser.getExtensionFilters().addAll( | |
| 525 | createExtensionFilters() ); | |
| 526 | ||
| 527 | final String lastDirectory = getPreferences().get( "lastDirectory", null ); | |
| 528 | File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | |
| 529 | ||
| 530 | if( !file.isDirectory() ) { | |
| 531 | file = new File( "." ); | |
| 532 | } | |
| 533 | ||
| 534 | fileChooser.setInitialDirectory( file ); | |
| 535 | return fileChooser; | |
| 536 | } | |
| 537 | ||
| 538 | private List<ExtensionFilter> createExtensionFilters() { | |
| 539 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 540 | ||
| 541 | // TODO: Return a list of all properties that match the filter prefix. | |
| 542 | // This will allow dynamic filters to be added and removed just by | |
| 543 | // updating the properties file. | |
| 544 | list.add( createExtensionFilter( ALL ) ); | |
| 545 | list.add( createExtensionFilter( SOURCE ) ); | |
| 546 | list.add( createExtensionFilter( DEFINITION ) ); | |
| 547 | list.add( createExtensionFilter( XML ) ); | |
| 548 | return list; | |
| 549 | } | |
| 550 | ||
| 551 | /** | |
| 552 | * Returns a filter for file name extensions recognized by the application | |
| 553 | * that can be opened by the user. | |
| 554 | * | |
| 555 | * @param filetype Used to find the globbing pattern for extensions. | |
| 556 | * @return A filename filter suitable for use by a FileDialog instance. | |
| 557 | */ | |
| 558 | private ExtensionFilter createExtensionFilter( final FileType filetype ) { | |
| 559 | final String tKey = String.format( "%s.title.%s", | |
| 560 | FILTER_EXTENSION_TITLES, | |
| 561 | filetype ); | |
| 562 | final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype ); | |
| 563 | ||
| 564 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 565 | } | |
| 566 | ||
| 567 | private void saveLastDirectory( final File file ) { | |
| 568 | getPreferences().put( "lastDirectory", file.getParent() ); | |
| 569 | } | |
| 570 | ||
| 571 | public void initPreferences() { | |
| 572 | int activeIndex = 0; | |
| 573 | ||
| 574 | final Preferences preferences = getPreferences(); | |
| 575 | final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | |
| 576 | final String activeFileName = preferences.get( "activeFile", null ); | |
| 577 | ||
| 578 | final List<File> files = new ArrayList<>( fileNames.length ); | |
| 579 | ||
| 580 | for( final String fileName : fileNames ) { | |
| 581 | final File file = new File( fileName ); | |
| 582 | ||
| 583 | if( file.exists() ) { | |
| 584 | files.add( file ); | |
| 585 | ||
| 586 | if( fileName.equals( activeFileName ) ) { | |
| 587 | activeIndex = files.size() - 1; | |
| 588 | } | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | if( files.isEmpty() ) { | |
| 593 | newEditor(); | |
| 594 | } | |
| 595 | else { | |
| 596 | openEditors( files, activeIndex ); | |
| 597 | } | |
| 598 | } | |
| 599 | ||
| 600 | public void persistPreferences() { | |
| 601 | final var allEditors = getTabs(); | |
| 602 | final List<String> fileNames = new ArrayList<>( allEditors.size() ); | |
| 603 | ||
| 604 | for( final var tab : allEditors ) { | |
| 605 | final var fileEditor = (FileEditorTab) tab; | |
| 606 | final var filePath = fileEditor.getPath(); | |
| 607 | ||
| 608 | if( filePath != null ) { | |
| 609 | fileNames.add( filePath.toString() ); | |
| 610 | } | |
| 611 | } | |
| 612 | ||
| 613 | final var preferences = getPreferences(); | |
| 614 | Utils.putPrefsStrings( preferences, | |
| 615 | "file", | |
| 616 | fileNames.toArray( new String[ 0 ] ) ); | |
| 617 | ||
| 618 | final var activeEditor = getActiveFileEditor(); | |
| 619 | final var filePath = activeEditor == null ? null : activeEditor.getPath(); | |
| 620 | ||
| 621 | if( filePath == null ) { | |
| 622 | preferences.remove( "activeFile" ); | |
| 623 | } | |
| 624 | else { | |
| 625 | preferences.put( "activeFile", filePath.toString() ); | |
| 626 | } | |
| 627 | } | |
| 628 | ||
| 629 | private List<String> getExtensions( final String key ) { | |
| 630 | return getSettings().getStringSettingList( key ); | |
| 631 | } | |
| 632 | ||
| 633 | private Notifier getNotifyService() { | |
| 634 | return sNotifier; | |
| 635 | } | |
| 636 | ||
| 637 | private Settings getSettings() { | |
| 638 | return SETTINGS; | |
| 639 | } | |
| 640 | ||
| 641 | private Window getWindow() { | |
| 642 | return getScene().getWindow(); | |
| 643 | } | |
| 644 | ||
| 645 | private Preferences getPreferences() { | |
| 646 | return sOptions.getState(); | |
| 647 | } | |
| 648 | } | |
| 649 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents different file type classifications. These are high-level mappings | |
| 32 | * that correspond to the list of glob patterns found within {@code | |
| 33 | * settings.properties}. | |
| 34 | */ | |
| 35 | public enum FileType { | |
| 36 | ||
| 37 | ALL( "all" ), | |
| 38 | RMARKDOWN( "rmarkdown" ), | |
| 39 | RXML( "rxml" ), | |
| 40 | SOURCE( "source" ), | |
| 41 | DEFINITION( "definition" ), | |
| 42 | XML( "xml" ), | |
| 43 | CSV( "csv" ), | |
| 44 | JSON( "json" ), | |
| 45 | TOML( "toml" ), | |
| 46 | YAML( "yaml" ), | |
| 47 | PROPERTIES( "properties" ), | |
| 48 | UNKNOWN( "unknown" ); | |
| 49 | ||
| 50 | private final String mType; | |
| 51 | ||
| 52 | /** | |
| 53 | * Default constructor for enumerated file type. | |
| 54 | * | |
| 55 | * @param type Human-readable name for the file type. | |
| 56 | */ | |
| 57 | FileType( final String type ) { | |
| 58 | mType = type; | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the file type that corresponds to the given string. | |
| 63 | * | |
| 64 | * @param type The string to compare against this enumeration of file types. | |
| 65 | * @return The corresponding File Type for the given string. | |
| 66 | * @throws IllegalArgumentException Type not found. | |
| 67 | */ | |
| 68 | public static FileType from( final String type ) { | |
| 69 | for( final FileType fileType : FileType.values() ) { | |
| 70 | if( fileType.isType( type ) ) { | |
| 71 | return fileType; | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | throw new IllegalArgumentException( type ); | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Answers whether this file type matches the given string, case insensitive | |
| 80 | * comparison. | |
| 81 | * | |
| 82 | * @param type Presumably a file name extension to check against. | |
| 83 | * @return true The given extension corresponds to this enumerated type. | |
| 84 | */ | |
| 85 | public boolean isType( final String type ) { | |
| 86 | return getType().equalsIgnoreCase( type ); | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Answers whether this file type belongs to the set of file types that have | |
| 91 | * embedded R statements. | |
| 92 | * | |
| 93 | * @return {@code true} when the file type is either R Markdown or R XML. | |
| 94 | */ | |
| 95 | public boolean isR() { | |
| 96 | return this == RMARKDOWN || this == RXML; | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * Returns the human-readable name for the file type. | |
| 101 | * | |
| 102 | * @return A non-null instance. | |
| 103 | */ | |
| 104 | private String getType() { | |
| 105 | return mType; | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Returns the lowercase version of the file name extension. | |
| 110 | * | |
| 111 | * @return The file name, in lower case. | |
| 112 | */ | |
| 113 | @Override | |
| 114 | public String toString() { | |
| 115 | return getType(); | |
| 116 | } | |
| 117 | } | |
| 118 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| ... | ||
| 37 | 11 | |
| 38 | 12 | /** |
| 39 | * Launches the application using the {@link Main} class. | |
| 13 | * Launches the application using the {@link MainApp} class. | |
| 40 | 14 | * |
| 41 | 15 | * <p> |
| ... | ||
| 50 | 24 | * @param args Command-line arguments. |
| 51 | 25 | */ |
| 52 | public static void main( final String[] args ) throws IOException { | |
| 26 | public static void main( final String[] args ) { | |
| 53 | 27 | showAppInfo(); |
| 54 | Main.main( args ); | |
| 28 | MainApp.main( args ); | |
| 55 | 29 | } |
| 56 | 30 | |
| 57 | 31 | @SuppressWarnings("RedundantStringFormatCall") |
| 58 | private static void showAppInfo() throws IOException { | |
| 32 | private static void showAppInfo() { | |
| 59 | 33 | out( format( "%s version %s", APP_TITLE, getVersion() ) ); |
| 60 | out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) ); | |
| 61 | out( format( "Portions copyright 2020 Karl Tauber." ) ); | |
| 34 | out( format( "Copyright 2016-%s White Magic Software, Ltd.", getYear() ) ); | |
| 35 | out( format( "Portions copyright 2015-2020 Karl Tauber." ) ); | |
| 62 | 36 | } |
| 63 | 37 | |
| 64 | 38 | private static void out( final String s ) { |
| 65 | 39 | System.out.println( s ); |
| 66 | 40 | } |
| 67 | 41 | |
| 68 | private static String getVersion() throws IOException { | |
| 69 | final Properties properties = loadProperties( "app.properties" ); | |
| 70 | return properties.getProperty( "application.version" ); | |
| 42 | /** | |
| 43 | * Returns the application version number retrieved from the application | |
| 44 | * properties file. The properties file is generated at build time, which | |
| 45 | * keys off the repository. | |
| 46 | * | |
| 47 | * @return The application version number. | |
| 48 | * @throws RuntimeException An {@link IOException} occurred. | |
| 49 | */ | |
| 50 | public static String getVersion() { | |
| 51 | try { | |
| 52 | final var properties = loadProperties( "app.properties" ); | |
| 53 | return properties.getProperty( "application.version" ); | |
| 54 | } catch( final Exception ex ) { | |
| 55 | throw new RuntimeException( ex ); | |
| 56 | } | |
| 71 | 57 | } |
| 72 | 58 | |
| 73 | 59 | private static String getYear() { |
| 74 | 60 | return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) ); |
| 75 | 61 | } |
| 76 | 62 | |
| 77 | 63 | @SuppressWarnings("SameParameterValue") |
| 78 | 64 | private static Properties loadProperties( final String resource ) |
| 79 | throws IOException { | |
| 80 | final Properties properties = new Properties(); | |
| 65 | throws IOException { | |
| 66 | final var properties = new Properties(); | |
| 81 | 67 | properties.load( getResourceAsStream( getResourceName( resource ) ) ); |
| 82 | 68 | return properties; |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.FilePreferencesFactory; | |
| 31 | import com.keenwrite.service.Options; | |
| 32 | import com.keenwrite.service.Snitch; | |
| 33 | import com.keenwrite.util.ResourceWalker; | |
| 34 | import com.keenwrite.util.StageState; | |
| 35 | import javafx.application.Application; | |
| 36 | import javafx.scene.Scene; | |
| 37 | import javafx.scene.image.Image; | |
| 38 | import javafx.stage.Stage; | |
| 39 | ||
| 40 | import java.awt.*; | |
| 41 | import java.io.FileInputStream; | |
| 42 | import java.io.IOException; | |
| 43 | import java.io.InputStream; | |
| 44 | import java.net.URI; | |
| 45 | import java.util.Map; | |
| 46 | import java.util.logging.LogManager; | |
| 47 | ||
| 48 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 49 | import static com.keenwrite.Constants.*; | |
| 50 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 51 | import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment; | |
| 52 | import static java.awt.font.TextAttribute.*; | |
| 53 | import static javafx.scene.input.KeyCode.F11; | |
| 54 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 55 | ||
| 56 | /** | |
| 57 | * Application entry point. The application allows users to edit Markdown | |
| 58 | * files and see a real-time preview of the edits. | |
| 59 | */ | |
| 60 | public final class Main extends Application { | |
| 61 | ||
| 62 | static { | |
| 63 | // Suppress logging to standard output. | |
| 64 | LogManager.getLogManager().reset(); | |
| 65 | ||
| 66 | // Suppress logging to standard error. | |
| 67 | System.err.close(); | |
| 68 | } | |
| 69 | ||
| 70 | private final Options mOptions = Services.load( Options.class ); | |
| 71 | private final Snitch mSnitch = Services.load( Snitch.class ); | |
| 72 | ||
| 73 | private final Thread mSnitchThread = new Thread( getSnitch() ); | |
| 74 | private final MainWindow mMainWindow = new MainWindow(); | |
| 75 | ||
| 76 | @SuppressWarnings({"FieldCanBeLocal"}) | |
| 77 | private StageState mStageState; | |
| 78 | ||
| 79 | /** | |
| 80 | * Application entry point. | |
| 81 | * | |
| 82 | * @param args Command-line arguments. | |
| 83 | */ | |
| 84 | public static void main( final String[] args ) { | |
| 85 | initPreferences(); | |
| 86 | initFonts(); | |
| 87 | launch( args ); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * JavaFX entry point. | |
| 92 | * | |
| 93 | * @param stage The primary application stage. | |
| 94 | */ | |
| 95 | @Override | |
| 96 | public void start( final Stage stage ) { | |
| 97 | initState( stage ); | |
| 98 | initStage( stage ); | |
| 99 | initSnitch(); | |
| 100 | ||
| 101 | stage.show(); | |
| 102 | ||
| 103 | // After the stage is visible, the panel dimensions are | |
| 104 | // known, which allows scaling images to fit the preview panel. | |
| 105 | getMainWindow().init(); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * This needs to run before the windowing system kicks in, otherwise the | |
| 110 | * fonts will not be found. | |
| 111 | */ | |
| 112 | @SuppressWarnings({"rawtypes", "unchecked"}) | |
| 113 | public static void initFonts() { | |
| 114 | final var ge = getLocalGraphicsEnvironment(); | |
| 115 | ||
| 116 | try { | |
| 117 | ResourceWalker.walk( | |
| 118 | FONT_DIRECTORY, path -> { | |
| 119 | final var uri = path.toUri(); | |
| 120 | final var filename = path.toString(); | |
| 121 | ||
| 122 | try( final var is = openFont( uri, filename ) ) { | |
| 123 | final var font = Font.createFont( Font.TRUETYPE_FONT, is ); | |
| 124 | final Map attributes = font.getAttributes(); | |
| 125 | ||
| 126 | attributes.put( LIGATURES, LIGATURES_ON ); | |
| 127 | attributes.put( KERNING, KERNING_ON ); | |
| 128 | ge.registerFont( font.deriveFont( attributes ) ); | |
| 129 | } catch( final Exception e ) { | |
| 130 | clue( e ); | |
| 131 | } | |
| 132 | } | |
| 133 | ); | |
| 134 | } catch( final Exception e ) { | |
| 135 | clue( e ); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | private static InputStream openFont( final URI uri, final String filename ) | |
| 140 | throws IOException { | |
| 141 | return uri.getScheme().equals( "jar" ) | |
| 142 | ? Main.class.getResourceAsStream( filename ) | |
| 143 | : new FileInputStream( filename ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Sets the factory used for reading user preferences. | |
| 148 | */ | |
| 149 | private static void initPreferences() { | |
| 150 | System.setProperty( | |
| 151 | "java.util.prefs.PreferencesFactory", | |
| 152 | FilePreferencesFactory.class.getName() | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | private void initState( final Stage stage ) { | |
| 157 | mStageState = new StageState( stage, getOptions().getState() ); | |
| 158 | } | |
| 159 | ||
| 160 | private void initStage( final Stage stage ) { | |
| 161 | stage.getIcons().addAll( | |
| 162 | createImage( FILE_LOGO_16 ), | |
| 163 | createImage( FILE_LOGO_32 ), | |
| 164 | createImage( FILE_LOGO_128 ), | |
| 165 | createImage( FILE_LOGO_256 ), | |
| 166 | createImage( FILE_LOGO_512 ) ); | |
| 167 | stage.setTitle( APP_TITLE ); | |
| 168 | stage.setScene( getScene() ); | |
| 169 | ||
| 170 | stage.addEventHandler( KEY_PRESSED, event -> { | |
| 171 | if( F11.equals( event.getCode() ) ) { | |
| 172 | stage.setFullScreen( !stage.isFullScreen() ); | |
| 173 | } | |
| 174 | } ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Watch for file system changes. | |
| 179 | */ | |
| 180 | private void initSnitch() { | |
| 181 | getSnitchThread().start(); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Stops the snitch service, if its running. | |
| 186 | * | |
| 187 | * @throws InterruptedException Couldn't stop the snitch thread. | |
| 188 | */ | |
| 189 | @Override | |
| 190 | public void stop() throws InterruptedException { | |
| 191 | getSnitch().stop(); | |
| 192 | ||
| 193 | final Thread thread = getSnitchThread(); | |
| 194 | thread.interrupt(); | |
| 195 | thread.join(); | |
| 196 | } | |
| 197 | ||
| 198 | private Snitch getSnitch() { | |
| 199 | return mSnitch; | |
| 200 | } | |
| 201 | ||
| 202 | private Thread getSnitchThread() { | |
| 203 | return mSnitchThread; | |
| 204 | } | |
| 205 | ||
| 206 | private Options getOptions() { | |
| 207 | return mOptions; | |
| 208 | } | |
| 209 | ||
| 210 | private MainWindow getMainWindow() { | |
| 211 | return mMainWindow; | |
| 212 | } | |
| 213 | ||
| 214 | private Scene getScene() { | |
| 215 | return getMainWindow().getScene(); | |
| 216 | } | |
| 217 | ||
| 218 | private Image createImage( final String filename ) { | |
| 219 | return new Image( filename ); | |
| 220 | } | |
| 221 | ||
| 222 | /** | |
| 223 | * This is here to suppress an IDE warning, the method is not used. | |
| 224 | */ | |
| 225 | public StageState getStageState() { | |
| 226 | return mStageState; | |
| 227 | } | |
| 228 | } | |
| 229 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.service.Snitch; | |
| 6 | import javafx.application.Application; | |
| 7 | import javafx.application.Platform; | |
| 8 | import javafx.stage.Stage; | |
| 9 | ||
| 10 | import java.util.function.BooleanSupplier; | |
| 11 | import java.util.logging.LogManager; | |
| 12 | ||
| 13 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 14 | import static com.keenwrite.Constants.LOGOS; | |
| 15 | import static com.keenwrite.preferences.Workspace.*; | |
| 16 | import static com.keenwrite.util.FontLoader.initFonts; | |
| 17 | import static javafx.scene.input.KeyCode.F11; | |
| 18 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 19 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 20 | ||
| 21 | /** | |
| 22 | * Application entry point. The application allows users to edit plain text | |
| 23 | * files in a markup notation and see a real-time preview of the formatted | |
| 24 | * output. | |
| 25 | */ | |
| 26 | @SuppressWarnings({"FieldCanBeLocal", "unused", "RedundantSuppression"}) | |
| 27 | public final class MainApp extends Application { | |
| 28 | ||
| 29 | private final Snitch mSnitch = Services.load( Snitch.class ); | |
| 30 | ||
| 31 | private Workspace mWorkspace; | |
| 32 | ||
| 33 | /** | |
| 34 | * Application entry point. | |
| 35 | * | |
| 36 | * @param args Command-line arguments. | |
| 37 | */ | |
| 38 | public static void main( final String[] args ) { | |
| 39 | disableLogging(); | |
| 40 | launch( args ); | |
| 41 | } | |
| 42 | ||
| 43 | /** | |
| 44 | * Suppress logging to standard output and standard error. | |
| 45 | */ | |
| 46 | private static void disableLogging() { | |
| 47 | LogManager.getLogManager().reset(); | |
| 48 | System.err.close(); | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * JavaFX entry point. | |
| 53 | * | |
| 54 | * @param stage The primary application stage. | |
| 55 | */ | |
| 56 | @Override | |
| 57 | public void start( final Stage stage ) { | |
| 58 | // Must be instantiated after the UI is initialized (i.e., not in main). | |
| 59 | mWorkspace = new Workspace(); | |
| 60 | ||
| 61 | initFonts(); | |
| 62 | initState( stage ); | |
| 63 | initStage( stage ); | |
| 64 | initIcons( stage ); | |
| 65 | initScene( stage ); | |
| 66 | initSnitch(); | |
| 67 | ||
| 68 | stage.show(); | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Saves the workspace then terminates the application. | |
| 73 | */ | |
| 74 | @Override | |
| 75 | public void stop() { | |
| 76 | save(); | |
| 77 | getSnitch().stop(); | |
| 78 | Platform.exit(); | |
| 79 | System.exit( 0 ); | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Saves the current application state configuration and user preferences. | |
| 84 | */ | |
| 85 | private void save() { | |
| 86 | mWorkspace.save(); | |
| 87 | } | |
| 88 | ||
| 89 | private void initState( final Stage stage ) { | |
| 90 | final var enable = createBoundsEnabledSupplier( stage ); | |
| 91 | ||
| 92 | stage.setX( mWorkspace.toDouble( KEY_UI_WINDOW_X ) ); | |
| 93 | stage.setY( mWorkspace.toDouble( KEY_UI_WINDOW_Y ) ); | |
| 94 | stage.setWidth( mWorkspace.toDouble( KEY_UI_WINDOW_W ) ); | |
| 95 | stage.setHeight( mWorkspace.toDouble( KEY_UI_WINDOW_H ) ); | |
| 96 | stage.setMaximized( mWorkspace.toBoolean( KEY_UI_WINDOW_MAX ) ); | |
| 97 | stage.setFullScreen( mWorkspace.toBoolean( KEY_UI_WINDOW_FULL ) ); | |
| 98 | ||
| 99 | mWorkspace.listen( KEY_UI_WINDOW_X, stage.xProperty(), enable ); | |
| 100 | mWorkspace.listen( KEY_UI_WINDOW_Y, stage.yProperty(), enable ); | |
| 101 | mWorkspace.listen( KEY_UI_WINDOW_W, stage.widthProperty(), enable ); | |
| 102 | mWorkspace.listen( KEY_UI_WINDOW_H, stage.heightProperty(), enable ); | |
| 103 | mWorkspace.listen( KEY_UI_WINDOW_MAX, stage.maximizedProperty() ); | |
| 104 | mWorkspace.listen( KEY_UI_WINDOW_FULL, stage.fullScreenProperty() ); | |
| 105 | } | |
| 106 | ||
| 107 | private void initStage( final Stage stage ) { | |
| 108 | stage.setTitle( APP_TITLE ); | |
| 109 | stage.addEventHandler( WINDOW_CLOSE_REQUEST, event -> stop() ); | |
| 110 | stage.addEventHandler( KEY_PRESSED, event -> { | |
| 111 | if( F11.equals( event.getCode() ) ) { | |
| 112 | stage.setFullScreen( !stage.isFullScreen() ); | |
| 113 | } | |
| 114 | } ); | |
| 115 | } | |
| 116 | ||
| 117 | private void initIcons( final Stage stage ) { | |
| 118 | stage.getIcons().addAll( LOGOS ); | |
| 119 | } | |
| 120 | ||
| 121 | private void initScene( final Stage stage ) { | |
| 122 | stage.setScene( (new MainScene( mWorkspace )).getScene() ); | |
| 123 | } | |
| 124 | ||
| 125 | /** | |
| 126 | * Watch for file system changes. | |
| 127 | */ | |
| 128 | private void initSnitch() { | |
| 129 | getSnitch().start(); | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * When the window is maximized, full screen, or iconified, prevent updating | |
| 134 | * the window bounds. This is used so that if the user exits the application | |
| 135 | * when full screen (or maximized), restarting the application will recall | |
| 136 | * the previous bounds, allowing for continuity of expected behaviour. | |
| 137 | * | |
| 138 | * @param stage The window to check for "normal" status. | |
| 139 | * @return {@code false} when the bounds must not be changed, ergo persisted. | |
| 140 | */ | |
| 141 | private BooleanSupplier createBoundsEnabledSupplier( final Stage stage ) { | |
| 142 | return () -> | |
| 143 | !(stage.isMaximized() || stage.isFullScreen() || stage.isIconified()); | |
| 144 | } | |
| 145 | ||
| 146 | private Snitch getSnitch() { | |
| 147 | return mSnitch; | |
| 148 | } | |
| 149 | } | |
| 1 | 150 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite; | |
| 3 | ||
| 4 | import com.keenwrite.editors.TextDefinition; | |
| 5 | import com.keenwrite.editors.TextEditor; | |
| 6 | import com.keenwrite.editors.TextResource; | |
| 7 | import com.keenwrite.editors.definition.DefinitionEditor; | |
| 8 | import com.keenwrite.editors.definition.DefinitionTabSceneFactory; | |
| 9 | import com.keenwrite.editors.definition.TreeTransformer; | |
| 10 | import com.keenwrite.editors.definition.yaml.YamlTreeTransformer; | |
| 11 | import com.keenwrite.editors.markdown.MarkdownEditor; | |
| 12 | import com.keenwrite.io.MediaType; | |
| 13 | import com.keenwrite.preferences.Key; | |
| 14 | import com.keenwrite.preferences.Workspace; | |
| 15 | import com.keenwrite.preview.HtmlPreview; | |
| 16 | import com.keenwrite.processors.IdentityProcessor; | |
| 17 | import com.keenwrite.processors.Processor; | |
| 18 | import com.keenwrite.processors.ProcessorContext; | |
| 19 | import com.keenwrite.processors.ProcessorFactory; | |
| 20 | import com.keenwrite.processors.markdown.Caret; | |
| 21 | import com.keenwrite.processors.markdown.CaretExtension; | |
| 22 | import com.keenwrite.service.events.Notifier; | |
| 23 | import com.keenwrite.sigils.RSigilOperator; | |
| 24 | import com.keenwrite.sigils.SigilOperator; | |
| 25 | import com.keenwrite.sigils.Tokens; | |
| 26 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 27 | import com.panemu.tiwulfx.control.dock.DetachableTab; | |
| 28 | import com.panemu.tiwulfx.control.dock.DetachableTabPane; | |
| 29 | import javafx.beans.property.*; | |
| 30 | import javafx.collections.ListChangeListener; | |
| 31 | import javafx.event.ActionEvent; | |
| 32 | import javafx.event.Event; | |
| 33 | import javafx.event.EventHandler; | |
| 34 | import javafx.scene.Scene; | |
| 35 | import javafx.scene.control.SplitPane; | |
| 36 | import javafx.scene.control.Tab; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | import javafx.scene.control.TreeItem.TreeModificationEvent; | |
| 39 | import javafx.scene.input.KeyEvent; | |
| 40 | import javafx.stage.Stage; | |
| 41 | import javafx.stage.Window; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.*; | |
| 46 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 47 | import java.util.function.Function; | |
| 48 | import java.util.stream.Collectors; | |
| 49 | ||
| 50 | import static com.keenwrite.Constants.*; | |
| 51 | import static com.keenwrite.ExportFormat.NONE; | |
| 52 | import static com.keenwrite.Messages.get; | |
| 53 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 54 | import static com.keenwrite.io.MediaType.*; | |
| 55 | import static com.keenwrite.preferences.Workspace.*; | |
| 56 | import static com.keenwrite.processors.ProcessorFactory.createProcessors; | |
| 57 | import static com.keenwrite.service.events.Notifier.NO; | |
| 58 | import static com.keenwrite.service.events.Notifier.YES; | |
| 59 | import static java.util.stream.Collectors.groupingBy; | |
| 60 | import static javafx.application.Platform.runLater; | |
| 61 | import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS; | |
| 62 | import static javafx.scene.input.KeyCode.SPACE; | |
| 63 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 64 | import static javafx.util.Duration.millis; | |
| 65 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 66 | ||
| 67 | /** | |
| 68 | * Responsible for wiring together the main application components for a | |
| 69 | * particular workspace (project). These include the definition views, | |
| 70 | * text editors, and preview pane along with any corresponding controllers. | |
| 71 | */ | |
| 72 | public final class MainPane extends SplitPane { | |
| 73 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 74 | ||
| 75 | /** | |
| 76 | * Used when opening files to determine how each file should be binned and | |
| 77 | * therefore what tab pane to be opened within. | |
| 78 | */ | |
| 79 | private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of( | |
| 80 | TEXT_MARKDOWN, TEXT_R_MARKDOWN, TEXT_R_XML, UNDEFINED | |
| 81 | ); | |
| 82 | ||
| 83 | /** | |
| 84 | * Prevents re-instantiation of processing classes. | |
| 85 | */ | |
| 86 | private final Map<TextResource, Processor<String>> mProcessors = | |
| 87 | new HashMap<>(); | |
| 88 | ||
| 89 | private final Workspace mWorkspace; | |
| 90 | ||
| 91 | /** | |
| 92 | * Groups similar file type tabs together. | |
| 93 | */ | |
| 94 | private final Map<MediaType, DetachableTabPane> mTabPanes = new HashMap<>(); | |
| 95 | ||
| 96 | /** | |
| 97 | * Stores definition names and values. | |
| 98 | */ | |
| 99 | private final Map<String, String> mResolvedMap = | |
| 100 | new HashMap<>( MAP_SIZE_DEFAULT ); | |
| 101 | ||
| 102 | /** | |
| 103 | * Renders the actively selected plain text editor tab. | |
| 104 | */ | |
| 105 | private final HtmlPreview mHtmlPreview; | |
| 106 | ||
| 107 | /** | |
| 108 | * Changing the active editor fires the value changed event. This allows | |
| 109 | * refreshes to happen when external definitions are modified and need to | |
| 110 | * trigger the processing chain. | |
| 111 | */ | |
| 112 | private final ObjectProperty<TextEditor> mActiveTextEditor = | |
| 113 | createActiveTextEditor(); | |
| 114 | ||
| 115 | /** | |
| 116 | * Changing the active definition editor fires the value changed event. This | |
| 117 | * allows refreshes to happen when external definitions are modified and need | |
| 118 | * to trigger the processing chain. | |
| 119 | */ | |
| 120 | private final ObjectProperty<TextDefinition> mActiveDefinitionEditor = | |
| 121 | createActiveDefinitionEditor( mActiveTextEditor ); | |
| 122 | ||
| 123 | /** | |
| 124 | * Responsible for creating a new scene when a tab is detached into | |
| 125 | * its own window frame. | |
| 126 | */ | |
| 127 | private final DefinitionTabSceneFactory mDefinitionTabSceneFactory = | |
| 128 | createDefinitionTabSceneFactory( mActiveDefinitionEditor ); | |
| 129 | ||
| 130 | /** | |
| 131 | * Tracks the number of detached tab panels opened into their own windows, | |
| 132 | * which allows unique identification of subordinate windows by their title. | |
| 133 | * It is doubtful more than 128 windows, much less 256, will be created. | |
| 134 | */ | |
| 135 | private byte mWindowCount; | |
| 136 | ||
| 137 | /** | |
| 138 | * Called when the definition data is changed. | |
| 139 | */ | |
| 140 | private final EventHandler<TreeModificationEvent<Event>> mTreeHandler = | |
| 141 | event -> { | |
| 142 | final var editor = mActiveDefinitionEditor.get(); | |
| 143 | ||
| 144 | resolve( editor ); | |
| 145 | process( getActiveTextEditor() ); | |
| 146 | save( editor ); | |
| 147 | }; | |
| 148 | ||
| 149 | /** | |
| 150 | * Adds all content panels to the main user interface. This will load the | |
| 151 | * configuration settings from the workspace to reproduce the settings from | |
| 152 | * a previous session. | |
| 153 | */ | |
| 154 | public MainPane( final Workspace workspace ) { | |
| 155 | mWorkspace = workspace; | |
| 156 | mHtmlPreview = new HtmlPreview( workspace ); | |
| 157 | ||
| 158 | open( bin( getRecentFiles() ) ); | |
| 159 | viewPreview(); | |
| 160 | ||
| 161 | final var ratio = 100f / getItems().size() / 100; | |
| 162 | final var positions = getDividerPositions(); | |
| 163 | ||
| 164 | for( int i = 0; i < positions.length; i++ ) { | |
| 165 | positions[ i ] = ratio * i; | |
| 166 | } | |
| 167 | ||
| 168 | // TODO: Load divider positions from exported settings, see bin() comment. | |
| 169 | setDividerPositions( positions ); | |
| 170 | ||
| 171 | // Once the main scene's window regains focus, update the active definition | |
| 172 | // editor to the currently selected tab. | |
| 173 | runLater( | |
| 174 | () -> getWindow().focusedProperty().addListener( ( c, o, n ) -> { | |
| 175 | if( n != null && n ) { | |
| 176 | final var pane = mTabPanes.get( TEXT_YAML ); | |
| 177 | final var model = pane.getSelectionModel(); | |
| 178 | final var tab = model.getSelectedItem(); | |
| 179 | ||
| 180 | if( tab != null ) { | |
| 181 | final var editor = (TextDefinition) tab.getContent(); | |
| 182 | ||
| 183 | mActiveDefinitionEditor.set( editor ); | |
| 184 | } | |
| 185 | } | |
| 186 | } ) | |
| 187 | ); | |
| 188 | } | |
| 189 | ||
| 190 | /** | |
| 191 | * Opens all the files into the application, provided the paths are unique. | |
| 192 | * This may only be called for any type of files that a user can edit | |
| 193 | * (i.e., update and persist), such as definitions and text files. | |
| 194 | * | |
| 195 | * @param files The list of files to open. | |
| 196 | */ | |
| 197 | public void open( final List<File> files ) { | |
| 198 | files.forEach( this::open ); | |
| 199 | } | |
| 200 | ||
| 201 | /** | |
| 202 | * This opens the given file. Since the preview pane is not a file that | |
| 203 | * can be opened, it is safe to add a listener to the detachable pane. | |
| 204 | * | |
| 205 | * @param file The file to open. | |
| 206 | */ | |
| 207 | private void open( final File file ) { | |
| 208 | final var tab = createTab( file ); | |
| 209 | final var node = tab.getContent(); | |
| 210 | final var mediaType = MediaType.valueFrom( file ); | |
| 211 | final var tabPane = obtainDetachableTabPane( mediaType ); | |
| 212 | final var newTabPane = !getItems().contains( tabPane ); | |
| 213 | ||
| 214 | tab.setTooltip( createTooltip( file ) ); | |
| 215 | tabPane.setFocusTraversable( false ); | |
| 216 | tabPane.setTabClosingPolicy( ALL_TABS ); | |
| 217 | tabPane.getTabs().add( tab ); | |
| 218 | ||
| 219 | if( newTabPane ) { | |
| 220 | var index = getItems().size(); | |
| 221 | ||
| 222 | if( node instanceof TextDefinition ) { | |
| 223 | tabPane.setSceneFactory( mDefinitionTabSceneFactory::create ); | |
| 224 | index = 0; | |
| 225 | } | |
| 226 | ||
| 227 | addTabPane( index, tabPane ); | |
| 228 | } | |
| 229 | ||
| 230 | getRecentFiles().add( file.getAbsolutePath() ); | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Opens a new text editor document using the default document file name. | |
| 235 | */ | |
| 236 | public void newTextEditor() { | |
| 237 | open( DOCUMENT_DEFAULT ); | |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Opens a new definition editor document using the default definition | |
| 242 | * file name. | |
| 243 | */ | |
| 244 | public void newDefinitionEditor() { | |
| 245 | open( DEFINITION_DEFAULT ); | |
| 246 | } | |
| 247 | ||
| 248 | /** | |
| 249 | * Iterates over all tab panes to find all {@link TextEditor}s and request | |
| 250 | * that they save themselves. | |
| 251 | */ | |
| 252 | public void saveAll() { | |
| 253 | mTabPanes.forEach( | |
| 254 | ( mt, tp ) -> tp.getTabs().forEach( ( tab ) -> { | |
| 255 | final var node = tab.getContent(); | |
| 256 | if( node instanceof TextEditor ) { | |
| 257 | save( ((TextEditor) node) ); | |
| 258 | } | |
| 259 | } ) | |
| 260 | ); | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Requests that the active {@link TextEditor} saves itself. Don't bother | |
| 265 | * checking if modified first because if the user swaps external media from | |
| 266 | * an external source (e.g., USB thumb drive), save should not second-guess | |
| 267 | * the user: save always re-saves. Also, it's less code. | |
| 268 | */ | |
| 269 | public void save() { | |
| 270 | save( getActiveTextEditor() ); | |
| 271 | } | |
| 272 | ||
| 273 | /** | |
| 274 | * Saves the active {@link TextEditor} under a new name. | |
| 275 | * | |
| 276 | * @param file The new active editor {@link File} reference. | |
| 277 | */ | |
| 278 | public void saveAs( final File file ) { | |
| 279 | assert file != null; | |
| 280 | final var editor = getActiveTextEditor(); | |
| 281 | final var tab = getTab( editor ); | |
| 282 | ||
| 283 | editor.rename( file ); | |
| 284 | tab.ifPresent( t -> { | |
| 285 | t.setText( editor.getFilename() ); | |
| 286 | t.setTooltip( createTooltip( file ) ); | |
| 287 | } ); | |
| 288 | ||
| 289 | save(); | |
| 290 | } | |
| 291 | ||
| 292 | /** | |
| 293 | * Saves the given {@link TextResource} to a file. This is typically used | |
| 294 | * to save either an instance of {@link TextEditor} or {@link TextDefinition}. | |
| 295 | * | |
| 296 | * @param resource The resource to export. | |
| 297 | */ | |
| 298 | private void save( final TextResource resource ) { | |
| 299 | try { | |
| 300 | resource.save(); | |
| 301 | } catch( final Exception ex ) { | |
| 302 | clue( ex ); | |
| 303 | sNotifier.alert( | |
| 304 | getWindow(), resource.getPath(), "TextResource.saveFailed", ex | |
| 305 | ); | |
| 306 | } | |
| 307 | } | |
| 308 | ||
| 309 | /** | |
| 310 | * Closes all open {@link TextEditor}s; all {@link TextDefinition}s stay open. | |
| 311 | * | |
| 312 | * @return {@code true} when all editors, modified or otherwise, were | |
| 313 | * permitted to close; {@code false} when one or more editors were modified | |
| 314 | * and the user requested no closing. | |
| 315 | */ | |
| 316 | public boolean closeAll() { | |
| 317 | var closable = true; | |
| 318 | ||
| 319 | for( final var entry : mTabPanes.entrySet() ) { | |
| 320 | final var tabPane = entry.getValue(); | |
| 321 | final var tabIterator = tabPane.getTabs().iterator(); | |
| 322 | ||
| 323 | while( tabIterator.hasNext() ) { | |
| 324 | final var tab = tabIterator.next(); | |
| 325 | final var node = tab.getContent(); | |
| 326 | ||
| 327 | if( node instanceof TextEditor && | |
| 328 | (closable &= canClose( (TextEditor) node )) ) { | |
| 329 | tabIterator.remove(); | |
| 330 | close( tab ); | |
| 331 | } | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | return closable; | |
| 336 | } | |
| 337 | ||
| 338 | /** | |
| 339 | * Calls the tab's {@link Tab#getOnClosed()} handler to carry out a close | |
| 340 | * event. | |
| 341 | * | |
| 342 | * @param tab The {@link Tab} that was closed. | |
| 343 | */ | |
| 344 | private void close( final Tab tab ) { | |
| 345 | final var handler = tab.getOnClosed(); | |
| 346 | ||
| 347 | if( handler != null ) { | |
| 348 | handler.handle( new ActionEvent() ); | |
| 349 | } | |
| 350 | } | |
| 351 | ||
| 352 | /** | |
| 353 | * Closes the active tab; delegates to {@link #canClose(TextEditor)}. | |
| 354 | */ | |
| 355 | public void close() { | |
| 356 | final var editor = getActiveTextEditor(); | |
| 357 | if( canClose( editor ) ) { | |
| 358 | close( editor ); | |
| 359 | } | |
| 360 | } | |
| 361 | ||
| 362 | /** | |
| 363 | * Closes the given {@link TextEditor}. This must not be called from within | |
| 364 | * a loop that iterates over the tab panes using {@code forEach}, lest a | |
| 365 | * concurrent modification exception be thrown. | |
| 366 | * | |
| 367 | * @param editor The {@link TextEditor} to close, without confirming with | |
| 368 | * the user. | |
| 369 | */ | |
| 370 | private void close( final TextEditor editor ) { | |
| 371 | getTab( editor ).ifPresent( | |
| 372 | ( tab ) -> { | |
| 373 | tab.getTabPane().getTabs().remove( tab ); | |
| 374 | close( tab ); | |
| 375 | } | |
| 376 | ); | |
| 377 | } | |
| 378 | ||
| 379 | /** | |
| 380 | * Answers whether the given {@link TextEditor} may be closed. | |
| 381 | * | |
| 382 | * @param editor The {@link TextEditor} to try closing. | |
| 383 | * @return {@code true} when the editor may be closed; {@code false} when | |
| 384 | * the user has requested to keep the editor open. | |
| 385 | */ | |
| 386 | private boolean canClose( final TextEditor editor ) { | |
| 387 | final var editorTab = getTab( editor ); | |
| 388 | final var canClose = new AtomicBoolean( true ); | |
| 389 | ||
| 390 | if( editor.isModified() ) { | |
| 391 | final var filename = new StringBuilder(); | |
| 392 | editorTab.ifPresent( ( tab ) -> filename.append( tab.getText() ) ); | |
| 393 | ||
| 394 | final var message = sNotifier.createNotification( | |
| 395 | Messages.get( "Alert.file.close.title" ), | |
| 396 | Messages.get( "Alert.file.close.text" ), | |
| 397 | filename.toString() | |
| 398 | ); | |
| 399 | ||
| 400 | final var dialog = sNotifier.createConfirmation( getWindow(), message ); | |
| 401 | ||
| 402 | dialog.showAndWait().ifPresent( | |
| 403 | save -> canClose.set( save == YES ? editor.save() : save == NO ) | |
| 404 | ); | |
| 405 | } | |
| 406 | ||
| 407 | return canClose.get(); | |
| 408 | } | |
| 409 | ||
| 410 | private ObjectProperty<TextEditor> createActiveTextEditor() { | |
| 411 | final var editor = new SimpleObjectProperty<TextEditor>(); | |
| 412 | ||
| 413 | editor.addListener( ( c, o, n ) -> { | |
| 414 | if( n != null ) { | |
| 415 | mHtmlPreview.setBaseUri( n.getPath() ); | |
| 416 | process( n ); | |
| 417 | } | |
| 418 | } ); | |
| 419 | ||
| 420 | return editor; | |
| 421 | } | |
| 422 | ||
| 423 | /** | |
| 424 | * Adds the HTML preview tab to its own tab pane. This will only add the | |
| 425 | * preview once. | |
| 426 | */ | |
| 427 | public void viewPreview() { | |
| 428 | final var tabPane = obtainDetachableTabPane( TEXT_HTML ); | |
| 429 | ||
| 430 | // Prevent multiple HTML previews because in the end, there can be only one. | |
| 431 | for( final var tab : tabPane.getTabs() ) { | |
| 432 | if( tab.getContent() == mHtmlPreview ) { | |
| 433 | return; | |
| 434 | } | |
| 435 | } | |
| 436 | ||
| 437 | tabPane.addTab( "HTML", mHtmlPreview ); | |
| 438 | addTabPane( tabPane ); | |
| 439 | } | |
| 440 | ||
| 441 | public void viewRefresh() { | |
| 442 | mHtmlPreview.refresh(); | |
| 443 | } | |
| 444 | ||
| 445 | /** | |
| 446 | * Returns the tab that contains the given {@link TextEditor}. | |
| 447 | * | |
| 448 | * @param editor The {@link TextEditor} instance to find amongst the tabs. | |
| 449 | * @return The first tab having content that matches the given tab. | |
| 450 | */ | |
| 451 | private Optional<Tab> getTab( final TextEditor editor ) { | |
| 452 | return mTabPanes.values() | |
| 453 | .stream() | |
| 454 | .flatMap( pane -> pane.getTabs().stream() ) | |
| 455 | .filter( tab -> editor.equals( tab.getContent() ) ) | |
| 456 | .findFirst(); | |
| 457 | } | |
| 458 | ||
| 459 | /** | |
| 460 | * Creates a new {@link DefinitionEditor} wrapped in a listener that | |
| 461 | * is used to detect when the active {@link DefinitionEditor} has changed. | |
| 462 | * Upon changing, the {@link #mResolvedMap} is updated and the active | |
| 463 | * text editor is refreshed. | |
| 464 | * | |
| 465 | * @param editor Text editor to update with the revised resolved map. | |
| 466 | * @return A newly configured property that represents the active | |
| 467 | * {@link DefinitionEditor}, never null. | |
| 468 | */ | |
| 469 | private ObjectProperty<TextDefinition> createActiveDefinitionEditor( | |
| 470 | final ObjectProperty<TextEditor> editor ) { | |
| 471 | final var definitions = new SimpleObjectProperty<TextDefinition>(); | |
| 472 | definitions.addListener( ( c, o, n ) -> { | |
| 473 | resolve( n == null ? createDefinitionEditor() : n ); | |
| 474 | process( editor.get() ); | |
| 475 | } ); | |
| 476 | ||
| 477 | return definitions; | |
| 478 | } | |
| 479 | ||
| 480 | /** | |
| 481 | * Instantiates a factory that's responsible for creating new scenes when | |
| 482 | * a tab is dropped outside of any application window. The definition tabs | |
| 483 | * are fairly complex in that only one may be active at any time. When | |
| 484 | * activated, the {@link #mResolvedMap} must be updated to reflect the | |
| 485 | * hierarchy displayed in the {@link DefinitionEditor}. | |
| 486 | * | |
| 487 | * @param activeDefinitionEditor The current {@link DefinitionEditor}. | |
| 488 | * @return An object that listens to {@link DefinitionEditor} tab focus | |
| 489 | * changes. | |
| 490 | */ | |
| 491 | private DefinitionTabSceneFactory createDefinitionTabSceneFactory( | |
| 492 | final ObjectProperty<TextDefinition> activeDefinitionEditor ) { | |
| 493 | return new DefinitionTabSceneFactory( ( tab ) -> { | |
| 494 | assert tab != null; | |
| 495 | ||
| 496 | var node = tab.getContent(); | |
| 497 | if( node instanceof TextDefinition ) { | |
| 498 | activeDefinitionEditor.set( (DefinitionEditor) node ); | |
| 499 | } | |
| 500 | } ); | |
| 501 | } | |
| 502 | ||
| 503 | private DetachableTab createTab( final File file ) { | |
| 504 | final var r = createTextResource( file ); | |
| 505 | final var tab = new DetachableTab( r.getFilename(), r.getNode() ); | |
| 506 | ||
| 507 | r.modifiedProperty().addListener( | |
| 508 | ( c, o, n ) -> tab.setText( r.getFilename() + (n ? "*" : "") ) | |
| 509 | ); | |
| 510 | ||
| 511 | // This is called when either the tab is closed by the user clicking on | |
| 512 | // the tab's close icon or when closing (all) from the file menu. | |
| 513 | tab.setOnClosed( | |
| 514 | ( __ ) -> getRecentFiles().remove( file.getAbsolutePath() ) | |
| 515 | ); | |
| 516 | ||
| 517 | return tab; | |
| 518 | } | |
| 519 | ||
| 520 | /** | |
| 521 | * Creates bins for the different {@link MediaType}s, which eventually are | |
| 522 | * added to the UI as separate tab panes. If ever a general-purpose scene | |
| 523 | * exporter is developed to serialize a scene to an FXML file, this could | |
| 524 | * be replaced by such a class. | |
| 525 | * <p> | |
| 526 | * When binning the files, this makes sure that at least one file exists | |
| 527 | * for every type. If the user has opted to close a particular type (such | |
| 528 | * as the definition pane), the view will suppressed elsewhere. | |
| 529 | * </p> | |
| 530 | * <p> | |
| 531 | * The order that the binned files are returned will be reflected in the | |
| 532 | * order that the corresponding panes are rendered in the UI. | |
| 533 | * </p> | |
| 534 | * | |
| 535 | * @param paths The file paths to bin according to their type. | |
| 536 | * @return An in-order list of files, first by structured definition files, | |
| 537 | * then by plain text documents. | |
| 538 | */ | |
| 539 | private List<File> bin( final SetProperty<String> paths ) { | |
| 540 | // Treat all files destined for the text editor as plain text documents | |
| 541 | // so that they are added to the same pane. Grouping by TEXT_PLAIN is a | |
| 542 | // bit arbitrary, but means explicitly capturing TEXT_PLAIN isn't needed. | |
| 543 | final Function<MediaType, MediaType> bin = | |
| 544 | m -> PLAIN_TEXT_FORMAT.contains( m ) ? TEXT_PLAIN : m; | |
| 545 | ||
| 546 | // Create two groups: YAML files and plain text files. | |
| 547 | final var bins = paths | |
| 548 | .stream() | |
| 549 | .collect( | |
| 550 | groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) ) | |
| 551 | ); | |
| 552 | ||
| 553 | bins.putIfAbsent( TEXT_YAML, List.of( DEFINITION_DEFAULT.toString() ) ); | |
| 554 | bins.putIfAbsent( TEXT_PLAIN, List.of( DOCUMENT_DEFAULT.toString() ) ); | |
| 555 | ||
| 556 | final var result = new ArrayList<File>( paths.size() ); | |
| 557 | ||
| 558 | // Ensure that the same types are listed together (keep insertion order). | |
| 559 | bins.forEach( ( mediaType, files ) -> result.addAll( | |
| 560 | files.stream().map( File::new ).collect( Collectors.toList() ) ) | |
| 561 | ); | |
| 562 | ||
| 563 | return result; | |
| 564 | } | |
| 565 | ||
| 566 | /** | |
| 567 | * Uses the given {@link TextDefinition} instance to update the | |
| 568 | * {@link #mResolvedMap}. | |
| 569 | * | |
| 570 | * @param editor A non-null, possibly empty definition editor. | |
| 571 | */ | |
| 572 | private void resolve( final TextDefinition editor ) { | |
| 573 | assert editor != null; | |
| 574 | ||
| 575 | final var tokens = createDefinitionTokens(); | |
| 576 | final var operator = new YamlSigilOperator( tokens ); | |
| 577 | final var map = new HashMap<String, String>(); | |
| 578 | ||
| 579 | editor.toMap().forEach( ( k, v ) -> map.put( operator.entoken( k ), v ) ); | |
| 580 | ||
| 581 | mResolvedMap.clear(); | |
| 582 | mResolvedMap.putAll( editor.interpolate( map, tokens ) ); | |
| 583 | } | |
| 584 | ||
| 585 | /** | |
| 586 | * Force the active editor to update, which will cause the processor | |
| 587 | * to re-evaluate the interpolated definition map thereby updating the | |
| 588 | * preview pane. | |
| 589 | * | |
| 590 | * @param editor Contains the source document to update in the preview pane. | |
| 591 | */ | |
| 592 | private void process( final TextEditor editor ) { | |
| 593 | mProcessors.getOrDefault( editor, IdentityProcessor.INSTANCE ) | |
| 594 | .apply( editor == null ? "" : editor.getText() ); | |
| 595 | mHtmlPreview.scrollTo( CARET_ID ); | |
| 596 | } | |
| 597 | ||
| 598 | /** | |
| 599 | * Lazily creates a {@link DetachableTabPane} configured to handle focus | |
| 600 | * requests by delegating to the selected tab's content. The tab pane is | |
| 601 | * associated with a given media type so that similar files can be grouped | |
| 602 | * together. | |
| 603 | * | |
| 604 | * @param mediaType The media type to associate with the tab pane. | |
| 605 | * @return An instance of {@link DetachableTabPane} that will handle | |
| 606 | * docking of tabs. | |
| 607 | */ | |
| 608 | private DetachableTabPane obtainDetachableTabPane( | |
| 609 | final MediaType mediaType ) { | |
| 610 | return mTabPanes.computeIfAbsent( | |
| 611 | mediaType, ( mt ) -> createDetachableTabPane() | |
| 612 | ); | |
| 613 | } | |
| 614 | ||
| 615 | /** | |
| 616 | * Creates an initialized {@link DetachableTabPane} instance. | |
| 617 | * | |
| 618 | * @return A new {@link DetachableTabPane} with all listeners configured. | |
| 619 | */ | |
| 620 | private DetachableTabPane createDetachableTabPane() { | |
| 621 | final var tabPane = new DetachableTabPane(); | |
| 622 | ||
| 623 | initStageOwnerFactory( tabPane ); | |
| 624 | initTabListener( tabPane ); | |
| 625 | initSelectionModelListener( tabPane ); | |
| 626 | ||
| 627 | return tabPane; | |
| 628 | } | |
| 629 | ||
| 630 | /** | |
| 631 | * When any {@link DetachableTabPane} is detached from the main window, | |
| 632 | * the stage owner factory must be given its parent window, which will | |
| 633 | * own the child window. The parent window is the {@link MainPane}'s | |
| 634 | * {@link Scene}'s {@link Window} instance. | |
| 635 | * | |
| 636 | * <p> | |
| 637 | * This will derives the new title from the main window title, incrementing | |
| 638 | * the window count to help uniquely identify the child windows. | |
| 639 | * </p> | |
| 640 | * | |
| 641 | * @param tabPane A new {@link DetachableTabPane} to configure. | |
| 642 | */ | |
| 643 | private void initStageOwnerFactory( final DetachableTabPane tabPane ) { | |
| 644 | tabPane.setStageOwnerFactory( ( stage ) -> { | |
| 645 | final var title = get( | |
| 646 | "Detach.tab.title", | |
| 647 | ((Stage) getWindow()).getTitle(), ++mWindowCount | |
| 648 | ); | |
| 649 | stage.setTitle( title ); | |
| 650 | return getScene().getWindow(); | |
| 651 | } ); | |
| 652 | } | |
| 653 | ||
| 654 | /** | |
| 655 | * Responsible for configuring the content of each {@link DetachableTab} when | |
| 656 | * it is added to the given {@link DetachableTabPane} instance. | |
| 657 | * <p> | |
| 658 | * For {@link TextEditor} contents, an instance of {@link ScrollEventHandler} | |
| 659 | * is initialized to perform synchronized scrolling between the editor and | |
| 660 | * its preview window. Additionally, the last tab in the tab pane's list of | |
| 661 | * tabs is given focus. | |
| 662 | * </p> | |
| 663 | * <p> | |
| 664 | * Note that multiple tabs can be added simultaneously. | |
| 665 | * </p> | |
| 666 | * | |
| 667 | * @param tabPane A new {@link DetachableTabPane} to configure. | |
| 668 | */ | |
| 669 | private void initTabListener( final DetachableTabPane tabPane ) { | |
| 670 | tabPane.getTabs().addListener( | |
| 671 | ( final ListChangeListener.Change<? extends Tab> listener ) -> { | |
| 672 | while( listener.next() ) { | |
| 673 | if( listener.wasAdded() ) { | |
| 674 | final var tabs = listener.getAddedSubList(); | |
| 675 | ||
| 676 | tabs.forEach( ( tab ) -> { | |
| 677 | final var node = tab.getContent(); | |
| 678 | ||
| 679 | if( node instanceof TextEditor ) { | |
| 680 | initScrollEventListener( tab ); | |
| 681 | } | |
| 682 | } ); | |
| 683 | ||
| 684 | // Select and give focus to the last tab opened. | |
| 685 | final var index = tabs.size() - 1; | |
| 686 | if( index >= 0 ) { | |
| 687 | final var tab = tabs.get( index ); | |
| 688 | tabPane.getSelectionModel().select( tab ); | |
| 689 | tab.getContent().requestFocus(); | |
| 690 | } | |
| 691 | } | |
| 692 | } | |
| 693 | } | |
| 694 | ); | |
| 695 | } | |
| 696 | ||
| 697 | /** | |
| 698 | * Responsible for handling tab change events. | |
| 699 | * | |
| 700 | * @param tabPane A new {@link DetachableTabPane} to configure. | |
| 701 | */ | |
| 702 | private void initSelectionModelListener( final DetachableTabPane tabPane ) { | |
| 703 | final var model = tabPane.getSelectionModel(); | |
| 704 | ||
| 705 | model.selectedItemProperty().addListener( ( c, o, n ) -> { | |
| 706 | if( o != null && n == null ) { | |
| 707 | final var node = o.getContent(); | |
| 708 | ||
| 709 | // If the last definition editor in the active pane was closed, | |
| 710 | // clear out the definitions then refresh the text editor. | |
| 711 | if( node instanceof TextDefinition ) { | |
| 712 | mActiveDefinitionEditor.set( createDefinitionEditor() ); | |
| 713 | } | |
| 714 | } | |
| 715 | else if( n != null ) { | |
| 716 | final var node = n.getContent(); | |
| 717 | ||
| 718 | if( node instanceof TextEditor ) { | |
| 719 | // Changing the active node will fire an event, which will | |
| 720 | // update the preview panel and grab focus. | |
| 721 | mActiveTextEditor.set( (TextEditor) node ); | |
| 722 | runLater( node::requestFocus ); | |
| 723 | } | |
| 724 | else if( node instanceof TextDefinition ) { | |
| 725 | mActiveDefinitionEditor.set( (DefinitionEditor) node ); | |
| 726 | } | |
| 727 | } | |
| 728 | } ); | |
| 729 | } | |
| 730 | ||
| 731 | /** | |
| 732 | * Synchronizes scrollbar positions between the given {@link Tab} that | |
| 733 | * contains an instance of {@link TextEditor} and {@link HtmlPreview} pane. | |
| 734 | * | |
| 735 | * @param tab The container for an instance of {@link TextEditor}. | |
| 736 | */ | |
| 737 | private void initScrollEventListener( final Tab tab ) { | |
| 738 | final var editor = (TextEditor) tab.getContent(); | |
| 739 | final var scrollPane = editor.getScrollPane(); | |
| 740 | final var scrollBar = mHtmlPreview.getVerticalScrollBar(); | |
| 741 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 742 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 743 | } | |
| 744 | ||
| 745 | private void addTabPane( final int index, final DetachableTabPane tabPane ) { | |
| 746 | final var items = getItems(); | |
| 747 | if( !items.contains( tabPane ) ) { | |
| 748 | items.add( index, tabPane ); | |
| 749 | } | |
| 750 | } | |
| 751 | ||
| 752 | private void addTabPane( final DetachableTabPane tabPane ) { | |
| 753 | addTabPane( getItems().size(), tabPane ); | |
| 754 | } | |
| 755 | ||
| 756 | /** | |
| 757 | * @param path Used by {@link ProcessorFactory} to determine | |
| 758 | * {@link Processor} type to create based on file type. | |
| 759 | * @param caret Used by {@link CaretExtension} to add ID attribute into | |
| 760 | * preview document for scrollbar synchronization. | |
| 761 | * @return A new {@link ProcessorContext} to use when creating an instance of | |
| 762 | * {@link Processor}. | |
| 763 | */ | |
| 764 | private ProcessorContext createProcessorContext( | |
| 765 | final Path path, final Caret caret ) { | |
| 766 | return new ProcessorContext( | |
| 767 | mHtmlPreview, mResolvedMap, path, caret, NONE, mWorkspace | |
| 768 | ); | |
| 769 | } | |
| 770 | ||
| 771 | public ProcessorContext createProcessorContext( final TextEditor t ) { | |
| 772 | return createProcessorContext( t.getPath(), t.getCaret() ); | |
| 773 | } | |
| 774 | ||
| 775 | private TextResource createTextResource( final File file ) { | |
| 776 | // TODO: Create PlainTextEditor that's returned by default. | |
| 777 | return MediaType.valueFrom( file ) == TEXT_YAML | |
| 778 | ? createDefinitionEditor( file ) | |
| 779 | : createMarkdownEditor( file ); | |
| 780 | } | |
| 781 | ||
| 782 | /** | |
| 783 | * Creates an instance of {@link MarkdownEditor} that listens for both | |
| 784 | * caret change events and text change events. Text change events must | |
| 785 | * take priority over caret change events because it's possible to change | |
| 786 | * the text without moving the caret (e.g., delete selected text). | |
| 787 | * | |
| 788 | * @param file The file containing contents for the text editor. | |
| 789 | * @return A non-null text editor. | |
| 790 | */ | |
| 791 | private TextResource createMarkdownEditor( final File file ) { | |
| 792 | final var path = file.toPath(); | |
| 793 | final var editor = new MarkdownEditor( file, getWorkspace() ); | |
| 794 | final var caret = editor.getCaret(); | |
| 795 | final var context = createProcessorContext( path, caret ); | |
| 796 | ||
| 797 | mProcessors.computeIfAbsent( editor, p -> createProcessors( context ) ); | |
| 798 | ||
| 799 | editor.addDirtyListener( ( c, o, n ) -> { | |
| 800 | if( n ) { | |
| 801 | // Reset the status to OK after changing the text. | |
| 802 | clue(); | |
| 803 | ||
| 804 | // Processing the text will update the status bar. | |
| 805 | process( getActiveTextEditor() ); | |
| 806 | } | |
| 807 | } ); | |
| 808 | ||
| 809 | editor.addEventListener( | |
| 810 | keyPressed( SPACE, CONTROL_DOWN ), this::autoinsert | |
| 811 | ); | |
| 812 | ||
| 813 | // Set the active editor, which refreshes the preview panel. | |
| 814 | mActiveTextEditor.set( editor ); | |
| 815 | ||
| 816 | return editor; | |
| 817 | } | |
| 818 | ||
| 819 | /** | |
| 820 | * Delegates to {@link #autoinsert()}. | |
| 821 | * | |
| 822 | * @param event Ignored. | |
| 823 | */ | |
| 824 | private void autoinsert( final KeyEvent event ) { | |
| 825 | autoinsert(); | |
| 826 | } | |
| 827 | ||
| 828 | /** | |
| 829 | * Finds a node that matches the word at the caret, then inserts the | |
| 830 | * corresponding definition. The definition token delimiters depend on | |
| 831 | * the type of file being edited. | |
| 832 | */ | |
| 833 | public void autoinsert() { | |
| 834 | final var definitions = getActiveTextDefinition(); | |
| 835 | final var editor = getActiveTextEditor(); | |
| 836 | final var mediaType = editor.getMediaType(); | |
| 837 | final var operator = getSigilOperator( mediaType ); | |
| 838 | ||
| 839 | DefinitionNameInjector.autoinsert( editor, definitions, operator ); | |
| 840 | } | |
| 841 | ||
| 842 | private TextDefinition createDefinitionEditor() { | |
| 843 | return createDefinitionEditor( DEFINITION_DEFAULT ); | |
| 844 | } | |
| 845 | ||
| 846 | private TextDefinition createDefinitionEditor( final File file ) { | |
| 847 | final var transformer = createTreeTransformer(); | |
| 848 | final var editor = new DefinitionEditor( file, transformer ); | |
| 849 | ||
| 850 | editor.addTreeChangeHandler( mTreeHandler ); | |
| 851 | ||
| 852 | return editor; | |
| 853 | } | |
| 854 | ||
| 855 | private TreeTransformer createTreeTransformer() { | |
| 856 | return new YamlTreeTransformer(); | |
| 857 | } | |
| 858 | ||
| 859 | private Tooltip createTooltip( final File file ) { | |
| 860 | final var path = file.toPath(); | |
| 861 | final var tooltip = new Tooltip( path.toString() ); | |
| 862 | ||
| 863 | tooltip.setShowDelay( millis( 200 ) ); | |
| 864 | return tooltip; | |
| 865 | } | |
| 866 | ||
| 867 | public TextEditor getActiveTextEditor() { | |
| 868 | return mActiveTextEditor.get(); | |
| 869 | } | |
| 870 | ||
| 871 | public ReadOnlyObjectProperty<TextEditor> activeTextEditorProperty() { | |
| 872 | return mActiveTextEditor; | |
| 873 | } | |
| 874 | ||
| 875 | public TextDefinition getActiveTextDefinition() { | |
| 876 | return mActiveDefinitionEditor.get(); | |
| 877 | } | |
| 878 | ||
| 879 | public Window getWindow() { | |
| 880 | return getScene().getWindow(); | |
| 881 | } | |
| 882 | ||
| 883 | public Workspace getWorkspace() { | |
| 884 | return mWorkspace; | |
| 885 | } | |
| 886 | ||
| 887 | /** | |
| 888 | * Returns the sigil operator for the given {@link MediaType}. | |
| 889 | * | |
| 890 | * @param mediaType The type of file being edited. | |
| 891 | */ | |
| 892 | private SigilOperator getSigilOperator( final MediaType mediaType ) { | |
| 893 | final var operator = new YamlSigilOperator( createDefinitionTokens() ); | |
| 894 | ||
| 895 | return switch( mediaType ) { | |
| 896 | case TEXT_R_MARKDOWN, TEXT_R_XML -> new RSigilOperator( | |
| 897 | createRTokens(), operator ); | |
| 898 | default -> operator; | |
| 899 | }; | |
| 900 | } | |
| 901 | ||
| 902 | /** | |
| 903 | * Returns the set of file names opened in the application. The names must | |
| 904 | * be converted to {@link File} objects. | |
| 905 | * | |
| 906 | * @return A {@link Set} of file names. | |
| 907 | */ | |
| 908 | private SetProperty<String> getRecentFiles() { | |
| 909 | return getWorkspace().setsProperty( KEY_UI_FILES_PATH ); | |
| 910 | } | |
| 911 | ||
| 912 | private StringProperty stringProperty( final Key key ) { | |
| 913 | return getWorkspace().stringProperty( key ); | |
| 914 | } | |
| 915 | ||
| 916 | private Tokens createRTokens() { | |
| 917 | return createTokens( KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ); | |
| 918 | } | |
| 919 | ||
| 920 | private Tokens createDefinitionTokens() { | |
| 921 | return createTokens( KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED ); | |
| 922 | } | |
| 923 | ||
| 924 | private Tokens createTokens( final Key began, final Key ended ) { | |
| 925 | return new Tokens( stringProperty( began ), stringProperty( ended ) ); | |
| 926 | } | |
| 927 | } | |
| 1 | 928 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.ui.actions.ApplicationActions; | |
| 6 | import com.keenwrite.ui.actions.ApplicationMenuBar; | |
| 7 | import com.keenwrite.ui.listeners.CaretListener; | |
| 8 | import javafx.scene.Node; | |
| 9 | import javafx.scene.Parent; | |
| 10 | import javafx.scene.Scene; | |
| 11 | import javafx.scene.layout.BorderPane; | |
| 12 | import org.controlsfx.control.StatusBar; | |
| 13 | ||
| 14 | import static com.keenwrite.Constants.STYLESHEET_SCENE; | |
| 15 | ||
| 16 | /** | |
| 17 | * Responsible for creating the bar scene: menu bar, tool bar, and status bar. | |
| 18 | */ | |
| 19 | public class MainScene { | |
| 20 | private final Scene mScene; | |
| 21 | ||
| 22 | public MainScene( final Workspace workspace ) { | |
| 23 | final var mainPane = createMainPane( workspace ); | |
| 24 | final var actions = createApplicationActions( mainPane ); | |
| 25 | final var menuBar = createMenuBar( actions ); | |
| 26 | final var appPane = new BorderPane(); | |
| 27 | final var statusBar = StatusBarNotifier.getStatusBar(); | |
| 28 | final var caretListener = createCaretListener( mainPane ); | |
| 29 | ||
| 30 | statusBar.getRightItems().add( caretListener ); | |
| 31 | ||
| 32 | appPane.setTop( menuBar ); | |
| 33 | appPane.setCenter( mainPane ); | |
| 34 | appPane.setBottom( statusBar ); | |
| 35 | ||
| 36 | mScene = createScene( appPane ); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Called by the {@link MainApp} to get a handle on the {@link Scene} | |
| 41 | * created by an instance of {@link MainScene}. | |
| 42 | * | |
| 43 | * @return The {@link Scene} created at construction time. | |
| 44 | */ | |
| 45 | public Scene getScene() { | |
| 46 | return mScene; | |
| 47 | } | |
| 48 | ||
| 49 | private MainPane createMainPane( final Workspace workspace ) { | |
| 50 | return new MainPane( workspace ); | |
| 51 | } | |
| 52 | ||
| 53 | private ApplicationActions createApplicationActions( | |
| 54 | final MainPane mainPane ) { | |
| 55 | return new ApplicationActions( mainPane ); | |
| 56 | } | |
| 57 | ||
| 58 | private Node createMenuBar( final ApplicationActions actions ) { | |
| 59 | return (new ApplicationMenuBar()).createMenuBar( actions ); | |
| 60 | } | |
| 61 | ||
| 62 | private Scene createScene( final Parent parent ) { | |
| 63 | final var scene = new Scene( parent ); | |
| 64 | final var stylesheets = scene.getStylesheets(); | |
| 65 | stylesheets.add( STYLESHEET_SCENE ); | |
| 66 | ||
| 67 | return scene; | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Creates the class responsible for updating the UI with the caret position | |
| 72 | * based on the active text editor. | |
| 73 | * | |
| 74 | * @return The {@link CaretListener} responsible for updating the | |
| 75 | * {@link StatusBar} whenever the caret changes position. | |
| 76 | */ | |
| 77 | private CaretListener createCaretListener( final MainPane mainPane ) { | |
| 78 | return new CaretListener( mainPane.activeTextEditorProperty() ); | |
| 79 | } | |
| 80 | } | |
| 1 | 81 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 31 | import com.keenwrite.definition.DefinitionFactory; | |
| 32 | import com.keenwrite.definition.DefinitionPane; | |
| 33 | import com.keenwrite.definition.DefinitionSource; | |
| 34 | import com.keenwrite.definition.MapInterpolator; | |
| 35 | import com.keenwrite.definition.yaml.YamlDefinitionSource; | |
| 36 | import com.keenwrite.editors.DefinitionNameInjector; | |
| 37 | import com.keenwrite.editors.markdown.MarkdownEditorPane; | |
| 38 | import com.keenwrite.exceptions.MissingFileException; | |
| 39 | import com.keenwrite.preferences.UserPreferences; | |
| 40 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 41 | import com.keenwrite.processors.Processor; | |
| 42 | import com.keenwrite.processors.ProcessorContext; | |
| 43 | import com.keenwrite.processors.ProcessorFactory; | |
| 44 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 45 | import com.keenwrite.service.Options; | |
| 46 | import com.keenwrite.service.Snitch; | |
| 47 | import com.keenwrite.spelling.api.SpellCheckListener; | |
| 48 | import com.keenwrite.spelling.api.SpellChecker; | |
| 49 | import com.keenwrite.spelling.impl.PermissiveSpeller; | |
| 50 | import com.keenwrite.spelling.impl.SymSpellSpeller; | |
| 51 | import com.keenwrite.util.Action; | |
| 52 | import com.keenwrite.util.ActionUtils; | |
| 53 | import com.keenwrite.util.SeparatorAction; | |
| 54 | import com.vladsch.flexmark.parser.Parser; | |
| 55 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 56 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 57 | import javafx.beans.binding.Bindings; | |
| 58 | import javafx.beans.binding.BooleanBinding; | |
| 59 | import javafx.beans.property.BooleanProperty; | |
| 60 | import javafx.beans.property.SimpleBooleanProperty; | |
| 61 | import javafx.beans.value.ChangeListener; | |
| 62 | import javafx.beans.value.ObservableBooleanValue; | |
| 63 | import javafx.beans.value.ObservableValue; | |
| 64 | import javafx.collections.ListChangeListener.Change; | |
| 65 | import javafx.collections.ObservableList; | |
| 66 | import javafx.event.Event; | |
| 67 | import javafx.event.EventHandler; | |
| 68 | import javafx.geometry.Pos; | |
| 69 | import javafx.scene.Node; | |
| 70 | import javafx.scene.Scene; | |
| 71 | import javafx.scene.control.*; | |
| 72 | import javafx.scene.image.ImageView; | |
| 73 | import javafx.scene.input.KeyEvent; | |
| 74 | import javafx.scene.layout.BorderPane; | |
| 75 | import javafx.scene.layout.VBox; | |
| 76 | import javafx.scene.text.Text; | |
| 77 | import javafx.stage.FileChooser; | |
| 78 | import javafx.stage.Window; | |
| 79 | import javafx.stage.WindowEvent; | |
| 80 | import javafx.util.Duration; | |
| 81 | import org.apache.commons.lang3.SystemUtils; | |
| 82 | import org.controlsfx.control.StatusBar; | |
| 83 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 84 | import org.fxmisc.richtext.model.StyleSpansBuilder; | |
| 85 | import org.reactfx.value.Val; | |
| 86 | ||
| 87 | import java.io.BufferedReader; | |
| 88 | import java.io.File; | |
| 89 | import java.io.IOException; | |
| 90 | import java.io.InputStreamReader; | |
| 91 | import java.nio.file.Path; | |
| 92 | import java.util.*; | |
| 93 | import java.util.concurrent.atomic.AtomicInteger; | |
| 94 | import java.util.function.Consumer; | |
| 95 | import java.util.function.Function; | |
| 96 | import java.util.prefs.Preferences; | |
| 97 | import java.util.stream.Collectors; | |
| 98 | ||
| 99 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 100 | import static com.keenwrite.Constants.*; | |
| 101 | import static com.keenwrite.ExportFormat.*; | |
| 102 | import static com.keenwrite.Messages.get; | |
| 103 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 104 | import static com.keenwrite.processors.ProcessorFactory.processChain; | |
| 105 | import static com.keenwrite.util.StageState.*; | |
| 106 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 107 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 108 | import static java.nio.file.Files.writeString; | |
| 109 | import static java.util.Collections.emptyList; | |
| 110 | import static java.util.Collections.singleton; | |
| 111 | import static javafx.application.Platform.runLater; | |
| 112 | import static javafx.event.Event.fireEvent; | |
| 113 | import static javafx.scene.control.Alert.AlertType.INFORMATION; | |
| 114 | import static javafx.scene.input.KeyCode.ENTER; | |
| 115 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 116 | import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; | |
| 117 | ||
| 118 | /** | |
| 119 | * Main window containing a tab pane in the center for file editors. | |
| 120 | */ | |
| 121 | public class MainWindow implements Observer { | |
| 122 | /** | |
| 123 | * The {@code OPTIONS} variable must be declared before all other variables | |
| 124 | * to prevent subsequent initializations from failing due to missing user | |
| 125 | * preferences. | |
| 126 | */ | |
| 127 | private static final Options sOptions = Services.load( Options.class ); | |
| 128 | private static final Snitch SNITCH = Services.load( Snitch.class ); | |
| 129 | ||
| 130 | private final Scene mScene; | |
| 131 | private final StatusBar mStatusBar; | |
| 132 | private final Text mLineNumberText; | |
| 133 | private final TextField mFindTextField; | |
| 134 | private final SpellChecker mSpellChecker; | |
| 135 | ||
| 136 | private final Object mMutex = new Object(); | |
| 137 | ||
| 138 | /** | |
| 139 | * Prevents re-instantiation of processing classes. | |
| 140 | */ | |
| 141 | private final Map<FileEditorTab, Processor<String>> mProcessors = | |
| 142 | new HashMap<>(); | |
| 143 | ||
| 144 | private final Map<String, String> mResolvedMap = | |
| 145 | new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 146 | ||
| 147 | private final EventHandler<PreferencesFxEvent> mRPreferencesListener = | |
| 148 | event -> rerender(); | |
| 149 | ||
| 150 | /** | |
| 151 | * Called when the definition data is changed. | |
| 152 | */ | |
| 153 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> | |
| 154 | mTreeHandler = event -> { | |
| 155 | exportDefinitions( getDefinitionPath() ); | |
| 156 | interpolateResolvedMap(); | |
| 157 | rerender(); | |
| 158 | }; | |
| 159 | ||
| 160 | /** | |
| 161 | * Called to inject the selected item when the user presses ENTER in the | |
| 162 | * definition pane. | |
| 163 | */ | |
| 164 | private final EventHandler<? super KeyEvent> mDefinitionKeyHandler = | |
| 165 | event -> { | |
| 166 | if( event.getCode() == ENTER ) { | |
| 167 | getDefinitionNameInjector().injectSelectedItem(); | |
| 168 | } | |
| 169 | }; | |
| 170 | ||
| 171 | private final ChangeListener<Integer> mCaretPositionListener = | |
| 172 | ( observable, oldPosition, newPosition ) -> { | |
| 173 | processActiveTab(); | |
| 174 | }; | |
| 175 | ||
| 176 | private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); | |
| 177 | private final DefinitionPane mDefinitionPane = createDefinitionPane(); | |
| 178 | private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); | |
| 179 | private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( | |
| 180 | mCaretPositionListener ); | |
| 181 | ||
| 182 | /** | |
| 183 | * Listens on the definition pane for double-click events. | |
| 184 | */ | |
| 185 | private final DefinitionNameInjector mDefinitionNameInjector | |
| 186 | = new DefinitionNameInjector( mDefinitionPane ); | |
| 187 | ||
| 188 | public MainWindow() { | |
| 189 | mStatusBar = createStatusBar(); | |
| 190 | mLineNumberText = createLineNumberText(); | |
| 191 | mFindTextField = createFindTextField(); | |
| 192 | mScene = createScene(); | |
| 193 | mSpellChecker = createSpellChecker(); | |
| 194 | ||
| 195 | // Add the close request listener before the window is shown. | |
| 196 | initLayout(); | |
| 197 | StatusBarNotifier.setStatusBar( mStatusBar ); | |
| 198 | } | |
| 199 | ||
| 200 | /** | |
| 201 | * Called after the stage is shown. | |
| 202 | */ | |
| 203 | public void init() { | |
| 204 | initFindInput(); | |
| 205 | initSnitch(); | |
| 206 | initDefinitionListener(); | |
| 207 | initTabAddedListener(); | |
| 208 | initTabChangedListener(); | |
| 209 | initPreferences(); | |
| 210 | initVariableNameInjector(); | |
| 211 | } | |
| 212 | ||
| 213 | private void initLayout() { | |
| 214 | final var scene = getScene(); | |
| 215 | ||
| 216 | scene.getStylesheets().add( STYLESHEET_SCENE ); | |
| 217 | scene.windowProperty().addListener( | |
| 218 | ( unused, oldWindow, newWindow ) -> | |
| 219 | newWindow.setOnCloseRequest( | |
| 220 | e -> { | |
| 221 | if( !getFileEditorPane().closeAllEditors() ) { | |
| 222 | e.consume(); | |
| 223 | } | |
| 224 | } | |
| 225 | ) | |
| 226 | ); | |
| 227 | } | |
| 228 | ||
| 229 | /** | |
| 230 | * Initialize the find input text field to listen on F3, ENTER, and | |
| 231 | * ESCAPE key presses. | |
| 232 | */ | |
| 233 | private void initFindInput() { | |
| 234 | final TextField input = getFindTextField(); | |
| 235 | ||
| 236 | input.setOnKeyPressed( ( KeyEvent event ) -> { | |
| 237 | switch( event.getCode() ) { | |
| 238 | case F3: | |
| 239 | case ENTER: | |
| 240 | editFindNext(); | |
| 241 | break; | |
| 242 | case F: | |
| 243 | if( !event.isControlDown() ) { | |
| 244 | break; | |
| 245 | } | |
| 246 | case ESCAPE: | |
| 247 | getStatusBar().setGraphic( null ); | |
| 248 | getActiveFileEditorTab().getEditorPane().requestFocus(); | |
| 249 | break; | |
| 250 | } | |
| 251 | } ); | |
| 252 | ||
| 253 | // Remove when the input field loses focus. | |
| 254 | input.focusedProperty().addListener( | |
| 255 | ( focused, oldFocus, newFocus ) -> { | |
| 256 | if( !newFocus ) { | |
| 257 | getStatusBar().setGraphic( null ); | |
| 258 | } | |
| 259 | } | |
| 260 | ); | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Watch for changes to external files. In particular, this awaits | |
| 265 | * modifications to any XSL files associated with XML files being edited. | |
| 266 | * When | |
| 267 | * an XSL file is modified (external to the application), the snitch's ears | |
| 268 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 269 | * date with what's on the file system. | |
| 270 | */ | |
| 271 | private void initSnitch() { | |
| 272 | SNITCH.addObserver( this ); | |
| 273 | } | |
| 274 | ||
| 275 | /** | |
| 276 | * Listen for {@link FileEditorTabPane} to receive open definition file | |
| 277 | * event. | |
| 278 | */ | |
| 279 | private void initDefinitionListener() { | |
| 280 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 281 | ( final ObservableValue<? extends Path> file, | |
| 282 | final Path oldPath, final Path newPath ) -> { | |
| 283 | openDefinitions( newPath ); | |
| 284 | rerender(); | |
| 285 | } | |
| 286 | ); | |
| 287 | } | |
| 288 | ||
| 289 | /** | |
| 290 | * Re-instantiates all processors then re-renders the active tab. This | |
| 291 | * will refresh the resolved map, force R to re-initialize, and brute-force | |
| 292 | * XSLT file reloads. | |
| 293 | */ | |
| 294 | private void rerender() { | |
| 295 | runLater( | |
| 296 | () -> { | |
| 297 | resetProcessors(); | |
| 298 | processActiveTab(); | |
| 299 | } | |
| 300 | ); | |
| 301 | } | |
| 302 | ||
| 303 | /** | |
| 304 | * When tabs are added, hook the various change listeners onto the new | |
| 305 | * tab sothat the preview pane refreshes as necessary. | |
| 306 | */ | |
| 307 | private void initTabAddedListener() { | |
| 308 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 309 | ||
| 310 | // Make sure the text processor kicks off when new files are opened. | |
| 311 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 312 | ||
| 313 | // Update the preview pane on tab changes. | |
| 314 | tabs.addListener( | |
| 315 | ( final Change<? extends Tab> change ) -> { | |
| 316 | while( change.next() ) { | |
| 317 | if( change.wasAdded() ) { | |
| 318 | // Multiple tabs can be added simultaneously. | |
| 319 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 320 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 321 | ||
| 322 | initTextChangeListener( tab ); | |
| 323 | initScrollEventListener( tab ); | |
| 324 | initSpellCheckListener( tab ); | |
| 325 | // initSyntaxListener( tab ); | |
| 326 | } | |
| 327 | } | |
| 328 | } | |
| 329 | } | |
| 330 | ); | |
| 331 | } | |
| 332 | ||
| 333 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 334 | tab.addTextChangeListener( | |
| 335 | ( __, ov, nv ) -> { | |
| 336 | process( tab ); | |
| 337 | } | |
| 338 | ); | |
| 339 | } | |
| 340 | ||
| 341 | private void initScrollEventListener( final FileEditorTab tab ) { | |
| 342 | final var scrollPane = tab.getScrollPane(); | |
| 343 | final var scrollBar = getPreviewPane().getVerticalScrollBar(); | |
| 344 | ||
| 345 | addShowListener( scrollPane, ( __ ) -> { | |
| 346 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 347 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 348 | } ); | |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Listen for changes to the any particular paragraph and perform a quick | |
| 353 | * spell check upon it. The style classes in the editor will be changed to | |
| 354 | * mark any spelling mistakes in the paragraph. The user may then interact | |
| 355 | * with any misspelled word (i.e., any piece of text that is marked) to | |
| 356 | * revise the spelling. | |
| 357 | * | |
| 358 | * @param tab The tab to spellcheck. | |
| 359 | */ | |
| 360 | private void initSpellCheckListener( final FileEditorTab tab ) { | |
| 361 | final var editor = tab.getEditorPane().getEditor(); | |
| 362 | ||
| 363 | // When the editor first appears, run a full spell check. This allows | |
| 364 | // spell checking while typing to be restricted to the active paragraph, | |
| 365 | // which is usually substantially smaller than the whole document. | |
| 366 | addShowListener( | |
| 367 | editor, ( __ ) -> spellcheck( editor, editor.getText() ) | |
| 368 | ); | |
| 369 | ||
| 370 | // Use the plain text changes so that notifications of style changes | |
| 371 | // are suppressed. Checking against the identity ensures that only | |
| 372 | // new text additions or deletions trigger proofreading. | |
| 373 | editor.plainTextChanges() | |
| 374 | .filter( p -> !p.isIdentity() ).subscribe( change -> { | |
| 375 | ||
| 376 | // Only perform a spell check on the current paragraph. The | |
| 377 | // entire document is processed once, when opened. | |
| 378 | final var offset = change.getPosition(); | |
| 379 | final var position = editor.offsetToPosition( offset, Forward ); | |
| 380 | final var paraId = position.getMajor(); | |
| 381 | final var paragraph = editor.getParagraph( paraId ); | |
| 382 | final var text = paragraph.getText(); | |
| 383 | ||
| 384 | // Ensure that styles aren't doubled-up. | |
| 385 | editor.clearStyle( paraId ); | |
| 386 | ||
| 387 | spellcheck( editor, text, paraId ); | |
| 388 | } ); | |
| 389 | } | |
| 390 | ||
| 391 | /** | |
| 392 | * Listen for new tab selection events. | |
| 393 | */ | |
| 394 | private void initTabChangedListener() { | |
| 395 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 396 | ||
| 397 | // Update the preview pane changing tabs. | |
| 398 | editorPane.addTabSelectionListener( | |
| 399 | ( __, oldTab, newTab ) -> { | |
| 400 | if( newTab == null ) { | |
| 401 | // Clear the preview pane when closing an editor. When the last | |
| 402 | // tab is closed, this ensures that the preview pane is empty. | |
| 403 | getPreviewPane().clear(); | |
| 404 | } | |
| 405 | else { | |
| 406 | final var tab = (FileEditorTab) newTab; | |
| 407 | updateVariableNameInjector( tab ); | |
| 408 | process( tab ); | |
| 409 | } | |
| 410 | } | |
| 411 | ); | |
| 412 | } | |
| 413 | ||
| 414 | /** | |
| 415 | * Reloads the preferences from the previous session. | |
| 416 | */ | |
| 417 | private void initPreferences() { | |
| 418 | initDefinitionPane(); | |
| 419 | getFileEditorPane().initPreferences(); | |
| 420 | getUserPreferences().addSaveEventHandler( mRPreferencesListener ); | |
| 421 | } | |
| 422 | ||
| 423 | private void initVariableNameInjector() { | |
| 424 | updateVariableNameInjector( getActiveFileEditorTab() ); | |
| 425 | } | |
| 426 | ||
| 427 | /** | |
| 428 | * Calls the listener when the given node is shown for the first time. The | |
| 429 | * visible property is not the same as the initial showing event; visibility | |
| 430 | * can be triggered numerous times (such as going off screen). | |
| 431 | * <p> | |
| 432 | * This is called, for example, before the drag handler can be attached, | |
| 433 | * because the scrollbar for the text editor pane must be visible. | |
| 434 | * </p> | |
| 435 | * | |
| 436 | * @param node The node to watch for showing. | |
| 437 | * @param consumer The consumer to invoke when the event fires. | |
| 438 | */ | |
| 439 | private void addShowListener( | |
| 440 | final Node node, final Consumer<Void> consumer ) { | |
| 441 | final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> | |
| 442 | runLater( () -> { | |
| 443 | if( newShow != null && newShow ) { | |
| 444 | try { | |
| 445 | consumer.accept( null ); | |
| 446 | } catch( final Exception ex ) { | |
| 447 | clue( ex ); | |
| 448 | } | |
| 449 | } | |
| 450 | } ); | |
| 451 | ||
| 452 | Val.flatMap( node.sceneProperty(), Scene::windowProperty ) | |
| 453 | .flatMap( Window::showingProperty ) | |
| 454 | .addListener( listener ); | |
| 455 | } | |
| 456 | ||
| 457 | private void scrollToCaret() { | |
| 458 | synchronized( mMutex ) { | |
| 459 | final var previewPane = getPreviewPane(); | |
| 460 | ||
| 461 | previewPane.scrollTo( CARET_ID ); | |
| 462 | previewPane.repaintScrollPane(); | |
| 463 | } | |
| 464 | } | |
| 465 | ||
| 466 | private void updateVariableNameInjector( final FileEditorTab tab ) { | |
| 467 | getDefinitionNameInjector().addListener( tab ); | |
| 468 | } | |
| 469 | ||
| 470 | /** | |
| 471 | * Called to update the status bar's caret position when a new tab is added | |
| 472 | * or the active tab is switched. | |
| 473 | * | |
| 474 | * @param tab The active tab containing a caret position to show. | |
| 475 | */ | |
| 476 | private void updateCaretStatus( final FileEditorTab tab ) { | |
| 477 | getLineNumberText().setText( tab.getCaretPosition().toString() ); | |
| 478 | } | |
| 479 | ||
| 480 | /** | |
| 481 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 482 | * tab. This can be called when the text changes, the caret paragraph | |
| 483 | * changes, or the file tab changes. | |
| 484 | * | |
| 485 | * @param tab The file editor tab that has been changed in some fashion. | |
| 486 | */ | |
| 487 | private void process( final FileEditorTab tab ) { | |
| 488 | if( tab != null ) { | |
| 489 | getPreviewPane().setPath( tab.getPath() ); | |
| 490 | ||
| 491 | final Processor<String> processor = getProcessors().computeIfAbsent( | |
| 492 | tab, p -> createProcessors( tab ) | |
| 493 | ); | |
| 494 | ||
| 495 | try { | |
| 496 | updateCaretStatus( tab ); | |
| 497 | processChain( processor, tab.getEditorText() ); | |
| 498 | scrollToCaret(); | |
| 499 | } catch( final Exception ex ) { | |
| 500 | clue( ex ); | |
| 501 | } | |
| 502 | } | |
| 503 | } | |
| 504 | ||
| 505 | private void processActiveTab() { | |
| 506 | process( getActiveFileEditorTab() ); | |
| 507 | } | |
| 508 | ||
| 509 | /** | |
| 510 | * Called when a definition source is opened. | |
| 511 | * | |
| 512 | * @param path Path to the definition source that was opened. | |
| 513 | */ | |
| 514 | private void openDefinitions( final Path path ) { | |
| 515 | try { | |
| 516 | final var ds = createDefinitionSource( path ); | |
| 517 | setDefinitionSource( ds ); | |
| 518 | ||
| 519 | final var prefs = getUserPreferences(); | |
| 520 | prefs.definitionPathProperty().setValue( path.toFile() ); | |
| 521 | prefs.save(); | |
| 522 | ||
| 523 | final var tooltipPath = new Tooltip( path.toString() ); | |
| 524 | tooltipPath.setShowDelay( Duration.millis( 200 ) ); | |
| 525 | ||
| 526 | final var pane = getDefinitionPane(); | |
| 527 | pane.update( ds ); | |
| 528 | pane.addTreeChangeHandler( mTreeHandler ); | |
| 529 | pane.addKeyEventHandler( mDefinitionKeyHandler ); | |
| 530 | pane.filenameProperty().setValue( path.getFileName().toString() ); | |
| 531 | pane.setTooltip( tooltipPath ); | |
| 532 | ||
| 533 | interpolateResolvedMap(); | |
| 534 | } catch( final Exception ex ) { | |
| 535 | clue( ex ); | |
| 536 | } | |
| 537 | } | |
| 538 | ||
| 539 | private void exportDefinitions( final Path path ) { | |
| 540 | try { | |
| 541 | final var pane = getDefinitionPane(); | |
| 542 | final var root = pane.getTreeView().getRoot(); | |
| 543 | final var problemChild = pane.isTreeWellFormed(); | |
| 544 | ||
| 545 | if( problemChild == null ) { | |
| 546 | getDefinitionSource().getTreeAdapter().export( root, path ); | |
| 547 | } | |
| 548 | else { | |
| 549 | clue( "yaml.error.tree.form", problemChild.getValue() ); | |
| 550 | } | |
| 551 | } catch( final Exception ex ) { | |
| 552 | clue( ex ); | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | private void interpolateResolvedMap() { | |
| 557 | final var treeMap = getDefinitionPane().toMap(); | |
| 558 | final var map = new HashMap<>( treeMap ); | |
| 559 | MapInterpolator.interpolate( map ); | |
| 560 | ||
| 561 | getResolvedMap().clear(); | |
| 562 | getResolvedMap().putAll( map ); | |
| 563 | } | |
| 564 | ||
| 565 | private void initDefinitionPane() { | |
| 566 | openDefinitions( getDefinitionPath() ); | |
| 567 | } | |
| 568 | ||
| 569 | //---- File actions ------------------------------------------------------- | |
| 570 | ||
| 571 | /** | |
| 572 | * Called when an {@link Observable} instance has changed. This is called | |
| 573 | * by both the {@link Snitch} service and the notify service. The @link | |
| 574 | * Snitch} service can be called for different file types, including | |
| 575 | * {@link DefinitionSource} instances. | |
| 576 | * | |
| 577 | * @param observable The observed instance. | |
| 578 | * @param value The noteworthy item. | |
| 579 | */ | |
| 580 | @Override | |
| 581 | public void update( final Observable observable, final Object value ) { | |
| 582 | if( value instanceof Path && observable instanceof Snitch ) { | |
| 583 | updateSelectedTab(); | |
| 584 | } | |
| 585 | } | |
| 586 | ||
| 587 | /** | |
| 588 | * Called when a file has been modified. | |
| 589 | */ | |
| 590 | private void updateSelectedTab() { | |
| 591 | rerender(); | |
| 592 | } | |
| 593 | ||
| 594 | /** | |
| 595 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 596 | * with the files (text and definition) currently loaded into the editor. | |
| 597 | */ | |
| 598 | private void resetProcessors() { | |
| 599 | getProcessors().clear(); | |
| 600 | } | |
| 601 | ||
| 602 | //---- File actions ------------------------------------------------------- | |
| 603 | ||
| 604 | private void fileNew() { | |
| 605 | getFileEditorPane().newEditor(); | |
| 606 | } | |
| 607 | ||
| 608 | private void fileOpen() { | |
| 609 | getFileEditorPane().openFileDialog(); | |
| 610 | } | |
| 611 | ||
| 612 | private void fileClose() { | |
| 613 | getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); | |
| 614 | } | |
| 615 | ||
| 616 | /** | |
| 617 | * TODO: Upon closing, first remove the tab change listeners. (There's no | |
| 618 | * need to re-render each tab when all are being closed.) | |
| 619 | */ | |
| 620 | private void fileCloseAll() { | |
| 621 | getFileEditorPane().closeAllEditors(); | |
| 622 | } | |
| 623 | ||
| 624 | private void fileSave() { | |
| 625 | getFileEditorPane().saveEditor( getActiveFileEditorTab() ); | |
| 626 | } | |
| 627 | ||
| 628 | private void fileSaveAs() { | |
| 629 | final FileEditorTab editor = getActiveFileEditorTab(); | |
| 630 | getFileEditorPane().saveEditorAs( editor ); | |
| 631 | getProcessors().remove( editor ); | |
| 632 | ||
| 633 | try { | |
| 634 | process( editor ); | |
| 635 | } catch( final Exception ex ) { | |
| 636 | clue( ex ); | |
| 637 | } | |
| 638 | } | |
| 639 | ||
| 640 | private void fileSaveAll() { | |
| 641 | getFileEditorPane().saveAllEditors(); | |
| 642 | } | |
| 643 | ||
| 644 | /** | |
| 645 | * Exports the contents of the current tab according to the given | |
| 646 | * {@link ExportFormat}. | |
| 647 | * | |
| 648 | * @param format Configures the {@link MarkdownProcessor} when exporting. | |
| 649 | */ | |
| 650 | private void fileExport( final ExportFormat format ) { | |
| 651 | final var tab = getActiveFileEditorTab(); | |
| 652 | final var context = createProcessorContext( tab, format ); | |
| 653 | final var chain = ProcessorFactory.createProcessors( context ); | |
| 654 | final var doc = tab.getEditorText(); | |
| 655 | final var export = processChain( chain, doc ); | |
| 656 | ||
| 657 | final var filename = format.toExportFilename( tab.getPath().toFile() ); | |
| 658 | final var dir = getPreferences().get( "lastDirectory", null ); | |
| 659 | final var lastDir = new File( dir == null ? "." : dir ); | |
| 660 | ||
| 661 | final FileChooser chooser = new FileChooser(); | |
| 662 | chooser.setTitle( get( "Dialog.file.choose.export.title" ) ); | |
| 663 | chooser.setInitialFileName( filename.getName() ); | |
| 664 | chooser.setInitialDirectory( lastDir ); | |
| 665 | ||
| 666 | final File file = chooser.showSaveDialog( getWindow() ); | |
| 667 | ||
| 668 | if( file != null ) { | |
| 669 | try { | |
| 670 | writeString( file.toPath(), export, UTF_8 ); | |
| 671 | final var m = get( "Main.status.export.success", file.toString() ); | |
| 672 | clue( m ); | |
| 673 | } catch( final IOException e ) { | |
| 674 | clue( e ); | |
| 675 | } | |
| 676 | } | |
| 677 | } | |
| 678 | ||
| 679 | private void fileExit() { | |
| 680 | final Window window = getWindow(); | |
| 681 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 682 | } | |
| 683 | ||
| 684 | //---- Edit actions ------------------------------------------------------- | |
| 685 | ||
| 686 | /** | |
| 687 | * Used to find text in the active file editor window. | |
| 688 | */ | |
| 689 | private void editFind() { | |
| 690 | final TextField input = getFindTextField(); | |
| 691 | getStatusBar().setGraphic( input ); | |
| 692 | input.requestFocus(); | |
| 693 | } | |
| 694 | ||
| 695 | public void editFindNext() { | |
| 696 | getActiveFileEditorTab().searchNext( getFindTextField().getText() ); | |
| 697 | } | |
| 698 | ||
| 699 | public void editPreferences() { | |
| 700 | getUserPreferences().show(); | |
| 701 | } | |
| 702 | ||
| 703 | //---- Insert actions ----------------------------------------------------- | |
| 704 | ||
| 705 | /** | |
| 706 | * Delegates to the active editor to handle wrapping the current text | |
| 707 | * selection with leading and trailing strings. | |
| 708 | * | |
| 709 | * @param leading The string to put before the selection. | |
| 710 | * @param trailing The string to put after the selection. | |
| 711 | */ | |
| 712 | private void insertMarkdown( | |
| 713 | final String leading, final String trailing ) { | |
| 714 | getActiveEditorPane().surroundSelection( leading, trailing ); | |
| 715 | } | |
| 716 | ||
| 717 | private void insertMarkdown( | |
| 718 | final String leading, final String trailing, final String hint ) { | |
| 719 | getActiveEditorPane().surroundSelection( leading, trailing, hint ); | |
| 720 | } | |
| 721 | ||
| 722 | //---- Help actions ------------------------------------------------------- | |
| 723 | ||
| 724 | private void helpAbout() { | |
| 725 | final Alert alert = new Alert( INFORMATION ); | |
| 726 | alert.setTitle( get( "Dialog.about.title", APP_TITLE ) ); | |
| 727 | alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) ); | |
| 728 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 729 | alert.setGraphic( new ImageView( ICON_DIALOG ) ); | |
| 730 | alert.initOwner( getWindow() ); | |
| 731 | ||
| 732 | alert.showAndWait(); | |
| 733 | } | |
| 734 | ||
| 735 | //---- Member creators ---------------------------------------------------- | |
| 736 | ||
| 737 | private SpellChecker createSpellChecker() { | |
| 738 | try { | |
| 739 | final Collection<String> lexicon = readLexicon( "en.txt" ); | |
| 740 | return SymSpellSpeller.forLexicon( lexicon ); | |
| 741 | } catch( final Exception ex ) { | |
| 742 | clue( ex ); | |
| 743 | return new PermissiveSpeller(); | |
| 744 | } | |
| 745 | } | |
| 746 | ||
| 747 | /** | |
| 748 | * Creates processors suited to parsing and rendering different file types. | |
| 749 | * | |
| 750 | * @param tab The tab that is subjected to processing. | |
| 751 | * @return A processor suited to the file type specified by the tab's path. | |
| 752 | */ | |
| 753 | private Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 754 | final var context = createProcessorContext( tab ); | |
| 755 | return ProcessorFactory.createProcessors( context ); | |
| 756 | } | |
| 757 | ||
| 758 | private ProcessorContext createProcessorContext( | |
| 759 | final FileEditorTab tab, final ExportFormat format ) { | |
| 760 | final var pane = getPreviewPane(); | |
| 761 | final var map = getResolvedMap(); | |
| 762 | return new ProcessorContext( pane, map, tab, format ); | |
| 763 | } | |
| 764 | ||
| 765 | private ProcessorContext createProcessorContext( final FileEditorTab tab ) { | |
| 766 | return createProcessorContext( tab, NONE ); | |
| 767 | } | |
| 768 | ||
| 769 | private DefinitionPane createDefinitionPane() { | |
| 770 | return new DefinitionPane(); | |
| 771 | } | |
| 772 | ||
| 773 | private HTMLPreviewPane createHTMLPreviewPane() { | |
| 774 | return new HTMLPreviewPane(); | |
| 775 | } | |
| 776 | ||
| 777 | private DefinitionSource createDefaultDefinitionSource() { | |
| 778 | return new YamlDefinitionSource( getDefinitionPath() ); | |
| 779 | } | |
| 780 | ||
| 781 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 782 | try { | |
| 783 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 784 | } catch( final Exception ex ) { | |
| 785 | clue( ex ); | |
| 786 | return createDefaultDefinitionSource(); | |
| 787 | } | |
| 788 | } | |
| 789 | ||
| 790 | private TextField createFindTextField() { | |
| 791 | return new TextField(); | |
| 792 | } | |
| 793 | ||
| 794 | private DefinitionFactory createDefinitionFactory() { | |
| 795 | return new DefinitionFactory(); | |
| 796 | } | |
| 797 | ||
| 798 | private StatusBar createStatusBar() { | |
| 799 | return new StatusBar(); | |
| 800 | } | |
| 801 | ||
| 802 | private Scene createScene() { | |
| 803 | final SplitPane splitPane = new SplitPane( | |
| 804 | getDefinitionPane(), | |
| 805 | getFileEditorPane(), | |
| 806 | getPreviewPane() ); | |
| 807 | ||
| 808 | splitPane.setDividerPositions( | |
| 809 | getFloat( K_PANE_SPLIT_DEFINITION, .22f ), | |
| 810 | getFloat( K_PANE_SPLIT_EDITOR, .60f ), | |
| 811 | getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); | |
| 812 | ||
| 813 | getDefinitionPane().prefHeightProperty() | |
| 814 | .bind( splitPane.heightProperty() ); | |
| 815 | ||
| 816 | final BorderPane borderPane = new BorderPane(); | |
| 817 | borderPane.setPrefSize( 1280, 800 ); | |
| 818 | borderPane.setTop( createMenuBar() ); | |
| 819 | borderPane.setBottom( getStatusBar() ); | |
| 820 | borderPane.setCenter( splitPane ); | |
| 821 | ||
| 822 | final VBox statusBar = new VBox(); | |
| 823 | statusBar.setAlignment( Pos.BASELINE_CENTER ); | |
| 824 | statusBar.getChildren().add( getLineNumberText() ); | |
| 825 | getStatusBar().getRightItems().add( statusBar ); | |
| 826 | ||
| 827 | // Force preview pane refresh on Windows. | |
| 828 | if( SystemUtils.IS_OS_WINDOWS ) { | |
| 829 | splitPane.getDividers().get( 1 ).positionProperty().addListener( | |
| 830 | ( l, oValue, nValue ) -> runLater( | |
| 831 | () -> getPreviewPane().repaintScrollPane() | |
| 832 | ) | |
| 833 | ); | |
| 834 | } | |
| 835 | ||
| 836 | return new Scene( borderPane ); | |
| 837 | } | |
| 838 | ||
| 839 | private Text createLineNumberText() { | |
| 840 | return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); | |
| 841 | } | |
| 842 | ||
| 843 | private Node createMenuBar() { | |
| 844 | final BooleanBinding activeFileEditorIsNull = | |
| 845 | getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 846 | ||
| 847 | // File actions | |
| 848 | final Action fileNewAction = Action | |
| 849 | .builder() | |
| 850 | .setText( "Main.menu.file.new" ) | |
| 851 | .setAccelerator( "Shortcut+N" ) | |
| 852 | .setIcon( FILE_ALT ) | |
| 853 | .setAction( e -> fileNew() ) | |
| 854 | .build(); | |
| 855 | final Action fileOpenAction = Action | |
| 856 | .builder() | |
| 857 | .setText( "Main.menu.file.open" ) | |
| 858 | .setAccelerator( "Shortcut+O" ) | |
| 859 | .setIcon( FOLDER_OPEN_ALT ) | |
| 860 | .setAction( e -> fileOpen() ) | |
| 861 | .build(); | |
| 862 | final Action fileCloseAction = Action | |
| 863 | .builder() | |
| 864 | .setText( "Main.menu.file.close" ) | |
| 865 | .setAccelerator( "Shortcut+W" ) | |
| 866 | .setAction( e -> fileClose() ) | |
| 867 | .setDisabled( activeFileEditorIsNull ) | |
| 868 | .build(); | |
| 869 | final Action fileCloseAllAction = Action | |
| 870 | .builder() | |
| 871 | .setText( "Main.menu.file.close_all" ) | |
| 872 | .setAction( e -> fileCloseAll() ) | |
| 873 | .setDisabled( activeFileEditorIsNull ) | |
| 874 | .build(); | |
| 875 | final Action fileSaveAction = Action | |
| 876 | .builder() | |
| 877 | .setText( "Main.menu.file.save" ) | |
| 878 | .setAccelerator( "Shortcut+S" ) | |
| 879 | .setIcon( FLOPPY_ALT ) | |
| 880 | .setAction( e -> fileSave() ) | |
| 881 | .setDisabled( createActiveBooleanProperty( | |
| 882 | FileEditorTab::modifiedProperty ).not() ) | |
| 883 | .build(); | |
| 884 | final Action fileSaveAsAction = Action | |
| 885 | .builder() | |
| 886 | .setText( "Main.menu.file.save_as" ) | |
| 887 | .setAction( e -> fileSaveAs() ) | |
| 888 | .setDisabled( activeFileEditorIsNull ) | |
| 889 | .build(); | |
| 890 | final Action fileSaveAllAction = Action | |
| 891 | .builder() | |
| 892 | .setText( "Main.menu.file.save_all" ) | |
| 893 | .setAccelerator( "Shortcut+Shift+S" ) | |
| 894 | .setAction( e -> fileSaveAll() ) | |
| 895 | .setDisabled( Bindings.not( | |
| 896 | getFileEditorPane().anyFileEditorModifiedProperty() ) ) | |
| 897 | .build(); | |
| 898 | final Action fileExportAction = Action | |
| 899 | .builder() | |
| 900 | .setText( "Main.menu.file.export" ) | |
| 901 | .build(); | |
| 902 | final Action fileExportHtmlSvgAction = Action | |
| 903 | .builder() | |
| 904 | .setText( "Main.menu.file.export.html_svg" ) | |
| 905 | .setAction( e -> fileExport( HTML_TEX_SVG ) ) | |
| 906 | .build(); | |
| 907 | final Action fileExportHtmlTexAction = Action | |
| 908 | .builder() | |
| 909 | .setText( "Main.menu.file.export.html_tex" ) | |
| 910 | .setAction( e -> fileExport( HTML_TEX_DELIMITED ) ) | |
| 911 | .build(); | |
| 912 | final Action fileExportMarkdownAction = Action | |
| 913 | .builder() | |
| 914 | .setText( "Main.menu.file.export.markdown" ) | |
| 915 | .setAction( e -> fileExport( MARKDOWN_PLAIN ) ) | |
| 916 | .build(); | |
| 917 | fileExportAction.addSubActions( | |
| 918 | fileExportHtmlSvgAction, | |
| 919 | fileExportHtmlTexAction, | |
| 920 | fileExportMarkdownAction ); | |
| 921 | ||
| 922 | final Action fileExitAction = Action | |
| 923 | .builder() | |
| 924 | .setText( "Main.menu.file.exit" ) | |
| 925 | .setAction( e -> fileExit() ) | |
| 926 | .build(); | |
| 927 | ||
| 928 | // Edit actions | |
| 929 | final Action editUndoAction = Action | |
| 930 | .builder() | |
| 931 | .setText( "Main.menu.edit.undo" ) | |
| 932 | .setAccelerator( "Shortcut+Z" ) | |
| 933 | .setIcon( UNDO ) | |
| 934 | .setAction( e -> getActiveEditorPane().undo() ) | |
| 935 | .setDisabled( createActiveBooleanProperty( | |
| 936 | FileEditorTab::canUndoProperty ).not() ) | |
| 937 | .build(); | |
| 938 | final Action editRedoAction = Action | |
| 939 | .builder() | |
| 940 | .setText( "Main.menu.edit.redo" ) | |
| 941 | .setAccelerator( "Shortcut+Y" ) | |
| 942 | .setIcon( REPEAT ) | |
| 943 | .setAction( e -> getActiveEditorPane().redo() ) | |
| 944 | .setDisabled( createActiveBooleanProperty( | |
| 945 | FileEditorTab::canRedoProperty ).not() ) | |
| 946 | .build(); | |
| 947 | ||
| 948 | final Action editCutAction = Action | |
| 949 | .builder() | |
| 950 | .setText( "Main.menu.edit.cut" ) | |
| 951 | .setAccelerator( "Shortcut+X" ) | |
| 952 | .setIcon( CUT ) | |
| 953 | .setAction( e -> getActiveEditorPane().cut() ) | |
| 954 | .setDisabled( activeFileEditorIsNull ) | |
| 955 | .build(); | |
| 956 | final Action editCopyAction = Action | |
| 957 | .builder() | |
| 958 | .setText( "Main.menu.edit.copy" ) | |
| 959 | .setAccelerator( "Shortcut+C" ) | |
| 960 | .setIcon( COPY ) | |
| 961 | .setAction( e -> getActiveEditorPane().copy() ) | |
| 962 | .setDisabled( activeFileEditorIsNull ) | |
| 963 | .build(); | |
| 964 | final Action editPasteAction = Action | |
| 965 | .builder() | |
| 966 | .setText( "Main.menu.edit.paste" ) | |
| 967 | .setAccelerator( "Shortcut+V" ) | |
| 968 | .setIcon( PASTE ) | |
| 969 | .setAction( e -> getActiveEditorPane().paste() ) | |
| 970 | .setDisabled( activeFileEditorIsNull ) | |
| 971 | .build(); | |
| 972 | final Action editSelectAllAction = Action | |
| 973 | .builder() | |
| 974 | .setText( "Main.menu.edit.selectAll" ) | |
| 975 | .setAccelerator( "Shortcut+A" ) | |
| 976 | .setAction( e -> getActiveEditorPane().selectAll() ) | |
| 977 | .setDisabled( activeFileEditorIsNull ) | |
| 978 | .build(); | |
| 979 | ||
| 980 | final Action editFindAction = Action | |
| 981 | .builder() | |
| 982 | .setText( "Main.menu.edit.find" ) | |
| 983 | .setAccelerator( "Ctrl+F" ) | |
| 984 | .setIcon( SEARCH ) | |
| 985 | .setAction( e -> editFind() ) | |
| 986 | .setDisabled( activeFileEditorIsNull ) | |
| 987 | .build(); | |
| 988 | final Action editFindNextAction = Action | |
| 989 | .builder() | |
| 990 | .setText( "Main.menu.edit.find.next" ) | |
| 991 | .setAccelerator( "F3" ) | |
| 992 | .setAction( e -> editFindNext() ) | |
| 993 | .setDisabled( activeFileEditorIsNull ) | |
| 994 | .build(); | |
| 995 | final Action editPreferencesAction = Action | |
| 996 | .builder() | |
| 997 | .setText( "Main.menu.edit.preferences" ) | |
| 998 | .setAccelerator( "Ctrl+Alt+S" ) | |
| 999 | .setAction( e -> editPreferences() ) | |
| 1000 | .build(); | |
| 1001 | ||
| 1002 | // Format actions | |
| 1003 | final Action formatBoldAction = Action | |
| 1004 | .builder() | |
| 1005 | .setText( "Main.menu.format.bold" ) | |
| 1006 | .setAccelerator( "Shortcut+B" ) | |
| 1007 | .setIcon( BOLD ) | |
| 1008 | .setAction( e -> insertMarkdown( "**", "**" ) ) | |
| 1009 | .setDisabled( activeFileEditorIsNull ) | |
| 1010 | .build(); | |
| 1011 | final Action formatItalicAction = Action | |
| 1012 | .builder() | |
| 1013 | .setText( "Main.menu.format.italic" ) | |
| 1014 | .setAccelerator( "Shortcut+I" ) | |
| 1015 | .setIcon( ITALIC ) | |
| 1016 | .setAction( e -> insertMarkdown( "*", "*" ) ) | |
| 1017 | .setDisabled( activeFileEditorIsNull ) | |
| 1018 | .build(); | |
| 1019 | final Action formatSuperscriptAction = Action | |
| 1020 | .builder() | |
| 1021 | .setText( "Main.menu.format.superscript" ) | |
| 1022 | .setAccelerator( "Shortcut+[" ) | |
| 1023 | .setIcon( SUPERSCRIPT ) | |
| 1024 | .setAction( e -> insertMarkdown( "^", "^" ) ) | |
| 1025 | .setDisabled( activeFileEditorIsNull ) | |
| 1026 | .build(); | |
| 1027 | final Action formatSubscriptAction = Action | |
| 1028 | .builder() | |
| 1029 | .setText( "Main.menu.format.subscript" ) | |
| 1030 | .setAccelerator( "Shortcut+]" ) | |
| 1031 | .setIcon( SUBSCRIPT ) | |
| 1032 | .setAction( e -> insertMarkdown( "~", "~" ) ) | |
| 1033 | .setDisabled( activeFileEditorIsNull ) | |
| 1034 | .build(); | |
| 1035 | final Action formatStrikethroughAction = Action | |
| 1036 | .builder() | |
| 1037 | .setText( "Main.menu.format.strikethrough" ) | |
| 1038 | .setAccelerator( "Shortcut+T" ) | |
| 1039 | .setIcon( STRIKETHROUGH ) | |
| 1040 | .setAction( e -> insertMarkdown( "~~", "~~" ) ) | |
| 1041 | .setDisabled( activeFileEditorIsNull ) | |
| 1042 | .build(); | |
| 1043 | ||
| 1044 | // Insert actions | |
| 1045 | final Action insertBlockquoteAction = Action | |
| 1046 | .builder() | |
| 1047 | .setText( "Main.menu.insert.blockquote" ) | |
| 1048 | .setAccelerator( "Ctrl+Q" ) | |
| 1049 | .setIcon( QUOTE_LEFT ) | |
| 1050 | .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) | |
| 1051 | .setDisabled( activeFileEditorIsNull ) | |
| 1052 | .build(); | |
| 1053 | final Action insertCodeAction = Action | |
| 1054 | .builder() | |
| 1055 | .setText( "Main.menu.insert.code" ) | |
| 1056 | .setAccelerator( "Shortcut+K" ) | |
| 1057 | .setIcon( CODE ) | |
| 1058 | .setAction( e -> insertMarkdown( "`", "`" ) ) | |
| 1059 | .setDisabled( activeFileEditorIsNull ) | |
| 1060 | .build(); | |
| 1061 | final Action insertFencedCodeBlockAction = Action | |
| 1062 | .builder() | |
| 1063 | .setText( "Main.menu.insert.fenced_code_block" ) | |
| 1064 | .setAccelerator( "Shortcut+Shift+K" ) | |
| 1065 | .setIcon( FILE_CODE_ALT ) | |
| 1066 | .setAction( e -> insertMarkdown( | |
| 1067 | "\n\n```\n", | |
| 1068 | "\n```\n\n", | |
| 1069 | get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) | |
| 1070 | .setDisabled( activeFileEditorIsNull ) | |
| 1071 | .build(); | |
| 1072 | final Action insertLinkAction = Action | |
| 1073 | .builder() | |
| 1074 | .setText( "Main.menu.insert.link" ) | |
| 1075 | .setAccelerator( "Shortcut+L" ) | |
| 1076 | .setIcon( LINK ) | |
| 1077 | .setAction( e -> getActiveEditorPane().insertLink() ) | |
| 1078 | .setDisabled( activeFileEditorIsNull ) | |
| 1079 | .build(); | |
| 1080 | final Action insertImageAction = Action | |
| 1081 | .builder() | |
| 1082 | .setText( "Main.menu.insert.image" ) | |
| 1083 | .setAccelerator( "Shortcut+G" ) | |
| 1084 | .setIcon( PICTURE_ALT ) | |
| 1085 | .setAction( e -> getActiveEditorPane().insertImage() ) | |
| 1086 | .setDisabled( activeFileEditorIsNull ) | |
| 1087 | .build(); | |
| 1088 | ||
| 1089 | // Number of heading actions (H1 ... H3) | |
| 1090 | final int HEADINGS = 3; | |
| 1091 | final Action[] headings = new Action[ HEADINGS ]; | |
| 1092 | ||
| 1093 | for( int i = 1; i <= HEADINGS; i++ ) { | |
| 1094 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 1095 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 1096 | final String text = "Main.menu.insert.heading." + i; | |
| 1097 | final String accelerator = "Shortcut+" + i; | |
| 1098 | final String prompt = text + ".prompt"; | |
| 1099 | ||
| 1100 | headings[ i - 1 ] = Action | |
| 1101 | .builder() | |
| 1102 | .setText( text ) | |
| 1103 | .setAccelerator( accelerator ) | |
| 1104 | .setIcon( HEADER ) | |
| 1105 | .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) | |
| 1106 | .setDisabled( activeFileEditorIsNull ) | |
| 1107 | .build(); | |
| 1108 | } | |
| 1109 | ||
| 1110 | final Action insertUnorderedListAction = Action | |
| 1111 | .builder() | |
| 1112 | .setText( "Main.menu.insert.unordered_list" ) | |
| 1113 | .setAccelerator( "Shortcut+U" ) | |
| 1114 | .setIcon( LIST_UL ) | |
| 1115 | .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) | |
| 1116 | .setDisabled( activeFileEditorIsNull ) | |
| 1117 | .build(); | |
| 1118 | final Action insertOrderedListAction = Action | |
| 1119 | .builder() | |
| 1120 | .setText( "Main.menu.insert.ordered_list" ) | |
| 1121 | .setAccelerator( "Shortcut+Shift+O" ) | |
| 1122 | .setIcon( LIST_OL ) | |
| 1123 | .setAction( e -> insertMarkdown( | |
| 1124 | "\n\n1. ", "" ) ) | |
| 1125 | .setDisabled( activeFileEditorIsNull ) | |
| 1126 | .build(); | |
| 1127 | final Action insertHorizontalRuleAction = Action | |
| 1128 | .builder() | |
| 1129 | .setText( "Main.menu.insert.horizontal_rule" ) | |
| 1130 | .setAccelerator( "Shortcut+H" ) | |
| 1131 | .setAction( e -> insertMarkdown( | |
| 1132 | "\n\n---\n\n", "" ) ) | |
| 1133 | .setDisabled( activeFileEditorIsNull ) | |
| 1134 | .build(); | |
| 1135 | ||
| 1136 | // Definition actions | |
| 1137 | final Action definitionCreateAction = Action | |
| 1138 | .builder() | |
| 1139 | .setText( "Main.menu.definition.create" ) | |
| 1140 | .setIcon( TREE ) | |
| 1141 | .setAction( e -> getDefinitionPane().addItem() ) | |
| 1142 | .build(); | |
| 1143 | final Action definitionInsertAction = Action | |
| 1144 | .builder() | |
| 1145 | .setText( "Main.menu.definition.insert" ) | |
| 1146 | .setAccelerator( "Ctrl+Space" ) | |
| 1147 | .setIcon( STAR ) | |
| 1148 | .setAction( e -> definitionInsert() ) | |
| 1149 | .build(); | |
| 1150 | ||
| 1151 | // Help actions | |
| 1152 | final Action helpAboutAction = Action | |
| 1153 | .builder() | |
| 1154 | .setText( "Main.menu.help.about" ) | |
| 1155 | .setAction( e -> helpAbout() ) | |
| 1156 | .build(); | |
| 1157 | ||
| 1158 | final Action SEPARATOR_ACTION = new SeparatorAction(); | |
| 1159 | ||
| 1160 | //---- MenuBar ---- | |
| 1161 | ||
| 1162 | // File Menu | |
| 1163 | final var fileMenu = ActionUtils.createMenu( | |
| 1164 | get( "Main.menu.file" ), | |
| 1165 | fileNewAction, | |
| 1166 | fileOpenAction, | |
| 1167 | SEPARATOR_ACTION, | |
| 1168 | fileCloseAction, | |
| 1169 | fileCloseAllAction, | |
| 1170 | SEPARATOR_ACTION, | |
| 1171 | fileSaveAction, | |
| 1172 | fileSaveAsAction, | |
| 1173 | fileSaveAllAction, | |
| 1174 | SEPARATOR_ACTION, | |
| 1175 | fileExportAction, | |
| 1176 | SEPARATOR_ACTION, | |
| 1177 | fileExitAction ); | |
| 1178 | ||
| 1179 | // Edit Menu | |
| 1180 | final var editMenu = ActionUtils.createMenu( | |
| 1181 | get( "Main.menu.edit" ), | |
| 1182 | SEPARATOR_ACTION, | |
| 1183 | editUndoAction, | |
| 1184 | editRedoAction, | |
| 1185 | SEPARATOR_ACTION, | |
| 1186 | editCutAction, | |
| 1187 | editCopyAction, | |
| 1188 | editPasteAction, | |
| 1189 | editSelectAllAction, | |
| 1190 | SEPARATOR_ACTION, | |
| 1191 | editFindAction, | |
| 1192 | editFindNextAction, | |
| 1193 | SEPARATOR_ACTION, | |
| 1194 | editPreferencesAction ); | |
| 1195 | ||
| 1196 | // Format Menu | |
| 1197 | final var formatMenu = ActionUtils.createMenu( | |
| 1198 | get( "Main.menu.format" ), | |
| 1199 | formatBoldAction, | |
| 1200 | formatItalicAction, | |
| 1201 | formatSuperscriptAction, | |
| 1202 | formatSubscriptAction, | |
| 1203 | formatStrikethroughAction | |
| 1204 | ); | |
| 1205 | ||
| 1206 | // Insert Menu | |
| 1207 | final var insertMenu = ActionUtils.createMenu( | |
| 1208 | get( "Main.menu.insert" ), | |
| 1209 | insertBlockquoteAction, | |
| 1210 | insertCodeAction, | |
| 1211 | insertFencedCodeBlockAction, | |
| 1212 | SEPARATOR_ACTION, | |
| 1213 | insertLinkAction, | |
| 1214 | insertImageAction, | |
| 1215 | SEPARATOR_ACTION, | |
| 1216 | headings[ 0 ], | |
| 1217 | headings[ 1 ], | |
| 1218 | headings[ 2 ], | |
| 1219 | SEPARATOR_ACTION, | |
| 1220 | insertUnorderedListAction, | |
| 1221 | insertOrderedListAction, | |
| 1222 | insertHorizontalRuleAction | |
| 1223 | ); | |
| 1224 | ||
| 1225 | // Definition Menu | |
| 1226 | final var definitionMenu = ActionUtils.createMenu( | |
| 1227 | get( "Main.menu.definition" ), | |
| 1228 | definitionCreateAction, | |
| 1229 | definitionInsertAction ); | |
| 1230 | ||
| 1231 | // Help Menu | |
| 1232 | final var helpMenu = ActionUtils.createMenu( | |
| 1233 | get( "Main.menu.help" ), | |
| 1234 | helpAboutAction ); | |
| 1235 | ||
| 1236 | //---- MenuBar ---- | |
| 1237 | final var menuBar = new MenuBar( | |
| 1238 | fileMenu, | |
| 1239 | editMenu, | |
| 1240 | formatMenu, | |
| 1241 | insertMenu, | |
| 1242 | definitionMenu, | |
| 1243 | helpMenu ); | |
| 1244 | ||
| 1245 | //---- ToolBar ---- | |
| 1246 | final var toolBar = ActionUtils.createToolBar( | |
| 1247 | fileNewAction, | |
| 1248 | fileOpenAction, | |
| 1249 | fileSaveAction, | |
| 1250 | SEPARATOR_ACTION, | |
| 1251 | editUndoAction, | |
| 1252 | editRedoAction, | |
| 1253 | editCutAction, | |
| 1254 | editCopyAction, | |
| 1255 | editPasteAction, | |
| 1256 | SEPARATOR_ACTION, | |
| 1257 | formatBoldAction, | |
| 1258 | formatItalicAction, | |
| 1259 | formatSuperscriptAction, | |
| 1260 | formatSubscriptAction, | |
| 1261 | insertBlockquoteAction, | |
| 1262 | insertCodeAction, | |
| 1263 | insertFencedCodeBlockAction, | |
| 1264 | SEPARATOR_ACTION, | |
| 1265 | insertLinkAction, | |
| 1266 | insertImageAction, | |
| 1267 | SEPARATOR_ACTION, | |
| 1268 | headings[ 0 ], | |
| 1269 | SEPARATOR_ACTION, | |
| 1270 | insertUnorderedListAction, | |
| 1271 | insertOrderedListAction ); | |
| 1272 | ||
| 1273 | return new VBox( menuBar, toolBar ); | |
| 1274 | } | |
| 1275 | ||
| 1276 | /** | |
| 1277 | * Performs the autoinsert function on the active file editor. | |
| 1278 | */ | |
| 1279 | private void definitionInsert() { | |
| 1280 | getDefinitionNameInjector().autoinsert(); | |
| 1281 | } | |
| 1282 | ||
| 1283 | /** | |
| 1284 | * Creates a boolean property that is bound to another boolean value of the | |
| 1285 | * active editor. | |
| 1286 | */ | |
| 1287 | private BooleanProperty createActiveBooleanProperty( | |
| 1288 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 1289 | ||
| 1290 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 1291 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 1292 | ||
| 1293 | if( tab != null ) { | |
| 1294 | b.bind( func.apply( tab ) ); | |
| 1295 | } | |
| 1296 | ||
| 1297 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 1298 | ( observable, oldFileEditor, newFileEditor ) -> { | |
| 1299 | b.unbind(); | |
| 1300 | ||
| 1301 | if( newFileEditor == null ) { | |
| 1302 | b.set( false ); | |
| 1303 | } | |
| 1304 | else { | |
| 1305 | b.bind( func.apply( newFileEditor ) ); | |
| 1306 | } | |
| 1307 | } | |
| 1308 | ); | |
| 1309 | ||
| 1310 | return b; | |
| 1311 | } | |
| 1312 | ||
| 1313 | //---- Convenience accessors ---------------------------------------------- | |
| 1314 | ||
| 1315 | private Preferences getPreferences() { | |
| 1316 | return sOptions.getState(); | |
| 1317 | } | |
| 1318 | ||
| 1319 | private int getCurrentParagraphIndex() { | |
| 1320 | return getActiveEditorPane().getCurrentParagraphIndex(); | |
| 1321 | } | |
| 1322 | ||
| 1323 | private float getFloat( final String key, final float defaultValue ) { | |
| 1324 | return getPreferences().getFloat( key, defaultValue ); | |
| 1325 | } | |
| 1326 | ||
| 1327 | public Window getWindow() { | |
| 1328 | return getScene().getWindow(); | |
| 1329 | } | |
| 1330 | ||
| 1331 | private MarkdownEditorPane getActiveEditorPane() { | |
| 1332 | return getActiveFileEditorTab().getEditorPane(); | |
| 1333 | } | |
| 1334 | ||
| 1335 | private FileEditorTab getActiveFileEditorTab() { | |
| 1336 | return getFileEditorPane().getActiveFileEditor(); | |
| 1337 | } | |
| 1338 | ||
| 1339 | //---- Member accessors --------------------------------------------------- | |
| 1340 | ||
| 1341 | protected Scene getScene() { | |
| 1342 | return mScene; | |
| 1343 | } | |
| 1344 | ||
| 1345 | private SpellChecker getSpellChecker() { | |
| 1346 | return mSpellChecker; | |
| 1347 | } | |
| 1348 | ||
| 1349 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 1350 | return mProcessors; | |
| 1351 | } | |
| 1352 | ||
| 1353 | private FileEditorTabPane getFileEditorPane() { | |
| 1354 | return mFileEditorPane; | |
| 1355 | } | |
| 1356 | ||
| 1357 | private HTMLPreviewPane getPreviewPane() { | |
| 1358 | return mPreviewPane; | |
| 1359 | } | |
| 1360 | ||
| 1361 | private void setDefinitionSource( | |
| 1362 | final DefinitionSource definitionSource ) { | |
| 1363 | assert definitionSource != null; | |
| 1364 | mDefinitionSource = definitionSource; | |
| 1365 | } | |
| 1366 | ||
| 1367 | private DefinitionSource getDefinitionSource() { | |
| 1368 | return mDefinitionSource; | |
| 1369 | } | |
| 1370 | ||
| 1371 | private DefinitionPane getDefinitionPane() { | |
| 1372 | return mDefinitionPane; | |
| 1373 | } | |
| 1374 | ||
| 1375 | private Text getLineNumberText() { | |
| 1376 | return mLineNumberText; | |
| 1377 | } | |
| 1378 | ||
| 1379 | private StatusBar getStatusBar() { | |
| 1380 | return mStatusBar; | |
| 1381 | } | |
| 1382 | ||
| 1383 | private TextField getFindTextField() { | |
| 1384 | return mFindTextField; | |
| 1385 | } | |
| 1386 | ||
| 1387 | private DefinitionNameInjector getDefinitionNameInjector() { | |
| 1388 | return mDefinitionNameInjector; | |
| 1389 | } | |
| 1390 | ||
| 1391 | /** | |
| 1392 | * Returns the variable map of interpolated definitions. | |
| 1393 | * | |
| 1394 | * @return A map to help dereference variables. | |
| 1395 | */ | |
| 1396 | private Map<String, String> getResolvedMap() { | |
| 1397 | return mResolvedMap; | |
| 1398 | } | |
| 1399 | ||
| 1400 | //---- Persistence accessors ---------------------------------------------- | |
| 1401 | ||
| 1402 | private UserPreferences getUserPreferences() { | |
| 1403 | return UserPreferences.getInstance(); | |
| 1404 | } | |
| 1405 | ||
| 1406 | private Path getDefinitionPath() { | |
| 1407 | return getUserPreferences().getDefinitionPath(); | |
| 1408 | } | |
| 1409 | ||
| 1410 | //---- Spelling ----------------------------------------------------------- | |
| 1411 | ||
| 1412 | /** | |
| 1413 | * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}. | |
| 1414 | * This is called to spell check the document, rather than a single paragraph. | |
| 1415 | * | |
| 1416 | * @param text The full document text. | |
| 1417 | */ | |
| 1418 | private void spellcheck( | |
| 1419 | final StyleClassedTextArea editor, final String text ) { | |
| 1420 | spellcheck( editor, text, -1 ); | |
| 1421 | } | |
| 1422 | ||
| 1423 | /** | |
| 1424 | * Spellchecks a subset of the entire document. | |
| 1425 | * | |
| 1426 | * @param text Look up words for this text in the lexicon. | |
| 1427 | * @param paraId Set to -1 to apply resulting style spans to the entire | |
| 1428 | * text. | |
| 1429 | */ | |
| 1430 | private void spellcheck( | |
| 1431 | final StyleClassedTextArea editor, final String text, final int paraId ) { | |
| 1432 | final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 1433 | final var runningIndex = new AtomicInteger( 0 ); | |
| 1434 | final var checker = getSpellChecker(); | |
| 1435 | ||
| 1436 | // The text nodes must be relayed through a contextual "visitor" that | |
| 1437 | // can return text in chunks with correlative offsets into the string. | |
| 1438 | // This allows Markdown, R Markdown, XML, and R XML documents to return | |
| 1439 | // sets of words to check. | |
| 1440 | ||
| 1441 | final var node = mParser.parse( text ); | |
| 1442 | final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> { | |
| 1443 | // Treat hyphenated compound words as individual words. | |
| 1444 | final var check = visited.replace( '-', ' ' ); | |
| 1445 | ||
| 1446 | checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> { | |
| 1447 | prevIndex += bIndex; | |
| 1448 | currIndex += bIndex; | |
| 1449 | ||
| 1450 | // Clear styling between lexiconically absent words. | |
| 1451 | builder.add( emptyList(), prevIndex - runningIndex.get() ); | |
| 1452 | builder.add( singleton( "spelling" ), currIndex - prevIndex ); | |
| 1453 | runningIndex.set( currIndex ); | |
| 1454 | } ); | |
| 1455 | } ); | |
| 1456 | ||
| 1457 | visitor.visit( node ); | |
| 1458 | ||
| 1459 | // If the running index was set, at least one word triggered the listener. | |
| 1460 | if( runningIndex.get() > 0 ) { | |
| 1461 | // Clear styling after the last lexiconically absent word. | |
| 1462 | builder.add( emptyList(), text.length() - runningIndex.get() ); | |
| 1463 | ||
| 1464 | final var spans = builder.create(); | |
| 1465 | ||
| 1466 | if( paraId >= 0 ) { | |
| 1467 | editor.setStyleSpans( paraId, 0, spans ); | |
| 1468 | } | |
| 1469 | else { | |
| 1470 | editor.setStyleSpans( 0, spans ); | |
| 1471 | } | |
| 1472 | } | |
| 1473 | } | |
| 1474 | ||
| 1475 | @SuppressWarnings("SameParameterValue") | |
| 1476 | private Collection<String> readLexicon( final String filename ) | |
| 1477 | throws Exception { | |
| 1478 | final var path = "/" + LEXICONS_DIRECTORY + "/" + filename; | |
| 1479 | ||
| 1480 | try( final var resource = getClass().getResourceAsStream( path ) ) { | |
| 1481 | if( resource == null ) { | |
| 1482 | throw new MissingFileException( path ); | |
| 1483 | } | |
| 1484 | ||
| 1485 | try( final var isr = new InputStreamReader( resource, UTF_8 ); | |
| 1486 | final var reader = new BufferedReader( isr ) ) { | |
| 1487 | return reader.lines().collect( Collectors.toList() ); | |
| 1488 | } | |
| 1489 | } | |
| 1490 | } | |
| 1491 | ||
| 1492 | // TODO: #59 -- Replace using Markdown processor instantiated for Markdown | |
| 1493 | // files. | |
| 1494 | private final Parser mParser = Parser.builder().build(); | |
| 1495 | ||
| 1496 | // TODO: #59 -- Replace with generic interface; provide Markdown/XML | |
| 1497 | // implementations. | |
| 1498 | private static final class TextVisitor { | |
| 1499 | private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>( | |
| 1500 | com.vladsch.flexmark.ast.Text.class, this::visit ) | |
| 1501 | ); | |
| 1502 | ||
| 1503 | private final SpellCheckListener mConsumer; | |
| 1504 | ||
| 1505 | public TextVisitor( final SpellCheckListener consumer ) { | |
| 1506 | mConsumer = consumer; | |
| 1507 | } | |
| 1508 | ||
| 1509 | private void visit( final com.vladsch.flexmark.util.ast.Node node ) { | |
| 1510 | if( node instanceof com.vladsch.flexmark.ast.Text ) { | |
| 1511 | mConsumer.accept( node.getChars().toString(), | |
| 1512 | node.getStartOffset(), | |
| 1513 | node.getEndOffset() ); | |
| 1514 | } | |
| 1515 | ||
| 1516 | mVisitor.visitChildren( node ); | |
| 1517 | } | |
| 1518 | } | |
| 1519 | } | |
| 1520 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * * Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * * Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 27 | 2 | package com.keenwrite; |
| 3 | ||
| 4 | import com.keenwrite.preferences.Key; | |
| 28 | 5 | |
| 29 | 6 | import java.text.MessageFormat; |
| 7 | import java.util.Enumeration; | |
| 30 | 8 | import java.util.ResourceBundle; |
| 31 | 9 | import java.util.Stack; |
| ... | ||
| 121 | 99 | return key; |
| 122 | 100 | } |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Returns the value for a key from the message bundle. | |
| 105 | * | |
| 106 | * @param key Retrieve the value for this key. | |
| 107 | * @return The value for the key. | |
| 108 | */ | |
| 109 | public static String get( final Key key ) { | |
| 110 | return get( key.toString() ); | |
| 123 | 111 | } |
| 124 | 112 | |
| ... | ||
| 141 | 129 | public static String get( final String key, final Object... args ) { |
| 142 | 130 | return MessageFormat.format( get( key ), args ); |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Answers whether the given key is contained in the application's messages | |
| 135 | * properties file. | |
| 136 | * | |
| 137 | * @param key The key to look for in the {@link ResourceBundle}. | |
| 138 | * @return {@code true} when the key exists as an exact match. | |
| 139 | */ | |
| 140 | public static boolean containsKey( final String key ) { | |
| 141 | return RESOURCE_BUNDLE.containsKey( key ); | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * Returns all key names in the application's messages properties file. | |
| 146 | * | |
| 147 | * @return All key names in the {@link ResourceBundle} encapsulated by | |
| 148 | * this class. | |
| 149 | */ | |
| 150 | public static Enumeration<String> getKeys() { | |
| 151 | return RESOURCE_BUNDLE.getKeys(); | |
| 143 | 152 | } |
| 144 | 153 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| 30 | 4 | import javafx.beans.property.BooleanProperty; |
| 31 | 5 | import javafx.beans.property.SimpleBooleanProperty; |
| 32 | 6 | import javafx.event.Event; |
| 33 | 7 | import javafx.event.EventHandler; |
| 34 | import javafx.scene.Node; | |
| 35 | 8 | import javafx.scene.control.ScrollBar; |
| 36 | 9 | import javafx.scene.control.skin.ScrollBarSkin; |
| 37 | 10 | import javafx.scene.input.MouseEvent; |
| 38 | 11 | import javafx.scene.input.ScrollEvent; |
| 39 | 12 | import javafx.scene.layout.StackPane; |
| 40 | 13 | import org.fxmisc.flowless.VirtualizedScrollPane; |
| 41 | 14 | import org.fxmisc.richtext.StyleClassedTextArea; |
| 42 | 15 | |
| 43 | 16 | import javax.swing.*; |
| 17 | import java.util.function.Consumer; | |
| 44 | 18 | |
| 45 | 19 | import static javafx.geometry.Orientation.VERTICAL; |
| ... | ||
| 102 | 76 | mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() ); |
| 103 | 77 | |
| 104 | final var thumb = getVerticalScrollBarThumb( mEditorScrollPane ); | |
| 105 | thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) ); | |
| 78 | initVerticalScrollBarThumb( | |
| 79 | mEditorScrollPane, | |
| 80 | thumb -> { | |
| 81 | final var handler = new MouseHandler( thumb.getOnMouseDragged() ); | |
| 82 | thumb.setOnMouseDragged( handler ); | |
| 83 | } | |
| 84 | ); | |
| 106 | 85 | } |
| 107 | 86 | |
| ... | ||
| 128 | 107 | if( isEnabled() ) { |
| 129 | 108 | final var eScrollPane = getEditorScrollPane(); |
| 130 | final int eScrollY = | |
| 109 | final var eScrollY = | |
| 131 | 110 | eScrollPane.estimatedScrollYProperty().getValue().intValue(); |
| 132 | final int eHeight = (int) | |
| 111 | final var eHeight = (int) | |
| 133 | 112 | (eScrollPane.totalHeightEstimateProperty().getValue().intValue() |
| 134 | 113 | - eScrollPane.getHeight()); |
| 135 | final double eRatio = eHeight > 0 | |
| 114 | final var eRatio = eHeight > 0 | |
| 136 | 115 | ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0; |
| 137 | 116 | |
| ... | ||
| 144 | 123 | } |
| 145 | 124 | } |
| 146 | ||
| 147 | private StackPane getVerticalScrollBarThumb( | |
| 148 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | |
| 149 | final ScrollBar scrollBar = getVerticalScrollBar( pane ); | |
| 150 | final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get()); | |
| 151 | 125 | |
| 152 | for( final Node node : skin.getChildren() ) { | |
| 153 | // Brittle, but what can you do? | |
| 154 | if( node.getStyleClass().contains( "thumb" ) ) { | |
| 155 | return (StackPane) node; | |
| 126 | private void initVerticalScrollBarThumb( | |
| 127 | final VirtualizedScrollPane<StyleClassedTextArea> pane, | |
| 128 | final Consumer<StackPane> consumer ) { | |
| 129 | // When the skin property is set, the stack pane is available (not null). | |
| 130 | getVerticalScrollBar( pane ).skinProperty().addListener( ( c, o, n ) -> { | |
| 131 | for( final var node : ((ScrollBarSkin) n).getChildren() ) { | |
| 132 | // Brittle, but what can you do? | |
| 133 | if( node.getStyleClass().contains( "thumb" ) ) { | |
| 134 | consumer.accept( (StackPane) node ); | |
| 135 | } | |
| 156 | 136 | } |
| 157 | } | |
| 158 | ||
| 159 | throw new IllegalArgumentException( "No scroll bar skin found." ); | |
| 137 | } ); | |
| 160 | 138 | } |
| 161 | 139 | |
| 140 | /** | |
| 141 | * Returns the vertical {@link ScrollBar} instance associated with the | |
| 142 | * given scroll pane. This is {@code null}-safe because the scroll pane | |
| 143 | * initializes its vertical {@link ScrollBar} upon construction. | |
| 144 | * | |
| 145 | * @param pane The scroll pane that contains a vertical {@link ScrollBar}. | |
| 146 | * @return The vertical {@link ScrollBar} associated with the scroll pane. | |
| 147 | * @throws IllegalStateException Could not obtain the vertical scroll bar. | |
| 148 | */ | |
| 162 | 149 | private ScrollBar getVerticalScrollBar( |
| 163 | 150 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { |
| 164 | 151 | |
| 165 | for( final Node node : pane.getChildrenUnmodifiable() ) { | |
| 152 | for( final var node : pane.getChildrenUnmodifiable() ) { | |
| 166 | 153 | if( node instanceof ScrollBar ) { |
| 167 | final ScrollBar scrollBar = (ScrollBar) node; | |
| 154 | final var scrollBar = (ScrollBar) node; | |
| 168 | 155 | |
| 169 | 156 | if( scrollBar.getOrientation() == VERTICAL ) { |
| 170 | 157 | return scrollBar; |
| 171 | 158 | } |
| 172 | 159 | } |
| 173 | 160 | } |
| 174 | 161 | |
| 175 | throw new IllegalArgumentException( "No vertical scroll pane found." ); | |
| 162 | throw new IllegalStateException( "No vertical scroll bar found." ); | |
| 176 | 163 | } |
| 177 | 164 | |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite; |
| 29 | 3 | |
| ... | ||
| 44 | 18 | |
| 45 | 19 | private static final Notifier sNotifier = Services.load( Notifier.class ); |
| 46 | private static StatusBar sStatusBar; | |
| 47 | ||
| 48 | public static void setStatusBar( final StatusBar statusBar ) { | |
| 49 | sStatusBar = statusBar; | |
| 50 | } | |
| 20 | private static final StatusBar sStatusBar = new StatusBar(); | |
| 51 | 21 | |
| 52 | 22 | /** |
| 53 | 23 | * Resets the status bar to a default message. |
| 54 | 24 | */ |
| 55 | public static void clearClue() { | |
| 25 | public static void clue() { | |
| 56 | 26 | // Don't burden the repaint thread if there's no status bar change. |
| 57 | 27 | if( !OK.equals( sStatusBar.getText() ) ) { |
| ... | ||
| 97 | 67 | public static Notifier getNotifier() { |
| 98 | 68 | return sNotifier; |
| 69 | } | |
| 70 | ||
| 71 | public static StatusBar getStatusBar() { | |
| 72 | return sStatusBar; | |
| 99 | 73 | } |
| 100 | 74 | |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.adapters; | |
| 29 | ||
| 30 | import org.xhtmlrenderer.event.DocumentListener; | |
| 31 | ||
| 32 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 33 | ||
| 34 | /** | |
| 35 | * Allows subclasses to implement only specific events of interest. | |
| 36 | */ | |
| 37 | public class DocumentAdapter implements DocumentListener { | |
| 38 | @Override | |
| 39 | public void documentStarted() { | |
| 40 | } | |
| 41 | ||
| 42 | @Override | |
| 43 | public void documentLoaded() { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public void onLayoutException( final Throwable t ) { | |
| 48 | clue( t ); | |
| 49 | } | |
| 50 | ||
| 51 | @Override | |
| 52 | public void onRenderException( final Throwable t ) { | |
| 53 | clue( t ); | |
| 54 | } | |
| 55 | } | |
| 56 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.adapters; | |
| 29 | ||
| 30 | import org.w3c.dom.Element; | |
| 31 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 32 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 33 | ||
| 34 | /** | |
| 35 | * Allows subclasses to implement only specific events of interest. | |
| 36 | */ | |
| 37 | public abstract class ReplacedElementAdapter implements ReplacedElementFactory { | |
| 38 | @Override | |
| 39 | public void reset() { | |
| 40 | } | |
| 41 | ||
| 42 | @Override | |
| 43 | public void remove( final Element e ) { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public void setFormSubmissionListener( | |
| 48 | final FormSubmissionListener listener ) { | |
| 49 | } | |
| 50 | } | |
| 51 | 1 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.controls; | |
| 29 | ||
| 30 | import com.keenwrite.Messages; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 32 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 33 | import javafx.beans.property.ObjectProperty; | |
| 34 | import javafx.beans.property.SimpleObjectProperty; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.scene.control.Button; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | import javafx.scene.input.KeyCode; | |
| 39 | import javafx.scene.input.KeyEvent; | |
| 40 | import javafx.stage.FileChooser; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.ArrayList; | |
| 46 | import java.util.List; | |
| 47 | ||
| 48 | /** | |
| 49 | * Button that opens a file chooser to select a local file for a URL. | |
| 50 | */ | |
| 51 | public class BrowseFileButton extends Button { | |
| 52 | private final List<ExtensionFilter> extensionFilters = new ArrayList<>(); | |
| 53 | ||
| 54 | public BrowseFileButton() { | |
| 55 | setGraphic( | |
| 56 | FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT ) | |
| 57 | ); | |
| 58 | setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) ); | |
| 59 | setOnAction( this::browse ); | |
| 60 | ||
| 61 | disableProperty().bind( basePath.isNull() ); | |
| 62 | ||
| 63 | // workaround for a JavaFX bug: | |
| 64 | // avoid closing the dialog that contains this control when the user | |
| 65 | // closes the FileChooser or DirectoryChooser using the ESC key | |
| 66 | addEventHandler( KeyEvent.KEY_RELEASED, e -> { | |
| 67 | if( e.getCode() == KeyCode.ESCAPE ) { | |
| 68 | e.consume(); | |
| 69 | } | |
| 70 | } ); | |
| 71 | } | |
| 72 | ||
| 73 | public void addExtensionFilter( ExtensionFilter extensionFilter ) { | |
| 74 | extensionFilters.add( extensionFilter ); | |
| 75 | } | |
| 76 | ||
| 77 | // 'basePath' property | |
| 78 | private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>(); | |
| 79 | ||
| 80 | public Path getBasePath() { | |
| 81 | return basePath.get(); | |
| 82 | } | |
| 83 | ||
| 84 | public void setBasePath( Path basePath ) { | |
| 85 | this.basePath.set( basePath ); | |
| 86 | } | |
| 87 | ||
| 88 | // 'url' property | |
| 89 | private final ObjectProperty<String> url = new SimpleObjectProperty<>(); | |
| 90 | ||
| 91 | public ObjectProperty<String> urlProperty() { | |
| 92 | return url; | |
| 93 | } | |
| 94 | ||
| 95 | protected void browse( ActionEvent e ) { | |
| 96 | FileChooser fileChooser = new FileChooser(); | |
| 97 | fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) ); | |
| 98 | fileChooser.getExtensionFilters().addAll( extensionFilters ); | |
| 99 | fileChooser.getExtensionFilters() | |
| 100 | .add( new ExtensionFilter( Messages.get( | |
| 101 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 102 | fileChooser.setInitialDirectory( getInitialDirectory() ); | |
| 103 | File result = fileChooser.showOpenDialog( getScene().getWindow() ); | |
| 104 | if( result != null ) { | |
| 105 | updateUrl( result ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | protected File getInitialDirectory() { | |
| 110 | //TODO build initial directory based on current value of 'url' property | |
| 111 | return getBasePath().toFile(); | |
| 112 | } | |
| 113 | ||
| 114 | protected void updateUrl( File file ) { | |
| 115 | String newUrl; | |
| 116 | try { | |
| 117 | newUrl = getBasePath().relativize( file.toPath() ).toString(); | |
| 118 | } catch( IllegalArgumentException ex ) { | |
| 119 | newUrl = file.toString(); | |
| 120 | } | |
| 121 | url.set( newUrl.replace( '\\', '/' ) ); | |
| 122 | } | |
| 123 | } | |
| 124 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.controls; | |
| 29 | ||
| 30 | import javafx.beans.property.SimpleStringProperty; | |
| 31 | import javafx.beans.property.StringProperty; | |
| 32 | import javafx.scene.control.TextField; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for escaping/unescaping characters for markdown. | |
| 37 | */ | |
| 38 | public class EscapeTextField extends TextField { | |
| 39 | ||
| 40 | public EscapeTextField() { | |
| 41 | escapedText.bindBidirectional( | |
| 42 | textProperty(), | |
| 43 | new StringConverter<>() { | |
| 44 | @Override | |
| 45 | public String toString( String object ) { | |
| 46 | return escape( object ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public String fromString( String string ) { | |
| 51 | return unescape( string ); | |
| 52 | } | |
| 53 | } | |
| 54 | ); | |
| 55 | escapeCharacters.addListener( | |
| 56 | e -> escapedText.set( escape( textProperty().get() ) ) | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | // 'escapedText' property | |
| 61 | private final StringProperty escapedText = new SimpleStringProperty(); | |
| 62 | ||
| 63 | public StringProperty escapedTextProperty() { | |
| 64 | return escapedText; | |
| 65 | } | |
| 66 | ||
| 67 | // 'escapeCharacters' property | |
| 68 | private final StringProperty escapeCharacters = new SimpleStringProperty(); | |
| 69 | ||
| 70 | public String getEscapeCharacters() { | |
| 71 | return escapeCharacters.get(); | |
| 72 | } | |
| 73 | ||
| 74 | public void setEscapeCharacters( String escapeCharacters ) { | |
| 75 | this.escapeCharacters.set( escapeCharacters ); | |
| 76 | } | |
| 77 | ||
| 78 | private String escape( final String s ) { | |
| 79 | final String escapeChars = getEscapeCharacters(); | |
| 80 | ||
| 81 | return isEmpty( escapeChars ) ? s : | |
| 82 | s.replaceAll( "([" + escapeChars.replaceAll( | |
| 83 | "(.)", | |
| 84 | "\\\\$1" ) + "])", "\\\\$1" ); | |
| 85 | } | |
| 86 | ||
| 87 | private String unescape( final String s ) { | |
| 88 | final String escapeChars = getEscapeCharacters(); | |
| 89 | ||
| 90 | return isEmpty( escapeChars ) ? s : | |
| 91 | s.replaceAll( "\\\\([" + escapeChars | |
| 92 | .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" ); | |
| 93 | } | |
| 94 | ||
| 95 | private static boolean isEmpty( final String s ) { | |
| 96 | return s == null || s.isEmpty(); | |
| 97 | } | |
| 98 | } | |
| 99 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.keenwrite.AbstractFileFactory; | |
| 31 | import com.keenwrite.FileType; | |
| 32 | import com.keenwrite.definition.yaml.YamlDefinitionSource; | |
| 33 | import com.keenwrite.util.ProtocolScheme; | |
| 34 | ||
| 35 | import java.nio.file.Path; | |
| 36 | ||
| 37 | import static com.keenwrite.Constants.GLOB_PREFIX_DEFINITION; | |
| 38 | import static com.keenwrite.FileType.YAML; | |
| 39 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 40 | import static java.lang.String.format; | |
| 41 | ||
| 42 | /** | |
| 43 | * Responsible for creating objects that can read and write definition data | |
| 44 | * sources. The data source could be YAML, TOML, JSON, flat files, or from a | |
| 45 | * database. | |
| 46 | */ | |
| 47 | public class DefinitionFactory extends AbstractFileFactory { | |
| 48 | ||
| 49 | /** | |
| 50 | * TODO: Use an error message key from messages properties file. | |
| 51 | */ | |
| 52 | private static final String MSG_UNKNOWN_FILE_TYPE = | |
| 53 | "Unknown type '%s' for file '%s'."; | |
| 54 | ||
| 55 | /** | |
| 56 | * Default (empty) constructor. | |
| 57 | */ | |
| 58 | public DefinitionFactory() { | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Creates a definition source capable of reading definitions from the given | |
| 63 | * path. | |
| 64 | * | |
| 65 | * @param path Path to a resource containing definitions. | |
| 66 | * @return The definition source appropriate for the given path. | |
| 67 | */ | |
| 68 | public DefinitionSource createDefinitionSource( final Path path ) { | |
| 69 | assert path != null; | |
| 70 | ||
| 71 | final var protocol = getProtocol( path.toString() ); | |
| 72 | DefinitionSource result = null; | |
| 73 | ||
| 74 | if( protocol.isFile() ) { | |
| 75 | final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION ); | |
| 76 | result = createFileDefinitionSource( filetype, path ); | |
| 77 | } | |
| 78 | else { | |
| 79 | unknownFileType( protocol, path.toString() ); | |
| 80 | } | |
| 81 | ||
| 82 | return result; | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * Creates a definition source based on the file type. | |
| 87 | * | |
| 88 | * @param filetype Property key name suffix from settings.properties file. | |
| 89 | * @param path Path to the file that corresponds to the extension. | |
| 90 | * @return A DefinitionSource capable of parsing the data stored at the path. | |
| 91 | */ | |
| 92 | private DefinitionSource createFileDefinitionSource( | |
| 93 | final FileType filetype, final Path path ) { | |
| 94 | assert filetype != null; | |
| 95 | assert path != null; | |
| 96 | ||
| 97 | if( filetype == YAML ) { | |
| 98 | return new YamlDefinitionSource( path ); | |
| 99 | } | |
| 100 | ||
| 101 | throw new IllegalArgumentException( filetype.toString() ); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Throws IllegalArgumentException because the given path could not be | |
| 106 | * recognized. This exists because | |
| 107 | * | |
| 108 | * @param type The detected path type (protocol, file extension, etc.). | |
| 109 | * @param path The path to a source of definitions. | |
| 110 | */ | |
| 111 | private void unknownFileType( | |
| 112 | final ProtocolScheme type, final String path ) { | |
| 113 | final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path ); | |
| 114 | throw new IllegalArgumentException( msg ); | |
| 115 | } | |
| 116 | } | |
| 117 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 32 | import javafx.beans.property.SimpleStringProperty; | |
| 33 | import javafx.beans.property.StringProperty; | |
| 34 | import javafx.collections.ObservableList; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.event.Event; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.geometry.Insets; | |
| 39 | import javafx.geometry.Pos; | |
| 40 | import javafx.scene.Node; | |
| 41 | import javafx.scene.control.*; | |
| 42 | import javafx.scene.input.KeyEvent; | |
| 43 | import javafx.scene.layout.BorderPane; | |
| 44 | import javafx.scene.layout.HBox; | |
| 45 | import javafx.util.StringConverter; | |
| 46 | ||
| 47 | import java.util.*; | |
| 48 | ||
| 49 | import static com.keenwrite.Messages.get; | |
| 50 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 51 | import static javafx.geometry.Pos.CENTER; | |
| 52 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 53 | ||
| 54 | /** | |
| 55 | * Provides the user interface that holds a {@link TreeView}, which | |
| 56 | * allows users to interact with key/value pairs loaded from the | |
| 57 | * {@link DocumentParser} and adapted using a {@link TreeAdapter}. | |
| 58 | */ | |
| 59 | public final class DefinitionPane extends BorderPane { | |
| 60 | ||
| 61 | /** | |
| 62 | * Contains a view of the definitions. | |
| 63 | */ | |
| 64 | private final TreeView<String> mTreeView = new TreeView<>(); | |
| 65 | ||
| 66 | /** | |
| 67 | * Handlers for key press events. | |
| 68 | */ | |
| 69 | private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers | |
| 70 | = new HashSet<>(); | |
| 71 | ||
| 72 | /** | |
| 73 | * Definition file name shown in the title of the pane. | |
| 74 | */ | |
| 75 | private final StringProperty mFilename = new SimpleStringProperty(); | |
| 76 | ||
| 77 | private final TitledPane mTitledPane = new TitledPane(); | |
| 78 | ||
| 79 | /** | |
| 80 | * Constructs a definition pane with a given tree view root. | |
| 81 | */ | |
| 82 | public DefinitionPane() { | |
| 83 | final var treeView = getTreeView(); | |
| 84 | treeView.setEditable( true ); | |
| 85 | treeView.setCellFactory( cell -> createTreeCell() ); | |
| 86 | treeView.setContextMenu( createContextMenu() ); | |
| 87 | treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); | |
| 88 | treeView.setShowRoot( false ); | |
| 89 | getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); | |
| 90 | ||
| 91 | final var bCreate = createButton( | |
| 92 | "create", TREE, e -> addItem() ); | |
| 93 | final var bRename = createButton( | |
| 94 | "rename", EDIT, e -> editSelectedItem() ); | |
| 95 | final var bDelete = createButton( | |
| 96 | "delete", TRASH, e -> deleteSelectedItems() ); | |
| 97 | ||
| 98 | final var buttonBar = new HBox(); | |
| 99 | buttonBar.getChildren().addAll( bCreate, bRename, bDelete ); | |
| 100 | buttonBar.setAlignment( CENTER ); | |
| 101 | buttonBar.setSpacing( 10 ); | |
| 102 | ||
| 103 | final var titledPane = getTitledPane(); | |
| 104 | titledPane.textProperty().bind( mFilename ); | |
| 105 | titledPane.setContent( treeView ); | |
| 106 | titledPane.setCollapsible( false ); | |
| 107 | titledPane.setPadding( new Insets( 0, 0, 0, 0 ) ); | |
| 108 | ||
| 109 | setTop( buttonBar ); | |
| 110 | setCenter( titledPane ); | |
| 111 | setAlignment( buttonBar, Pos.TOP_CENTER ); | |
| 112 | setAlignment( titledPane, Pos.TOP_CENTER ); | |
| 113 | ||
| 114 | titledPane.prefHeightProperty().bind( this.heightProperty() ); | |
| 115 | } | |
| 116 | ||
| 117 | public void setTooltip( final Tooltip tooltip ) { | |
| 118 | getTitledPane().setTooltip( tooltip ); | |
| 119 | } | |
| 120 | ||
| 121 | private TitledPane getTitledPane() { | |
| 122 | return mTitledPane; | |
| 123 | } | |
| 124 | ||
| 125 | private Button createButton( | |
| 126 | final String msgKey, | |
| 127 | final FontAwesomeIcon icon, | |
| 128 | final EventHandler<ActionEvent> eventHandler ) { | |
| 129 | final var keyPrefix = "Pane.definition.button." + msgKey; | |
| 130 | final var button = new Button( get( keyPrefix + ".label" ) ); | |
| 131 | button.setOnAction( eventHandler ); | |
| 132 | ||
| 133 | button.setGraphic( | |
| 134 | FontAwesomeIconFactory.get().createIcon( icon ) | |
| 135 | ); | |
| 136 | button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); | |
| 137 | ||
| 138 | return button; | |
| 139 | } | |
| 140 | ||
| 141 | /** | |
| 142 | * Changes the root of the {@link TreeView} to the root of the | |
| 143 | * {@link TreeView} from the {@link DefinitionSource}. | |
| 144 | * | |
| 145 | * @param definitionSource Container for the hierarchy of key/value pairs | |
| 146 | * to replace the existing hierarchy. | |
| 147 | */ | |
| 148 | public void update( final DefinitionSource definitionSource ) { | |
| 149 | assert definitionSource != null; | |
| 150 | ||
| 151 | final TreeAdapter treeAdapter = definitionSource.getTreeAdapter(); | |
| 152 | final TreeItem<String> root = treeAdapter.adapt( | |
| 153 | get( "Pane.definition.node.root.title" ) | |
| 154 | ); | |
| 155 | ||
| 156 | getTreeView().setRoot( root ); | |
| 157 | } | |
| 158 | ||
| 159 | public Map<String, String> toMap() { | |
| 160 | return TreeItemAdapter.toMap( getTreeView().getRoot() ); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} | |
| 165 | * is modified. The modifications include: item value changes, item additions, | |
| 166 | * and item removals. | |
| 167 | * <p> | |
| 168 | * Safe to call multiple times; if a handler is already registered, the | |
| 169 | * old handler is used. | |
| 170 | * </p> | |
| 171 | * | |
| 172 | * @param handler The handler to call whenever any {@link TreeItem} changes. | |
| 173 | */ | |
| 174 | public void addTreeChangeHandler( | |
| 175 | final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { | |
| 176 | final TreeItem<String> root = getTreeView().getRoot(); | |
| 177 | root.addEventHandler( TreeItem.valueChangedEvent(), handler ); | |
| 178 | root.addEventHandler( TreeItem.childrenModificationEvent(), handler ); | |
| 179 | } | |
| 180 | ||
| 181 | public void addKeyEventHandler( | |
| 182 | final EventHandler<? super KeyEvent> handler ) { | |
| 183 | getKeyEventHandlers().add( handler ); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably | |
| 188 | * well-formed for export. A tree is considered well-formed if the following | |
| 189 | * conditions are met: | |
| 190 | * | |
| 191 | * <ul> | |
| 192 | * <li>The root node contains at least one child node having a leaf.</li> | |
| 193 | * <li>There are no leaf nodes with sibling leaf nodes.</li> | |
| 194 | * </ul> | |
| 195 | * | |
| 196 | * @return {@code null} if the document is well-formed, otherwise the | |
| 197 | * problematic child {@link TreeItem}. | |
| 198 | */ | |
| 199 | public TreeItem<String> isTreeWellFormed() { | |
| 200 | final var root = getTreeView().getRoot(); | |
| 201 | ||
| 202 | for( final var child : root.getChildren() ) { | |
| 203 | final var problemChild = isWellFormed( child ); | |
| 204 | ||
| 205 | if( child.isLeaf() || problemChild != null ) { | |
| 206 | return problemChild; | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | return null; | |
| 211 | } | |
| 212 | ||
| 213 | /** | |
| 214 | * Determines whether the document is well-formed by ensuring that | |
| 215 | * child branches do not contain multiple leaves. | |
| 216 | * | |
| 217 | * @param item The sub-tree to check for well-formedness. | |
| 218 | * @return {@code null} when the tree is well-formed, otherwise the | |
| 219 | * problematic {@link TreeItem}. | |
| 220 | */ | |
| 221 | private TreeItem<String> isWellFormed( final TreeItem<String> item ) { | |
| 222 | int childLeafs = 0; | |
| 223 | int childBranches = 0; | |
| 224 | ||
| 225 | for( final TreeItem<String> child : item.getChildren() ) { | |
| 226 | if( child.isLeaf() ) { | |
| 227 | childLeafs++; | |
| 228 | } | |
| 229 | else { | |
| 230 | childBranches++; | |
| 231 | } | |
| 232 | ||
| 233 | final var problemChild = isWellFormed( child ); | |
| 234 | ||
| 235 | if( problemChild != null ) { | |
| 236 | return problemChild; | |
| 237 | } | |
| 238 | } | |
| 239 | ||
| 240 | return ((childBranches > 0 && childLeafs == 0) || | |
| 241 | (childBranches == 0 && childLeafs <= 1)) ? null : item; | |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}. | |
| 246 | * | |
| 247 | * @param text The value to find, never {@code null}. | |
| 248 | * @return The leaf that contains the given value, or {@code null} if | |
| 249 | * not found. | |
| 250 | */ | |
| 251 | public DefinitionTreeItem<String> findLeafExact( final String text ) { | |
| 252 | return getTreeRoot().findLeafExact( text ); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 257 | * | |
| 258 | * @param text The value to find, never {@code null}. | |
| 259 | * @return The leaf that contains the given value, or {@code null} if | |
| 260 | * not found. | |
| 261 | */ | |
| 262 | public DefinitionTreeItem<String> findLeafContains( final String text ) { | |
| 263 | return getTreeRoot().findLeafContains( text ); | |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 268 | * | |
| 269 | * @param text The value to find, never {@code null}. | |
| 270 | * @return The leaf that contains the given value, or {@code null} if | |
| 271 | * not found. | |
| 272 | */ | |
| 273 | public DefinitionTreeItem<String> findLeafContainsNoCase( | |
| 274 | final String text ) { | |
| 275 | return getTreeRoot().findLeafContainsNoCase( text ); | |
| 276 | } | |
| 277 | ||
| 278 | /** | |
| 279 | * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}. | |
| 280 | * | |
| 281 | * @param text The value to find, never {@code null}. | |
| 282 | * @return The leaf that contains the given value, or {@code null} if | |
| 283 | * not found. | |
| 284 | */ | |
| 285 | public DefinitionTreeItem<String> findLeafStartsWith( final String text ) { | |
| 286 | return getTreeRoot().findLeafStartsWith( text ); | |
| 287 | } | |
| 288 | ||
| 289 | /** | |
| 290 | * Expands the node to the root, recursively. | |
| 291 | * | |
| 292 | * @param <T> The type of tree item to expand (usually String). | |
| 293 | * @param node The node to expand. | |
| 294 | */ | |
| 295 | public <T> void expand( final TreeItem<T> node ) { | |
| 296 | if( node != null ) { | |
| 297 | expand( node.getParent() ); | |
| 298 | ||
| 299 | if( !node.isLeaf() ) { | |
| 300 | node.setExpanded( true ); | |
| 301 | } | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | public void select( final TreeItem<String> item ) { | |
| 306 | getSelectionModel().clearSelection(); | |
| 307 | getSelectionModel().select( getTreeView().getRow( item ) ); | |
| 308 | } | |
| 309 | ||
| 310 | /** | |
| 311 | * Collapses the tree, recursively. | |
| 312 | */ | |
| 313 | public void collapse() { | |
| 314 | collapse( getTreeRoot().getChildren() ); | |
| 315 | } | |
| 316 | ||
| 317 | /** | |
| 318 | * Collapses the tree, recursively. | |
| 319 | * | |
| 320 | * @param <T> The type of tree item to expand (usually String). | |
| 321 | * @param nodes The nodes to collapse. | |
| 322 | */ | |
| 323 | private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { | |
| 324 | for( final var node : nodes ) { | |
| 325 | node.setExpanded( false ); | |
| 326 | collapse( node.getChildren() ); | |
| 327 | } | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * @return {@code true} when the user is editing a {@link TreeItem}. | |
| 332 | */ | |
| 333 | private boolean isEditingTreeItem() { | |
| 334 | return getTreeView().editingItemProperty().getValue() != null; | |
| 335 | } | |
| 336 | ||
| 337 | /** | |
| 338 | * Changes to edit mode for the selected item. | |
| 339 | */ | |
| 340 | private void editSelectedItem() { | |
| 341 | getTreeView().edit( getSelectedItem() ); | |
| 342 | } | |
| 343 | ||
| 344 | /** | |
| 345 | * Removes all selected items from the {@link TreeView}. | |
| 346 | */ | |
| 347 | private void deleteSelectedItems() { | |
| 348 | for( final var item : getSelectedItems() ) { | |
| 349 | final var parent = item.getParent(); | |
| 350 | ||
| 351 | if( parent != null ) { | |
| 352 | parent.getChildren().remove( item ); | |
| 353 | } | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | /** | |
| 358 | * Deletes the selected item. | |
| 359 | */ | |
| 360 | private void deleteSelectedItem() { | |
| 361 | final var c = getSelectedItem(); | |
| 362 | getSiblings( c ).remove( c ); | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Adds a new item under the selected item (or root if nothing is selected). | |
| 367 | * There are a few conditions to consider: when adding to the root, | |
| 368 | * when adding to a leaf, and when adding to a non-leaf. Items added to the | |
| 369 | * root must contain two items: a key and a value. | |
| 370 | */ | |
| 371 | public void addItem() { | |
| 372 | final var value = createTreeItem(); | |
| 373 | getSelectedItem().getChildren().add( value ); | |
| 374 | expand( value ); | |
| 375 | select( value ); | |
| 376 | } | |
| 377 | ||
| 378 | private ContextMenu createContextMenu() { | |
| 379 | final ContextMenu menu = new ContextMenu(); | |
| 380 | final ObservableList<MenuItem> items = menu.getItems(); | |
| 381 | ||
| 382 | addMenuItem( items, "Definition.menu.create" ) | |
| 383 | .setOnAction( e -> addItem() ); | |
| 384 | ||
| 385 | addMenuItem( items, "Definition.menu.rename" ) | |
| 386 | .setOnAction( e -> editSelectedItem() ); | |
| 387 | ||
| 388 | addMenuItem( items, "Definition.menu.remove" ) | |
| 389 | .setOnAction( e -> deleteSelectedItem() ); | |
| 390 | ||
| 391 | return menu; | |
| 392 | } | |
| 393 | ||
| 394 | /** | |
| 395 | * Executes hot-keys for edits to the definition tree. | |
| 396 | * | |
| 397 | * @param event Contains the key code of the key that was pressed. | |
| 398 | */ | |
| 399 | private void keyEventFilter( final KeyEvent event ) { | |
| 400 | if( !isEditingTreeItem() ) { | |
| 401 | switch( event.getCode() ) { | |
| 402 | case ENTER: | |
| 403 | expand( getSelectedItem() ); | |
| 404 | event.consume(); | |
| 405 | break; | |
| 406 | ||
| 407 | case DELETE: | |
| 408 | deleteSelectedItems(); | |
| 409 | break; | |
| 410 | ||
| 411 | case INSERT: | |
| 412 | addItem(); | |
| 413 | break; | |
| 414 | ||
| 415 | case R: | |
| 416 | if( event.isControlDown() ) { | |
| 417 | editSelectedItem(); | |
| 418 | } | |
| 419 | ||
| 420 | break; | |
| 421 | } | |
| 422 | ||
| 423 | for( final var handler : getKeyEventHandlers() ) { | |
| 424 | handler.handle( event ); | |
| 425 | } | |
| 426 | } | |
| 427 | } | |
| 428 | ||
| 429 | /** | |
| 430 | * Adds a menu item to a list of menu items. | |
| 431 | * | |
| 432 | * @param items The list of menu items to append to. | |
| 433 | * @param labelKey The resource bundle key name for the menu item's label. | |
| 434 | * @return The menu item added to the list of menu items. | |
| 435 | */ | |
| 436 | private MenuItem addMenuItem( | |
| 437 | final List<MenuItem> items, final String labelKey ) { | |
| 438 | final MenuItem menuItem = createMenuItem( labelKey ); | |
| 439 | items.add( menuItem ); | |
| 440 | return menuItem; | |
| 441 | } | |
| 442 | ||
| 443 | private MenuItem createMenuItem( final String labelKey ) { | |
| 444 | return new MenuItem( get( labelKey ) ); | |
| 445 | } | |
| 446 | ||
| 447 | private DefinitionTreeItem<String> createTreeItem() { | |
| 448 | return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) ); | |
| 449 | } | |
| 450 | ||
| 451 | private TreeCell<String> createTreeCell() { | |
| 452 | return new FocusAwareTextFieldTreeCell( createStringConverter() ) { | |
| 453 | @Override | |
| 454 | public void commitEdit( final String newValue ) { | |
| 455 | super.commitEdit( newValue ); | |
| 456 | select( getTreeItem() ); | |
| 457 | requestFocus(); | |
| 458 | } | |
| 459 | }; | |
| 460 | } | |
| 461 | ||
| 462 | @Override | |
| 463 | public void requestFocus() { | |
| 464 | super.requestFocus(); | |
| 465 | getTreeView().requestFocus(); | |
| 466 | } | |
| 467 | ||
| 468 | private StringConverter<String> createStringConverter() { | |
| 469 | return new StringConverter<>() { | |
| 470 | @Override | |
| 471 | public String toString( final String object ) { | |
| 472 | return object == null ? "" : object; | |
| 473 | } | |
| 474 | ||
| 475 | @Override | |
| 476 | public String fromString( final String string ) { | |
| 477 | return string == null ? "" : string; | |
| 478 | } | |
| 479 | }; | |
| 480 | } | |
| 481 | ||
| 482 | /** | |
| 483 | * Returns the tree view that contains the definition hierarchy. | |
| 484 | * | |
| 485 | * @return A non-null instance. | |
| 486 | */ | |
| 487 | public TreeView<String> getTreeView() { | |
| 488 | return mTreeView; | |
| 489 | } | |
| 490 | ||
| 491 | /** | |
| 492 | * Returns this pane. | |
| 493 | * | |
| 494 | * @return this | |
| 495 | */ | |
| 496 | public Node getNode() { | |
| 497 | return this; | |
| 498 | } | |
| 499 | ||
| 500 | /** | |
| 501 | * Returns the property used to set the title of the pane: the file name. | |
| 502 | * | |
| 503 | * @return A non-null property used for showing the definition file name. | |
| 504 | */ | |
| 505 | public StringProperty filenameProperty() { | |
| 506 | return mFilename; | |
| 507 | } | |
| 508 | ||
| 509 | /** | |
| 510 | * Returns the root of the tree. | |
| 511 | * | |
| 512 | * @return The first node added to the definition tree. | |
| 513 | */ | |
| 514 | private DefinitionTreeItem<String> getTreeRoot() { | |
| 515 | final var root = getTreeView().getRoot(); | |
| 516 | ||
| 517 | return root instanceof DefinitionTreeItem | |
| 518 | ? (DefinitionTreeItem<String>) root | |
| 519 | : new DefinitionTreeItem<>( "root" ); | |
| 520 | } | |
| 521 | ||
| 522 | private ObservableList<TreeItem<String>> getSiblings( | |
| 523 | final TreeItem<String> item ) { | |
| 524 | final var root = getTreeView().getRoot(); | |
| 525 | final var parent = (item == null || item == root) ? root : item.getParent(); | |
| 526 | ||
| 527 | return parent.getChildren(); | |
| 528 | } | |
| 529 | ||
| 530 | private MultipleSelectionModel<TreeItem<String>> getSelectionModel() { | |
| 531 | return getTreeView().getSelectionModel(); | |
| 532 | } | |
| 533 | ||
| 534 | /** | |
| 535 | * Returns a copy of all the selected items. | |
| 536 | * | |
| 537 | * @return A list, possibly empty, containing all selected items in the | |
| 538 | * {@link TreeView}. | |
| 539 | */ | |
| 540 | private List<TreeItem<String>> getSelectedItems() { | |
| 541 | return new ArrayList<>( getSelectionModel().getSelectedItems() ); | |
| 542 | } | |
| 543 | ||
| 544 | public TreeItem<String> getSelectedItem() { | |
| 545 | final var item = getSelectionModel().getSelectedItem(); | |
| 546 | return item == null ? getTreeView().getRoot() : item; | |
| 547 | } | |
| 548 | ||
| 549 | private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() { | |
| 550 | return mKeyEventHandlers; | |
| 551 | } | |
| 552 | ||
| 553 | /** | |
| 554 | * Answers whether there are any definitions in the tree. | |
| 555 | * | |
| 556 | * @return {@code true} when there are no definitions; {@code false} when | |
| 557 | * there's at least one definition. | |
| 558 | */ | |
| 559 | public boolean isEmpty() { | |
| 560 | return getTreeRoot().isEmpty(); | |
| 561 | } | |
| 562 | } | |
| 563 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents behaviours for reading and writing string definitions. This | |
| 32 | * class cannot have any direct hooks into the user interface, as it defines | |
| 33 | * entry points into the definition data model loaded into an object | |
| 34 | * hierarchy. That hierarchy is converted to a UI model using an adapter | |
| 35 | * pattern. | |
| 36 | */ | |
| 37 | public interface DefinitionSource { | |
| 38 | ||
| 39 | /** | |
| 40 | * Creates an object capable of producing view-based objects from this | |
| 41 | * definition source. | |
| 42 | * | |
| 43 | * @return A hierarchical tree suitable for displaying in the definition pane. | |
| 44 | */ | |
| 45 | TreeAdapter getTreeAdapter(); | |
| 46 | } | |
| 47 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.util.Stack; | |
| 33 | import java.util.function.BiFunction; | |
| 34 | ||
| 35 | import static java.text.Normalizer.Form.NFD; | |
| 36 | import static java.text.Normalizer.normalize; | |
| 37 | ||
| 38 | /** | |
| 39 | * Provides behaviour afforded to definition keys and corresponding value. | |
| 40 | * | |
| 41 | * @param <T> The type of {@link TreeItem} (usually string). | |
| 42 | */ | |
| 43 | public class DefinitionTreeItem<T> extends TreeItem<T> { | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new item with a default value. | |
| 47 | * | |
| 48 | * @param value Passed up to superclass. | |
| 49 | */ | |
| 50 | public DefinitionTreeItem( final T value ) { | |
| 51 | super( value ); | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * Finds a leaf starting at the current node with text that matches the given | |
| 56 | * value. Search is performed case-sensitively. | |
| 57 | * | |
| 58 | * @param text The text to match against each leaf in the tree. | |
| 59 | * @return The leaf that has a value exactly matching the given text. | |
| 60 | */ | |
| 61 | public DefinitionTreeItem<T> findLeafExact( final String text ) { | |
| 62 | return findLeaf( text, DefinitionTreeItem::valueEquals ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Finds a leaf starting at the current node with text that matches the given | |
| 67 | * value. Search is performed case-sensitively. | |
| 68 | * | |
| 69 | * @param text The text to match against each leaf in the tree. | |
| 70 | * @return The leaf that has a value that contains the given text. | |
| 71 | */ | |
| 72 | public DefinitionTreeItem<T> findLeafContains( final String text ) { | |
| 73 | return findLeaf( text, DefinitionTreeItem::valueContains ); | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Finds a leaf starting at the current node with text that matches the given | |
| 78 | * value. Search is performed case-insensitively. | |
| 79 | * | |
| 80 | * @param text The text to match against each leaf in the tree. | |
| 81 | * @return The leaf that has a value that contains the given text. | |
| 82 | */ | |
| 83 | public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) { | |
| 84 | return findLeaf( text, DefinitionTreeItem::valueContainsNoCase ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Finds a leaf starting at the current node with text that matches the given | |
| 89 | * value. Search is performed case-sensitively. | |
| 90 | * | |
| 91 | * @param text The text to match against each leaf in the tree. | |
| 92 | * @return The leaf that has a value that starts with the given text. | |
| 93 | */ | |
| 94 | public DefinitionTreeItem<T> findLeafStartsWith( final String text ) { | |
| 95 | return findLeaf( text, DefinitionTreeItem::valueStartsWith ); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Finds a leaf starting at the current node with text that matches the given | |
| 100 | * value. | |
| 101 | * | |
| 102 | * @param text The text to match against each leaf in the tree. | |
| 103 | * @param findMode What algorithm is used to match the given text. | |
| 104 | * @return The leaf that has a value starting with the given text, or {@code | |
| 105 | * null} if there was no match found. | |
| 106 | */ | |
| 107 | public DefinitionTreeItem<T> findLeaf( | |
| 108 | final String text, | |
| 109 | final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) { | |
| 110 | final var stack = new Stack<DefinitionTreeItem<T>>(); | |
| 111 | stack.push( this ); | |
| 112 | ||
| 113 | // Don't hunt for blank (empty) keys. | |
| 114 | boolean found = text.isBlank(); | |
| 115 | ||
| 116 | while( !found && !stack.isEmpty() ) { | |
| 117 | final var node = stack.pop(); | |
| 118 | ||
| 119 | for( final var child : node.getChildren() ) { | |
| 120 | final var result = (DefinitionTreeItem<T>) child; | |
| 121 | ||
| 122 | if( result.isLeaf() ) { | |
| 123 | if( found = findMode.apply( result, text ) ) { | |
| 124 | return result; | |
| 125 | } | |
| 126 | } | |
| 127 | else { | |
| 128 | stack.push( result ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | return null; | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Returns the value of the string without diacritic marks. | |
| 138 | * | |
| 139 | * @return A non-null, possibly empty string. | |
| 140 | */ | |
| 141 | private String getDiacriticlessValue() { | |
| 142 | return normalize( getValue().toString(), NFD ) | |
| 143 | .replaceAll( "\\p{M}", "" ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Returns true if this node is a leaf and its value equals the given text. | |
| 148 | * | |
| 149 | * @param s The text to compare against the node value. | |
| 150 | * @return true Node is a leaf and its value equals the given value. | |
| 151 | */ | |
| 152 | private boolean valueEquals( final String s ) { | |
| 153 | return isLeaf() && getValue().equals( s ); | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Returns true if this node is a leaf and its value contains the given text. | |
| 158 | * | |
| 159 | * @param s The text to compare against the node value. | |
| 160 | * @return true Node is a leaf and its value contains the given value. | |
| 161 | */ | |
| 162 | private boolean valueContains( final String s ) { | |
| 163 | return isLeaf() && getDiacriticlessValue().contains( s ); | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns true if this node is a leaf and its value contains the given text. | |
| 168 | * | |
| 169 | * @param s The text to compare against the node value. | |
| 170 | * @return true Node is a leaf and its value contains the given value. | |
| 171 | */ | |
| 172 | private boolean valueContainsNoCase( final String s ) { | |
| 173 | return isLeaf() && getDiacriticlessValue() | |
| 174 | .toLowerCase().contains( s.toLowerCase() ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns true if this node is a leaf and its value starts with the given | |
| 179 | * text. | |
| 180 | * | |
| 181 | * @param s The text to compare against the node value. | |
| 182 | * @return true Node is a leaf and its value starts with the given value. | |
| 183 | */ | |
| 184 | private boolean valueStartsWith( final String s ) { | |
| 185 | return isLeaf() && getDiacriticlessValue().startsWith( s ); | |
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Returns the path for this node, with nodes made distinct using the | |
| 190 | * separator character. This uses two loops: one for pushing nodes onto a | |
| 191 | * stack and one for popping them off to create the path in desired order. | |
| 192 | * | |
| 193 | * @return A non-null string, possibly empty. | |
| 194 | */ | |
| 195 | public String toPath() { | |
| 196 | return TreeItemAdapter.toPath( getParent() ); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Answers whether there are any definitions in this tree. | |
| 201 | * | |
| 202 | * @return {@code true} when there are no definitions in the tree; {@code | |
| 203 | * false} when there is at least one definition present. | |
| 204 | */ | |
| 205 | public boolean isEmpty() { | |
| 206 | return getChildren().isEmpty(); | |
| 207 | } | |
| 208 | } | |
| 209 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for parsing structured document formats. | |
| 32 | * | |
| 33 | * @param <T> The type of "node" for the document's object model. | |
| 34 | */ | |
| 35 | public interface DocumentParser<T> { | |
| 36 | ||
| 37 | /** | |
| 38 | * Parses a document into a nested object hierarchy. The object returned | |
| 39 | * from this call must be the root node in the document tree. | |
| 40 | * | |
| 41 | * @return The document's root node, which may be empty but never null. | |
| 42 | */ | |
| 43 | T getDocumentRoot(); | |
| 44 | } | |
| 45 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.TextField; | |
| 32 | import javafx.scene.control.cell.TextFieldTreeCell; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for fixing a focus lost bug in the JavaFX implementation. | |
| 37 | * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details. | |
| 38 | * This implementation borrows from the official documentation on creating | |
| 39 | * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm | |
| 40 | */ | |
| 41 | public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> { | |
| 42 | private TextField mTextField; | |
| 43 | ||
| 44 | public FocusAwareTextFieldTreeCell( | |
| 45 | final StringConverter<String> converter ) { | |
| 46 | super( converter ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public void startEdit() { | |
| 51 | super.startEdit(); | |
| 52 | var textField = mTextField; | |
| 53 | ||
| 54 | if( textField == null ) { | |
| 55 | textField = createTextField(); | |
| 56 | } | |
| 57 | else { | |
| 58 | textField.setText( getItem() ); | |
| 59 | } | |
| 60 | ||
| 61 | setText( null ); | |
| 62 | setGraphic( textField ); | |
| 63 | textField.selectAll(); | |
| 64 | textField.requestFocus(); | |
| 65 | ||
| 66 | // When the focus is lost, commit the edit then close the input field. | |
| 67 | // This fixes the unexpected behaviour when user clicks away. | |
| 68 | textField.focusedProperty().addListener( ( l, o, n ) -> { | |
| 69 | if( !n ) { | |
| 70 | commitEdit( mTextField.getText() ); | |
| 71 | } | |
| 72 | } ); | |
| 73 | ||
| 74 | mTextField = textField; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public void cancelEdit() { | |
| 79 | super.cancelEdit(); | |
| 80 | setText( getItem() ); | |
| 81 | setGraphic( getTreeItem().getGraphic() ); | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void updateItem( String item, boolean empty ) { | |
| 86 | super.updateItem( item, empty ); | |
| 87 | ||
| 88 | String text = null; | |
| 89 | Node graphic = null; | |
| 90 | ||
| 91 | if( !empty ) { | |
| 92 | if( isEditing() ) { | |
| 93 | final var textField = mTextField; | |
| 94 | ||
| 95 | if( textField != null ) { | |
| 96 | textField.setText( getString() ); | |
| 97 | } | |
| 98 | ||
| 99 | graphic = textField; | |
| 100 | } | |
| 101 | else { | |
| 102 | text = getString(); | |
| 103 | graphic = getTreeItem().getGraphic(); | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | setText( text ); | |
| 108 | setGraphic( graphic ); | |
| 109 | } | |
| 110 | ||
| 111 | private TextField createTextField() { | |
| 112 | final var textField = new TextField( getString() ); | |
| 113 | ||
| 114 | textField.setOnKeyReleased( t -> { | |
| 115 | switch( t.getCode() ) { | |
| 116 | case ENTER -> commitEdit( textField.getText() ); | |
| 117 | case ESCAPE -> cancelEdit(); | |
| 118 | } | |
| 119 | } ); | |
| 120 | ||
| 121 | return textField; | |
| 122 | } | |
| 123 | ||
| 124 | private String getString() { | |
| 125 | return getConverter().toString( getItem() ); | |
| 126 | } | |
| 127 | } | |
| 128 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 31 | ||
| 32 | import java.util.Map; | |
| 33 | import java.util.regex.Matcher; | |
| 34 | ||
| 35 | import static com.keenwrite.sigils.YamlSigilOperator.REGEX_PATTERN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for performing string interpolation on key/value pairs stored | |
| 39 | * in a map. The values in the map can use a delimited syntax to refer to | |
| 40 | * keys in the map. | |
| 41 | */ | |
| 42 | public class MapInterpolator { | |
| 43 | private static final int GROUP_DELIMITED = 1; | |
| 44 | ||
| 45 | /** | |
| 46 | * Empty. | |
| 47 | */ | |
| 48 | private MapInterpolator() { | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * Performs string interpolation on the values in the given map. This will | |
| 53 | * change any value in the map that contains a variable that matches | |
| 54 | * {@link YamlSigilOperator#REGEX_PATTERN}. | |
| 55 | * | |
| 56 | * @param map Contains values that represent references to keys. | |
| 57 | */ | |
| 58 | public static void interpolate( final Map<String, String> map ) { | |
| 59 | map.replaceAll( ( k, v ) -> resolve( map, v ) ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Given a value with zero or more key references, this will resolve all | |
| 64 | * the values, recursively. If a key cannot be dereferenced, the value will | |
| 65 | * contain the key name. | |
| 66 | * | |
| 67 | * @param map Map to search for keys when resolving key references. | |
| 68 | * @param value Value containing zero or more key references | |
| 69 | * @return The given value with all embedded key references interpolated. | |
| 70 | */ | |
| 71 | private static String resolve( | |
| 72 | final Map<String, String> map, String value ) { | |
| 73 | final Matcher matcher = REGEX_PATTERN.matcher( value ); | |
| 74 | ||
| 75 | while( matcher.find() ) { | |
| 76 | final String keyName = matcher.group( GROUP_DELIMITED ); | |
| 77 | final String mapValue = map.get( keyName ); | |
| 78 | final String keyValue = mapValue == null | |
| 79 | ? keyName | |
| 80 | : resolve( map, mapValue ); | |
| 81 | ||
| 82 | value = value.replace( keyName, keyValue ); | |
| 83 | } | |
| 84 | ||
| 85 | return value; | |
| 86 | } | |
| 87 | } | |
| 88 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | import javafx.scene.control.TreeView; | |
| 32 | ||
| 33 | /** | |
| 34 | * Indicates that this is the top-most {@link TreeItem}. This class allows | |
| 35 | * the {@link TreeItemAdapter} to ignore the topmost definition. Such | |
| 36 | * contortions are necessary because {@link TreeView} requires a root item | |
| 37 | * that isn't part of the user's definition file. | |
| 38 | * <p> | |
| 39 | * Another approach would be to associate object pairs per {@link TreeItem}, | |
| 40 | * but that would be a waste of memory since the only "exception" case is | |
| 41 | * the root {@link TreeItem}. | |
| 42 | * </p> | |
| 43 | * | |
| 44 | * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}. | |
| 45 | */ | |
| 46 | public class RootTreeItem<T> extends DefinitionTreeItem<T> { | |
| 47 | /** | |
| 48 | * Default constructor, calls the superclass, no other behaviour. | |
| 49 | * | |
| 50 | * @param value The {@link TreeItem} node name to construct the superclass. | |
| 51 | * @see TreeItemAdapter#toMap(TreeItem) for details on how this | |
| 52 | * class is used. | |
| 53 | */ | |
| 54 | public RootTreeItem( final T value ) { | |
| 55 | super( value ); | |
| 56 | } | |
| 57 | } | |
| 58 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.io.IOException; | |
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for converting an object hierarchy into a {@link TreeItem} | |
| 37 | * hierarchy. | |
| 38 | */ | |
| 39 | public interface TreeAdapter { | |
| 40 | /** | |
| 41 | * Adapts the document produced by the given parser into a {@link TreeItem} | |
| 42 | * object that can be presented to the user within a GUI. | |
| 43 | * | |
| 44 | * @param root The default root node name. | |
| 45 | * @return The parsed document in a {@link TreeItem} that can be displayed | |
| 46 | * in a panel. | |
| 47 | */ | |
| 48 | TreeItem<String> adapt( String root ); | |
| 49 | ||
| 50 | /** | |
| 51 | * Exports the given root node to the given path. | |
| 52 | * | |
| 53 | * @param root The root node to export. | |
| 54 | * @param path Where to persist the data. | |
| 55 | * @throws IOException Could not write the data to the given path. | |
| 56 | */ | |
| 57 | void export( TreeItem<String> root, Path path ) throws IOException; | |
| 58 | } | |
| 59 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 32 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 33 | import javafx.scene.control.TreeItem; | |
| 34 | import javafx.scene.control.TreeView; | |
| 35 | ||
| 36 | import java.util.HashMap; | |
| 37 | import java.util.Iterator; | |
| 38 | import java.util.Map; | |
| 39 | import java.util.Stack; | |
| 40 | ||
| 41 | import static com.keenwrite.Constants.DEFAULT_MAP_SIZE; | |
| 42 | ||
| 43 | /** | |
| 44 | * Given a {@link TreeItem}, this will generate a flat map with all the | |
| 45 | * values in the tree recursively interpolated. The application integrates | |
| 46 | * definition files as follows: | |
| 47 | * <ol> | |
| 48 | * <li>Load YAML file into {@link JsonNode} hierarchy.</li> | |
| 49 | * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li> | |
| 50 | * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li> | |
| 51 | * <li>Substitute flat map variables into document as required.</li> | |
| 52 | * </ol> | |
| 53 | * | |
| 54 | * <p> | |
| 55 | * This class is responsible for producing the interpolated flat map. This | |
| 56 | * allows dynamic edits of the {@link TreeView} to be displayed in the | |
| 57 | * {@link HTMLPreviewPane} without having to reload the definition file. | |
| 58 | * Reloading the definition file would work, but has a number of drawbacks. | |
| 59 | * </p> | |
| 60 | */ | |
| 61 | public class TreeItemAdapter { | |
| 62 | /** | |
| 63 | * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}). | |
| 64 | */ | |
| 65 | public static final String SEPARATOR = "."; | |
| 66 | ||
| 67 | /** | |
| 68 | * Default buffer length for keys ({@link StringBuilder} has 16 character | |
| 69 | * buffer) that should be large enough for most keys to avoid reallocating | |
| 70 | * memory to increase the {@link StringBuilder}'s buffer. | |
| 71 | */ | |
| 72 | public static final int DEFAULT_KEY_LENGTH = 64; | |
| 73 | ||
| 74 | /** | |
| 75 | * In-order traversal of a {@link TreeItem} hierarchy, exposing each item | |
| 76 | * as a consecutive list. | |
| 77 | */ | |
| 78 | private static final class TreeIterator | |
| 79 | implements Iterator<TreeItem<String>> { | |
| 80 | private final Stack<TreeItem<String>> mStack = new Stack<>(); | |
| 81 | ||
| 82 | public TreeIterator( final TreeItem<String> root ) { | |
| 83 | if( root != null ) { | |
| 84 | mStack.push( root ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public boolean hasNext() { | |
| 90 | return !mStack.isEmpty(); | |
| 91 | } | |
| 92 | ||
| 93 | @Override | |
| 94 | public TreeItem<String> next() { | |
| 95 | final TreeItem<String> next = mStack.pop(); | |
| 96 | next.getChildren().forEach( mStack::push ); | |
| 97 | ||
| 98 | return next; | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | private TreeItemAdapter() { | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Iterate over a given root node (at any level of the tree) and process each | |
| 107 | * leaf node into a flat map. Values must be interpolated separately. | |
| 108 | */ | |
| 109 | public static Map<String, String> toMap( final TreeItem<String> root ) { | |
| 110 | final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 111 | final TreeIterator iterator = new TreeIterator( root ); | |
| 112 | ||
| 113 | iterator.forEachRemaining( item -> { | |
| 114 | if( item.isLeaf() ) { | |
| 115 | map.put( toPath( item.getParent() ), item.getValue() ); | |
| 116 | } | |
| 117 | } ); | |
| 118 | ||
| 119 | return map; | |
| 120 | } | |
| 121 | ||
| 122 | ||
| 123 | /** | |
| 124 | * For a given node, this will ascend the tree to generate a key name | |
| 125 | * that is associated with the leaf node's value. | |
| 126 | * | |
| 127 | * @param node Ascendants represent the key to this node's value. | |
| 128 | * @param <T> Data type that the {@link TreeItem} contains. | |
| 129 | * @return The string representation of the node's unique key. | |
| 130 | */ | |
| 131 | public static <T> String toPath( TreeItem<T> node ) { | |
| 132 | assert node != null; | |
| 133 | ||
| 134 | final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH ); | |
| 135 | final Stack<TreeItem<T>> stack = new Stack<>(); | |
| 136 | ||
| 137 | while( node != null && !(node instanceof RootTreeItem) ) { | |
| 138 | stack.push( node ); | |
| 139 | node = node.getParent(); | |
| 140 | } | |
| 141 | ||
| 142 | // Gets set at end of first iteration (to avoid an if condition). | |
| 143 | String separator = ""; | |
| 144 | ||
| 145 | while( !stack.empty() ) { | |
| 146 | final T subkey = stack.pop().getValue(); | |
| 147 | key.append( separator ); | |
| 148 | key.append( subkey ); | |
| 149 | separator = SEPARATOR; | |
| 150 | } | |
| 151 | ||
| 152 | return YamlSigilOperator.entoken( key.toString() ); | |
| 153 | } | |
| 154 | } | |
| 155 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.keenwrite.definition.DefinitionSource; | |
| 31 | import com.keenwrite.definition.TreeAdapter; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Represents a definition data source for YAML files. | |
| 37 | */ | |
| 38 | public class YamlDefinitionSource implements DefinitionSource { | |
| 39 | ||
| 40 | private final YamlTreeAdapter mYamlTreeAdapter; | |
| 41 | ||
| 42 | /** | |
| 43 | * Constructs a new YAML definition source, populated from the given file. | |
| 44 | * | |
| 45 | * @param path Path to the YAML definition file. | |
| 46 | */ | |
| 47 | public YamlDefinitionSource( final Path path ) { | |
| 48 | assert path != null; | |
| 49 | ||
| 50 | mYamlTreeAdapter = new YamlTreeAdapter( path ); | |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public TreeAdapter getTreeAdapter() { | |
| 55 | return mYamlTreeAdapter; | |
| 56 | } | |
| 57 | } | |
| 58 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 33 | import com.keenwrite.definition.DocumentParser; | |
| 34 | ||
| 35 | import java.io.InputStream; | |
| 36 | import java.nio.file.Files; | |
| 37 | import java.nio.file.Path; | |
| 38 | ||
| 39 | /** | |
| 40 | * Responsible for reading a YAML document into an object hierarchy. | |
| 41 | */ | |
| 42 | public class YamlParser implements DocumentParser<JsonNode> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Start of the Universe (the YAML document node that contains all others). | |
| 46 | */ | |
| 47 | private final JsonNode mDocumentRoot; | |
| 48 | ||
| 49 | /** | |
| 50 | * Creates a new YamlParser instance that attempts to parse the contents | |
| 51 | * of the YAML document given from a path. In the event that the file either | |
| 52 | * does not exist or is empty, a fake | |
| 53 | * | |
| 54 | * @param path Path to a file containing YAML data to parse. | |
| 55 | */ | |
| 56 | public YamlParser( final Path path ) { | |
| 57 | assert path != null; | |
| 58 | mDocumentRoot = parse( path ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the parent node for the entire YAML document tree. | |
| 63 | * | |
| 64 | * @return The document root, never {@code null}. | |
| 65 | */ | |
| 66 | @Override | |
| 67 | public JsonNode getDocumentRoot() { | |
| 68 | return mDocumentRoot; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Parses the given path containing YAML data into an object hierarchy. | |
| 73 | * | |
| 74 | * @param path {@link Path} to the YAML resource to parse. | |
| 75 | * @return The parsed contents, or an empty object hierarchy. | |
| 76 | */ | |
| 77 | private JsonNode parse( final Path path ) { | |
| 78 | try( final InputStream in = Files.newInputStream( path ) ) { | |
| 79 | return new ObjectMapper( new YAMLFactory() ).readTree( in ); | |
| 80 | } catch( final Exception e ) { | |
| 81 | // Ensure that a document root node exists by relying on the | |
| 82 | // default failure condition when processing. This is required | |
| 83 | // because the input stream could not be read. | |
| 84 | return new ObjectMapper().createObjectNode(); | |
| 85 | } | |
| 86 | } | |
| 87 | } | |
| 88 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.node.ObjectNode; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; | |
| 33 | import com.keenwrite.definition.RootTreeItem; | |
| 34 | import com.keenwrite.definition.TreeAdapter; | |
| 35 | import com.keenwrite.definition.DefinitionTreeItem; | |
| 36 | import javafx.scene.control.TreeItem; | |
| 37 | import javafx.scene.control.TreeView; | |
| 38 | ||
| 39 | import java.io.IOException; | |
| 40 | import java.nio.file.Path; | |
| 41 | import java.util.Map.Entry; | |
| 42 | ||
| 43 | /** | |
| 44 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user | |
| 45 | * interface and vice-versa. | |
| 46 | */ | |
| 47 | public class YamlTreeAdapter implements TreeAdapter { | |
| 48 | private final YamlParser mParser; | |
| 49 | ||
| 50 | /** | |
| 51 | * Constructs a new instance that will use the given path to read | |
| 52 | * the object hierarchy from a data source. | |
| 53 | * | |
| 54 | * @param path Path to YAML contents to parse. | |
| 55 | */ | |
| 56 | public YamlTreeAdapter( final Path path ) { | |
| 57 | mParser = new YamlParser( path ); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public void export( final TreeItem<String> treeItem, final Path path ) | |
| 62 | throws IOException { | |
| 63 | final YAMLMapper mapper = new YAMLMapper(); | |
| 64 | final ObjectNode root = mapper.createObjectNode(); | |
| 65 | ||
| 66 | // Iterate over the root item's children. The root item is used by the | |
| 67 | // application to ensure definitions can always be added to a tree, as | |
| 68 | // such it is not meant to be exported, only its children. | |
| 69 | for( final TreeItem<String> child : treeItem.getChildren() ) { | |
| 70 | export( child, root ); | |
| 71 | } | |
| 72 | ||
| 73 | // Writes as UTF8 by default. | |
| 74 | mapper.writeValue( path.toFile(), root ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Recursive method to generate an object hierarchy that represents the | |
| 79 | * given {@link TreeItem} hierarchy. | |
| 80 | * | |
| 81 | * @param item The {@link TreeItem} to reproduce as an object hierarchy. | |
| 82 | * @param node The {@link ObjectNode} to update to reflect the | |
| 83 | * {@link TreeItem} hierarchy. | |
| 84 | */ | |
| 85 | private void export( final TreeItem<String> item, ObjectNode node ) { | |
| 86 | final var children = item.getChildren(); | |
| 87 | ||
| 88 | // If the current item has more than one non-leaf child, it's an | |
| 89 | // object node and must become a new nested object. | |
| 90 | if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) { | |
| 91 | node = node.putObject( item.getValue() ); | |
| 92 | } | |
| 93 | ||
| 94 | for( final TreeItem<String> child : children ) { | |
| 95 | if( child.isLeaf() ) { | |
| 96 | node.put( item.getValue(), child.getValue() ); | |
| 97 | } | |
| 98 | else { | |
| 99 | export( child, node ); | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 106 | * keys. Only the first document in the stream is adapted. | |
| 107 | * | |
| 108 | * @param root Root {@link TreeItem} node name. | |
| 109 | * @return A {@link TreeItem} populated with all the keys in the YAML | |
| 110 | * document. | |
| 111 | */ | |
| 112 | public TreeItem<String> adapt( final String root ) { | |
| 113 | final JsonNode rootNode = getYamlParser().getDocumentRoot(); | |
| 114 | final TreeItem<String> rootItem = createRootTreeItem( root ); | |
| 115 | ||
| 116 | rootItem.setExpanded( true ); | |
| 117 | adapt( rootNode, rootItem ); | |
| 118 | return rootItem; | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 123 | * leaf node. | |
| 124 | * | |
| 125 | * @param rootNode A JSON node (YAML node) to adapt. | |
| 126 | * @param rootItem The tree item to use as the root when processing the node. | |
| 127 | */ | |
| 128 | private void adapt( | |
| 129 | final JsonNode rootNode, final TreeItem<String> rootItem ) { | |
| 130 | rootNode.fields().forEachRemaining( | |
| 131 | ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem ) | |
| 132 | ); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 137 | * | |
| 138 | * @param rootNode The node to adapt. | |
| 139 | * @param rootItem The item to adapt using the node's key. | |
| 140 | */ | |
| 141 | private void adapt( | |
| 142 | final Entry<String, JsonNode> rootNode, | |
| 143 | final TreeItem<String> rootItem ) { | |
| 144 | final JsonNode leafNode = rootNode.getValue(); | |
| 145 | final String key = rootNode.getKey(); | |
| 146 | final TreeItem<String> leaf = createTreeItem( key ); | |
| 147 | ||
| 148 | if( leafNode.isValueNode() ) { | |
| 149 | leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) ); | |
| 150 | } | |
| 151 | ||
| 152 | rootItem.getChildren().add( leaf ); | |
| 153 | ||
| 154 | if( leafNode.isObject() ) { | |
| 155 | adapt( leafNode, leaf ); | |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Creates a new {@link TreeItem} that can be added to the {@link TreeView}. | |
| 161 | * | |
| 162 | * @param value The node's value. | |
| 163 | * @return A new {@link TreeItem}, never {@code null}. | |
| 164 | */ | |
| 165 | private TreeItem<String> createTreeItem( final String value ) { | |
| 166 | return new DefinitionTreeItem<>( value ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Creates a new {@link TreeItem} that is intended to be the root-level item | |
| 171 | * added to the {@link TreeView}. This allows the root item to be | |
| 172 | * distinguished from the other items so that reference keys do not include | |
| 173 | * "Definition" as part of their name. | |
| 174 | * | |
| 175 | * @param value The node's value. | |
| 176 | * @return A new {@link TreeItem}, never {@code null}. | |
| 177 | */ | |
| 178 | private TreeItem<String> createRootTreeItem( final String value ) { | |
| 179 | return new RootTreeItem<>( value ); | |
| 180 | } | |
| 181 | ||
| 182 | public YamlParser getYamlParser() { | |
| 183 | return mParser; | |
| 184 | } | |
| 185 | } | |
| 186 | 1 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.dialogs; | |
| 29 | ||
| 30 | import static com.keenwrite.Messages.get; | |
| 31 | import com.keenwrite.service.events.impl.ButtonOrderPane; | |
| 32 | import static javafx.scene.control.ButtonType.CANCEL; | |
| 33 | import static javafx.scene.control.ButtonType.OK; | |
| 34 | import javafx.scene.control.Dialog; | |
| 35 | import javafx.stage.Window; | |
| 36 | ||
| 37 | /** | |
| 38 | * Superclass that abstracts common behaviours for all dialogs. | |
| 39 | * | |
| 40 | * @param <T> The type of dialog to create (usually String). | |
| 41 | */ | |
| 42 | public abstract class AbstractDialog<T> extends Dialog<T> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Ensures that all dialogs can be closed. | |
| 46 | * | |
| 47 | * @param owner The parent window of this dialog. | |
| 48 | * @param title The messages title to display in the title bar. | |
| 49 | */ | |
| 50 | @SuppressWarnings( "OverridableMethodCallInConstructor" ) | |
| 51 | public AbstractDialog( final Window owner, final String title ) { | |
| 52 | setTitle( get( title ) ); | |
| 53 | setResizable( true ); | |
| 54 | ||
| 55 | initOwner( owner ); | |
| 56 | initCloseAction(); | |
| 57 | initDialogPane(); | |
| 58 | initDialogButtons(); | |
| 59 | initComponents(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Initialize the component layout. | |
| 64 | */ | |
| 65 | protected abstract void initComponents(); | |
| 66 | ||
| 67 | /** | |
| 68 | * Set the dialog to use a button order pane with an OK and a CANCEL button. | |
| 69 | */ | |
| 70 | protected void initDialogPane() { | |
| 71 | setDialogPane( new ButtonOrderPane() ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Set an OK and CANCEL button on the dialog. | |
| 76 | */ | |
| 77 | protected void initDialogButtons() { | |
| 78 | getDialogPane().getButtonTypes().addAll( OK, CANCEL ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Attaches a setOnCloseRequest to the dialog's [X] button so that the user | |
| 83 | * can always close the window, even if there's an error. | |
| 84 | */ | |
| 85 | protected final void initCloseAction() { | |
| 86 | final Window window = getDialogPane().getScene().getWindow(); | |
| 87 | window.setOnCloseRequest( event -> window.hide() ); | |
| 88 | } | |
| 89 | } | |
| 90 | 1 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.dialogs; | |
| 28 | ||
| 29 | import static com.keenwrite.Messages.get; | |
| 30 | import com.keenwrite.controls.BrowseFileButton; | |
| 31 | import com.keenwrite.controls.EscapeTextField; | |
| 32 | import java.nio.file.Path; | |
| 33 | import javafx.application.Platform; | |
| 34 | import javafx.beans.binding.Bindings; | |
| 35 | import javafx.beans.property.SimpleStringProperty; | |
| 36 | import javafx.beans.property.StringProperty; | |
| 37 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 38 | import static javafx.scene.control.ButtonType.OK; | |
| 39 | import javafx.scene.control.DialogPane; | |
| 40 | import javafx.scene.control.Label; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | import javafx.stage.Window; | |
| 43 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown image. | |
| 47 | */ | |
| 48 | public class ImageDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty image = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public ImageDialog( final Window owner, final Path basePath ) { | |
| 53 | super(owner, "Dialog.image.title" ); | |
| 54 | ||
| 55 | final DialogPane dialogPane = getDialogPane(); | |
| 56 | dialogPane.setContent( pane ); | |
| 57 | ||
| 58 | linkBrowseFileButton.setBasePath( basePath ); | |
| 59 | linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) ); | |
| 60 | linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() ); | |
| 61 | ||
| 62 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 63 | urlField.escapedTextProperty().isEmpty() | |
| 64 | .or( textField.escapedTextProperty().isEmpty() ) ); | |
| 65 | ||
| 66 | image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) ); | |
| 69 | previewField.textProperty().bind( image ); | |
| 70 | ||
| 71 | setResultConverter( dialogButton -> { | |
| 72 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 73 | return (data == ButtonData.OK_DONE) ? image.get() : null; | |
| 74 | } ); | |
| 75 | ||
| 76 | Platform.runLater( () -> { | |
| 77 | urlField.requestFocus(); | |
| 78 | ||
| 79 | if( urlField.getText().startsWith( "http://" ) ) { | |
| 80 | urlField.selectRange( "http://".length(), urlField.getLength() ); | |
| 81 | } | |
| 82 | } ); | |
| 83 | } | |
| 84 | ||
| 85 | @Override | |
| 86 | protected void initComponents() { | |
| 87 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 88 | pane = new MigPane(); | |
| 89 | Label urlLabel = new Label(); | |
| 90 | urlField = new EscapeTextField(); | |
| 91 | linkBrowseFileButton = new BrowseFileButton(); | |
| 92 | Label textLabel = new Label(); | |
| 93 | textField = new EscapeTextField(); | |
| 94 | Label titleLabel = new Label(); | |
| 95 | titleField = new EscapeTextField(); | |
| 96 | Label previewLabel = new Label(); | |
| 97 | previewField = new Label(); | |
| 98 | ||
| 99 | //======== pane ======== | |
| 100 | { | |
| 101 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" ); | |
| 102 | pane.setRows( "[][][][]" ); | |
| 103 | ||
| 104 | //---- urlLabel ---- | |
| 105 | urlLabel.setText( get( "Dialog.image.urlLabel.text" ) ); | |
| 106 | pane.add( urlLabel, "cell 0 0" ); | |
| 107 | ||
| 108 | //---- urlField ---- | |
| 109 | urlField.setEscapeCharacters( "()" ); | |
| 110 | urlField.setText( "http://yourlink.com" ); | |
| 111 | urlField.setPromptText( "http://yourlink.com" ); | |
| 112 | pane.add( urlField, "cell 1 0" ); | |
| 113 | pane.add( linkBrowseFileButton, "cell 2 0" ); | |
| 114 | ||
| 115 | //---- textLabel ---- | |
| 116 | textLabel.setText( get( "Dialog.image.textLabel.text" ) ); | |
| 117 | pane.add( textLabel, "cell 0 1" ); | |
| 118 | ||
| 119 | //---- textField ---- | |
| 120 | textField.setEscapeCharacters( "[]" ); | |
| 121 | pane.add( textField, "cell 1 1 2 1" ); | |
| 122 | ||
| 123 | //---- titleLabel ---- | |
| 124 | titleLabel.setText( get( "Dialog.image.titleLabel.text" ) ); | |
| 125 | pane.add( titleLabel, "cell 0 2" ); | |
| 126 | pane.add( titleField, "cell 1 2 2 1" ); | |
| 127 | ||
| 128 | //---- previewLabel ---- | |
| 129 | previewLabel.setText( get( "Dialog.image.previewLabel.text" ) ); | |
| 130 | pane.add( previewLabel, "cell 0 3" ); | |
| 131 | pane.add( previewField, "cell 1 3 2 1" ); | |
| 132 | } | |
| 133 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 134 | } | |
| 135 | ||
| 136 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 137 | private MigPane pane; | |
| 138 | private EscapeTextField urlField; | |
| 139 | private BrowseFileButton linkBrowseFileButton; | |
| 140 | private EscapeTextField textField; | |
| 141 | private EscapeTextField titleField; | |
| 142 | private Label previewField; | |
| 143 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 144 | } | |
| 145 | 1 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "ImageDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "ImageDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 34 | name: "linkBrowseFileButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 39 | name: "textLabel" | |
| 40 | "text": new FormMessage( null, "ImageDialog.textLabel.text" ) | |
| 41 | auxiliary() { | |
| 42 | "JavaCodeGenerator.variableLocal": true | |
| 43 | } | |
| 44 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 45 | "value": "cell 0 1" | |
| 46 | } ) | |
| 47 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 48 | name: "textField" | |
| 49 | "escapeCharacters": "[]" | |
| 50 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 51 | "value": "cell 1 1 2 1" | |
| 52 | } ) | |
| 53 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 54 | name: "titleLabel" | |
| 55 | "text": new FormMessage( null, "ImageDialog.titleLabel.text" ) | |
| 56 | auxiliary() { | |
| 57 | "JavaCodeGenerator.variableLocal": true | |
| 58 | } | |
| 59 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 60 | "value": "cell 0 2" | |
| 61 | } ) | |
| 62 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 63 | name: "titleField" | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 1 2 2 1" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 68 | name: "previewLabel" | |
| 69 | "text": new FormMessage( null, "ImageDialog.previewLabel.text" ) | |
| 70 | auxiliary() { | |
| 71 | "JavaCodeGenerator.variableLocal": true | |
| 72 | } | |
| 73 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 74 | "value": "cell 0 3" | |
| 75 | } ) | |
| 76 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 77 | name: "previewField" | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 1 3 2 1" | |
| 80 | } ) | |
| 81 | }, new FormLayoutConstraints( null ) { | |
| 82 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 83 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 84 | } ) | |
| 85 | } | |
| 86 | } | |
| 87 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.dialogs; | |
| 29 | ||
| 30 | import com.keenwrite.controls.EscapeTextField; | |
| 31 | import com.keenwrite.editors.markdown.HyperlinkModel; | |
| 32 | import javafx.application.Platform; | |
| 33 | import javafx.beans.binding.Bindings; | |
| 34 | import javafx.beans.property.SimpleStringProperty; | |
| 35 | import javafx.beans.property.StringProperty; | |
| 36 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 37 | import javafx.scene.control.DialogPane; | |
| 38 | import javafx.scene.control.Label; | |
| 39 | import javafx.stage.Window; | |
| 40 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 41 | ||
| 42 | import static com.keenwrite.Messages.get; | |
| 43 | import static javafx.scene.control.ButtonType.OK; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown link. | |
| 47 | */ | |
| 48 | public class LinkDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty link = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public LinkDialog( | |
| 53 | final Window owner, final HyperlinkModel hyperlink ) { | |
| 54 | super( owner, "Dialog.link.title" ); | |
| 55 | ||
| 56 | final DialogPane dialogPane = getDialogPane(); | |
| 57 | dialogPane.setContent( pane ); | |
| 58 | ||
| 59 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 60 | urlField.escapedTextProperty().isEmpty() ); | |
| 61 | ||
| 62 | textField.setText( hyperlink.getText() ); | |
| 63 | urlField.setText( hyperlink.getUrl() ); | |
| 64 | titleField.setText( hyperlink.getTitle() ); | |
| 65 | ||
| 66 | link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() ) | |
| 69 | .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) | |
| 70 | .otherwise( urlField.escapedTextProperty() ) ) ); | |
| 71 | ||
| 72 | setResultConverter( dialogButton -> { | |
| 73 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 74 | return (data == ButtonData.OK_DONE) ? link.get() : null; | |
| 75 | } ); | |
| 76 | ||
| 77 | Platform.runLater( () -> { | |
| 78 | urlField.requestFocus(); | |
| 79 | urlField.selectRange( 0, urlField.getLength() ); | |
| 80 | } ); | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | protected void initComponents() { | |
| 85 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 86 | pane = new MigPane(); | |
| 87 | Label urlLabel = new Label(); | |
| 88 | urlField = new EscapeTextField(); | |
| 89 | Label textLabel = new Label(); | |
| 90 | textField = new EscapeTextField(); | |
| 91 | Label titleLabel = new Label(); | |
| 92 | titleField = new EscapeTextField(); | |
| 93 | ||
| 94 | //======== pane ======== | |
| 95 | { | |
| 96 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" ); | |
| 97 | pane.setRows( "[][][][]" ); | |
| 98 | ||
| 99 | //---- urlLabel ---- | |
| 100 | urlLabel.setText( get( "Dialog.link.urlLabel.text" ) ); | |
| 101 | pane.add( urlLabel, "cell 0 0" ); | |
| 102 | ||
| 103 | //---- urlField ---- | |
| 104 | urlField.setEscapeCharacters( "()" ); | |
| 105 | pane.add( urlField, "cell 1 0" ); | |
| 106 | ||
| 107 | //---- textLabel ---- | |
| 108 | textLabel.setText( get( "Dialog.link.textLabel.text" ) ); | |
| 109 | pane.add( textLabel, "cell 0 1" ); | |
| 110 | ||
| 111 | //---- textField ---- | |
| 112 | textField.setEscapeCharacters( "[]" ); | |
| 113 | pane.add( textField, "cell 1 1 3 1" ); | |
| 114 | ||
| 115 | //---- titleLabel ---- | |
| 116 | titleLabel.setText( get( "Dialog.link.titleLabel.text" ) ); | |
| 117 | pane.add( titleLabel, "cell 0 2" ); | |
| 118 | pane.add( titleField, "cell 1 2 3 1" ); | |
| 119 | } | |
| 120 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 121 | } | |
| 122 | ||
| 123 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 124 | private MigPane pane; | |
| 125 | private EscapeTextField urlField; | |
| 126 | private EscapeTextField textField; | |
| 127 | private EscapeTextField titleField; | |
| 128 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 129 | } | |
| 130 | 1 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "LinkDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "LinkDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) { | |
| 34 | name: "linkBrowseDirectoyButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 39 | name: "linkBrowseFileButton" | |
| 40 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 41 | "value": "cell 3 0" | |
| 42 | } ) | |
| 43 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 44 | name: "textLabel" | |
| 45 | "text": new FormMessage( null, "LinkDialog.textLabel.text" ) | |
| 46 | auxiliary() { | |
| 47 | "JavaCodeGenerator.variableLocal": true | |
| 48 | } | |
| 49 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 50 | "value": "cell 0 1" | |
| 51 | } ) | |
| 52 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 53 | name: "textField" | |
| 54 | "escapeCharacters": "[]" | |
| 55 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 56 | "value": "cell 1 1 3 1" | |
| 57 | } ) | |
| 58 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 59 | name: "titleLabel" | |
| 60 | "text": new FormMessage( null, "LinkDialog.titleLabel.text" ) | |
| 61 | auxiliary() { | |
| 62 | "JavaCodeGenerator.variableLocal": true | |
| 63 | } | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 0 2" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 68 | name: "titleField" | |
| 69 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 70 | "value": "cell 1 2 3 1" | |
| 71 | } ) | |
| 72 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 73 | name: "previewLabel" | |
| 74 | "text": new FormMessage( null, "LinkDialog.previewLabel.text" ) | |
| 75 | auxiliary() { | |
| 76 | "JavaCodeGenerator.variableLocal": true | |
| 77 | } | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 0 3" | |
| 80 | } ) | |
| 81 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 82 | name: "previewField" | |
| 83 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 84 | "value": "cell 1 3 3 1" | |
| 85 | } ) | |
| 86 | }, new FormLayoutConstraints( null ) { | |
| 87 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 88 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 89 | } ) | |
| 90 | } | |
| 91 | } | |
| 92 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.AbstractFileFactory; | |
| 31 | import com.keenwrite.sigils.RSigilOperator; | |
| 32 | import com.keenwrite.sigils.SigilOperator; | |
| 33 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 34 | ||
| 35 | import java.nio.file.Path; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for creating a definition name decorator suited to a particular | |
| 39 | * file type. | |
| 40 | */ | |
| 41 | public class DefinitionDecoratorFactory extends AbstractFileFactory { | |
| 42 | ||
| 43 | /** | |
| 44 | * Prevent instantiation. | |
| 45 | */ | |
| 46 | private DefinitionDecoratorFactory() { | |
| 47 | } | |
| 48 | ||
| 49 | public static SigilOperator newInstance( final Path path ) { | |
| 50 | return switch( lookup( path ) ) { | |
| 51 | case RMARKDOWN, RXML -> new RSigilOperator(); | |
| 52 | default -> new YamlSigilOperator(); | |
| 53 | }; | |
| 54 | } | |
| 55 | } | |
| 56 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.FileEditorTab; | |
| 31 | import com.keenwrite.definition.DefinitionPane; | |
| 32 | import com.keenwrite.definition.DefinitionTreeItem; | |
| 33 | import com.keenwrite.sigils.SigilOperator; | |
| 34 | import javafx.scene.control.TreeItem; | |
| 35 | import javafx.scene.input.KeyEvent; | |
| 36 | import org.fxmisc.richtext.StyledTextArea; | |
| 37 | ||
| 38 | import java.nio.file.Path; | |
| 39 | import java.text.BreakIterator; | |
| 40 | ||
| 41 | import static com.keenwrite.Constants.*; | |
| 42 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 43 | import static java.lang.Character.isWhitespace; | |
| 44 | import static javafx.scene.input.KeyCode.SPACE; | |
| 45 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 46 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 47 | ||
| 48 | /** | |
| 49 | * Provides the logic for injecting variable names within the editor. | |
| 50 | */ | |
| 51 | public final class DefinitionNameInjector { | |
| 52 | ||
| 53 | /** | |
| 54 | * Recipient of name injections. | |
| 55 | */ | |
| 56 | private FileEditorTab mTab; | |
| 57 | ||
| 58 | /** | |
| 59 | * Initiates double-click events. | |
| 60 | */ | |
| 61 | private final DefinitionPane mDefinitionPane; | |
| 62 | ||
| 63 | /** | |
| 64 | * Initializes the variable name injector against the given pane. | |
| 65 | * | |
| 66 | * @param pane The definition panel to listen to for double-click events. | |
| 67 | */ | |
| 68 | public DefinitionNameInjector( final DefinitionPane pane ) { | |
| 69 | mDefinitionPane = pane; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Trap Control+Space. | |
| 74 | * | |
| 75 | * @param tab Editor where variable names get injected. | |
| 76 | */ | |
| 77 | public void addListener( final FileEditorTab tab ) { | |
| 78 | assert tab != null; | |
| 79 | mTab = tab; | |
| 80 | ||
| 81 | tab.getEditorPane().addKeyboardListener( | |
| 82 | keyPressed( SPACE, CONTROL_DOWN ), | |
| 83 | this::autoinsert | |
| 84 | ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Inserts the currently selected variable from the {@link DefinitionPane}. | |
| 89 | */ | |
| 90 | public void injectSelectedItem() { | |
| 91 | final var pane = getDefinitionPane(); | |
| 92 | final TreeItem<String> item = pane.getSelectedItem(); | |
| 93 | ||
| 94 | if( item.isLeaf() ) { | |
| 95 | final var leaf = pane.findLeafExact( item.getValue() ); | |
| 96 | final var editor = getEditor(); | |
| 97 | ||
| 98 | editor.insertText( editor.getCaretPosition(), decorate( leaf ) ); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 104 | * substitute the definition reference. | |
| 105 | */ | |
| 106 | public void autoinsert() { | |
| 107 | final String paragraph = getCaretParagraph(); | |
| 108 | final int[] bounds = getWordBoundariesAtCaret(); | |
| 109 | ||
| 110 | try { | |
| 111 | if( isEmptyDefinitionPane() ) { | |
| 112 | clue( STATUS_DEFINITION_EMPTY ); | |
| 113 | } | |
| 114 | else { | |
| 115 | final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] ); | |
| 116 | ||
| 117 | if( word.isBlank() ) { | |
| 118 | clue( STATUS_DEFINITION_BLANK ); | |
| 119 | } | |
| 120 | else { | |
| 121 | final var leaf = findLeaf( word ); | |
| 122 | ||
| 123 | if( leaf == null ) { | |
| 124 | clue( STATUS_DEFINITION_MISSING, word ); | |
| 125 | } | |
| 126 | else { | |
| 127 | replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) ); | |
| 128 | expand( leaf ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | } catch( final Exception ignored ) { | |
| 133 | clue( STATUS_DEFINITION_BLANK ); | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 139 | * substitute the definition reference. | |
| 140 | * | |
| 141 | * @param e Ignored -- it can only be Control+SPACE. | |
| 142 | */ | |
| 143 | private void autoinsert( final KeyEvent e ) { | |
| 144 | autoinsert(); | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Finds the start and end indexes for the word in the current paragraph | |
| 149 | * where the caret is located. There are a few different scenarios, where | |
| 150 | * the caret can be at: the start, end, or middle of a word; also, the | |
| 151 | * caret can be at the end or beginning of a punctuated word; as well, the | |
| 152 | * caret could be at the beginning or end of the line or document. | |
| 153 | */ | |
| 154 | private int[] getWordBoundariesAtCaret() { | |
| 155 | final var paragraph = getCaretParagraph(); | |
| 156 | final var length = paragraph.length(); | |
| 157 | int offset = getCurrentCaretColumn(); | |
| 158 | ||
| 159 | int began = offset; | |
| 160 | int ended = offset; | |
| 161 | ||
| 162 | while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) { | |
| 163 | began--; | |
| 164 | } | |
| 165 | ||
| 166 | while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) { | |
| 167 | ended++; | |
| 168 | } | |
| 169 | ||
| 170 | final var iterator = BreakIterator.getWordInstance(); | |
| 171 | iterator.setText( paragraph ); | |
| 172 | ||
| 173 | while( began < length && iterator.isBoundary( began + 1 ) ) { | |
| 174 | began++; | |
| 175 | } | |
| 176 | ||
| 177 | while( ended > 0 && iterator.isBoundary( ended - 1 ) ) { | |
| 178 | ended--; | |
| 179 | } | |
| 180 | ||
| 181 | return new int[]{began, ended}; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Decorates a {@link TreeItem} using the syntax specific to the type of | |
| 186 | * document being edited. | |
| 187 | * | |
| 188 | * @param leaf The path to the leaf (the definition key) to be decorated. | |
| 189 | */ | |
| 190 | private String decorate( final DefinitionTreeItem<String> leaf ) { | |
| 191 | return decorate( leaf.toPath() ); | |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Decorates a variable using the syntax specific to the type of document | |
| 196 | * being edited. | |
| 197 | * | |
| 198 | * @param variable The variable to decorate in dot-notation without any | |
| 199 | * start or end sigils present. | |
| 200 | */ | |
| 201 | private String decorate( final String variable ) { | |
| 202 | return getVariableDecorator().apply( variable ); | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Updates the text at the given position within the current paragraph. | |
| 207 | * | |
| 208 | * @param posBegan The starting index in the paragraph text to replace. | |
| 209 | * @param posEnded The ending index in the paragraph text to replace. | |
| 210 | * @param text Overwrite the paragraph substring with this text. | |
| 211 | */ | |
| 212 | private void replaceText( | |
| 213 | final int posBegan, final int posEnded, final String text ) { | |
| 214 | final int p = getCurrentParagraph(); | |
| 215 | ||
| 216 | getEditor().replaceText( p, posBegan, p, posEnded, text ); | |
| 217 | } | |
| 218 | ||
| 219 | /** | |
| 220 | * Returns the caret's current paragraph position. | |
| 221 | * | |
| 222 | * @return A number greater than or equal to 0. | |
| 223 | */ | |
| 224 | private int getCurrentParagraph() { | |
| 225 | return getEditor().getCurrentParagraph(); | |
| 226 | } | |
| 227 | ||
| 228 | /** | |
| 229 | * Returns the text for the paragraph that contains the caret. | |
| 230 | * | |
| 231 | * @return A non-null string, possibly empty. | |
| 232 | */ | |
| 233 | private String getCaretParagraph() { | |
| 234 | return getEditor().getText( getCurrentParagraph() ); | |
| 235 | } | |
| 236 | ||
| 237 | /** | |
| 238 | * Returns the caret position within the current paragraph. | |
| 239 | * | |
| 240 | * @return A value from 0 to the length of the current paragraph. | |
| 241 | */ | |
| 242 | private int getCurrentCaretColumn() { | |
| 243 | return getEditor().getCaretColumn(); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Looks for the given word, matching first by exact, next by a starts-with | |
| 248 | * condition with diacritics replaced, then by containment. | |
| 249 | * | |
| 250 | * @param word The word to match by: exact, at the beginning, or containment. | |
| 251 | * @return The matching {@link DefinitionTreeItem} for the given word, or | |
| 252 | * {@code null} if none found. | |
| 253 | */ | |
| 254 | @SuppressWarnings("ConstantConditions") | |
| 255 | private DefinitionTreeItem<String> findLeaf( final String word ) { | |
| 256 | assert word != null; | |
| 257 | ||
| 258 | final var pane = getDefinitionPane(); | |
| 259 | DefinitionTreeItem<String> leaf = null; | |
| 260 | ||
| 261 | leaf = leaf == null ? pane.findLeafExact( word ) : leaf; | |
| 262 | leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf; | |
| 263 | leaf = leaf == null ? pane.findLeafContains( word ) : leaf; | |
| 264 | leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf; | |
| 265 | ||
| 266 | return leaf; | |
| 267 | } | |
| 268 | ||
| 269 | /** | |
| 270 | * Answers whether there are any definitions in the tree. | |
| 271 | * | |
| 272 | * @return {@code true} when there are no definitions; {@code false} when | |
| 273 | * there's at least one definition. | |
| 274 | */ | |
| 275 | private boolean isEmptyDefinitionPane() { | |
| 276 | return getDefinitionPane().isEmpty(); | |
| 277 | } | |
| 278 | ||
| 279 | /** | |
| 280 | * Collapses the tree then expands and selects the given node. | |
| 281 | * | |
| 282 | * @param node The node to expand. | |
| 283 | */ | |
| 284 | private void expand( final TreeItem<String> node ) { | |
| 285 | final DefinitionPane pane = getDefinitionPane(); | |
| 286 | pane.collapse(); | |
| 287 | pane.expand( node ); | |
| 288 | pane.select( node ); | |
| 289 | } | |
| 290 | ||
| 291 | /** | |
| 292 | * @return A variable decorator that corresponds to the given file type. | |
| 293 | */ | |
| 294 | private SigilOperator getVariableDecorator() { | |
| 295 | return DefinitionDecoratorFactory.newInstance( getFilename() ); | |
| 296 | } | |
| 297 | ||
| 298 | private Path getFilename() { | |
| 299 | return getFileEditorTab().getPath(); | |
| 300 | } | |
| 301 | ||
| 302 | private EditorPane getEditorPane() { | |
| 303 | return getFileEditorTab().getEditorPane(); | |
| 304 | } | |
| 305 | ||
| 306 | private StyledTextArea<?, ?> getEditor() { | |
| 307 | return getEditorPane().getEditor(); | |
| 308 | } | |
| 309 | ||
| 310 | public FileEditorTab getFileEditorTab() { | |
| 311 | return mTab; | |
| 312 | } | |
| 313 | ||
| 314 | private DefinitionPane getDefinitionPane() { | |
| 315 | return mDefinitionPane; | |
| 316 | } | |
| 317 | } | |
| 318 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.UserPreferences; | |
| 31 | import javafx.beans.property.IntegerProperty; | |
| 32 | import javafx.beans.property.ObjectProperty; | |
| 33 | import javafx.beans.property.SimpleObjectProperty; | |
| 34 | import javafx.beans.value.ChangeListener; | |
| 35 | import javafx.event.Event; | |
| 36 | import javafx.scene.control.ScrollPane; | |
| 37 | import javafx.scene.layout.Pane; | |
| 38 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 39 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 40 | import org.fxmisc.undo.UndoManager; | |
| 41 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 42 | import org.fxmisc.wellbehaved.event.Nodes; | |
| 43 | ||
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.function.Consumer; | |
| 46 | ||
| 47 | import static com.keenwrite.StatusBarNotifier.clearClue; | |
| 48 | import static java.lang.String.format; | |
| 49 | import static javafx.application.Platform.runLater; | |
| 50 | import static org.fxmisc.wellbehaved.event.InputMap.consume; | |
| 51 | ||
| 52 | /** | |
| 53 | * Represents common editing features for various types of text editors. | |
| 54 | */ | |
| 55 | public class EditorPane extends Pane { | |
| 56 | ||
| 57 | /** | |
| 58 | * Used when changing the text area font size. | |
| 59 | */ | |
| 60 | private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;"; | |
| 61 | ||
| 62 | private final StyleClassedTextArea mEditor = | |
| 63 | new StyleClassedTextArea( false ); | |
| 64 | private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane = | |
| 65 | new VirtualizedScrollPane<>( mEditor ); | |
| 66 | private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>(); | |
| 67 | ||
| 68 | public EditorPane() { | |
| 69 | getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS ); | |
| 70 | fontsSizeProperty().addListener( | |
| 71 | ( l, o, n ) -> setFontSize( n.intValue() ) | |
| 72 | ); | |
| 73 | ||
| 74 | // Clear out any previous alerts after the user has typed. If the problem | |
| 75 | // persists, re-rendering the document will re-raise the error. If there | |
| 76 | // was no previous error, clearing the alert is essentially a no-op. | |
| 77 | mEditor.textProperty().addListener( | |
| 78 | ( l, o, n ) -> clearClue() | |
| 79 | ); | |
| 80 | } | |
| 81 | ||
| 82 | @Override | |
| 83 | public void requestFocus() { | |
| 84 | requestFocus( 3 ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * There's a race-condition between displaying the {@link EditorPane} | |
| 89 | * and giving the {@link #mEditor} focus. Try to focus up to {@code max} | |
| 90 | * times before giving up. | |
| 91 | * | |
| 92 | * @param max The number of attempts to try to request focus. | |
| 93 | */ | |
| 94 | private void requestFocus( final int max ) { | |
| 95 | if( max > 0 ) { | |
| 96 | runLater( | |
| 97 | () -> { | |
| 98 | final var editor = getEditor(); | |
| 99 | ||
| 100 | if( !editor.isFocused() ) { | |
| 101 | editor.requestFocus(); | |
| 102 | requestFocus( max - 1 ); | |
| 103 | } | |
| 104 | } | |
| 105 | ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | public void undo() { | |
| 110 | getUndoManager().undo(); | |
| 111 | } | |
| 112 | ||
| 113 | public void redo() { | |
| 114 | getUndoManager().redo(); | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Cuts the actively selected text; if no text is selected, this will cut | |
| 119 | * the entire paragraph. | |
| 120 | */ | |
| 121 | public void cut() { | |
| 122 | final var editor = getEditor(); | |
| 123 | final var selected = editor.getSelectedText(); | |
| 124 | ||
| 125 | if( selected == null || selected.isEmpty() ) { | |
| 126 | editor.selectParagraph(); | |
| 127 | } | |
| 128 | ||
| 129 | editor.cut(); | |
| 130 | } | |
| 131 | ||
| 132 | public void copy() { | |
| 133 | getEditor().copy(); | |
| 134 | } | |
| 135 | ||
| 136 | public void paste() { | |
| 137 | getEditor().paste(); | |
| 138 | } | |
| 139 | ||
| 140 | public void selectAll() { | |
| 141 | getEditor().selectAll(); | |
| 142 | } | |
| 143 | ||
| 144 | public UndoManager<?> getUndoManager() { | |
| 145 | return getEditor().getUndoManager(); | |
| 146 | } | |
| 147 | ||
| 148 | public String getText() { | |
| 149 | return getEditor().getText(); | |
| 150 | } | |
| 151 | ||
| 152 | public void setText( final String text ) { | |
| 153 | final var editor = getEditor(); | |
| 154 | editor.deselect(); | |
| 155 | editor.replaceText( text ); | |
| 156 | getUndoManager().mark(); | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Call to hook into changes to the text area. | |
| 161 | * | |
| 162 | * @param listener Receives editor text change events. | |
| 163 | */ | |
| 164 | public void addTextChangeListener( | |
| 165 | final ChangeListener<? super String> listener ) { | |
| 166 | getEditor().textProperty().addListener( listener ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Notifies observers when the caret changes position. | |
| 171 | * | |
| 172 | * @param listener Receives change event. | |
| 173 | */ | |
| 174 | public void addCaretPositionListener( | |
| 175 | final ChangeListener<? super Integer> listener ) { | |
| 176 | getEditor().caretPositionProperty().addListener( listener ); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * This method adds listeners to editor events. | |
| 181 | * | |
| 182 | * @param <T> The event type. | |
| 183 | * @param <U> The consumer type for the given event type. | |
| 184 | * @param event The event of interest. | |
| 185 | * @param consumer The method to call when the event happens. | |
| 186 | */ | |
| 187 | public <T extends Event, U extends T> void addKeyboardListener( | |
| 188 | final EventPattern<? super T, ? extends U> event, | |
| 189 | final Consumer<? super U> consumer ) { | |
| 190 | Nodes.addInputMap( getEditor(), consume( event, consumer ) ); | |
| 191 | } | |
| 192 | ||
| 193 | /** | |
| 194 | * Repositions the cursor and scroll bar to the top of the file. | |
| 195 | */ | |
| 196 | public void scrollToTop() { | |
| 197 | getEditor().moveTo( 0 ); | |
| 198 | getScrollPane().scrollYToPixel( 0 ); | |
| 199 | } | |
| 200 | ||
| 201 | public StyleClassedTextArea getEditor() { | |
| 202 | return mEditor; | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Returns the scroll pane that contains the text area. | |
| 207 | * | |
| 208 | * @return The scroll pane that contains the content to edit. | |
| 209 | */ | |
| 210 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 211 | return mScrollPane; | |
| 212 | } | |
| 213 | ||
| 214 | public Path getPath() { | |
| 215 | return mPath.get(); | |
| 216 | } | |
| 217 | ||
| 218 | public void setPath( final Path path ) { | |
| 219 | mPath.set( path ); | |
| 220 | } | |
| 221 | ||
| 222 | /** | |
| 223 | * Sets the font size in points. | |
| 224 | * | |
| 225 | * @param size The new font size to use for the text editor. | |
| 226 | */ | |
| 227 | private void setFontSize( final int size ) { | |
| 228 | mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) ); | |
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Returns the text editor font size property for handling font size change | |
| 233 | * events. | |
| 234 | */ | |
| 235 | private IntegerProperty fontsSizeProperty() { | |
| 236 | return UserPreferences.getInstance().fontsSizeEditorProperty(); | |
| 237 | } | |
| 238 | } | |
| 239 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors; | |
| 3 | ||
| 4 | import com.keenwrite.editors.definition.DefinitionEditor; | |
| 5 | import com.keenwrite.editors.definition.DefinitionTreeItem; | |
| 6 | import com.keenwrite.editors.markdown.MarkdownEditor; | |
| 7 | import com.keenwrite.sigils.Tokens; | |
| 8 | import javafx.scene.control.TreeItem; | |
| 9 | ||
| 10 | import java.util.Map; | |
| 11 | ||
| 12 | /** | |
| 13 | * Differentiates an instance of {@link TextResource} from an instance of | |
| 14 | * {@link DefinitionEditor} or {@link MarkdownEditor}. | |
| 15 | */ | |
| 16 | public interface TextDefinition extends TextResource { | |
| 17 | /** | |
| 18 | * Converts the definitions into a map, ready for interpolation. | |
| 19 | * | |
| 20 | * @return The list of key value pairs delimited with tokens. | |
| 21 | */ | |
| 22 | Map<String, String> toMap(); | |
| 23 | ||
| 24 | /** | |
| 25 | * Performs string interpolation on the values in the given map. This will | |
| 26 | * change any value in the map that contains a variable that matches | |
| 27 | * the definition regex pattern against the given {@link Tokens}. | |
| 28 | * | |
| 29 | * @param map Contains values that represent references to keys. | |
| 30 | * @param tokens The beginning and ending tokens that delimit variables. | |
| 31 | */ | |
| 32 | Map<String, String> interpolate( Map<String, String> map, Tokens tokens ); | |
| 33 | ||
| 34 | /** | |
| 35 | * Requests that the visual representation be expanded to the given | |
| 36 | * node. | |
| 37 | * | |
| 38 | * @param node Request expansion to this node. | |
| 39 | */ | |
| 40 | <T> void expand( TreeItem<T> node ); | |
| 41 | ||
| 42 | /** | |
| 43 | * Adds a new item to the definition hierarchy. | |
| 44 | */ | |
| 45 | void createDefinition(); | |
| 46 | ||
| 47 | /** | |
| 48 | * Edits the currently selected definition in the hierarchy. | |
| 49 | */ | |
| 50 | void renameDefinition(); | |
| 51 | ||
| 52 | /** | |
| 53 | * Removes the currently selected definition in the hierarchy. | |
| 54 | */ | |
| 55 | void deleteDefinitions(); | |
| 56 | ||
| 57 | /** | |
| 58 | * Finds the definition that exact matches the given text. | |
| 59 | * | |
| 60 | * @param text The value to find, never {@code null}. | |
| 61 | * @return The leaf that contains the given value. | |
| 62 | */ | |
| 63 | DefinitionTreeItem<String> findLeafExact( String text ); | |
| 64 | ||
| 65 | /** | |
| 66 | * Finds the definition that starts with the given text. | |
| 67 | * | |
| 68 | * @param text The value to find, never {@code null}. | |
| 69 | * @return The leaf that starts with the given value. | |
| 70 | */ | |
| 71 | DefinitionTreeItem<String> findLeafStartsWith( String text ); | |
| 72 | ||
| 73 | /** | |
| 74 | * Finds the definition that contains the given text, matching case. | |
| 75 | * | |
| 76 | * @param text The value to find, never {@code null}. | |
| 77 | * @return The leaf that contains the exact given value. | |
| 78 | */ | |
| 79 | DefinitionTreeItem<String> findLeafContains( String text ); | |
| 80 | ||
| 81 | /** | |
| 82 | * Finds the definition that contains the given text, ignoring case. | |
| 83 | * | |
| 84 | * @param text The value to find, never {@code null}. | |
| 85 | * @return The leaf that contains the given value, regardless of case. | |
| 86 | */ | |
| 87 | DefinitionTreeItem<String> findLeafContainsNoCase( String text ); | |
| 88 | ||
| 89 | /** | |
| 90 | * Answers whether there are any definitions written. | |
| 91 | * | |
| 92 | * @return {@code true} when there are no definitions. | |
| 93 | */ | |
| 94 | boolean isEmpty(); | |
| 95 | } | |
| 1 | 96 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors; | |
| 3 | ||
| 4 | import com.keenwrite.processors.markdown.Caret; | |
| 5 | import javafx.scene.control.IndexRange; | |
| 6 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 7 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 8 | ||
| 9 | /** | |
| 10 | * Responsible for differentiating an instance of {@link TextResource} from | |
| 11 | * other {@link TextResource} subtypes, such as a {@link TextDefinition}. | |
| 12 | * This is primarily used as a marker interface, but also defines a minimal | |
| 13 | * set of functionality required by all {@link TextEditor} instances, which | |
| 14 | * includes scrolling facilities. | |
| 15 | */ | |
| 16 | public interface TextEditor extends TextResource { | |
| 17 | ||
| 18 | /** | |
| 19 | * Returns the scrollbars associated with the editor's view so that they | |
| 20 | * can be moved for synchronized scrolling. | |
| 21 | * | |
| 22 | * @return The initialized horizontal and vertical scrollbars. | |
| 23 | */ | |
| 24 | VirtualizedScrollPane<StyleClassedTextArea> getScrollPane(); | |
| 25 | ||
| 26 | StyleClassedTextArea getTextArea(); | |
| 27 | ||
| 28 | /** | |
| 29 | * Requests that styling be added to the document between the given | |
| 30 | * integer values. | |
| 31 | * | |
| 32 | * @param indexes Document offset where style is to start and end. | |
| 33 | * @param style The style class to apply between the given offset indexes. | |
| 34 | */ | |
| 35 | default void stylize( final IndexRange indexes, final String style ) { | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * Requests that the most recent styling for the given style class be | |
| 40 | * removed from the document between the given integer values. | |
| 41 | */ | |
| 42 | default void unstylize( final String style ) { | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Returns the complete text for the specified paragraph index. | |
| 47 | * | |
| 48 | * @param paragraph The zero-based paragraph index. | |
| 49 | * @throws IndexOutOfBoundsException The paragraph index is less than zero | |
| 50 | * or greater than the number of | |
| 51 | * paragraphs in the document. | |
| 52 | */ | |
| 53 | String getText( int paragraph ) throws IndexOutOfBoundsException; | |
| 54 | ||
| 55 | /** | |
| 56 | * Returns the text between the indexes specified by the given | |
| 57 | * {@link IndexRange}. | |
| 58 | * | |
| 59 | * @param indexes The start and end document indexes to reference. | |
| 60 | * @return The text between the specified indexes. | |
| 61 | * @throws IndexOutOfBoundsException The indexes are invalid. | |
| 62 | */ | |
| 63 | String getText( IndexRange indexes ) throws IndexOutOfBoundsException; | |
| 64 | ||
| 65 | /** | |
| 66 | * Moves the caret to the given document offset. | |
| 67 | * | |
| 68 | * @param offset The absolute offset into the document, zero-based. | |
| 69 | */ | |
| 70 | void moveTo( final int offset ); | |
| 71 | ||
| 72 | /** | |
| 73 | * Returns an object that can be used to track the current caret position | |
| 74 | * within the document. | |
| 75 | * | |
| 76 | * @return The caret's position, which is updated continuously. | |
| 77 | */ | |
| 78 | Caret getCaret(); | |
| 79 | ||
| 80 | /** | |
| 81 | * Replaces the text within the given range with the given string. | |
| 82 | * | |
| 83 | * @param indexes The starting and ending document indexes that represent | |
| 84 | * the range of text to replace. | |
| 85 | * @param s The text to replace, which can be shorter or longer than the | |
| 86 | * specified range. | |
| 87 | */ | |
| 88 | void replaceText( IndexRange indexes, String s ); | |
| 89 | ||
| 90 | /** | |
| 91 | * Returns the starting and ending indexes into the document for the | |
| 92 | * word at the current caret position. | |
| 93 | * <p> | |
| 94 | * Finds the start and end indexes for the word in the current document, | |
| 95 | * where the caret is located. There are a few different scenarios, where | |
| 96 | * the caret can be at: the start, end, or middle of a word; also, the | |
| 97 | * caret can be at the end or beginning of a punctuated word; as well, the | |
| 98 | * caret could be at the beginning or end of the line or document. | |
| 99 | * </p> | |
| 100 | * | |
| 101 | * @return The start and ending index into the current document that | |
| 102 | * represent the word boundaries of the word under the caret. | |
| 103 | */ | |
| 104 | IndexRange getCaretWord(); | |
| 105 | ||
| 106 | /** | |
| 107 | * Convenience method to get the word at the current caret position. | |
| 108 | * | |
| 109 | * @return This will return the empty string if the caret is out of bounds. | |
| 110 | */ | |
| 111 | default String getCaretWordText() { | |
| 112 | return getText( getCaretWord() ); | |
| 113 | } | |
| 114 | ||
| 115 | /** | |
| 116 | * Requests undoing the last text-changing action. | |
| 117 | */ | |
| 118 | void undo(); | |
| 119 | ||
| 120 | /** | |
| 121 | * Requests redoing the last text-changing action that was undone. | |
| 122 | */ | |
| 123 | void redo(); | |
| 124 | ||
| 125 | /** | |
| 126 | * Requests cutting the selected text, or the current line if none selected. | |
| 127 | */ | |
| 128 | void cut(); | |
| 129 | ||
| 130 | /** | |
| 131 | * Requests copying the selected text, or no operation if none selected. | |
| 132 | */ | |
| 133 | void copy(); | |
| 134 | ||
| 135 | /** | |
| 136 | * Requests pasting from the clipboard into the editor. This will replace | |
| 137 | * text if selected, otherwise the clipboard contents are inserted at the | |
| 138 | * cursor. | |
| 139 | */ | |
| 140 | void paste(); | |
| 141 | ||
| 142 | /** | |
| 143 | * Requests selecting the entire document. This will replace the existing | |
| 144 | * selection, if any. | |
| 145 | */ | |
| 146 | void selectAll(); | |
| 147 | ||
| 148 | /** | |
| 149 | * Requests making the selected text, or word at caret, bold. | |
| 150 | */ | |
| 151 | default void bold() { | |
| 152 | } | |
| 153 | ||
| 154 | /** | |
| 155 | * Requests making the selected text, or word at caret, italic. | |
| 156 | */ | |
| 157 | default void italic() { | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Requests making the selected text, or word at caret, a superscript. | |
| 162 | */ | |
| 163 | default void superscript() { | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Requests making the selected text, or word at caret, a subscript. | |
| 168 | */ | |
| 169 | default void subscript() { | |
| 170 | } | |
| 171 | ||
| 172 | /** | |
| 173 | * Requests making the selected text, or word at caret, struck. | |
| 174 | */ | |
| 175 | default void strikethrough() { | |
| 176 | } | |
| 177 | ||
| 178 | /** | |
| 179 | * Requests making the selected text, or word at caret, a blockquote block. | |
| 180 | */ | |
| 181 | default void blockquote() { | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Requests making the selected text, or word at caret, inline code. | |
| 186 | */ | |
| 187 | default void code() { | |
| 188 | } | |
| 189 | ||
| 190 | /** | |
| 191 | * Requests making the selected text, or word at caret, a fenced code block. | |
| 192 | */ | |
| 193 | default void fencedCodeBlock() { | |
| 194 | } | |
| 195 | ||
| 196 | /** | |
| 197 | * Requests making the selected text, or word at caret, a heading. | |
| 198 | * | |
| 199 | * @param level The heading level to apply (typically 1 through 3). | |
| 200 | */ | |
| 201 | default void heading( final int level ) { | |
| 202 | } | |
| 203 | ||
| 204 | /** | |
| 205 | * Requests making the selected text, or word at caret, an unordered list | |
| 206 | * block. | |
| 207 | */ | |
| 208 | default void unorderedList() { | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Requests making the selected text, or word at caret, an ordered list block. | |
| 213 | */ | |
| 214 | default void orderedList() { | |
| 215 | } | |
| 216 | ||
| 217 | /** | |
| 218 | * Requests making the selected text, or inserting at the caret, a | |
| 219 | * horizontal rule. | |
| 220 | */ | |
| 221 | default void horizontalRule() { | |
| 222 | } | |
| 223 | } | |
| 1 | 224 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors; | |
| 3 | ||
| 4 | import com.keenwrite.io.MediaType; | |
| 5 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 6 | import javafx.scene.Node; | |
| 7 | import org.mozilla.universalchardet.UniversalDetector; | |
| 8 | ||
| 9 | import java.io.File; | |
| 10 | import java.nio.charset.Charset; | |
| 11 | import java.nio.file.Path; | |
| 12 | ||
| 13 | import static com.keenwrite.Constants.DEFAULT_CHARSET; | |
| 14 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 15 | import static java.nio.charset.Charset.forName; | |
| 16 | import static java.nio.file.Files.readAllBytes; | |
| 17 | import static java.nio.file.Files.write; | |
| 18 | import static java.util.Locale.ENGLISH; | |
| 19 | ||
| 20 | /** | |
| 21 | * A text resource can be persisted and retrieved from its persisted location. | |
| 22 | */ | |
| 23 | public interface TextResource { | |
| 24 | /** | |
| 25 | * Sets the text string that to be changed through some graphical user | |
| 26 | * interface. For example, a YAML document must be parsed from the given | |
| 27 | * text string into a tree view with which the user may interact. | |
| 28 | * | |
| 29 | * @param text The new content for the resource. | |
| 30 | */ | |
| 31 | void setText( String text ); | |
| 32 | ||
| 33 | /** | |
| 34 | * Returns the text string that may have been modified by the user through | |
| 35 | * some graphical user interface. | |
| 36 | * | |
| 37 | * @return The text value, based on the value set from | |
| 38 | * {@link #setText(String)}, but possibly mutated. | |
| 39 | */ | |
| 40 | String getText(); | |
| 41 | ||
| 42 | /** | |
| 43 | * Return the character encoding for this file. | |
| 44 | * | |
| 45 | * @return A non-null character set, primarily detected from file contents. | |
| 46 | */ | |
| 47 | Charset getEncoding(); | |
| 48 | ||
| 49 | /** | |
| 50 | * Renames the current file to the given fully qualified file name. | |
| 51 | * | |
| 52 | * @param file The new file name. | |
| 53 | */ | |
| 54 | void rename( final File file ); | |
| 55 | ||
| 56 | /** | |
| 57 | * Returns the file name, without any directory components, for this instance. | |
| 58 | * Useful for showing as a tab title. | |
| 59 | * | |
| 60 | * @return The file name value returned from {@link #getFile()}. | |
| 61 | */ | |
| 62 | default String getFilename() { | |
| 63 | final var filename = getFile().toPath().getFileName(); | |
| 64 | return filename == null ? "" : filename.toString(); | |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * Returns the fully qualified {@link File} to the editable text resource. | |
| 69 | * Useful for showing as a tab tooltip, saving the file, or reading it. | |
| 70 | * | |
| 71 | * @return A non-null {@link File} instance. | |
| 72 | */ | |
| 73 | File getFile(); | |
| 74 | ||
| 75 | /** | |
| 76 | * Returns the {@link MediaType} associated with the file being edited. | |
| 77 | * | |
| 78 | * @return The {@link MediaType} for the editor's file. | |
| 79 | */ | |
| 80 | default MediaType getMediaType() { | |
| 81 | return MediaType.valueFrom( getFile() ); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Returns the fully qualified {@link Path} to the editable text resource. | |
| 86 | * This delegates to {@link #getFile()}. | |
| 87 | * | |
| 88 | * @return A non-null {@link Path} instance. | |
| 89 | */ | |
| 90 | default Path getPath() { | |
| 91 | return getFile().toPath(); | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Read the file contents and update the text accordingly. If the file | |
| 96 | * cannot be read then no changes will happen to the text. Fails silently. | |
| 97 | * | |
| 98 | * @param path The fully qualified {@link Path}, including a file name, to | |
| 99 | * fully read into the editor. | |
| 100 | * @return The character encoding for the file at the given {@link Path}. | |
| 101 | */ | |
| 102 | default Charset open( final Path path ) { | |
| 103 | final var file = path.toFile(); | |
| 104 | Charset encoding = DEFAULT_CHARSET; | |
| 105 | ||
| 106 | try { | |
| 107 | if( file.exists() ) { | |
| 108 | if( file.canWrite() && file.canRead() ) { | |
| 109 | final var bytes = readAllBytes( path ); | |
| 110 | encoding = detectEncoding( bytes ); | |
| 111 | ||
| 112 | setText( asString( bytes, encoding ) ); | |
| 113 | } | |
| 114 | else { | |
| 115 | clue( "TextResource.load.error.permissions", file.toString() ); | |
| 116 | } | |
| 117 | } | |
| 118 | else { | |
| 119 | clue( "TextResource.load.error.unsaved", file.toString() ); | |
| 120 | } | |
| 121 | } catch( final Exception ex ) { | |
| 122 | clue( ex ); | |
| 123 | } | |
| 124 | ||
| 125 | return encoding; | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Read the file contents and update the text accordingly. If the file | |
| 130 | * cannot be read then no changes will happen to the text. This delegates | |
| 131 | * to {@link #open(Path)}. | |
| 132 | * | |
| 133 | * @param file The {@link File} to fully read into the editor. | |
| 134 | * @return The file's character encoding. | |
| 135 | */ | |
| 136 | default Charset open( final File file ) { | |
| 137 | return open( file.toPath() ); | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Save the file contents and clear the modified flag. If the file cannot | |
| 142 | * be saved, the exception is swallowed and this method returns {@code false}. | |
| 143 | * | |
| 144 | * @return {@code true} the file was saved; {@code false} if upon exception. | |
| 145 | */ | |
| 146 | default boolean save() { | |
| 147 | try { | |
| 148 | write( getPath(), asBytes( getText() ) ); | |
| 149 | clearModifiedProperty(); | |
| 150 | return true; | |
| 151 | } catch( final Exception ex ) { | |
| 152 | clue( ex ); | |
| 153 | } | |
| 154 | ||
| 155 | return false; | |
| 156 | } | |
| 157 | ||
| 158 | /** | |
| 159 | * Returns the node associated with this {@link TextResource}. | |
| 160 | * | |
| 161 | * @return The view component for the {@link TextResource}. | |
| 162 | */ | |
| 163 | Node getNode(); | |
| 164 | ||
| 165 | /** | |
| 166 | * Answers whether the resource has been modified. | |
| 167 | * | |
| 168 | * @return {@code true} the resource has changed; {@code false} means that | |
| 169 | * no changes to the resource have been made. | |
| 170 | */ | |
| 171 | default boolean isModified() { | |
| 172 | return modifiedProperty().get(); | |
| 173 | } | |
| 174 | ||
| 175 | /** | |
| 176 | * Returns a property that answers whether this text resource has been | |
| 177 | * changed from the original text that was opened. | |
| 178 | * | |
| 179 | * @return A property representing the modified state of this | |
| 180 | * {@link TextResource}. | |
| 181 | */ | |
| 182 | ReadOnlyBooleanProperty modifiedProperty(); | |
| 183 | ||
| 184 | /** | |
| 185 | * Lowers the modified flag such that listeners to the modified property | |
| 186 | * will be informed that the text that's being edited no longer differs | |
| 187 | * from what's persisted. | |
| 188 | */ | |
| 189 | void clearModifiedProperty(); | |
| 190 | ||
| 191 | private String asString( final byte[] text, final Charset encoding ) { | |
| 192 | return new String( text, encoding ); | |
| 193 | } | |
| 194 | ||
| 195 | /** | |
| 196 | * Converts the given string to an array of bytes using the encoding that was | |
| 197 | * originally detected (if any) and associated with this file. | |
| 198 | * | |
| 199 | * @param text The text to convert into the original file encoding. | |
| 200 | * @return A series of bytes ready for writing to a file. | |
| 201 | */ | |
| 202 | private byte[] asBytes( final String text ) { | |
| 203 | return text.getBytes( getEncoding() ); | |
| 204 | } | |
| 205 | ||
| 206 | private Charset detectEncoding( final byte[] bytes ) { | |
| 207 | final var detector = new UniversalDetector( null ); | |
| 208 | detector.handleData( bytes, 0, bytes.length ); | |
| 209 | detector.dataEnd(); | |
| 210 | ||
| 211 | final var charset = detector.getDetectedCharset(); | |
| 212 | ||
| 213 | return charset == null | |
| 214 | ? DEFAULT_CHARSET | |
| 215 | : forName( charset.toUpperCase( ENGLISH ) ); | |
| 216 | } | |
| 217 | } | |
| 1 | 218 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import com.keenwrite.Constants; | |
| 5 | import com.keenwrite.editors.TextDefinition; | |
| 6 | import com.keenwrite.sigils.Tokens; | |
| 7 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 8 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 9 | import javafx.beans.property.BooleanProperty; | |
| 10 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 11 | import javafx.beans.property.SimpleBooleanProperty; | |
| 12 | import javafx.collections.ObservableList; | |
| 13 | import javafx.event.ActionEvent; | |
| 14 | import javafx.event.Event; | |
| 15 | import javafx.event.EventHandler; | |
| 16 | import javafx.scene.Node; | |
| 17 | import javafx.scene.control.*; | |
| 18 | import javafx.scene.input.KeyEvent; | |
| 19 | import javafx.scene.layout.BorderPane; | |
| 20 | import javafx.scene.layout.HBox; | |
| 21 | ||
| 22 | import java.io.File; | |
| 23 | import java.nio.charset.Charset; | |
| 24 | import java.util.*; | |
| 25 | import java.util.regex.Pattern; | |
| 26 | ||
| 27 | import static com.keenwrite.Constants.DEFINITION_DEFAULT; | |
| 28 | import static com.keenwrite.Messages.get; | |
| 29 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 30 | import static java.lang.String.format; | |
| 31 | import static java.util.regex.Pattern.compile; | |
| 32 | import static java.util.regex.Pattern.quote; | |
| 33 | import static javafx.geometry.Pos.CENTER; | |
| 34 | import static javafx.geometry.Pos.TOP_CENTER; | |
| 35 | import static javafx.scene.control.SelectionMode.MULTIPLE; | |
| 36 | import static javafx.scene.control.TreeItem.childrenModificationEvent; | |
| 37 | import static javafx.scene.control.TreeItem.valueChangedEvent; | |
| 38 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 39 | ||
| 40 | /** | |
| 41 | * Provides the user interface that holds a {@link TreeView}, which | |
| 42 | * allows users to interact with key/value pairs loaded from the | |
| 43 | * document parser and adapted using a {@link TreeTransformer}. | |
| 44 | */ | |
| 45 | public final class DefinitionEditor extends BorderPane | |
| 46 | implements TextDefinition { | |
| 47 | private static final int GROUP_DELIMITED = 1; | |
| 48 | ||
| 49 | /** | |
| 50 | * Contains the root that is added to the view. | |
| 51 | */ | |
| 52 | private final DefinitionTreeItem<String> mTreeRoot = createRootTreeItem(); | |
| 53 | ||
| 54 | /** | |
| 55 | * Contains a view of the definitions. | |
| 56 | */ | |
| 57 | private final TreeView<String> mTreeView = new TreeView<>( mTreeRoot ); | |
| 58 | ||
| 59 | /** | |
| 60 | * Used to adapt the structured document into a {@link TreeView}. | |
| 61 | */ | |
| 62 | private final TreeTransformer mTreeTransformer; | |
| 63 | ||
| 64 | /** | |
| 65 | * Handlers for key press events. | |
| 66 | */ | |
| 67 | private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers | |
| 68 | = new HashSet<>(); | |
| 69 | ||
| 70 | /** | |
| 71 | * File being edited by this editor instance. | |
| 72 | */ | |
| 73 | private File mFile; | |
| 74 | ||
| 75 | /** | |
| 76 | * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if | |
| 77 | * either no encoding could be determined or this is a new (empty) file. | |
| 78 | */ | |
| 79 | private final Charset mEncoding; | |
| 80 | ||
| 81 | /** | |
| 82 | * Tracks whether the in-memory definitions have changed with respect to the | |
| 83 | * persisted definitions. | |
| 84 | */ | |
| 85 | private final BooleanProperty mModified = new SimpleBooleanProperty(); | |
| 86 | ||
| 87 | /** | |
| 88 | * This is provided for unit tests that are not backed by files. | |
| 89 | * | |
| 90 | * @param treeTransformer Responsible for transforming the definitions into | |
| 91 | * {@link TreeItem} instances. | |
| 92 | */ | |
| 93 | public DefinitionEditor( | |
| 94 | final TreeTransformer treeTransformer ) { | |
| 95 | this( DEFINITION_DEFAULT, treeTransformer ); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Constructs a definition pane with a given tree view root. | |
| 100 | * | |
| 101 | * @param file The file to | |
| 102 | */ | |
| 103 | public DefinitionEditor( | |
| 104 | final File file, | |
| 105 | final TreeTransformer treeTransformer ) { | |
| 106 | assert file != null; | |
| 107 | assert treeTransformer != null; | |
| 108 | ||
| 109 | mFile = file; | |
| 110 | mTreeTransformer = treeTransformer; | |
| 111 | ||
| 112 | mTreeView.setEditable( true ); | |
| 113 | mTreeView.setCellFactory( new TreeCellFactory() ); | |
| 114 | mTreeView.setContextMenu( createContextMenu() ); | |
| 115 | mTreeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); | |
| 116 | mTreeView.setShowRoot( false ); | |
| 117 | getSelectionModel().setSelectionMode( MULTIPLE ); | |
| 118 | ||
| 119 | final var buttonBar = new HBox(); | |
| 120 | buttonBar.getChildren().addAll( | |
| 121 | createButton( "create", e -> createDefinition() ), | |
| 122 | createButton( "rename", e -> renameDefinition() ), | |
| 123 | createButton( "delete", e -> deleteDefinitions() ) | |
| 124 | ); | |
| 125 | buttonBar.setAlignment( CENTER ); | |
| 126 | buttonBar.setSpacing( 10 ); | |
| 127 | ||
| 128 | setTop( buttonBar ); | |
| 129 | setCenter( mTreeView ); | |
| 130 | setAlignment( buttonBar, TOP_CENTER ); | |
| 131 | addTreeChangeHandler( event -> mModified.set( true ) ); | |
| 132 | mEncoding = open( mFile ); | |
| 133 | } | |
| 134 | ||
| 135 | @Override | |
| 136 | public void setText( final String document ) { | |
| 137 | final var foster = mTreeTransformer.transform( document ); | |
| 138 | final var biological = getTreeRoot(); | |
| 139 | ||
| 140 | for( final var child : foster.getChildren() ) { | |
| 141 | biological.getChildren().add( child ); | |
| 142 | } | |
| 143 | ||
| 144 | getTreeView().refresh(); | |
| 145 | } | |
| 146 | ||
| 147 | @Override | |
| 148 | public String getText() { | |
| 149 | final var result = new StringBuilder( 32768 ); | |
| 150 | ||
| 151 | try { | |
| 152 | final var root = getTreeView().getRoot(); | |
| 153 | final var problem = isTreeWellFormed(); | |
| 154 | ||
| 155 | problem.ifPresentOrElse( | |
| 156 | ( node ) -> clue( "yaml.error.tree.form", node ), | |
| 157 | () -> result.append( mTreeTransformer.transform( root ) ) | |
| 158 | ); | |
| 159 | } catch( final Exception ex ) { | |
| 160 | // Catch errors while checking for a well-formed tree (e.g., stack smash). | |
| 161 | // Also catch any transformation exceptions (e.g., Json processing). | |
| 162 | clue( ex ); | |
| 163 | } | |
| 164 | ||
| 165 | return result.toString(); | |
| 166 | } | |
| 167 | ||
| 168 | @Override | |
| 169 | public File getFile() { | |
| 170 | return mFile; | |
| 171 | } | |
| 172 | ||
| 173 | @Override | |
| 174 | public void rename( final File file ) { | |
| 175 | mFile = file; | |
| 176 | } | |
| 177 | ||
| 178 | @Override | |
| 179 | public Charset getEncoding() { | |
| 180 | return mEncoding; | |
| 181 | } | |
| 182 | ||
| 183 | @Override | |
| 184 | public Node getNode() { | |
| 185 | return this; | |
| 186 | } | |
| 187 | ||
| 188 | @Override | |
| 189 | public ReadOnlyBooleanProperty modifiedProperty() { | |
| 190 | return mModified; | |
| 191 | } | |
| 192 | ||
| 193 | @Override | |
| 194 | public void clearModifiedProperty() { | |
| 195 | mModified.setValue( false ); | |
| 196 | } | |
| 197 | ||
| 198 | private Button createButton( | |
| 199 | final String msgKey, final EventHandler<ActionEvent> eventHandler ) { | |
| 200 | final var keyPrefix = "App.action.definition." + msgKey; | |
| 201 | final var button = new Button( get( keyPrefix + ".text" ) ); | |
| 202 | final var icon = get( keyPrefix + ".icon" ); | |
| 203 | final var glyph = FontAwesomeIcon.valueOf( icon.toUpperCase() ); | |
| 204 | ||
| 205 | button.setOnAction( eventHandler ); | |
| 206 | button.setGraphic( | |
| 207 | FontAwesomeIconFactory.get().createIcon( glyph ) | |
| 208 | ); | |
| 209 | button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); | |
| 210 | ||
| 211 | return button; | |
| 212 | } | |
| 213 | ||
| 214 | @Override | |
| 215 | public Map<String, String> toMap() { | |
| 216 | return new TreeItemMapper().toMap( getTreeView().getRoot() ); | |
| 217 | } | |
| 218 | ||
| 219 | @Override | |
| 220 | public Map<String, String> interpolate( | |
| 221 | final Map<String, String> map, final Tokens tokens ) { | |
| 222 | ||
| 223 | // Non-greedy match of key names delimited by definition tokens. | |
| 224 | final var pattern = compile( | |
| 225 | format( "(%s.*?%s)", | |
| 226 | quote( tokens.getBegan() ), | |
| 227 | quote( tokens.getEnded() ) | |
| 228 | ) | |
| 229 | ); | |
| 230 | ||
| 231 | map.replaceAll( ( k, v ) -> resolve( map, v, pattern ) ); | |
| 232 | return map; | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Given a value with zero or more key references, this will resolve all | |
| 237 | * the values, recursively. If a key cannot be de-referenced, the value will | |
| 238 | * contain the key name. | |
| 239 | * | |
| 240 | * @param map Map to search for keys when resolving key references. | |
| 241 | * @param value Value containing zero or more key references. | |
| 242 | * @param pattern The regular expression pattern to match variable key names. | |
| 243 | * @return The given value with all embedded key references interpolated. | |
| 244 | */ | |
| 245 | private String resolve( | |
| 246 | final Map<String, String> map, String value, final Pattern pattern ) { | |
| 247 | final var matcher = pattern.matcher( value ); | |
| 248 | ||
| 249 | while( matcher.find() ) { | |
| 250 | final var keyName = matcher.group( GROUP_DELIMITED ); | |
| 251 | final var mapValue = map.get( keyName ); | |
| 252 | final var keyValue = mapValue == null | |
| 253 | ? keyName | |
| 254 | : resolve( map, mapValue, pattern ); | |
| 255 | ||
| 256 | value = value.replace( keyName, keyValue ); | |
| 257 | } | |
| 258 | ||
| 259 | return value; | |
| 260 | } | |
| 261 | ||
| 262 | ||
| 263 | /** | |
| 264 | * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} | |
| 265 | * is modified. The modifications include: item value changes, item additions, | |
| 266 | * and item removals. | |
| 267 | * <p> | |
| 268 | * Safe to call multiple times; if a handler is already registered, the | |
| 269 | * old handler is used. | |
| 270 | * </p> | |
| 271 | * | |
| 272 | * @param handler The handler to call whenever any {@link TreeItem} changes. | |
| 273 | */ | |
| 274 | public void addTreeChangeHandler( | |
| 275 | final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { | |
| 276 | final var root = getTreeView().getRoot(); | |
| 277 | root.addEventHandler( valueChangedEvent(), handler ); | |
| 278 | root.addEventHandler( childrenModificationEvent(), handler ); | |
| 279 | } | |
| 280 | ||
| 281 | public void addKeyEventHandler( | |
| 282 | final EventHandler<? super KeyEvent> handler ) { | |
| 283 | getKeyEventHandlers().add( handler ); | |
| 284 | } | |
| 285 | ||
| 286 | /** | |
| 287 | * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably | |
| 288 | * well-formed for export. A tree is considered well-formed if the following | |
| 289 | * conditions are met: | |
| 290 | * | |
| 291 | * <ul> | |
| 292 | * <li>The root node contains at least one child node having a leaf.</li> | |
| 293 | * <li>There are no leaf nodes with sibling leaf nodes.</li> | |
| 294 | * </ul> | |
| 295 | * | |
| 296 | * @return {@code null} if the document is well-formed, otherwise the | |
| 297 | * problematic child {@link TreeItem}. | |
| 298 | */ | |
| 299 | public Optional<TreeItem<String>> isTreeWellFormed() { | |
| 300 | final var root = getTreeView().getRoot(); | |
| 301 | ||
| 302 | for( final var child : root.getChildren() ) { | |
| 303 | final var problemChild = isWellFormed( child ); | |
| 304 | ||
| 305 | if( child.isLeaf() || problemChild != null ) { | |
| 306 | return Optional.ofNullable( problemChild ); | |
| 307 | } | |
| 308 | } | |
| 309 | ||
| 310 | return Optional.empty(); | |
| 311 | } | |
| 312 | ||
| 313 | /** | |
| 314 | * Determines whether the document is well-formed by ensuring that | |
| 315 | * child branches do not contain multiple leaves. | |
| 316 | * | |
| 317 | * @param item The sub-tree to check for well-formedness. | |
| 318 | * @return {@code null} when the tree is well-formed, otherwise the | |
| 319 | * problematic {@link TreeItem}. | |
| 320 | */ | |
| 321 | private TreeItem<String> isWellFormed( final TreeItem<String> item ) { | |
| 322 | int childLeafs = 0; | |
| 323 | int childBranches = 0; | |
| 324 | ||
| 325 | for( final var child : item.getChildren() ) { | |
| 326 | if( child.isLeaf() ) { | |
| 327 | childLeafs++; | |
| 328 | } | |
| 329 | else { | |
| 330 | childBranches++; | |
| 331 | } | |
| 332 | ||
| 333 | final var problemChild = isWellFormed( child ); | |
| 334 | ||
| 335 | if( problemChild != null ) { | |
| 336 | return problemChild; | |
| 337 | } | |
| 338 | } | |
| 339 | ||
| 340 | return ((childBranches > 0 && childLeafs == 0) || | |
| 341 | (childBranches == 0 && childLeafs <= 1)) ? null : item; | |
| 342 | } | |
| 343 | ||
| 344 | @Override | |
| 345 | public DefinitionTreeItem<String> findLeafExact( final String text ) { | |
| 346 | return getTreeRoot().findLeafExact( text ); | |
| 347 | } | |
| 348 | ||
| 349 | @Override | |
| 350 | public DefinitionTreeItem<String> findLeafContains( final String text ) { | |
| 351 | return getTreeRoot().findLeafContains( text ); | |
| 352 | } | |
| 353 | ||
| 354 | @Override | |
| 355 | public DefinitionTreeItem<String> findLeafContainsNoCase( | |
| 356 | final String text ) { | |
| 357 | return getTreeRoot().findLeafContainsNoCase( text ); | |
| 358 | } | |
| 359 | ||
| 360 | @Override | |
| 361 | public DefinitionTreeItem<String> findLeafStartsWith( final String text ) { | |
| 362 | return getTreeRoot().findLeafStartsWith( text ); | |
| 363 | } | |
| 364 | ||
| 365 | public void select( final TreeItem<String> item ) { | |
| 366 | getSelectionModel().clearSelection(); | |
| 367 | getSelectionModel().select( getTreeView().getRow( item ) ); | |
| 368 | } | |
| 369 | ||
| 370 | /** | |
| 371 | * Collapses the tree, recursively. | |
| 372 | */ | |
| 373 | public void collapse() { | |
| 374 | collapse( getTreeRoot().getChildren() ); | |
| 375 | } | |
| 376 | ||
| 377 | /** | |
| 378 | * Collapses the tree, recursively. | |
| 379 | * | |
| 380 | * @param <T> The type of tree item to expand (usually String). | |
| 381 | * @param nodes The nodes to collapse. | |
| 382 | */ | |
| 383 | private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { | |
| 384 | for( final var node : nodes ) { | |
| 385 | node.setExpanded( false ); | |
| 386 | collapse( node.getChildren() ); | |
| 387 | } | |
| 388 | } | |
| 389 | ||
| 390 | /** | |
| 391 | * @return {@code true} when the user is editing a {@link TreeItem}. | |
| 392 | */ | |
| 393 | private boolean isEditingTreeItem() { | |
| 394 | return getTreeView().editingItemProperty().getValue() != null; | |
| 395 | } | |
| 396 | ||
| 397 | /** | |
| 398 | * Changes to edit mode for the selected item. | |
| 399 | */ | |
| 400 | @Override | |
| 401 | public void renameDefinition() { | |
| 402 | getTreeView().edit( getSelectedItem() ); | |
| 403 | } | |
| 404 | ||
| 405 | /** | |
| 406 | * Removes all selected items from the {@link TreeView}. | |
| 407 | */ | |
| 408 | @Override | |
| 409 | public void deleteDefinitions() { | |
| 410 | for( final var item : getSelectedItems() ) { | |
| 411 | final var parent = item.getParent(); | |
| 412 | ||
| 413 | if( parent != null ) { | |
| 414 | parent.getChildren().remove( item ); | |
| 415 | } | |
| 416 | } | |
| 417 | } | |
| 418 | ||
| 419 | /** | |
| 420 | * Deletes the selected item. | |
| 421 | */ | |
| 422 | private void deleteSelectedItem() { | |
| 423 | final var c = getSelectedItem(); | |
| 424 | getSiblings( c ).remove( c ); | |
| 425 | } | |
| 426 | ||
| 427 | /** | |
| 428 | * Adds a new item under the selected item (or root if nothing is selected). | |
| 429 | * There are a few conditions to consider: when adding to the root, | |
| 430 | * when adding to a leaf, and when adding to a non-leaf. Items added to the | |
| 431 | * root must contain two items: a key and a value. | |
| 432 | */ | |
| 433 | @Override | |
| 434 | public void createDefinition() { | |
| 435 | final var value = createDefinitionTreeItem(); | |
| 436 | getSelectedItem().getChildren().add( value ); | |
| 437 | expand( value ); | |
| 438 | select( value ); | |
| 439 | } | |
| 440 | ||
| 441 | private ContextMenu createContextMenu() { | |
| 442 | final var menu = new ContextMenu(); | |
| 443 | final var items = menu.getItems(); | |
| 444 | ||
| 445 | addMenuItem( items, "App.action.definition.create.text" ) | |
| 446 | .setOnAction( e -> createDefinition() ); | |
| 447 | addMenuItem( items, "App.action.definition.rename.text" ) | |
| 448 | .setOnAction( e -> renameDefinition() ); | |
| 449 | addMenuItem( items, "App.action.definition.delete.text" ) | |
| 450 | .setOnAction( e -> deleteSelectedItem() ); | |
| 451 | ||
| 452 | return menu; | |
| 453 | } | |
| 454 | ||
| 455 | /** | |
| 456 | * Executes hot-keys for edits to the definition tree. | |
| 457 | * | |
| 458 | * @param event Contains the key code of the key that was pressed. | |
| 459 | */ | |
| 460 | private void keyEventFilter( final KeyEvent event ) { | |
| 461 | if( !isEditingTreeItem() ) { | |
| 462 | switch( event.getCode() ) { | |
| 463 | case ENTER -> { | |
| 464 | expand( getSelectedItem() ); | |
| 465 | event.consume(); | |
| 466 | } | |
| 467 | ||
| 468 | case DELETE -> deleteDefinitions(); | |
| 469 | case INSERT -> createDefinition(); | |
| 470 | ||
| 471 | case R -> { | |
| 472 | if( event.isControlDown() ) { | |
| 473 | renameDefinition(); | |
| 474 | } | |
| 475 | } | |
| 476 | } | |
| 477 | ||
| 478 | for( final var handler : getKeyEventHandlers() ) { | |
| 479 | handler.handle( event ); | |
| 480 | } | |
| 481 | } | |
| 482 | } | |
| 483 | ||
| 484 | /** | |
| 485 | * Adds a menu item to a list of menu items. | |
| 486 | * | |
| 487 | * @param items The list of menu items to append to. | |
| 488 | * @param labelKey The resource bundle key name for the menu item's label. | |
| 489 | * @return The menu item added to the list of menu items. | |
| 490 | */ | |
| 491 | private MenuItem addMenuItem( | |
| 492 | final List<MenuItem> items, final String labelKey ) { | |
| 493 | final MenuItem menuItem = createMenuItem( labelKey ); | |
| 494 | items.add( menuItem ); | |
| 495 | return menuItem; | |
| 496 | } | |
| 497 | ||
| 498 | private MenuItem createMenuItem( final String labelKey ) { | |
| 499 | return new MenuItem( get( labelKey ) ); | |
| 500 | } | |
| 501 | ||
| 502 | /** | |
| 503 | * Creates a new {@link TreeItem} that is intended to be the root-level item | |
| 504 | * added to the {@link TreeView}. This allows the root item to be | |
| 505 | * distinguished from the other items so that reference keys do not include | |
| 506 | * "Definition" as part of their name. | |
| 507 | * | |
| 508 | * @return A new {@link TreeItem}, never {@code null}. | |
| 509 | */ | |
| 510 | private RootTreeItem<String> createRootTreeItem() { | |
| 511 | return new RootTreeItem<>( get( "Pane.definition.node.root.title" ) ); | |
| 512 | } | |
| 513 | ||
| 514 | private DefinitionTreeItem<String> createDefinitionTreeItem() { | |
| 515 | return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) ); | |
| 516 | } | |
| 517 | ||
| 518 | @Override | |
| 519 | public void requestFocus() { | |
| 520 | super.requestFocus(); | |
| 521 | getTreeView().requestFocus(); | |
| 522 | } | |
| 523 | ||
| 524 | /** | |
| 525 | * Expands the node to the root, recursively. | |
| 526 | * | |
| 527 | * @param <T> The type of tree item to expand (usually String). | |
| 528 | * @param node The node to expand. | |
| 529 | */ | |
| 530 | @Override | |
| 531 | public <T> void expand( final TreeItem<T> node ) { | |
| 532 | if( node != null ) { | |
| 533 | expand( node.getParent() ); | |
| 534 | node.setExpanded( !node.isLeaf() ); | |
| 535 | } | |
| 536 | } | |
| 537 | ||
| 538 | /** | |
| 539 | * Answers whether there are any definitions in the tree. | |
| 540 | * | |
| 541 | * @return {@code true} when there are no definitions; {@code false} when | |
| 542 | * there's at least one definition. | |
| 543 | */ | |
| 544 | @Override | |
| 545 | public boolean isEmpty() { | |
| 546 | return getTreeRoot().isEmpty(); | |
| 547 | } | |
| 548 | ||
| 549 | /** | |
| 550 | * Returns the actively selected item in the tree. | |
| 551 | * | |
| 552 | * @return The selected item, or the tree root item if no item is selected. | |
| 553 | */ | |
| 554 | public TreeItem<String> getSelectedItem() { | |
| 555 | final var item = getSelectionModel().getSelectedItem(); | |
| 556 | return item == null ? getTreeRoot() : item; | |
| 557 | } | |
| 558 | ||
| 559 | /** | |
| 560 | * Returns the {@link TreeView} that contains the definition hierarchy. | |
| 561 | * | |
| 562 | * @return A non-null instance. | |
| 563 | */ | |
| 564 | private TreeView<String> getTreeView() { | |
| 565 | return mTreeView; | |
| 566 | } | |
| 567 | ||
| 568 | /** | |
| 569 | * Returns the root of the tree. | |
| 570 | * | |
| 571 | * @return The first node added to the definition tree. | |
| 572 | */ | |
| 573 | private DefinitionTreeItem<String> getTreeRoot() { | |
| 574 | return mTreeRoot; | |
| 575 | } | |
| 576 | ||
| 577 | private ObservableList<TreeItem<String>> getSiblings( | |
| 578 | final TreeItem<String> item ) { | |
| 579 | final var root = getTreeView().getRoot(); | |
| 580 | final var parent = (item == null || item == root) ? root : item.getParent(); | |
| 581 | ||
| 582 | return parent.getChildren(); | |
| 583 | } | |
| 584 | ||
| 585 | private MultipleSelectionModel<TreeItem<String>> getSelectionModel() { | |
| 586 | return getTreeView().getSelectionModel(); | |
| 587 | } | |
| 588 | ||
| 589 | /** | |
| 590 | * Returns a copy of all the selected items. | |
| 591 | * | |
| 592 | * @return A list, possibly empty, containing all selected items in the | |
| 593 | * {@link TreeView}. | |
| 594 | */ | |
| 595 | private List<TreeItem<String>> getSelectedItems() { | |
| 596 | return new ArrayList<>( getSelectionModel().getSelectedItems() ); | |
| 597 | } | |
| 598 | ||
| 599 | private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() { | |
| 600 | return mKeyEventHandlers; | |
| 601 | } | |
| 602 | } | |
| 1 | 603 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import com.panemu.tiwulfx.control.dock.DetachableTabPane; | |
| 5 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 6 | import javafx.scene.Scene; | |
| 7 | import javafx.scene.control.SingleSelectionModel; | |
| 8 | import javafx.scene.control.Tab; | |
| 9 | import javafx.scene.layout.VBox; | |
| 10 | ||
| 11 | import java.util.function.Consumer; | |
| 12 | ||
| 13 | import static javafx.scene.layout.Priority.ALWAYS; | |
| 14 | ||
| 15 | /** | |
| 16 | * Responsible for delegating tab selection events to a consumer. This is | |
| 17 | * required so that when a tab is detached from the main view into its own | |
| 18 | * window (scene), any tab changes in that scene can have an effect on the | |
| 19 | * main view. | |
| 20 | * | |
| 21 | * @author Amrullah Syadzili | |
| 22 | * @author White Magic Software, Ltd. | |
| 23 | */ | |
| 24 | public class DefinitionTabSceneFactory { | |
| 25 | ||
| 26 | private final Consumer<Tab> mTabSelectionConsumer; | |
| 27 | ||
| 28 | public DefinitionTabSceneFactory( final Consumer<Tab> tabSelectionConsumer ) { | |
| 29 | mTabSelectionConsumer = tabSelectionConsumer; | |
| 30 | } | |
| 31 | ||
| 32 | public Scene create( final DetachableTabPane tabPane ) { | |
| 33 | final var container = new TabContainer( tabPane ); | |
| 34 | final var scene = new Scene( container, 300, 900 ); | |
| 35 | ||
| 36 | scene.windowProperty().addListener( ( c, o, n ) -> { | |
| 37 | if( n != null ) { | |
| 38 | n.focusedProperty().addListener( ( __ ) -> { | |
| 39 | final var tab = container.getSelectedTab(); | |
| 40 | ||
| 41 | if( tab != null ) { | |
| 42 | mTabSelectionConsumer.accept( tab ); | |
| 43 | } | |
| 44 | } ); | |
| 45 | } | |
| 46 | } ); | |
| 47 | ||
| 48 | return scene; | |
| 49 | } | |
| 50 | ||
| 51 | private final class TabContainer extends VBox { | |
| 52 | private final DetachableTabPane mTabPane; | |
| 53 | ||
| 54 | public TabContainer( final DetachableTabPane tabPane ) { | |
| 55 | mTabPane = tabPane; | |
| 56 | setVgrow( tabPane, ALWAYS ); | |
| 57 | getChildren().add( tabPane ); | |
| 58 | ||
| 59 | selectedItemProperty().addListener( | |
| 60 | ( c, o, n ) -> { | |
| 61 | if( n != null ) { | |
| 62 | mTabSelectionConsumer.accept( n ); | |
| 63 | } | |
| 64 | } | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | private SingleSelectionModel<Tab> getSelectionModel() { | |
| 69 | return mTabPane.getSelectionModel(); | |
| 70 | } | |
| 71 | ||
| 72 | private ReadOnlyObjectProperty<Tab> selectedItemProperty() { | |
| 73 | return getSelectionModel().selectedItemProperty(); | |
| 74 | } | |
| 75 | ||
| 76 | private Tab getSelectedTab() { | |
| 77 | return getSelectionModel().getSelectedItem(); | |
| 78 | } | |
| 79 | } | |
| 80 | } | |
| 1 | 81 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import javafx.scene.control.TreeItem; | |
| 5 | ||
| 6 | import java.util.Stack; | |
| 7 | import java.util.function.BiFunction; | |
| 8 | ||
| 9 | import static java.text.Normalizer.Form.NFD; | |
| 10 | import static java.text.Normalizer.normalize; | |
| 11 | ||
| 12 | /** | |
| 13 | * Provides behaviour afforded to definition keys and corresponding value. | |
| 14 | * | |
| 15 | * @param <T> The type of {@link TreeItem} (usually string). | |
| 16 | */ | |
| 17 | public class DefinitionTreeItem<T> extends TreeItem<T> { | |
| 18 | ||
| 19 | /** | |
| 20 | * Constructs a new item with a default value. | |
| 21 | * | |
| 22 | * @param value Passed up to superclass. | |
| 23 | */ | |
| 24 | public DefinitionTreeItem( final T value ) { | |
| 25 | super( value ); | |
| 26 | } | |
| 27 | ||
| 28 | /** | |
| 29 | * Finds a leaf starting at the current node with text that matches the given | |
| 30 | * value. Search is performed case-sensitively. | |
| 31 | * | |
| 32 | * @param text The text to match against each leaf in the tree. | |
| 33 | * @return The leaf that has a value exactly matching the given text. | |
| 34 | */ | |
| 35 | public DefinitionTreeItem<T> findLeafExact( final String text ) { | |
| 36 | return findLeaf( text, DefinitionTreeItem::valueEquals ); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Finds a leaf starting at the current node with text that matches the given | |
| 41 | * value. Search is performed case-sensitively. | |
| 42 | * | |
| 43 | * @param text The text to match against each leaf in the tree. | |
| 44 | * @return The leaf that has a value that contains the given text. | |
| 45 | */ | |
| 46 | public DefinitionTreeItem<T> findLeafContains( final String text ) { | |
| 47 | return findLeaf( text, DefinitionTreeItem::valueContains ); | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Finds a leaf starting at the current node with text that matches the given | |
| 52 | * value. Search is performed case-insensitively. | |
| 53 | * | |
| 54 | * @param text The text to match against each leaf in the tree. | |
| 55 | * @return The leaf that has a value that contains the given text. | |
| 56 | */ | |
| 57 | public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) { | |
| 58 | return findLeaf( text, DefinitionTreeItem::valueContainsNoCase ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Finds a leaf starting at the current node with text that matches the given | |
| 63 | * value. Search is performed case-sensitively. | |
| 64 | * | |
| 65 | * @param text The text to match against each leaf in the tree. | |
| 66 | * @return The leaf that has a value that starts with the given text. | |
| 67 | */ | |
| 68 | public DefinitionTreeItem<T> findLeafStartsWith( final String text ) { | |
| 69 | return findLeaf( text, DefinitionTreeItem::valueStartsWith ); | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Finds a leaf starting at the current node with text that matches the given | |
| 74 | * value. | |
| 75 | * | |
| 76 | * @param text The text to match against each leaf in the tree. | |
| 77 | * @param findMode What algorithm is used to match the given text. | |
| 78 | * @return The leaf that has a value starting with the given text, or {@code | |
| 79 | * null} if there was no match found. | |
| 80 | */ | |
| 81 | public DefinitionTreeItem<T> findLeaf( | |
| 82 | final String text, | |
| 83 | final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) { | |
| 84 | final var stack = new Stack<DefinitionTreeItem<T>>(); | |
| 85 | stack.push( this ); | |
| 86 | ||
| 87 | // Don't hunt for blank (empty) keys. | |
| 88 | boolean found = text.isBlank(); | |
| 89 | ||
| 90 | while( !found && !stack.isEmpty() ) { | |
| 91 | final var node = stack.pop(); | |
| 92 | ||
| 93 | for( final var child : node.getChildren() ) { | |
| 94 | final var result = (DefinitionTreeItem<T>) child; | |
| 95 | ||
| 96 | if( result.isLeaf() ) { | |
| 97 | if( found = findMode.apply( result, text ) ) { | |
| 98 | return result; | |
| 99 | } | |
| 100 | } | |
| 101 | else { | |
| 102 | stack.push( result ); | |
| 103 | } | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | return null; | |
| 108 | } | |
| 109 | ||
| 110 | /** | |
| 111 | * Returns the value of the string without diacritic marks. | |
| 112 | * | |
| 113 | * @return A non-null, possibly empty string. | |
| 114 | */ | |
| 115 | private String getDiacriticlessValue() { | |
| 116 | return normalize( getValue().toString(), NFD ) | |
| 117 | .replaceAll( "\\p{M}", "" ); | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Returns true if this node is a leaf and its value equals the given text. | |
| 122 | * | |
| 123 | * @param s The text to compare against the node value. | |
| 124 | * @return true Node is a leaf and its value equals the given value. | |
| 125 | */ | |
| 126 | private boolean valueEquals( final String s ) { | |
| 127 | return isLeaf() && getValue().equals( s ); | |
| 128 | } | |
| 129 | ||
| 130 | /** | |
| 131 | * Returns true if this node is a leaf and its value contains the given text. | |
| 132 | * | |
| 133 | * @param s The text to compare against the node value. | |
| 134 | * @return true Node is a leaf and its value contains the given value. | |
| 135 | */ | |
| 136 | private boolean valueContains( final String s ) { | |
| 137 | return isLeaf() && getDiacriticlessValue().contains( s ); | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Returns true if this node is a leaf and its value contains the given text. | |
| 142 | * | |
| 143 | * @param s The text to compare against the node value. | |
| 144 | * @return true Node is a leaf and its value contains the given value. | |
| 145 | */ | |
| 146 | private boolean valueContainsNoCase( final String s ) { | |
| 147 | return isLeaf() && | |
| 148 | getDiacriticlessValue().toLowerCase().contains( s.toLowerCase() ); | |
| 149 | } | |
| 150 | ||
| 151 | /** | |
| 152 | * Returns true if this node is a leaf and its value starts with the given | |
| 153 | * text. | |
| 154 | * | |
| 155 | * @param s The text to compare against the node value. | |
| 156 | * @return true Node is a leaf and its value starts with the given value. | |
| 157 | */ | |
| 158 | private boolean valueStartsWith( final String s ) { | |
| 159 | return isLeaf() && getDiacriticlessValue().startsWith( s ); | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Returns the path for this node, with nodes made distinct using the | |
| 164 | * separator character. This uses two loops: one for pushing nodes onto a | |
| 165 | * stack and one for popping them off to create the path in desired order. | |
| 166 | * | |
| 167 | * @return A non-null string, possibly empty. | |
| 168 | */ | |
| 169 | public String toPath() { | |
| 170 | return new TreeItemMapper().toPath( getParent() ); | |
| 171 | } | |
| 172 | ||
| 173 | /** | |
| 174 | * Answers whether there are any definitions in this tree. | |
| 175 | * | |
| 176 | * @return {@code true} when there are no definitions in the tree; {@code | |
| 177 | * false} when there is at least one definition present. | |
| 178 | */ | |
| 179 | public boolean isEmpty() { | |
| 180 | return getChildren().isEmpty(); | |
| 181 | } | |
| 182 | } | |
| 1 | 183 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import javafx.scene.Node; | |
| 5 | import javafx.scene.control.TextField; | |
| 6 | import javafx.scene.control.cell.TextFieldTreeCell; | |
| 7 | import javafx.util.StringConverter; | |
| 8 | ||
| 9 | /** | |
| 10 | * Responsible for fixing a focus lost bug in the JavaFX implementation. | |
| 11 | * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details. | |
| 12 | * This implementation borrows from the official documentation on creating | |
| 13 | * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm | |
| 14 | */ | |
| 15 | public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> { | |
| 16 | private TextField mTextField; | |
| 17 | ||
| 18 | public FocusAwareTextFieldTreeCell( | |
| 19 | final StringConverter<String> converter ) { | |
| 20 | super( converter ); | |
| 21 | } | |
| 22 | ||
| 23 | @Override | |
| 24 | public void startEdit() { | |
| 25 | super.startEdit(); | |
| 26 | var textField = mTextField; | |
| 27 | ||
| 28 | if( textField == null ) { | |
| 29 | textField = createTextField(); | |
| 30 | } | |
| 31 | else { | |
| 32 | textField.setText( getItem() ); | |
| 33 | } | |
| 34 | ||
| 35 | setText( null ); | |
| 36 | setGraphic( textField ); | |
| 37 | textField.selectAll(); | |
| 38 | textField.requestFocus(); | |
| 39 | ||
| 40 | // When the focus is lost, commit the edit then close the input field. | |
| 41 | // This fixes the unexpected behaviour when user clicks away. | |
| 42 | textField.focusedProperty().addListener( ( l, o, n ) -> { | |
| 43 | if( !n ) { | |
| 44 | commitEdit( mTextField.getText() ); | |
| 45 | } | |
| 46 | } ); | |
| 47 | ||
| 48 | mTextField = textField; | |
| 49 | } | |
| 50 | ||
| 51 | @Override | |
| 52 | public void cancelEdit() { | |
| 53 | super.cancelEdit(); | |
| 54 | setText( getItem() ); | |
| 55 | setGraphic( getTreeItem().getGraphic() ); | |
| 56 | } | |
| 57 | ||
| 58 | @Override | |
| 59 | public void updateItem( String item, boolean empty ) { | |
| 60 | super.updateItem( item, empty ); | |
| 61 | ||
| 62 | String text = null; | |
| 63 | Node graphic = null; | |
| 64 | ||
| 65 | if( !empty ) { | |
| 66 | if( isEditing() ) { | |
| 67 | final var textField = mTextField; | |
| 68 | ||
| 69 | if( textField != null ) { | |
| 70 | textField.setText( getString() ); | |
| 71 | } | |
| 72 | ||
| 73 | graphic = textField; | |
| 74 | } | |
| 75 | else { | |
| 76 | text = getString(); | |
| 77 | graphic = getTreeItem().getGraphic(); | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | setText( text ); | |
| 82 | setGraphic( graphic ); | |
| 83 | } | |
| 84 | ||
| 85 | private TextField createTextField() { | |
| 86 | final var textField = new TextField( getString() ); | |
| 87 | ||
| 88 | textField.setOnKeyReleased( t -> { | |
| 89 | switch( t.getCode() ) { | |
| 90 | case ENTER -> commitEdit( textField.getText() ); | |
| 91 | case ESCAPE -> cancelEdit(); | |
| 92 | } | |
| 93 | } ); | |
| 94 | ||
| 95 | return textField; | |
| 96 | } | |
| 97 | ||
| 98 | private String getString() { | |
| 99 | return getConverter().toString( getItem() ); | |
| 100 | } | |
| 101 | } | |
| 1 | 102 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import javafx.scene.control.TreeItem; | |
| 5 | import javafx.scene.control.TreeView; | |
| 6 | ||
| 7 | /** | |
| 8 | * Marker interface for top-most {@link TreeItem}. This class allows the | |
| 9 | * {@link TreeItemMapper} to ignore the topmost definition. Such contortions | |
| 10 | * are necessary because {@link TreeView} requires a root item that isn't part | |
| 11 | * of the user's definition file. | |
| 12 | * <p> | |
| 13 | * Another approach would be to associate object pairs per {@link TreeItem}, | |
| 14 | * but that would be a waste of memory since the only "exception" case is | |
| 15 | * the root {@link TreeItem}. | |
| 16 | * </p> | |
| 17 | * | |
| 18 | * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}. | |
| 19 | */ | |
| 20 | public final class RootTreeItem<T> extends DefinitionTreeItem<T> { | |
| 21 | /** | |
| 22 | * Default constructor, calls the superclass, no other behaviour. | |
| 23 | * | |
| 24 | * @param value The {@link TreeItem} node name to construct the superclass. | |
| 25 | * @see TreeItemMapper#toMap(TreeItem) for details on how this | |
| 26 | * class is used. | |
| 27 | */ | |
| 28 | public RootTreeItem( final T value ) { | |
| 29 | super( value ); | |
| 30 | } | |
| 31 | } | |
| 1 | 32 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import javafx.collections.ObservableList; | |
| 5 | import javafx.scene.control.TreeCell; | |
| 6 | import javafx.scene.control.TreeItem; | |
| 7 | import javafx.scene.control.TreeView; | |
| 8 | import javafx.scene.input.ClipboardContent; | |
| 9 | import javafx.scene.input.DataFormat; | |
| 10 | import javafx.scene.input.DragEvent; | |
| 11 | import javafx.scene.input.MouseEvent; | |
| 12 | import javafx.util.Callback; | |
| 13 | import javafx.util.StringConverter; | |
| 14 | ||
| 15 | import java.util.Objects; | |
| 16 | ||
| 17 | import static com.keenwrite.io.MediaType.APP_JAVA_OBJECT; | |
| 18 | import static javafx.scene.input.TransferMode.MOVE; | |
| 19 | ||
| 20 | /** | |
| 21 | * Responsible for producing {@link TreeCell} instances that can be edited | |
| 22 | * and respond to drag and drop functionality. | |
| 23 | */ | |
| 24 | public class TreeCellFactory | |
| 25 | implements Callback<TreeView<String>, TreeCell<String>> { | |
| 26 | private static final String STYLE_CLASS_DROP_TARGET = "drop-target"; | |
| 27 | private static final DataFormat JAVA_FORMAT = | |
| 28 | new DataFormat( APP_JAVA_OBJECT.toString() ); | |
| 29 | ||
| 30 | private TreeItem<String> mDraggedTreeItem; | |
| 31 | private TreeCell<String> mTargetCell; | |
| 32 | ||
| 33 | /** | |
| 34 | * Constructs a new {@link TreeCell} manufacturing facility called when | |
| 35 | * a new {@link TreeItem} is added to one of the editor's {@link TreeView}s. | |
| 36 | */ | |
| 37 | public TreeCellFactory() { | |
| 38 | } | |
| 39 | ||
| 40 | @Override | |
| 41 | public TreeCell<String> call( final TreeView<String> treeView ) { | |
| 42 | final var cell = createTreeCell(); | |
| 43 | ||
| 44 | cell.setOnDragDetected( event -> dragDetected( event, cell ) ); | |
| 45 | cell.setOnDragOver( event -> dragOver( event, cell ) ); | |
| 46 | cell.setOnDragDropped( event -> dragDropped( event, cell, treeView ) ); | |
| 47 | cell.setOnDragDone( event -> dragClear() ); | |
| 48 | ||
| 49 | return cell; | |
| 50 | } | |
| 51 | ||
| 52 | private TreeCell<String> createTreeCell() { | |
| 53 | return new FocusAwareTextFieldTreeCell( createStringConverter() ) { | |
| 54 | @Override | |
| 55 | public void commitEdit( final String newValue ) { | |
| 56 | super.commitEdit( newValue ); | |
| 57 | //mEditor.select( getTreeItem() ); | |
| 58 | requestFocus(); | |
| 59 | } | |
| 60 | }; | |
| 61 | } | |
| 62 | ||
| 63 | private StringConverter<String> createStringConverter() { | |
| 64 | return new StringConverter<>() { | |
| 65 | @Override | |
| 66 | public String toString( final String object ) { | |
| 67 | return sanitize( object ); | |
| 68 | } | |
| 69 | ||
| 70 | @Override | |
| 71 | public String fromString( final String string ) { | |
| 72 | return sanitize( string ); | |
| 73 | } | |
| 74 | ||
| 75 | private String sanitize( final String string ) { | |
| 76 | return string == null ? "" : string; | |
| 77 | } | |
| 78 | }; | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Drag start. | |
| 83 | * | |
| 84 | * @param event The drag start {@link MouseEvent}. | |
| 85 | * @param treeCell The cell being dragged. | |
| 86 | */ | |
| 87 | private void dragDetected( | |
| 88 | final MouseEvent event, final TreeCell<String> treeCell ) { | |
| 89 | final var sourceItem = treeCell.getTreeItem(); | |
| 90 | ||
| 91 | // Prevent dragging the root item. | |
| 92 | if( sourceItem != null && sourceItem.getParent() != null ) { | |
| 93 | final var dragboard = treeCell.startDragAndDrop( MOVE ); | |
| 94 | final var clipboard = new ClipboardContent(); | |
| 95 | clipboard.put( JAVA_FORMAT, sourceItem.getValue() ); | |
| 96 | dragboard.setContent( clipboard ); | |
| 97 | dragboard.setDragView( treeCell.snapshot( null, null ) ); | |
| 98 | event.consume(); | |
| 99 | ||
| 100 | mDraggedTreeItem = sourceItem; | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Drag over another {@link TreeCell} instance. | |
| 106 | * | |
| 107 | * @param event The drag over {@link DragEvent}. | |
| 108 | * @param treeCell The cell dragged over. | |
| 109 | * @throws IllegalStateException Drag transfer "move" mode denied. | |
| 110 | */ | |
| 111 | private void dragOver( | |
| 112 | final DragEvent event, final TreeCell<String> treeCell ) { | |
| 113 | if( event.getDragboard().hasContent( JAVA_FORMAT ) ) { | |
| 114 | final var thisItem = treeCell.getTreeItem(); | |
| 115 | ||
| 116 | if( mDraggedTreeItem == null || | |
| 117 | thisItem == null || | |
| 118 | thisItem == mDraggedTreeItem ) { | |
| 119 | return; | |
| 120 | } | |
| 121 | ||
| 122 | // Ignore dragging over the root item. | |
| 123 | if( mDraggedTreeItem.getParent() == null ) { | |
| 124 | dragClear(); | |
| 125 | return; | |
| 126 | } | |
| 127 | ||
| 128 | event.acceptTransferModes( MOVE ); | |
| 129 | ||
| 130 | if( !Objects.equals( mTargetCell, treeCell ) ) { | |
| 131 | dragClear(); | |
| 132 | mTargetCell = treeCell; | |
| 133 | mTargetCell.getStyleClass().add( STYLE_CLASS_DROP_TARGET ); | |
| 134 | } | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Dragged item is dropped | |
| 140 | * | |
| 141 | * @param event The drag dropped {@link DragEvent}. | |
| 142 | * @param treeCell The cell dropped onto. | |
| 143 | */ | |
| 144 | private void dragDropped( final DragEvent event, | |
| 145 | final TreeCell<String> treeCell, | |
| 146 | final TreeView<String> treeView ) { | |
| 147 | if( !event.getDragboard().hasContent( JAVA_FORMAT ) ) { | |
| 148 | return; | |
| 149 | } | |
| 150 | ||
| 151 | final var sourceItem = mDraggedTreeItem; | |
| 152 | final var sourceItemParent = mDraggedTreeItem.getParent(); | |
| 153 | final var targetItem = treeCell.getTreeItem(); | |
| 154 | final var targetItemParent = targetItem.getParent(); | |
| 155 | ||
| 156 | sourceItemParent.getChildren().remove( sourceItem ); | |
| 157 | ||
| 158 | final ObservableList<TreeItem<String>> children; | |
| 159 | final int index; | |
| 160 | ||
| 161 | // Dropping onto a parent node makes the source item the first child. | |
| 162 | if( Objects.equals( sourceItemParent, targetItem ) ) { | |
| 163 | children = targetItem.getChildren(); | |
| 164 | index = 0; | |
| 165 | } | |
| 166 | else if( targetItemParent != null) { | |
| 167 | children = targetItemParent.getChildren(); | |
| 168 | index = children.indexOf( targetItem ) + 1; | |
| 169 | } | |
| 170 | else { | |
| 171 | children = sourceItemParent.getChildren(); | |
| 172 | index = 0; | |
| 173 | } | |
| 174 | ||
| 175 | children.add( index, sourceItem ); | |
| 176 | ||
| 177 | treeView.getSelectionModel().clearSelection(); | |
| 178 | treeView.getSelectionModel().select( sourceItem ); | |
| 179 | ||
| 180 | // TODO: Notify a listener of the old and new tree item position. | |
| 181 | ||
| 182 | event.setDropCompleted( true ); | |
| 183 | } | |
| 184 | ||
| 185 | private void dragClear() { | |
| 186 | final var targetCell = mTargetCell; | |
| 187 | ||
| 188 | if( targetCell != null ) { | |
| 189 | targetCell.getStyleClass().remove( STYLE_CLASS_DROP_TARGET ); | |
| 190 | } | |
| 191 | } | |
| 192 | } | |
| 1 | 193 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.keenwrite.preview.HtmlPreview; | |
| 6 | import javafx.scene.control.TreeItem; | |
| 7 | import javafx.scene.control.TreeView; | |
| 8 | ||
| 9 | import java.util.HashMap; | |
| 10 | import java.util.Iterator; | |
| 11 | import java.util.Map; | |
| 12 | import java.util.Stack; | |
| 13 | ||
| 14 | import static com.keenwrite.Constants.MAP_SIZE_DEFAULT; | |
| 15 | ||
| 16 | /** | |
| 17 | * Given a {@link TreeItem}, this will generate a flat map with all the | |
| 18 | * values in the tree recursively interpolated. The application integrates | |
| 19 | * definition files as follows: | |
| 20 | * <ol> | |
| 21 | * <li>Load YAML file into {@link JsonNode} hierarchy.</li> | |
| 22 | * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li> | |
| 23 | * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li> | |
| 24 | * <li>Substitute flat map variables into document as required.</li> | |
| 25 | * </ol> | |
| 26 | * | |
| 27 | * <p> | |
| 28 | * This class is responsible for producing the interpolated flat map. This | |
| 29 | * allows dynamic edits of the {@link TreeView} to be displayed in the | |
| 30 | * {@link HtmlPreview} without having to reload the definition file. | |
| 31 | * Reloading the definition file would work, but has a number of drawbacks. | |
| 32 | * </p> | |
| 33 | */ | |
| 34 | public class TreeItemMapper { | |
| 35 | /** | |
| 36 | * Separates definition keys (e.g., the dots in {@code $root.node.var$}). | |
| 37 | */ | |
| 38 | public static final String SEPARATOR = "."; | |
| 39 | ||
| 40 | /** | |
| 41 | * Default buffer length for keys ({@link StringBuilder} has 16 character | |
| 42 | * buffer) that should be large enough for most keys to avoid reallocating | |
| 43 | * memory to increase the {@link StringBuilder}'s buffer. | |
| 44 | */ | |
| 45 | public static final int DEFAULT_KEY_LENGTH = 64; | |
| 46 | ||
| 47 | /** | |
| 48 | * In-order traversal of a {@link TreeItem} hierarchy, exposing each item | |
| 49 | * as a consecutive list. | |
| 50 | */ | |
| 51 | private static final class TreeIterator | |
| 52 | implements Iterator<TreeItem<String>> { | |
| 53 | private final Stack<TreeItem<String>> mStack = new Stack<>(); | |
| 54 | ||
| 55 | public TreeIterator( final TreeItem<String> root ) { | |
| 56 | if( root != null ) { | |
| 57 | mStack.push( root ); | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public boolean hasNext() { | |
| 63 | return !mStack.isEmpty(); | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | public TreeItem<String> next() { | |
| 68 | final TreeItem<String> next = mStack.pop(); | |
| 69 | next.getChildren().forEach( mStack::push ); | |
| 70 | ||
| 71 | return next; | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | public TreeItemMapper() { | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Iterate over a given root node (at any level of the tree) and process each | |
| 80 | * leaf node into a flat map. Values must be interpolated separately. | |
| 81 | */ | |
| 82 | public Map<String, String> toMap( final TreeItem<String> root ) { | |
| 83 | final var map = new HashMap<String, String>( MAP_SIZE_DEFAULT ); | |
| 84 | final var iterator = new TreeIterator( root ); | |
| 85 | ||
| 86 | iterator.forEachRemaining( item -> { | |
| 87 | if( item.isLeaf() ) { | |
| 88 | map.put( toPath( item.getParent() ), item.getValue() ); | |
| 89 | } | |
| 90 | } ); | |
| 91 | ||
| 92 | return map; | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * For a given node, this will ascend the tree to generate a key name | |
| 97 | * that is associated with the leaf node's value. | |
| 98 | * | |
| 99 | * @param node Ascendants represent the key to this node's value. | |
| 100 | * @param <T> Data type that the {@link TreeItem} contains. | |
| 101 | * @return The string representation of the node's unique key. | |
| 102 | */ | |
| 103 | public <T> String toPath( TreeItem<T> node ) { | |
| 104 | assert node != null; | |
| 105 | ||
| 106 | final var key = new StringBuilder( DEFAULT_KEY_LENGTH ); | |
| 107 | final var stack = new Stack<TreeItem<T>>(); | |
| 108 | ||
| 109 | while( node != null && !(node instanceof RootTreeItem) ) { | |
| 110 | stack.push( node ); | |
| 111 | node = node.getParent(); | |
| 112 | } | |
| 113 | ||
| 114 | // Gets set at end of first iteration (to avoid an if condition). | |
| 115 | var separator = ""; | |
| 116 | ||
| 117 | while( !stack.empty() ) { | |
| 118 | final T subkey = stack.pop().getValue(); | |
| 119 | key.append( separator ); | |
| 120 | key.append( subkey ); | |
| 121 | separator = SEPARATOR; | |
| 122 | } | |
| 123 | ||
| 124 | return key.toString(); | |
| 125 | } | |
| 126 | } | |
| 1 | 127 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition; | |
| 3 | ||
| 4 | import javafx.scene.control.TreeItem; | |
| 5 | ||
| 6 | import java.util.function.Consumer; | |
| 7 | import java.util.function.Function; | |
| 8 | ||
| 9 | /** | |
| 10 | * Responsible for converting an object hierarchy into a {@link TreeItem} | |
| 11 | * hierarchy. | |
| 12 | */ | |
| 13 | public interface TreeTransformer { | |
| 14 | /** | |
| 15 | * Adapts the document produced by the given parser into a {@link TreeItem} | |
| 16 | * object that can be presented to the user within a GUI. The root of the | |
| 17 | * tree must be merged by the view layer. | |
| 18 | * | |
| 19 | * @param document The document to transform into a viewable hierarchy. | |
| 20 | */ | |
| 21 | TreeItem<String> transform( String document ); | |
| 22 | ||
| 23 | /** | |
| 24 | * Exports the given root node to the given path. | |
| 25 | * | |
| 26 | * @param root The root node to export. | |
| 27 | */ | |
| 28 | String transform( TreeItem<String> root ); | |
| 29 | } | |
| 1 | 30 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. | |
| 2 | * | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /** | |
| 29 | * This package contains classes that pertain to hierarchical, structured | |
| 30 | * data formats, which can be used as interpolated variables. | |
| 31 | */ | |
| 32 | package com.keenwrite.editors.definition; | |
| 1 | 33 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition.yaml; | |
| 3 | ||
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 7 | ||
| 8 | import java.util.function.Function; | |
| 9 | ||
| 10 | /** | |
| 11 | * Responsible for reading a YAML document into an object hierarchy. | |
| 12 | */ | |
| 13 | class YamlParser implements Function<String, JsonNode> { | |
| 14 | ||
| 15 | /** | |
| 16 | * Creates a new instance that can parse the contents of a YAML | |
| 17 | * document. | |
| 18 | */ | |
| 19 | YamlParser() { | |
| 20 | } | |
| 21 | ||
| 22 | @Override | |
| 23 | public JsonNode apply( final String yaml ) { | |
| 24 | try { | |
| 25 | return new ObjectMapper( new YAMLFactory() ).readTree( yaml ); | |
| 26 | } catch( final Exception ex ) { | |
| 27 | // Ensure that a document root node exists. | |
| 28 | return new ObjectMapper().createObjectNode(); | |
| 29 | } | |
| 30 | } | |
| 31 | } | |
| 1 | 32 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition.yaml; | |
| 3 | ||
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.node.ObjectNode; | |
| 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; | |
| 7 | import com.keenwrite.editors.definition.DefinitionTreeItem; | |
| 8 | import com.keenwrite.editors.definition.TreeTransformer; | |
| 9 | import javafx.scene.control.TreeItem; | |
| 10 | import javafx.scene.control.TreeView; | |
| 11 | ||
| 12 | import java.util.Map.Entry; | |
| 13 | ||
| 14 | /** | |
| 15 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user | |
| 16 | * interface and vice-versa. | |
| 17 | */ | |
| 18 | public final class YamlTreeTransformer implements TreeTransformer { | |
| 19 | ||
| 20 | /** | |
| 21 | * Constructs a new instance that will use the given path to read the object | |
| 22 | * hierarchy from a data source. | |
| 23 | */ | |
| 24 | public YamlTreeTransformer() { | |
| 25 | } | |
| 26 | ||
| 27 | @Override | |
| 28 | public String transform( final TreeItem<String> treeItem ) { | |
| 29 | try { | |
| 30 | final YAMLMapper mapper = new YAMLMapper(); | |
| 31 | final ObjectNode root = mapper.createObjectNode(); | |
| 32 | ||
| 33 | // Iterate over the root item's children. The root item is used by the | |
| 34 | // application to ensure definitions can always be added to a tree, as | |
| 35 | // such it is not meant to be exported, only its children. | |
| 36 | for( final TreeItem<String> child : treeItem.getChildren() ) { | |
| 37 | transform( child, root ); | |
| 38 | } | |
| 39 | ||
| 40 | return mapper.writeValueAsString( root ); | |
| 41 | } catch( final Exception ex ) { | |
| 42 | throw new RuntimeException( ex ); | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * Recursive method to generate an object hierarchy that represents the | |
| 48 | * given {@link TreeItem} hierarchy. | |
| 49 | * | |
| 50 | * @param item The {@link TreeItem} to reproduce as an object hierarchy. | |
| 51 | * @param node The {@link ObjectNode} to update to reflect the | |
| 52 | * {@link TreeItem} hierarchy. | |
| 53 | */ | |
| 54 | private void transform( final TreeItem<String> item, ObjectNode node ) { | |
| 55 | final var children = item.getChildren(); | |
| 56 | ||
| 57 | // If the current item has more than one non-leaf child, it's an | |
| 58 | // object node and must become a new nested object. | |
| 59 | if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) { | |
| 60 | node = node.putObject( item.getValue() ); | |
| 61 | } | |
| 62 | ||
| 63 | for( final var child : children ) { | |
| 64 | if( child.isLeaf() ) { | |
| 65 | node.put( item.getValue(), child.getValue() ); | |
| 66 | } | |
| 67 | else { | |
| 68 | transform( child, node ); | |
| 69 | } | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 75 | * keys. | |
| 76 | * | |
| 77 | * @param document The YAML document to convert to a hierarchy of | |
| 78 | * {@link TreeItem} instances. | |
| 79 | * @throws StackOverflowError If infinite recursion is encountered. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public TreeItem<String> transform( final String document ) { | |
| 83 | final var parser = new YamlParser(); | |
| 84 | final var jsonNode = parser.apply( document ); | |
| 85 | final var rootItem = createTreeItem( "root" ); | |
| 86 | ||
| 87 | transform( jsonNode, rootItem ); | |
| 88 | ||
| 89 | return rootItem; | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 94 | * leaf node. | |
| 95 | * | |
| 96 | * @param node A JSON node (YAML node) to adapt. | |
| 97 | * @param item The tree item to use as the root when processing the node. | |
| 98 | * @throws StackOverflowError If infinite recursion is encountered. | |
| 99 | */ | |
| 100 | private void transform( final JsonNode node, final TreeItem<String> item ) { | |
| 101 | node.fields().forEachRemaining( leaf -> transform( leaf, item ) ); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 106 | * | |
| 107 | * @param node The node to adapt. | |
| 108 | * @param item The item to adapt using the node's key. | |
| 109 | * @throws StackOverflowError If infinite recursion is encountered. | |
| 110 | */ | |
| 111 | private void transform( | |
| 112 | final Entry<String, JsonNode> node, final TreeItem<String> item ) { | |
| 113 | final var leafNode = node.getValue(); | |
| 114 | final var key = node.getKey(); | |
| 115 | final var leaf = createTreeItem( key ); | |
| 116 | ||
| 117 | if( leafNode.isValueNode() ) { | |
| 118 | leaf.getChildren().add( createTreeItem( node.getValue().asText() ) ); | |
| 119 | } | |
| 120 | ||
| 121 | item.getChildren().add( leaf ); | |
| 122 | ||
| 123 | if( leafNode.isObject() ) { | |
| 124 | transform( leafNode, leaf ); | |
| 125 | } | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Creates a new {@link TreeItem} that can be added to the {@link TreeView}. | |
| 130 | * | |
| 131 | * @param value The node's value. | |
| 132 | * @return A new {@link TreeItem}, never {@code null}. | |
| 133 | */ | |
| 134 | private TreeItem<String> createTreeItem( final String value ) { | |
| 135 | return new DefinitionTreeItem<>( value ); | |
| 136 | } | |
| 137 | } | |
| 1 | 138 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. | |
| 2 | * | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /** | |
| 29 | * This package contains classes that can parse YAML documents into a GUI | |
| 30 | * representation. | |
| 31 | */ | |
| 32 | package com.keenwrite.editors.definition.yaml; | |
| 1 | 33 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.markdown; | |
| 3 | ||
| 4 | import com.keenwrite.Constants; | |
| 5 | import com.keenwrite.editors.TextEditor; | |
| 6 | import com.keenwrite.preferences.LocaleProperty; | |
| 7 | import com.keenwrite.preferences.Workspace; | |
| 8 | import com.keenwrite.processors.markdown.Caret; | |
| 9 | import com.keenwrite.spelling.impl.TextEditorSpeller; | |
| 10 | import javafx.beans.binding.Bindings; | |
| 11 | import javafx.beans.property.BooleanProperty; | |
| 12 | import javafx.beans.property.DoubleProperty; | |
| 13 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 14 | import javafx.beans.property.SimpleBooleanProperty; | |
| 15 | import javafx.beans.value.ChangeListener; | |
| 16 | import javafx.event.Event; | |
| 17 | import javafx.scene.Node; | |
| 18 | import javafx.scene.control.IndexRange; | |
| 19 | import javafx.scene.input.KeyCode; | |
| 20 | import javafx.scene.input.KeyEvent; | |
| 21 | import javafx.scene.layout.BorderPane; | |
| 22 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 23 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 24 | import org.fxmisc.richtext.model.StyleSpans; | |
| 25 | import org.fxmisc.undo.UndoManager; | |
| 26 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 27 | import org.fxmisc.wellbehaved.event.Nodes; | |
| 28 | ||
| 29 | import java.io.File; | |
| 30 | import java.nio.charset.Charset; | |
| 31 | import java.text.BreakIterator; | |
| 32 | import java.util.*; | |
| 33 | import java.util.function.Consumer; | |
| 34 | import java.util.function.Supplier; | |
| 35 | import java.util.regex.Pattern; | |
| 36 | ||
| 37 | import static com.keenwrite.Constants.*; | |
| 38 | import static com.keenwrite.Messages.get; | |
| 39 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 40 | import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_EDITOR_SIZE; | |
| 41 | import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_LOCALE; | |
| 42 | import static java.lang.Character.isWhitespace; | |
| 43 | import static java.lang.Math.max; | |
| 44 | import static java.lang.String.format; | |
| 45 | import static java.util.Collections.singletonList; | |
| 46 | import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS; | |
| 47 | import static javafx.scene.input.KeyCode.*; | |
| 48 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 49 | import static javafx.scene.input.KeyCombination.SHIFT_DOWN; | |
| 50 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 51 | import static org.apache.commons.lang3.StringUtils.stripEnd; | |
| 52 | import static org.apache.commons.lang3.StringUtils.stripStart; | |
| 53 | import static org.fxmisc.richtext.model.StyleSpans.singleton; | |
| 54 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 55 | import static org.fxmisc.wellbehaved.event.InputMap.consume; | |
| 56 | ||
| 57 | /** | |
| 58 | * Responsible for editing Markdown documents. | |
| 59 | */ | |
| 60 | public class MarkdownEditor extends BorderPane implements TextEditor { | |
| 61 | private static final String NEWLINE = System.lineSeparator(); | |
| 62 | ||
| 63 | /** | |
| 64 | * Regular expression that matches the type of markup block. This is used | |
| 65 | * when Enter is pressed to continue the block environment. | |
| 66 | */ | |
| 67 | private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile( | |
| 68 | "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" ); | |
| 69 | ||
| 70 | /** | |
| 71 | * The text editor. | |
| 72 | */ | |
| 73 | private final StyleClassedTextArea mTextArea = | |
| 74 | new StyleClassedTextArea( false ); | |
| 75 | ||
| 76 | /** | |
| 77 | * Wraps the text editor in scrollbars. | |
| 78 | */ | |
| 79 | private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane = | |
| 80 | new VirtualizedScrollPane<>( mTextArea ); | |
| 81 | ||
| 82 | private final Workspace mWorkspace; | |
| 83 | ||
| 84 | /** | |
| 85 | * Tracks where the caret is located in this document. This offers observable | |
| 86 | * properties for caret position changes. | |
| 87 | */ | |
| 88 | private final Caret mCaret = createCaret( mTextArea ); | |
| 89 | ||
| 90 | /** | |
| 91 | * File being edited by this editor instance. | |
| 92 | */ | |
| 93 | private File mFile; | |
| 94 | ||
| 95 | /** | |
| 96 | * Set to {@code true} upon text or caret position changes. Value is {@code | |
| 97 | * false} by default. | |
| 98 | */ | |
| 99 | private final BooleanProperty mDirty = new SimpleBooleanProperty(); | |
| 100 | ||
| 101 | /** | |
| 102 | * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if | |
| 103 | * either no encoding could be determined or this is a new (empty) file. | |
| 104 | */ | |
| 105 | private final Charset mEncoding; | |
| 106 | ||
| 107 | /** | |
| 108 | * Tracks whether the in-memory definitions have changed with respect to the | |
| 109 | * persisted definitions. | |
| 110 | */ | |
| 111 | private final BooleanProperty mModified = new SimpleBooleanProperty(); | |
| 112 | ||
| 113 | public MarkdownEditor( final Workspace workspace ) { | |
| 114 | this( DOCUMENT_DEFAULT, workspace ); | |
| 115 | } | |
| 116 | ||
| 117 | public MarkdownEditor( final File file, final Workspace workspace ) { | |
| 118 | mEncoding = open( mFile = file ); | |
| 119 | mWorkspace = workspace; | |
| 120 | ||
| 121 | initTextArea( mTextArea ); | |
| 122 | initStyle( mTextArea ); | |
| 123 | initScrollPane( mScrollPane ); | |
| 124 | initSpellchecker( mTextArea ); | |
| 125 | initHotKeys(); | |
| 126 | initUndoManager(); | |
| 127 | } | |
| 128 | ||
| 129 | private void initTextArea( final StyleClassedTextArea textArea ) { | |
| 130 | textArea.setWrapText( true ); | |
| 131 | textArea.requestFollowCaret(); | |
| 132 | textArea.moveTo( 0 ); | |
| 133 | ||
| 134 | textArea.textProperty().addListener( ( c, o, n ) -> { | |
| 135 | // Fire, regardless of whether the caret position has changed. | |
| 136 | mDirty.set( false ); | |
| 137 | ||
| 138 | // Prevent a caret position change from raising the dirty bits. | |
| 139 | mDirty.set( true ); | |
| 140 | } ); | |
| 141 | textArea.caretPositionProperty().addListener( ( c, o, n ) -> { | |
| 142 | // Fire when the caret position has changed and the text has not. | |
| 143 | mDirty.set( true ); | |
| 144 | mDirty.set( false ); | |
| 145 | } ); | |
| 146 | } | |
| 147 | ||
| 148 | private void initStyle( final StyleClassedTextArea textArea ) { | |
| 149 | textArea.getStyleClass().add( "markdown" ); | |
| 150 | ||
| 151 | final var stylesheets = textArea.getStylesheets(); | |
| 152 | stylesheets.add( STYLESHEET_MARKDOWN ); | |
| 153 | stylesheets.add( getStylesheetPath( getLocale() ) ); | |
| 154 | ||
| 155 | localeProperty().addListener( ( c, o, n ) -> { | |
| 156 | if( n != null ) { | |
| 157 | stylesheets.remove( max( 0, stylesheets.size() - 1 ) ); | |
| 158 | stylesheets.add( getStylesheetPath( getLocale() ) ); | |
| 159 | } | |
| 160 | } ); | |
| 161 | ||
| 162 | fontSizeProperty().addListener( ( c, o, n ) -> { | |
| 163 | mTextArea.setStyle( format( "-fx-font-size: %spt;", getFontSize() ) ); | |
| 164 | } ); | |
| 165 | } | |
| 166 | ||
| 167 | private void initScrollPane( | |
| 168 | final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) { | |
| 169 | scrollpane.setVbarPolicy( ALWAYS ); | |
| 170 | setCenter( scrollpane ); | |
| 171 | } | |
| 172 | ||
| 173 | private void initSpellchecker( final StyleClassedTextArea textarea ) { | |
| 174 | final var speller = new TextEditorSpeller(); | |
| 175 | speller.checkDocument( textarea ); | |
| 176 | speller.checkParagraphs( textarea ); | |
| 177 | } | |
| 178 | ||
| 179 | private void initHotKeys() { | |
| 180 | addEventListener( keyPressed( ENTER ), this::onEnterPressed ); | |
| 181 | addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut ); | |
| 182 | addEventListener( keyPressed( TAB ), this::tab ); | |
| 183 | addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab ); | |
| 184 | } | |
| 185 | ||
| 186 | private void initUndoManager() { | |
| 187 | final var undoManager = getUndoManager(); | |
| 188 | final var markedPosition = undoManager.atMarkedPositionProperty(); | |
| 189 | ||
| 190 | undoManager.forgetHistory(); | |
| 191 | undoManager.mark(); | |
| 192 | mModified.bind( Bindings.not( markedPosition ) ); | |
| 193 | } | |
| 194 | ||
| 195 | @Override | |
| 196 | public void moveTo( final int offset ) { | |
| 197 | assert 0 <= offset && offset <= mTextArea.getLength(); | |
| 198 | mTextArea.moveTo( offset ); | |
| 199 | mTextArea.requestFollowCaret(); | |
| 200 | } | |
| 201 | ||
| 202 | /** | |
| 203 | * Delegate the focus request to the text area itself. | |
| 204 | */ | |
| 205 | @Override | |
| 206 | public void requestFocus() { | |
| 207 | mTextArea.requestFocus(); | |
| 208 | } | |
| 209 | ||
| 210 | @Override | |
| 211 | public void setText( final String text ) { | |
| 212 | mTextArea.clear(); | |
| 213 | mTextArea.appendText( text ); | |
| 214 | mTextArea.getUndoManager().mark(); | |
| 215 | } | |
| 216 | ||
| 217 | @Override | |
| 218 | public String getText() { | |
| 219 | return mTextArea.getText(); | |
| 220 | } | |
| 221 | ||
| 222 | @Override | |
| 223 | public Charset getEncoding() { | |
| 224 | return mEncoding; | |
| 225 | } | |
| 226 | ||
| 227 | @Override | |
| 228 | public File getFile() { | |
| 229 | return mFile; | |
| 230 | } | |
| 231 | ||
| 232 | @Override | |
| 233 | public void rename( final File file ) { | |
| 234 | mFile = file; | |
| 235 | } | |
| 236 | ||
| 237 | @Override | |
| 238 | public void undo() { | |
| 239 | final var manager = getUndoManager(); | |
| 240 | xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" ); | |
| 241 | } | |
| 242 | ||
| 243 | @Override | |
| 244 | public void redo() { | |
| 245 | final var manager = getUndoManager(); | |
| 246 | xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" ); | |
| 247 | } | |
| 248 | ||
| 249 | /** | |
| 250 | * Performs an undo or redo action, if possible, otherwise displays an error | |
| 251 | * message to the user. | |
| 252 | * | |
| 253 | * @param ready Answers whether the action can be executed. | |
| 254 | * @param action The action to execute. | |
| 255 | * @param key The informational message key having a value to display if | |
| 256 | * the {@link Supplier} is not ready. | |
| 257 | */ | |
| 258 | private void xxdo( | |
| 259 | final Supplier<Boolean> ready, final Runnable action, final String key ) { | |
| 260 | if( ready.get() ) { | |
| 261 | action.run(); | |
| 262 | } | |
| 263 | else { | |
| 264 | clue( key ); | |
| 265 | } | |
| 266 | } | |
| 267 | ||
| 268 | @Override | |
| 269 | public void cut() { | |
| 270 | final var selected = mTextArea.getSelectedText(); | |
| 271 | ||
| 272 | // Emulate selecting the current line by firing Home then Shift+Down Arrow. | |
| 273 | if( selected == null || selected.isEmpty() ) { | |
| 274 | // Note: mTextArea.selectLine() does not select empty lines. | |
| 275 | mTextArea.fireEvent( keyEvent( HOME, false ) ); | |
| 276 | mTextArea.fireEvent( keyEvent( DOWN, true ) ); | |
| 277 | } | |
| 278 | ||
| 279 | mTextArea.cut(); | |
| 280 | } | |
| 281 | ||
| 282 | private Event keyEvent( final KeyCode code, final boolean shift ) { | |
| 283 | return new KeyEvent( | |
| 284 | KEY_PRESSED, "", "", code, shift, false, false, false | |
| 285 | ); | |
| 286 | } | |
| 287 | ||
| 288 | @Override | |
| 289 | public void copy() { | |
| 290 | mTextArea.copy(); | |
| 291 | } | |
| 292 | ||
| 293 | @Override | |
| 294 | public void paste() { | |
| 295 | mTextArea.paste(); | |
| 296 | } | |
| 297 | ||
| 298 | @Override | |
| 299 | public void selectAll() { | |
| 300 | mTextArea.selectAll(); | |
| 301 | } | |
| 302 | ||
| 303 | @Override | |
| 304 | public void bold() { | |
| 305 | enwrap( "**" ); | |
| 306 | } | |
| 307 | ||
| 308 | @Override | |
| 309 | public void italic() { | |
| 310 | enwrap( "*" ); | |
| 311 | } | |
| 312 | ||
| 313 | @Override | |
| 314 | public void superscript() { | |
| 315 | enwrap( "^" ); | |
| 316 | } | |
| 317 | ||
| 318 | @Override | |
| 319 | public void subscript() { | |
| 320 | enwrap( "~" ); | |
| 321 | } | |
| 322 | ||
| 323 | @Override | |
| 324 | public void strikethrough() { | |
| 325 | enwrap( "~~" ); | |
| 326 | } | |
| 327 | ||
| 328 | @Override | |
| 329 | public void blockquote() { | |
| 330 | block( "> " ); | |
| 331 | } | |
| 332 | ||
| 333 | @Override | |
| 334 | public void code() { | |
| 335 | enwrap( "`" ); | |
| 336 | } | |
| 337 | ||
| 338 | @Override | |
| 339 | public void fencedCodeBlock() { | |
| 340 | final var key = "App.action.insert.fenced_code_block.prompt.text"; | |
| 341 | ||
| 342 | // TODO: Introduce sample text if nothing is selected. | |
| 343 | //enwrap( "\n\n```\n", "\n```\n\n", get( key ) ); | |
| 344 | } | |
| 345 | ||
| 346 | @Override | |
| 347 | public void heading( final int level ) { | |
| 348 | final var hashes = new String( new char[ level ] ).replace( "\0", "#" ); | |
| 349 | block( format( "%s ", hashes ) ); | |
| 350 | } | |
| 351 | ||
| 352 | @Override | |
| 353 | public void unorderedList() { | |
| 354 | block( "* " ); | |
| 355 | } | |
| 356 | ||
| 357 | @Override | |
| 358 | public void orderedList() { | |
| 359 | block( "1. " ); | |
| 360 | } | |
| 361 | ||
| 362 | @Override | |
| 363 | public void horizontalRule() { | |
| 364 | block( format( "---%n%n" ) ); | |
| 365 | } | |
| 366 | ||
| 367 | @Override | |
| 368 | public Node getNode() { | |
| 369 | return this; | |
| 370 | } | |
| 371 | ||
| 372 | @Override | |
| 373 | public ReadOnlyBooleanProperty modifiedProperty() { | |
| 374 | return mModified; | |
| 375 | } | |
| 376 | ||
| 377 | @Override | |
| 378 | public void clearModifiedProperty() { | |
| 379 | getUndoManager().mark(); | |
| 380 | } | |
| 381 | ||
| 382 | @Override | |
| 383 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 384 | return mScrollPane; | |
| 385 | } | |
| 386 | ||
| 387 | @Override | |
| 388 | public StyleClassedTextArea getTextArea() { | |
| 389 | return mTextArea; | |
| 390 | } | |
| 391 | ||
| 392 | private final Map<String, IndexRange> mStyles = new HashMap<>(); | |
| 393 | ||
| 394 | @Override | |
| 395 | public void stylize( final IndexRange range, final String style ) { | |
| 396 | final var began = range.getStart(); | |
| 397 | final var ended = range.getEnd() + 1; | |
| 398 | ||
| 399 | assert 0 <= began && began <= ended; | |
| 400 | assert style != null; | |
| 401 | ||
| 402 | // TODO: Ensure spell check and find highlights can coexist. | |
| 403 | // final var spans = mTextArea.getStyleSpans( range ); | |
| 404 | // System.out.println( "SPANS: " + spans ); | |
| 405 | ||
| 406 | // final var spans = mTextArea.getStyleSpans( range ); | |
| 407 | // mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style | |
| 408 | // ) ); | |
| 409 | ||
| 410 | // final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 411 | // builder.add( singleton( style ), range.getLength() + 1 ); | |
| 412 | // mTextArea.setStyleSpans( began, builder.create() ); | |
| 413 | ||
| 414 | // final var s = mTextArea.getStyleSpans( began, ended ); | |
| 415 | // System.out.println( "STYLES: " +s ); | |
| 416 | ||
| 417 | mStyles.put( style, range ); | |
| 418 | mTextArea.setStyleClass( began, ended, style ); | |
| 419 | ||
| 420 | // Ensure that whenever the user interacts with the text that the found | |
| 421 | // word will have its highlighting removed. The handler removes itself. | |
| 422 | // This won't remove the highlighting if the caret position moves by mouse. | |
| 423 | final var handler = mTextArea.getOnKeyPressed(); | |
| 424 | mTextArea.setOnKeyPressed( ( event ) -> { | |
| 425 | mTextArea.setOnKeyPressed( handler ); | |
| 426 | unstylize( style ); | |
| 427 | } ); | |
| 428 | ||
| 429 | //mTextArea.setStyleSpans(began, ended, s); | |
| 430 | } | |
| 431 | ||
| 432 | private static StyleSpans<Collection<String>> merge( | |
| 433 | StyleSpans<Collection<String>> spans, int len, String style ) { | |
| 434 | spans = spans.overlay( | |
| 435 | singleton( singletonList( style ), len ), | |
| 436 | ( bottomSpan, list ) -> { | |
| 437 | final List<String> l = | |
| 438 | new ArrayList<>( bottomSpan.size() + list.size() ); | |
| 439 | l.addAll( bottomSpan ); | |
| 440 | l.addAll( list ); | |
| 441 | return l; | |
| 442 | } ); | |
| 443 | ||
| 444 | return spans; | |
| 445 | } | |
| 446 | ||
| 447 | @Override | |
| 448 | public void unstylize( final String style ) { | |
| 449 | final var indexes = mStyles.remove( style ); | |
| 450 | if( indexes != null ) { | |
| 451 | mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 ); | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | @Override | |
| 456 | public Caret getCaret() { | |
| 457 | return mCaret; | |
| 458 | } | |
| 459 | ||
| 460 | private Caret createCaret( final StyleClassedTextArea editor ) { | |
| 461 | return Caret | |
| 462 | .builder() | |
| 463 | .with( Caret.Mutator::setEditor, editor ) | |
| 464 | .build(); | |
| 465 | } | |
| 466 | ||
| 467 | /** | |
| 468 | * This method adds listeners to editor events. | |
| 469 | * | |
| 470 | * @param <T> The event type. | |
| 471 | * @param <U> The consumer type for the given event type. | |
| 472 | * @param event The event of interest. | |
| 473 | * @param consumer The method to call when the event happens. | |
| 474 | */ | |
| 475 | public <T extends Event, U extends T> void addEventListener( | |
| 476 | final EventPattern<? super T, ? extends U> event, | |
| 477 | final Consumer<? super U> consumer ) { | |
| 478 | Nodes.addInputMap( mTextArea, consume( event, consumer ) ); | |
| 479 | } | |
| 480 | ||
| 481 | @SuppressWarnings( "unused" ) | |
| 482 | private void onEnterPressed( final KeyEvent event ) { | |
| 483 | final var currentLine = getCaretParagraph(); | |
| 484 | final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); | |
| 485 | ||
| 486 | // By default, insert a new line by itself. | |
| 487 | String newText = NEWLINE; | |
| 488 | ||
| 489 | // If the pattern was matched then determine what block type to continue. | |
| 490 | if( matcher.matches() ) { | |
| 491 | if( matcher.group( 2 ).isEmpty() ) { | |
| 492 | final var pos = mTextArea.getCaretPosition(); | |
| 493 | mTextArea.selectRange( pos - currentLine.length(), pos ); | |
| 494 | } | |
| 495 | else { | |
| 496 | // Indent the new line with the same whitespace characters and | |
| 497 | // list markers as current line. This ensures that the indentation | |
| 498 | // is propagated. | |
| 499 | newText = newText.concat( matcher.group( 1 ) ); | |
| 500 | } | |
| 501 | } | |
| 502 | ||
| 503 | mTextArea.replaceSelection( newText ); | |
| 504 | } | |
| 505 | ||
| 506 | private void cut( final KeyEvent event ) { | |
| 507 | cut(); | |
| 508 | } | |
| 509 | ||
| 510 | private void tab( final KeyEvent event ) { | |
| 511 | final var range = mTextArea.selectionProperty().getValue(); | |
| 512 | final var sb = new StringBuilder( 1024 ); | |
| 513 | ||
| 514 | if( range.getLength() > 0 ) { | |
| 515 | final var selection = mTextArea.getSelectedText(); | |
| 516 | ||
| 517 | selection.lines().forEach( | |
| 518 | ( l ) -> sb.append( "\t" ).append( l ).append( NEWLINE ) | |
| 519 | ); | |
| 520 | } | |
| 521 | else { | |
| 522 | sb.append( "\t" ); | |
| 523 | } | |
| 524 | ||
| 525 | mTextArea.replaceSelection( sb.toString() ); | |
| 526 | } | |
| 527 | ||
| 528 | private void untab( final KeyEvent event ) { | |
| 529 | final var range = mTextArea.selectionProperty().getValue(); | |
| 530 | ||
| 531 | if( range.getLength() > 0 ) { | |
| 532 | final var selection = mTextArea.getSelectedText(); | |
| 533 | final var sb = new StringBuilder( selection.length() ); | |
| 534 | ||
| 535 | selection.lines().forEach( | |
| 536 | ( l ) -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l ) | |
| 537 | .append( NEWLINE ) | |
| 538 | ); | |
| 539 | ||
| 540 | mTextArea.replaceSelection( sb.toString() ); | |
| 541 | } | |
| 542 | else { | |
| 543 | final var p = getCaretParagraph(); | |
| 544 | ||
| 545 | if( p.startsWith( "\t" ) ) { | |
| 546 | mTextArea.selectParagraph(); | |
| 547 | mTextArea.replaceSelection( p.substring( 1 ) ); | |
| 548 | } | |
| 549 | } | |
| 550 | } | |
| 551 | ||
| 552 | /** | |
| 553 | * Observers may listen for changes to the property returned from this method | |
| 554 | * to receive notifications when either the text or caret have changed. This | |
| 555 | * should not be used to track whether the text has been modified. | |
| 556 | */ | |
| 557 | public void addDirtyListener( ChangeListener<Boolean> listener ) { | |
| 558 | mDirty.addListener( listener ); | |
| 559 | } | |
| 560 | ||
| 561 | /** | |
| 562 | * Surrounds the selected text or word under the caret in Markdown markup. | |
| 563 | * | |
| 564 | * @param token The beginning and ending token for enclosing the text. | |
| 565 | */ | |
| 566 | private void enwrap( final String token ) { | |
| 567 | enwrap( token, token ); | |
| 568 | } | |
| 569 | ||
| 570 | /** | |
| 571 | * Surrounds the selected text or word under the caret in Markdown markup. | |
| 572 | * | |
| 573 | * @param began The beginning token for enclosing the text. | |
| 574 | * @param ended The ending token for enclosing the text. | |
| 575 | */ | |
| 576 | private void enwrap( final String began, String ended ) { | |
| 577 | // Ensure selected text takes precedence over the word at caret position. | |
| 578 | final var selected = mTextArea.selectionProperty().getValue(); | |
| 579 | final var range = selected.getLength() == 0 | |
| 580 | ? getCaretWord() | |
| 581 | : selected; | |
| 582 | String text = mTextArea.getText( range ); | |
| 583 | ||
| 584 | int length = range.getLength(); | |
| 585 | text = stripStart( text, null ); | |
| 586 | final int beganIndex = range.getStart() + (length - text.length()); | |
| 587 | ||
| 588 | length = text.length(); | |
| 589 | text = stripEnd( text, null ); | |
| 590 | final int endedIndex = range.getEnd() - (length - text.length()); | |
| 591 | ||
| 592 | mTextArea.replaceText( beganIndex, endedIndex, began + text + ended ); | |
| 593 | } | |
| 594 | ||
| 595 | /** | |
| 596 | * Inserts the given block-level markup at the current caret position | |
| 597 | * within the document. This will prepend two blank lines to ensure that | |
| 598 | * the block element begins at the start of a new line. | |
| 599 | * | |
| 600 | * @param markup The text to insert at the caret. | |
| 601 | */ | |
| 602 | private void block( final String markup ) { | |
| 603 | final int pos = mTextArea.getCaretPosition(); | |
| 604 | mTextArea.insertText( pos, format( "%n%n%s", markup ) ); | |
| 605 | } | |
| 606 | ||
| 607 | /** | |
| 608 | * Returns the caret position within the current paragraph. | |
| 609 | * | |
| 610 | * @return A value from 0 to the length of the current paragraph. | |
| 611 | */ | |
| 612 | private int getCaretColumn() { | |
| 613 | return mTextArea.getCaretColumn(); | |
| 614 | } | |
| 615 | ||
| 616 | @Override | |
| 617 | public IndexRange getCaretWord() { | |
| 618 | final var paragraph = getCaretParagraph(); | |
| 619 | final var length = paragraph.length(); | |
| 620 | final var column = getCaretColumn(); | |
| 621 | ||
| 622 | var began = column; | |
| 623 | var ended = column; | |
| 624 | ||
| 625 | while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) { | |
| 626 | began--; | |
| 627 | } | |
| 628 | ||
| 629 | while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) { | |
| 630 | ended++; | |
| 631 | } | |
| 632 | ||
| 633 | final var iterator = BreakIterator.getWordInstance(); | |
| 634 | iterator.setText( paragraph ); | |
| 635 | ||
| 636 | while( began < length && iterator.isBoundary( began + 1 ) ) { | |
| 637 | began++; | |
| 638 | } | |
| 639 | ||
| 640 | while( ended > 0 && iterator.isBoundary( ended - 1 ) ) { | |
| 641 | ended--; | |
| 642 | } | |
| 643 | ||
| 644 | final var offset = getCaretDocumentOffset( column ); | |
| 645 | ||
| 646 | return IndexRange.normalize( began + offset, ended + offset ); | |
| 647 | } | |
| 648 | ||
| 649 | private int getCaretDocumentOffset( final int column ) { | |
| 650 | return mTextArea.getCaretPosition() - column; | |
| 651 | } | |
| 652 | ||
| 653 | /** | |
| 654 | * Returns the index of the paragraph where the caret resides. | |
| 655 | * | |
| 656 | * @return A number greater than or equal to 0. | |
| 657 | */ | |
| 658 | private int getCurrentParagraph() { | |
| 659 | return mTextArea.getCurrentParagraph(); | |
| 660 | } | |
| 661 | ||
| 662 | /** | |
| 663 | * Returns the text for the paragraph that contains the caret. | |
| 664 | * | |
| 665 | * @return A non-null string, possibly empty. | |
| 666 | */ | |
| 667 | private String getCaretParagraph() { | |
| 668 | return getText( getCurrentParagraph() ); | |
| 669 | } | |
| 670 | ||
| 671 | @Override | |
| 672 | public String getText( final int paragraph ) { | |
| 673 | return mTextArea.getText( paragraph ); | |
| 674 | } | |
| 675 | ||
| 676 | @Override | |
| 677 | public String getText( final IndexRange indexes ) | |
| 678 | throws IndexOutOfBoundsException { | |
| 679 | return mTextArea.getText( indexes.getStart(), indexes.getEnd() ); | |
| 680 | } | |
| 681 | ||
| 682 | @Override | |
| 683 | public void replaceText( final IndexRange indexes, final String s ) { | |
| 684 | mTextArea.replaceText( indexes, s ); | |
| 685 | } | |
| 686 | ||
| 687 | private UndoManager<?> getUndoManager() { | |
| 688 | return mTextArea.getUndoManager(); | |
| 689 | } | |
| 690 | ||
| 691 | /** | |
| 692 | * Returns the path to a {@link Locale}-specific stylesheet. | |
| 693 | * | |
| 694 | * @return A non-null string to inject into the HTML document head. | |
| 695 | */ | |
| 696 | private static String getStylesheetPath( final Locale locale ) { | |
| 697 | return get( | |
| 698 | sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ), | |
| 699 | locale.getLanguage(), | |
| 700 | locale.getScript(), | |
| 701 | locale.getCountry() | |
| 702 | ); | |
| 703 | } | |
| 704 | ||
| 705 | private Locale getLocale() { | |
| 706 | return localeProperty().toLocale(); | |
| 707 | } | |
| 708 | ||
| 709 | private LocaleProperty localeProperty() { | |
| 710 | return mWorkspace.localeProperty( KEY_UI_FONT_LOCALE ); | |
| 711 | } | |
| 712 | ||
| 713 | private double getFontSize() { | |
| 714 | return fontSizeProperty().get(); | |
| 715 | } | |
| 716 | ||
| 717 | private DoubleProperty fontSizeProperty() { | |
| 718 | return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE ); | |
| 719 | } | |
| 720 | } | |
| 1 | 721 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.dialogs.ImageDialog; | |
| 31 | import com.keenwrite.dialogs.LinkDialog; | |
| 32 | import com.keenwrite.editors.EditorPane; | |
| 33 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 34 | import com.vladsch.flexmark.ast.Link; | |
| 35 | import javafx.scene.control.Dialog; | |
| 36 | import javafx.scene.control.IndexRange; | |
| 37 | import javafx.scene.input.KeyCode; | |
| 38 | import javafx.scene.input.KeyEvent; | |
| 39 | import javafx.stage.Window; | |
| 40 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 41 | ||
| 42 | import java.nio.file.Path; | |
| 43 | import java.util.regex.Matcher; | |
| 44 | import java.util.regex.Pattern; | |
| 45 | ||
| 46 | import static com.keenwrite.Constants.STYLESHEET_MARKDOWN; | |
| 47 | import static com.keenwrite.util.Utils.ltrim; | |
| 48 | import static com.keenwrite.util.Utils.rtrim; | |
| 49 | import static javafx.scene.input.KeyCode.ENTER; | |
| 50 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 51 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 52 | ||
| 53 | /** | |
| 54 | * Provides the ability to edit a text document. | |
| 55 | */ | |
| 56 | public class MarkdownEditorPane extends EditorPane { | |
| 57 | private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile( | |
| 58 | "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" ); | |
| 59 | ||
| 60 | public MarkdownEditorPane() { | |
| 61 | initEditor(); | |
| 62 | } | |
| 63 | ||
| 64 | private void initEditor() { | |
| 65 | final StyleClassedTextArea textArea = getEditor(); | |
| 66 | ||
| 67 | textArea.setWrapText( true ); | |
| 68 | textArea.getStyleClass().add( "markdown-editor" ); | |
| 69 | textArea.getStylesheets().add( STYLESHEET_MARKDOWN ); | |
| 70 | ||
| 71 | addKeyboardListener( keyPressed( ENTER ), this::enterPressed ); | |
| 72 | addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut ); | |
| 73 | } | |
| 74 | ||
| 75 | public void insertLink() { | |
| 76 | insertObject( createLinkDialog() ); | |
| 77 | } | |
| 78 | ||
| 79 | public void insertImage() { | |
| 80 | insertObject( createImageDialog() ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Gets the index of the paragraph where the caret is positioned. | |
| 85 | * | |
| 86 | * @return The paragraph number for the caret. | |
| 87 | */ | |
| 88 | public int getCurrentParagraphIndex() { | |
| 89 | return getEditor().getCurrentParagraph(); | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * @param leading Characters to insert at the beginning of the current | |
| 94 | * selection (or paragraph). | |
| 95 | * @param trailing Characters to insert at the end of the current selection | |
| 96 | * (or paragraph). | |
| 97 | */ | |
| 98 | public void surroundSelection( final String leading, final String trailing ) { | |
| 99 | surroundSelection( leading, trailing, null ); | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * @param leading Characters to insert at the beginning of the current | |
| 104 | * selection (or paragraph). | |
| 105 | * @param trailing Characters to insert at the end of the current selection | |
| 106 | * (or paragraph). | |
| 107 | * @param hint Instructional text inserted within the leading and | |
| 108 | * trailing characters, provided no text is selected. | |
| 109 | */ | |
| 110 | public void surroundSelection( | |
| 111 | String leading, String trailing, final String hint ) { | |
| 112 | final StyleClassedTextArea textArea = getEditor(); | |
| 113 | ||
| 114 | // Note: not using textArea.insertText() to insert leading and trailing | |
| 115 | // because this would add two changes to undo history | |
| 116 | final IndexRange selection = textArea.getSelection(); | |
| 117 | int start = selection.getStart(); | |
| 118 | int end = selection.getEnd(); | |
| 119 | ||
| 120 | final String selectedText = textArea.getSelectedText(); | |
| 121 | ||
| 122 | String trimmedText = selectedText.trim(); | |
| 123 | if( trimmedText.length() < selectedText.length() ) { | |
| 124 | start += selectedText.indexOf( trimmedText ); | |
| 125 | end = start + trimmedText.length(); | |
| 126 | } | |
| 127 | ||
| 128 | // remove leading whitespaces from leading text if selection starts at zero | |
| 129 | if( start == 0 ) { | |
| 130 | leading = ltrim( leading ); | |
| 131 | } | |
| 132 | ||
| 133 | // remove trailing whitespaces from trailing text if selection ends at | |
| 134 | // text end | |
| 135 | if( end == textArea.getLength() ) { | |
| 136 | trailing = rtrim( trailing ); | |
| 137 | } | |
| 138 | ||
| 139 | // remove leading line separators from leading text | |
| 140 | // if there are line separators before the selected text | |
| 141 | if( leading.startsWith( "\n" ) ) { | |
| 142 | for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) { | |
| 143 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 144 | break; | |
| 145 | } | |
| 146 | ||
| 147 | leading = leading.substring( 1 ); | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | // remove trailing line separators from trailing or leading text | |
| 152 | // if there are line separators after the selected text | |
| 153 | final boolean trailingIsEmpty = trailing.isEmpty(); | |
| 154 | String str = trailingIsEmpty ? leading : trailing; | |
| 155 | ||
| 156 | if( str.endsWith( "\n" ) ) { | |
| 157 | final int length = textArea.getLength(); | |
| 158 | ||
| 159 | for( int i = end; i < length && str.endsWith( "\n" ); i++ ) { | |
| 160 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 161 | break; | |
| 162 | } | |
| 163 | ||
| 164 | str = str.substring( 0, str.length() - 1 ); | |
| 165 | } | |
| 166 | ||
| 167 | if( trailingIsEmpty ) { | |
| 168 | leading = str; | |
| 169 | } | |
| 170 | else { | |
| 171 | trailing = str; | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | int selStart = start + leading.length(); | |
| 176 | int selEnd = end + leading.length(); | |
| 177 | ||
| 178 | // insert hint text if selection is empty | |
| 179 | if( hint != null && trimmedText.isEmpty() ) { | |
| 180 | trimmedText = hint; | |
| 181 | selEnd = selStart + hint.length(); | |
| 182 | } | |
| 183 | ||
| 184 | // prevent undo merging with previous text entered by user | |
| 185 | getUndoManager().preventMerge(); | |
| 186 | ||
| 187 | // replace text and update selection | |
| 188 | textArea.replaceText( start, end, leading + trimmedText + trailing ); | |
| 189 | textArea.selectRange( selStart, selEnd ); | |
| 190 | } | |
| 191 | ||
| 192 | private void enterPressed( final KeyEvent e ) { | |
| 193 | final StyleClassedTextArea textArea = getEditor(); | |
| 194 | final String currentLine = | |
| 195 | textArea.getText( textArea.getCurrentParagraph() ); | |
| 196 | final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); | |
| 197 | ||
| 198 | String newText = "\n"; | |
| 199 | ||
| 200 | if( matcher.matches() ) { | |
| 201 | if( !matcher.group( 2 ).isEmpty() ) { | |
| 202 | // indent new line with same whitespace characters and list markers | |
| 203 | // as current line | |
| 204 | newText = newText.concat( matcher.group( 1 ) ); | |
| 205 | } | |
| 206 | else { | |
| 207 | // current line contains only whitespace characters and list markers | |
| 208 | // --> empty current line | |
| 209 | final int caretPosition = textArea.getCaretPosition(); | |
| 210 | textArea.selectRange( caretPosition - currentLine.length(), | |
| 211 | caretPosition ); | |
| 212 | } | |
| 213 | } | |
| 214 | ||
| 215 | textArea.replaceSelection( newText ); | |
| 216 | ||
| 217 | // Ensure that the window scrolls when Enter is pressed at the bottom of | |
| 218 | // the pane. | |
| 219 | textArea.requestFollowCaret(); | |
| 220 | } | |
| 221 | ||
| 222 | private void cut( final KeyEvent event ) { | |
| 223 | super.cut(); | |
| 224 | } | |
| 225 | ||
| 226 | /** | |
| 227 | * Returns one of: selected text, word under cursor, or parsed hyperlink from | |
| 228 | * the markdown AST. | |
| 229 | * | |
| 230 | * @return An instance containing the link URL and display text. | |
| 231 | */ | |
| 232 | private HyperlinkModel getHyperlink() { | |
| 233 | final var textArea = getEditor(); | |
| 234 | final var selectedText = textArea.getSelectedText(); | |
| 235 | ||
| 236 | // Get the current paragraph, convert to Markdown nodes. | |
| 237 | final var mp = MarkdownProcessor.create(); | |
| 238 | final var p = textArea.getCurrentParagraph(); | |
| 239 | final var paragraph = textArea.getText( p ); | |
| 240 | final var node = mp.toNode( paragraph ); | |
| 241 | final var visitor = new LinkVisitor( textArea.getCaretColumn() ); | |
| 242 | final var link = visitor.process( node ); | |
| 243 | ||
| 244 | if( link != null ) { | |
| 245 | textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() ); | |
| 246 | } | |
| 247 | ||
| 248 | return createHyperlinkModel( | |
| 249 | link, selectedText, "https://localhost" | |
| 250 | ); | |
| 251 | } | |
| 252 | ||
| 253 | @SuppressWarnings("SameParameterValue") | |
| 254 | private HyperlinkModel createHyperlinkModel( | |
| 255 | final Link link, final String selection, final String url ) { | |
| 256 | ||
| 257 | return link == null | |
| 258 | ? new HyperlinkModel( selection, url ) | |
| 259 | : new HyperlinkModel( link ); | |
| 260 | } | |
| 261 | ||
| 262 | private Path getParentPath() { | |
| 263 | final Path path = getPath(); | |
| 264 | return path != null ? path.getParent() : null; | |
| 265 | } | |
| 266 | ||
| 267 | private Dialog<String> createLinkDialog() { | |
| 268 | return new LinkDialog( getWindow(), getHyperlink() ); | |
| 269 | } | |
| 270 | ||
| 271 | private Dialog<String> createImageDialog() { | |
| 272 | return new ImageDialog( getWindow(), getParentPath() ); | |
| 273 | } | |
| 274 | ||
| 275 | private void insertObject( final Dialog<String> dialog ) { | |
| 276 | dialog.showAndWait().ifPresent( | |
| 277 | result -> getEditor().replaceSelection( result ) | |
| 278 | ); | |
| 279 | } | |
| 280 | ||
| 281 | private Window getWindow() { | |
| 282 | return getScrollPane().getScene().getWindow(); | |
| 283 | } | |
| 284 | } | |
| 285 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.exceptions; |
| 29 | 3 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | /** | |
| 5 | * Represents different file type classifications. These are high-level mappings | |
| 6 | * that correspond to the list of glob patterns found within {@code | |
| 7 | * settings.properties}. | |
| 8 | */ | |
| 9 | public enum FileType { | |
| 10 | ||
| 11 | ALL( "all" ), | |
| 12 | RMARKDOWN( "rmarkdown" ), | |
| 13 | RXML( "rxml" ), | |
| 14 | SOURCE( "source" ), | |
| 15 | DEFINITION( "definition" ), | |
| 16 | XML( "xml" ), | |
| 17 | CSV( "csv" ), | |
| 18 | JSON( "json" ), | |
| 19 | TOML( "toml" ), | |
| 20 | YAML( "yaml" ), | |
| 21 | PROPERTIES( "properties" ), | |
| 22 | UNKNOWN( "unknown" ); | |
| 23 | ||
| 24 | private final String mType; | |
| 25 | ||
| 26 | /** | |
| 27 | * Default constructor for enumerated file type. | |
| 28 | * | |
| 29 | * @param type Human-readable name for the file type. | |
| 30 | */ | |
| 31 | FileType( final String type ) { | |
| 32 | mType = type; | |
| 33 | } | |
| 34 | ||
| 35 | /** | |
| 36 | * Returns the file type that corresponds to the given string. | |
| 37 | * | |
| 38 | * @param type The string to compare against this enumeration of file types. | |
| 39 | * @return The corresponding File Type for the given string. | |
| 40 | * @throws IllegalArgumentException Type not found. | |
| 41 | */ | |
| 42 | public static FileType from( final String type ) { | |
| 43 | for( final FileType fileType : FileType.values() ) { | |
| 44 | if( fileType.isType( type ) ) { | |
| 45 | return fileType; | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | throw new IllegalArgumentException( type ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Answers whether this file type matches the given string, case insensitive | |
| 54 | * comparison. | |
| 55 | * | |
| 56 | * @param type Presumably a file name extension to check against. | |
| 57 | * @return true The given extension corresponds to this enumerated type. | |
| 58 | */ | |
| 59 | public boolean isType( final String type ) { | |
| 60 | return getType().equalsIgnoreCase( type ); | |
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * Returns the human-readable name for the file type. | |
| 65 | * | |
| 66 | * @return A non-null instance. | |
| 67 | */ | |
| 68 | private String getType() { | |
| 69 | return mType; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Returns the lowercase version of the file name extension. | |
| 74 | * | |
| 75 | * @return The file name, in lower case. | |
| 76 | */ | |
| 77 | @Override | |
| 78 | public String toString() { | |
| 79 | return getType(); | |
| 80 | } | |
| 81 | } | |
| 1 | 82 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | import java.net.MalformedURLException; | |
| 5 | import java.net.URI; | |
| 6 | import java.net.URL; | |
| 7 | import java.net.http.HttpClient; | |
| 8 | import java.net.http.HttpRequest; | |
| 9 | ||
| 10 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 11 | import static com.keenwrite.io.MediaType.UNDEFINED; | |
| 12 | import static java.net.http.HttpClient.Redirect.NORMAL; | |
| 13 | import static java.net.http.HttpRequest.BodyPublishers.noBody; | |
| 14 | import static java.net.http.HttpResponse.BodyHandlers.discarding; | |
| 15 | import static java.time.Duration.ofSeconds; | |
| 16 | ||
| 17 | /** | |
| 18 | * Responsible for determining {@link MediaType} based on the content-type from | |
| 19 | * an HTTP request. | |
| 20 | */ | |
| 21 | public class HttpMediaType { | |
| 22 | ||
| 23 | private final static HttpClient HTTP_CLIENT = HttpClient | |
| 24 | .newBuilder() | |
| 25 | .connectTimeout( ofSeconds( 5 ) ) | |
| 26 | .followRedirects( NORMAL ) | |
| 27 | .build(); | |
| 28 | ||
| 29 | /** | |
| 30 | * Performs an HTTP HEAD request to determine the media type based on the | |
| 31 | * Content-Type header returned from the server. | |
| 32 | * | |
| 33 | * @param uri Determine the media type for this resource. | |
| 34 | * @return The data type for the resource or {@link MediaType#UNDEFINED} if | |
| 35 | * unmapped. | |
| 36 | * @throws MalformedURLException The {@link URI} could not be converted to | |
| 37 | * a {@link URL}. | |
| 38 | */ | |
| 39 | public static MediaType valueFrom( final URI uri ) | |
| 40 | throws MalformedURLException { | |
| 41 | final var mediaType = new MediaType[]{UNDEFINED}; | |
| 42 | ||
| 43 | try { | |
| 44 | clue( "Main.status.image.request.init" ); | |
| 45 | final var request = HttpRequest | |
| 46 | .newBuilder( uri ) | |
| 47 | .method( "HEAD", noBody() ) | |
| 48 | .build(); | |
| 49 | clue( "Main.status.image.request.fetch", uri.getHost() ); | |
| 50 | final var response = HTTP_CLIENT.send( request, discarding() ); | |
| 51 | final var headers = response.headers(); | |
| 52 | final var map = headers.map(); | |
| 53 | ||
| 54 | map.forEach( ( key, values ) -> { | |
| 55 | if( "content-type".equalsIgnoreCase( key ) ) { | |
| 56 | var header = values.get( 0 ); | |
| 57 | // Trim off the character encoding. | |
| 58 | var i = header.indexOf( ';' ); | |
| 59 | header = header.substring( 0, i == -1 ? header.length() : i ); | |
| 60 | ||
| 61 | // Split the type and subtype. | |
| 62 | i = header.indexOf( '/' ); | |
| 63 | i = i == -1 ? header.length() : i; | |
| 64 | final var type = header.substring( 0, i ); | |
| 65 | final var subtype = header.substring( i + 1 ); | |
| 66 | ||
| 67 | mediaType[ 0 ] = MediaType.valueFrom( type, subtype ); | |
| 68 | clue( "Main.status.image.request.success", mediaType[ 0 ] ); | |
| 69 | } | |
| 70 | } ); | |
| 71 | } catch( final Exception ex ) { | |
| 72 | clue( ex ); | |
| 73 | } | |
| 74 | ||
| 75 | return mediaType[ 0 ]; | |
| 76 | } | |
| 77 | } | |
| 1 | 78 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | import java.io.File; | |
| 5 | import java.nio.file.Path; | |
| 6 | ||
| 7 | import static com.keenwrite.io.MediaType.TypeName.*; | |
| 8 | import static com.keenwrite.io.MediaTypeExtensions.getMediaType; | |
| 9 | import static org.apache.commons.io.FilenameUtils.getExtension; | |
| 10 | ||
| 11 | /** | |
| 12 | * Defines various file formats and format contents. | |
| 13 | * | |
| 14 | * @see | |
| 15 | * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA | |
| 16 | * Media Types</a> | |
| 17 | */ | |
| 18 | public enum MediaType { | |
| 19 | APP_JAVA_OBJECT( | |
| 20 | APPLICATION, "x-java-serialized-object" | |
| 21 | ), | |
| 22 | ||
| 23 | FONT_OTF( "otf" ), | |
| 24 | FONT_TTF( "ttf" ), | |
| 25 | ||
| 26 | IMAGE_APNG( "apng" ), | |
| 27 | IMAGE_ACES( "aces" ), | |
| 28 | IMAGE_AVCI( "avci" ), | |
| 29 | IMAGE_AVCS( "avcs" ), | |
| 30 | IMAGE_BMP( "bmp" ), | |
| 31 | IMAGE_CGM( "cgm" ), | |
| 32 | IMAGE_DICOM_RLE( "dicom_rle" ), | |
| 33 | IMAGE_EMF( "emf" ), | |
| 34 | IMAGE_EXAMPLE( "example" ), | |
| 35 | IMAGE_FITS( "fits" ), | |
| 36 | IMAGE_G3FAX( "g3fax" ), | |
| 37 | IMAGE_GIF( "gif" ), | |
| 38 | IMAGE_HEIC( "heic" ), | |
| 39 | IMAGE_HEIF( "heif" ), | |
| 40 | IMAGE_HEJ2K( "hej2k" ), | |
| 41 | IMAGE_HSJ2( "hsj2" ), | |
| 42 | IMAGE_X_ICON( "x-icon" ), | |
| 43 | IMAGE_JLS( "jls" ), | |
| 44 | IMAGE_JP2( "jp2" ), | |
| 45 | IMAGE_JPEG( "jpeg" ), | |
| 46 | IMAGE_JPH( "jph" ), | |
| 47 | IMAGE_JPHC( "jphc" ), | |
| 48 | IMAGE_JPM( "jpm" ), | |
| 49 | IMAGE_JPX( "jpx" ), | |
| 50 | IMAGE_JXR( "jxr" ), | |
| 51 | IMAGE_JXRA( "jxrA" ), | |
| 52 | IMAGE_JXRS( "jxrS" ), | |
| 53 | IMAGE_JXS( "jxs" ), | |
| 54 | IMAGE_JXSC( "jxsc" ), | |
| 55 | IMAGE_JXSI( "jxsi" ), | |
| 56 | IMAGE_JXSS( "jxss" ), | |
| 57 | IMAGE_KTX( "ktx" ), | |
| 58 | IMAGE_KTX2( "ktx2" ), | |
| 59 | IMAGE_NAPLPS( "naplps" ), | |
| 60 | IMAGE_PNG( "png" ), | |
| 61 | IMAGE_SVG_XML( "svg+xml" ), | |
| 62 | IMAGE_T38( "t38" ), | |
| 63 | IMAGE_TIFF( "tiff" ), | |
| 64 | IMAGE_WEBP( "webp" ), | |
| 65 | IMAGE_WMF( "wmf" ), | |
| 66 | ||
| 67 | TEXT_HTML( TEXT, "html" ), | |
| 68 | TEXT_MARKDOWN( TEXT, "markdown" ), | |
| 69 | TEXT_PLAIN( TEXT, "plain" ), | |
| 70 | TEXT_R_MARKDOWN( TEXT, "R+markdown" ), | |
| 71 | TEXT_R_XML( TEXT, "R+xml" ), | |
| 72 | TEXT_YAML( TEXT, "yaml" ), | |
| 73 | ||
| 74 | UNDEFINED( TypeName.UNDEFINED, "undefined" ); | |
| 75 | ||
| 76 | /** | |
| 77 | * The IANA-defined types. | |
| 78 | */ | |
| 79 | public enum TypeName { | |
| 80 | APPLICATION, | |
| 81 | IMAGE, | |
| 82 | TEXT, | |
| 83 | UNDEFINED | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * The fully qualified IANA-defined media type. | |
| 88 | */ | |
| 89 | private final String mMediaType; | |
| 90 | ||
| 91 | /** | |
| 92 | * The IANA-defined type name. | |
| 93 | */ | |
| 94 | private final TypeName mTypeName; | |
| 95 | ||
| 96 | /** | |
| 97 | * The IANA-defined subtype name. | |
| 98 | */ | |
| 99 | private final String mSubtype; | |
| 100 | ||
| 101 | /** | |
| 102 | * Constructs an instance using the default type name of "image". | |
| 103 | * | |
| 104 | * @param subtype The image subtype name. | |
| 105 | */ | |
| 106 | MediaType( final String subtype ) { | |
| 107 | this( IMAGE, subtype ); | |
| 108 | } | |
| 109 | ||
| 110 | /** | |
| 111 | * Constructs an instance using an IANA-defined type and subtype pair. | |
| 112 | * | |
| 113 | * @param typeName The media type's type name. | |
| 114 | * @param subtype The media type's subtype name. | |
| 115 | */ | |
| 116 | MediaType( final TypeName typeName, final String subtype ) { | |
| 117 | mTypeName = typeName; | |
| 118 | mSubtype = subtype; | |
| 119 | mMediaType = typeName.toString().toLowerCase() + '/' + subtype; | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Returns the {@link MediaType} associated with the given file. | |
| 124 | * | |
| 125 | * @param file Has a file name that may contain an extension associated with | |
| 126 | * a known {@link MediaType}. | |
| 127 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 128 | * assigned, otherwise the {@link MediaType} associated with this | |
| 129 | * {@link File}'s file name extension. | |
| 130 | */ | |
| 131 | public static MediaType valueFrom( final File file ) { | |
| 132 | return valueFrom( file.getName() ); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Returns the {@link MediaType} associated with the path to a file. | |
| 137 | * | |
| 138 | * @param path Has a file name that may contain an extension associated with | |
| 139 | * a known {@link MediaType}. | |
| 140 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 141 | * assigned, otherwise the {@link MediaType} associated with this | |
| 142 | * {@link File}'s file name extension. | |
| 143 | */ | |
| 144 | public static MediaType valueFrom( final Path path ) { | |
| 145 | return valueFrom( path.toFile() ); | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Returns the {@link MediaType} associated with the given file name. | |
| 150 | * | |
| 151 | * @param filename The file name that may contain an extension associated | |
| 152 | * with a known {@link MediaType}. | |
| 153 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 154 | * assigned, otherwise the {@link MediaType} associated with this | |
| 155 | * URL's file name extension. | |
| 156 | */ | |
| 157 | public static MediaType valueFrom( final String filename ) { | |
| 158 | return getMediaType( getExtension( filename ) ); | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * Returns the {@link MediaType} for the given type and subtype names. | |
| 163 | * | |
| 164 | * @param type The IANA-defined type name. | |
| 165 | * @param subtype The IANA-defined subtype name. | |
| 166 | * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that | |
| 167 | * matches the given type and subtype names. | |
| 168 | */ | |
| 169 | public static MediaType valueFrom( | |
| 170 | final String type, final String subtype ) { | |
| 171 | for( final var mediaType : MediaType.values() ) { | |
| 172 | if( mediaType.equals( type, subtype ) ) { | |
| 173 | return mediaType; | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | return UNDEFINED; | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Answers whether the given type and subtype names equal this enumerated | |
| 182 | * value. This performs a case-insensitive comparison. | |
| 183 | * | |
| 184 | * @param type The type name to compare against this {@link MediaType}. | |
| 185 | * @param subtype The subtype name to compare against this {@link MediaType}. | |
| 186 | * @return {@code true} when the type and subtype name match. | |
| 187 | */ | |
| 188 | public boolean equals( final String type, final String subtype ) { | |
| 189 | return mTypeName.name().equalsIgnoreCase( type ) && | |
| 190 | mSubtype.equalsIgnoreCase( subtype ); | |
| 191 | } | |
| 192 | ||
| 193 | /** | |
| 194 | * Answers whether the given {@link TypeName} matches this type name. | |
| 195 | * | |
| 196 | * @param typeName The {@link TypeName} to compare against the internal value. | |
| 197 | * @return {@code true} if the given value is the same IANA-defined type name. | |
| 198 | */ | |
| 199 | public boolean isType( final TypeName typeName ) { | |
| 200 | return mTypeName == typeName; | |
| 201 | } | |
| 202 | ||
| 203 | /** | |
| 204 | * Returns the IANA-defined type and sub-type. | |
| 205 | * | |
| 206 | * @return The unique media type identifier. | |
| 207 | */ | |
| 208 | public String toString() { | |
| 209 | return mMediaType; | |
| 210 | } | |
| 211 | ||
| 212 | /** | |
| 213 | * Used by {@link MediaTypeExtensions} to initialize associations where the | |
| 214 | * subtype name and the file name extension have a 1:1 mapping. | |
| 215 | * | |
| 216 | * @return The IANA subtype value. | |
| 217 | */ | |
| 218 | String getSubtype() { | |
| 219 | return mSubtype; | |
| 220 | } | |
| 221 | } | |
| 1 | 222 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | import java.util.Set; | |
| 5 | ||
| 6 | import static com.keenwrite.io.MediaType.*; | |
| 7 | import static java.util.Set.of; | |
| 8 | ||
| 9 | /** | |
| 10 | * Responsible for associating file extensions with {@link MediaType} instances. | |
| 11 | */ | |
| 12 | enum MediaTypeExtensions { | |
| 13 | MEDIA_FONT_OTF( FONT_OTF ), | |
| 14 | MEDIA_FONT_TTF( FONT_TTF ), | |
| 15 | ||
| 16 | MEDIA_IMAGE_APNG( IMAGE_APNG ), | |
| 17 | MEDIA_IMAGE_BMP( IMAGE_BMP ), | |
| 18 | MEDIA_IMAGE_GIF( IMAGE_GIF ), | |
| 19 | MEDIA_IMAGE_ICO( IMAGE_X_ICON, of( "ico", "cur" ) ), | |
| 20 | MEDIA_IMAGE_JPEG( IMAGE_JPEG, of( "jpg", "jpeg", "jfif", "pjpeg", "pjp" ) ), | |
| 21 | MEDIA_IMAGE_PNG( IMAGE_PNG ), | |
| 22 | MEDIA_IMAGE_SVG( IMAGE_SVG_XML, of( "svg" ) ), | |
| 23 | MEDIA_IMAGE_TIFF( IMAGE_TIFF, of( "tif", "tiff" ) ), | |
| 24 | MEDIA_IMAGE_WEBP( IMAGE_WEBP ), | |
| 25 | ||
| 26 | MEDIA_TEXT_MARKDOWN( TEXT_MARKDOWN, of( | |
| 27 | "md", "markdown", "mdown", "mdtxt", "mdtext", "mdwn", "mkd", "mkdown", | |
| 28 | "mkdn" ) ), | |
| 29 | MEDIA_TEXT_PLAIN( TEXT_PLAIN, of( "asc", "ascii", "txt", "text", "utxt" ) ), | |
| 30 | MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ), | |
| 31 | MEDIA_TEXT_R_XML( TEXT_R_XML, of( "Rxml" ) ), | |
| 32 | MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ); | |
| 33 | ||
| 34 | private final MediaType mMediaType; | |
| 35 | private final Set<String> mExtensions; | |
| 36 | ||
| 37 | /** | |
| 38 | * Several media types have only one corresponding standard file name | |
| 39 | * extension; this constructor calls {@link MediaType#getSubtype()} to obtain | |
| 40 | * said extension. Some {@link MediaType}s have a single extension but their | |
| 41 | * assigned IANA name differs (e.g., {@code svg} maps to {@code svg+xml}) | |
| 42 | * and thus must not use this constructor. | |
| 43 | * | |
| 44 | * @param mediaType The {@link MediaType} containing only one extension. | |
| 45 | */ | |
| 46 | MediaTypeExtensions( final MediaType mediaType ) { | |
| 47 | this( mediaType, of( mediaType.getSubtype() ) ); | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Constructs an association of file name extensions to a single {@link | |
| 52 | * MediaType}. | |
| 53 | * | |
| 54 | * @param mediaType The {@link MediaType} to associate with the given | |
| 55 | * file name extensions. | |
| 56 | * @param extensions The file name extensions used to lookup a corresponding | |
| 57 | * {@link MediaType}. | |
| 58 | */ | |
| 59 | MediaTypeExtensions( | |
| 60 | final MediaType mediaType, final Set<String> extensions ) { | |
| 61 | assert mediaType != null; | |
| 62 | assert extensions != null; | |
| 63 | assert !extensions.isEmpty(); | |
| 64 | ||
| 65 | mMediaType = mediaType; | |
| 66 | mExtensions = extensions; | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Returns the {@link MediaType} associated with the given file name | |
| 71 | * extension. The extension must not contain a period. | |
| 72 | * | |
| 73 | * @param extension File name extension, case insensitive, {@code null}-safe. | |
| 74 | * @return The associated {@link MediaType} as defined by IANA. | |
| 75 | */ | |
| 76 | static MediaType getMediaType( final String extension ) { | |
| 77 | final var sanitized = sanitize( extension ); | |
| 78 | ||
| 79 | for( final var mediaType : MediaTypeExtensions.values() ) { | |
| 80 | if( mediaType.isType( sanitized ) ) { | |
| 81 | return mediaType.getMediaType(); | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | return UNDEFINED; | |
| 86 | } | |
| 87 | ||
| 88 | private boolean isType( final String sanitized ) { | |
| 89 | for( final var extension : mExtensions ) { | |
| 90 | if( extension.equalsIgnoreCase( sanitized ) ) { | |
| 91 | return true; | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | return false; | |
| 96 | } | |
| 97 | ||
| 98 | private static String sanitize( final String extension ) { | |
| 99 | return extension == null ? "" : extension.toLowerCase(); | |
| 100 | } | |
| 101 | ||
| 102 | private MediaType getMediaType() { | |
| 103 | return mMediaType; | |
| 104 | } | |
| 105 | } | |
| 1 | 106 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.predicates; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.io.FileInputStream; | |
| 32 | import java.io.FileOutputStream; | |
| 33 | import java.util.*; | |
| 34 | import java.util.prefs.AbstractPreferences; | |
| 35 | import java.util.prefs.BackingStoreException; | |
| 36 | ||
| 37 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 38 | ||
| 39 | /** | |
| 40 | * Preferences implementation that stores to a user-defined file. Local file | |
| 41 | * storage is preferred over a certain operating system's monolithic trash heap | |
| 42 | * called a registry. When the OS is locked down, the default Preferences | |
| 43 | * implementation will try to write to the registry and fail due to permissions | |
| 44 | * problems. This class sidesteps the issue entirely by writing to the user's | |
| 45 | * home directory, where permissions should be a bit more lax. | |
| 46 | */ | |
| 47 | public class FilePreferences extends AbstractPreferences { | |
| 48 | ||
| 49 | private final Map<String, String> mRoot = new TreeMap<>(); | |
| 50 | private final Map<String, FilePreferences> mChildren = new TreeMap<>(); | |
| 51 | private boolean mRemoved; | |
| 52 | ||
| 53 | private final Object mMutex = new Object(); | |
| 54 | ||
| 55 | public FilePreferences( | |
| 56 | final AbstractPreferences parent, final String name ) { | |
| 57 | super( parent, name ); | |
| 58 | ||
| 59 | try { | |
| 60 | sync(); | |
| 61 | } catch( final BackingStoreException ex ) { | |
| 62 | clue( ex ); | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | protected void putSpi( final String key, final String value ) { | |
| 68 | synchronized( mMutex ) { | |
| 69 | mRoot.put( key, value ); | |
| 70 | } | |
| 71 | ||
| 72 | try { | |
| 73 | flush(); | |
| 74 | } catch( final BackingStoreException ex ) { | |
| 75 | clue( ex ); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | protected String getSpi( final String key ) { | |
| 81 | synchronized( mMutex ) { | |
| 82 | return mRoot.get( key ); | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | protected void removeSpi( final String key ) { | |
| 88 | synchronized( mMutex ) { | |
| 89 | mRoot.remove( key ); | |
| 90 | } | |
| 91 | ||
| 92 | try { | |
| 93 | flush(); | |
| 94 | } catch( final BackingStoreException ex ) { | |
| 95 | clue( ex ); | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | @Override | |
| 100 | protected void removeNodeSpi() throws BackingStoreException { | |
| 101 | mRemoved = true; | |
| 102 | flush(); | |
| 103 | } | |
| 104 | ||
| 105 | @Override | |
| 106 | protected String[] keysSpi() { | |
| 107 | synchronized( mMutex ) { | |
| 108 | return mRoot.keySet().toArray( new String[ 0 ] ); | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | @Override | |
| 113 | protected String[] childrenNamesSpi() { | |
| 114 | return mChildren.keySet().toArray( new String[ 0 ] ); | |
| 115 | } | |
| 116 | ||
| 117 | @Override | |
| 118 | protected FilePreferences childSpi( final String name ) { | |
| 119 | FilePreferences child = mChildren.get( name ); | |
| 120 | ||
| 121 | if( child == null || child.isRemoved() ) { | |
| 122 | child = new FilePreferences( this, name ); | |
| 123 | mChildren.put( name, child ); | |
| 124 | } | |
| 125 | ||
| 126 | return child; | |
| 127 | } | |
| 128 | ||
| 129 | @Override | |
| 130 | protected void syncSpi() { | |
| 131 | if( isRemoved() ) { | |
| 132 | return; | |
| 133 | } | |
| 134 | ||
| 135 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 136 | ||
| 137 | if( !file.exists() ) { | |
| 138 | return; | |
| 139 | } | |
| 140 | ||
| 141 | synchronized( mMutex ) { | |
| 142 | final Properties p = new Properties(); | |
| 143 | ||
| 144 | try( final var inputStream = new FileInputStream( file ) ) { | |
| 145 | p.load( inputStream ); | |
| 146 | ||
| 147 | final String path = getPath(); | |
| 148 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 149 | ||
| 150 | while( propertyNames.hasMoreElements() ) { | |
| 151 | final String propKey = (String) propertyNames.nextElement(); | |
| 152 | ||
| 153 | if( propKey.startsWith( path ) ) { | |
| 154 | final String subKey = propKey.substring( path.length() ); | |
| 155 | ||
| 156 | // Only load immediate descendants | |
| 157 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 158 | mRoot.put( subKey, p.getProperty( propKey ) ); | |
| 159 | } | |
| 160 | } | |
| 161 | } | |
| 162 | } catch( final Exception ex ) { | |
| 163 | clue( ex ); | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | private String getPath() { | |
| 169 | final FilePreferences parent = (FilePreferences) parent(); | |
| 170 | ||
| 171 | return parent == null ? "" : parent.getPath() + name() + '.'; | |
| 172 | } | |
| 173 | ||
| 174 | @Override | |
| 175 | protected void flushSpi() { | |
| 176 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 177 | ||
| 178 | synchronized( mMutex ) { | |
| 179 | final Properties p = new Properties(); | |
| 180 | ||
| 181 | try { | |
| 182 | final String path = getPath(); | |
| 183 | ||
| 184 | if( file.exists() ) { | |
| 185 | try( final var fis = new FileInputStream( file ) ) { | |
| 186 | p.load( fis ); | |
| 187 | } | |
| 188 | ||
| 189 | final List<String> toRemove = new ArrayList<>(); | |
| 190 | ||
| 191 | // Make a list of all direct children of this node to be removed | |
| 192 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 193 | ||
| 194 | while( propertyNames.hasMoreElements() ) { | |
| 195 | final String propKey = (String) propertyNames.nextElement(); | |
| 196 | if( propKey.startsWith( path ) ) { | |
| 197 | final String subKey = propKey.substring( path.length() ); | |
| 198 | ||
| 199 | // Only do immediate descendants | |
| 200 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 201 | toRemove.add( propKey ); | |
| 202 | } | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | // Remove them now that the enumeration is done with | |
| 207 | for( final String propKey : toRemove ) { | |
| 208 | p.remove( propKey ); | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | // If this node hasn't been removed, add back in any values | |
| 213 | if( !mRemoved ) { | |
| 214 | for( final String s : mRoot.keySet() ) { | |
| 215 | p.setProperty( path + s, mRoot.get( s ) ); | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | try( final var fos = new FileOutputStream( file ) ) { | |
| 220 | p.store( fos, "FilePreferences" ); | |
| 221 | } | |
| 222 | } catch( final Exception ex ) { | |
| 223 | clue( ex ); | |
| 224 | } | |
| 225 | } | |
| 226 | } | |
| 227 | } | |
| 228 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.util.prefs.Preferences; | |
| 32 | import java.util.prefs.PreferencesFactory; | |
| 33 | ||
| 34 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | |
| 35 | import static java.io.File.separator; | |
| 36 | import static java.lang.System.getProperty; | |
| 37 | ||
| 38 | /** | |
| 39 | * PreferencesFactory implementation that stores the preferences in a | |
| 40 | * user-defined file. Usage: | |
| 41 | * <pre> | |
| 42 | * System.setProperty( "java.util.prefs.PreferencesFactory", | |
| 43 | * FilePreferencesFactory.class.getName() ); | |
| 44 | * </pre> | |
| 45 | */ | |
| 46 | public class FilePreferencesFactory implements PreferencesFactory { | |
| 47 | ||
| 48 | private static File sPreferencesFile; | |
| 49 | private Preferences rootPreferences; | |
| 50 | ||
| 51 | @Override | |
| 52 | public Preferences systemRoot() { | |
| 53 | return userRoot(); | |
| 54 | } | |
| 55 | ||
| 56 | @Override | |
| 57 | public Preferences userRoot() { | |
| 58 | final var prefs = rootPreferences; | |
| 59 | ||
| 60 | if( prefs == null ) { | |
| 61 | rootPreferences = new FilePreferences( null, "" ); | |
| 62 | } | |
| 63 | ||
| 64 | return rootPreferences; | |
| 65 | } | |
| 66 | ||
| 67 | public static File getPreferencesFile() { | |
| 68 | final var prefs = sPreferencesFile; | |
| 69 | ||
| 70 | if( prefs == null ) { | |
| 71 | sPreferencesFile = new File( getPreferencesFilename() ).getAbsoluteFile(); | |
| 72 | } | |
| 73 | ||
| 74 | return sPreferencesFile; | |
| 75 | } | |
| 76 | ||
| 77 | public static String getPreferencesFilename() { | |
| 78 | final var filename = getProperty( "application.name", APP_TITLE_LOWERCASE ); | |
| 79 | return getProperty( "user.home" ) + separator + '.' + filename; | |
| 80 | } | |
| 81 | } | |
| 82 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import javafx.beans.property.SimpleObjectProperty; | |
| 5 | ||
| 6 | import java.io.File; | |
| 7 | ||
| 8 | public class FileProperty extends SimpleObjectProperty<File> { | |
| 9 | public FileProperty( final File file ) { | |
| 10 | super( file ); | |
| 11 | } | |
| 12 | ||
| 13 | public void setValue( final String filename ) { | |
| 14 | setValue( new File( filename ) ); | |
| 15 | } | |
| 16 | } | |
| 1 | 17 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | /** | |
| 5 | * Responsible for creating a type hierarchy of preference storage keys. | |
| 6 | */ | |
| 7 | public class Key { | |
| 8 | private final Key mParent; | |
| 9 | private final String mName; | |
| 10 | ||
| 11 | private Key( final Key parent, final String name ) { | |
| 12 | mParent = parent; | |
| 13 | mName = name; | |
| 14 | } | |
| 15 | ||
| 16 | /** | |
| 17 | * Returns a new key with no parent. | |
| 18 | * | |
| 19 | * @param name The key name, never {@code null}. | |
| 20 | * @return The new {@link Key} instance with a name but no parent. | |
| 21 | */ | |
| 22 | public static Key key( final String name ) { | |
| 23 | assert name != null && !name.isEmpty(); | |
| 24 | return key( null, name ); | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Returns a new key with a given parent. | |
| 29 | * | |
| 30 | * @param parent The parent of this {@link Key}, or {@code null} if this is | |
| 31 | * the topmost key in the chain. | |
| 32 | * @param name The key name, never {@code null}. | |
| 33 | * @return The new {@link Key} instance with a name and parent. | |
| 34 | */ | |
| 35 | public static Key key( final Key parent, final String name ) { | |
| 36 | assert name != null && !name.isEmpty(); | |
| 37 | return new Key( parent, name ); | |
| 38 | } | |
| 39 | ||
| 40 | private Key parent() { | |
| 41 | return mParent; | |
| 42 | } | |
| 43 | ||
| 44 | private String name() { | |
| 45 | return mName; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Returns a dot-separated path representing the key's name. | |
| 50 | * | |
| 51 | * @return The recursively derived dot-separated key name. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public String toString() { | |
| 55 | return parent() == null ? name() : parent().toString() + '.' + name(); | |
| 56 | } | |
| 57 | } | |
| 1 | 58 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import javafx.beans.property.SimpleListProperty; | |
| 5 | import javafx.beans.property.SimpleObjectProperty; | |
| 6 | import javafx.collections.ObservableList; | |
| 7 | ||
| 8 | import java.util.LinkedHashMap; | |
| 9 | import java.util.Locale; | |
| 10 | import java.util.Map; | |
| 11 | import java.util.Objects; | |
| 12 | ||
| 13 | import static com.keenwrite.Constants.LOCALE_DEFAULT; | |
| 14 | import static java.util.Locale.forLanguageTag; | |
| 15 | import static javafx.collections.FXCollections.observableArrayList; | |
| 16 | ||
| 17 | public class LocaleProperty extends SimpleObjectProperty<String> { | |
| 18 | ||
| 19 | /** | |
| 20 | * Lists the locales having fonts that are supported by the application. | |
| 21 | * When the Markdown and preview CSS files are loaded, a general file is | |
| 22 | * first loaded, then a specific file is loaded according to the locale. | |
| 23 | * The specific file overrides font families so that different languages | |
| 24 | * may be presented. | |
| 25 | * <p> | |
| 26 | * Using an instance of {@link LinkedHashMap} preserves display order. | |
| 27 | * </p> | |
| 28 | * <p> | |
| 29 | * See | |
| 30 | * <a href="https://www.oracle.com/java/technologies/javase/jdk12locales.html">JDK 12 Locales</a> | |
| 31 | * for details. | |
| 32 | * </p> | |
| 33 | */ | |
| 34 | private static final Map<String, Locale> sLocales = new LinkedHashMap<>(); | |
| 35 | ||
| 36 | static { | |
| 37 | final String[] tags = { | |
| 38 | "en-Latn-AU", | |
| 39 | "en-Latn-CA", | |
| 40 | "en-Latn-GB", | |
| 41 | "en-Latn-NZ", | |
| 42 | "en-Latn-US", | |
| 43 | "en-Latn-ZA", | |
| 44 | "ja-Jpan-JP", | |
| 45 | "ko-Kore-KR", | |
| 46 | "zh-Hans-CN", | |
| 47 | "zh-Hans-SG", | |
| 48 | "zh-Hant-HK", | |
| 49 | "zh-Hant-TW", | |
| 50 | }; | |
| 51 | ||
| 52 | for( final var tag : tags ) { | |
| 53 | final var locale = forLanguageTag( tag ); | |
| 54 | sLocales.put( locale.getDisplayName(), locale ); | |
| 55 | } | |
| 56 | } | |
| 57 | ||
| 58 | public LocaleProperty( final Locale locale ) { | |
| 59 | super( sanitize( locale ).getDisplayName() ); | |
| 60 | } | |
| 61 | ||
| 62 | public static String parseLocale( final String languageTag ) { | |
| 63 | final var locale = forLanguageTag( languageTag ); | |
| 64 | final var key = getKey( sLocales, locale ); | |
| 65 | return key == null ? LOCALE_DEFAULT.getDisplayName() : key; | |
| 66 | } | |
| 67 | ||
| 68 | public static String toLanguageTag( final String displayName ) { | |
| 69 | return sLocales.getOrDefault( displayName, LOCALE_DEFAULT ).toLanguageTag(); | |
| 70 | } | |
| 71 | ||
| 72 | public Locale toLocale() { | |
| 73 | return sLocales.getOrDefault( getValue(), LOCALE_DEFAULT ); | |
| 74 | } | |
| 75 | ||
| 76 | private static Locale sanitize( final Locale locale ) { | |
| 77 | return locale == null || "und".equalsIgnoreCase( locale.toLanguageTag() ) | |
| 78 | ? LOCALE_DEFAULT | |
| 79 | : locale; | |
| 80 | } | |
| 81 | ||
| 82 | public static ObservableList<String> localeListProperty() { | |
| 83 | return new SimpleListProperty<>( observableArrayList( sLocales.keySet() ) ); | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * Performs an O(n) search through the given map to find the key that is | |
| 88 | * mapped to the given value. A bi-directional map would be faster, but | |
| 89 | * also introduces additional dependencies. This doesn't need to be fast | |
| 90 | * because it happens once, at start up, and there aren't a lot of values. | |
| 91 | * | |
| 92 | * @param map The map containing a key to find based on a value. | |
| 93 | * @param value The value to find within the map. | |
| 94 | * @param <K> The type of key associated with a value. | |
| 95 | * @param <V> The type of value associated with a key. | |
| 96 | * @return The key that corresponds to the given value, or {@code null} if | |
| 97 | * the key is not found. | |
| 98 | */ | |
| 99 | @SuppressWarnings( "SameParameterValue" ) | |
| 100 | private static <K, V> K getKey( final Map<K, V> map, final V value ) { | |
| 101 | for( final var entry : map.entrySet() ) { | |
| 102 | if( Objects.equals( value, entry.getValue() ) ) { | |
| 103 | return entry.getKey(); | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | return null; | |
| 108 | } | |
| 109 | } | |
| 1 | 110 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import java.util.Collections; | |
| 5 | import java.util.HashMap; | |
| 6 | import java.util.Locale; | |
| 7 | import java.util.Map; | |
| 8 | ||
| 9 | import static java.util.Arrays.asList; | |
| 10 | ||
| 11 | /** | |
| 12 | * Responsible for adding an ISO 15924 alpha-4 script code to {@link Locale} | |
| 13 | * instances. This allows all {@link Locale} objects to produce language tags | |
| 14 | * using the same format. | |
| 15 | */ | |
| 16 | public class LocaleScripts { | |
| 17 | /** | |
| 18 | * ISO 15924 alpha-4 script code to represent Latin scripts. | |
| 19 | */ | |
| 20 | private static final String SCRIPT_LATIN = "Latn"; | |
| 21 | ||
| 22 | /** | |
| 23 | * This value is returned when a script hasn't been mapped for an instance of | |
| 24 | * {@link Locale}. | |
| 25 | */ | |
| 26 | private static final Map<String, String> SCRIPT_DEFAULT = m( SCRIPT_LATIN ); | |
| 27 | ||
| 28 | private static final Map<String, Map<String, String>> SCRIPTS = | |
| 29 | new HashMap<>(); | |
| 30 | ||
| 31 | static { | |
| 32 | put( "en", m( "Latn" ) ); | |
| 33 | put( "jp", m( "Jpan" ) ); | |
| 34 | put( "ko", m( "Kore" ) ); | |
| 35 | put( "zh", m( "Hant" ), m( "Hans", "CN", "MN", "MY", "SG" ) ); | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * Adds a script to a given {@link Locale} object. If the given {@link Locale} | |
| 40 | * already has a script, then it is returned unchanged. | |
| 41 | * | |
| 42 | * @param locale The {@link Locale} to update with its associated script. | |
| 43 | * @return The given {@link Locale} with a script included. | |
| 44 | */ | |
| 45 | public static Locale withScript( Locale locale ) { | |
| 46 | assert locale != null; | |
| 47 | ||
| 48 | final var script = locale.getScript(); | |
| 49 | ||
| 50 | if( script == null || script.isBlank() ) { | |
| 51 | final var builder = new Locale.Builder(); | |
| 52 | builder.setLocale( locale ); | |
| 53 | builder.setScript( getScript( locale ) ); | |
| 54 | locale = builder.build(); | |
| 55 | } | |
| 56 | ||
| 57 | return locale; | |
| 58 | } | |
| 59 | ||
| 60 | @SafeVarargs | |
| 61 | private static void put( | |
| 62 | final String language, final Map<String, String>... scripts ) { | |
| 63 | final var merged = new HashMap<String, String>(); | |
| 64 | asList( scripts ).forEach( merged::putAll ); | |
| 65 | SCRIPTS.put( language, merged ); | |
| 66 | } | |
| 67 | ||
| 68 | /** | |
| 69 | * Returns the ISO 15924 alpha-4 script code for the given {@link Locale}. | |
| 70 | * | |
| 71 | * @param locale Language and country are used to find the script code. | |
| 72 | * @return The ISO code for the given locale, or {@link #SCRIPT_LATIN} if | |
| 73 | * no code has been mapped yet. | |
| 74 | */ | |
| 75 | private static String getScript( final Locale locale ) { | |
| 76 | return SCRIPTS.getOrDefault( locale.getLanguage(), SCRIPT_DEFAULT ) | |
| 77 | .getOrDefault( locale.getCountry(), SCRIPT_LATIN ); | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Helper method to instantiate a new {@link Map} having all keys referencing | |
| 82 | * the same value. | |
| 83 | * | |
| 84 | * @param v The value to associate with each key. | |
| 85 | * @param k The keys to associate with the given value. | |
| 86 | * @return A new {@link Map} with all keys referencing the same value. | |
| 87 | */ | |
| 88 | private static Map<String, String> m( final String v, final String... k ) { | |
| 89 | final var map = new HashMap<String, String>(); | |
| 90 | asList( k ).forEach( ( key ) -> map.put( key, v ) ); | |
| 91 | return Collections.unmodifiableMap( map ); | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Helper method to instantiate a new {@link Map} having an empty key | |
| 96 | * referencing the given value. This provides a default value so that | |
| 97 | * an unmapped country code can return a valid script code. | |
| 98 | * | |
| 99 | * @param v The value to associate with an empty key. | |
| 100 | * @return A new {@link Map} with the empty key referencing the given value. | |
| 101 | */ | |
| 102 | private static Map<String, String> m( final String v ) { | |
| 103 | return m( v, "" ); | |
| 104 | } | |
| 105 | } | |
| 1 | 106 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import com.dlsc.formsfx.model.structure.StringField; | |
| 5 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 6 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 7 | import com.dlsc.preferencesfx.model.Category; | |
| 8 | import com.dlsc.preferencesfx.model.Group; | |
| 9 | import com.dlsc.preferencesfx.model.Setting; | |
| 10 | import javafx.beans.property.DoubleProperty; | |
| 11 | import javafx.beans.property.ObjectProperty; | |
| 12 | import javafx.beans.property.StringProperty; | |
| 13 | import javafx.event.EventHandler; | |
| 14 | import javafx.scene.Node; | |
| 15 | import javafx.scene.control.Label; | |
| 16 | ||
| 17 | import java.io.File; | |
| 18 | ||
| 19 | import static com.keenwrite.Constants.ICON_DIALOG; | |
| 20 | import static com.keenwrite.Messages.get; | |
| 21 | import static com.keenwrite.preferences.LocaleProperty.localeListProperty; | |
| 22 | import static com.keenwrite.preferences.Workspace.*; | |
| 23 | ||
| 24 | /** | |
| 25 | * Provides the ability for users to configure their preferences. This links | |
| 26 | * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC. | |
| 27 | */ | |
| 28 | @SuppressWarnings( "SameParameterValue" ) | |
| 29 | public class PreferencesController { | |
| 30 | ||
| 31 | private final Workspace mWorkspace; | |
| 32 | private final PreferencesFx mPreferencesFx; | |
| 33 | ||
| 34 | public PreferencesController( final Workspace workspace ) { | |
| 35 | mWorkspace = workspace; | |
| 36 | ||
| 37 | // All properties must be initialized before creating the dialog. | |
| 38 | mPreferencesFx = createPreferencesFx(); | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Display the user preferences settings dialog (non-modal). | |
| 43 | */ | |
| 44 | public void show() { | |
| 45 | getPreferencesFx().show( false ); | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Call to persist the settings. Strictly speaking, this could watch on | |
| 50 | * all values for external changes then save automatically. | |
| 51 | */ | |
| 52 | public void save() { | |
| 53 | getPreferencesFx().saveSettings(); | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Delegates to the {@link PreferencesFx} event handler for monitoring | |
| 58 | * save events. | |
| 59 | * | |
| 60 | * @param eventHandler The handler to call when the preferences are saved. | |
| 61 | */ | |
| 62 | public void addSaveEventHandler( | |
| 63 | final EventHandler<? super PreferencesFxEvent> eventHandler ) { | |
| 64 | final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED; | |
| 65 | getPreferencesFx().addEventHandler( eventType, eventHandler ); | |
| 66 | } | |
| 67 | ||
| 68 | /** | |
| 69 | * Creates the preferences dialog. | |
| 70 | * <p> | |
| 71 | * TODO: Make this dynamic by iterating over all "Preferences.*" values | |
| 72 | * that follow a particular naming pattern. | |
| 73 | * </p> | |
| 74 | * | |
| 75 | * @return A new instance of preferences for users to edit. | |
| 76 | */ | |
| 77 | @SuppressWarnings( "unchecked" ) | |
| 78 | private PreferencesFx createPreferencesFx() { | |
| 79 | final Setting<StringField, StringProperty> scriptSetting = | |
| 80 | Setting.of( "Script", stringProperty( KEY_R_SCRIPT ) ); | |
| 81 | final StringField field = scriptSetting.getElement(); | |
| 82 | field.multiline( true ); | |
| 83 | ||
| 84 | return PreferencesFx.of( | |
| 85 | new XmlStorageHandler(), | |
| 86 | Category.of( | |
| 87 | get( KEY_R ), | |
| 88 | Group.of( | |
| 89 | get( KEY_R_DIR ), | |
| 90 | Setting.of( label( KEY_R_DIR, | |
| 91 | stringProperty( KEY_DEF_DELIM_BEGAN ).get(), | |
| 92 | stringProperty( KEY_DEF_DELIM_ENDED ).get() ) ), | |
| 93 | Setting.of( title( KEY_R_DIR ), fileProperty( KEY_R_DIR ), true ) | |
| 94 | ), | |
| 95 | Group.of( | |
| 96 | get( KEY_R_SCRIPT ), | |
| 97 | Setting.of( label( KEY_R_SCRIPT ) ), | |
| 98 | scriptSetting | |
| 99 | ), | |
| 100 | Group.of( | |
| 101 | get( KEY_R_DELIM_BEGAN ), | |
| 102 | Setting.of( label( KEY_R_DELIM_BEGAN ) ), | |
| 103 | Setting.of( title( KEY_R_DELIM_BEGAN ), | |
| 104 | stringProperty( KEY_R_DELIM_BEGAN ) ) | |
| 105 | ), | |
| 106 | Group.of( | |
| 107 | get( KEY_R_DELIM_ENDED ), | |
| 108 | Setting.of( label( KEY_R_DELIM_ENDED ) ), | |
| 109 | Setting.of( title( KEY_R_DELIM_ENDED ), | |
| 110 | stringProperty( KEY_R_DELIM_ENDED ) ) | |
| 111 | ) | |
| 112 | ), | |
| 113 | Category.of( | |
| 114 | get( KEY_IMAGES ), | |
| 115 | Group.of( | |
| 116 | get( KEY_IMAGES_DIR ), | |
| 117 | Setting.of( label( KEY_IMAGES_DIR ) ), | |
| 118 | Setting.of( title( KEY_IMAGES_DIR ), | |
| 119 | fileProperty( KEY_IMAGES_DIR ), | |
| 120 | true ) | |
| 121 | ), | |
| 122 | Group.of( | |
| 123 | get( KEY_IMAGES_ORDER ), | |
| 124 | Setting.of( label( KEY_IMAGES_ORDER ) ), | |
| 125 | Setting.of( title( KEY_IMAGES_ORDER ), | |
| 126 | stringProperty( KEY_IMAGES_ORDER ) ) | |
| 127 | ) | |
| 128 | ), | |
| 129 | Category.of( | |
| 130 | get( KEY_DEF ), | |
| 131 | Group.of( | |
| 132 | get( KEY_DEF_PATH ), | |
| 133 | Setting.of( label( KEY_DEF_PATH ) ), | |
| 134 | Setting.of( title( KEY_DEF_PATH ), | |
| 135 | fileProperty( KEY_DEF_PATH ), | |
| 136 | false ) | |
| 137 | ), | |
| 138 | Group.of( | |
| 139 | get( KEY_DEF_DELIM_BEGAN ), | |
| 140 | Setting.of( label( KEY_DEF_DELIM_BEGAN ) ), | |
| 141 | Setting.of( title( KEY_DEF_DELIM_BEGAN ), | |
| 142 | stringProperty( KEY_DEF_DELIM_BEGAN ) ) | |
| 143 | ), | |
| 144 | Group.of( | |
| 145 | get( KEY_DEF_DELIM_ENDED ), | |
| 146 | Setting.of( label( KEY_DEF_DELIM_ENDED ) ), | |
| 147 | Setting.of( title( KEY_DEF_DELIM_ENDED ), | |
| 148 | stringProperty( KEY_DEF_DELIM_ENDED ) ) | |
| 149 | ) | |
| 150 | ), | |
| 151 | Category.of( | |
| 152 | get( KEY_UI_FONT ), | |
| 153 | Group.of( | |
| 154 | get( KEY_UI_FONT_PREVIEW_SIZE ), | |
| 155 | Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ), | |
| 156 | Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ), | |
| 157 | doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ) | |
| 158 | ), | |
| 159 | Group.of( | |
| 160 | get( KEY_UI_FONT_EDITOR_SIZE ), | |
| 161 | Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ), | |
| 162 | Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ), | |
| 163 | doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ) | |
| 164 | ), | |
| 165 | Group.of( | |
| 166 | get( KEY_UI_FONT_LOCALE ), | |
| 167 | Setting.of( label( KEY_UI_FONT_LOCALE ) ), | |
| 168 | Setting.of( title( KEY_UI_FONT_LOCALE ), | |
| 169 | localeListProperty(), | |
| 170 | localeProperty( KEY_UI_FONT_LOCALE ) ) | |
| 171 | ) | |
| 172 | ) | |
| 173 | ).instantPersistent( false ).dialogIcon( ICON_DIALOG ); | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * Creates a label for the given key after interpolating its value. | |
| 178 | * | |
| 179 | * @param key The key to find in the resource bundle. | |
| 180 | * @return The value of the key as a label. | |
| 181 | */ | |
| 182 | private Node label( final Key key ) { | |
| 183 | return label( key, (String[]) null ); | |
| 184 | } | |
| 185 | ||
| 186 | private Node label( final Key key, final String... values ) { | |
| 187 | return new Label( get( key.toString() + ".desc", (Object[]) values ) ); | |
| 188 | } | |
| 189 | ||
| 190 | private String title( final Key key ) { | |
| 191 | return get( key.toString() + ".title" ); | |
| 192 | } | |
| 193 | ||
| 194 | private ObjectProperty<File> fileProperty( final Key key ) { | |
| 195 | return mWorkspace.fileProperty( key ); | |
| 196 | } | |
| 197 | ||
| 198 | private StringProperty stringProperty( final Key key ) { | |
| 199 | return mWorkspace.stringProperty( key ); | |
| 200 | } | |
| 201 | ||
| 202 | @SuppressWarnings( "SameParameterValue" ) | |
| 203 | private DoubleProperty doubleProperty( final Key key ) { | |
| 204 | return mWorkspace.doubleProperty( key ); | |
| 205 | } | |
| 206 | ||
| 207 | private ObjectProperty<String> localeProperty( final Key key ) { | |
| 208 | return mWorkspace.localeProperty( key ); | |
| 209 | } | |
| 210 | ||
| 211 | private PreferencesFx getPreferencesFx() { | |
| 212 | return mPreferencesFx; | |
| 213 | } | |
| 214 | } | |
| 1 | 215 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import com.dlsc.formsfx.model.structure.StringField; | |
| 31 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 32 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 33 | import com.dlsc.preferencesfx.model.Category; | |
| 34 | import com.dlsc.preferencesfx.model.Group; | |
| 35 | import com.dlsc.preferencesfx.model.Setting; | |
| 36 | import javafx.beans.property.*; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.scene.Node; | |
| 39 | import javafx.scene.control.Label; | |
| 40 | ||
| 41 | import java.io.File; | |
| 42 | import java.nio.file.Path; | |
| 43 | ||
| 44 | import static com.keenwrite.Constants.*; | |
| 45 | import static com.keenwrite.Messages.get; | |
| 46 | ||
| 47 | /** | |
| 48 | * Responsible for user preferences that can be changed from the GUI. The | |
| 49 | * settings are displayed and persisted using {@link PreferencesFx}. | |
| 50 | */ | |
| 51 | public class UserPreferences { | |
| 52 | /** | |
| 53 | * Implementation of the initialization-on-demand holder design pattern, | |
| 54 | * an for a lazy-loaded singleton. In all versions of Java, the idiom enables | |
| 55 | * a safe, highly concurrent lazy initialization of static fields with good | |
| 56 | * performance. The implementation relies upon the initialization phase of | |
| 57 | * execution within the Java Virtual Machine (JVM) as specified by the Java | |
| 58 | * Language Specification. When the class {@link UserPreferencesContainer} | |
| 59 | * is loaded, its initialization completes trivially because there are no | |
| 60 | * static variables to initialize. | |
| 61 | * <p> | |
| 62 | * The static class definition {@link UserPreferencesContainer} within the | |
| 63 | * {@link UserPreferences} is not initialized until such time that | |
| 64 | * {@link UserPreferencesContainer} must be executed. The static | |
| 65 | * {@link UserPreferencesContainer} class executes when | |
| 66 | * {@link #getInstance} is called. The first call will trigger loading and | |
| 67 | * initialization of the {@link UserPreferencesContainer} thereby | |
| 68 | * instantiating the {@link #INSTANCE}. | |
| 69 | * </p> | |
| 70 | * <p> | |
| 71 | * This indirection is necessary because the {@link UserPreferences} class | |
| 72 | * references {@link PreferencesFx}, which must not be instantiated until the | |
| 73 | * UI is ready. | |
| 74 | * </p> | |
| 75 | */ | |
| 76 | private static class UserPreferencesContainer { | |
| 77 | private static final UserPreferences INSTANCE = new UserPreferences(); | |
| 78 | } | |
| 79 | ||
| 80 | public static UserPreferences getInstance() { | |
| 81 | return UserPreferencesContainer.INSTANCE; | |
| 82 | } | |
| 83 | ||
| 84 | private final PreferencesFx mPreferencesFx; | |
| 85 | ||
| 86 | private final ObjectProperty<File> mPropRDirectory; | |
| 87 | private final StringProperty mPropRScript; | |
| 88 | private final ObjectProperty<File> mPropImagesDirectory; | |
| 89 | private final StringProperty mPropImagesOrder; | |
| 90 | private final ObjectProperty<File> mPropDefinitionPath; | |
| 91 | private final StringProperty mPropRDelimBegan; | |
| 92 | private final StringProperty mPropRDelimEnded; | |
| 93 | private final StringProperty mPropDefDelimBegan; | |
| 94 | private final StringProperty mPropDefDelimEnded; | |
| 95 | private final IntegerProperty mPropFontsSizeEditor; | |
| 96 | ||
| 97 | private UserPreferences() { | |
| 98 | mPropRDirectory = simpleFile( USER_DIRECTORY ); | |
| 99 | mPropRScript = new SimpleStringProperty( "" ); | |
| 100 | ||
| 101 | mPropImagesDirectory = simpleFile( USER_DIRECTORY ); | |
| 102 | mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ); | |
| 103 | ||
| 104 | mPropDefinitionPath = simpleFile( | |
| 105 | getSetting( "file.definition.default", DEFINITION_NAME ) | |
| 106 | ); | |
| 107 | ||
| 108 | mPropDefDelimBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ); | |
| 109 | mPropDefDelimEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ); | |
| 110 | ||
| 111 | mPropRDelimBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ); | |
| 112 | mPropRDelimEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ); | |
| 113 | ||
| 114 | mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR ); | |
| 115 | ||
| 116 | // All properties must be initialized before creating the dialog. | |
| 117 | mPreferencesFx = createPreferencesFx(); | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Display the user preferences settings dialog (non-modal). | |
| 122 | */ | |
| 123 | public void show() { | |
| 124 | getPreferencesFx().show( false ); | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Call to persist the settings. Strictly speaking, this could watch on | |
| 129 | * all values for external changes then save automatically. | |
| 130 | */ | |
| 131 | public void save() { | |
| 132 | getPreferencesFx().saveSettings(); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Creates the preferences dialog. | |
| 137 | * <p> | |
| 138 | * TODO: Make this dynamic by iterating over all "Preferences.*" values | |
| 139 | * that follow a particular naming pattern. | |
| 140 | * </p> | |
| 141 | * | |
| 142 | * @return A new instance of preferences for users to edit. | |
| 143 | */ | |
| 144 | @SuppressWarnings("unchecked") | |
| 145 | private PreferencesFx createPreferencesFx() { | |
| 146 | final Setting<StringField, StringProperty> scriptSetting = | |
| 147 | Setting.of( "Script", mPropRScript ); | |
| 148 | final StringField field = scriptSetting.getElement(); | |
| 149 | field.multiline( true ); | |
| 150 | ||
| 151 | return PreferencesFx.of( | |
| 152 | UserPreferences.class, | |
| 153 | Category.of( | |
| 154 | get( "Preferences.r" ), | |
| 155 | Group.of( | |
| 156 | get( "Preferences.r.directory" ), | |
| 157 | Setting.of( label( "Preferences.r.directory.desc", false ) ), | |
| 158 | Setting.of( "Directory", mPropRDirectory, true ) | |
| 159 | ), | |
| 160 | Group.of( | |
| 161 | get( "Preferences.r.script" ), | |
| 162 | Setting.of( label( "Preferences.r.script.desc" ) ), | |
| 163 | scriptSetting | |
| 164 | ), | |
| 165 | Group.of( | |
| 166 | get( "Preferences.r.delimiter.began" ), | |
| 167 | Setting.of( label( "Preferences.r.delimiter.began.desc" ) ), | |
| 168 | Setting.of( "Opening", mPropRDelimBegan ) | |
| 169 | ), | |
| 170 | Group.of( | |
| 171 | get( "Preferences.r.delimiter.ended" ), | |
| 172 | Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ), | |
| 173 | Setting.of( "Closing", mPropRDelimEnded ) | |
| 174 | ) | |
| 175 | ), | |
| 176 | Category.of( | |
| 177 | get( "Preferences.images" ), | |
| 178 | Group.of( | |
| 179 | get( "Preferences.images.directory" ), | |
| 180 | Setting.of( label( "Preferences.images.directory.desc" ) ), | |
| 181 | Setting.of( "Directory", mPropImagesDirectory, true ) | |
| 182 | ), | |
| 183 | Group.of( | |
| 184 | get( "Preferences.images.suffixes" ), | |
| 185 | Setting.of( label( "Preferences.images.suffixes.desc" ) ), | |
| 186 | Setting.of( "Extensions", mPropImagesOrder ) | |
| 187 | ) | |
| 188 | ), | |
| 189 | Category.of( | |
| 190 | get( "Preferences.definitions" ), | |
| 191 | Group.of( | |
| 192 | get( "Preferences.definitions.path" ), | |
| 193 | Setting.of( label( "Preferences.definitions.path.desc" ) ), | |
| 194 | Setting.of( "Path", mPropDefinitionPath, false ) | |
| 195 | ), | |
| 196 | Group.of( | |
| 197 | get( "Preferences.definitions.delimiter.began" ), | |
| 198 | Setting.of( label( | |
| 199 | "Preferences.definitions.delimiter.began.desc" ) ), | |
| 200 | Setting.of( "Opening", mPropDefDelimBegan ) | |
| 201 | ), | |
| 202 | Group.of( | |
| 203 | get( "Preferences.definitions.delimiter.ended" ), | |
| 204 | Setting.of( label( | |
| 205 | "Preferences.definitions.delimiter.ended.desc" ) ), | |
| 206 | Setting.of( "Closing", mPropDefDelimEnded ) | |
| 207 | ) | |
| 208 | ), | |
| 209 | Category.of( | |
| 210 | get( "Preferences.fonts" ), | |
| 211 | Group.of( | |
| 212 | get( "Preferences.fonts.size_editor" ), | |
| 213 | Setting.of( label( "Preferences.fonts.size_editor.desc" ) ), | |
| 214 | Setting.of( "Points", mPropFontsSizeEditor ) | |
| 215 | ) | |
| 216 | ) | |
| 217 | ).instantPersistent( false ) | |
| 218 | .dialogIcon( ICON_DIALOG ); | |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Wraps a {@link File} inside a {@link SimpleObjectProperty}. | |
| 223 | * | |
| 224 | * @param path The file name to use when constructing the {@link File}. | |
| 225 | * @return A new {@link SimpleObjectProperty} instance with a {@link File} | |
| 226 | * that references the given {@code path}. | |
| 227 | */ | |
| 228 | private SimpleObjectProperty<File> simpleFile( final String path ) { | |
| 229 | return new SimpleObjectProperty<>( new File( path ) ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Creates a label for the given key after interpolating its value. | |
| 234 | * | |
| 235 | * @param key The key to find in the resource bundle. | |
| 236 | * @return The value of the key as a label. | |
| 237 | */ | |
| 238 | private Node label( final String key ) { | |
| 239 | return new Label( get( key, true ) ); | |
| 240 | } | |
| 241 | ||
| 242 | /** | |
| 243 | * Creates a label for the given key. | |
| 244 | * | |
| 245 | * @param key The key to find in the resource bundle. | |
| 246 | * @param interpolate {@code true} means to interpolate the value. | |
| 247 | * @return The value of the key, interpolated if {@code interpolate} is | |
| 248 | * {@code true}. | |
| 249 | */ | |
| 250 | @SuppressWarnings("SameParameterValue") | |
| 251 | private Node label( final String key, final boolean interpolate ) { | |
| 252 | return new Label( get( key, interpolate ) ); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Delegates to the {@link PreferencesFx} event handler for monitoring | |
| 257 | * save events. | |
| 258 | * | |
| 259 | * @param eventHandler The handler to call when the preferences are saved. | |
| 260 | */ | |
| 261 | public void addSaveEventHandler( | |
| 262 | final EventHandler<? super PreferencesFxEvent> eventHandler ) { | |
| 263 | final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED; | |
| 264 | getPreferencesFx().addEventHandler( eventType, eventHandler ); | |
| 265 | } | |
| 266 | ||
| 267 | /** | |
| 268 | * Returns the value for a key from the settings properties file. | |
| 269 | * | |
| 270 | * @param key Key within the settings properties file to find. | |
| 271 | * @param value Default value to return if the key is not found. | |
| 272 | * @return The value for the given key from the settings file, or the | |
| 273 | * given {@code value} if no key found. | |
| 274 | */ | |
| 275 | @SuppressWarnings("SameParameterValue") | |
| 276 | private String getSetting( final String key, final String value ) { | |
| 277 | return SETTINGS.getSetting( key, value ); | |
| 278 | } | |
| 279 | ||
| 280 | public ObjectProperty<File> definitionPathProperty() { | |
| 281 | return mPropDefinitionPath; | |
| 282 | } | |
| 283 | ||
| 284 | public Path getDefinitionPath() { | |
| 285 | return definitionPathProperty().getValue().toPath(); | |
| 286 | } | |
| 287 | ||
| 288 | private StringProperty defDelimiterBegan() { | |
| 289 | return mPropDefDelimBegan; | |
| 290 | } | |
| 291 | ||
| 292 | public String getDefDelimiterBegan() { | |
| 293 | return defDelimiterBegan().get(); | |
| 294 | } | |
| 295 | ||
| 296 | private StringProperty defDelimiterEnded() { | |
| 297 | return mPropDefDelimEnded; | |
| 298 | } | |
| 299 | ||
| 300 | public String getDefDelimiterEnded() { | |
| 301 | return defDelimiterEnded().get(); | |
| 302 | } | |
| 303 | ||
| 304 | public ObjectProperty<File> rDirectoryProperty() { | |
| 305 | return mPropRDirectory; | |
| 306 | } | |
| 307 | ||
| 308 | public File getRDirectory() { | |
| 309 | return rDirectoryProperty().getValue(); | |
| 310 | } | |
| 311 | ||
| 312 | public StringProperty rScriptProperty() { | |
| 313 | return mPropRScript; | |
| 314 | } | |
| 315 | ||
| 316 | public String getRScript() { | |
| 317 | return rScriptProperty().getValue(); | |
| 318 | } | |
| 319 | ||
| 320 | private StringProperty rDelimiterBegan() { | |
| 321 | return mPropRDelimBegan; | |
| 322 | } | |
| 323 | ||
| 324 | public String getRDelimiterBegan() { | |
| 325 | return rDelimiterBegan().get(); | |
| 326 | } | |
| 327 | ||
| 328 | private StringProperty rDelimiterEnded() { | |
| 329 | return mPropRDelimEnded; | |
| 330 | } | |
| 331 | ||
| 332 | public String getRDelimiterEnded() { | |
| 333 | return rDelimiterEnded().get(); | |
| 334 | } | |
| 335 | ||
| 336 | private ObjectProperty<File> imagesDirectoryProperty() { | |
| 337 | return mPropImagesDirectory; | |
| 338 | } | |
| 339 | ||
| 340 | public File getImagesDirectory() { | |
| 341 | return imagesDirectoryProperty().getValue(); | |
| 342 | } | |
| 343 | ||
| 344 | private StringProperty imagesOrderProperty() { | |
| 345 | return mPropImagesOrder; | |
| 346 | } | |
| 347 | ||
| 348 | public String getImagesOrder() { | |
| 349 | return imagesOrderProperty().getValue(); | |
| 350 | } | |
| 351 | ||
| 352 | public IntegerProperty fontsSizeEditorProperty() { | |
| 353 | return mPropFontsSizeEditor; | |
| 354 | } | |
| 355 | ||
| 356 | /** | |
| 357 | * Returns the preferred font size of the text editor. | |
| 358 | * | |
| 359 | * @return A non-negative integer, in points. | |
| 360 | */ | |
| 361 | public int getFontsSizeEditor() { | |
| 362 | return mPropFontsSizeEditor.intValue(); | |
| 363 | } | |
| 364 | ||
| 365 | private PreferencesFx getPreferencesFx() { | |
| 366 | return mPreferencesFx; | |
| 367 | } | |
| 368 | } | |
| 369 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import com.keenwrite.Constants; | |
| 5 | import com.keenwrite.sigils.Tokens; | |
| 6 | import javafx.application.Platform; | |
| 7 | import javafx.beans.property.*; | |
| 8 | import org.apache.commons.configuration2.XMLConfiguration; | |
| 9 | import org.apache.commons.configuration2.builder.fluent.Configurations; | |
| 10 | import org.apache.commons.configuration2.io.FileHandler; | |
| 11 | ||
| 12 | import java.io.File; | |
| 13 | import java.util.HashSet; | |
| 14 | import java.util.LinkedHashSet; | |
| 15 | import java.util.Map; | |
| 16 | import java.util.Set; | |
| 17 | import java.util.function.BiConsumer; | |
| 18 | import java.util.function.BooleanSupplier; | |
| 19 | import java.util.function.Consumer; | |
| 20 | import java.util.function.Function; | |
| 21 | ||
| 22 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | |
| 23 | import static com.keenwrite.Constants.*; | |
| 24 | import static com.keenwrite.Launcher.getVersion; | |
| 25 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 26 | import static com.keenwrite.preferences.Key.key; | |
| 27 | import static java.util.Map.entry; | |
| 28 | import static javafx.application.Platform.runLater; | |
| 29 | import static javafx.collections.FXCollections.observableSet; | |
| 30 | ||
| 31 | /** | |
| 32 | * Responsible for defining behaviours for separate projects. A workspace has | |
| 33 | * the ability to save and restore a session, including the window dimensions, | |
| 34 | * tab setup, files, and user preferences. | |
| 35 | * <p> | |
| 36 | * The configuration must support hierarchical (nested) configuration nodes | |
| 37 | * to persist the user interface state. Although possible with a flat | |
| 38 | * configuration file, it's not nearly as simple or elegant. | |
| 39 | * </p> | |
| 40 | * <p> | |
| 41 | * Neither JSON nor HOCON support schema validation and versioning, which makes | |
| 42 | * XML the more suitable configuration file format. Schema validation and | |
| 43 | * versioning provide future-proofing and ease of reading and upgrading previous | |
| 44 | * versions of the configuration file. | |
| 45 | * </p> | |
| 46 | * <p> | |
| 47 | * Persistent preferences may be set directly by the user or indirectly by | |
| 48 | * the act of using the application. | |
| 49 | * </p> | |
| 50 | * <p> | |
| 51 | * Note the following definitions: | |
| 52 | * </p> | |
| 53 | * <dl> | |
| 54 | * <dt>File</dt> | |
| 55 | * <dd>References a file name (no path), path, or directory.</dd> | |
| 56 | * <dt>Path</dt> | |
| 57 | * <dd>Fully qualified file name, which includes all parent directories.</dd> | |
| 58 | * <dt>Dir</dt> | |
| 59 | * <dd>Directory without a file name ({@link File#isDirectory()} is true) | |
| 60 | * .</dd> | |
| 61 | * </dl> | |
| 62 | */ | |
| 63 | public class Workspace { | |
| 64 | private static final Key KEY_ROOT = key( "workspace" ); | |
| 65 | ||
| 66 | public static final Key KEY_META = key( KEY_ROOT, "meta" ); | |
| 67 | public static final Key KEY_META_NAME = key( KEY_META, "name" ); | |
| 68 | public static final Key KEY_META_VERSION = key( KEY_META, "version" ); | |
| 69 | ||
| 70 | public static final Key KEY_R = key( KEY_ROOT, "r" ); | |
| 71 | public static final Key KEY_R_SCRIPT = key( KEY_R, "script" ); | |
| 72 | public static final Key KEY_R_DIR = key( KEY_R, "dir" ); | |
| 73 | public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" ); | |
| 74 | public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" ); | |
| 75 | public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" ); | |
| 76 | ||
| 77 | public static final Key KEY_IMAGES = key( KEY_ROOT, "images" ); | |
| 78 | public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" ); | |
| 79 | public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" ); | |
| 80 | ||
| 81 | public static final Key KEY_DEF = key( KEY_ROOT, "definition" ); | |
| 82 | public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" ); | |
| 83 | public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" ); | |
| 84 | public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" ); | |
| 85 | public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" ); | |
| 86 | ||
| 87 | //@formatter:off | |
| 88 | public static final Key KEY_UI = key( KEY_ROOT, "ui" ); | |
| 89 | ||
| 90 | public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" ); | |
| 91 | public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" ); | |
| 92 | public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" ); | |
| 93 | public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" ); | |
| 94 | ||
| 95 | public static final Key KEY_UI_FILES = key( KEY_UI, "files" ); | |
| 96 | public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" ); | |
| 97 | ||
| 98 | public static final Key KEY_UI_FONT = key( KEY_UI, "font" ); | |
| 99 | public static final Key KEY_UI_FONT_LOCALE = key( KEY_UI_FONT, "locale" ); | |
| 100 | public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" ); | |
| 101 | public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" ); | |
| 102 | public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" ); | |
| 103 | public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" ); | |
| 104 | ||
| 105 | public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" ); | |
| 106 | public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" ); | |
| 107 | public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" ); | |
| 108 | public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" ); | |
| 109 | public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" ); | |
| 110 | public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" ); | |
| 111 | public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" ); | |
| 112 | ||
| 113 | private final Map<Key, Property<?>> VALUES = Map.ofEntries( | |
| 114 | entry( KEY_META_VERSION, new SimpleStringProperty( getVersion() ) ), | |
| 115 | entry( KEY_META_NAME, new SimpleStringProperty( "default" ) ), | |
| 116 | ||
| 117 | entry( KEY_R_SCRIPT, new SimpleStringProperty( "" ) ), | |
| 118 | entry( KEY_R_DIR, new FileProperty( USER_DIRECTORY ) ), | |
| 119 | entry( KEY_R_DELIM_BEGAN, new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ) ), | |
| 120 | entry( KEY_R_DELIM_ENDED, new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ) ), | |
| 121 | ||
| 122 | entry( KEY_IMAGES_DIR, new FileProperty( USER_DIRECTORY ) ), | |
| 123 | entry( KEY_IMAGES_ORDER, new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ) ), | |
| 124 | ||
| 125 | entry( KEY_DEF_PATH, new FileProperty( DEFINITION_DEFAULT ) ), | |
| 126 | entry( KEY_DEF_DELIM_BEGAN, new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ), | |
| 127 | entry( KEY_DEF_DELIM_ENDED, new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ) ), | |
| 128 | ||
| 129 | entry( KEY_UI_RECENT_DIR, new FileProperty( USER_DIRECTORY ) ), | |
| 130 | entry( KEY_UI_RECENT_DOCUMENT, new FileProperty( DOCUMENT_DEFAULT ) ), | |
| 131 | entry( KEY_UI_RECENT_DEFINITION, new FileProperty( DEFINITION_DEFAULT ) ), | |
| 132 | ||
| 133 | entry( KEY_UI_FONT_LOCALE, new LocaleProperty( LOCALE_DEFAULT ) ), | |
| 134 | entry( KEY_UI_FONT_EDITOR_SIZE, new SimpleDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ), | |
| 135 | entry( KEY_UI_FONT_PREVIEW_SIZE, new SimpleDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ), | |
| 136 | ||
| 137 | entry( KEY_UI_WINDOW_X, new SimpleDoubleProperty( WINDOW_X_DEFAULT ) ), | |
| 138 | entry( KEY_UI_WINDOW_Y, new SimpleDoubleProperty( WINDOW_Y_DEFAULT ) ), | |
| 139 | entry( KEY_UI_WINDOW_W, new SimpleDoubleProperty( WINDOW_W_DEFAULT ) ), | |
| 140 | entry( KEY_UI_WINDOW_H, new SimpleDoubleProperty( WINDOW_H_DEFAULT ) ), | |
| 141 | entry( KEY_UI_WINDOW_MAX, new SimpleBooleanProperty() ), | |
| 142 | entry( KEY_UI_WINDOW_FULL, new SimpleBooleanProperty() ) | |
| 143 | ); | |
| 144 | //@formatter:on | |
| 145 | ||
| 146 | /** | |
| 147 | * Helps instantiate {@link Property} instances for XML configuration items. | |
| 148 | */ | |
| 149 | private static final Map<Class<?>, Function<String, Object>> UNMARSHALL = | |
| 150 | Map.of( | |
| 151 | LocaleProperty.class, LocaleProperty::parseLocale, | |
| 152 | SimpleBooleanProperty.class, Boolean::parseBoolean, | |
| 153 | SimpleDoubleProperty.class, Double::parseDouble, | |
| 154 | SimpleFloatProperty.class, Float::parseFloat, | |
| 155 | FileProperty.class, File::new | |
| 156 | ); | |
| 157 | ||
| 158 | private static final Map<Class<?>, Function<String, Object>> MARSHALL = | |
| 159 | Map.of( | |
| 160 | LocaleProperty.class, LocaleProperty::toLanguageTag | |
| 161 | ); | |
| 162 | ||
| 163 | private final Map<Key, SetProperty<?>> SETS = Map.ofEntries( | |
| 164 | entry( | |
| 165 | KEY_UI_FILES_PATH, | |
| 166 | new SimpleSetProperty<>( observableSet( new HashSet<>() ) ) | |
| 167 | ) | |
| 168 | ); | |
| 169 | ||
| 170 | /** | |
| 171 | * Creates a new {@link Workspace} that will attempt to load a configuration | |
| 172 | * file. If the configuration file cannot be loaded, the workspace settings | |
| 173 | * will return default values. This allows unit tests to provide an instance | |
| 174 | * of {@link Workspace} when necessary without encountering failures. | |
| 175 | */ | |
| 176 | public Workspace() { | |
| 177 | load(); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Returns a value that represents a setting in the application that the user | |
| 182 | * may configure, either directly or indirectly. | |
| 183 | * | |
| 184 | * @param key The reference to the users' preference stored in deference | |
| 185 | * of app reëntrance. | |
| 186 | * @return An observable property to be persisted. | |
| 187 | */ | |
| 188 | @SuppressWarnings( "unchecked" ) | |
| 189 | public <T, U extends Property<T>> U valuesProperty( final Key key ) { | |
| 190 | // The type that goes into the map must come out. | |
| 191 | return (U) VALUES.get( key ); | |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Returns a list of values that represent a setting in the application that | |
| 196 | * the user may configure, either directly or indirectly. The property | |
| 197 | * returned is backed by a mutable {@link Set}. | |
| 198 | * | |
| 199 | * @param key The {@link Key} associated with a preference value. | |
| 200 | * @return An observable property to be persisted. | |
| 201 | */ | |
| 202 | @SuppressWarnings( "unchecked" ) | |
| 203 | public <T> SetProperty<T> setsProperty( final Key key ) { | |
| 204 | // The type that goes into the map must come out. | |
| 205 | return (SetProperty<T>) SETS.get( key ); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * Returns the {@link Boolean} preference value associated with the given | |
| 210 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 211 | * associated with a value that matches the return type. | |
| 212 | * | |
| 213 | * @param key The {@link Key} associated with a preference value. | |
| 214 | * @return The value associated with the given {@link Key}. | |
| 215 | */ | |
| 216 | public boolean toBoolean( final Key key ) { | |
| 217 | return (Boolean) valuesProperty( key ).getValue(); | |
| 218 | } | |
| 219 | ||
| 220 | /** | |
| 221 | * Returns the {@link Double} preference value associated with the given | |
| 222 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 223 | * associated with a value that matches the return type. | |
| 224 | * | |
| 225 | * @param key The {@link Key} associated with a preference value. | |
| 226 | * @return The value associated with the given {@link Key}. | |
| 227 | */ | |
| 228 | public double toDouble( final Key key ) { | |
| 229 | return (Double) valuesProperty( key ).getValue(); | |
| 230 | } | |
| 231 | ||
| 232 | public File toFile( final Key key ) { | |
| 233 | return fileProperty( key ).get(); | |
| 234 | } | |
| 235 | ||
| 236 | public String toString( final Key key ) { | |
| 237 | return stringProperty( key ).get(); | |
| 238 | } | |
| 239 | ||
| 240 | public Tokens toTokens( final Key began, final Key ended ) { | |
| 241 | return new Tokens( stringProperty( began ), stringProperty( ended ) ); | |
| 242 | } | |
| 243 | ||
| 244 | @SuppressWarnings( "SameParameterValue" ) | |
| 245 | public DoubleProperty doubleProperty( final Key key ) { | |
| 246 | return valuesProperty( key ); | |
| 247 | } | |
| 248 | ||
| 249 | /** | |
| 250 | * Returns the {@link File} {@link Property} associated with the given | |
| 251 | * {@link Key} from the internal list of preference values. The caller | |
| 252 | * must be sure that the given {@link Key} is associated with a {@link File} | |
| 253 | * {@link Property}. | |
| 254 | * | |
| 255 | * @param key The {@link Key} associated with a preference value. | |
| 256 | * @return The value associated with the given {@link Key}. | |
| 257 | */ | |
| 258 | public ObjectProperty<File> fileProperty( final Key key ) { | |
| 259 | return valuesProperty( key ); | |
| 260 | } | |
| 261 | ||
| 262 | public LocaleProperty localeProperty( final Key key ) { | |
| 263 | return valuesProperty( key ); | |
| 264 | } | |
| 265 | ||
| 266 | public StringProperty stringProperty( final Key key ) { | |
| 267 | return valuesProperty( key ); | |
| 268 | } | |
| 269 | ||
| 270 | public void loadValueKeys( final Consumer<Key> consumer ) { | |
| 271 | VALUES.keySet().forEach( consumer ); | |
| 272 | } | |
| 273 | ||
| 274 | public void loadSetKeys( final Consumer<Key> consumer ) { | |
| 275 | SETS.keySet().forEach( consumer ); | |
| 276 | } | |
| 277 | ||
| 278 | /** | |
| 279 | * Calls the given consumer for all single-value keys. For lists, see | |
| 280 | * {@link #saveSets(BiConsumer)}. | |
| 281 | * | |
| 282 | * @param consumer Called to accept each preference key value. | |
| 283 | */ | |
| 284 | public void saveValues( final BiConsumer<Key, Property<?>> consumer ) { | |
| 285 | VALUES.forEach( consumer ); | |
| 286 | } | |
| 287 | ||
| 288 | /** | |
| 289 | * Calls the given consumer for all multi-value keys. For single items, see | |
| 290 | * {@link #saveValues(BiConsumer)}. Callers are responsible for iterating | |
| 291 | * over the list of items retrieved through this method. | |
| 292 | * | |
| 293 | * @param consumer Called to accept each preference key list. | |
| 294 | */ | |
| 295 | public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) { | |
| 296 | SETS.forEach( consumer ); | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)}, | |
| 301 | * providing a value of {@code true} for the {@link BooleanSupplier} to | |
| 302 | * indicate the property changes always take effect. | |
| 303 | * | |
| 304 | * @param key The value to bind to the internal key property. | |
| 305 | * @param property The external property value that sets the internal value. | |
| 306 | */ | |
| 307 | public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) { | |
| 308 | listen( key, property, () -> true ); | |
| 309 | } | |
| 310 | ||
| 311 | /** | |
| 312 | * Binds a read-only property to a value in the preferences. This allows | |
| 313 | * user interface properties to change and the preferences will be | |
| 314 | * synchronized automatically. | |
| 315 | * <p> | |
| 316 | * This calls {@link Platform#runLater(Runnable)} to ensure that all pending | |
| 317 | * application window states are finished before assessing whether property | |
| 318 | * changes should be applied. Without this, exiting the application while the | |
| 319 | * window is maximized would persist the window's maximum dimensions, | |
| 320 | * preventing restoration to its prior, non-maximum size. | |
| 321 | * </p> | |
| 322 | * | |
| 323 | * @param key The value to bind to the internal key property. | |
| 324 | * @param property The external property value that sets the internal value. | |
| 325 | * @param enabled Indicates whether property changes should be applied. | |
| 326 | */ | |
| 327 | public <T> void listen( | |
| 328 | final Key key, | |
| 329 | final ReadOnlyProperty<T> property, | |
| 330 | final BooleanSupplier enabled ) { | |
| 331 | property.addListener( | |
| 332 | ( c, o, n ) -> runLater( () -> { | |
| 333 | if( enabled.getAsBoolean() ) { | |
| 334 | valuesProperty( key ).setValue( n ); | |
| 335 | } | |
| 336 | } ) | |
| 337 | ); | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Saves the current workspace. | |
| 342 | */ | |
| 343 | public void save() { | |
| 344 | try { | |
| 345 | final var config = new XMLConfiguration(); | |
| 346 | ||
| 347 | // The root config key can only be set for an empty configuration file. | |
| 348 | config.setRootElementName( APP_TITLE_LOWERCASE ); | |
| 349 | ||
| 350 | saveValues( ( key, property ) -> | |
| 351 | config.setProperty( key.toString(), marshall( property ) ) | |
| 352 | ); | |
| 353 | ||
| 354 | saveSets( | |
| 355 | ( key, set ) -> { | |
| 356 | final var keyName = key.toString(); | |
| 357 | set.forEach( ( value ) -> config.addProperty( keyName, value ) ); | |
| 358 | } | |
| 359 | ); | |
| 360 | new FileHandler( config ).save( FILE_PREFERENCES ); | |
| 361 | } catch( final Exception ex ) { | |
| 362 | clue( ex ); | |
| 363 | } | |
| 364 | } | |
| 365 | ||
| 366 | /** | |
| 367 | * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file. | |
| 368 | * If not found, this will fall back to an empty configuration file, leaving | |
| 369 | * the application to fill in default values. | |
| 370 | */ | |
| 371 | private void load() { | |
| 372 | try { | |
| 373 | final var config = new Configurations().xml( FILE_PREFERENCES ); | |
| 374 | ||
| 375 | loadValueKeys( ( key ) -> { | |
| 376 | final var configValue = config.getProperty( key.toString() ); | |
| 377 | final var propertyValue = valuesProperty( key ); | |
| 378 | propertyValue.setValue( unmarshall( propertyValue, configValue ) ); | |
| 379 | } ); | |
| 380 | ||
| 381 | loadSetKeys( ( key ) -> { | |
| 382 | final var configSet = | |
| 383 | new LinkedHashSet<>( config.getList( key.toString() ) ); | |
| 384 | final var propertySet = setsProperty( key ); | |
| 385 | propertySet.setValue( observableSet( configSet ) ); | |
| 386 | } ); | |
| 387 | } catch( final Exception ex ) { | |
| 388 | clue( ex ); | |
| 389 | } | |
| 390 | } | |
| 391 | ||
| 392 | private Object unmarshall( | |
| 393 | final Property<?> property, final Object configValue ) { | |
| 394 | return UNMARSHALL | |
| 395 | .getOrDefault( property.getClass(), ( value ) -> value ) | |
| 396 | .apply( configValue.toString() ); | |
| 397 | } | |
| 398 | ||
| 399 | private Object marshall( final Property<?> property ) { | |
| 400 | return MARSHALL | |
| 401 | .getOrDefault( property.getClass(), ( v ) -> property.getValue() ) | |
| 402 | .apply( property.getValue().toString() ); | |
| 403 | } | |
| 404 | } | |
| 1 | 405 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preferences; | |
| 3 | ||
| 4 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 5 | import com.dlsc.preferencesfx.util.StorageHandler; | |
| 6 | import javafx.collections.ObservableList; | |
| 7 | ||
| 8 | import java.util.prefs.Preferences; | |
| 9 | ||
| 10 | /** | |
| 11 | * Prevents {@link PreferencesFx} from saving. Saving and loading preferences | |
| 12 | * and application window state is accomplished by the {@link Workspace}. | |
| 13 | * <p> | |
| 14 | * This implies that undo/redo functionality must be disabled because the | |
| 15 | * {@link Workspace} does not preserve previous states. | |
| 16 | * </p> | |
| 17 | */ | |
| 18 | public class XmlStorageHandler implements StorageHandler { | |
| 19 | @Override | |
| 20 | public void saveSelectedCategory( final String breadcrumb ) { } | |
| 21 | ||
| 22 | @Override | |
| 23 | public String loadSelectedCategory() { | |
| 24 | return ""; | |
| 25 | } | |
| 26 | ||
| 27 | @Override | |
| 28 | public void saveDividerPosition( final double dividerPosition ) { | |
| 29 | } | |
| 30 | ||
| 31 | @Override | |
| 32 | public double loadDividerPosition() { | |
| 33 | return 0; | |
| 34 | } | |
| 35 | ||
| 36 | @Override | |
| 37 | public void saveWindowWidth( final double windowWidth ) { } | |
| 38 | ||
| 39 | @Override | |
| 40 | public double loadWindowWidth() { | |
| 41 | return 0; | |
| 42 | } | |
| 43 | ||
| 44 | @Override | |
| 45 | public void saveWindowHeight( final double windowHeight ) { } | |
| 46 | ||
| 47 | @Override | |
| 48 | public double loadWindowHeight() { | |
| 49 | return 0; | |
| 50 | } | |
| 51 | ||
| 52 | @Override | |
| 53 | public void saveWindowPosX( final double windowPosX ) { } | |
| 54 | ||
| 55 | @Override | |
| 56 | public double loadWindowPosX() { | |
| 57 | return 0; | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public void saveWindowPosY( final double windowPosY ) { } | |
| 62 | ||
| 63 | @Override | |
| 64 | public double loadWindowPosY() { | |
| 65 | return 0; | |
| 66 | } | |
| 67 | ||
| 68 | @Override | |
| 69 | public void saveObject( final String breadcrumb, final Object object ) { } | |
| 70 | ||
| 71 | @Override | |
| 72 | public Object loadObject( | |
| 73 | final String breadcrumb, final Object defaultObject ) { | |
| 74 | return defaultObject; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public <T> T loadObject( | |
| 79 | final String breadcrumb, final Class<T> type, final T defaultObject ) { | |
| 80 | return defaultObject; | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | @SuppressWarnings("rawtypes") | |
| 85 | public ObservableList loadObservableList( | |
| 86 | final String breadcrumb, final ObservableList defaultObservableList ) { | |
| 87 | return defaultObservableList; | |
| 88 | } | |
| 89 | ||
| 90 | @Override | |
| 91 | public <T> ObservableList<T> loadObservableList( | |
| 92 | final String breadcrumb, | |
| 93 | final Class<T> type, | |
| 94 | final ObservableList<T> defaultObservableList ) { | |
| 95 | return defaultObservableList; | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public boolean clearPreferences() { | |
| 100 | return false; | |
| 101 | } | |
| 102 | ||
| 103 | @Override | |
| 104 | public Preferences getPreferences() { | |
| 105 | return null; | |
| 106 | } | |
| 107 | } | |
| 1 | 108 |
| 1 | /* | |
| 2 | * Copyright 2006 Patrick Wright | |
| 1 | /* Copyright 2006 Patrick Wright | |
| 3 | 2 | * Copyright 2007 Wisconsin Court System |
| 4 | 3 | * Copyright 2020 White Magic Software, Ltd. |
| ... | ||
| 20 | 19 | package com.keenwrite.preview; |
| 21 | 20 | |
| 22 | import com.keenwrite.adapters.ReplacedElementAdapter; | |
| 21 | import com.keenwrite.ui.adapters.ReplacedElementAdapter; | |
| 22 | import com.keenwrite.util.BoundedCache; | |
| 23 | 23 | import org.w3c.dom.Element; |
| 24 | 24 | import org.xhtmlrenderer.extend.ReplacedElement; |
| 25 | 25 | import org.xhtmlrenderer.extend.ReplacedElementFactory; |
| 26 | 26 | import org.xhtmlrenderer.extend.UserAgentCallback; |
| 27 | 27 | import org.xhtmlrenderer.layout.LayoutContext; |
| 28 | 28 | import org.xhtmlrenderer.render.BlockBox; |
| 29 | 29 | |
| 30 | import java.util.HashSet; | |
| 30 | import java.util.LinkedHashSet; | |
| 31 | import java.util.Map; | |
| 31 | 32 | import java.util.Set; |
| 33 | ||
| 34 | import static com.keenwrite.preview.SvgReplacedElementFactory.HTML_IMAGE; | |
| 35 | import static com.keenwrite.preview.SvgReplacedElementFactory.HTML_IMAGE_SRC; | |
| 36 | import static com.keenwrite.processors.markdown.tex.TexNode.HTML_TEX; | |
| 37 | import static java.util.Arrays.asList; | |
| 32 | 38 | |
| 39 | /** | |
| 40 | * Responsible for running one or more factories to perform post-processing on | |
| 41 | * the HTML document prior to displaying it. | |
| 42 | */ | |
| 33 | 43 | public class ChainedReplacedElementFactory extends ReplacedElementAdapter { |
| 34 | private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>(); | |
| 44 | /** | |
| 45 | * Retain insertion order so that client classes can control the order that | |
| 46 | * factories are used to resolve images. | |
| 47 | */ | |
| 48 | private final Set<ReplacedElementFactory> mFactories = new LinkedHashSet<>(); | |
| 49 | ||
| 50 | /** | |
| 51 | * A bounded cache that removes the oldest image if the maximum number of | |
| 52 | * cached images has been reached. This constrains the number of images | |
| 53 | * loaded into memory. | |
| 54 | */ | |
| 55 | private final Map<String, ReplacedElement> mCache = new BoundedCache<>( 150 ); | |
| 56 | ||
| 57 | public ChainedReplacedElementFactory( | |
| 58 | final ReplacedElementFactory... factories ) { | |
| 59 | mFactories.addAll( asList( factories ) ); | |
| 60 | } | |
| 35 | 61 | |
| 36 | 62 | @Override |
| 37 | 63 | public ReplacedElement createReplacedElement( |
| 38 | final LayoutContext c, | |
| 39 | final BlockBox box, | |
| 40 | final UserAgentCallback uac, | |
| 41 | final int cssWidth, | |
| 42 | final int cssHeight ) { | |
| 43 | for( final var f : mFactoryList ) { | |
| 44 | final var r = f.createReplacedElement( | |
| 45 | c, box, uac, cssWidth, cssHeight ); | |
| 64 | final LayoutContext c, | |
| 65 | final BlockBox box, | |
| 66 | final UserAgentCallback uac, | |
| 67 | final int width, | |
| 68 | final int height ) { | |
| 69 | for( final var f : mFactories ) { | |
| 70 | final var e = box.getElement(); | |
| 46 | 71 | |
| 47 | if( r != null ) { | |
| 48 | return r; | |
| 72 | // Exit early for super-speed. | |
| 73 | if( e == null ) { | |
| 74 | break; | |
| 75 | } | |
| 76 | ||
| 77 | // If the source image is cached, don't bother fetching. This optimization | |
| 78 | // avoids making multiple HTTP requests for the same URI. | |
| 79 | final var node = e.getNodeName(); | |
| 80 | final var source = switch( node ) { | |
| 81 | case HTML_IMAGE -> e.getAttribute( HTML_IMAGE_SRC ); | |
| 82 | case HTML_TEX -> e.getTextContent(); | |
| 83 | default -> ""; | |
| 84 | }; | |
| 85 | ||
| 86 | // HTML <img> or <tex> elements without source data shall not pass. | |
| 87 | if( source.isBlank() ) { | |
| 88 | break; | |
| 89 | } | |
| 90 | ||
| 91 | final var replaced = mCache.computeIfAbsent( | |
| 92 | source, k -> f.createReplacedElement( c, box, uac, width, height ) | |
| 93 | ); | |
| 94 | ||
| 95 | if( replaced != null ) { | |
| 96 | return replaced; | |
| 49 | 97 | } |
| 50 | 98 | } |
| 51 | 99 | |
| 52 | 100 | return null; |
| 53 | 101 | } |
| 54 | 102 | |
| 55 | 103 | @Override |
| 56 | 104 | public void reset() { |
| 57 | for( final var factory : mFactoryList ) { | |
| 105 | for( final var factory : mFactories ) { | |
| 58 | 106 | factory.reset(); |
| 59 | 107 | } |
| 60 | 108 | } |
| 61 | 109 | |
| 62 | 110 | @Override |
| 63 | 111 | public void remove( final Element element ) { |
| 64 | for( final var factory : mFactoryList ) { | |
| 112 | for( final var factory : mFactories ) { | |
| 65 | 113 | factory.remove( element ); |
| 66 | 114 | } |
| 67 | 115 | } |
| 68 | 116 | |
| 69 | 117 | public void addFactory( final ReplacedElementFactory factory ) { |
| 70 | mFactoryList.add( factory ); | |
| 118 | mFactories.add( factory ); | |
| 119 | } | |
| 120 | ||
| 121 | public void clearCache() { | |
| 122 | mCache.clear(); | |
| 71 | 123 | } |
| 72 | 124 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.keenwrite.exceptions.MissingFileException; | |
| 31 | import javafx.beans.property.IntegerProperty; | |
| 32 | import javafx.beans.property.SimpleIntegerProperty; | |
| 33 | import org.xhtmlrenderer.extend.FSImage; | |
| 34 | import org.xhtmlrenderer.resource.ImageResource; | |
| 35 | import org.xhtmlrenderer.swing.ImageResourceLoader; | |
| 36 | ||
| 37 | import javax.imageio.ImageIO; | |
| 38 | import java.net.URI; | |
| 39 | import java.net.URL; | |
| 40 | import java.nio.file.Paths; | |
| 41 | ||
| 42 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 43 | import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER; | |
| 44 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 45 | import static java.lang.String.valueOf; | |
| 46 | import static java.nio.file.Files.exists; | |
| 47 | import static org.xhtmlrenderer.swing.AWTFSImage.createImage; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for loading images. If the image cannot be found, a placeholder | |
| 51 | * is used instead. | |
| 52 | */ | |
| 53 | public class CustomImageLoader extends ImageResourceLoader { | |
| 54 | /** | |
| 55 | * Placeholder that's displayed when image cannot be found. | |
| 56 | */ | |
| 57 | private FSImage mBrokenImage; | |
| 58 | ||
| 59 | private final IntegerProperty mWidthProperty = new SimpleIntegerProperty(); | |
| 60 | ||
| 61 | /** | |
| 62 | * Gets an {@link IntegerProperty} that represents the maximum width an | |
| 63 | * image should be scaled. | |
| 64 | * | |
| 65 | * @return The maximum width for an image. | |
| 66 | */ | |
| 67 | public IntegerProperty widthProperty() { | |
| 68 | return mWidthProperty; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Gets an image resolved from the given URI. If the image cannot be found, | |
| 73 | * this will return a custom placeholder image indicating the reference | |
| 74 | * is broken. | |
| 75 | * | |
| 76 | * @param uri Path to the image resource to load. | |
| 77 | * @param width Ignored. | |
| 78 | * @param height Ignored. | |
| 79 | * @return The scaled image, or a placeholder image if the URI's content | |
| 80 | * could not be retrieved. | |
| 81 | */ | |
| 82 | @Override | |
| 83 | public synchronized ImageResource get( | |
| 84 | final String uri, final int width, final int height ) { | |
| 85 | assert uri != null; | |
| 86 | assert width >= 0; | |
| 87 | assert height >= 0; | |
| 88 | ||
| 89 | try { | |
| 90 | final var protocol = getProtocol( uri ); | |
| 91 | final ImageResource imageResource; | |
| 92 | ||
| 93 | if( protocol.isFile() ) { | |
| 94 | if( exists( Paths.get( new URI( uri ) ) ) ) { | |
| 95 | imageResource = super.get( uri, width, height ); | |
| 96 | } | |
| 97 | else { | |
| 98 | throw new MissingFileException( uri ); | |
| 99 | } | |
| 100 | } | |
| 101 | else if( protocol.isHttp() ) { | |
| 102 | // FlyingSaucer will silently swallow any images that fail to load. | |
| 103 | // Consequently, the following lines load the resource over HTTP and | |
| 104 | // translate errors into a broken image icon. | |
| 105 | final var url = new URL( uri ); | |
| 106 | final var image = ImageIO.read( url ); | |
| 107 | imageResource = new ImageResource( uri, createImage( image ) ); | |
| 108 | } | |
| 109 | else { | |
| 110 | // Caught below to return a broken image; exception is swallowed. | |
| 111 | throw new UnsupportedOperationException( valueOf( protocol ) ); | |
| 112 | } | |
| 113 | ||
| 114 | return scale( imageResource ); | |
| 115 | } catch( final Exception e ) { | |
| 116 | clue( e ); | |
| 117 | return new ImageResource( uri, getBrokenImage() ); | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Scales the image found at the given URI. | |
| 123 | * | |
| 124 | * @param ir {@link ImageResource} of image loaded successfully. | |
| 125 | * @return Resource representing the rendered image and path. | |
| 126 | */ | |
| 127 | private ImageResource scale( final ImageResource ir ) { | |
| 128 | final var image = ir.getImage(); | |
| 129 | final var imageWidth = image.getWidth(); | |
| 130 | final var imageHeight = image.getHeight(); | |
| 131 | ||
| 132 | int maxWidth = mWidthProperty.get(); | |
| 133 | int newWidth = imageWidth; | |
| 134 | int newHeight = imageHeight; | |
| 135 | ||
| 136 | // Maintain aspect ratio while shrinking image to view port bounds. | |
| 137 | if( imageWidth > maxWidth ) { | |
| 138 | newWidth = maxWidth; | |
| 139 | newHeight = (newWidth * imageHeight) / imageWidth; | |
| 140 | } | |
| 141 | ||
| 142 | image.scale( newWidth, newHeight ); | |
| 143 | return ir; | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Lazily initializes the broken image placeholder. | |
| 148 | * | |
| 149 | * @return The {@link FSImage} that represents a broken image icon. | |
| 150 | */ | |
| 151 | private FSImage getBrokenImage() { | |
| 152 | final var image = mBrokenImage; | |
| 153 | ||
| 154 | if( image == null ) { | |
| 155 | mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER ); | |
| 156 | } | |
| 157 | ||
| 158 | return mBrokenImage; | |
| 159 | } | |
| 160 | } | |
| 161 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preview; | |
| 3 | ||
| 4 | import org.jsoup.helper.W3CDom; | |
| 5 | import org.jsoup.nodes.Node; | |
| 6 | import org.jsoup.nodes.TextNode; | |
| 7 | import org.jsoup.select.NodeVisitor; | |
| 8 | import org.w3c.dom.DOMImplementation; | |
| 9 | import org.w3c.dom.Document; | |
| 10 | ||
| 11 | import javax.xml.parsers.DocumentBuilder; | |
| 12 | import javax.xml.parsers.DocumentBuilderFactory; | |
| 13 | import java.util.LinkedHashMap; | |
| 14 | import java.util.Map; | |
| 15 | ||
| 16 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 17 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 18 | ||
| 19 | /** | |
| 20 | * Responsible for converting JSoup document object model (DOM) to a W3C DOM. | |
| 21 | * Provides a lighter implementation than the superclass by overriding the | |
| 22 | * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories, | |
| 23 | * builders, and implementations. | |
| 24 | */ | |
| 25 | class DomConverter extends W3CDom { | |
| 26 | /** | |
| 27 | * Retain insertion order using an instance of {@link LinkedHashMap} so | |
| 28 | * that ligature substitution uses longer ligatures ahead of shorter | |
| 29 | * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff" | |
| 30 | * ligature. | |
| 31 | */ | |
| 32 | private static final Map<String, String> LIGATURES = new LinkedHashMap<>(); | |
| 33 | ||
| 34 | static { | |
| 35 | LIGATURES.put( "ffi", "\uFB03" ); | |
| 36 | LIGATURES.put( "ffl", "\uFB04" ); | |
| 37 | LIGATURES.put( "ff", "\uFB00" ); | |
| 38 | LIGATURES.put( "fi", "\uFB01" ); | |
| 39 | LIGATURES.put( "fl", "\uFB02" ); | |
| 40 | } | |
| 41 | ||
| 42 | private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() { | |
| 43 | @Override | |
| 44 | public void head( final Node node, final int depth ) { | |
| 45 | if( node instanceof TextNode ) { | |
| 46 | final var parent = node.parentNode(); | |
| 47 | final var name = parent == null ? "root" : parent.nodeName(); | |
| 48 | ||
| 49 | if( !("pre".equalsIgnoreCase( name ) || | |
| 50 | "code".equalsIgnoreCase( name ) || | |
| 51 | "tt".equalsIgnoreCase( name )) ) { | |
| 52 | // Calling getWholeText() will return newlines, which must be kept | |
| 53 | // to ensure that preformatted text maintains its formatting. | |
| 54 | final var textNode = (TextNode) node; | |
| 55 | textNode.text( replace( textNode.getWholeText(), LIGATURES ) ); | |
| 56 | } | |
| 57 | } | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public void tail( final Node node, final int depth ) { | |
| 62 | } | |
| 63 | }; | |
| 64 | ||
| 65 | private static final DocumentBuilderFactory DOCUMENT_FACTORY; | |
| 66 | private static DocumentBuilder DOCUMENT_BUILDER; | |
| 67 | private static DOMImplementation DOM_IMPL; | |
| 68 | ||
| 69 | static { | |
| 70 | DOCUMENT_FACTORY = DocumentBuilderFactory.newInstance(); | |
| 71 | DOCUMENT_FACTORY.setNamespaceAware( true ); | |
| 72 | ||
| 73 | try { | |
| 74 | DOCUMENT_BUILDER = DOCUMENT_FACTORY.newDocumentBuilder(); | |
| 75 | DOM_IMPL = DOCUMENT_BUILDER.getDOMImplementation(); | |
| 76 | } catch( final Exception ex ) { | |
| 77 | clue( ex ); | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | @Override | |
| 82 | public Document fromJsoup( final org.jsoup.nodes.Document in ) { | |
| 83 | assert in != null; | |
| 84 | assert DOCUMENT_BUILDER != null; | |
| 85 | assert DOM_IMPL != null; | |
| 86 | ||
| 87 | final var out = DOCUMENT_BUILDER.newDocument(); | |
| 88 | final org.jsoup.nodes.DocumentType doctype = in.documentType(); | |
| 89 | ||
| 90 | if( doctype != null ) { | |
| 91 | out.appendChild( | |
| 92 | DOM_IMPL.createDocumentType( | |
| 93 | doctype.name(), | |
| 94 | doctype.publicId(), | |
| 95 | doctype.systemId() | |
| 96 | ) | |
| 97 | ); | |
| 98 | } | |
| 99 | ||
| 100 | out.setXmlStandalone( true ); | |
| 101 | in.traverse( LIGATURE_VISITOR ); | |
| 102 | convert( in, out ); | |
| 103 | ||
| 104 | return out; | |
| 105 | } | |
| 106 | } | |
| 1 | 107 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.keenwrite.adapters.DocumentAdapter; | |
| 31 | import javafx.beans.property.BooleanProperty; | |
| 32 | import javafx.beans.property.SimpleBooleanProperty; | |
| 33 | import javafx.embed.swing.SwingNode; | |
| 34 | import javafx.scene.Node; | |
| 35 | import org.jsoup.Jsoup; | |
| 36 | import org.jsoup.helper.W3CDom; | |
| 37 | import org.xhtmlrenderer.layout.SharedContext; | |
| 38 | import org.xhtmlrenderer.render.Box; | |
| 39 | import org.xhtmlrenderer.simple.XHTMLPanel; | |
| 40 | import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; | |
| 41 | import org.xhtmlrenderer.swing.*; | |
| 42 | ||
| 43 | import javax.swing.*; | |
| 44 | import java.awt.*; | |
| 45 | import java.awt.event.ComponentAdapter; | |
| 46 | import java.awt.event.ComponentEvent; | |
| 47 | import java.net.URI; | |
| 48 | import java.nio.file.Path; | |
| 49 | ||
| 50 | import static com.keenwrite.Constants.DEFAULT_DIRECTORY; | |
| 51 | import static com.keenwrite.Constants.STYLESHEET_PREVIEW; | |
| 52 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 53 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 54 | import static java.awt.Desktop.Action.BROWSE; | |
| 55 | import static java.awt.Desktop.getDesktop; | |
| 56 | import static java.lang.Math.max; | |
| 57 | import static java.lang.String.format; | |
| 58 | import static javax.swing.SwingUtilities.invokeLater; | |
| 59 | import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER; | |
| 60 | ||
| 61 | /** | |
| 62 | * HTML preview pane is responsible for rendering an HTML document. | |
| 63 | */ | |
| 64 | public final class HTMLPreviewPane extends SwingNode { | |
| 65 | /** | |
| 66 | * Suppresses scrolling to the top on every key press. | |
| 67 | */ | |
| 68 | private static class HTMLPanel extends XHTMLPanel { | |
| 69 | @Override | |
| 70 | public void resetScrollPosition() { | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Suppresses scroll attempts until after the document has loaded. | |
| 76 | */ | |
| 77 | private static final class DocumentEventHandler extends DocumentAdapter { | |
| 78 | private final BooleanProperty mReadyProperty = new SimpleBooleanProperty(); | |
| 79 | ||
| 80 | public BooleanProperty readyProperty() { | |
| 81 | return mReadyProperty; | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void documentStarted() { | |
| 86 | mReadyProperty.setValue( Boolean.FALSE ); | |
| 87 | } | |
| 88 | ||
| 89 | @Override | |
| 90 | public void documentLoaded() { | |
| 91 | mReadyProperty.setValue( Boolean.TRUE ); | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Ensure that images are constrained to the panel width upon resizing. | |
| 97 | */ | |
| 98 | private final class ResizeListener extends ComponentAdapter { | |
| 99 | @Override | |
| 100 | public void componentResized( final ComponentEvent e ) { | |
| 101 | setWidth( e ); | |
| 102 | } | |
| 103 | ||
| 104 | @Override | |
| 105 | public void componentShown( final ComponentEvent e ) { | |
| 106 | setWidth( e ); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Sets the width of the {@link HTMLPreviewPane} so that images can be | |
| 111 | * scaled to fit. The scale factor is adjusted a bit below the full width | |
| 112 | * to prevent the horizontal scrollbar from appearing. | |
| 113 | * | |
| 114 | * @param event The component that defines the image scaling width. | |
| 115 | */ | |
| 116 | private void setWidth( final ComponentEvent event ) { | |
| 117 | final int width = (int) (event.getComponent().getWidth() * .95); | |
| 118 | HTMLPreviewPane.this.mImageLoader.widthProperty().set( width ); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Responsible for opening hyperlinks. External hyperlinks are opened in | |
| 124 | * the system's default browser; local file system links are opened in the | |
| 125 | * editor. | |
| 126 | */ | |
| 127 | private static class HyperlinkListener extends LinkListener { | |
| 128 | @Override | |
| 129 | public void linkClicked( final BasicPanel panel, final String link ) { | |
| 130 | try { | |
| 131 | switch( getProtocol( link ) ) { | |
| 132 | case HTTP: | |
| 133 | final var desktop = getDesktop(); | |
| 134 | ||
| 135 | if( desktop.isSupported( BROWSE ) ) { | |
| 136 | desktop.browse( new URI( link ) ); | |
| 137 | } | |
| 138 | break; | |
| 139 | case FILE: | |
| 140 | // TODO: #88 -- publish a message to the event bus. | |
| 141 | break; | |
| 142 | } | |
| 143 | } catch( final Exception ex ) { | |
| 144 | clue( ex ); | |
| 145 | } | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | /** | |
| 150 | * Render CSS using points (pt) not pixels (px) to reduce the chance of | |
| 151 | * poor rendering. | |
| 152 | */ | |
| 153 | private static final String HTML_PREFIX = format( | |
| 154 | "<!DOCTYPE html>" | |
| 155 | + "<html lang='en'>" | |
| 156 | + "<head><title> </title><meta charset='utf-8'/>" | |
| 157 | + "<link rel='stylesheet' href='%s'/>" | |
| 158 | + "</head>" | |
| 159 | + "<body>", | |
| 160 | HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) | |
| 161 | ); | |
| 162 | ||
| 163 | private static final String HTML_SUFFIX = "</body></html>"; | |
| 164 | ||
| 165 | /** | |
| 166 | * Used to reset the {@link #mHtmlDocument} buffer so that the | |
| 167 | * {@link #HTML_PREFIX} need not be appended all the time. | |
| 168 | */ | |
| 169 | private static final int HTML_PREFIX_LENGTH = HTML_PREFIX.length(); | |
| 170 | ||
| 171 | private static final W3CDom W3C_DOM = new W3CDom(); | |
| 172 | private static final XhtmlNamespaceHandler NS_HANDLER = | |
| 173 | new XhtmlNamespaceHandler(); | |
| 174 | ||
| 175 | /** | |
| 176 | * The buffer is reused so that previous memory allocations need not repeat. | |
| 177 | */ | |
| 178 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 179 | ||
| 180 | private final HTMLPanel mHtmlRenderer = new HTMLPanel(); | |
| 181 | private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer ); | |
| 182 | private final CustomImageLoader mImageLoader = new CustomImageLoader(); | |
| 183 | ||
| 184 | private Path mPath = DEFAULT_DIRECTORY; | |
| 185 | ||
| 186 | /** | |
| 187 | * Creates a new preview pane that can scroll to the caret position within the | |
| 188 | * document. | |
| 189 | */ | |
| 190 | public HTMLPreviewPane() { | |
| 191 | setStyle( "-fx-background-color: white;" ); | |
| 192 | ||
| 193 | // No need to append same prefix each time the HTML content is updated. | |
| 194 | mHtmlDocument.append( HTML_PREFIX ); | |
| 195 | ||
| 196 | // Inject an SVG renderer that produces high-quality SVG buffered images. | |
| 197 | final var factory = new ChainedReplacedElementFactory(); | |
| 198 | factory.addFactory( new SvgReplacedElementFactory() ); | |
| 199 | factory.addFactory( new SwingReplacedElementFactory( | |
| 200 | NO_OP_REPAINT_LISTENER, mImageLoader ) ); | |
| 201 | ||
| 202 | final var context = getSharedContext(); | |
| 203 | final var textRenderer = context.getTextRenderer(); | |
| 204 | context.setReplacedElementFactory( factory ); | |
| 205 | textRenderer.setSmoothingThreshold( 0 ); | |
| 206 | ||
| 207 | setContent( mScrollPane ); | |
| 208 | mHtmlRenderer.addDocumentListener( new DocumentEventHandler() ); | |
| 209 | mHtmlRenderer.addComponentListener( new ResizeListener() ); | |
| 210 | ||
| 211 | // The default mouse click listener attempts navigation within the | |
| 212 | // preview panel. We want to usurp that behaviour to open the link in | |
| 213 | // a platform-specific browser. | |
| 214 | for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) { | |
| 215 | if( !(listener instanceof HoverListener) ) { | |
| 216 | mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener ); | |
| 217 | } | |
| 218 | } | |
| 219 | ||
| 220 | mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() ); | |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Updates the internal HTML source, loads it into the preview pane, then | |
| 225 | * scrolls to the caret position. | |
| 226 | * | |
| 227 | * @param html The new HTML document to display. | |
| 228 | */ | |
| 229 | public void process( final String html ) { | |
| 230 | final var docJsoup = Jsoup.parse( decorate( html ) ); | |
| 231 | final var docW3c = W3C_DOM.fromJsoup( docJsoup ); | |
| 232 | ||
| 233 | // Access to a Swing component must occur from the Event Dispatch | |
| 234 | // Thread (EDT) according to Swing threading restrictions. | |
| 235 | invokeLater( | |
| 236 | () -> mHtmlRenderer.setDocument( docW3c, getBaseUrl(), NS_HANDLER ) | |
| 237 | ); | |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Clears the preview pane by rendering an empty string. | |
| 242 | */ | |
| 243 | public void clear() { | |
| 244 | process( "" ); | |
| 245 | } | |
| 246 | ||
| 247 | /** | |
| 248 | * Scrolls to the closest element matching the given identifier without | |
| 249 | * waiting for the document to be ready. Be sure the document is ready | |
| 250 | * before calling this method. | |
| 251 | * | |
| 252 | * @param id Scroll the preview pane to this unique paragraph identifier. | |
| 253 | */ | |
| 254 | public void scrollTo( final String id ) { | |
| 255 | scrollTo( getBoxById( id ) ); | |
| 256 | } | |
| 257 | ||
| 258 | /** | |
| 259 | * Scrolls to the location specified by the {@link Box} that corresponds | |
| 260 | * to a point somewhere in the preview pane. If there is no caret, then | |
| 261 | * this will not change the scroll position. Changing the scroll position | |
| 262 | * to the top if the {@link Box} instance is {@code null} will result in | |
| 263 | * jumping around a lot and inconsistent synchronization issues. | |
| 264 | * | |
| 265 | * @param box The rectangular region containing the caret, or {@code null} | |
| 266 | * if the HTML does not have a caret. | |
| 267 | */ | |
| 268 | private void scrollTo( final Box box ) { | |
| 269 | if( box != null ) { | |
| 270 | scrollTo( createPoint( box ) ); | |
| 271 | } | |
| 272 | } | |
| 273 | ||
| 274 | private void scrollTo( final Point point ) { | |
| 275 | mHtmlRenderer.scrollTo( point ); | |
| 276 | } | |
| 277 | ||
| 278 | private Box getBoxById( final String id ) { | |
| 279 | return getSharedContext().getBoxById( id ); | |
| 280 | } | |
| 281 | ||
| 282 | private String decorate( final String html ) { | |
| 283 | // Trim the HTML back to only the prefix. | |
| 284 | mHtmlDocument.setLength( HTML_PREFIX_LENGTH ); | |
| 285 | ||
| 286 | // Write the HTML body element followed by closing tags. | |
| 287 | return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString(); | |
| 288 | } | |
| 289 | ||
| 290 | public Path getPath() { | |
| 291 | return mPath; | |
| 292 | } | |
| 293 | ||
| 294 | public void setPath( final Path path ) { | |
| 295 | assert path != null; | |
| 296 | mPath = path; | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * Content to embed in a panel. | |
| 301 | * | |
| 302 | * @return The content to display to the user. | |
| 303 | */ | |
| 304 | public Node getNode() { | |
| 305 | return this; | |
| 306 | } | |
| 307 | ||
| 308 | public void repaintScrollPane() { | |
| 309 | getScrollPane().repaint(); | |
| 310 | } | |
| 311 | ||
| 312 | public JScrollBar getVerticalScrollBar() { | |
| 313 | return getScrollPane().getVerticalScrollBar(); | |
| 314 | } | |
| 315 | ||
| 316 | /** | |
| 317 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 318 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 319 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 320 | * vertical centering. | |
| 321 | * | |
| 322 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 323 | * @return A coordinate suitable for scrolling to. | |
| 324 | */ | |
| 325 | private Point createPoint( final Box box ) { | |
| 326 | assert box != null; | |
| 327 | ||
| 328 | int x = box.getAbsX(); | |
| 329 | ||
| 330 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 331 | // area within the view port. Otherwise the view port will have jumped too | |
| 332 | // high up and the most recently typed letters won't be visible. | |
| 333 | int y = max( | |
| 334 | box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2), | |
| 335 | 0 ); | |
| 336 | ||
| 337 | if( !box.getStyle().isInline() ) { | |
| 338 | final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() ); | |
| 339 | x += margin.left(); | |
| 340 | y += margin.top(); | |
| 341 | } | |
| 342 | ||
| 343 | return new Point( x, y ); | |
| 344 | } | |
| 345 | ||
| 346 | private JScrollPane getScrollPane() { | |
| 347 | return mScrollPane; | |
| 348 | } | |
| 349 | ||
| 350 | private String getBaseUrl() { | |
| 351 | final var basePath = getPath(); | |
| 352 | final var parent = basePath == null ? null : basePath.getParent(); | |
| 353 | ||
| 354 | return parent == null ? "" : parent.toUri().toString(); | |
| 355 | } | |
| 356 | ||
| 357 | private SharedContext getSharedContext() { | |
| 358 | return mHtmlRenderer.getSharedContext(); | |
| 359 | } | |
| 360 | } | |
| 361 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preview; | |
| 3 | ||
| 4 | import com.keenwrite.ui.adapters.DocumentAdapter; | |
| 5 | import javafx.beans.property.BooleanProperty; | |
| 6 | import javafx.beans.property.SimpleBooleanProperty; | |
| 7 | import org.xhtmlrenderer.layout.SharedContext; | |
| 8 | import org.xhtmlrenderer.render.Box; | |
| 9 | import org.xhtmlrenderer.simple.XHTMLPanel; | |
| 10 | import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; | |
| 11 | import org.xhtmlrenderer.swing.BasicPanel; | |
| 12 | import org.xhtmlrenderer.swing.FSMouseListener; | |
| 13 | import org.xhtmlrenderer.swing.HoverListener; | |
| 14 | import org.xhtmlrenderer.swing.LinkListener; | |
| 15 | ||
| 16 | import java.awt.event.ComponentAdapter; | |
| 17 | import java.awt.event.ComponentEvent; | |
| 18 | import java.net.URI; | |
| 19 | ||
| 20 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 21 | import static com.keenwrite.util.ProtocolScheme.getProtocol; | |
| 22 | import static java.awt.Desktop.Action.BROWSE; | |
| 23 | import static java.awt.Desktop.getDesktop; | |
| 24 | import static javax.swing.SwingUtilities.invokeLater; | |
| 25 | import static org.jsoup.Jsoup.parse; | |
| 26 | ||
| 27 | /** | |
| 28 | * Responsible for configuring FlyingSaucer's {@link XHTMLPanel}. | |
| 29 | */ | |
| 30 | public class HtmlPanel extends XHTMLPanel { | |
| 31 | ||
| 32 | /** | |
| 33 | * Suppresses scroll attempts until after the document has loaded. | |
| 34 | */ | |
| 35 | private static final class DocumentEventHandler extends DocumentAdapter { | |
| 36 | private final BooleanProperty mReadyProperty = new SimpleBooleanProperty(); | |
| 37 | ||
| 38 | @Override | |
| 39 | public void documentStarted() { | |
| 40 | mReadyProperty.setValue( Boolean.FALSE ); | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void documentLoaded() { | |
| 45 | mReadyProperty.setValue( Boolean.TRUE ); | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Ensures that the preview panel fills its container's area completely. | |
| 51 | */ | |
| 52 | private final class ComponentEventHandler extends ComponentAdapter { | |
| 53 | /** | |
| 54 | * Invoked when the component's size changes. | |
| 55 | */ | |
| 56 | public void componentResized( final ComponentEvent e ) { | |
| 57 | setPreferredSize( e.getComponent().getPreferredSize() ); | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Responsible for opening hyperlinks. External hyperlinks are opened in | |
| 63 | * the system's default browser; local file system links are opened in the | |
| 64 | * editor. | |
| 65 | */ | |
| 66 | private static final class HyperlinkListener extends LinkListener { | |
| 67 | @Override | |
| 68 | public void linkClicked( final BasicPanel panel, final String link ) { | |
| 69 | switch( getProtocol( link ) ) { | |
| 70 | case HTTP -> { | |
| 71 | final var desktop = getDesktop(); | |
| 72 | ||
| 73 | if( desktop.isSupported( BROWSE ) ) { | |
| 74 | try { | |
| 75 | desktop.browse( new URI( link ) ); | |
| 76 | } catch( final Exception ex ) { | |
| 77 | clue( ex ); | |
| 78 | } | |
| 79 | } | |
| 80 | } | |
| 81 | case FILE -> { | |
| 82 | // TODO: #88 -- publish a message to the event bus. | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | private static final DomConverter CONVERTER = new DomConverter(); | |
| 89 | private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler(); | |
| 90 | ||
| 91 | public HtmlPanel() { | |
| 92 | addDocumentListener( new DocumentEventHandler() ); | |
| 93 | removeMouseTrackingListeners(); | |
| 94 | addMouseTrackingListener( new HyperlinkListener() ); | |
| 95 | addComponentListener( new ComponentEventHandler() ); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Updates the document model displayed by the renderer. Effectively, this | |
| 100 | * updates the HTML document to provide new content. | |
| 101 | * | |
| 102 | * @param html A complete HTML5 document, including doctype. | |
| 103 | * @param baseUri URI to use for finding relative files, such as images. | |
| 104 | */ | |
| 105 | public void render( final String html, final String baseUri ) { | |
| 106 | final var doc = CONVERTER.fromJsoup( parse( html ) ); | |
| 107 | ||
| 108 | // Access to a Swing component must occur from the Event Dispatch | |
| 109 | // Thread (EDT) according to Swing threading restrictions. Setting a new | |
| 110 | // document invokes a Swing repaint operation. | |
| 111 | invokeLater( () -> setDocument( doc, baseUri, XNH ) ); | |
| 112 | } | |
| 113 | ||
| 114 | /** | |
| 115 | * Delegates to the {@link SharedContext}. | |
| 116 | * | |
| 117 | * @param id The HTML element identifier to retrieve in {@link Box} form. | |
| 118 | * @return The {@link Box} that corresponds to the given element ID, or | |
| 119 | * {@code null} if none found. | |
| 120 | */ | |
| 121 | public Box getBoxById( final String id ) { | |
| 122 | return getSharedContext().getBoxById( id ); | |
| 123 | } | |
| 124 | ||
| 125 | /** | |
| 126 | * Suppress scrolling to the top on updates. | |
| 127 | */ | |
| 128 | @Override | |
| 129 | public void resetScrollPosition() { | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * The default mouse click listener attempts navigation within the preview | |
| 134 | * panel. We want to usurp that behaviour to open the link in a | |
| 135 | * platform-specific browser. | |
| 136 | */ | |
| 137 | private void removeMouseTrackingListeners() { | |
| 138 | for( final var listener : getMouseTrackingListeners() ) { | |
| 139 | if( !(listener instanceof HoverListener) ) { | |
| 140 | removeMouseTrackingListener( (FSMouseListener) listener ); | |
| 141 | } | |
| 142 | } | |
| 143 | } | |
| 144 | } | |
| 1 | 145 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.preview; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.LocaleProperty; | |
| 5 | import com.keenwrite.preferences.Workspace; | |
| 6 | import javafx.beans.property.DoubleProperty; | |
| 7 | import javafx.embed.swing.SwingNode; | |
| 8 | import org.xhtmlrenderer.render.Box; | |
| 9 | import org.xhtmlrenderer.swing.SwingReplacedElementFactory; | |
| 10 | ||
| 11 | import javax.swing.*; | |
| 12 | import java.awt.*; | |
| 13 | import java.net.URL; | |
| 14 | import java.nio.file.Path; | |
| 15 | import java.util.Locale; | |
| 16 | ||
| 17 | import static com.keenwrite.Constants.*; | |
| 18 | import static com.keenwrite.Messages.get; | |
| 19 | import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_LOCALE; | |
| 20 | import static com.keenwrite.preferences.Workspace.KEY_UI_FONT_PREVIEW_SIZE; | |
| 21 | import static java.lang.Math.max; | |
| 22 | import static java.lang.String.format; | |
| 23 | import static javafx.scene.CacheHint.SPEED; | |
| 24 | import static javax.swing.SwingUtilities.invokeLater; | |
| 25 | ||
| 26 | /** | |
| 27 | * Responsible for parsing an HTML document. | |
| 28 | */ | |
| 29 | public final class HtmlPreview extends SwingNode { | |
| 30 | ||
| 31 | // The order is important: Swing factory will replace SVG images with | |
| 32 | // a blank image, which will cause the chained factory to cache the image | |
| 33 | // and exit. Instead, the SVG must execute first to rasterize the content. | |
| 34 | // Consequently, the chained factory must maintain insertion order. | |
| 35 | private static final ChainedReplacedElementFactory FACTORY | |
| 36 | = new ChainedReplacedElementFactory( | |
| 37 | new SvgReplacedElementFactory(), | |
| 38 | new SwingReplacedElementFactory() | |
| 39 | ); | |
| 40 | ||
| 41 | /** | |
| 42 | * Render CSS using points (pt) not pixels (px) to reduce the chance of | |
| 43 | * poor rendering. | |
| 44 | */ | |
| 45 | private static final String HTML_HEAD = | |
| 46 | """ | |
| 47 | <!DOCTYPE html> | |
| 48 | <html lang='%s'><head><title> </title><meta charset='utf-8'> | |
| 49 | <link rel='stylesheet' href='%s'> | |
| 50 | <link rel='stylesheet' href='%s'> | |
| 51 | <style>body{font-size: %spt;}</style> | |
| 52 | <base href='%s'> | |
| 53 | </head><body> | |
| 54 | """; | |
| 55 | ||
| 56 | private static final String HTML_TAIL = "</body></html>"; | |
| 57 | ||
| 58 | private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW ); | |
| 59 | ||
| 60 | /** | |
| 61 | * The buffer is reused so that previous memory allocations need not repeat. | |
| 62 | */ | |
| 63 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 64 | ||
| 65 | private HtmlPanel mView; | |
| 66 | private JScrollPane mScrollPane; | |
| 67 | private String mBaseUriPath = ""; | |
| 68 | private URL mLocaleUrl; | |
| 69 | private final Workspace mWorkspace; | |
| 70 | ||
| 71 | /** | |
| 72 | * Creates a new preview pane that can scroll to the caret position within the | |
| 73 | * document. | |
| 74 | * | |
| 75 | * @param workspace Contains locale and font size information. | |
| 76 | */ | |
| 77 | public HtmlPreview( final Workspace workspace ) { | |
| 78 | mWorkspace = workspace; | |
| 79 | mLocaleUrl = toUrl( getLocale() ); | |
| 80 | ||
| 81 | // Attempts to prevent a flash of black un-styled content upon load. | |
| 82 | setStyle( "-fx-background-color: white;" ); | |
| 83 | ||
| 84 | invokeLater( () -> { | |
| 85 | mView = new HtmlPanel(); | |
| 86 | mScrollPane = new JScrollPane( mView ); | |
| 87 | ||
| 88 | // Enabling the cache attempts to prevent black flashes when resizing. | |
| 89 | setCache( true ); | |
| 90 | setCacheHint( SPEED ); | |
| 91 | setContent( mScrollPane ); | |
| 92 | ||
| 93 | final var context = mView.getSharedContext(); | |
| 94 | final var textRenderer = context.getTextRenderer(); | |
| 95 | context.setReplacedElementFactory( FACTORY ); | |
| 96 | textRenderer.setSmoothingThreshold( 0 ); | |
| 97 | ||
| 98 | localeProperty().addListener( ( c, o, n ) -> { | |
| 99 | mLocaleUrl = toUrl( getLocale() ); | |
| 100 | rerender(); | |
| 101 | } ); | |
| 102 | ||
| 103 | fontSizeProperty().addListener( ( c, o, n ) -> rerender() ); | |
| 104 | } ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Updates the internal HTML source shown in the preview pane. | |
| 109 | * | |
| 110 | * @param html The new HTML document to display. | |
| 111 | */ | |
| 112 | public void render( final String html ) { | |
| 113 | mView.render( decorate( html ), getBaseUri() ); | |
| 114 | } | |
| 115 | ||
| 116 | /** | |
| 117 | * Clears the caches then rerenders the content. | |
| 118 | */ | |
| 119 | public void refresh() { | |
| 120 | FACTORY.clearCache(); | |
| 121 | rerender(); | |
| 122 | } | |
| 123 | ||
| 124 | private void rerender() { | |
| 125 | render( mHtmlDocument.toString() ); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Attaches the HTML head prefix and HTML tail suffix to the given HTML | |
| 130 | * string. | |
| 131 | * | |
| 132 | * @param html The HTML to adorn with opening and closing tags. | |
| 133 | * @return A complete HTML document, ready for rendering. | |
| 134 | */ | |
| 135 | private String decorate( final String html ) { | |
| 136 | mHtmlDocument.setLength( 0 ); | |
| 137 | mHtmlDocument.append( html ); | |
| 138 | return head() + mHtmlDocument + tail(); | |
| 139 | } | |
| 140 | ||
| 141 | private String head() { | |
| 142 | return format( | |
| 143 | HTML_HEAD, | |
| 144 | getLocale().getLanguage(), | |
| 145 | HTML_STYLE_PREVIEW, | |
| 146 | mLocaleUrl, | |
| 147 | getFontSize(), | |
| 148 | mBaseUriPath | |
| 149 | ); | |
| 150 | } | |
| 151 | ||
| 152 | private String tail() { | |
| 153 | return HTML_TAIL; | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Clears the preview pane by rendering an empty string. | |
| 158 | */ | |
| 159 | public void clear() { | |
| 160 | render( "" ); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Sets the base URI to the containing directory the file being edited. | |
| 165 | * | |
| 166 | * @param path The path to the file being edited. | |
| 167 | */ | |
| 168 | public void setBaseUri( final Path path ) { | |
| 169 | final var parent = path.getParent(); | |
| 170 | mBaseUriPath = parent == null ? "" : parent.toUri().toString(); | |
| 171 | } | |
| 172 | ||
| 173 | /** | |
| 174 | * Scrolls to the closest element matching the given identifier without | |
| 175 | * waiting for the document to be ready. Be sure the document is ready | |
| 176 | * before calling this method. | |
| 177 | * | |
| 178 | * @param id Scroll the preview pane to this unique paragraph identifier. | |
| 179 | */ | |
| 180 | public void scrollTo( final String id ) { | |
| 181 | scrollTo( mView.getBoxById( id ) ); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Scrolls to the location specified by the {@link Box} that corresponds | |
| 186 | * to a point somewhere in the preview pane. If there is no caret, then | |
| 187 | * this will not change the scroll position. Changing the scroll position | |
| 188 | * to the top if the {@link Box} instance is {@code null} will result in | |
| 189 | * jumping around a lot and inconsistent synchronization issues. | |
| 190 | * | |
| 191 | * @param box The rectangular region containing the caret, or {@code null} | |
| 192 | * if the HTML does not have a caret. | |
| 193 | */ | |
| 194 | private void scrollTo( final Box box ) { | |
| 195 | if( box != null ) { | |
| 196 | scrollTo( createPoint( box ) ); | |
| 197 | } | |
| 198 | } | |
| 199 | ||
| 200 | private void scrollTo( final Point point ) { | |
| 201 | invokeLater( () -> { | |
| 202 | mView.scrollTo( point ); | |
| 203 | getScrollPane().repaint(); | |
| 204 | } ); | |
| 205 | } | |
| 206 | ||
| 207 | /** | |
| 208 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 209 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 210 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 211 | * vertical centering. | |
| 212 | * | |
| 213 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 214 | * @return A coordinate suitable for scrolling to. | |
| 215 | */ | |
| 216 | private Point createPoint( final Box box ) { | |
| 217 | assert box != null; | |
| 218 | ||
| 219 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 220 | // area within the view port. Otherwise the view port will have jumped too | |
| 221 | // high up and the most recently typed letters won't be visible. | |
| 222 | int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 ); | |
| 223 | int x = box.getAbsX(); | |
| 224 | ||
| 225 | if( !box.getStyle().isInline() ) { | |
| 226 | final var margin = box.getMargin( mView.getLayoutContext() ); | |
| 227 | y += margin.top(); | |
| 228 | x += margin.left(); | |
| 229 | } | |
| 230 | ||
| 231 | return new Point( x, y ); | |
| 232 | } | |
| 233 | ||
| 234 | private String getBaseUri() { | |
| 235 | return mBaseUriPath; | |
| 236 | } | |
| 237 | ||
| 238 | private JScrollPane getScrollPane() { | |
| 239 | return mScrollPane; | |
| 240 | } | |
| 241 | ||
| 242 | public JScrollBar getVerticalScrollBar() { | |
| 243 | return getScrollPane().getVerticalScrollBar(); | |
| 244 | } | |
| 245 | ||
| 246 | private int getVerticalScrollBarHeight() { | |
| 247 | return getVerticalScrollBar().getHeight(); | |
| 248 | } | |
| 249 | ||
| 250 | /** | |
| 251 | * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen | |
| 252 | * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166 | |
| 253 | * alpha-2 country code or UN M.49 numeric-3 area code. For example, this | |
| 254 | * could return "en-Latn-CA" for Canadian English written in the Latin | |
| 255 | * character set. | |
| 256 | * | |
| 257 | * @return Unique identifier for language and country. | |
| 258 | */ | |
| 259 | private static URL toUrl( final Locale locale ) { | |
| 260 | return toUrl( | |
| 261 | get( | |
| 262 | sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ), | |
| 263 | locale.getLanguage(), | |
| 264 | locale.getScript(), | |
| 265 | locale.getCountry() | |
| 266 | ) | |
| 267 | ); | |
| 268 | } | |
| 269 | ||
| 270 | private static URL toUrl( final String path ) { | |
| 271 | return HtmlPreview.class.getResource( path ); | |
| 272 | } | |
| 273 | ||
| 274 | private Locale getLocale() { | |
| 275 | return localeProperty().toLocale(); | |
| 276 | } | |
| 277 | ||
| 278 | private LocaleProperty localeProperty() { | |
| 279 | return mWorkspace.localeProperty( KEY_UI_FONT_LOCALE ); | |
| 280 | } | |
| 281 | ||
| 282 | private double getFontSize() { | |
| 283 | return fontSizeProperty().get(); | |
| 284 | } | |
| 285 | ||
| 286 | private DoubleProperty fontSizeProperty() { | |
| 287 | return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ); | |
| 288 | } | |
| 289 | } | |
| 1 | 290 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.preview; |
| 29 | 3 | |
| ... | ||
| 40 | 14 | */ |
| 41 | 15 | public class MathRenderer { |
| 16 | ||
| 17 | /** | |
| 18 | * Singleton instance for rendering math symbols. | |
| 19 | */ | |
| 20 | public static final MathRenderer MATH_RENDERER = new MathRenderer(); | |
| 42 | 21 | |
| 43 | 22 | /** |
| ... | ||
| 50 | 29 | private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D(); |
| 51 | 30 | |
| 52 | public MathRenderer() { | |
| 31 | private MathRenderer() { | |
| 53 | 32 | mGraphics.scale( FONT_SIZE, FONT_SIZE ); |
| 54 | 33 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.preview; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.preview; |
| 29 | 3 | |
| 30 | 4 | import org.apache.batik.anim.dom.SAXSVGDocumentFactory; |
| 31 | 5 | import org.apache.batik.gvt.renderer.ImageRenderer; |
| 32 | import org.apache.batik.transcoder.TranscoderException; | |
| 33 | 6 | import org.apache.batik.transcoder.TranscoderInput; |
| 34 | 7 | import org.apache.batik.transcoder.TranscoderOutput; |
| ... | ||
| 44 | 17 | import java.awt.*; |
| 45 | 18 | import java.awt.image.BufferedImage; |
| 46 | import java.io.IOException; | |
| 19 | import java.io.File; | |
| 47 | 20 | import java.io.StringReader; |
| 48 | 21 | import java.io.StringWriter; |
| 49 | import java.net.URL; | |
| 22 | import java.net.URI; | |
| 23 | import java.nio.file.Path; | |
| 50 | 24 | import java.text.NumberFormat; |
| 51 | 25 | |
| ... | ||
| 130 | 104 | try { |
| 131 | 105 | image = rasterizeString( BROKEN_IMAGE_SVG, w ); |
| 132 | } catch( final Exception e ) { | |
| 106 | } catch( final Exception ex ) { | |
| 133 | 107 | image = new BufferedImage( w, h, TYPE_INT_RGB ); |
| 134 | 108 | final var graphics = (Graphics2D) image.getGraphics(); |
| ... | ||
| 177 | 151 | final RenderingHints hints = renderer.getRenderingHints(); |
| 178 | 152 | hints.putAll( RENDERING_HINTS ); |
| 179 | ||
| 180 | 153 | renderer.setRenderingHints( hints ); |
| 181 | 154 | |
| 182 | 155 | return renderer; |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Rasterizes the vector graphic file at the given URL. If any exception | |
| 188 | * happens, a red circle is returned instead. | |
| 189 | * | |
| 190 | * @param url The URL to a vector graphic file, which must include the | |
| 191 | * protocol scheme (such as file:// or https://). | |
| 192 | * @param width The number of pixels wide to render the image. The aspect | |
| 193 | * ratio is maintained. | |
| 194 | * @return Either the rasterized image upon success or a red circle. | |
| 195 | */ | |
| 196 | public static BufferedImage rasterize( final String url, final int width ) { | |
| 197 | try { | |
| 198 | return rasterize( new URL( url ), width ); | |
| 199 | } catch( final Exception ex ) { | |
| 200 | clue( ex ); | |
| 201 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 202 | 156 | } |
| 203 | 157 | } |
| 204 | 158 | |
| 205 | 159 | /** |
| 206 | 160 | * Rasterizes the given document into an image. |
| 207 | 161 | * |
| 208 | 162 | * @param svg The SVG {@link Document} to rasterize. |
| 209 | 163 | * @param width The rasterized image's width (in pixels). |
| 210 | 164 | * @return The rasterized image. |
| 211 | * @throws TranscoderException Signifies an issue with the input document. | |
| 212 | 165 | */ |
| 213 | public static BufferedImage rasterize( final Document svg, final int width ) | |
| 214 | throws TranscoderException { | |
| 215 | final var transcoder = new BufferedImageTranscoder(); | |
| 216 | final var input = new TranscoderInput( svg ); | |
| 217 | ||
| 218 | transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | |
| 219 | transcoder.transcode( input, null ); | |
| 166 | public static BufferedImage rasterize( final Document svg, final int width ) { | |
| 167 | try { | |
| 168 | final var transcoder = new BufferedImageTranscoder(); | |
| 169 | final var input = new TranscoderInput( svg ); | |
| 220 | 170 | |
| 221 | return transcoder.getImage(); | |
| 222 | } | |
| 171 | transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | |
| 172 | transcoder.transcode( input, null ); | |
| 173 | return transcoder.getImage(); | |
| 174 | } catch( final Exception ex ) { | |
| 175 | clue( ex ); | |
| 176 | } | |
| 223 | 177 | |
| 224 | /** | |
| 225 | * Converts an SVG drawing into a rasterized image that can be drawn on | |
| 226 | * a graphics context. | |
| 227 | * | |
| 228 | * @param url The path to the image (can be web address). | |
| 229 | * @param width Scale the image width to this size (aspect ratio is | |
| 230 | * maintained). | |
| 231 | * @return The vector graphic transcoded into a raster image format. | |
| 232 | * @throws IOException Could not read the vector graphic. | |
| 233 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 234 | * instance of {@link Image}. | |
| 235 | */ | |
| 236 | public static BufferedImage rasterize( final URL url, final int width ) | |
| 237 | throws IOException, TranscoderException { | |
| 238 | return rasterize( FACTORY_DOM.createDocument( url.toString() ), width ); | |
| 178 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 239 | 179 | } |
| 240 | 180 | |
| 241 | 181 | public static BufferedImage rasterize( final Document document ) { |
| 242 | 182 | try { |
| 243 | 183 | final var root = document.getDocumentElement(); |
| 244 | 184 | final var width = root.getAttribute( "width" ); |
| 245 | 185 | return rasterize( document, INT_FORMAT.parse( width ).intValue() ); |
| 246 | 186 | } catch( final Exception ex ) { |
| 247 | 187 | clue( ex ); |
| 248 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 249 | 188 | } |
| 189 | ||
| 190 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 250 | 191 | } |
| 251 | 192 | |
| 252 | 193 | /** |
| 253 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 194 | * Rasterizes the vector graphic file at the given URI. If any exception | |
| 195 | * happens, a broken image icon is returned instead. | |
| 196 | * | |
| 197 | * @param path The {@link Path} to a vector graphic file. | |
| 198 | * @param width Scale the image to the given width (px); aspect ratio is | |
| 199 | * maintained. | |
| 200 | * @return A rasterized image as an instance of {@link BufferedImage}. | |
| 201 | */ | |
| 202 | public static BufferedImage rasterize( final Path path, final int width ) { | |
| 203 | return rasterize( path.toUri(), width ); | |
| 204 | } | |
| 205 | ||
| 206 | /** | |
| 207 | * Rasterizes the vector graphic file at the given URI. If any exception | |
| 208 | * happens, a broken image icon is returned instead. | |
| 209 | * | |
| 210 | * @param uri The URI to a vector graphic file, which must include the | |
| 211 | * protocol scheme (such as file:// or https://). | |
| 212 | * @param width Scale the image to the given width (px); aspect ratio is | |
| 213 | * maintained. | |
| 214 | * @return A rasterized image as an instance of {@link BufferedImage}. | |
| 215 | */ | |
| 216 | public static BufferedImage rasterize( final String uri, final int width ) { | |
| 217 | return rasterize( new File( uri ).toURI(), width ); | |
| 218 | } | |
| 219 | ||
| 220 | /** | |
| 221 | * Converts an SVG drawing into a rasterized image that can be drawn on | |
| 254 | 222 | * a graphics context. |
| 255 | 223 | * |
| 256 | * @param svg The SVG xml document. | |
| 257 | * @param w Scale the image width to this size (aspect ratio is | |
| 258 | * maintained). | |
| 224 | * @param uri The path to the image (can be web address). | |
| 225 | * @param width Scale the image to the given width (px); aspect ratio is | |
| 226 | * maintained. | |
| 259 | 227 | * @return The vector graphic transcoded into a raster image format. |
| 260 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 261 | * instance of {@link Image}. | |
| 262 | 228 | */ |
| 263 | public static BufferedImage rasterizeString( final String svg, final int w ) | |
| 264 | throws IOException, TranscoderException { | |
| 265 | return rasterize( toDocument( svg ), w ); | |
| 229 | public static BufferedImage rasterize( final URI uri, final int width ) { | |
| 230 | try { | |
| 231 | return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width ); | |
| 232 | } catch( final Exception ex ) { | |
| 233 | clue( ex ); | |
| 234 | } | |
| 235 | ||
| 236 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 266 | 237 | } |
| 267 | 238 | |
| ... | ||
| 281 | 252 | } catch( final Exception ex ) { |
| 282 | 253 | clue( ex ); |
| 283 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 284 | 254 | } |
| 255 | ||
| 256 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 285 | 257 | } |
| 286 | 258 | |
| 287 | 259 | /** |
| 288 | * Converts an SVG XML string into a new {@link Document} instance. | |
| 260 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 261 | * a graphics context. | |
| 289 | 262 | * |
| 290 | * @param xml The XML containing SVG elements. | |
| 291 | * @return The SVG contents parsed into a {@link Document} object model. | |
| 292 | * @throws IOException Could | |
| 263 | * @param svg The SVG xml document. | |
| 264 | * @param w Scale the image width to this size (aspect ratio is | |
| 265 | * maintained). | |
| 266 | * @return The vector graphic transcoded into a raster image format. | |
| 293 | 267 | */ |
| 294 | private static Document toDocument( final String xml ) throws IOException { | |
| 295 | try( final var reader = new StringReader( xml ) ) { | |
| 296 | return FACTORY_DOM.createSVGDocument( | |
| 297 | "http://www.w3.org/2000/svg", reader ); | |
| 298 | } | |
| 268 | public static BufferedImage rasterizeString( final String svg, final int w ) { | |
| 269 | return rasterize( toDocument( svg ), w ); | |
| 299 | 270 | } |
| 300 | 271 | |
| ... | ||
| 315 | 286 | |
| 316 | 287 | return BROKEN_IMAGE_SVG; |
| 288 | } | |
| 289 | ||
| 290 | /** | |
| 291 | * Converts an SVG XML string into a new {@link Document} instance. | |
| 292 | * | |
| 293 | * @param xml The XML containing SVG elements. | |
| 294 | * @return The SVG contents parsed into a {@link Document} object model. | |
| 295 | */ | |
| 296 | private static Document toDocument( final String xml ) { | |
| 297 | try( final var reader = new StringReader( xml ) ) { | |
| 298 | return FACTORY_DOM.createSVGDocument( | |
| 299 | "http://www.w3.org/2000/svg", reader ); | |
| 300 | } catch( final Exception ex ) { | |
| 301 | throw new IllegalArgumentException( ex ); | |
| 302 | } | |
| 317 | 303 | } |
| 318 | 304 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.preview; |
| 29 | 3 | |
| 30 | import com.keenwrite.util.BoundedCache; | |
| 31 | import org.apache.commons.io.FilenameUtils; | |
| 4 | import com.keenwrite.io.HttpMediaType; | |
| 5 | import com.keenwrite.io.MediaType; | |
| 6 | import com.keenwrite.ui.adapters.ReplacedElementAdapter; | |
| 32 | 7 | import org.w3c.dom.Element; |
| 33 | 8 | import org.xhtmlrenderer.extend.ReplacedElement; |
| 34 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 35 | 9 | import org.xhtmlrenderer.extend.UserAgentCallback; |
| 36 | 10 | import org.xhtmlrenderer.layout.LayoutContext; |
| 37 | 11 | import org.xhtmlrenderer.render.BlockBox; |
| 38 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 39 | 12 | import org.xhtmlrenderer.swing.ImageReplacedElement; |
| 40 | 13 | |
| 41 | 14 | import java.awt.image.BufferedImage; |
| 42 | import java.util.Map; | |
| 43 | import java.util.function.Function; | |
| 15 | import java.net.URI; | |
| 16 | import java.nio.file.Paths; | |
| 44 | 17 | |
| 45 | 18 | import static com.keenwrite.StatusBarNotifier.clue; |
| 19 | import static com.keenwrite.io.MediaType.*; | |
| 20 | import static com.keenwrite.preview.MathRenderer.MATH_RENDERER; | |
| 21 | import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER; | |
| 46 | 22 | import static com.keenwrite.preview.SvgRasterizer.rasterize; |
| 47 | 23 | import static com.keenwrite.processors.markdown.tex.TexNode.HTML_TEX; |
| 24 | import static com.keenwrite.util.ProtocolScheme.getProtocol; | |
| 48 | 25 | |
| 49 | 26 | /** |
| 50 | 27 | * Responsible for running {@link SvgRasterizer} on SVG images detected within |
| 51 | 28 | * a document to transform them into rasterized versions. |
| 52 | 29 | */ |
| 53 | public class SvgReplacedElementFactory implements ReplacedElementFactory { | |
| 54 | ||
| 55 | /** | |
| 56 | * Prevent instantiation until needed. | |
| 57 | */ | |
| 58 | private static class MathRendererContainer { | |
| 59 | private static final MathRenderer INSTANCE = new MathRenderer(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Returns the singleton instance for rendering math symbols. | |
| 64 | * | |
| 65 | * @return A non-null instance, loaded, configured, and ready to render math. | |
| 66 | */ | |
| 67 | public static MathRenderer getInstance() { | |
| 68 | return MathRendererContainer.INSTANCE; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * SVG filename extension maps to an SVG image element. | |
| 73 | */ | |
| 74 | private static final String SVG_FILE = "svg"; | |
| 30 | public class SvgReplacedElementFactory extends ReplacedElementAdapter { | |
| 75 | 31 | |
| 76 | private static final String HTML_IMAGE = "img"; | |
| 77 | private static final String HTML_IMAGE_SRC = "src"; | |
| 32 | public static final String HTML_IMAGE = "img"; | |
| 33 | public static final String HTML_IMAGE_SRC = "src"; | |
| 78 | 34 | |
| 79 | /** | |
| 80 | * A bounded cache that removes the oldest image if the maximum number of | |
| 81 | * cached images has been reached. This constrains the number of images | |
| 82 | * loaded into memory. | |
| 83 | */ | |
| 84 | private final Map<String, BufferedImage> mImageCache = | |
| 85 | new BoundedCache<>( 150 ); | |
| 35 | private static final ImageReplacedElement BROKEN_IMAGE = | |
| 36 | createImageReplacedElement( BROKEN_IMAGE_PLACEHOLDER ); | |
| 86 | 37 | |
| 87 | 38 | @Override |
| 88 | 39 | public ReplacedElement createReplacedElement( |
| 89 | final LayoutContext c, | |
| 90 | final BlockBox box, | |
| 91 | final UserAgentCallback uac, | |
| 92 | final int cssWidth, | |
| 93 | final int cssHeight ) { | |
| 94 | BufferedImage image = null; | |
| 40 | final LayoutContext c, | |
| 41 | final BlockBox box, | |
| 42 | final UserAgentCallback uac, | |
| 43 | final int cssWidth, | |
| 44 | final int cssHeight ) { | |
| 95 | 45 | final var e = box.getElement(); |
| 96 | 46 | |
| 97 | if( e != null ) { | |
| 98 | try { | |
| 99 | final var nodeName = e.getNodeName(); | |
| 47 | ImageReplacedElement image = null; | |
| 100 | 48 | |
| 101 | if( HTML_IMAGE.equals( nodeName ) ) { | |
| 102 | final var src = e.getAttribute( HTML_IMAGE_SRC ); | |
| 103 | final var ext = FilenameUtils.getExtension( src ); | |
| 49 | try { | |
| 50 | BufferedImage raster = null; | |
| 104 | 51 | |
| 105 | if( SVG_FILE.equalsIgnoreCase( ext ) ) { | |
| 106 | image = getCachedImage( | |
| 107 | src, svg -> rasterize( svg, box.getContentWidth() ) ); | |
| 52 | switch( e.getNodeName() ) { | |
| 53 | case HTML_IMAGE -> { | |
| 54 | final var source = e.getAttribute( HTML_IMAGE_SRC ); | |
| 55 | URI uri = null; | |
| 56 | ||
| 57 | if( getProtocol( source ).isHttp() ) { | |
| 58 | var mediaType = MediaType.valueFrom( source ); | |
| 59 | ||
| 60 | if( isSvg( mediaType ) || mediaType == UNDEFINED ) { | |
| 61 | uri = new URI( source ); | |
| 62 | ||
| 63 | // Attempt to rasterize SVG depending on URL resource content. | |
| 64 | if( !isSvg( HttpMediaType.valueFrom( uri ) ) ) { | |
| 65 | uri = null; | |
| 66 | } | |
| 67 | } | |
| 108 | 68 | } |
| 109 | } | |
| 110 | else if( HTML_TEX.equals( nodeName ) ) { | |
| 111 | // Convert the TeX element to a raster graphic if not yet cached. | |
| 112 | final var src = e.getTextContent(); | |
| 113 | image = getCachedImage( | |
| 114 | src, __ -> rasterize( getInstance().render( src ) ) | |
| 115 | ); | |
| 69 | else if( isSvg( MediaType.valueFrom( source ) ) ) { | |
| 70 | // Attempt to rasterize based on file name. | |
| 71 | final var base = new URI( getBaseUri( e ) ).getPath(); | |
| 72 | uri = Paths.get( base, source ).toUri(); | |
| 73 | } | |
| 74 | ||
| 75 | if( uri != null ) { | |
| 76 | raster = rasterize( uri, box.getContentWidth() ); | |
| 77 | } | |
| 116 | 78 | } |
| 117 | } catch( final Exception ex ) { | |
| 118 | clue( ex ); | |
| 79 | case HTML_TEX -> | |
| 80 | // Convert the TeX element to a raster graphic. | |
| 81 | raster = rasterize( MATH_RENDERER.render( e.getTextContent() ) ); | |
| 119 | 82 | } |
| 120 | } | |
| 121 | ||
| 122 | if( image != null ) { | |
| 123 | final var w = image.getWidth( null ); | |
| 124 | final var h = image.getHeight( null ); | |
| 125 | 83 | |
| 126 | return new ImageReplacedElement( image, w, h ); | |
| 84 | if( raster != null ) { | |
| 85 | image = createImageReplacedElement( raster ); | |
| 86 | } | |
| 87 | } catch( final Exception ex ) { | |
| 88 | image = BROKEN_IMAGE; | |
| 89 | clue( ex ); | |
| 127 | 90 | } |
| 128 | 91 | |
| 129 | return null; | |
| 92 | return image; | |
| 130 | 93 | } |
| 131 | 94 | |
| 132 | @Override | |
| 133 | public void reset() { | |
| 134 | } | |
| 95 | private String getBaseUri( final Element e ) { | |
| 96 | try { | |
| 97 | final var doc = e.getOwnerDocument(); | |
| 98 | final var html = doc.getDocumentElement(); | |
| 99 | final var head = html.getFirstChild(); | |
| 100 | final var children = head.getChildNodes(); | |
| 135 | 101 | |
| 136 | @Override | |
| 137 | public void remove( final Element e ) { | |
| 102 | for( int i = children.getLength() - 1; i >= 0; i-- ) { | |
| 103 | final var child = children.item( i ); | |
| 104 | final var name = child.getLocalName(); | |
| 105 | ||
| 106 | if( "base".equalsIgnoreCase( name ) ) { | |
| 107 | final var attrs = child.getAttributes(); | |
| 108 | final var item = attrs.getNamedItem( "href" ); | |
| 109 | ||
| 110 | return item.getNodeValue(); | |
| 111 | } | |
| 112 | } | |
| 113 | } catch( final Exception ex ) { | |
| 114 | clue( ex ); | |
| 115 | } | |
| 116 | ||
| 117 | return ""; | |
| 138 | 118 | } |
| 139 | 119 | |
| 140 | @Override | |
| 141 | public void setFormSubmissionListener( FormSubmissionListener listener ) { | |
| 120 | private static ImageReplacedElement createImageReplacedElement( | |
| 121 | final BufferedImage bi ) { | |
| 122 | return new ImageReplacedElement( bi, bi.getWidth(), bi.getHeight() ); | |
| 142 | 123 | } |
| 143 | 124 | |
| 144 | /** | |
| 145 | * Returns an image associated with a string; the string's pre-computed | |
| 146 | * hash code is returned as the string value, making this operation very | |
| 147 | * quick to return the corresponding {@link BufferedImage}. | |
| 148 | * | |
| 149 | * @param src The source used for the key into the image cache. | |
| 150 | * @param rasterizer {@link Function} to call to rasterize an image. | |
| 151 | * @return The image that corresponds to the given source string. | |
| 152 | */ | |
| 153 | private BufferedImage getCachedImage( | |
| 154 | final String src, final Function<String, BufferedImage> rasterizer ) { | |
| 155 | return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) ); | |
| 125 | private static boolean isSvg( final MediaType mediaType ) { | |
| 126 | return mediaType == TEXT_PLAIN || mediaType == IMAGE_SVG_XML; | |
| 156 | 127 | } |
| 157 | 128 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for transforming a document through a variety of chained | |
| 32 | * handlers. If there are conditions where this handler should not process the | |
| 33 | * entire chain, create a second handler, or split the chain into reusable | |
| 34 | * sub-chains. | |
| 35 | * | |
| 36 | * @param <T> The type of object to process. | |
| 37 | */ | |
| 38 | public abstract class AbstractProcessor<T> implements Processor<T> { | |
| 39 | ||
| 40 | /** | |
| 41 | * Used while processing the entire chain; null to signify no more links. | |
| 42 | */ | |
| 43 | private final Processor<T> mNext; | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new default handler with no successor. | |
| 47 | */ | |
| 48 | protected AbstractProcessor() { | |
| 49 | this( null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a new default handler with a given successor. | |
| 54 | * | |
| 55 | * @param successor The next processor in the chain. | |
| 56 | */ | |
| 57 | public AbstractProcessor( final Processor<T> successor ) { | |
| 58 | mNext = successor; | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public Processor<T> next() { | |
| 63 | return mNext; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * This algorithm is incorrect, but works for the one use case of removing | |
| 68 | * the ending HTML Preview Processor from the end of the processor chain. | |
| 69 | * The processor chain is immutable so this creates a succession of | |
| 70 | * delegators that wrap each processor in the chain, except for the one | |
| 71 | * to be removed. | |
| 72 | * <p> | |
| 73 | * An alternative is to update the {@link ProcessorFactory} with the ability | |
| 74 | * to create a processor chain devoid of an {@link HtmlPreviewProcessor}. | |
| 75 | * </p> | |
| 76 | * | |
| 77 | * @param removal The {@link Processor} to remove from the chain. | |
| 78 | * @return A delegating processor chain starting from this processor | |
| 79 | * onwards with the given processor removed from the chain. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public Processor<T> remove( final Class<? extends Processor<T>> removal ) { | |
| 83 | Processor<T> p = this; | |
| 84 | final ProcessorDelegator<T> head = new ProcessorDelegator<>( p ); | |
| 85 | ProcessorDelegator<T> result = head; | |
| 86 | ||
| 87 | while( p != null ) { | |
| 88 | final Processor<T> next = p.next(); | |
| 89 | ||
| 90 | if( next != null && next.getClass() != removal ) { | |
| 91 | final var delegator = new ProcessorDelegator<>( next ); | |
| 92 | ||
| 93 | result.setNext( delegator ); | |
| 94 | result = delegator; | |
| 95 | } | |
| 96 | ||
| 97 | p = p.next(); | |
| 98 | } | |
| 99 | ||
| 100 | return head; | |
| 101 | } | |
| 102 | ||
| 103 | private static final class ProcessorDelegator<T> | |
| 104 | extends AbstractProcessor<T> { | |
| 105 | private final Processor<T> mDelegate; | |
| 106 | private Processor<T> mNext; | |
| 107 | ||
| 108 | public ProcessorDelegator( final Processor<T> delegate ) { | |
| 109 | super( delegate ); | |
| 110 | ||
| 111 | assert delegate != null; | |
| 112 | ||
| 113 | mDelegate = delegate; | |
| 114 | } | |
| 115 | ||
| 116 | @Override | |
| 117 | public T apply( T t ) { | |
| 118 | return mDelegate.apply( t ); | |
| 119 | } | |
| 120 | ||
| 121 | protected void setNext( final Processor<T> next ) { | |
| 122 | mNext = next; | |
| 123 | } | |
| 124 | ||
| 125 | @Override | |
| 126 | public Processor<T> next() { | |
| 127 | return mNext; | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 131 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| ... | ||
| 37 | 11 | * {@code $variable$}. |
| 38 | 12 | */ |
| 39 | public class DefinitionProcessor extends AbstractProcessor<String> { | |
| 13 | public class DefinitionProcessor extends ExecutorProcessor<String> { | |
| 40 | 14 | |
| 41 | 15 | private final Map<String, String> mDefinitions; |
| 42 | 16 | |
| 17 | /** | |
| 18 | * Constructs a processor capable of interpolating string definitions. | |
| 19 | * | |
| 20 | * @param successor Subsequent link in the processing chain. | |
| 21 | * @param context Contains resolved definitions map. | |
| 22 | */ | |
| 43 | 23 | public DefinitionProcessor( |
| 44 | final Processor<String> successor, final Map<String, String> map ) { | |
| 24 | final Processor<String> successor, | |
| 25 | final ProcessorContext context ) { | |
| 45 | 26 | super( successor ); |
| 46 | mDefinitions = map; | |
| 27 | mDefinitions = context.getResolvedMap(); | |
| 47 | 28 | } |
| 48 | 29 | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import java.util.Optional; | |
| 5 | import java.util.concurrent.atomic.AtomicReference; | |
| 6 | ||
| 7 | /** | |
| 8 | * Responsible for transforming data through a variety of chained handlers. | |
| 9 | * | |
| 10 | * @param <T> The data type to process. | |
| 11 | */ | |
| 12 | public class ExecutorProcessor<T> implements Processor<T> { | |
| 13 | ||
| 14 | /** | |
| 15 | * The next link in the processing chain. | |
| 16 | */ | |
| 17 | private final Processor<T> mNext; | |
| 18 | ||
| 19 | protected ExecutorProcessor() { | |
| 20 | this( null ); | |
| 21 | } | |
| 22 | ||
| 23 | /** | |
| 24 | * Constructs a new processor having a given successor. | |
| 25 | * | |
| 26 | * @param successor The next processor in the chain. | |
| 27 | */ | |
| 28 | public ExecutorProcessor( final Processor<T> successor ) { | |
| 29 | mNext = successor; | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Calls every link in the chain to process the given data. | |
| 34 | * | |
| 35 | * @param data The data to transform. | |
| 36 | * @return The data after processing by every link in the chain. | |
| 37 | */ | |
| 38 | @Override | |
| 39 | public T apply( final T data ) { | |
| 40 | // Start processing using the first processor after the executor. | |
| 41 | Optional<Processor<T>> handler = next(); | |
| 42 | final var result = new MutableReference( data ); | |
| 43 | ||
| 44 | while( handler.isPresent() ) { | |
| 45 | handler = handler.flatMap( p -> { | |
| 46 | result.set( p.apply( result.get() ) ); | |
| 47 | return p.next(); | |
| 48 | } ); | |
| 49 | } | |
| 50 | ||
| 51 | return result.get(); | |
| 52 | } | |
| 53 | ||
| 54 | @Override | |
| 55 | public Optional<Processor<T>> next() { | |
| 56 | return Optional.ofNullable( mNext ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * A minor micro-optimization, since the processors cannot be run in parallel, | |
| 61 | * avoid using an {@link AtomicReference} during processor execution. This | |
| 62 | * is about twice as fast for the first four processor links in the chain. | |
| 63 | */ | |
| 64 | private final class MutableReference { | |
| 65 | private T mObject; | |
| 66 | ||
| 67 | MutableReference( final T object ) { | |
| 68 | set( object ); | |
| 69 | } | |
| 70 | ||
| 71 | void set( final T object ) { | |
| 72 | mObject = object; | |
| 73 | } | |
| 74 | ||
| 75 | T get() { | |
| 76 | return mObject; | |
| 77 | } | |
| 78 | } | |
| 79 | } | |
| 1 | 80 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 30 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 4 | import com.keenwrite.preview.HtmlPreview; | |
| 31 | 5 | |
| 32 | 6 | /** |
| 33 | 7 | * Responsible for notifying the HTMLPreviewPane when the succession chain has |
| 34 | 8 | * updated. This decouples knowledge of changes to the editor panel from the |
| 35 | 9 | * HTML preview panel as well as any processing that takes place before the |
| 36 | * final HTML preview is rendered. This should be the last link in the processor | |
| 10 | * final HTML preview is rendered. This is the last link in the processor | |
| 37 | 11 | * chain. |
| 38 | 12 | */ |
| 39 | public class HtmlPreviewProcessor extends AbstractProcessor<String> { | |
| 13 | public class HtmlPreviewProcessor extends ExecutorProcessor<String> { | |
| 40 | 14 | |
| 41 | // There is only one preview panel. | |
| 42 | private static HTMLPreviewPane sHtmlPreviewPane; | |
| 15 | /** | |
| 16 | * There is only one preview panel. | |
| 17 | */ | |
| 18 | private static HtmlPreview sHtmlPreviewPane; | |
| 43 | 19 | |
| 44 | 20 | /** |
| 45 | 21 | * Constructs the end of a processing chain. |
| 46 | 22 | * |
| 47 | 23 | * @param htmlPreviewPane The pane to update with the post-processed document. |
| 48 | 24 | */ |
| 49 | public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) { | |
| 25 | public HtmlPreviewProcessor( final HtmlPreview htmlPreviewPane ) { | |
| 50 | 26 | sHtmlPreviewPane = htmlPreviewPane; |
| 51 | 27 | } |
| 52 | 28 | |
| 53 | 29 | /** |
| 54 | 30 | * Update the preview panel using HTML from the succession chain. |
| 55 | 31 | * |
| 56 | 32 | * @param html The document content to render in the preview pane. The HTML |
| 57 | 33 | * should not contain a doctype, head, or body tag, only |
| 58 | 34 | * content to render within the body. |
| 59 | * @return {@code null} to indicate no more processors in the chain. | |
| 35 | * @return The given {@code html} string. | |
| 60 | 36 | */ |
| 61 | 37 | @Override |
| 62 | 38 | public String apply( final String html ) { |
| 63 | getHtmlPreviewPane().process( html ); | |
| 39 | assert html != null; | |
| 64 | 40 | |
| 65 | // No more processing required. | |
| 66 | return null; | |
| 41 | getHtmlPreviewPane().render( html ); | |
| 42 | return html; | |
| 67 | 43 | } |
| 68 | 44 | |
| 69 | private HTMLPreviewPane getHtmlPreviewPane() { | |
| 45 | private HtmlPreview getHtmlPreviewPane() { | |
| 70 | 46 | return sHtmlPreviewPane; |
| 71 | 47 | } |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 30 | 4 | /** |
| 31 | 5 | * Responsible for transforming a string into itself. This is used at the |
| 32 | 6 | * end of a processing chain when no more processing is required. |
| 33 | 7 | */ |
| 34 | public class IdentityProcessor extends AbstractProcessor<String> { | |
| 8 | public class IdentityProcessor extends ExecutorProcessor<String> { | |
| 35 | 9 | public static final IdentityProcessor INSTANCE = new IdentityProcessor(); |
| 36 | 10 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 30 | import com.keenwrite.StatusBarNotifier; | |
| 31 | import com.keenwrite.preferences.UserPreferences; | |
| 32 | import javafx.beans.property.ObjectProperty; | |
| 33 | import javafx.beans.property.StringProperty; | |
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 6 | import com.vladsch.flexmark.ast.Paragraph; | |
| 7 | import com.vladsch.flexmark.ast.Text; | |
| 8 | import javafx.beans.property.Property; | |
| 34 | 9 | |
| 35 | 10 | import javax.script.ScriptEngine; |
| ... | ||
| 42 | 17 | |
| 43 | 18 | import static com.keenwrite.Constants.STATUS_PARSE_ERROR; |
| 19 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 20 | import static com.keenwrite.preferences.Workspace.*; | |
| 44 | 21 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; |
| 45 | 22 | import static com.keenwrite.sigils.RSigilOperator.PREFIX; |
| ... | ||
| 55 | 32 | */ |
| 56 | 33 | private static final int MAX_CACHED_R_STATEMENTS = 512; |
| 34 | ||
| 35 | private final MarkdownProcessor mMarkdownProcessor; | |
| 57 | 36 | |
| 58 | 37 | /** |
| 59 | 38 | * Where to put document inline evaluated R expressions. |
| 60 | 39 | */ |
| 61 | private final Map<String, Object> mEvalCache = new LinkedHashMap<>() { | |
| 40 | private final Map<String, String> mEvalCache = new LinkedHashMap<>() { | |
| 62 | 41 | @Override |
| 63 | 42 | protected boolean removeEldestEntry( |
| 64 | final Map.Entry<String, Object> eldest ) { | |
| 43 | final Map.Entry<String, String> eldest ) { | |
| 65 | 44 | return size() > MAX_CACHED_R_STATEMENTS; |
| 66 | 45 | } |
| 67 | 46 | }; |
| 68 | 47 | |
| 69 | /** | |
| 70 | * Only one editor is open at a time. | |
| 71 | */ | |
| 72 | 48 | private static final ScriptEngine ENGINE = |
| 73 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 49 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 74 | 50 | |
| 75 | 51 | private static final int PREFIX_LENGTH = PREFIX.length(); |
| 76 | 52 | |
| 77 | 53 | private final AtomicBoolean mDirty = new AtomicBoolean( false ); |
| 54 | ||
| 55 | private final Workspace mWorkspace; | |
| 78 | 56 | |
| 79 | 57 | /** |
| 80 | 58 | * Constructs a processor capable of evaluating R statements. |
| 81 | 59 | * |
| 82 | 60 | * @param successor Subsequent link in the processing chain. |
| 83 | * @param map Resolved definitions map. | |
| 61 | * @param context Contains resolved definitions map. | |
| 84 | 62 | */ |
| 85 | 63 | public InlineRProcessor( |
| 86 | final Processor<String> successor, | |
| 87 | final Map<String, String> map ) { | |
| 88 | super( successor, map ); | |
| 64 | final Processor<String> successor, | |
| 65 | final ProcessorContext context ) { | |
| 66 | super( successor, context ); | |
| 67 | ||
| 68 | mWorkspace = context.getWorkspace(); | |
| 69 | mMarkdownProcessor = MarkdownProcessor.create( context ); | |
| 89 | 70 | |
| 90 | 71 | bootstrapScriptProperty().addListener( |
| 91 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 72 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 92 | 73 | workingDirectoryProperty().addListener( |
| 93 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 74 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 94 | 75 | |
| 95 | getUserPreferences().addSaveEventHandler( ( handler ) -> { | |
| 96 | if( isDirty() ) { | |
| 97 | init(); | |
| 98 | setDirty( false ); | |
| 99 | } | |
| 100 | } ); | |
| 76 | // TODO: Watch the "R" property keys in the workspace, directly. | |
| 77 | ||
| 78 | // If the user saves the preferences, make sure that any R-related settings | |
| 79 | // changes are applied. | |
| 80 | // getWorkspace().addSaveEventHandler( ( handler ) -> { | |
| 81 | // if( isDirty() ) { | |
| 82 | // init(); | |
| 83 | // setDirty( false ); | |
| 84 | // } | |
| 85 | // } ); | |
| 101 | 86 | |
| 102 | 87 | init(); |
| ... | ||
| 115 | 100 | final var dir = wd.toString().replace( '\\', '/' ); |
| 116 | 101 | final var map = getDefinitions(); |
| 117 | final var prefs = UserPreferences.getInstance(); | |
| 118 | final var defBegan = prefs.getDefDelimiterBegan(); | |
| 119 | final var defEnded = prefs.getDefDelimiterEnded(); | |
| 102 | final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN ); | |
| 103 | final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED ); | |
| 120 | 104 | |
| 121 | 105 | map.put( defBegan + "application.r.working.directory" + defEnded, dir ); |
| ... | ||
| 166 | 150 | |
| 167 | 151 | while( currIndex >= 0 ) { |
| 168 | // Copy everything up to, but not including, an R statement (`r#). | |
| 152 | // Copy everything up to, but not including, the opening token. | |
| 169 | 153 | sb.append( text, prevIndex, currIndex ); |
| 170 | 154 | |
| 171 | 155 | // Jump to the start of the R statement. |
| 172 | 156 | prevIndex = currIndex + PREFIX_LENGTH; |
| 173 | 157 | |
| 174 | // Find the statement ending (`), without indexing past the text boundary. | |
| 158 | // Find the closing token, without indexing past the text boundary. | |
| 175 | 159 | currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) ); |
| 176 | 160 | |
| 177 | 161 | // Only evaluate inline R statements that have end delimiters. |
| 178 | 162 | if( currIndex > 1 ) { |
| 179 | 163 | // Extract the inline R statement to be evaluated. |
| 180 | 164 | final String r = text.substring( prevIndex, currIndex ); |
| 181 | 165 | |
| 182 | 166 | // Pass the R statement into the R engine for evaluation. |
| 183 | 167 | try { |
| 184 | final Object result = evalText( r ); | |
| 168 | final var result = evalCached( r ); | |
| 185 | 169 | |
| 186 | 170 | // Append the string representation of the result into the text. |
| 187 | 171 | sb.append( result ); |
| 188 | } catch( final Exception e ) { | |
| 172 | } catch( final Exception ex ) { | |
| 173 | // Inform the user that there was a problem. | |
| 174 | clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex ); | |
| 175 | ||
| 189 | 176 | // If the string couldn't be parsed using R, append the statement |
| 190 | 177 | // that failed to parse, instead of its evaluated value. |
| 191 | 178 | sb.append( PREFIX ).append( r ).append( SUFFIX ); |
| 192 | ||
| 193 | // Tell the user that there was a problem. | |
| 194 | StatusBarNotifier.clue( STATUS_PARSE_ERROR, | |
| 195 | e.getMessage(), | |
| 196 | currIndex ); | |
| 197 | 179 | } |
| 198 | 180 | |
| ... | ||
| 216 | 198 | * @return The object resulting from the evaluation. |
| 217 | 199 | */ |
| 218 | private Object evalText( final String r ) { | |
| 219 | return mEvalCache.computeIfAbsent( r, v -> eval( r ) ); | |
| 200 | private String evalCached( final String r ) { | |
| 201 | return mEvalCache.computeIfAbsent( r, v -> evalHtml( r ) ); | |
| 202 | } | |
| 203 | ||
| 204 | /** | |
| 205 | * Converts the given string to HTML, trimming new lines, and inlining | |
| 206 | * the text if it is a paragraph. Otherwise, the resulting HTML is most likely | |
| 207 | * complex (e.g., a Markdown table) and should be rendered as its HTML | |
| 208 | * equivalent. | |
| 209 | * | |
| 210 | * @param r The R expression to evaluate then convert to HTML. | |
| 211 | * @return The result from the R expression as an HTML element. | |
| 212 | */ | |
| 213 | private String evalHtml( final String r ) { | |
| 214 | final var markdown = eval( r ); | |
| 215 | var node = mMarkdownProcessor.toNode( markdown ).getFirstChild(); | |
| 216 | ||
| 217 | if( node != null && node.isOrDescendantOfType( Paragraph.class ) ) { | |
| 218 | node = new Text( node.getChars() ); | |
| 219 | } | |
| 220 | ||
| 221 | // Trimming prevents displaced commas and unwanted newlines. | |
| 222 | return mMarkdownProcessor.toHtml( node ).trim(); | |
| 220 | 223 | } |
| 221 | 224 | |
| 222 | 225 | /** |
| 223 | 226 | * Evaluate an R expression and return the resulting object. |
| 224 | 227 | * |
| 225 | 228 | * @param r The expression to evaluate. |
| 226 | 229 | * @return The object resulting from the evaluation. |
| 227 | 230 | */ |
| 228 | private Object eval( final String r ) { | |
| 231 | private String eval( final String r ) { | |
| 229 | 232 | try { |
| 230 | return getScriptEngine().eval( r ); | |
| 233 | return ENGINE.eval( r ).toString(); | |
| 231 | 234 | } catch( final Exception ex ) { |
| 232 | final String expr = r.substring( 0, min( r.length(), 30 ) ); | |
| 233 | StatusBarNotifier.clue( "Main.status.error.r", expr, ex.getMessage() ); | |
| 235 | final var expr = r.substring( 0, min( r.length(), 30 ) ); | |
| 236 | clue( "Main.status.error.r", expr, ex.getMessage() ); | |
| 237 | return ""; | |
| 234 | 238 | } |
| 235 | ||
| 236 | return ""; | |
| 237 | 239 | } |
| 238 | 240 | |
| 239 | 241 | /** |
| 240 | 242 | * Return the given path if not {@code null}, otherwise return the path to |
| 241 | 243 | * the user's directory. |
| 242 | 244 | * |
| 243 | 245 | * @return A non-null path. |
| 244 | 246 | */ |
| 245 | 247 | private Path getWorkingDirectory() { |
| 246 | return getUserPreferences().getRDirectory().toPath(); | |
| 248 | return workingDirectoryProperty().getValue().toPath(); | |
| 247 | 249 | } |
| 248 | 250 | |
| 249 | private ObjectProperty<File> workingDirectoryProperty() { | |
| 250 | return getUserPreferences().rDirectoryProperty(); | |
| 251 | private Property<File> workingDirectoryProperty() { | |
| 252 | return getWorkspace().fileProperty( KEY_R_DIR ); | |
| 251 | 253 | } |
| 252 | 254 | |
| 253 | 255 | /** |
| 254 | 256 | * Loads the R init script from the application's persisted preferences. |
| 255 | 257 | * |
| 256 | 258 | * @return A non-null string, possibly empty. |
| 257 | 259 | */ |
| 258 | 260 | private String getBootstrapScript() { |
| 259 | return getUserPreferences().getRScript(); | |
| 260 | } | |
| 261 | ||
| 262 | private StringProperty bootstrapScriptProperty() { | |
| 263 | return getUserPreferences().rScriptProperty(); | |
| 261 | return bootstrapScriptProperty().getValue(); | |
| 264 | 262 | } |
| 265 | 263 | |
| 266 | private UserPreferences getUserPreferences() { | |
| 267 | return UserPreferences.getInstance(); | |
| 264 | private Property<String> bootstrapScriptProperty() { | |
| 265 | return getWorkspace().valuesProperty( KEY_R_SCRIPT ); | |
| 268 | 266 | } |
| 269 | 267 | |
| 270 | private ScriptEngine getScriptEngine() { | |
| 271 | return ENGINE; | |
| 268 | private Workspace getWorkspace() { | |
| 269 | return mWorkspace; | |
| 272 | 270 | } |
| 273 | 271 | } |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 30 | 4 | /** |
| 31 | * This is the default processor used when an unknown filename extension is | |
| 5 | * This is the default processor used when an unknown file name extension is | |
| 32 | 6 | * encountered. It processes the text by enclosing it in an HTML {@code <pre>} |
| 33 | 7 | * element. |
| 34 | 8 | */ |
| 35 | public class PreformattedProcessor extends AbstractProcessor<String> { | |
| 9 | public class PreformattedProcessor extends ExecutorProcessor<String> { | |
| 36 | 10 | |
| 37 | 11 | /** |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 4 | import java.util.Optional; | |
| 30 | 5 | import java.util.function.UnaryOperator; |
| 31 | 6 | |
| 32 | 7 | /** |
| 33 | 8 | * Responsible for processing documents from one known format to another. |
| 34 | 9 | * Processes the given content providing a transformation from one document |
| 35 | * format into another. For example, this could convert from XML to text using | |
| 36 | * an XSLT processor, or from markdown to HTML. | |
| 10 | * format into another. For example, this could convert Markdown to HTML. | |
| 37 | 11 | * |
| 38 | * @param <T> The type of processor to create. | |
| 12 | * @param <T> The data type to process. | |
| 39 | 13 | */ |
| 40 | 14 | public interface Processor<T> extends UnaryOperator<T> { |
| 41 | ||
| 42 | /** | |
| 43 | * Removes the given processor from the chain, returning a new immutable | |
| 44 | * chain equivalent to this chain, but without the given processor. | |
| 45 | * | |
| 46 | * @param processor The {@link Processor} to remove from the chain. | |
| 47 | * @return A delegating processor chain starting from this processor | |
| 48 | * onwards with the given processor removed from the chain. | |
| 49 | */ | |
| 50 | Processor<T> remove( Class<? extends Processor<T>> processor ); | |
| 51 | 15 | |
| 52 | 16 | /** |
| 53 | * Adds a document processor to call after this processor finishes processing | |
| 54 | * the document given to the process method. | |
| 17 | * Returns the next link in the processor chain. | |
| 55 | 18 | * |
| 56 | * @return The processor that should transform the document after this | |
| 57 | * instance has finished processing, or {@code null} if this is the last | |
| 58 | * processor in the chain. | |
| 19 | * @return The processor intended to transform the data after this instance | |
| 20 | * has finished processing, or {@link Optional#empty} if this is the last | |
| 21 | * link in the chain. | |
| 59 | 22 | */ |
| 60 | default Processor<T> next() { | |
| 61 | return null; | |
| 23 | default Optional<Processor<T>> next() { | |
| 24 | return Optional.empty(); | |
| 62 | 25 | } |
| 63 | 26 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 4 | import com.keenwrite.Constants; | |
| 30 | 5 | import com.keenwrite.ExportFormat; |
| 31 | import com.keenwrite.FileEditorTab; | |
| 32 | import com.keenwrite.FileType; | |
| 33 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 34 | import com.keenwrite.processors.markdown.CaretPosition; | |
| 6 | import com.keenwrite.io.FileType; | |
| 7 | import com.keenwrite.preferences.Workspace; | |
| 8 | import com.keenwrite.preview.HtmlPreview; | |
| 9 | import com.keenwrite.processors.markdown.Caret; | |
| 35 | 10 | |
| 36 | 11 | import java.nio.file.Path; |
| 37 | 12 | import java.util.Map; |
| 38 | 13 | |
| 39 | 14 | import static com.keenwrite.AbstractFileFactory.lookup; |
| 15 | import static com.keenwrite.Constants.DEFAULT_DIRECTORY; | |
| 40 | 16 | |
| 41 | 17 | /** |
| 42 | 18 | * Provides a context for configuring a chain of {@link Processor} instances. |
| 43 | 19 | */ |
| 44 | 20 | public class ProcessorContext { |
| 45 | private final HTMLPreviewPane mPreviewPane; | |
| 21 | private final HtmlPreview mHtmlPreview; | |
| 46 | 22 | private final Map<String, String> mResolvedMap; |
| 23 | private final Path mPath; | |
| 24 | private final Caret mCaret; | |
| 47 | 25 | private final ExportFormat mExportFormat; |
| 48 | private final FileEditorTab mTab; | |
| 26 | private final Workspace mWorkspace; | |
| 49 | 27 | |
| 50 | 28 | /** |
| 51 | 29 | * Creates a new context for use by the {@link ProcessorFactory} when |
| 52 | 30 | * instantiating new {@link Processor} instances. Although all the |
| 53 | 31 | * parameters are required, not all {@link Processor} instances will use |
| 54 | 32 | * all parameters. |
| 55 | 33 | * |
| 56 | * @param previewPane Where to display the final (HTML) output. | |
| 57 | * @param resolvedMap Fully expanded interpolated strings. | |
| 58 | * @param tab Tab containing path to the document to process. | |
| 59 | * @param format Indicate configuration options for export format. | |
| 34 | * @param htmlPreview Where to display the final (HTML) output. | |
| 35 | * @param resolvedMap Fully expanded interpolated strings. | |
| 36 | * @param path Path to the document to process. | |
| 37 | * @param caret Location of the caret in the edited document, which is | |
| 38 | * used to synchronize the scrollbars. | |
| 39 | * @param exportFormat Indicate configuration options for export format. | |
| 60 | 40 | */ |
| 61 | 41 | public ProcessorContext( |
| 62 | final HTMLPreviewPane previewPane, | |
| 42 | final HtmlPreview htmlPreview, | |
| 63 | 43 | final Map<String, String> resolvedMap, |
| 64 | final FileEditorTab tab, | |
| 65 | final ExportFormat format ) { | |
| 66 | assert previewPane != null; | |
| 44 | final Path path, | |
| 45 | final Caret caret, | |
| 46 | final ExportFormat exportFormat, | |
| 47 | final Workspace workspace ) { | |
| 48 | assert htmlPreview != null; | |
| 67 | 49 | assert resolvedMap != null; |
| 68 | assert tab != null; | |
| 69 | assert format != null; | |
| 50 | assert path != null; | |
| 51 | assert caret != null; | |
| 52 | assert exportFormat != null; | |
| 53 | assert workspace != null; | |
| 70 | 54 | |
| 71 | mPreviewPane = previewPane; | |
| 55 | mHtmlPreview = htmlPreview; | |
| 72 | 56 | mResolvedMap = resolvedMap; |
| 73 | mTab = tab; | |
| 74 | mExportFormat = format; | |
| 57 | mPath = path; | |
| 58 | mCaret = caret; | |
| 59 | mExportFormat = exportFormat; | |
| 60 | mWorkspace = workspace; | |
| 75 | 61 | } |
| 76 | 62 | |
| 77 | 63 | @SuppressWarnings("SameParameterValue") |
| 78 | 64 | boolean isExportFormat( final ExportFormat format ) { |
| 79 | 65 | return mExportFormat == format; |
| 80 | 66 | } |
| 81 | 67 | |
| 82 | HTMLPreviewPane getPreviewPane() { | |
| 83 | return mPreviewPane; | |
| 68 | HtmlPreview getPreview() { | |
| 69 | return mHtmlPreview; | |
| 84 | 70 | } |
| 85 | 71 | |
| 72 | /** | |
| 73 | * Returns the variable map of interpolated definitions. | |
| 74 | * | |
| 75 | * @return A map to help dereference variables. | |
| 76 | */ | |
| 86 | 77 | Map<String, String> getResolvedMap() { |
| 87 | 78 | return mResolvedMap; |
| ... | ||
| 98 | 89 | * @return Caret position in the document. |
| 99 | 90 | */ |
| 100 | public CaretPosition getCaretPosition() { | |
| 101 | return mTab.getCaretPosition(); | |
| 91 | public Caret getCaret() { | |
| 92 | return mCaret; | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Returns the directory that contains the file being edited. | |
| 97 | * When {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is | |
| 98 | * {@code null}. This will get absolute path to the file before trying to | |
| 99 | * get te parent path, which should always be a valid path. In the unlikely | |
| 100 | * event that the base path cannot be determined by the path alone, the | |
| 101 | * default user directory is returned. This is necessary for the creation | |
| 102 | * of new files. | |
| 103 | * | |
| 104 | * @return Path to the directory containing a file being edited, or the | |
| 105 | * default user directory if the base path cannot be determined. | |
| 106 | */ | |
| 107 | public Path getBasePath() { | |
| 108 | final var path = getPath().toAbsolutePath().getParent(); | |
| 109 | return path == null ? DEFAULT_DIRECTORY : path; | |
| 102 | 110 | } |
| 103 | 111 | |
| 104 | 112 | public Path getPath() { |
| 105 | return mTab.getPath(); | |
| 113 | return mPath; | |
| 106 | 114 | } |
| 107 | 115 | |
| 108 | 116 | FileType getFileType() { |
| 109 | 117 | return lookup( getPath() ); |
| 118 | } | |
| 119 | ||
| 120 | public Workspace getWorkspace() { | |
| 121 | return mWorkspace; | |
| 110 | 122 | } |
| 111 | 123 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.AbstractFileFactory; |
| 31 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 5 | import com.keenwrite.preview.HtmlPreview; | |
| 32 | 6 | import com.keenwrite.processors.markdown.MarkdownProcessor; |
| 33 | ||
| 34 | import java.nio.file.Path; | |
| 35 | import java.util.Map; | |
| 36 | 7 | |
| 37 | 8 | import static com.keenwrite.ExportFormat.NONE; |
| 38 | 9 | |
| 39 | 10 | /** |
| 40 | 11 | * Responsible for creating processors capable of parsing, transforming, |
| 41 | 12 | * interpolating, and rendering known file types. |
| 42 | 13 | */ |
| 43 | 14 | public class ProcessorFactory extends AbstractFileFactory { |
| 44 | 15 | |
| 45 | private final ProcessorContext mProcessorContext; | |
| 16 | private final ProcessorContext mContext; | |
| 46 | 17 | |
| 47 | 18 | /** |
| 48 | 19 | * Constructs a factory with the ability to create processors that can perform |
| 49 | 20 | * text and caret processing to generate a final preview. |
| 50 | 21 | * |
| 51 | * @param processorContext Parameters needed to construct various processors. | |
| 22 | * @param context Parameters needed to construct various processors. | |
| 52 | 23 | */ |
| 53 | private ProcessorFactory( final ProcessorContext processorContext ) { | |
| 54 | mProcessorContext = processorContext; | |
| 24 | private ProcessorFactory( final ProcessorContext context ) { | |
| 25 | mContext = context; | |
| 55 | 26 | } |
| 56 | 27 | |
| 57 | 28 | private Processor<String> createProcessor() { |
| 58 | final ProcessorContext context = getProcessorContext(); | |
| 29 | final var context = getProcessorContext(); | |
| 59 | 30 | |
| 60 | 31 | // If the content is not to be exported, then the successor processor |
| ... | ||
| 68 | 39 | // to SVG. Without conversion would require client-side rendering of |
| 69 | 40 | // math (such as using the JavaScript-based KaTeX engine). |
| 70 | final Processor<String> successor = context.isExportFormat( NONE ) | |
| 71 | ? createHtmlPreviewProcessor() | |
| 72 | : createIdentityProcessor(); | |
| 41 | final var successor = context.isExportFormat( NONE ) | |
| 42 | ? createHtmlPreviewProcessor() | |
| 43 | : createIdentityProcessor(); | |
| 73 | 44 | |
| 74 | return switch( context.getFileType() ) { | |
| 45 | final var processor = switch( context.getFileType() ) { | |
| 75 | 46 | case RMARKDOWN -> createRProcessor( successor ); |
| 76 | 47 | case SOURCE -> createMarkdownProcessor( successor ); |
| 77 | 48 | case RXML -> createRXMLProcessor( successor ); |
| 78 | 49 | case XML -> createXMLProcessor( successor ); |
| 79 | 50 | default -> createPreformattedProcessor( successor ); |
| 80 | 51 | }; |
| 52 | ||
| 53 | return new ExecutorProcessor<>( processor ); | |
| 81 | 54 | } |
| 82 | 55 | |
| ... | ||
| 89 | 62 | */ |
| 90 | 63 | public static Processor<String> createProcessors( |
| 91 | final ProcessorContext context ) { | |
| 64 | final ProcessorContext context ) { | |
| 92 | 65 | return new ProcessorFactory( context ).createProcessor(); |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Executes the processing chain, operating on the given string. | |
| 97 | * | |
| 98 | * @param handler The first processor in the chain to call. | |
| 99 | * @param text The initial value of the text to process. | |
| 100 | * @return The final value of the text that was processed by the chain. | |
| 101 | */ | |
| 102 | public static String processChain( Processor<String> handler, String text ) { | |
| 103 | while( handler != null && text != null ) { | |
| 104 | text = handler.apply( text ); | |
| 105 | handler = handler.next(); | |
| 106 | } | |
| 107 | ||
| 108 | return text; | |
| 109 | 66 | } |
| 110 | 67 | |
| ... | ||
| 137 | 94 | */ |
| 138 | 95 | private Processor<String> createMarkdownProcessor( |
| 139 | final Processor<String> successor ) { | |
| 96 | final Processor<String> successor ) { | |
| 140 | 97 | final var dp = createDefinitionProcessor( successor ); |
| 141 | 98 | return MarkdownProcessor.create( dp, getProcessorContext() ); |
| 142 | 99 | } |
| 143 | 100 | |
| 144 | 101 | private Processor<String> createDefinitionProcessor( |
| 145 | final Processor<String> successor ) { | |
| 146 | return new DefinitionProcessor( successor, getResolvedMap() ); | |
| 102 | final Processor<String> successor ) { | |
| 103 | return new DefinitionProcessor( successor, getProcessorContext() ); | |
| 147 | 104 | } |
| 148 | 105 | |
| 149 | 106 | private Processor<String> createRProcessor( |
| 150 | final Processor<String> successor ) { | |
| 151 | final var irp = new InlineRProcessor( successor, getResolvedMap() ); | |
| 152 | final var rvp = new RVariableProcessor( irp, getResolvedMap() ); | |
| 107 | final Processor<String> successor ) { | |
| 108 | final var irp = new InlineRProcessor( successor, getProcessorContext() ); | |
| 109 | final var rvp = new RVariableProcessor( irp, getProcessorContext() ); | |
| 153 | 110 | return MarkdownProcessor.create( rvp, getProcessorContext() ); |
| 154 | 111 | } |
| 155 | 112 | |
| 156 | 113 | protected Processor<String> createRXMLProcessor( |
| 157 | final Processor<String> successor ) { | |
| 158 | final var xmlp = new XmlProcessor( successor, getPath() ); | |
| 114 | final Processor<String> successor ) { | |
| 115 | final var xmlp = new XmlProcessor( successor, getProcessorContext() ); | |
| 159 | 116 | return createRProcessor( xmlp ); |
| 160 | 117 | } |
| 161 | 118 | |
| 162 | 119 | private Processor<String> createXMLProcessor( |
| 163 | final Processor<String> successor ) { | |
| 164 | final var xmlp = new XmlProcessor( successor, getPath() ); | |
| 120 | final Processor<String> successor ) { | |
| 121 | final var xmlp = new XmlProcessor( successor, getProcessorContext() ); | |
| 165 | 122 | return createDefinitionProcessor( xmlp ); |
| 166 | 123 | } |
| 167 | 124 | |
| 168 | 125 | private Processor<String> createPreformattedProcessor( |
| 169 | final Processor<String> successor ) { | |
| 126 | final Processor<String> successor ) { | |
| 170 | 127 | return new PreformattedProcessor( successor ); |
| 171 | 128 | } |
| 172 | 129 | |
| 173 | 130 | private ProcessorContext getProcessorContext() { |
| 174 | return mProcessorContext; | |
| 175 | } | |
| 176 | ||
| 177 | private HTMLPreviewPane getPreviewPane() { | |
| 178 | return getProcessorContext().getPreviewPane(); | |
| 179 | } | |
| 180 | ||
| 181 | /** | |
| 182 | * Returns the variable map of interpolated definitions. | |
| 183 | * | |
| 184 | * @return A map to help dereference variables. | |
| 185 | */ | |
| 186 | private Map<String, String> getResolvedMap() { | |
| 187 | return getProcessorContext().getResolvedMap(); | |
| 131 | return mContext; | |
| 188 | 132 | } |
| 189 | 133 | |
| 190 | /** | |
| 191 | * Returns the {@link Path} from the {@link ProcessorContext}. | |
| 192 | * | |
| 193 | * @return A non-null {@link Path} instance. | |
| 194 | */ | |
| 195 | private Path getPath() { | |
| 196 | return getProcessorContext().getPath(); | |
| 134 | private HtmlPreview getPreviewPane() { | |
| 135 | return getProcessorContext().getPreview(); | |
| 197 | 136 | } |
| 198 | 137 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| 4 | import com.keenwrite.preferences.Workspace; | |
| 30 | 5 | import com.keenwrite.sigils.RSigilOperator; |
| 6 | import com.keenwrite.sigils.SigilOperator; | |
| 7 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 31 | 8 | |
| 32 | 9 | import java.util.HashMap; |
| 33 | 10 | import java.util.Map; |
| 11 | ||
| 12 | import static com.keenwrite.preferences.Workspace.*; | |
| 34 | 13 | |
| 35 | 14 | /** |
| 36 | 15 | * Converts the keys of the resolved map from default form to R form, then |
| 37 | 16 | * performs a substitution on the text. The default R variable syntax is |
| 38 | 17 | * {@code v$tree$leaf}. |
| 39 | 18 | */ |
| 40 | 19 | public class RVariableProcessor extends DefinitionProcessor { |
| 20 | ||
| 21 | private final SigilOperator mSigilOperator; | |
| 41 | 22 | |
| 42 | 23 | public RVariableProcessor( |
| 43 | final Processor<String> rp, final Map<String, String> map ) { | |
| 44 | super( rp, map ); | |
| 24 | final InlineRProcessor irp, final ProcessorContext context ) { | |
| 25 | super( irp, context ); | |
| 26 | mSigilOperator = createSigilOperator( context.getWorkspace() ); | |
| 45 | 27 | } |
| 46 | 28 | |
| ... | ||
| 66 | 48 | for( final var entry : map.entrySet() ) { |
| 67 | 49 | final var key = entry.getKey(); |
| 68 | rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) ); | |
| 50 | rMap.put( mSigilOperator.entoken( key ), toRValue( map.get( key ) ) ); | |
| 69 | 51 | } |
| 70 | 52 | |
| ... | ||
| 86 | 68 | @SuppressWarnings("SameParameterValue") |
| 87 | 69 | private String escape( |
| 88 | final String haystack, final char needle, final String thread ) { | |
| 70 | final String haystack, final char needle, final String thread ) { | |
| 89 | 71 | int end = haystack.indexOf( needle ); |
| 90 | 72 | |
| ... | ||
| 97 | 79 | |
| 98 | 80 | // Replace up to 32 occurrences before the string reallocates its buffer. |
| 99 | final StringBuilder sb = new StringBuilder( length + 32 ); | |
| 81 | final var sb = new StringBuilder( length + 32 ); | |
| 100 | 82 | |
| 101 | 83 | while( end >= 0 ) { |
| 102 | 84 | sb.append( haystack, start, end ).append( thread ); |
| 103 | 85 | start = end + 1; |
| 104 | 86 | end = haystack.indexOf( needle, start ); |
| 105 | 87 | } |
| 106 | 88 | |
| 107 | 89 | return sb.append( haystack.substring( start ) ).toString(); |
| 90 | } | |
| 91 | ||
| 92 | private SigilOperator createSigilOperator( final Workspace workspace ) { | |
| 93 | final var tokens = workspace.toTokens( | |
| 94 | KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ); | |
| 95 | final var antecedent = createDefinitionOperator( workspace ); | |
| 96 | return new RSigilOperator( tokens, antecedent ); | |
| 97 | } | |
| 98 | ||
| 99 | private SigilOperator createDefinitionOperator( | |
| 100 | final Workspace workspace ) { | |
| 101 | final var tokens = workspace.toTokens( | |
| 102 | KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED ); | |
| 103 | return new YamlSigilOperator( tokens ); | |
| 108 | 104 | } |
| 109 | 105 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors; |
| 29 | 3 | |
| ... | ||
| 37 | 11 | import javax.xml.stream.XMLStreamException; |
| 38 | 12 | import javax.xml.stream.events.ProcessingInstruction; |
| 39 | import javax.xml.stream.events.XMLEvent; | |
| 40 | 13 | import javax.xml.transform.*; |
| 41 | 14 | import javax.xml.transform.stream.StreamResult; |
| 42 | 15 | import javax.xml.transform.stream.StreamSource; |
| 43 | import java.io.File; | |
| 44 | 16 | import java.io.Reader; |
| 45 | 17 | import java.io.StringReader; |
| ... | ||
| 61 | 33 | * </p> |
| 62 | 34 | */ |
| 63 | public class XmlProcessor extends AbstractProcessor<String> | |
| 35 | public class XmlProcessor extends ExecutorProcessor<String> | |
| 64 | 36 | implements ErrorListener { |
| 65 | 37 | |
| 66 | private final Snitch snitch = Services.load( Snitch.class ); | |
| 38 | private final Snitch mSnitch = Services.load( Snitch.class ); | |
| 67 | 39 | |
| 68 | private XMLInputFactory xmlInputFactory; | |
| 69 | private TransformerFactory transformerFactory; | |
| 70 | private Transformer transformer; | |
| 40 | private final XMLInputFactory mXmlInputFactory = | |
| 41 | XMLInputFactory.newInstance(); | |
| 42 | private final TransformerFactory mTransformerFactory = | |
| 43 | new TransformerFactoryImpl(); | |
| 44 | private Transformer mTransformer; | |
| 71 | 45 | |
| 72 | private Path path; | |
| 46 | private final Path mPath; | |
| 73 | 47 | |
| 74 | 48 | /** |
| 75 | 49 | * Constructs an XML processor that can transform an XML document into another |
| 76 | 50 | * format based on the XSL file specified as a processing instruction. The |
| 77 | 51 | * path must point to the directory where the XSL file is found, which implies |
| 78 | 52 | * that they must be in the same directory. |
| 79 | 53 | * |
| 80 | * @param processor Next link in the processing chain. | |
| 81 | * @param path The path to the XML file content to be processed. | |
| 54 | * @param successor Next link in the processing chain. | |
| 55 | * @param context Contains path to the XML file content to be processed. | |
| 82 | 56 | */ |
| 83 | public XmlProcessor( final Processor<String> processor, final Path path ) { | |
| 84 | super( processor ); | |
| 85 | setPath( path ); | |
| 57 | public XmlProcessor( | |
| 58 | final Processor<String> successor, | |
| 59 | final ProcessorContext context ) { | |
| 60 | super( successor ); | |
| 61 | mPath = context.getPath(); | |
| 62 | ||
| 63 | // Bubble problems up to the user interface, rather than standard error. | |
| 64 | mTransformerFactory.setErrorListener( this ); | |
| 86 | 65 | } |
| 87 | 66 | |
| ... | ||
| 119 | 98 | |
| 120 | 99 | // Listen for external file modification events. |
| 121 | getSnitch().listen( xsl ); | |
| 100 | mSnitch.listen( xsl ); | |
| 122 | 101 | |
| 123 | 102 | getTransformer( xsl ).transform( |
| ... | ||
| 141 | 120 | * transformer. |
| 142 | 121 | */ |
| 143 | private Transformer getTransformer( final Path xsl ) | |
| 122 | private synchronized Transformer getTransformer( final Path xsl ) | |
| 144 | 123 | throws TransformerConfigurationException { |
| 145 | if( this.transformer == null ) { | |
| 146 | this.transformer = createTransformer( xsl ); | |
| 124 | if( mTransformer == null ) { | |
| 125 | mTransformer = createTransformer( xsl ); | |
| 147 | 126 | } |
| 148 | 127 | |
| 149 | return this.transformer; | |
| 128 | return mTransformer; | |
| 150 | 129 | } |
| 151 | 130 | |
| ... | ||
| 160 | 139 | protected Transformer createTransformer( final Path xsl ) |
| 161 | 140 | throws TransformerConfigurationException { |
| 162 | final Source xslt = new StreamSource( xsl.toFile() ); | |
| 141 | final var xslt = new StreamSource( xsl.toFile() ); | |
| 163 | 142 | |
| 164 | 143 | return getTransformerFactory().newTransformer( xslt ); |
| 165 | 144 | } |
| 166 | 145 | |
| 167 | 146 | private Path getXslPath( final String filename ) { |
| 168 | final Path xmlPath = getPath(); | |
| 169 | final File xmlDirectory = xmlPath.toFile().getParentFile(); | |
| 147 | final var xmlDirectory = mPath.toFile().getParentFile(); | |
| 170 | 148 | |
| 171 | 149 | return Paths.get( xmlDirectory.getPath(), filename ); |
| 172 | 150 | } |
| 173 | 151 | |
| 174 | 152 | /** |
| 175 | 153 | * Given XML text, this will use a StAX pull reader to obtain the XML |
| 176 | 154 | * stylesheet processing instruction. This will throw a parse exception if the |
| 177 | * href pseudo-attribute filename value cannot be found. | |
| 155 | * href pseudo-attribute file name value cannot be found. | |
| 178 | 156 | * |
| 179 | 157 | * @param xml The XML containing an xml-stylesheet processing instruction. |
| 180 | 158 | * @return The href pseudo-attribute value. |
| 181 | 159 | * @throws XMLStreamException Could not parse the XML file. |
| 182 | 160 | */ |
| 183 | 161 | private String getXsltFilename( final String xml ) |
| 184 | 162 | throws XMLStreamException, XPathException { |
| 185 | ||
| 186 | 163 | String result = ""; |
| 187 | 164 | |
| 188 | 165 | try( final StringReader sr = new StringReader( xml ) ) { |
| 166 | final XMLEventReader reader = createXmlEventReader( sr ); | |
| 189 | 167 | boolean found = false; |
| 190 | 168 | int count = 0; |
| 191 | final XMLEventReader reader = createXMLEventReader( sr ); | |
| 192 | 169 | |
| 193 | 170 | // If the processing instruction wasn't found in the first 10 lines, |
| 194 | 171 | // fail fast. This should iterate twice through the loop. |
| 195 | 172 | while( !found && reader.hasNext() && count++ < 10 ) { |
| 196 | final XMLEvent event = reader.nextEvent(); | |
| 173 | final var event = reader.nextEvent(); | |
| 197 | 174 | |
| 198 | 175 | if( event.isProcessingInstruction() ) { |
| 199 | final ProcessingInstruction pi = (ProcessingInstruction) event; | |
| 200 | final String target = pi.getTarget(); | |
| 176 | final var pi = (ProcessingInstruction) event; | |
| 177 | final var target = pi.getTarget(); | |
| 201 | 178 | |
| 202 | 179 | if( "xml-stylesheet".equalsIgnoreCase( target ) ) { |
| ... | ||
| 211 | 188 | } |
| 212 | 189 | |
| 213 | private XMLEventReader createXMLEventReader( final Reader reader ) | |
| 190 | private XMLEventReader createXmlEventReader( final Reader reader ) | |
| 214 | 191 | throws XMLStreamException { |
| 215 | return getXMLInputFactory().createXMLEventReader( reader ); | |
| 216 | } | |
| 217 | ||
| 218 | private synchronized XMLInputFactory getXMLInputFactory() { | |
| 219 | if( this.xmlInputFactory == null ) { | |
| 220 | this.xmlInputFactory = createXMLInputFactory(); | |
| 221 | } | |
| 222 | ||
| 223 | return this.xmlInputFactory; | |
| 224 | } | |
| 225 | ||
| 226 | private XMLInputFactory createXMLInputFactory() { | |
| 227 | return XMLInputFactory.newInstance(); | |
| 192 | return mXmlInputFactory.createXMLEventReader( reader ); | |
| 228 | 193 | } |
| 229 | 194 | |
| 230 | 195 | private synchronized TransformerFactory getTransformerFactory() { |
| 231 | if( this.transformerFactory == null ) { | |
| 232 | this.transformerFactory = createTransformerFactory(); | |
| 233 | } | |
| 234 | ||
| 235 | return this.transformerFactory; | |
| 236 | } | |
| 237 | ||
| 238 | /** | |
| 239 | * Returns a high-performance XSLT 2 transformation engine. | |
| 240 | * | |
| 241 | * @return An XSL transforming engine. | |
| 242 | */ | |
| 243 | private TransformerFactory createTransformerFactory() { | |
| 244 | final TransformerFactory factory = new TransformerFactoryImpl(); | |
| 245 | ||
| 246 | // Bubble problems up to the user interface, rather than standard error. | |
| 247 | factory.setErrorListener( this ); | |
| 248 | ||
| 249 | return factory; | |
| 196 | return mTransformerFactory; | |
| 250 | 197 | } |
| 251 | 198 | |
| ... | ||
| 279 | 226 | public void fatalError( final TransformerException ex ) { |
| 280 | 227 | throw new RuntimeException( ex ); |
| 281 | } | |
| 282 | ||
| 283 | private void setPath( final Path path ) { | |
| 284 | this.path = path; | |
| 285 | } | |
| 286 | ||
| 287 | private Path getPath() { | |
| 288 | return this.path; | |
| 289 | } | |
| 290 | ||
| 291 | private Snitch getSnitch() { | |
| 292 | return this.snitch; | |
| 293 | 228 | } |
| 294 | 229 | } |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.markdown; | |
| 3 | ||
| 4 | import com.keenwrite.util.GenericBuilder; | |
| 5 | import javafx.beans.value.ObservableValue; | |
| 6 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 7 | import org.fxmisc.richtext.model.Paragraph; | |
| 8 | import org.reactfx.collection.LiveList; | |
| 9 | ||
| 10 | import java.util.Collection; | |
| 11 | ||
| 12 | import static com.keenwrite.Constants.STATUS_BAR_LINE; | |
| 13 | import static com.keenwrite.Messages.get; | |
| 14 | ||
| 15 | /** | |
| 16 | * Represents the absolute, relative, and maximum position of the caret. The | |
| 17 | * caret position is a character offset into the text. | |
| 18 | */ | |
| 19 | public class Caret { | |
| 20 | ||
| 21 | public static GenericBuilder<Caret.Mutator, Caret> builder() { | |
| 22 | return GenericBuilder.of( Caret.Mutator::new, Caret::new ); | |
| 23 | } | |
| 24 | ||
| 25 | /** | |
| 26 | * Used for building a new {@link Caret} instance. | |
| 27 | */ | |
| 28 | public static class Mutator { | |
| 29 | /** | |
| 30 | * Caret's current paragraph index (i.e., current caret line number). | |
| 31 | */ | |
| 32 | private ObservableValue<Integer> mParagraph; | |
| 33 | ||
| 34 | /** | |
| 35 | * Used to count the number of lines in the text editor document. | |
| 36 | */ | |
| 37 | private LiveList<Paragraph<Collection<String>, String, | |
| 38 | Collection<String>>> mParagraphs; | |
| 39 | ||
| 40 | /** | |
| 41 | * Caret offset into the full text, represented as a string index. | |
| 42 | */ | |
| 43 | private ObservableValue<Integer> mTextOffset; | |
| 44 | ||
| 45 | /** | |
| 46 | * Caret offset into the current paragraph, represented as a string index. | |
| 47 | */ | |
| 48 | private ObservableValue<Integer> mParaOffset; | |
| 49 | ||
| 50 | /** | |
| 51 | * Total number of characters in the document. | |
| 52 | */ | |
| 53 | private ObservableValue<Integer> mTextLength; | |
| 54 | ||
| 55 | /** | |
| 56 | * Configures this caret position using properties from the given editor. | |
| 57 | * | |
| 58 | * @param editor The text editor that has a caret with position properties. | |
| 59 | */ | |
| 60 | public void setEditor( final StyleClassedTextArea editor ) { | |
| 61 | mParagraph = editor.currentParagraphProperty(); | |
| 62 | mParagraphs = editor.getParagraphs(); | |
| 63 | mParaOffset = editor.caretColumnProperty(); | |
| 64 | mTextOffset = editor.caretPositionProperty(); | |
| 65 | mTextLength = editor.lengthProperty(); | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | private final Mutator mMutator; | |
| 70 | ||
| 71 | /** | |
| 72 | * Force using the builder pattern. | |
| 73 | */ | |
| 74 | private Caret( final Mutator mutator ) { | |
| 75 | assert mutator != null; | |
| 76 | ||
| 77 | mMutator = mutator; | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Allows observers to be notified when the value of the caret changes. | |
| 82 | * | |
| 83 | * @return An observer for the caret's document offset. | |
| 84 | */ | |
| 85 | public ObservableValue<Integer> textOffsetProperty() { | |
| 86 | return mMutator.mTextOffset; | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Answers whether the caret's offset into the text is between the given | |
| 91 | * offsets. | |
| 92 | * | |
| 93 | * @param began Starting value compared against the caret's text offset. | |
| 94 | * @param ended Ending value compared against the caret's text offset. | |
| 95 | * @return {@code true} when the caret's text offset is between the given | |
| 96 | * values, inclusively (for either value). | |
| 97 | */ | |
| 98 | public boolean isBetweenText( final int began, final int ended ) { | |
| 99 | final var offset = getTextOffset(); | |
| 100 | return began <= offset && offset <= ended; | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Answers whether the caret's offset into the paragraph is before the given | |
| 105 | * offset. | |
| 106 | * | |
| 107 | * @param offset Compared against the caret's paragraph offset. | |
| 108 | * @return {@code true} the caret's offset is before the given offset. | |
| 109 | */ | |
| 110 | public boolean isBeforeColumn( final int offset ) { | |
| 111 | return getParaOffset() < offset; | |
| 112 | } | |
| 113 | ||
| 114 | /** | |
| 115 | * Answers whether the caret's offset into the text is before the given | |
| 116 | * text offset. | |
| 117 | * | |
| 118 | * @param offset Compared against the caret's text offset. | |
| 119 | * @return {@code true} the caret's offset is after the given offset. | |
| 120 | */ | |
| 121 | public boolean isAfterColumn( final int offset ) { | |
| 122 | return getParaOffset() > offset; | |
| 123 | } | |
| 124 | ||
| 125 | /** | |
| 126 | * Answers whether the caret's offset into the text exceeds the length of | |
| 127 | * the text. | |
| 128 | * | |
| 129 | * @return {@code true} when the caret is at the end of the text boundary. | |
| 130 | */ | |
| 131 | public boolean isAfterText() { | |
| 132 | return getTextOffset() >= getTextLength(); | |
| 133 | } | |
| 134 | ||
| 135 | public boolean isAfter( final int offset ) { | |
| 136 | return offset >= getTextOffset(); | |
| 137 | } | |
| 138 | ||
| 139 | private int getParagraph() { | |
| 140 | return mMutator.mParagraph.getValue(); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Returns the number of lines in the text editor. | |
| 145 | * | |
| 146 | * @return The size of the text editor's paragraph list plus one. | |
| 147 | */ | |
| 148 | private int getParagraphCount() { | |
| 149 | return mMutator.mParagraphs.size() + 1; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Returns the absolute position of the caret within the entire document. | |
| 154 | * | |
| 155 | * @return A zero-based index of the caret position. | |
| 156 | */ | |
| 157 | private int getTextOffset() { | |
| 158 | return mMutator.mTextOffset.getValue(); | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * Returns the position of the caret within the current paragraph being | |
| 163 | * edited. | |
| 164 | * | |
| 165 | * @return A zero-based index of the caret position relative to the | |
| 166 | * current paragraph. | |
| 167 | */ | |
| 168 | private int getParaOffset() { | |
| 169 | return mMutator.mParaOffset.getValue(); | |
| 170 | } | |
| 171 | ||
| 172 | /** | |
| 173 | * Returns the total number of characters in the document being edited. | |
| 174 | * | |
| 175 | * @return A zero-based count of the total characters in the document. | |
| 176 | */ | |
| 177 | private int getTextLength() { | |
| 178 | return mMutator.mTextLength.getValue(); | |
| 179 | } | |
| 180 | ||
| 181 | /** | |
| 182 | * Returns a human-readable string that shows the current caret position | |
| 183 | * within the text. Typically this will include the current line number, | |
| 184 | * the number of lines, and the character offset into the text. | |
| 185 | * | |
| 186 | * @return A string to present to an end user. | |
| 187 | */ | |
| 188 | @Override | |
| 189 | public String toString() { | |
| 190 | return get( STATUS_BAR_LINE, | |
| 191 | getParagraph() + 1, | |
| 192 | getParagraphCount(), | |
| 193 | getTextOffset() + 1 ); | |
| 194 | } | |
| 195 | } | |
| 1 | 196 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown; |
| 29 | 3 | |
| ... | ||
| 49 | 23 | */ |
| 50 | 24 | public class CaretExtension implements HtmlRendererExtension { |
| 25 | ||
| 26 | private final Caret mCaret; | |
| 27 | ||
| 28 | private CaretExtension( final Caret caret ) { | |
| 29 | mCaret = caret; | |
| 30 | } | |
| 31 | ||
| 32 | public static CaretExtension create( final Caret caret ) { | |
| 33 | return new CaretExtension( caret ); | |
| 34 | } | |
| 35 | ||
| 36 | @Override | |
| 37 | public void extend( | |
| 38 | final Builder builder, @NotNull final String rendererType ) { | |
| 39 | builder.attributeProviderFactory( | |
| 40 | IdAttributeProvider.createFactory( mCaret ) ); | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 45 | } | |
| 51 | 46 | |
| 52 | 47 | /** |
| 53 | 48 | * Responsible for creating the id attribute. This class is instantiated |
| 54 | 49 | * once: for the HTML element containing the {@link Constants#CARET_ID}. |
| 55 | 50 | */ |
| 56 | 51 | public static class IdAttributeProvider implements AttributeProvider { |
| 57 | private final CaretPosition mCaret; | |
| 52 | private final Caret mCaret; | |
| 58 | 53 | |
| 59 | public IdAttributeProvider( final CaretPosition caret ) { | |
| 54 | public IdAttributeProvider( final Caret caret ) { | |
| 60 | 55 | mCaret = caret; |
| 61 | 56 | } |
| 62 | 57 | |
| 63 | 58 | private static AttributeProviderFactory createFactory( |
| 64 | final CaretPosition caret ) { | |
| 59 | final Caret caret ) { | |
| 65 | 60 | return new IndependentAttributeProviderFactory() { |
| 66 | 61 | @Override |
| 67 | 62 | public @NotNull AttributeProvider apply( |
| 68 | @NotNull final LinkResolverContext context ) { | |
| 63 | @NotNull final LinkResolverContext context ) { | |
| 69 | 64 | return new IdAttributeProvider( caret ); |
| 70 | 65 | } |
| 71 | 66 | }; |
| 72 | 67 | } |
| 73 | 68 | |
| 74 | 69 | @Override |
| 75 | 70 | public void setAttributes( @NotNull Node curr, |
| 76 | 71 | @NotNull AttributablePart part, |
| 77 | 72 | @NotNull MutableAttributes attributes ) { |
| 73 | final var outside = mCaret.isAfterText() ? 1 : 0; | |
| 78 | 74 | final var began = curr.getStartOffset(); |
| 79 | final var ended = curr.getEndOffset(); | |
| 75 | final var ended = curr.getEndOffset() + outside; | |
| 80 | 76 | final var prev = curr.getPrevious(); |
| 81 | 77 | |
| 82 | 78 | // If the caret is within the bounds of the current node or the |
| 83 | 79 | // caret is within the bounds of the end of the previous node and |
| 84 | 80 | // the start of the current node, then mark the current node with |
| 85 | 81 | // a caret indicator. |
| 86 | 82 | if( mCaret.isBetweenText( began, ended ) || |
| 87 | prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) { | |
| 88 | ||
| 89 | // This magic line enables synchronizing the text editor with preview. | |
| 83 | prev != null && mCaret.isBetweenText( prev.getEndOffset(), began ) ) { | |
| 84 | // This line empowers synchronizing the text editor with the preview. | |
| 90 | 85 | attributes.addValue( AttributeImpl.of( "id", CARET_ID ) ); |
| 91 | 86 | } |
| 92 | 87 | } |
| 93 | } | |
| 94 | ||
| 95 | private final CaretPosition mCaret; | |
| 96 | ||
| 97 | private CaretExtension( final CaretPosition caret ) { | |
| 98 | mCaret = caret; | |
| 99 | } | |
| 100 | ||
| 101 | @Override | |
| 102 | public void extend( | |
| 103 | final Builder builder, @NotNull final String rendererType ) { | |
| 104 | builder.attributeProviderFactory( | |
| 105 | IdAttributeProvider.createFactory( mCaret ) ); | |
| 106 | } | |
| 107 | ||
| 108 | public static CaretExtension create( final CaretPosition caret ) { | |
| 109 | return new CaretExtension( caret ); | |
| 110 | } | |
| 111 | ||
| 112 | @Override | |
| 113 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 114 | 88 | } |
| 115 | 89 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.util.GenericBuilder; | |
| 31 | import javafx.beans.value.ObservableValue; | |
| 32 | import org.fxmisc.richtext.model.Paragraph; | |
| 33 | import org.reactfx.collection.LiveList; | |
| 34 | ||
| 35 | import java.util.Collection; | |
| 36 | ||
| 37 | import static com.keenwrite.Constants.STATUS_BAR_LINE; | |
| 38 | import static com.keenwrite.Messages.get; | |
| 39 | ||
| 40 | /** | |
| 41 | * Represents the absolute, relative, and maximum position of the caret. | |
| 42 | * The caret position is a character offset into the text. | |
| 43 | */ | |
| 44 | public class CaretPosition { | |
| 45 | ||
| 46 | public static GenericBuilder<CaretPosition.Mutator, CaretPosition> builder() { | |
| 47 | return GenericBuilder.of( CaretPosition.Mutator::new, CaretPosition::new ); | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Used for building a new {@link CaretPosition} instance. | |
| 52 | */ | |
| 53 | public static class Mutator { | |
| 54 | /** | |
| 55 | * Caret's current paragraph index (i.e., current caret line number). | |
| 56 | */ | |
| 57 | private ObservableValue<Integer> mParagraph; | |
| 58 | ||
| 59 | /** | |
| 60 | * Used to count the number of lines in the text editor document. | |
| 61 | */ | |
| 62 | private LiveList<Paragraph<Collection<String>, String, | |
| 63 | Collection<String>>> mParagraphs; | |
| 64 | ||
| 65 | /** | |
| 66 | * Caret offset into the full text, represented as a string index. | |
| 67 | */ | |
| 68 | private ObservableValue<Integer> mTextOffset; | |
| 69 | ||
| 70 | /** | |
| 71 | * Caret offset into the current paragraph, represented as a string index. | |
| 72 | */ | |
| 73 | private ObservableValue<Integer> mParaOffset; | |
| 74 | ||
| 75 | public void setParagraph( final ObservableValue<Integer> paragraph ) { | |
| 76 | mParagraph = paragraph; | |
| 77 | } | |
| 78 | ||
| 79 | public void setParagraphs( | |
| 80 | final LiveList<Paragraph<Collection<String>, String, | |
| 81 | Collection<String>>> paragraphs ) { | |
| 82 | mParagraphs = paragraphs; | |
| 83 | } | |
| 84 | ||
| 85 | public void setTextOffset( final ObservableValue<Integer> textOffset ) { | |
| 86 | mTextOffset = textOffset; | |
| 87 | } | |
| 88 | ||
| 89 | public void setParaOffset( final ObservableValue<Integer> paraOffset ) { | |
| 90 | mParaOffset = paraOffset; | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | private final Mutator mMutator; | |
| 95 | ||
| 96 | /** | |
| 97 | * Force using the builder pattern. | |
| 98 | */ | |
| 99 | private CaretPosition( final Mutator mutator ) { | |
| 100 | mMutator = mutator; | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Answers whether the caret's offset into the text is between the given | |
| 105 | * offsets. | |
| 106 | * | |
| 107 | * @param began Starting value compared against the caret's text offset. | |
| 108 | * @param ended Ending value compared against the caret's text offset. | |
| 109 | * @return {@code true} when the caret's text offset is between the given | |
| 110 | * values, inclusively (for either value). | |
| 111 | */ | |
| 112 | public boolean isBetweenText( final int began, final int ended ) { | |
| 113 | final int offset = getTextOffset(); | |
| 114 | return began <= offset && offset <= ended; | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Answers whether the caret's offset into the paragraph is before the given | |
| 119 | * offset. | |
| 120 | * | |
| 121 | * @param offset Compared against the caret's paragraph offset. | |
| 122 | * @return {@code true} the caret's offset is before the given offset. | |
| 123 | */ | |
| 124 | public boolean isBeforeColumn( final int offset ) { | |
| 125 | return getParaOffset() < offset; | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Answers whether the caret's offset into the text is before the given | |
| 130 | * text offset. | |
| 131 | * | |
| 132 | * @param offset Compared against the caret's text offset. | |
| 133 | * @return {@code true} the caret's offset is after the given offset. | |
| 134 | */ | |
| 135 | public boolean isAfterColumn( final int offset ) { | |
| 136 | return getParaOffset() > offset; | |
| 137 | } | |
| 138 | ||
| 139 | private int getParagraph() { | |
| 140 | return mMutator.mParagraph.getValue(); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Returns the number of lines in the text editor. | |
| 145 | * | |
| 146 | * @return The size of the text editor's paragraph list plus one. | |
| 147 | */ | |
| 148 | private int getParagraphCount() { | |
| 149 | return mMutator.mParagraphs.size() + 1; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Returns the absolute position of the caret within the entire document. | |
| 154 | * | |
| 155 | * @return A zero-based index of the caret position. | |
| 156 | */ | |
| 157 | private int getTextOffset() { | |
| 158 | return mMutator.mTextOffset.getValue(); | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * Returns the position of the caret within the current paragraph being | |
| 163 | * edited. | |
| 164 | * | |
| 165 | * @return A zero-based index of the caret position relative to the | |
| 166 | * current paragraph. | |
| 167 | */ | |
| 168 | private int getParaOffset() { | |
| 169 | return mMutator.mParaOffset.getValue(); | |
| 170 | } | |
| 171 | ||
| 172 | /** | |
| 173 | * Returns a human-readable string that shows the current caret position | |
| 174 | * within the text. Typically this will include the current line number, | |
| 175 | * the number of lines, and the character offset into the text. | |
| 176 | * | |
| 177 | * @return A string to present to an end user. | |
| 178 | */ | |
| 179 | @Override | |
| 180 | public String toString() { | |
| 181 | return get( STATUS_BAR_LINE, | |
| 182 | getParagraph(), | |
| 183 | getParagraphCount(), | |
| 184 | getTextOffset() ); | |
| 185 | } | |
| 186 | } | |
| 187 | 1 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.markdown; | |
| 3 | ||
| 4 | import com.keenwrite.processors.DefinitionProcessor; | |
| 5 | import com.keenwrite.processors.IdentityProcessor; | |
| 6 | import com.keenwrite.processors.ProcessorContext; | |
| 7 | import com.vladsch.flexmark.ast.FencedCodeBlock; | |
| 8 | import com.vladsch.flexmark.html.renderer.DelegatingNodeRendererFactory; | |
| 9 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 10 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 11 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 12 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 13 | import com.vladsch.flexmark.util.sequence.BasedSequence; | |
| 14 | import org.jetbrains.annotations.NotNull; | |
| 15 | ||
| 16 | import java.io.ByteArrayOutputStream; | |
| 17 | import java.util.HashSet; | |
| 18 | import java.util.Set; | |
| 19 | import java.util.zip.Deflater; | |
| 20 | ||
| 21 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 22 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 23 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 24 | import static com.vladsch.flexmark.html.renderer.LinkType.LINK; | |
| 25 | import static java.lang.String.format; | |
| 26 | import static java.util.Base64.getUrlEncoder; | |
| 27 | import static java.util.zip.Deflater.BEST_COMPRESSION; | |
| 28 | import static java.util.zip.Deflater.FULL_FLUSH; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for converting textual diagram descriptions into HTML image | |
| 32 | * elements. | |
| 33 | */ | |
| 34 | public class FencedBlockExtension implements HtmlRendererExtension { | |
| 35 | private final static String DIAGRAM_STYLE = "diagram-"; | |
| 36 | private final static int DIAGRAM_STYLE_LEN = DIAGRAM_STYLE.length(); | |
| 37 | ||
| 38 | private final DefinitionProcessor mProcessor; | |
| 39 | ||
| 40 | public FencedBlockExtension( final ProcessorContext context ) { | |
| 41 | assert context != null; | |
| 42 | mProcessor = new DefinitionProcessor( IdentityProcessor.INSTANCE, context ); | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Creates a new parser for fenced blocks. This calls out to a web service | |
| 47 | * to generate SVG files of text diagrams. | |
| 48 | * <p> | |
| 49 | * Internally, this creates a {@link DefinitionProcessor} to substitute | |
| 50 | * variable definitions. This is necessary because the order of processors | |
| 51 | * matters. If the {@link DefinitionProcessor} comes before an instance of | |
| 52 | * {@link MarkdownProcessor}, for example, then the caret position in the | |
| 53 | * preview pane will not align with the caret position in the editor | |
| 54 | * pane. The {@link MarkdownProcessor} must come before all else. However, | |
| 55 | * when parsing fenced blocks, the variables within the block must be | |
| 56 | * interpolated before being sent to the diagram web service. | |
| 57 | * </p> | |
| 58 | * | |
| 59 | * @param context Used to create a new {@link DefinitionProcessor}. | |
| 60 | * @return A new {@link FencedBlockExtension} capable of shunting ASCII | |
| 61 | * diagrams to a service for conversion to SVG. | |
| 62 | */ | |
| 63 | public static FencedBlockExtension create( final ProcessorContext context ) { | |
| 64 | return new FencedBlockExtension( context ); | |
| 65 | } | |
| 66 | ||
| 67 | @Override | |
| 68 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 69 | } | |
| 70 | ||
| 71 | @Override | |
| 72 | public void extend( | |
| 73 | @NotNull final Builder builder, @NotNull final String rendererType ) { | |
| 74 | builder.nodeRendererFactory( new Factory() ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Converts the given {@link BasedSequence} to a lowercase value. | |
| 79 | * | |
| 80 | * @param text The character string to convert to lowercase. | |
| 81 | * @return The lowercase text value, or the empty string for no text. | |
| 82 | */ | |
| 83 | private static String sanitize( final BasedSequence text ) { | |
| 84 | assert text != null; | |
| 85 | return text.toString().toLowerCase(); | |
| 86 | } | |
| 87 | ||
| 88 | private class CustomRenderer implements NodeRenderer { | |
| 89 | ||
| 90 | @Override | |
| 91 | public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 92 | final var set = new HashSet<NodeRenderingHandler<?>>(); | |
| 93 | ||
| 94 | set.add( new NodeRenderingHandler<>( | |
| 95 | FencedCodeBlock.class, ( node, context, html ) -> { | |
| 96 | final var style = sanitize( node.getInfo() ); | |
| 97 | ||
| 98 | if( style.startsWith( DIAGRAM_STYLE ) ) { | |
| 99 | final var type = style.substring( DIAGRAM_STYLE_LEN ); | |
| 100 | final var content = node.getContentChars().normalizeEOL(); | |
| 101 | final var text = FencedBlockExtension.this.mProcessor.apply( content ); | |
| 102 | final var encoded = encode( text ); | |
| 103 | final var source = format( | |
| 104 | "https://kroki.io/%s/svg/%s", type, encoded ); | |
| 105 | ||
| 106 | final var link = context.resolveLink( LINK, source, false ); | |
| 107 | ||
| 108 | html.attr( "src", source ); | |
| 109 | html.withAttr( link ); | |
| 110 | html.tagVoid( "img" ); | |
| 111 | } | |
| 112 | else { | |
| 113 | context.delegateRender(); | |
| 114 | } | |
| 115 | } ) ); | |
| 116 | ||
| 117 | return set; | |
| 118 | } | |
| 119 | ||
| 120 | private byte[] compress( byte[] source ) { | |
| 121 | final var inLen = source.length; | |
| 122 | final var result = new byte[ inLen ]; | |
| 123 | final var deflater = new Deflater( BEST_COMPRESSION ); | |
| 124 | ||
| 125 | deflater.setInput( source, 0, inLen ); | |
| 126 | deflater.finish(); | |
| 127 | final var outLen = deflater.deflate( result, 0, inLen, FULL_FLUSH ); | |
| 128 | deflater.end(); | |
| 129 | ||
| 130 | try( final var out = new ByteArrayOutputStream() ) { | |
| 131 | out.write( result, 0, outLen ); | |
| 132 | return out.toByteArray(); | |
| 133 | } catch( final Exception ex ) { | |
| 134 | clue( ex ); | |
| 135 | throw new RuntimeException( ex ); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | private String encode( final String decoded ) { | |
| 140 | return getUrlEncoder().encodeToString( compress( decoded.getBytes() ) ); | |
| 141 | } | |
| 142 | } | |
| 143 | ||
| 144 | private class Factory implements DelegatingNodeRendererFactory { | |
| 145 | public Factory() {} | |
| 146 | ||
| 147 | @NotNull | |
| 148 | @Override | |
| 149 | public NodeRenderer apply( @NotNull final DataHolder options ) { | |
| 150 | return new CustomRenderer(); | |
| 151 | } | |
| 152 | ||
| 153 | /** | |
| 154 | * Return {@code null} to indicate this may delegate to the core renderer. | |
| 155 | */ | |
| 156 | @Override | |
| 157 | public Set<Class<?>> getDelegates() { | |
| 158 | return null; | |
| 159 | } | |
| 160 | } | |
| 161 | } | |
| 1 | 162 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.exceptions.MissingFileException; |
| 31 | import com.keenwrite.preferences.UserPreferences; | |
| 5 | import com.keenwrite.preferences.Workspace; | |
| 32 | 6 | import com.vladsch.flexmark.ast.Image; |
| 33 | 7 | import com.vladsch.flexmark.html.IndependentLinkResolverFactory; |
| 34 | 8 | import com.vladsch.flexmark.html.LinkResolver; |
| 35 | 9 | import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext; |
| 36 | import com.vladsch.flexmark.html.renderer.LinkStatus; | |
| 37 | 10 | import com.vladsch.flexmark.html.renderer.ResolvedLink; |
| 38 | 11 | import com.vladsch.flexmark.util.ast.Node; |
| ... | ||
| 46 | 19 | |
| 47 | 20 | import static com.keenwrite.StatusBarNotifier.clue; |
| 48 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 21 | import static com.keenwrite.preferences.Workspace.KEY_IMAGES_DIR; | |
| 22 | import static com.keenwrite.preferences.Workspace.KEY_IMAGES_ORDER; | |
| 23 | import static com.keenwrite.util.ProtocolScheme.getProtocol; | |
| 49 | 24 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; |
| 50 | 25 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; |
| 26 | import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID; | |
| 51 | 27 | import static java.lang.String.format; |
| 52 | import static org.apache.commons.io.FilenameUtils.getExtension; | |
| 53 | import static org.apache.commons.io.FilenameUtils.removeExtension; | |
| 54 | 28 | |
| 55 | 29 | /** |
| ... | ||
| 62 | 36 | * Creates an extension capable of using a relative path to embed images. |
| 63 | 37 | * |
| 64 | * @param path The {@link Path} to the file being edited; the parent path | |
| 65 | * is the starting location of the relative image directory. | |
| 66 | * @return The new {@link ImageLinkExtension}, never {@code null}. | |
| 38 | * @param basePath The directory to search for images, either directly or | |
| 39 | * through the images directory setting, not {@code null}. | |
| 40 | * @param workspace Contains user preferences for image directory and image | |
| 41 | * file name extension lookup order. | |
| 42 | * @return The new {@link ImageLinkExtension}, not {@code null}. | |
| 67 | 43 | */ |
| 68 | public static ImageLinkExtension create( @NotNull final Path path ) { | |
| 69 | return new ImageLinkExtension( path ); | |
| 44 | public static ImageLinkExtension create( | |
| 45 | @NotNull final Path basePath, | |
| 46 | @NotNull final Workspace workspace ) { | |
| 47 | return new ImageLinkExtension( basePath, workspace ); | |
| 48 | } | |
| 49 | ||
| 50 | private final Path mBasePath; | |
| 51 | private final Workspace mWorkspace; | |
| 52 | ||
| 53 | private ImageLinkExtension( | |
| 54 | @NotNull final Path basePath, @NotNull final Workspace workspace ) { | |
| 55 | mBasePath = basePath; | |
| 56 | mWorkspace = workspace; | |
| 57 | } | |
| 58 | ||
| 59 | @Override | |
| 60 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 61 | } | |
| 62 | ||
| 63 | @Override | |
| 64 | public void extend( | |
| 65 | @NotNull final Builder builder, @NotNull final String rendererType ) { | |
| 66 | builder.linkResolverFactory( new Factory() ); | |
| 70 | 67 | } |
| 71 | 68 | |
| 72 | 69 | private class Factory extends IndependentLinkResolverFactory { |
| 73 | 70 | @Override |
| 74 | 71 | public @NotNull LinkResolver apply( |
| 75 | @NotNull final LinkResolverBasicContext context ) { | |
| 72 | @NotNull final LinkResolverBasicContext context ) { | |
| 76 | 73 | return new ImageLinkResolver(); |
| 77 | 74 | } |
| 78 | 75 | } |
| 79 | 76 | |
| 80 | 77 | private class ImageLinkResolver implements LinkResolver { |
| 81 | private final UserPreferences mUserPref = getUserPreferences(); | |
| 82 | private final File mImagesUserPrefix = mUserPref.getImagesDirectory(); | |
| 83 | private final String mImageExtensions = mUserPref.getImagesOrder(); | |
| 84 | ||
| 85 | 78 | public ImageLinkResolver() { |
| 86 | 79 | } |
| 87 | 80 | |
| 88 | /** | |
| 89 | * You can also set/clear/modify attributes through | |
| 90 | * {@link ResolvedLink#getAttributes()} and | |
| 91 | * {@link ResolvedLink#getNonNullAttributes()}. | |
| 92 | */ | |
| 93 | 81 | @NotNull |
| 94 | 82 | @Override |
| 95 | 83 | public ResolvedLink resolveLink( |
| 96 | @NotNull final Node node, | |
| 97 | @NotNull final LinkResolverBasicContext context, | |
| 98 | @NotNull final ResolvedLink link ) { | |
| 84 | @NotNull final Node node, | |
| 85 | @NotNull final LinkResolverBasicContext context, | |
| 86 | @NotNull final ResolvedLink link ) { | |
| 99 | 87 | return node instanceof Image ? resolve( link ) : link; |
| 100 | 88 | } |
| 101 | 89 | |
| 102 | 90 | private ResolvedLink resolve( final ResolvedLink link ) { |
| 103 | var url = link.getUrl(); | |
| 104 | final var protocol = getProtocol( url ); | |
| 91 | var uri = link.getUrl(); | |
| 92 | final var protocol = getProtocol( uri ); | |
| 105 | 93 | |
| 106 | try { | |
| 107 | if( protocol.isHttp() ) { | |
| 108 | return valid( link, url ); | |
| 109 | } | |
| 110 | } catch( final Exception ignored ) { | |
| 111 | // Try to resolve the image path, dynamically. | |
| 94 | if( protocol.isHttp() ) { | |
| 95 | return valid( link, uri ); | |
| 112 | 96 | } |
| 113 | ||
| 114 | try { | |
| 115 | final Path imagePrefix = getImagePrefix().toPath(); | |
| 116 | 97 | |
| 117 | // Path to the file being edited. | |
| 118 | Path editPath = getEditPath(); | |
| 98 | // Determine the fully-qualified file name (fqfn). | |
| 99 | final var fqfn = Paths.get( getBasePath().toString(), uri ).toFile(); | |
| 119 | 100 | |
| 120 | // If there is no parent path to the file, it means the file has not | |
| 121 | // been saved. Default to using the value from the user's preferences. | |
| 122 | // The user's preferences will be defaulted to a the application's | |
| 123 | // starting directory. | |
| 124 | editPath = editPath == null | |
| 125 | ? imagePrefix | |
| 126 | : Path.of( editPath.toString(), imagePrefix.toString() ); | |
| 101 | if( fqfn.isFile() ) { | |
| 102 | return valid( link, uri ); | |
| 103 | } | |
| 127 | 104 | |
| 128 | final var urlExt = getExtension( url ); | |
| 129 | url = removeExtension( url ); | |
| 105 | // At this point either the image directory is qualified or needs to be | |
| 106 | // qualified using the image prefix, as set in the user preferences. | |
| 107 | try { | |
| 108 | final var imagePrefix = getImagePrefix(); | |
| 109 | final var basePath = getBasePath().resolve( imagePrefix ); | |
| 130 | 110 | |
| 131 | final var suffixes = urlExt + ' ' + getImageExtensions(); | |
| 132 | final var imagePathPrefix = Path.of( editPath.toString(), url ); | |
| 133 | var suffix = ".*"; | |
| 111 | final var imagePathPrefix = Path.of( basePath.toString(), uri ); | |
| 112 | final var suffixes = getImageExtensions(); | |
| 134 | 113 | boolean missing = true; |
| 135 | 114 | |
| 136 | 115 | // Iterate over the user's preferred image file type extensions. |
| 137 | for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) { | |
| 138 | final String imagePath = format( "%s.%s", imagePathPrefix, ext ); | |
| 139 | final File file = new File( imagePath ); | |
| 116 | for( final var ext : Splitter.on( ' ' ).split( suffixes ) ) { | |
| 117 | final var imagePath = format( "%s.%s", imagePathPrefix, ext ); | |
| 118 | final var file = new File( imagePath ); | |
| 140 | 119 | |
| 141 | 120 | if( file.exists() ) { |
| 142 | url = file.toString(); | |
| 121 | uri += '.' + ext; | |
| 122 | final var path = Path.of( imagePrefix.toString(), uri ); | |
| 123 | uri = path.normalize().toString(); | |
| 143 | 124 | missing = false; |
| 144 | break; | |
| 145 | } | |
| 146 | else if( !urlExt.isBlank() ) { | |
| 147 | // The file is missing because the user specified a prefix. | |
| 148 | suffix = urlExt; | |
| 149 | 125 | break; |
| 150 | 126 | } |
| 151 | 127 | } |
| 152 | 128 | |
| 153 | 129 | if( missing ) { |
| 154 | throw new MissingFileException( imagePathPrefix + suffix ); | |
| 155 | } | |
| 156 | ||
| 157 | if( protocol.isFile() ) { | |
| 158 | // When generating an HTML document, ensure images use a path that's | |
| 159 | // relative to the source document. This handles displaying within | |
| 160 | // the application and when exporting to an HTML file. | |
| 161 | url = editPath.relativize( Paths.get( url ) ).toString(); | |
| 130 | throw new MissingFileException( imagePathPrefix + ".*" ); | |
| 162 | 131 | } |
| 163 | 132 | |
| 164 | return valid( link, url ); | |
| 133 | return valid( link, uri ); | |
| 165 | 134 | } catch( final Exception ex ) { |
| 166 | 135 | clue( ex ); |
| 167 | 136 | } |
| 168 | 137 | |
| 169 | 138 | return link; |
| 170 | 139 | } |
| 171 | 140 | |
| 172 | 141 | private ResolvedLink valid( final ResolvedLink link, final String url ) { |
| 173 | return link.withStatus( LinkStatus.VALID ).withUrl( url ); | |
| 142 | return link.withStatus( VALID ).withUrl( url ); | |
| 174 | 143 | } |
| 175 | 144 | |
| 176 | private File getImagePrefix() { | |
| 177 | return mImagesUserPrefix; | |
| 145 | private Path getImagePrefix() { | |
| 146 | return mWorkspace.toFile( KEY_IMAGES_DIR ).toPath(); | |
| 178 | 147 | } |
| 179 | 148 | |
| 180 | 149 | private String getImageExtensions() { |
| 181 | return mImageExtensions; | |
| 150 | return mWorkspace.toString( KEY_IMAGES_ORDER ); | |
| 182 | 151 | } |
| 183 | 152 | |
| 184 | private Path getEditPath() { | |
| 185 | return mPath.getParent(); | |
| 153 | private Path getBasePath() { | |
| 154 | return mBasePath; | |
| 186 | 155 | } |
| 187 | } | |
| 188 | ||
| 189 | private final Path mPath; | |
| 190 | ||
| 191 | private ImageLinkExtension( @NotNull final Path path ) { | |
| 192 | mPath = path; | |
| 193 | } | |
| 194 | ||
| 195 | @Override | |
| 196 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 197 | } | |
| 198 | ||
| 199 | @Override | |
| 200 | public void extend( @NotNull final Builder builder, | |
| 201 | @NotNull final String rendererType ) { | |
| 202 | builder.linkResolverFactory( new Factory() ); | |
| 203 | } | |
| 204 | ||
| 205 | private UserPreferences getUserPreferences() { | |
| 206 | return UserPreferences.getInstance(); | |
| 207 | 156 | } |
| 208 | 157 | } |
| 1 | package com.keenwrite.processors.markdown; | |
| 2 | ||
| 3 | import com.vladsch.flexmark.ast.Text; | |
| 4 | import com.vladsch.flexmark.html.HtmlWriter; | |
| 5 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 6 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; | |
| 7 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | |
| 8 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 9 | import com.vladsch.flexmark.util.ast.TextCollectingVisitor; | |
| 10 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 11 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 12 | import org.jetbrains.annotations.NotNull; | |
| 13 | import org.jetbrains.annotations.Nullable; | |
| 14 | ||
| 15 | import java.util.LinkedHashMap; | |
| 16 | import java.util.Map; | |
| 17 | import java.util.Set; | |
| 18 | ||
| 19 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 20 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 21 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 22 | ||
| 23 | /** | |
| 24 | * Responsible for substituting multi-codepoint glyphs with single codepoint | |
| 25 | * glyphs. The text is adorned with ligatures prior to rendering as HTML. | |
| 26 | * This requires a font that supports ligatures. | |
| 27 | * <p> | |
| 28 | * TODO: #81 -- I18N | |
| 29 | * </p> | |
| 30 | */ | |
| 31 | public class LigatureExtension implements HtmlRendererExtension { | |
| 32 | /** | |
| 33 | * Retain insertion order so that ligature substitution uses longer ligatures | |
| 34 | * ahead of shorter ligatures. The word "ruffian" should use the "ffi" | |
| 35 | * ligature, not the "ff" ligature. | |
| 36 | */ | |
| 37 | private static final Map<String, String> LIGATURES = new LinkedHashMap<>(); | |
| 38 | ||
| 39 | static { | |
| 40 | LIGATURES.put( "ffi", "\uFB03" ); | |
| 41 | LIGATURES.put( "ffl", "\uFB04" ); | |
| 42 | LIGATURES.put( "ff", "\uFB00" ); | |
| 43 | LIGATURES.put( "fi", "\uFB01" ); | |
| 44 | LIGATURES.put( "fl", "\uFB02" ); | |
| 45 | LIGATURES.put( "ft", "\uFB05" ); | |
| 46 | LIGATURES.put( "AE", "\u00C6" ); | |
| 47 | LIGATURES.put( "OE", "\u0152" ); | |
| 48 | // "ae", "\u00E6", | |
| 49 | // "oe", "\u0153", | |
| 50 | } | |
| 51 | ||
| 52 | private static class LigatureRenderer implements NodeRenderer { | |
| 53 | private final TextCollectingVisitor mVisitor = new TextCollectingVisitor(); | |
| 54 | ||
| 55 | @SuppressWarnings("unused") | |
| 56 | public LigatureRenderer( final DataHolder options ) { | |
| 57 | } | |
| 58 | ||
| 59 | @Override | |
| 60 | public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 61 | return Set.of( new NodeRenderingHandler<>( | |
| 62 | Text.class, LigatureRenderer.this::render ) ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * This will pick the fastest string replacement algorithm based on the | |
| 67 | * text length. The insertion order of the {@link #LIGATURES} is | |
| 68 | * important to give precedence to longer ligatures. | |
| 69 | * | |
| 70 | * @param textNode The text node containing text to replace with ligatures. | |
| 71 | * @param context Not used. | |
| 72 | * @param html Where to write the text adorned with ligatures. | |
| 73 | */ | |
| 74 | private void render( | |
| 75 | @NotNull final Text textNode, | |
| 76 | @NotNull final NodeRendererContext context, | |
| 77 | @NotNull final HtmlWriter html ) { | |
| 78 | final var text = mVisitor.collectAndGetText( textNode ); | |
| 79 | html.text( replace( text, LIGATURES ) ); | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | private static class Factory implements NodeRendererFactory { | |
| 84 | @NotNull | |
| 85 | @Override | |
| 86 | public NodeRenderer apply( @NotNull DataHolder options ) { | |
| 87 | return new LigatureRenderer( options ); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | private LigatureExtension() { | |
| 92 | } | |
| 93 | ||
| 94 | @Override | |
| 95 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public void extend( @NotNull final Builder builder, | |
| 100 | @NotNull final String rendererType ) { | |
| 101 | if( "HTML".equalsIgnoreCase( rendererType ) ) { | |
| 102 | builder.nodeRendererFactory( new Factory() ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | public static LigatureExtension create() { | |
| 107 | return new LigatureExtension(); | |
| 108 | } | |
| 109 | } | |
| 110 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.ExportFormat; |
| 5 | import com.keenwrite.io.MediaType; | |
| 6 | import com.keenwrite.preferences.Workspace; | |
| 31 | 7 | import com.keenwrite.processors.*; |
| 32 | 8 | import com.keenwrite.processors.markdown.r.RExtension; |
| ... | ||
| 47 | 23 | import java.util.HashSet; |
| 48 | 24 | |
| 49 | import static com.keenwrite.AbstractFileFactory.lookup; | |
| 50 | import static com.keenwrite.Constants.USER_DIRECTORY; | |
| 25 | import static com.keenwrite.Constants.DEFAULT_DIRECTORY; | |
| 51 | 26 | import static com.keenwrite.ExportFormat.NONE; |
| 27 | import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; | |
| 28 | import static com.keenwrite.io.MediaType.TEXT_R_XML; | |
| 52 | 29 | |
| 53 | 30 | /** |
| 54 | 31 | * Responsible for parsing a Markdown document and rendering it as HTML. |
| 55 | 32 | */ |
| 56 | public class MarkdownProcessor extends AbstractProcessor<String> { | |
| 33 | public class MarkdownProcessor extends ExecutorProcessor<String> { | |
| 57 | 34 | |
| 58 | 35 | private final IParse mParser; |
| 59 | 36 | private final IRender mRenderer; |
| 60 | 37 | |
| 61 | 38 | private MarkdownProcessor( |
| 62 | final Processor<String> successor, | |
| 63 | final Collection<Extension> extensions ) { | |
| 39 | final Processor<String> successor, | |
| 40 | final Collection<Extension> extensions ) { | |
| 64 | 41 | super( successor ); |
| 65 | ||
| 66 | // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38 | |
| 67 | // TODO: Uncomment when ligatures are fixed. | |
| 68 | // extensions.add( LigatureExtension.create() ); | |
| 69 | 42 | |
| 70 | 43 | mParser = Parser.builder().extensions( extensions ).build(); |
| 71 | 44 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); |
| 72 | 45 | } |
| 73 | 46 | |
| 74 | public static MarkdownProcessor create() { | |
| 75 | return create( IdentityProcessor.INSTANCE, Path.of( USER_DIRECTORY ) ); | |
| 47 | public static MarkdownProcessor create( final Workspace workspace ) { | |
| 48 | return create( IdentityProcessor.INSTANCE, workspace, DEFAULT_DIRECTORY ); | |
| 49 | } | |
| 50 | ||
| 51 | public static MarkdownProcessor create( final ProcessorContext context ) { | |
| 52 | return create( IdentityProcessor.INSTANCE, context ); | |
| 76 | 53 | } |
| 77 | 54 | |
| 78 | 55 | public static MarkdownProcessor create( |
| 79 | final Processor<String> successor, final Path path ) { | |
| 80 | final var extensions = createExtensions( path, NONE ); | |
| 56 | final Processor<String> successor, | |
| 57 | final Workspace workspace, | |
| 58 | final Path dir ) { | |
| 59 | final var extensions = createExtensions( NONE, workspace, dir ); | |
| 81 | 60 | return new MarkdownProcessor( successor, extensions ); |
| 82 | 61 | } |
| 83 | 62 | |
| 84 | 63 | public static MarkdownProcessor create( |
| 85 | final Processor<String> successor, final ProcessorContext context ) { | |
| 64 | final Processor<String> successor, final ProcessorContext context ) { | |
| 86 | 65 | final var extensions = createExtensions( context ); |
| 87 | 66 | return new MarkdownProcessor( successor, extensions ); |
| ... | ||
| 102 | 81 | */ |
| 103 | 82 | private static Collection<Extension> createExtensions( |
| 104 | final ProcessorContext context ) { | |
| 105 | final var path = context.getPath(); | |
| 83 | final ProcessorContext context ) { | |
| 84 | final var path = context.getPath(); | |
| 85 | final var dir = context.getBasePath(); | |
| 106 | 86 | final var format = context.getExportFormat(); |
| 107 | final var extensions = createExtensions( path, format ); | |
| 87 | final var workspace = context.getWorkspace(); | |
| 88 | final var extensions = createExtensions( format, workspace, dir ); | |
| 108 | 89 | |
| 109 | extensions.add( CaretExtension.create( context.getCaretPosition() ) ); | |
| 90 | final var mediaType = MediaType.valueFrom( path ); | |
| 91 | if( mediaType == TEXT_R_MARKDOWN || mediaType == TEXT_R_XML ) { | |
| 92 | extensions.add( RExtension.create() ); | |
| 93 | } | |
| 94 | ||
| 95 | extensions.add( FencedBlockExtension.create( context ) ); | |
| 96 | extensions.add( CaretExtension.create( context.getCaret() ) ); | |
| 110 | 97 | |
| 111 | 98 | return extensions; |
| ... | ||
| 120 | 107 | * R statements. |
| 121 | 108 | * |
| 122 | * @param path Path name for referencing image files via relative paths | |
| 109 | * @param dir Directory for referencing image files via relative paths | |
| 123 | 110 | * and dynamic file types. |
| 124 | 111 | * @param format TeX export format to use when generating HTMl documents. |
| 125 | 112 | * @return {@link Collection} of extensions invoked when parsing Markdown. |
| 126 | 113 | */ |
| 127 | 114 | private static Collection<Extension> createExtensions( |
| 128 | final Path path, final ExportFormat format ) { | |
| 115 | final ExportFormat format, final Workspace workspace, final Path dir ) { | |
| 129 | 116 | final var extensions = createDefaultExtensions(); |
| 130 | 117 | |
| 131 | extensions.add( ImageLinkExtension.create( path ) ); | |
| 118 | extensions.add( ImageLinkExtension.create( dir, workspace ) ); | |
| 132 | 119 | extensions.add( TeXExtension.create( format ) ); |
| 133 | ||
| 134 | if( lookup( path ).isR() ) { | |
| 135 | extensions.add( RExtension.create() ); | |
| 136 | } | |
| 137 | 120 | |
| 138 | 121 | return extensions; |
| ... | ||
| 179 | 162 | public Node toNode( final String markdown ) { |
| 180 | 163 | return parse( markdown ); |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns the result of converting the given AST into an HTML string. | |
| 168 | * | |
| 169 | * @param node The AST {@link Node} to convert to an HTML string. | |
| 170 | * @return The given {@link Node} as an HTML string. | |
| 171 | */ | |
| 172 | public String toHtml( final Node node ) { | |
| 173 | return getRenderer().render( node ); | |
| 181 | 174 | } |
| 182 | 175 | |
| ... | ||
| 198 | 191 | */ |
| 199 | 192 | private String toHtml( final String markdown ) { |
| 200 | return getRenderer().render( parse( markdown ) ); | |
| 193 | return toHtml( parse( markdown ) ); | |
| 201 | 194 | } |
| 202 | 195 | |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown.r; |
| 29 | 3 | |
| ... | ||
| 39 | 13 | import com.vladsch.flexmark.util.data.DataHolder; |
| 40 | 14 | import com.vladsch.flexmark.util.data.MutableDataHolder; |
| 41 | import com.vladsch.flexmark.util.sequence.BasedSequence; | |
| 42 | 15 | |
| 43 | 16 | import java.util.BitSet; |
| ... | ||
| 52 | 25 | */ |
| 53 | 26 | public final class RExtension implements Parser.ParserExtension { |
| 54 | private final static InlineParserFactory R_INLINE_PARSER_FACTORY = | |
| 55 | RInlineParser::new; | |
| 27 | private static final InlineParserFactory FACTORY = CustomParser::new; | |
| 28 | ||
| 29 | private RExtension() { | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Creates an extension capable of intercepting R code blocks and preventing | |
| 34 | * them from being converted into HTML {@code <code>} elements. | |
| 35 | */ | |
| 36 | public static RExtension create() { | |
| 37 | return new RExtension(); | |
| 38 | } | |
| 39 | ||
| 40 | @Override | |
| 41 | public void extend( final Parser.Builder builder ) { | |
| 42 | builder.customInlineParserFactory( FACTORY ); | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public void parserOptions( final MutableDataHolder options ) { | |
| 47 | } | |
| 56 | 48 | |
| 57 | 49 | /** |
| ... | ||
| 67 | 59 | * </p> |
| 68 | 60 | */ |
| 69 | private static class RInlineParser extends InlineParserImpl { | |
| 70 | private RInlineParser( | |
| 71 | final DataHolder options, | |
| 72 | final BitSet specialCharacters, | |
| 73 | final BitSet delimiterCharacters, | |
| 74 | final Map<Character, DelimiterProcessor> delimiterProcessors, | |
| 75 | final LinkRefProcessorData referenceLinkProcessors, | |
| 76 | final List<InlineParserExtensionFactory> inlineParserExtensions ) { | |
| 61 | private static class CustomParser extends InlineParserImpl { | |
| 62 | private CustomParser( | |
| 63 | final DataHolder options, | |
| 64 | final BitSet specialCharacters, | |
| 65 | final BitSet delimiterCharacters, | |
| 66 | final Map<Character, DelimiterProcessor> delimiterProcessors, | |
| 67 | final LinkRefProcessorData referenceLinkProcessors, | |
| 68 | final List<InlineParserExtensionFactory> inlineParserExtensions ) { | |
| 77 | 69 | super( options, |
| 78 | 70 | specialCharacters, |
| ... | ||
| 87 | 79 | * changes the behaviour to retain R code snippets, identified by |
| 88 | 80 | * {@link RSigilOperator#PREFIX}, so that subsequent processing can |
| 89 | * invoke R. If other languages are added, this {@link RInlineParser} will | |
| 81 | * invoke R. If other languages are added, the {@link CustomParser} will | |
| 90 | 82 | * have to be rewritten to identify more than merely R. |
| 91 | 83 | * |
| 92 | 84 | * @return The return value from {@link super#parseBackticks()}. |
| 93 | 85 | * @inheritDoc |
| 94 | 86 | */ |
| 95 | 87 | @Override |
| 96 | 88 | protected final boolean parseBackticks() { |
| 97 | final var foundCode = super.parseBackticks(); | |
| 89 | final var foundTicks = super.parseBackticks(); | |
| 98 | 90 | |
| 99 | if( foundCode ) { | |
| 100 | final var block = getBlock(); | |
| 101 | final var codeNode = block.getLastChild(); | |
| 102 | final var code = codeNode == null | |
| 103 | ? BasedSequence.of( "" ) | |
| 104 | : codeNode.getChars(); | |
| 91 | if( foundTicks ) { | |
| 92 | final var blockNode = getBlock(); | |
| 93 | final var codeNode = blockNode.getLastChild(); | |
| 105 | 94 | |
| 106 | if( code.startsWith( RSigilOperator.PREFIX ) ) { | |
| 107 | assert codeNode != null; | |
| 108 | codeNode.unlink(); | |
| 109 | block.appendChild( new Text( code ) ); | |
| 95 | if( codeNode != null ) { | |
| 96 | final var code = codeNode.getChars(); | |
| 97 | ||
| 98 | if( code.startsWith( RSigilOperator.PREFIX ) ) { | |
| 99 | codeNode.unlink(); | |
| 100 | blockNode.appendChild( new Text( code ) ); | |
| 101 | } | |
| 110 | 102 | } |
| 111 | 103 | } |
| 112 | 104 | |
| 113 | return foundCode; | |
| 105 | return foundTicks; | |
| 114 | 106 | } |
| 115 | } | |
| 116 | ||
| 117 | private RExtension() { | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Creates an extension capable of intercepting R code blocks and preventing | |
| 122 | * them from being converted into HTML {@code <code>} elements. | |
| 123 | */ | |
| 124 | public static RExtension create() { | |
| 125 | return new RExtension(); | |
| 126 | } | |
| 127 | ||
| 128 | @Override | |
| 129 | public void extend( final Parser.Builder builder ) { | |
| 130 | builder.customInlineParserFactory( R_INLINE_PARSER_FACTORY ); | |
| 131 | } | |
| 132 | ||
| 133 | @Override | |
| 134 | public void parserOptions( final MutableDataHolder options ) { | |
| 135 | 107 | } |
| 136 | 108 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown.tex; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown.tex; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.markdown.tex; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.ExportFormat; |
| 31 | 5 | import com.keenwrite.preview.SvgRasterizer; |
| 32 | import com.keenwrite.preview.SvgReplacedElementFactory; | |
| 33 | 6 | import com.vladsch.flexmark.html.HtmlWriter; |
| 34 | 7 | import com.vladsch.flexmark.html.renderer.NodeRenderer; |
| ... | ||
| 43 | 16 | import java.util.Set; |
| 44 | 17 | |
| 18 | import static com.keenwrite.preview.MathRenderer.MATH_RENDERER; | |
| 45 | 19 | import static com.keenwrite.processors.markdown.tex.TexNode.*; |
| 46 | 20 | |
| ... | ||
| 109 | 83 | final NodeRendererContext context, |
| 110 | 84 | final HtmlWriter html ) { |
| 111 | final var renderer = SvgReplacedElementFactory.getInstance(); | |
| 112 | 85 | final var tex = node.getText().toStringOrNull(); |
| 113 | final var doc = renderer.render( tex == null ? "" : tex ); | |
| 86 | final var doc = MATH_RENDERER.render( tex == null ? "" : tex ); | |
| 114 | 87 | final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() ); |
| 115 | 88 | html.raw( svg ); |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.text; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.text; |
| 29 | 3 | |
| 30 | 4 | import java.util.Map; |
| 31 | import org.ahocorasick.trie.Emit; | |
| 32 | import org.ahocorasick.trie.Trie.TrieBuilder; | |
| 5 | ||
| 33 | 6 | import static org.ahocorasick.trie.Trie.builder; |
| 34 | 7 | |
| ... | ||
| 47 | 20 | public String replace( final String text, final Map<String, String> map ) { |
| 48 | 21 | // Create a buffer sufficiently large that re-allocations are minimized. |
| 49 | final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) ); | |
| 50 | ||
| 51 | // The TrieBuilder should only match whole words and ignore overlaps (there | |
| 52 | // shouldn't be any). | |
| 53 | final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps(); | |
| 22 | final var sb = new StringBuilder( (int)(text.length() * 1.25) ); | |
| 54 | 23 | |
| 55 | for( final String key : keys( map ) ) { | |
| 56 | builder.addKeyword( key ); | |
| 57 | } | |
| 24 | // Definition names cannot overlap. | |
| 25 | final var builder = builder().ignoreOverlaps(); | |
| 26 | builder.addKeywords( keys( map ) ); | |
| 58 | 27 | |
| 59 | 28 | int index = 0; |
| 60 | 29 | |
| 61 | 30 | // Replace all instances with dereferenced variables. |
| 62 | for( final Emit emit : builder.build().parseText( text ) ) { | |
| 31 | for( final var emit : builder.build().parseText( text ) ) { | |
| 63 | 32 | sb.append( text, index, emit.getStart() ); |
| 64 | 33 | sb.append( map.get( emit.getKeyword() ) ); |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.text; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.text; |
| 29 | 3 | |
| ... | ||
| 47 | 21 | */ |
| 48 | 22 | public static TextReplacer getTextReplacer( final int length ) { |
| 49 | // After about 1,500 characters, the StringUtils implementation is less | |
| 50 | // performant than the Aho-Corsick implementation. | |
| 51 | // | |
| 52 | // See http://stackoverflow.com/a/40836618/59087 | |
| 23 | // After about 1,500 characters, the StringUtils implementation is slower | |
| 24 | // than the Aho-Corsick algorithm implementation. | |
| 53 | 25 | return length < 1500 ? APACHE : AHO_CORASICK; |
| 54 | 26 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.processors.text; |
| 29 | 3 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.search; | |
| 3 | ||
| 4 | import com.keenwrite.util.CyclicIterator; | |
| 5 | import javafx.beans.property.ObjectProperty; | |
| 6 | import javafx.beans.property.SimpleObjectProperty; | |
| 7 | import javafx.beans.value.ObservableValue; | |
| 8 | import javafx.scene.control.IndexRange; | |
| 9 | import org.ahocorasick.trie.Emit; | |
| 10 | import org.ahocorasick.trie.Trie; | |
| 11 | ||
| 12 | import java.util.ArrayList; | |
| 13 | import java.util.List; | |
| 14 | ||
| 15 | import static org.ahocorasick.trie.Trie.builder; | |
| 16 | ||
| 17 | /** | |
| 18 | * Responsible for finding words in a text document. This implementation uses | |
| 19 | * a {@link Trie} for efficiency. | |
| 20 | */ | |
| 21 | public class SearchModel { | |
| 22 | private final ObjectProperty<IndexRange> mMatchOffset = | |
| 23 | new SimpleObjectProperty<>(); | |
| 24 | private final ObjectProperty<Integer> mMatchCount = | |
| 25 | new SimpleObjectProperty<>(); | |
| 26 | private final ObjectProperty<Integer> mMatchIndex = | |
| 27 | new SimpleObjectProperty<>(); | |
| 28 | ||
| 29 | private CyclicIterator<Emit> mMatches = new CyclicIterator<>( List.of() ); | |
| 30 | ||
| 31 | private String mNeedle = ""; | |
| 32 | ||
| 33 | /** | |
| 34 | * Creates a new {@link SearchModel} that finds all text string in a | |
| 35 | * document simultaneously. | |
| 36 | */ | |
| 37 | public SearchModel() { | |
| 38 | } | |
| 39 | ||
| 40 | public ObjectProperty<Integer> matchCountProperty() { | |
| 41 | return mMatchCount; | |
| 42 | } | |
| 43 | ||
| 44 | public ObjectProperty<Integer> matchIndexProperty() { | |
| 45 | return mMatchIndex; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Observers watch this property to be notified when a needle has been | |
| 50 | * found in the haystack. Use {@link IndexRange#getStart()} to get the | |
| 51 | * absolute offset into the text (zero-based). | |
| 52 | * | |
| 53 | * @return The {@link IndexRange} property to observe, representing the | |
| 54 | * most recently matched text offset into the document. | |
| 55 | */ | |
| 56 | public ObservableValue<IndexRange> matchOffsetProperty() { | |
| 57 | return mMatchOffset; | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * Searches the document for text matching the given parameter value. This | |
| 62 | * is the main entry point for kicking off text searches. | |
| 63 | * | |
| 64 | * @param needle The text string to find in the document, no regex allowed. | |
| 65 | * @param haystack The document to search within for a text string. | |
| 66 | */ | |
| 67 | public void search( final String needle, final String haystack ) { | |
| 68 | assert needle != null; | |
| 69 | assert haystack != null; | |
| 70 | ||
| 71 | final var trie = builder() | |
| 72 | .ignoreCase() | |
| 73 | .ignoreOverlaps() | |
| 74 | .addKeyword( needle ) | |
| 75 | .build(); | |
| 76 | final var emits = trie.parseText( haystack ); | |
| 77 | ||
| 78 | mMatches = new CyclicIterator<>( new ArrayList<>( emits ) ); | |
| 79 | mMatchCount.set( emits.size() ); | |
| 80 | mNeedle = needle; | |
| 81 | advance(); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Searches the document for the last known needle. | |
| 86 | * | |
| 87 | * @param haystack The new text to search. | |
| 88 | */ | |
| 89 | public void search( final String haystack ) { | |
| 90 | search( mNeedle, haystack ); | |
| 91 | } | |
| 92 | ||
| 93 | /** | |
| 94 | * Moves the search iterator to the next match, wrapping as needed. | |
| 95 | */ | |
| 96 | public void advance() { | |
| 97 | if( mMatches.hasNext() ) { | |
| 98 | setCurrent( mMatches.next() ); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * Moves the search iterator to the previous match, wrapping as needed. | |
| 104 | */ | |
| 105 | public void retreat() { | |
| 106 | if( mMatches.hasPrevious() ) { | |
| 107 | setCurrent( mMatches.previous() ); | |
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | private void setCurrent( final Emit emit ) { | |
| 112 | mMatchOffset.set( new IndexRange( emit.getStart(), emit.getEnd() ) ); | |
| 113 | mMatchIndex.set( mMatches.getIndex() + 1 ); | |
| 114 | } | |
| 115 | } | |
| 1 | 116 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 31 | ||
| 32 | import java.util.prefs.BackingStoreException; | |
| 33 | import java.util.prefs.Preferences; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for persisting options that are safe to load before the UI | |
| 37 | * is shown. This can include items like window dimensions, last file | |
| 38 | * opened, split pane locations, and more. This cannot be used to persist | |
| 39 | * options that are user-controlled (i.e., all options available through | |
| 40 | * {@link PreferencesFx}). | |
| 41 | */ | |
| 42 | public interface Options extends Service { | |
| 43 | ||
| 44 | /** | |
| 45 | * Returns the {@link Preferences} that persist settings that cannot | |
| 46 | * be configured via the user interface. | |
| 47 | * | |
| 48 | * @return A valid {@link Preferences} instance, never {@code null}. | |
| 49 | */ | |
| 50 | Preferences getState(); | |
| 51 | ||
| 52 | /** | |
| 53 | * Stores the key and value into the user preferences to be loaded the next | |
| 54 | * time the application is launched. | |
| 55 | * | |
| 56 | * @param key Name of the key to persist along with its value. | |
| 57 | * @param value Value to associate with the key. | |
| 58 | * @throws BackingStoreException Could not persist the change. | |
| 59 | */ | |
| 60 | void put( String key, String value ) throws BackingStoreException; | |
| 61 | ||
| 62 | /** | |
| 63 | * Retrieves the value for a key in the user preferences. | |
| 64 | * | |
| 65 | * @param key Retrieve the value of this key. | |
| 66 | * @param defaultValue The value to return in the event that the given key has | |
| 67 | * no associated value. | |
| 68 | * @return The value associated with the key. | |
| 69 | */ | |
| 70 | String get( String key, String defaultValue ); | |
| 71 | ||
| 72 | /** | |
| 73 | * Retrieves the value for a key in the user preferences. This will return | |
| 74 | * the empty string if the value cannot be found. | |
| 75 | * | |
| 76 | * @param key The key to find in the preferences. | |
| 77 | * @return A non-null, possibly empty value for the key. | |
| 78 | */ | |
| 79 | String get( String key ); | |
| 80 | } | |
| 81 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service; |
| 29 | 3 | |
| ... | ||
| 65 | 39 | */ |
| 66 | 40 | void ignore( Path file ); |
| 41 | ||
| 42 | /** | |
| 43 | * Start listening for events on a new thread. | |
| 44 | */ | |
| 45 | void start(); | |
| 67 | 46 | |
| 68 | 47 | /** |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.events; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.events; |
| 29 | 3 | |
| 30 | 4 | import javafx.scene.control.Alert; |
| 31 | 5 | import javafx.scene.control.ButtonType; |
| 32 | 6 | import javafx.stage.Window; |
| 7 | ||
| 8 | import java.nio.file.Path; | |
| 33 | 9 | |
| 34 | 10 | /** |
| ... | ||
| 42 | 18 | |
| 43 | 19 | /** |
| 44 | * Constructs a default alert message text for a modal alert dialog. | |
| 20 | * Constructs an alert message text for a modal alert dialog. | |
| 45 | 21 | * |
| 46 | * @param title The dialog box message title. | |
| 47 | * @param message The dialog box message content (needs formatting). | |
| 22 | * @param parent The window responsible for the child dialog. | |
| 23 | * @param path The path to a file that was not actionable. | |
| 24 | * @param titleKey The dialog box message title. | |
| 25 | * @param messageKey The dialog box message content (needs formatting). | |
| 26 | * @param ex The problem that requires user attention. | |
| 27 | */ | |
| 28 | void alert( | |
| 29 | Window parent, | |
| 30 | Path path, | |
| 31 | String titleKey, | |
| 32 | String messageKey, | |
| 33 | Exception ex ); | |
| 34 | ||
| 35 | /** | |
| 36 | * Constructs an alert message text for a modal alert dialog. | |
| 37 | * | |
| 38 | * @param parent The window responsible for the child dialog. | |
| 39 | * @param path The path to a file that was not actionable. | |
| 40 | * @param key Prefix for both title and message key. | |
| 41 | * @param ex The problem that requires user attention. | |
| 42 | */ | |
| 43 | default void alert( | |
| 44 | Window parent, | |
| 45 | Path path, | |
| 46 | String key, | |
| 47 | Exception ex ) { | |
| 48 | alert( parent, path, key + ".title", key + ".message", ex ); | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * Contains all the information that the user needs to know about a problem. | |
| 53 | * | |
| 54 | * @param title The dialog box message title (i.e., the error context). | |
| 55 | * @param message The message content (formatted with the given args). | |
| 48 | 56 | * @param args The arguments to the message content that must be formatted. |
| 49 | 57 | * @return The message suitable for building a modal alert dialog. |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.events.impl; |
| 29 | 3 | |
| 30 | 4 | import javafx.scene.Node; |
| 31 | 5 | import javafx.scene.control.ButtonBar; |
| 32 | 6 | import javafx.scene.control.DialogPane; |
| 33 | 7 | |
| 34 | import static com.keenwrite.Constants.SETTINGS; | |
| 8 | import static com.keenwrite.Constants.sSettings; | |
| 35 | 9 | import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS; |
| 36 | 10 | |
| ... | ||
| 55 | 29 | @SuppressWarnings("SameParameterValue") |
| 56 | 30 | private String getSetting( final String key, final String defaultValue ) { |
| 57 | return SETTINGS.getSetting( key, defaultValue ); | |
| 31 | return sSettings.getSetting( key, defaultValue ); | |
| 58 | 32 | } |
| 59 | 33 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.events.impl; |
| 29 | 3 | |
| ... | ||
| 64 | 38 | return this.content; |
| 65 | 39 | } |
| 40 | ||
| 66 | 41 | } |
| 67 | 42 | |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.events.impl; |
| 29 | 3 | |
| 30 | 4 | import com.keenwrite.service.events.Notification; |
| 31 | 5 | import com.keenwrite.service.events.Notifier; |
| 32 | 6 | import javafx.scene.control.Alert; |
| 33 | 7 | import javafx.scene.control.Alert.AlertType; |
| 34 | 8 | import javafx.stage.Window; |
| 9 | ||
| 10 | import java.nio.file.Path; | |
| 35 | 11 | |
| 12 | import static com.keenwrite.Messages.get; | |
| 36 | 13 | import static javafx.scene.control.Alert.AlertType.CONFIRMATION; |
| 37 | 14 | import static javafx.scene.control.Alert.AlertType.ERROR; |
| 38 | 15 | |
| 39 | 16 | /** |
| 40 | 17 | * Provides the ability to notify the user of events that need attention, |
| 41 | 18 | * such as prompting the user to confirm closing when there are unsaved changes. |
| 42 | 19 | */ |
| 43 | 20 | public final class DefaultNotifier implements Notifier { |
| 44 | 21 | |
| 45 | /** | |
| 46 | * Contains all the information that the user needs to know about a problem. | |
| 47 | * | |
| 48 | * @param title The context for the message. | |
| 49 | * @param message The message content (formatted with the given args). | |
| 50 | * @param args Parameters for the message content. | |
| 51 | * @return A notification instance, never null. | |
| 52 | */ | |
| 53 | 22 | @Override |
| 54 | 23 | public Notification createNotification( |
| 55 | 24 | final String title, |
| 56 | 25 | final String message, |
| 57 | 26 | final Object... args ) { |
| 58 | 27 | return new DefaultNotification( title, message, args ); |
| 59 | 28 | } |
| 60 | 29 | |
| 61 | private Alert createAlertDialog( | |
| 30 | @Override | |
| 31 | public void alert( | |
| 62 | 32 | final Window parent, |
| 63 | final AlertType alertType, | |
| 64 | final Notification message ) { | |
| 65 | ||
| 66 | final Alert alert = new Alert( alertType ); | |
| 67 | ||
| 68 | alert.setDialogPane( new ButtonOrderPane() ); | |
| 69 | alert.setTitle( message.getTitle() ); | |
| 70 | alert.setHeaderText( null ); | |
| 71 | alert.setContentText( message.getContent() ); | |
| 72 | alert.initOwner( parent ); | |
| 33 | final Path path, | |
| 34 | final String titleKey, | |
| 35 | final String messageKey, | |
| 36 | final Exception ex ) { | |
| 37 | final var message = createNotification( | |
| 38 | get( titleKey ), get( messageKey ), path, ex.getMessage() | |
| 39 | ); | |
| 73 | 40 | |
| 74 | return alert; | |
| 41 | createError( parent, message ).showAndWait(); | |
| 75 | 42 | } |
| 76 | 43 | |
| 77 | 44 | @Override |
| 78 | public Alert createConfirmation( final Window parent, | |
| 79 | final Notification message ) { | |
| 80 | final Alert alert = createAlertDialog( parent, CONFIRMATION, message ); | |
| 45 | public Alert createConfirmation( | |
| 46 | final Window parent, final Notification message ) { | |
| 47 | final var alert = createAlertDialog( parent, CONFIRMATION, message ); | |
| 81 | 48 | |
| 82 | 49 | alert.getButtonTypes().setAll( YES, NO, CANCEL ); |
| 83 | 50 | |
| 84 | 51 | return alert; |
| 85 | 52 | } |
| 86 | 53 | |
| 87 | 54 | @Override |
| 88 | 55 | public Alert createError( final Window parent, final Notification message ) { |
| 89 | 56 | return createAlertDialog( parent, ERROR, message ); |
| 57 | } | |
| 58 | ||
| 59 | private Alert createAlertDialog( | |
| 60 | final Window parent, | |
| 61 | final AlertType alertType, | |
| 62 | final Notification message ) { | |
| 63 | final var alert = new Alert( alertType ); | |
| 64 | ||
| 65 | alert.setDialogPane( new ButtonOrderPane() ); | |
| 66 | alert.setTitle( message.getTitle() ); | |
| 67 | alert.setHeaderText( null ); | |
| 68 | alert.setContentText( message.getContent() ); | |
| 69 | alert.initOwner( parent ); | |
| 70 | ||
| 71 | return alert; | |
| 90 | 72 | } |
| 91 | 73 | } |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.service.impl; | |
| 28 | ||
| 29 | import com.keenwrite.service.Options; | |
| 30 | ||
| 31 | import java.util.prefs.BackingStoreException; | |
| 32 | import java.util.prefs.Preferences; | |
| 33 | ||
| 34 | import static com.keenwrite.Constants.PREFS_ROOT; | |
| 35 | import static com.keenwrite.Constants.PREFS_STATE; | |
| 36 | import static java.util.prefs.Preferences.userRoot; | |
| 37 | ||
| 38 | /** | |
| 39 | * Persistent options user can change at runtime. | |
| 40 | */ | |
| 41 | public class DefaultOptions implements Options { | |
| 42 | public DefaultOptions() { | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * This will throw IllegalArgumentException if the value exceeds the maximum | |
| 47 | * preferences value length. | |
| 48 | * | |
| 49 | * @param key The name of the key to associate with the value. | |
| 50 | * @param value The value to persist. | |
| 51 | * @throws BackingStoreException New value not persisted. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public void put( final String key, final String value ) | |
| 55 | throws BackingStoreException { | |
| 56 | getState().put( key, value ); | |
| 57 | getState().flush(); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public String get( final String key, final String value ) { | |
| 62 | return getState().get( key, value ); | |
| 63 | } | |
| 64 | ||
| 65 | @Override | |
| 66 | public String get( final String key ) { | |
| 67 | return get( key, "" ); | |
| 68 | } | |
| 69 | ||
| 70 | private Preferences getRootPreferences() { | |
| 71 | return userRoot().node( PREFS_ROOT ); | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public Preferences getState() { | |
| 76 | return getRootPreferences().node( PREFS_STATE ); | |
| 77 | } | |
| 78 | } | |
| 79 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.impl; |
| 29 | 3 | |
| ... | ||
| 44 | 18 | * Responsible for loading settings that help avoid hard-coded assumptions. |
| 45 | 19 | */ |
| 46 | public class DefaultSettings implements Settings { | |
| 20 | public final class DefaultSettings implements Settings { | |
| 47 | 21 | |
| 48 | 22 | private static final char VALUE_SEPARATOR = ','; |
| 49 | 23 | |
| 50 | private PropertiesConfiguration mProperties; | |
| 24 | private final PropertiesConfiguration mProperties = createProperties(); | |
| 51 | 25 | |
| 52 | 26 | public DefaultSettings() { |
| 53 | setProperties( createProperties() ); | |
| 54 | 27 | } |
| 55 | 28 | |
| ... | ||
| 140 | 113 | private URL getPropertySource() { |
| 141 | 114 | return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS ); |
| 142 | } | |
| 143 | ||
| 144 | private void setProperties( final PropertiesConfiguration properties ) { | |
| 145 | mProperties = properties; | |
| 146 | 115 | } |
| 147 | 116 | |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.service.impl; |
| 29 | 3 | |
| ... | ||
| 39 | 13 | |
| 40 | 14 | import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT; |
| 15 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 41 | 16 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; |
| 42 | 17 | |
| 43 | 18 | /** |
| 44 | 19 | * Listens for file changes. Other classes can register paths to be monitored |
| 45 | 20 | * and listen for changes to those paths. |
| 46 | 21 | */ |
| 47 | 22 | public class DefaultSnitch extends Observable implements Snitch { |
| 23 | private final Thread mSnitchThread = new Thread( this ); | |
| 48 | 24 | |
| 49 | 25 | /** |
| ... | ||
| 68 | 44 | |
| 69 | 45 | public DefaultSnitch() { |
| 46 | } | |
| 47 | ||
| 48 | @Override | |
| 49 | public void start() { | |
| 50 | mSnitchThread.start(); | |
| 70 | 51 | } |
| 71 | 52 | |
| 72 | 53 | @Override |
| 73 | 54 | public void stop() { |
| 74 | 55 | setListening( false ); |
| 56 | ||
| 57 | try { | |
| 58 | mSnitchThread.interrupt(); | |
| 59 | mSnitchThread.join(); | |
| 60 | } catch( final Exception ex ) { | |
| 61 | clue( ex ); | |
| 62 | } | |
| 75 | 63 | } |
| 76 | 64 | |
| ... | ||
| 157 | 145 | ignore( path ); |
| 158 | 146 | } |
| 159 | } catch( final IOException | InterruptedException ex ) { | |
| 147 | } catch( final Exception ex ) { | |
| 160 | 148 | // Stop eavesdropping. |
| 161 | 149 | setListening( false ); |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.sigils; |
| 29 | 3 | |
| ... | ||
| 39 | 13 | public static final char SUFFIX = '`'; |
| 40 | 14 | |
| 41 | private final String mDelimiterBegan = | |
| 42 | getUserPreferences().getRDelimiterBegan(); | |
| 43 | private final String mDelimiterEnded = | |
| 44 | getUserPreferences().getRDelimiterEnded(); | |
| 15 | /** | |
| 16 | * Definition variables are inserted into the document before R variables, | |
| 17 | * so this is required to reformat the definition variable suitable for R. | |
| 18 | */ | |
| 19 | private final SigilOperator mAntecedent; | |
| 20 | ||
| 21 | public RSigilOperator( final Tokens tokens, final SigilOperator antecedent ) { | |
| 22 | super( tokens ); | |
| 23 | mAntecedent = antecedent; | |
| 24 | } | |
| 45 | 25 | |
| 46 | 26 | /** |
| 47 | 27 | * Returns the given string R-escaping backticks prepended and appended. This |
| 48 | 28 | * is not null safe. Do not pass null into this method. |
| 49 | 29 | * |
| 50 | 30 | * @param key The string to adorn with R token delimiters. |
| 51 | * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`". | |
| 31 | * @return PREFIX + delimiterBegan + variableName+ delimiterEnded + SUFFIX. | |
| 52 | 32 | */ |
| 53 | 33 | @Override |
| 54 | 34 | public String apply( final String key ) { |
| 55 | 35 | assert key != null; |
| 56 | ||
| 57 | return PREFIX | |
| 58 | + mDelimiterBegan | |
| 59 | + entoken( key ) | |
| 60 | + mDelimiterEnded | |
| 61 | + SUFFIX; | |
| 36 | return PREFIX + getBegan() + entoken( key ) + getEnded() + SUFFIX; | |
| 62 | 37 | } |
| 63 | 38 | |
| 64 | 39 | /** |
| 65 | 40 | * Transforms a definition key (bracketed by token delimiters) into the |
| 66 | 41 | * expected format for an R variable key name. |
| 67 | 42 | * |
| 68 | 43 | * @param key The variable name to transform, can be empty but not null. |
| 69 | 44 | * @return The transformed variable name. |
| 70 | 45 | */ |
| 71 | public static String entoken( final String key ) { | |
| 72 | return "v$" + | |
| 73 | YamlSigilOperator.detoken( key ) | |
| 74 | .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R ); | |
| 46 | public String entoken( final String key ) { | |
| 47 | return "v$" + mAntecedent.detoken( key ) | |
| 48 | .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R ); | |
| 75 | 49 | } |
| 76 | 50 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.sigils; |
| 29 | ||
| 30 | import com.keenwrite.preferences.UserPreferences; | |
| 31 | 3 | |
| 32 | 4 | import java.util.function.UnaryOperator; |
| ... | ||
| 39 | 11 | */ |
| 40 | 12 | public abstract class SigilOperator implements UnaryOperator<String> { |
| 41 | protected static UserPreferences getUserPreferences() { | |
| 42 | return UserPreferences.getInstance(); | |
| 13 | private final Tokens mTokens; | |
| 14 | ||
| 15 | SigilOperator( final Tokens tokens ) { | |
| 16 | mTokens = tokens; | |
| 17 | } | |
| 18 | ||
| 19 | /** | |
| 20 | * Removes start and stop definition key delimiters from the given key. This | |
| 21 | * method does not check for delimiters, only that there are sufficient | |
| 22 | * characters to remove from either end of the given key. | |
| 23 | * | |
| 24 | * @param key The key adorned with start and stop tokens. | |
| 25 | * @return The given key with the delimiters removed. | |
| 26 | */ | |
| 27 | String detoken( final String key ) { | |
| 28 | return key; | |
| 29 | } | |
| 30 | ||
| 31 | String getBegan() { | |
| 32 | return mTokens.getBegan(); | |
| 33 | } | |
| 34 | ||
| 35 | String getEnded() { | |
| 36 | return mTokens.getEnded(); | |
| 43 | 37 | } |
| 38 | ||
| 39 | /** | |
| 40 | * Wraps the given key in the began and ended tokens. This may perform any | |
| 41 | * preprocessing necessary to ensure the transformation happens. | |
| 42 | * | |
| 43 | * @param key The variable name to transform. | |
| 44 | * @return The given key with tokens to delimit it (from the edited text). | |
| 45 | */ | |
| 46 | public abstract String entoken( final String key ); | |
| 44 | 47 | } |
| 45 | 48 | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.sigils; | |
| 3 | ||
| 4 | import javafx.beans.property.StringProperty; | |
| 5 | ||
| 6 | import java.util.AbstractMap.SimpleImmutableEntry; | |
| 7 | ||
| 8 | /** | |
| 9 | * Convenience class for pairing a start and end sigil together. | |
| 10 | */ | |
| 11 | public final class Tokens | |
| 12 | extends SimpleImmutableEntry<StringProperty, StringProperty> { | |
| 13 | ||
| 14 | /** | |
| 15 | * Associates a new key-value pair. | |
| 16 | * | |
| 17 | * @param began The starting sigil. | |
| 18 | * @param ended The ending sigil. | |
| 19 | */ | |
| 20 | public Tokens( final StringProperty began, final StringProperty ended ) { | |
| 21 | super( began, ended ); | |
| 22 | } | |
| 23 | ||
| 24 | /** | |
| 25 | * @return The opening sigil token. | |
| 26 | */ | |
| 27 | public String getBegan() { | |
| 28 | return getKey().get(); | |
| 29 | } | |
| 30 | ||
| 31 | /** | |
| 32 | * @return The closing sigil token, or the empty string if none set. | |
| 33 | */ | |
| 34 | public String getEnded() { | |
| 35 | return getValue().get(); | |
| 36 | } | |
| 37 | } | |
| 1 | 38 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.sigils; |
| 29 | ||
| 30 | import java.util.regex.Pattern; | |
| 31 | ||
| 32 | import static java.lang.String.format; | |
| 33 | import static java.util.regex.Pattern.compile; | |
| 34 | import static java.util.regex.Pattern.quote; | |
| 35 | 3 | |
| 36 | 4 | /** |
| 37 | 5 | * Brackets definition keys with token delimiters. |
| 38 | 6 | */ |
| 39 | 7 | public class YamlSigilOperator extends SigilOperator { |
| 40 | 8 | public static final char KEY_SEPARATOR_DEF = '.'; |
| 41 | ||
| 42 | private static final String mDelimiterBegan = | |
| 43 | getUserPreferences().getDefDelimiterBegan(); | |
| 44 | private static final String mDelimiterEnded = | |
| 45 | getUserPreferences().getDefDelimiterEnded(); | |
| 46 | ||
| 47 | /** | |
| 48 | * Non-greedy match of key names delimited by definition tokens. | |
| 49 | */ | |
| 50 | private static final String REGEX = | |
| 51 | format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) ); | |
| 52 | 9 | |
| 53 | /** | |
| 54 | * Compiled regular expression for matching delimited references. | |
| 55 | */ | |
| 56 | public static final Pattern REGEX_PATTERN = compile( REGEX ); | |
| 10 | public YamlSigilOperator( final Tokens tokens ) { | |
| 11 | super( tokens ); | |
| 12 | } | |
| 57 | 13 | |
| 58 | 14 | /** |
| ... | ||
| 74 | 30 | * @return The given key bracketed by definition token symbols. |
| 75 | 31 | */ |
| 76 | public static String entoken( final String key ) { | |
| 32 | public String entoken( final String key ) { | |
| 77 | 33 | assert key != null; |
| 78 | return mDelimiterBegan + key + mDelimiterEnded; | |
| 34 | return getBegan() + key + getEnded(); | |
| 79 | 35 | } |
| 80 | 36 | |
| ... | ||
| 87 | 43 | * @return The given key with the delimiters removed. |
| 88 | 44 | */ |
| 89 | public static String detoken( final String key ) { | |
| 90 | final int beganLen = mDelimiterBegan.length(); | |
| 91 | final int endedLen = mDelimiterEnded.length(); | |
| 45 | public String detoken( final String key ) { | |
| 46 | final int beganLen = getBegan().length(); | |
| 47 | final int endedLen = getEnded().length(); | |
| 92 | 48 | |
| 93 | 49 | return key.length() > beganLen + endedLen |
| 94 | ? key.substring( beganLen, key.length() - endedLen ) | |
| 95 | : key; | |
| 50 | ? key.substring( beganLen, key.length() - endedLen ) | |
| 51 | : key; | |
| 96 | 52 | } |
| 97 | 53 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.spelling.api; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.spelling.api; |
| 29 | 3 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * Redistribution and use in source and binary forms, with or without | |
| 4 | * modification, are permitted provided that the following conditions are met: | |
| 5 | * | |
| 6 | * o Redistributions of source code must retain the above copyright | |
| 7 | * notice, this list of conditions and the following disclaimer. | |
| 8 | * | |
| 9 | * o Redistributions in binary form must reproduce the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer in the | |
| 11 | * documentation and/or other materials provided with the distribution. | |
| 12 | * | |
| 13 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 14 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 15 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 16 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 17 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 18 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 19 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 20 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 21 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 24 | */ | |
| 25 | ||
| 26 | /** | |
| 27 | * This package defines interfaces for spell checking implementations. | |
| 28 | */ | |
| 29 | package com.keenwrite.spelling.api; | |
| 1 | 30 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.spelling.impl; |
| 29 | 3 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.spelling.impl; |
| 29 | 3 | |
| 4 | import com.keenwrite.exceptions.MissingFileException; | |
| 30 | 5 | import com.keenwrite.spelling.api.SpellCheckListener; |
| 31 | 6 | import com.keenwrite.spelling.api.SpellChecker; |
| 32 | 7 | import io.gitlab.rxp90.jsymspell.SuggestItem; |
| 33 | 8 | import io.gitlab.rxp90.jsymspell.SymSpell; |
| 34 | 9 | import io.gitlab.rxp90.jsymspell.SymSpellBuilder; |
| 35 | 10 | |
| 11 | import java.io.BufferedReader; | |
| 12 | import java.io.InputStreamReader; | |
| 36 | 13 | import java.text.BreakIterator; |
| 37 | 14 | import java.util.ArrayList; |
| 38 | 15 | import java.util.Collection; |
| 39 | 16 | import java.util.List; |
| 17 | import java.util.stream.Collectors; | |
| 40 | 18 | |
| 19 | import static com.keenwrite.Constants.LEXICONS_DIRECTORY; | |
| 20 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 41 | 21 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity; |
| 42 | 22 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL; |
| 43 | 23 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST; |
| 44 | 24 | import static java.lang.Character.isLetter; |
| 25 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 45 | 26 | |
| 46 | 27 | /** |
| ... | ||
| 53 | 34 | |
| 54 | 35 | /** |
| 55 | * Creates a new lexicon for the given collection of lexemes. | |
| 36 | * Creates a new spellchecker for a lexicon of words found in the specified | |
| 37 | * file. | |
| 56 | 38 | * |
| 57 | * @param lexiconWords The words in the lexicon to add for spell checking, | |
| 58 | * must not be empty. | |
| 39 | * @param filename Lexicon language file (e.g., "en.txt"). | |
| 59 | 40 | * @return An instance of {@link SpellChecker} that can check if a word |
| 60 | * is correct and suggest alternatives. | |
| 41 | * is correct and suggest alternatives, or {@link PermissiveSpeller} if the | |
| 42 | * lexicon cannot be loaded. | |
| 61 | 43 | */ |
| 62 | public static SpellChecker forLexicon( | |
| 44 | public static SpellChecker forLexicon( final String filename ) { | |
| 45 | try { | |
| 46 | final var lexicon = readLexicon( filename ); | |
| 47 | return SymSpellSpeller.forLexicon( lexicon ); | |
| 48 | } catch( final Exception ex ) { | |
| 49 | clue( ex ); | |
| 50 | return new PermissiveSpeller(); | |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 54 | private static SpellChecker forLexicon( | |
| 63 | 55 | final Collection<String> lexiconWords ) { |
| 64 | 56 | assert lexiconWords != null && !lexiconWords.isEmpty(); |
| 65 | 57 | |
| 66 | final SymSpellBuilder builder = new SymSpellBuilder() | |
| 58 | final var builder = new SymSpellBuilder() | |
| 67 | 59 | .setLexiconWords( lexiconWords ); |
| 68 | 60 | |
| ... | ||
| 127 | 119 | previousIndex = boundaryIndex; |
| 128 | 120 | boundaryIndex = mBreakIterator.next(); |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | @SuppressWarnings("SameParameterValue") | |
| 125 | private static Collection<String> readLexicon( final String filename ) | |
| 126 | throws Exception { | |
| 127 | final var path = '/' + LEXICONS_DIRECTORY + '/' + filename; | |
| 128 | ||
| 129 | try( final var resource = | |
| 130 | SymSpellSpeller.class.getResourceAsStream( path ) ) { | |
| 131 | if( resource == null ) { | |
| 132 | throw new MissingFileException( path ); | |
| 133 | } | |
| 134 | ||
| 135 | try( final var isr = new InputStreamReader( resource, UTF_8 ); | |
| 136 | final var reader = new BufferedReader( isr ) ) { | |
| 137 | return reader.lines().collect( Collectors.toList() ); | |
| 138 | } | |
| 129 | 139 | } |
| 130 | 140 | } |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.spelling.impl; | |
| 3 | ||
| 4 | import com.keenwrite.spelling.api.SpellCheckListener; | |
| 5 | import com.keenwrite.spelling.api.SpellChecker; | |
| 6 | import com.vladsch.flexmark.parser.Parser; | |
| 7 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 8 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 9 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 10 | import org.fxmisc.richtext.model.StyleSpansBuilder; | |
| 11 | ||
| 12 | import java.util.Collection; | |
| 13 | import java.util.concurrent.atomic.AtomicInteger; | |
| 14 | ||
| 15 | import static com.keenwrite.spelling.impl.SymSpellSpeller.forLexicon; | |
| 16 | import static java.util.Collections.emptyList; | |
| 17 | import static java.util.Collections.singleton; | |
| 18 | import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; | |
| 19 | ||
| 20 | /** | |
| 21 | * Responsible for checking the spelling of a document being edited. | |
| 22 | */ | |
| 23 | public class TextEditorSpeller { | |
| 24 | /** | |
| 25 | * Only load the dictionary into memory once, because it's huge. | |
| 26 | */ | |
| 27 | private static final SpellChecker sSpellChecker = forLexicon( "en.txt" ); | |
| 28 | ||
| 29 | public TextEditorSpeller() { | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}. | |
| 34 | * call to spell check the entire document. | |
| 35 | */ | |
| 36 | public void checkDocument( final StyleClassedTextArea editor ) { | |
| 37 | spellcheck( editor, editor.getText(), -1 ); | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * Listen for changes to the any particular paragraph and perform a quick | |
| 42 | * spell check upon it. The style classes in the editor will be changed to | |
| 43 | * mark any spelling mistakes in the paragraph. The user may then interact | |
| 44 | * with any misspelled word (i.e., any piece of text that is marked) to | |
| 45 | * revise the spelling. | |
| 46 | * | |
| 47 | * @param editor The text area containing paragraphs to spellcheck. | |
| 48 | */ | |
| 49 | public void checkParagraphs( final StyleClassedTextArea editor ) { | |
| 50 | // Use the plain text changes so that notifications of style changes | |
| 51 | // are suppressed. Checking against the identity ensures that only | |
| 52 | // new text additions or deletions trigger proofreading. | |
| 53 | editor.plainTextChanges() | |
| 54 | .filter( p -> !p.isIdentity() ).subscribe( change -> { | |
| 55 | ||
| 56 | // Check current paragraph; the whole document was checked upon opening. | |
| 57 | final var offset = change.getPosition(); | |
| 58 | final var position = editor.offsetToPosition( offset, Forward ); | |
| 59 | final var paraId = position.getMajor(); | |
| 60 | final var paragraph = editor.getParagraph( paraId ); | |
| 61 | final var text = paragraph.getText(); | |
| 62 | ||
| 63 | // Prevent doubling-up styles. | |
| 64 | editor.clearStyle( paraId ); | |
| 65 | ||
| 66 | spellcheck( editor, text, paraId ); | |
| 67 | } ); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Spellchecks a subset of the entire document. | |
| 72 | * | |
| 73 | * @param text Look up words for this text in the lexicon. | |
| 74 | * @param paraId Set to -1 to apply resulting style spans to the entire | |
| 75 | * text. | |
| 76 | */ | |
| 77 | private void spellcheck( | |
| 78 | final StyleClassedTextArea editor, final String text, final int paraId ) { | |
| 79 | final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 80 | final var runningIndex = new AtomicInteger( 0 ); | |
| 81 | ||
| 82 | // The text nodes must be relayed through a contextual "visitor" that | |
| 83 | // can return text in chunks with correlative offsets into the string. | |
| 84 | // This allows Markdown, R Markdown, XML, and R XML documents to return | |
| 85 | // sets of words to check. | |
| 86 | ||
| 87 | final var node = mParser.parse( text ); | |
| 88 | final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> { | |
| 89 | // Treat hyphenated compound words as individual words. | |
| 90 | final var check = visited.replace( '-', ' ' ); | |
| 91 | ||
| 92 | sSpellChecker.proofread( check, ( misspelled, prevIndex, currIndex ) -> { | |
| 93 | prevIndex += bIndex; | |
| 94 | currIndex += bIndex; | |
| 95 | ||
| 96 | // Clear styling between lexiconically absent words. | |
| 97 | builder.add( emptyList(), prevIndex - runningIndex.get() ); | |
| 98 | builder.add( singleton( "spelling" ), currIndex - prevIndex ); | |
| 99 | runningIndex.set( currIndex ); | |
| 100 | } ); | |
| 101 | } ); | |
| 102 | ||
| 103 | visitor.visit( node ); | |
| 104 | ||
| 105 | // If the running index was set, at least one word triggered the listener. | |
| 106 | if( runningIndex.get() > 0 ) { | |
| 107 | // Clear styling after the last lexiconically absent word. | |
| 108 | builder.add( emptyList(), text.length() - runningIndex.get() ); | |
| 109 | ||
| 110 | final var spans = builder.create(); | |
| 111 | ||
| 112 | if( paraId >= 0 ) { | |
| 113 | editor.setStyleSpans( paraId, 0, spans ); | |
| 114 | } | |
| 115 | else { | |
| 116 | editor.setStyleSpans( 0, spans ); | |
| 117 | } | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * TODO: #59 -- Replace using Markdown processor instantiated for Markdown | |
| 123 | * files. | |
| 124 | */ | |
| 125 | private final Parser mParser = Parser.builder().build(); | |
| 126 | ||
| 127 | /** | |
| 128 | * TODO: #59 -- Replace with generic interface; provide Markdown/XML | |
| 129 | * implementations. | |
| 130 | */ | |
| 131 | private static final class TextVisitor { | |
| 132 | private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>( | |
| 133 | com.vladsch.flexmark.ast.Text.class, this::visit ) | |
| 134 | ); | |
| 135 | ||
| 136 | private final SpellCheckListener mConsumer; | |
| 137 | ||
| 138 | public TextVisitor( final SpellCheckListener consumer ) { | |
| 139 | mConsumer = consumer; | |
| 140 | } | |
| 141 | ||
| 142 | private void visit( final com.vladsch.flexmark.util.ast.Node node ) { | |
| 143 | if( node instanceof com.vladsch.flexmark.ast.Text ) { | |
| 144 | mConsumer.accept( node.getChars().toString(), | |
| 145 | node.getStartOffset(), | |
| 146 | node.getEndOffset() ); | |
| 147 | } | |
| 148 | ||
| 149 | mVisitor.visitChildren( node ); | |
| 150 | } | |
| 151 | } | |
| 152 | } | |
| 1 | 153 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. | |
| 2 | * | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /** | |
| 29 | * This package contains classes for spell checking implementations. | |
| 30 | */ | |
| 31 | package com.keenwrite.spelling.impl; | |
| 1 | 32 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import com.keenwrite.Messages; | |
| 5 | import com.keenwrite.util.GenericBuilder; | |
| 6 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 7 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 8 | import javafx.event.ActionEvent; | |
| 9 | import javafx.event.EventHandler; | |
| 10 | import javafx.scene.control.Button; | |
| 11 | import javafx.scene.control.Menu; | |
| 12 | import javafx.scene.control.MenuItem; | |
| 13 | import javafx.scene.control.Tooltip; | |
| 14 | import javafx.scene.input.KeyCombination; | |
| 15 | ||
| 16 | import java.util.ArrayList; | |
| 17 | import java.util.List; | |
| 18 | ||
| 19 | import static de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory.get; | |
| 20 | import static javafx.scene.input.KeyCombination.valueOf; | |
| 21 | ||
| 22 | /** | |
| 23 | * Defines actions the user can take through GUI interactions | |
| 24 | */ | |
| 25 | public class Action implements MenuAction { | |
| 26 | private final String mText; | |
| 27 | private final KeyCombination mAccelerator; | |
| 28 | private final GlyphIcons mIcon; | |
| 29 | private final EventHandler<ActionEvent> mHandler; | |
| 30 | private final List<MenuAction> mSubActions = new ArrayList<>(); | |
| 31 | ||
| 32 | public Action( | |
| 33 | final String text, | |
| 34 | final String accelerator, | |
| 35 | final GlyphIcons icon, | |
| 36 | final EventHandler<ActionEvent> handler ) { | |
| 37 | assert text != null; | |
| 38 | assert handler != null; | |
| 39 | ||
| 40 | mText = text; | |
| 41 | mAccelerator = accelerator == null ? null : valueOf( accelerator ); | |
| 42 | mIcon = icon; | |
| 43 | mHandler = handler; | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * Runs this action. Most actions are mapped to menu items, but some actions | |
| 48 | * (such as the Insert key to toggle overwrite mode) are not. | |
| 49 | */ | |
| 50 | public void execute() { | |
| 51 | mHandler.handle( new ActionEvent() ); | |
| 52 | } | |
| 53 | ||
| 54 | @Override | |
| 55 | public MenuItem createMenuItem() { | |
| 56 | // This will either become a menu or a menu item, depending on whether | |
| 57 | // sub-actions are defined. | |
| 58 | final MenuItem menuItem; | |
| 59 | ||
| 60 | if( mSubActions.isEmpty() ) { | |
| 61 | // Regular menu item has no sub-menus. | |
| 62 | menuItem = new MenuItem( mText ); | |
| 63 | } | |
| 64 | else { | |
| 65 | // Sub-actions are translated into sub-menu items beneath this action. | |
| 66 | final var submenu = new Menu( mText ); | |
| 67 | ||
| 68 | for( final var action : mSubActions ) { | |
| 69 | // Recursive call that creates a sub-menu hierarchy. | |
| 70 | submenu.getItems().add( action.createMenuItem() ); | |
| 71 | } | |
| 72 | ||
| 73 | menuItem = submenu; | |
| 74 | } | |
| 75 | ||
| 76 | if( mAccelerator != null ) { | |
| 77 | menuItem.setAccelerator( mAccelerator ); | |
| 78 | } | |
| 79 | ||
| 80 | if( mIcon != null ) { | |
| 81 | menuItem.setGraphic( get().createIcon( mIcon ) ); | |
| 82 | } | |
| 83 | ||
| 84 | menuItem.setOnAction( mHandler ); | |
| 85 | ||
| 86 | return menuItem; | |
| 87 | } | |
| 88 | ||
| 89 | @Override | |
| 90 | public Button createToolBarNode() { | |
| 91 | final var button = createIconButton(); | |
| 92 | var tooltip = mText; | |
| 93 | ||
| 94 | if( tooltip.endsWith( "..." ) ) { | |
| 95 | tooltip = tooltip.substring( 0, tooltip.length() - 3 ); | |
| 96 | } | |
| 97 | ||
| 98 | if( mAccelerator != null ) { | |
| 99 | tooltip += " (" + mAccelerator.getDisplayText() + ')'; | |
| 100 | } | |
| 101 | ||
| 102 | button.setTooltip( new Tooltip( tooltip ) ); | |
| 103 | button.setFocusTraversable( false ); | |
| 104 | button.setOnAction( mHandler ); | |
| 105 | ||
| 106 | return button; | |
| 107 | } | |
| 108 | ||
| 109 | private Button createIconButton() { | |
| 110 | final var button = new Button(); | |
| 111 | button.setGraphic( get().createIcon( mIcon, "1.2em" ) ); | |
| 112 | return button; | |
| 113 | } | |
| 114 | ||
| 115 | /** | |
| 116 | * Adds subordinate actions to the menu. This is used to establish sub-menu | |
| 117 | * relationships. The default behaviour does not wire up any registration; | |
| 118 | * subclasses are responsible for handling how actions relate to one another. | |
| 119 | * | |
| 120 | * @param action Actions that only exist with respect to this action. | |
| 121 | */ | |
| 122 | public MenuAction addSubActions( final MenuAction... action ) { | |
| 123 | mSubActions.addAll( List.of( action ) ); | |
| 124 | return this; | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * TODO: Reuse the {@link GenericBuilder}. | |
| 129 | * | |
| 130 | * @return The {@link Builder} for an instance of {@link Action}. | |
| 131 | */ | |
| 132 | public static Builder builder() { | |
| 133 | return new Builder(); | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Provides a fluent interface around constructing actions so that duplication | |
| 138 | * can be avoided. | |
| 139 | */ | |
| 140 | public static class Builder { | |
| 141 | private String mText; | |
| 142 | private String mAccelerator; | |
| 143 | private GlyphIcons mIcon; | |
| 144 | private EventHandler<ActionEvent> mHandler; | |
| 145 | ||
| 146 | /** | |
| 147 | * Sets the text, icon, and accelerator for a given action identifier. | |
| 148 | * See the "App.action" entries in the messages properties file for details. | |
| 149 | * | |
| 150 | * @param id The identifier to look up in the properties file. | |
| 151 | * @return An instance of {@link Builder} that can be built into an | |
| 152 | * instance of {@link Action}. | |
| 153 | */ | |
| 154 | public Builder setId( final String id ) { | |
| 155 | final var prefix = "App.action." + id + "."; | |
| 156 | final var text = prefix + "text"; | |
| 157 | final var icon = prefix + "icon"; | |
| 158 | final var accelerator = prefix + "accelerator"; | |
| 159 | final var builder = setText( text ).setIcon( icon ); | |
| 160 | ||
| 161 | return Messages.containsKey( accelerator ) | |
| 162 | ? builder.setAccelerator( Messages.get( accelerator ) ) | |
| 163 | : builder; | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Sets the action text based on a resource bundle key. | |
| 168 | * | |
| 169 | * @param key The key to look up in the {@link Messages}. | |
| 170 | * @return The corresponding value, or the key name if none found. | |
| 171 | */ | |
| 172 | private Builder setText( final String key ) { | |
| 173 | mText = Messages.get( key, key ); | |
| 174 | return this; | |
| 175 | } | |
| 176 | ||
| 177 | private Builder setAccelerator( final String accelerator ) { | |
| 178 | mAccelerator = accelerator; | |
| 179 | return this; | |
| 180 | } | |
| 181 | ||
| 182 | private Builder setIcon( final GlyphIcons icon ) { | |
| 183 | mIcon = icon; | |
| 184 | return this; | |
| 185 | } | |
| 186 | ||
| 187 | private Builder setIcon( final String iconKey ) { | |
| 188 | assert iconKey != null; | |
| 189 | ||
| 190 | final var iconValue = Messages.get( iconKey ); | |
| 191 | ||
| 192 | return iconKey.equals( iconValue ) | |
| 193 | ? this | |
| 194 | : setIcon( getIcon( iconValue ) ); | |
| 195 | } | |
| 196 | ||
| 197 | public Builder setHandler( final EventHandler<ActionEvent> handler ) { | |
| 198 | mHandler = handler; | |
| 199 | return this; | |
| 200 | } | |
| 201 | ||
| 202 | public Action build() { | |
| 203 | return new Action( mText, mAccelerator, mIcon, mHandler ); | |
| 204 | } | |
| 205 | ||
| 206 | private GlyphIcons getIcon( final String name ) { | |
| 207 | return FontAwesomeIcon.valueOf( name.toUpperCase() ); | |
| 208 | } | |
| 209 | } | |
| 210 | } | |
| 1 | 211 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import com.keenwrite.ExportFormat; | |
| 5 | import com.keenwrite.MainPane; | |
| 6 | import com.keenwrite.editors.TextDefinition; | |
| 7 | import com.keenwrite.editors.TextEditor; | |
| 8 | import com.keenwrite.preferences.PreferencesController; | |
| 9 | import com.keenwrite.preferences.Workspace; | |
| 10 | import com.keenwrite.processors.ProcessorContext; | |
| 11 | import com.keenwrite.search.SearchModel; | |
| 12 | import com.keenwrite.ui.controls.SearchBar; | |
| 13 | import javafx.scene.control.Alert; | |
| 14 | import javafx.scene.image.ImageView; | |
| 15 | import javafx.stage.Window; | |
| 16 | import javafx.stage.WindowEvent; | |
| 17 | ||
| 18 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 19 | import static com.keenwrite.Constants.ICON_DIALOG; | |
| 20 | import static com.keenwrite.ExportFormat.*; | |
| 21 | import static com.keenwrite.Messages.get; | |
| 22 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 23 | import static com.keenwrite.StatusBarNotifier.getStatusBar; | |
| 24 | import static com.keenwrite.preferences.Workspace.KEY_UI_RECENT_DIR; | |
| 25 | import static com.keenwrite.processors.ProcessorFactory.createProcessors; | |
| 26 | import static java.nio.file.Files.writeString; | |
| 27 | import static javafx.event.Event.fireEvent; | |
| 28 | import static javafx.scene.control.Alert.AlertType.INFORMATION; | |
| 29 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 30 | ||
| 31 | /** | |
| 32 | * Responsible for abstracting how functionality is mapped to the application. | |
| 33 | * This allows users to customize accelerator keys and will provide pluggable | |
| 34 | * functionality so that different text markup languages can change documents | |
| 35 | * using their respective syntax. | |
| 36 | */ | |
| 37 | @SuppressWarnings("NonAsciiCharacters") | |
| 38 | public class ApplicationActions { | |
| 39 | private static final String STYLE_SEARCH = "search"; | |
| 40 | ||
| 41 | /** | |
| 42 | * When an action is executed, this is one of the recipients. | |
| 43 | */ | |
| 44 | private final MainPane mMainPane; | |
| 45 | ||
| 46 | /** | |
| 47 | * Tracks finding text in the active document. | |
| 48 | */ | |
| 49 | private final SearchModel mSearchModel; | |
| 50 | ||
| 51 | public ApplicationActions( final MainPane mainPane ) { | |
| 52 | mMainPane = mainPane; | |
| 53 | mSearchModel = new SearchModel(); | |
| 54 | mSearchModel.matchOffsetProperty().addListener( ( c, o, n ) -> { | |
| 55 | final var editor = getActiveTextEditor(); | |
| 56 | ||
| 57 | // Clear highlighted areas before adding highlighting to a new region. | |
| 58 | if( o != null ) { | |
| 59 | editor.unstylize( STYLE_SEARCH ); | |
| 60 | } | |
| 61 | ||
| 62 | if( n != null ) { | |
| 63 | editor.moveTo( n.getStart() ); | |
| 64 | editor.stylize( n, STYLE_SEARCH ); | |
| 65 | } | |
| 66 | } ); | |
| 67 | ||
| 68 | // When the active text editor changes, update the haystack. | |
| 69 | mMainPane.activeTextEditorProperty().addListener( | |
| 70 | ( c, o, n ) -> mSearchModel.search( getActiveTextEditor().getText() ) | |
| 71 | ); | |
| 72 | } | |
| 73 | ||
| 74 | public void file‿new() { | |
| 75 | getMainPane().newTextEditor(); | |
| 76 | } | |
| 77 | ||
| 78 | public void file‿open() { | |
| 79 | getMainPane().open( createFileChooser().openFiles() ); | |
| 80 | } | |
| 81 | ||
| 82 | public void file‿close() { | |
| 83 | getMainPane().close(); | |
| 84 | } | |
| 85 | ||
| 86 | public void file‿close_all() { | |
| 87 | getMainPane().closeAll(); | |
| 88 | } | |
| 89 | ||
| 90 | public void file‿save() { | |
| 91 | getMainPane().save(); | |
| 92 | } | |
| 93 | ||
| 94 | public void file‿save_as() { | |
| 95 | final var file = createFileChooser().saveAs(); | |
| 96 | file.ifPresent( ( f ) -> getMainPane().saveAs( f ) ); | |
| 97 | } | |
| 98 | ||
| 99 | public void file‿save_all() { | |
| 100 | getMainPane().saveAll(); | |
| 101 | } | |
| 102 | ||
| 103 | public void file‿export‿html_svg() { | |
| 104 | file‿export( HTML_TEX_SVG ); | |
| 105 | } | |
| 106 | ||
| 107 | public void file‿export‿html_tex() { | |
| 108 | file‿export( HTML_TEX_DELIMITED ); | |
| 109 | } | |
| 110 | ||
| 111 | public void file‿export‿markdown() { | |
| 112 | file‿export( MARKDOWN_PLAIN ); | |
| 113 | } | |
| 114 | ||
| 115 | private void file‿export( final ExportFormat format ) { | |
| 116 | final var editor = getActiveTextEditor(); | |
| 117 | final var context = createProcessorContext( editor ); | |
| 118 | final var chain = createProcessors( context ); | |
| 119 | final var doc = editor.getText(); | |
| 120 | final var export = chain.apply( doc ); | |
| 121 | final var filename = format.toExportFilename( editor.getPath() ); | |
| 122 | final var chooser = createFileChooser(); | |
| 123 | final var file = chooser.exportAs( filename ); | |
| 124 | ||
| 125 | file.ifPresent( ( f ) -> { | |
| 126 | try { | |
| 127 | writeString( f.toPath(), export ); | |
| 128 | final var m = get( "Main.status.export.success", f.toString() ); | |
| 129 | clue( m ); | |
| 130 | } catch( final Exception e ) { | |
| 131 | clue( e ); | |
| 132 | } | |
| 133 | } ); | |
| 134 | } | |
| 135 | ||
| 136 | private ProcessorContext createProcessorContext( final TextEditor editor ) { | |
| 137 | return getMainPane().createProcessorContext( editor ); | |
| 138 | } | |
| 139 | ||
| 140 | public void file‿exit() { | |
| 141 | final var window = getWindow(); | |
| 142 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 143 | } | |
| 144 | ||
| 145 | public void edit‿undo() { | |
| 146 | getActiveTextEditor().undo(); | |
| 147 | } | |
| 148 | ||
| 149 | public void edit‿redo() { | |
| 150 | getActiveTextEditor().redo(); | |
| 151 | } | |
| 152 | ||
| 153 | public void edit‿cut() { | |
| 154 | getActiveTextEditor().cut(); | |
| 155 | } | |
| 156 | ||
| 157 | public void edit‿copy() { | |
| 158 | getActiveTextEditor().copy(); | |
| 159 | } | |
| 160 | ||
| 161 | public void edit‿paste() { | |
| 162 | getActiveTextEditor().paste(); | |
| 163 | } | |
| 164 | ||
| 165 | public void edit‿select_all() { | |
| 166 | getActiveTextEditor().selectAll(); | |
| 167 | } | |
| 168 | ||
| 169 | public void edit‿find() { | |
| 170 | final var nodes = getStatusBar().getLeftItems(); | |
| 171 | ||
| 172 | if( nodes.isEmpty() ) { | |
| 173 | final var searchBar = new SearchBar(); | |
| 174 | ||
| 175 | searchBar.matchIndexProperty().bind( mSearchModel.matchIndexProperty() ); | |
| 176 | searchBar.matchCountProperty().bind( mSearchModel.matchCountProperty() ); | |
| 177 | ||
| 178 | searchBar.setOnCancelAction( ( event ) -> { | |
| 179 | final var editor = getActiveTextEditor(); | |
| 180 | nodes.remove( searchBar ); | |
| 181 | editor.unstylize( STYLE_SEARCH ); | |
| 182 | editor.getNode().requestFocus(); | |
| 183 | } ); | |
| 184 | ||
| 185 | searchBar.addInputListener( ( c, o, n ) -> { | |
| 186 | if( n != null && !n.isEmpty() ) { | |
| 187 | mSearchModel.search( n, getActiveTextEditor().getText() ); | |
| 188 | } | |
| 189 | } ); | |
| 190 | ||
| 191 | searchBar.setOnNextAction( ( event ) -> edit‿find_next() ); | |
| 192 | searchBar.setOnPrevAction( ( event ) -> edit‿find_prev() ); | |
| 193 | ||
| 194 | nodes.add( searchBar ); | |
| 195 | searchBar.requestFocus(); | |
| 196 | } | |
| 197 | else { | |
| 198 | nodes.clear(); | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | public void edit‿find_next() { | |
| 203 | mSearchModel.advance(); | |
| 204 | } | |
| 205 | ||
| 206 | public void edit‿find_prev() { | |
| 207 | mSearchModel.retreat(); | |
| 208 | } | |
| 209 | ||
| 210 | public void edit‿preferences() { | |
| 211 | new PreferencesController( getWorkspace() ).show(); | |
| 212 | } | |
| 213 | ||
| 214 | public void format‿bold() { | |
| 215 | getActiveTextEditor().bold(); | |
| 216 | } | |
| 217 | ||
| 218 | public void format‿italic() { | |
| 219 | getActiveTextEditor().italic(); | |
| 220 | } | |
| 221 | ||
| 222 | public void format‿superscript() { | |
| 223 | getActiveTextEditor().superscript(); | |
| 224 | } | |
| 225 | ||
| 226 | public void format‿subscript() { | |
| 227 | getActiveTextEditor().subscript(); | |
| 228 | } | |
| 229 | ||
| 230 | public void format‿strikethrough() { | |
| 231 | getActiveTextEditor().strikethrough(); | |
| 232 | } | |
| 233 | ||
| 234 | public void insert‿blockquote() { | |
| 235 | getActiveTextEditor().blockquote(); | |
| 236 | } | |
| 237 | ||
| 238 | public void insert‿code() { | |
| 239 | getActiveTextEditor().code(); | |
| 240 | } | |
| 241 | ||
| 242 | public void insert‿fenced_code_block() { | |
| 243 | getActiveTextEditor().fencedCodeBlock(); | |
| 244 | } | |
| 245 | ||
| 246 | public void insert‿link() { | |
| 247 | createMarkdownDialog().insertLink( getActiveTextEditor().getTextArea() ); | |
| 248 | } | |
| 249 | ||
| 250 | public void insert‿image() { | |
| 251 | createMarkdownDialog().insertImage( getActiveTextEditor().getTextArea() ); | |
| 252 | } | |
| 253 | ||
| 254 | private MarkdownCommands createMarkdownDialog() { | |
| 255 | return new MarkdownCommands( | |
| 256 | getMainPane(), getActiveTextEditor().getPath() ); | |
| 257 | } | |
| 258 | ||
| 259 | public void insert‿heading_1() { | |
| 260 | insert‿heading( 1 ); | |
| 261 | } | |
| 262 | ||
| 263 | public void insert‿heading_2() { | |
| 264 | insert‿heading( 2 ); | |
| 265 | } | |
| 266 | ||
| 267 | public void insert‿heading_3() { | |
| 268 | insert‿heading( 3 ); | |
| 269 | } | |
| 270 | ||
| 271 | private void insert‿heading( final int level ) { | |
| 272 | getActiveTextEditor().heading( level ); | |
| 273 | } | |
| 274 | ||
| 275 | public void insert‿unordered_list() { | |
| 276 | getActiveTextEditor().unorderedList(); | |
| 277 | } | |
| 278 | ||
| 279 | public void insert‿ordered_list() { | |
| 280 | getActiveTextEditor().orderedList(); | |
| 281 | } | |
| 282 | ||
| 283 | public void insert‿horizontal_rule() { | |
| 284 | getActiveTextEditor().horizontalRule(); | |
| 285 | } | |
| 286 | ||
| 287 | public void definition‿create() { | |
| 288 | getActiveTextDefinition().createDefinition(); | |
| 289 | } | |
| 290 | ||
| 291 | public void definition‿rename() { | |
| 292 | getActiveTextDefinition().renameDefinition(); | |
| 293 | } | |
| 294 | ||
| 295 | public void definition‿delete() { | |
| 296 | getActiveTextDefinition().deleteDefinitions(); | |
| 297 | } | |
| 298 | ||
| 299 | public void definition‿autoinsert() { | |
| 300 | getMainPane().autoinsert(); | |
| 301 | } | |
| 302 | ||
| 303 | public void view‿refresh() { | |
| 304 | getMainPane().viewRefresh(); | |
| 305 | } | |
| 306 | ||
| 307 | public void view‿preview() { | |
| 308 | getMainPane().viewPreview(); | |
| 309 | } | |
| 310 | ||
| 311 | public void help‿about() { | |
| 312 | final Alert alert = new Alert( INFORMATION ); | |
| 313 | alert.setTitle( get( "Dialog.about.title", APP_TITLE ) ); | |
| 314 | alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) ); | |
| 315 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 316 | alert.setGraphic( new ImageView( ICON_DIALOG ) ); | |
| 317 | alert.initOwner( getWindow() ); | |
| 318 | alert.showAndWait(); | |
| 319 | } | |
| 320 | ||
| 321 | private FileChooserCommand createFileChooser() { | |
| 322 | final var dir = getWorkspace().fileProperty( KEY_UI_RECENT_DIR ); | |
| 323 | return new FileChooserCommand( getWindow(), dir ); | |
| 324 | } | |
| 325 | ||
| 326 | private MainPane getMainPane() { | |
| 327 | return mMainPane; | |
| 328 | } | |
| 329 | ||
| 330 | private TextEditor getActiveTextEditor() { | |
| 331 | return getMainPane().getActiveTextEditor(); | |
| 332 | } | |
| 333 | ||
| 334 | private TextDefinition getActiveTextDefinition() { | |
| 335 | return getMainPane().getActiveTextDefinition(); | |
| 336 | } | |
| 337 | ||
| 338 | private Workspace getWorkspace() { | |
| 339 | return mMainPane.getWorkspace(); | |
| 340 | } | |
| 341 | ||
| 342 | private Window getWindow() { | |
| 343 | return getMainPane().getWindow(); | |
| 344 | } | |
| 345 | } | |
| 1 | 346 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import javafx.event.ActionEvent; | |
| 5 | import javafx.event.EventHandler; | |
| 6 | import javafx.scene.Node; | |
| 7 | import javafx.scene.control.Menu; | |
| 8 | import javafx.scene.control.MenuBar; | |
| 9 | import javafx.scene.control.MenuItem; | |
| 10 | import javafx.scene.control.ToolBar; | |
| 11 | import javafx.scene.layout.VBox; | |
| 12 | ||
| 13 | import java.util.HashMap; | |
| 14 | import java.util.Map; | |
| 15 | ||
| 16 | import static com.keenwrite.Messages.get; | |
| 17 | ||
| 18 | /** | |
| 19 | * Responsible for wiring all application actions to menus, toolbar buttons, | |
| 20 | * and keyboard shortcuts. | |
| 21 | */ | |
| 22 | public class ApplicationMenuBar { | |
| 23 | ||
| 24 | private final Map<String, Action> mMap = new HashMap<>( 64 ); | |
| 25 | ||
| 26 | /** | |
| 27 | * Empty constructor. | |
| 28 | */ | |
| 29 | public ApplicationMenuBar() { | |
| 30 | } | |
| 31 | ||
| 32 | /** | |
| 33 | * Creates the main application affordances. | |
| 34 | * | |
| 35 | * @param actions The {@link ApplicationActions} that map user interface | |
| 36 | * selections to executable code. | |
| 37 | * @return An instance of {@link Node} that contains the menu and toolbar. | |
| 38 | */ | |
| 39 | public Node createMenuBar( final ApplicationActions actions ) { | |
| 40 | final var SEPARATOR_ACTION = new SeparatorAction(); | |
| 41 | ||
| 42 | //@formatter:off | |
| 43 | final var menuBar = new MenuBar( | |
| 44 | createMenu( | |
| 45 | get( "Main.menu.file" ), | |
| 46 | addAction( "file.new", e -> actions.file‿new() ), | |
| 47 | addAction( "file.open", e -> actions.file‿open() ), | |
| 48 | SEPARATOR_ACTION, | |
| 49 | addAction( "file.close", e -> actions.file‿close() ), | |
| 50 | addAction( "file.close_all", e -> actions.file‿close_all() ), | |
| 51 | SEPARATOR_ACTION, | |
| 52 | addAction( "file.save", e -> actions.file‿save() ), | |
| 53 | addAction( "file.save_as", e -> actions.file‿save_as() ), | |
| 54 | addAction( "file.save_all", e -> actions.file‿save_all() ), | |
| 55 | SEPARATOR_ACTION, | |
| 56 | addAction( "file.export", e -> {} ) | |
| 57 | .addSubActions( | |
| 58 | addAction( "file.export.html_svg", e -> actions.file‿export‿html_svg() ), | |
| 59 | addAction( "file.export.html_tex", e -> actions.file‿export‿html_tex() ), | |
| 60 | addAction( "file.export.markdown", e -> actions.file‿export‿markdown() ) | |
| 61 | ), | |
| 62 | SEPARATOR_ACTION, | |
| 63 | addAction( "file.exit", e -> actions.file‿exit() ) | |
| 64 | ), | |
| 65 | createMenu( | |
| 66 | get( "Main.menu.edit" ), | |
| 67 | SEPARATOR_ACTION, | |
| 68 | addAction( "edit.undo", e -> actions.edit‿undo() ), | |
| 69 | addAction( "edit.redo", e -> actions.edit‿redo() ), | |
| 70 | SEPARATOR_ACTION, | |
| 71 | addAction( "edit.cut", e -> actions.edit‿cut() ), | |
| 72 | addAction( "edit.copy", e -> actions.edit‿copy() ), | |
| 73 | addAction( "edit.paste", e -> actions.edit‿paste() ), | |
| 74 | addAction( "edit.select_all", e -> actions.edit‿select_all() ), | |
| 75 | SEPARATOR_ACTION, | |
| 76 | addAction( "edit.find", e -> actions.edit‿find() ), | |
| 77 | addAction( "edit.find_next", e -> actions.edit‿find_next() ), | |
| 78 | addAction( "edit.find_prev", e -> actions.edit‿find_prev() ), | |
| 79 | SEPARATOR_ACTION, | |
| 80 | addAction( "edit.preferences", e -> actions.edit‿preferences() ) | |
| 81 | ), | |
| 82 | createMenu( | |
| 83 | get( "Main.menu.format" ), | |
| 84 | addAction( "format.bold", e -> actions.format‿bold() ), | |
| 85 | addAction( "format.italic", e -> actions.format‿italic() ), | |
| 86 | addAction( "format.superscript", e -> actions.format‿superscript() ), | |
| 87 | addAction( "format.subscript", e -> actions.format‿subscript() ), | |
| 88 | addAction( "format.strikethrough", e -> actions.format‿strikethrough() ) | |
| 89 | ), | |
| 90 | createMenu( | |
| 91 | get( "Main.menu.insert" ), | |
| 92 | addAction( "insert.blockquote", e -> actions.insert‿blockquote() ), | |
| 93 | addAction( "insert.code", e -> actions.insert‿code() ), | |
| 94 | addAction( "insert.fenced_code_block", e -> actions.insert‿fenced_code_block() ), | |
| 95 | SEPARATOR_ACTION, | |
| 96 | addAction( "insert.link", e -> actions.insert‿link() ), | |
| 97 | addAction( "insert.image", e -> actions.insert‿image() ), | |
| 98 | SEPARATOR_ACTION, | |
| 99 | addAction( "insert.heading_1", e -> actions.insert‿heading_1() ), | |
| 100 | addAction( "insert.heading_2", e -> actions.insert‿heading_2() ), | |
| 101 | addAction( "insert.heading_3", e -> actions.insert‿heading_3() ), | |
| 102 | SEPARATOR_ACTION, | |
| 103 | addAction( "insert.unordered_list", e -> actions.insert‿unordered_list() ), | |
| 104 | addAction( "insert.ordered_list", e -> actions.insert‿ordered_list() ), | |
| 105 | addAction( "insert.horizontal_rule", e -> actions.insert‿horizontal_rule() ) | |
| 106 | ), | |
| 107 | createMenu( | |
| 108 | get( "Main.menu.definition" ), | |
| 109 | addAction( "definition.insert", e -> actions.definition‿autoinsert() ), | |
| 110 | SEPARATOR_ACTION, | |
| 111 | addAction( "definition.create", e -> actions.definition‿create() ), | |
| 112 | addAction( "definition.rename", e -> actions.definition‿rename() ), | |
| 113 | addAction( "definition.delete", e -> actions.definition‿delete() ) | |
| 114 | ), | |
| 115 | createMenu( | |
| 116 | get( "Main.menu.view" ), | |
| 117 | addAction( "view.refresh", e -> actions.view‿refresh() ), | |
| 118 | SEPARATOR_ACTION, | |
| 119 | addAction( "view.preview", e -> actions.view‿preview() ) | |
| 120 | ), | |
| 121 | createMenu( | |
| 122 | get( "Main.menu.help" ), | |
| 123 | addAction( "help.about", e -> actions.help‿about() ) | |
| 124 | ) ); | |
| 125 | //@formatter:on | |
| 126 | ||
| 127 | //@formatter:off | |
| 128 | final var toolBar = createToolBar( | |
| 129 | getAction( "file.new" ), | |
| 130 | getAction( "file.open" ), | |
| 131 | getAction( "file.save" ), | |
| 132 | SEPARATOR_ACTION, | |
| 133 | getAction( "edit.undo" ), | |
| 134 | getAction( "edit.redo" ), | |
| 135 | getAction( "edit.cut" ), | |
| 136 | getAction( "edit.copy" ), | |
| 137 | getAction( "edit.paste" ), | |
| 138 | SEPARATOR_ACTION, | |
| 139 | getAction( "format.bold" ), | |
| 140 | getAction( "format.italic" ), | |
| 141 | getAction( "format.superscript" ), | |
| 142 | getAction( "format.subscript" ), | |
| 143 | getAction( "insert.blockquote" ), | |
| 144 | getAction( "insert.code" ), | |
| 145 | getAction( "insert.fenced_code_block" ), | |
| 146 | SEPARATOR_ACTION, | |
| 147 | getAction( "insert.link" ), | |
| 148 | getAction( "insert.image" ), | |
| 149 | SEPARATOR_ACTION, | |
| 150 | getAction( "insert.heading_1" ), | |
| 151 | SEPARATOR_ACTION, | |
| 152 | getAction( "insert.unordered_list" ), | |
| 153 | getAction( "insert.ordered_list" ) | |
| 154 | ); | |
| 155 | //@formatter:on | |
| 156 | ||
| 157 | return new VBox( menuBar, toolBar ); | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Adds a new action to the list of actions. | |
| 162 | * | |
| 163 | * @param key The name of the action to register in {@link #mMap}. | |
| 164 | * @param handler Performs the action upon request. | |
| 165 | * @return The newly registered action. | |
| 166 | */ | |
| 167 | private Action addAction( | |
| 168 | final String key, final EventHandler<ActionEvent> handler ) { | |
| 169 | assert key != null; | |
| 170 | assert handler != null; | |
| 171 | ||
| 172 | final var action = Action | |
| 173 | .builder() | |
| 174 | .setId( key ) | |
| 175 | .setHandler( handler ) | |
| 176 | .build(); | |
| 177 | ||
| 178 | mMap.put( key, action ); | |
| 179 | ||
| 180 | return action; | |
| 181 | } | |
| 182 | ||
| 183 | private Action getAction( final String key ) { | |
| 184 | return mMap.get( key ); | |
| 185 | } | |
| 186 | ||
| 187 | public static Menu createMenu( | |
| 188 | final String text, final MenuAction... actions ) { | |
| 189 | return new Menu( text, null, createMenuItems( actions ) ); | |
| 190 | } | |
| 191 | ||
| 192 | public static MenuItem[] createMenuItems( final MenuAction... actions ) { | |
| 193 | final var menuItems = new MenuItem[ actions.length ]; | |
| 194 | ||
| 195 | for( var i = 0; i < actions.length; i++ ) { | |
| 196 | menuItems[ i ] = actions[ i ].createMenuItem(); | |
| 197 | } | |
| 198 | ||
| 199 | return menuItems; | |
| 200 | } | |
| 201 | ||
| 202 | private static ToolBar createToolBar( final MenuAction... actions ) { | |
| 203 | return new ToolBar( createToolBarButtons( actions ) ); | |
| 204 | } | |
| 205 | ||
| 206 | private static Node[] createToolBarButtons( final MenuAction... actions ) { | |
| 207 | final var len = actions.length; | |
| 208 | final var nodes = new Node[ len ]; | |
| 209 | ||
| 210 | for( var i = 0; i < len; i++ ) { | |
| 211 | nodes[ i ] = actions[ i ].createToolBarNode(); | |
| 212 | } | |
| 213 | ||
| 214 | return nodes; | |
| 215 | } | |
| 216 | } | |
| 1 | 217 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import com.keenwrite.Messages; | |
| 5 | import com.keenwrite.io.FileType; | |
| 6 | import com.keenwrite.service.Settings; | |
| 7 | import javafx.beans.property.Property; | |
| 8 | import javafx.stage.FileChooser; | |
| 9 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 10 | import javafx.stage.Window; | |
| 11 | ||
| 12 | import java.io.File; | |
| 13 | import java.util.ArrayList; | |
| 14 | import java.util.List; | |
| 15 | import java.util.Optional; | |
| 16 | ||
| 17 | import static com.keenwrite.Constants.*; | |
| 18 | import static com.keenwrite.Messages.get; | |
| 19 | import static com.keenwrite.io.FileType.*; | |
| 20 | import static java.lang.String.format; | |
| 21 | ||
| 22 | /** | |
| 23 | * Responsible for opening a dialog that provides users with the ability to | |
| 24 | * select files. | |
| 25 | */ | |
| 26 | public class FileChooserCommand { | |
| 27 | private static final String FILTER_EXTENSION_TITLES = | |
| 28 | "Dialog.file.choose.filter"; | |
| 29 | ||
| 30 | /** | |
| 31 | * Dialog owner. | |
| 32 | */ | |
| 33 | private final Window mParent; | |
| 34 | ||
| 35 | /** | |
| 36 | * Set to the directory of most recently selected file. | |
| 37 | */ | |
| 38 | private final Property<File> mDirectory; | |
| 39 | ||
| 40 | /** | |
| 41 | * Constructs a new {@link FileChooserCommand} that will attach to a given | |
| 42 | * parent window and update the given property upon a successful selection. | |
| 43 | * | |
| 44 | * @param parent The parent window that will own the dialog. | |
| 45 | * @param directory The most recently opened file's directory property. | |
| 46 | */ | |
| 47 | public FileChooserCommand( | |
| 48 | final Window parent, final Property<File> directory ) { | |
| 49 | mParent = parent; | |
| 50 | mDirectory = directory; | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Returns a list of files to be opened. | |
| 55 | * | |
| 56 | * @return A non-null, possibly empty list of files to open. | |
| 57 | */ | |
| 58 | public List<File> openFiles() { | |
| 59 | final var dialog = createFileChooser( | |
| 60 | "Dialog.file.choose.open.title" ); | |
| 61 | final var list = dialog.showOpenMultipleDialog( mParent ); | |
| 62 | final List<java.io.File> selected = list == null ? List.of() : list; | |
| 63 | final var files = new ArrayList<File>( selected.size() ); | |
| 64 | ||
| 65 | files.addAll( selected ); | |
| 66 | ||
| 67 | if( !files.isEmpty() ) { | |
| 68 | setRecentDirectory( files.get( 0 ) ); | |
| 69 | } | |
| 70 | ||
| 71 | return files; | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Allows saving the document under a new file name. | |
| 76 | * | |
| 77 | * @return The new file name. | |
| 78 | */ | |
| 79 | public Optional<File> saveAs() { | |
| 80 | final var dialog = createFileChooser( "Dialog.file.choose.save.title" ); | |
| 81 | return saveOrExportAs( dialog ); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Allows exporting the document to a new file format. | |
| 86 | * | |
| 87 | * @return The file name for exporting into. | |
| 88 | */ | |
| 89 | public Optional<File> exportAs( final File filename ) { | |
| 90 | final var dialog = createFileChooser( "Dialog.file.choose.export.title" ); | |
| 91 | dialog.setInitialFileName( filename.getName() ); | |
| 92 | return saveOrExportAs( dialog ); | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Helper method called when saving or exporting. | |
| 97 | * | |
| 98 | * @param dialog The {@link FileChooser} to display. | |
| 99 | * @return The file selected by the user. | |
| 100 | */ | |
| 101 | private Optional<File> saveOrExportAs( final FileChooser dialog ) { | |
| 102 | final var file = dialog.showSaveDialog( mParent ); | |
| 103 | ||
| 104 | setRecentDirectory( file ); | |
| 105 | ||
| 106 | return Optional.ofNullable( file ); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Opens a new {@link FileChooser} at the previously selected directory. | |
| 111 | * | |
| 112 | * @param key Message key from resource bundle. | |
| 113 | * @return {@link FileChooser} GUI allowing the user to pick a file. | |
| 114 | */ | |
| 115 | private FileChooser createFileChooser( final String key ) { | |
| 116 | final var chooser = new FileChooser(); | |
| 117 | ||
| 118 | chooser.setTitle( get( key ) ); | |
| 119 | chooser.getExtensionFilters().addAll( createExtensionFilters() ); | |
| 120 | chooser.setInitialDirectory( mDirectory.getValue() ); | |
| 121 | ||
| 122 | return chooser; | |
| 123 | } | |
| 124 | ||
| 125 | private List<ExtensionFilter> createExtensionFilters() { | |
| 126 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 127 | ||
| 128 | // TODO: Return a list of all properties that match the filter prefix. | |
| 129 | // This will allow dynamic filters to be added and removed just by | |
| 130 | // updating the properties file. | |
| 131 | list.add( createExtensionFilter( ALL ) ); | |
| 132 | list.add( createExtensionFilter( SOURCE ) ); | |
| 133 | list.add( createExtensionFilter( DEFINITION ) ); | |
| 134 | list.add( createExtensionFilter( XML ) ); | |
| 135 | ||
| 136 | return list; | |
| 137 | } | |
| 138 | ||
| 139 | /** | |
| 140 | * Returns a filter for file name extensions recognized by the application | |
| 141 | * that can be opened by the user. | |
| 142 | * | |
| 143 | * @param filetype Used to find the globbing pattern for extensions. | |
| 144 | * @return A file name filter suitable for use by a FileDialog instance. | |
| 145 | */ | |
| 146 | private ExtensionFilter createExtensionFilter( | |
| 147 | final FileType filetype ) { | |
| 148 | final var tKey = format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype ); | |
| 149 | final var eKey = format( "%s.%s", GLOB_PREFIX_FILE, filetype ); | |
| 150 | ||
| 151 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 152 | } | |
| 153 | ||
| 154 | /** | |
| 155 | * Sets the value for the most recent directly selected. This will get the | |
| 156 | * parent location from the given file. If the parent is a readable directory | |
| 157 | * then this will update the most recent directory property. | |
| 158 | * | |
| 159 | * @param file A file contained in a directory. | |
| 160 | */ | |
| 161 | private void setRecentDirectory( final File file ) { | |
| 162 | if( file != null ) { | |
| 163 | final var parent = file.getParentFile(); | |
| 164 | final var dir = parent == null ? USER_DIRECTORY : parent; | |
| 165 | ||
| 166 | if( dir.isDirectory() && dir.canRead() ) { | |
| 167 | mDirectory.setValue( dir ); | |
| 168 | } | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | private List<String> getExtensions( final String key ) { | |
| 173 | return getSettings().getStringSettingList( key ); | |
| 174 | } | |
| 175 | ||
| 176 | private static Settings getSettings() { | |
| 177 | return sSettings; | |
| 178 | } | |
| 179 | } | |
| 1 | 180 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import com.keenwrite.MainPane; | |
| 5 | import com.keenwrite.editors.markdown.HyperlinkModel; | |
| 6 | import com.keenwrite.editors.markdown.LinkVisitor; | |
| 7 | import com.keenwrite.preferences.Workspace; | |
| 8 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 9 | import com.keenwrite.ui.dialogs.ImageDialog; | |
| 10 | import com.keenwrite.ui.dialogs.LinkDialog; | |
| 11 | import com.vladsch.flexmark.ast.Link; | |
| 12 | import javafx.scene.control.Dialog; | |
| 13 | import javafx.stage.Window; | |
| 14 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 15 | ||
| 16 | import java.nio.file.Path; | |
| 17 | ||
| 18 | /** | |
| 19 | * TODO: Integrate the methods into {@link ApplicationActions} | |
| 20 | * | |
| 21 | * @deprecated Migrate into {@link ApplicationActions}. | |
| 22 | */ | |
| 23 | @Deprecated | |
| 24 | public class MarkdownCommands { | |
| 25 | ||
| 26 | private final MainPane mParent; | |
| 27 | private final Path mBase; | |
| 28 | ||
| 29 | public MarkdownCommands( final MainPane parent, final Path path ) { | |
| 30 | mParent = parent; | |
| 31 | mBase = path.getParent(); | |
| 32 | } | |
| 33 | ||
| 34 | public void insertLink( final StyleClassedTextArea textArea ) { | |
| 35 | insertObject( createLinkDialog( textArea ), textArea ); | |
| 36 | } | |
| 37 | ||
| 38 | public void insertImage( final StyleClassedTextArea textArea ) { | |
| 39 | insertObject( createImageDialog(), textArea ); | |
| 40 | } | |
| 41 | ||
| 42 | /** | |
| 43 | * Returns one of: selected text, word under cursor, or parsed hyperlink from | |
| 44 | * the markdown AST. | |
| 45 | * | |
| 46 | * @return An instance containing the link URL and display text. | |
| 47 | */ | |
| 48 | private HyperlinkModel getHyperlink( final StyleClassedTextArea textArea ) { | |
| 49 | final var selectedText = textArea.getSelectedText(); | |
| 50 | ||
| 51 | // Get the current paragraph, convert to Markdown nodes. | |
| 52 | final var mp = MarkdownProcessor.create( getWorkspace() ); | |
| 53 | final var p = textArea.getCurrentParagraph(); | |
| 54 | final var paragraph = textArea.getText( p ); | |
| 55 | final var node = mp.toNode( paragraph ); | |
| 56 | final var visitor = new LinkVisitor( textArea.getCaretColumn() ); | |
| 57 | final var link = visitor.process( node ); | |
| 58 | ||
| 59 | if( link != null ) { | |
| 60 | textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() ); | |
| 61 | } | |
| 62 | ||
| 63 | return createHyperlinkModel( | |
| 64 | link, selectedText, "https://localhost" | |
| 65 | ); | |
| 66 | } | |
| 67 | ||
| 68 | @SuppressWarnings("SameParameterValue") | |
| 69 | private HyperlinkModel createHyperlinkModel( | |
| 70 | final Link link, final String selection, final String url ) { | |
| 71 | ||
| 72 | return link == null | |
| 73 | ? new HyperlinkModel( selection, url ) | |
| 74 | : new HyperlinkModel( link ); | |
| 75 | } | |
| 76 | ||
| 77 | private Dialog<String> createLinkDialog( | |
| 78 | final StyleClassedTextArea textArea ) { | |
| 79 | return new LinkDialog( getWindow(), getHyperlink( textArea ) ); | |
| 80 | } | |
| 81 | ||
| 82 | private Dialog<String> createImageDialog() { | |
| 83 | return new ImageDialog( getWindow(), getParentPath() ); | |
| 84 | } | |
| 85 | ||
| 86 | private void insertObject( | |
| 87 | final Dialog<String> dialog, final StyleClassedTextArea textArea ) { | |
| 88 | dialog.showAndWait().ifPresent( textArea::replaceSelection ); | |
| 89 | } | |
| 90 | ||
| 91 | private Path getParentPath() { | |
| 92 | return mBase; | |
| 93 | } | |
| 94 | ||
| 95 | private Workspace getWorkspace() { | |
| 96 | return mParent.getWorkspace(); | |
| 97 | } | |
| 98 | ||
| 99 | private Window getWindow() { | |
| 100 | return mParent.getWindow(); | |
| 101 | } | |
| 102 | } | |
| 1 | 103 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import javafx.scene.Node; | |
| 5 | import javafx.scene.control.Button; | |
| 6 | import javafx.scene.control.MenuItem; | |
| 7 | import javafx.scene.control.Separator; | |
| 8 | ||
| 9 | /** | |
| 10 | * Implementations are responsible for creating menu items and toolbar buttons. | |
| 11 | */ | |
| 12 | public interface MenuAction { | |
| 13 | /** | |
| 14 | * Creates a menu item based on the {@link Action} parameters. | |
| 15 | * | |
| 16 | * @return A new {@link MenuItem} instance. | |
| 17 | */ | |
| 18 | MenuItem createMenuItem(); | |
| 19 | ||
| 20 | /** | |
| 21 | * Creates an instance of {@link Button} or {@link Separator} based on the | |
| 22 | * {@link Action} parameters. | |
| 23 | * | |
| 24 | * @return A new {@link Button} or {@link Separator} instance. | |
| 25 | */ | |
| 26 | Node createToolBarNode(); | |
| 27 | } | |
| 1 | 28 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.actions; | |
| 3 | ||
| 4 | import javafx.scene.Node; | |
| 5 | import javafx.scene.control.*; | |
| 6 | ||
| 7 | /** | |
| 8 | * Represents a {@link MenuBar} or {@link ToolBar} action that has no | |
| 9 | * operation, acting as a placeholder for line separators. | |
| 10 | */ | |
| 11 | public class SeparatorAction implements MenuAction { | |
| 12 | @Override | |
| 13 | public MenuItem createMenuItem() { | |
| 14 | return new SeparatorMenuItem(); | |
| 15 | } | |
| 16 | ||
| 17 | @Override | |
| 18 | public Node createToolBarNode() { | |
| 19 | return new Separator(); | |
| 20 | } | |
| 21 | } | |
| 1 | 22 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. | |
| 2 | * | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /** | |
| 29 | * This package contains classes that define commands as executable actions. | |
| 30 | */ | |
| 31 | package com.keenwrite.ui.actions; | |
| 1 | 32 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.adapters; | |
| 3 | ||
| 4 | import org.xhtmlrenderer.event.DocumentListener; | |
| 5 | ||
| 6 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 7 | ||
| 8 | /** | |
| 9 | * Allows subclasses to implement only specific events of interest. | |
| 10 | */ | |
| 11 | public class DocumentAdapter implements DocumentListener { | |
| 12 | @Override | |
| 13 | public void documentStarted() { | |
| 14 | } | |
| 15 | ||
| 16 | @Override | |
| 17 | public void documentLoaded() { | |
| 18 | } | |
| 19 | ||
| 20 | @Override | |
| 21 | public void onLayoutException( final Throwable t ) { | |
| 22 | clue( t ); | |
| 23 | } | |
| 24 | ||
| 25 | @Override | |
| 26 | public void onRenderException( final Throwable t ) { | |
| 27 | clue( t ); | |
| 28 | } | |
| 29 | } | |
| 1 | 30 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.adapters; | |
| 3 | ||
| 4 | import org.w3c.dom.Element; | |
| 5 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 6 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 7 | ||
| 8 | /** | |
| 9 | * Allows subclasses to implement only specific events of interest. | |
| 10 | */ | |
| 11 | public abstract class ReplacedElementAdapter implements ReplacedElementFactory { | |
| 12 | @Override | |
| 13 | public void reset() { | |
| 14 | } | |
| 15 | ||
| 16 | @Override | |
| 17 | public void remove( final Element e ) { | |
| 18 | } | |
| 19 | ||
| 20 | @Override | |
| 21 | public void setFormSubmissionListener( | |
| 22 | final FormSubmissionListener listener ) { | |
| 23 | } | |
| 24 | } | |
| 1 | 25 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.ui.controls; | |
| 29 | ||
| 30 | import com.keenwrite.Messages; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 32 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 33 | import javafx.beans.property.ObjectProperty; | |
| 34 | import javafx.beans.property.SimpleObjectProperty; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.scene.control.Button; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | import javafx.scene.input.KeyCode; | |
| 39 | import javafx.scene.input.KeyEvent; | |
| 40 | import javafx.stage.FileChooser; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.ArrayList; | |
| 46 | import java.util.List; | |
| 47 | ||
| 48 | /** | |
| 49 | * Button that opens a file chooser to select a local file for a URL. | |
| 50 | */ | |
| 51 | public class BrowseFileButton extends Button { | |
| 52 | ||
| 53 | private final List<ExtensionFilter> mExtensionFilters = new ArrayList<>(); | |
| 54 | private final ObjectProperty<Path> mBasePath = new SimpleObjectProperty<>(); | |
| 55 | private final ObjectProperty<String> mUrl = new SimpleObjectProperty<>(); | |
| 56 | ||
| 57 | public BrowseFileButton() { | |
| 58 | setGraphic( | |
| 59 | FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT ) | |
| 60 | ); | |
| 61 | setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) ); | |
| 62 | setOnAction( this::browse ); | |
| 63 | ||
| 64 | disableProperty().bind( mBasePath.isNull() ); | |
| 65 | ||
| 66 | // workaround for a JavaFX bug: | |
| 67 | // avoid closing the dialog that contains this control when the user | |
| 68 | // closes the FileChooser or DirectoryChooser using the ESC key | |
| 69 | addEventHandler( KeyEvent.KEY_RELEASED, e -> { | |
| 70 | if( e.getCode() == KeyCode.ESCAPE ) { | |
| 71 | e.consume(); | |
| 72 | } | |
| 73 | } ); | |
| 74 | } | |
| 75 | ||
| 76 | public void addExtensionFilter( ExtensionFilter extensionFilter ) { | |
| 77 | mExtensionFilters.add( extensionFilter ); | |
| 78 | } | |
| 79 | ||
| 80 | public ObjectProperty<String> urlProperty() { | |
| 81 | return mUrl; | |
| 82 | } | |
| 83 | ||
| 84 | private void browse( ActionEvent e ) { | |
| 85 | var fileChooser = new FileChooser(); | |
| 86 | fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) ); | |
| 87 | fileChooser.getExtensionFilters().addAll( mExtensionFilters ); | |
| 88 | fileChooser.getExtensionFilters() | |
| 89 | .add( new ExtensionFilter( Messages.get( | |
| 90 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 91 | fileChooser.setInitialDirectory( getInitialDirectory() ); | |
| 92 | var result = fileChooser.showOpenDialog( getScene().getWindow() ); | |
| 93 | if( result != null ) { | |
| 94 | updateUrl( result ); | |
| 95 | } | |
| 96 | } | |
| 97 | ||
| 98 | private File getInitialDirectory() { | |
| 99 | //TODO build initial directory based on current value of 'url' property | |
| 100 | return getBasePath().toFile(); | |
| 101 | } | |
| 102 | ||
| 103 | private void updateUrl( File file ) { | |
| 104 | String newUrl; | |
| 105 | try { | |
| 106 | newUrl = getBasePath().relativize( file.toPath() ).toString(); | |
| 107 | } catch( IllegalArgumentException ex ) { | |
| 108 | newUrl = file.toString(); | |
| 109 | } | |
| 110 | mUrl.set( newUrl.replace( '\\', '/' ) ); | |
| 111 | } | |
| 112 | ||
| 113 | public void setBasePath( Path basePath ) { | |
| 114 | this.mBasePath.set( basePath ); | |
| 115 | } | |
| 116 | ||
| 117 | private Path getBasePath() { | |
| 118 | return mBasePath.get(); | |
| 119 | } | |
| 120 | } | |
| 1 | 121 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.ui.controls; | |
| 29 | ||
| 30 | import javafx.beans.property.SimpleStringProperty; | |
| 31 | import javafx.beans.property.StringProperty; | |
| 32 | import javafx.scene.control.TextField; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for escaping/unescaping characters for markdown. | |
| 37 | */ | |
| 38 | public class EscapeTextField extends TextField { | |
| 39 | ||
| 40 | public EscapeTextField() { | |
| 41 | escapedText.bindBidirectional( | |
| 42 | textProperty(), | |
| 43 | new StringConverter<>() { | |
| 44 | @Override | |
| 45 | public String toString( String object ) { | |
| 46 | return escape( object ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public String fromString( String string ) { | |
| 51 | return unescape( string ); | |
| 52 | } | |
| 53 | } | |
| 54 | ); | |
| 55 | escapeCharacters.addListener( | |
| 56 | e -> escapedText.set( escape( textProperty().get() ) ) | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | // 'escapedText' property | |
| 61 | private final StringProperty escapedText = new SimpleStringProperty(); | |
| 62 | ||
| 63 | public StringProperty escapedTextProperty() { | |
| 64 | return escapedText; | |
| 65 | } | |
| 66 | ||
| 67 | // 'escapeCharacters' property | |
| 68 | private final StringProperty escapeCharacters = new SimpleStringProperty(); | |
| 69 | ||
| 70 | public String getEscapeCharacters() { | |
| 71 | return escapeCharacters.get(); | |
| 72 | } | |
| 73 | ||
| 74 | public void setEscapeCharacters( String escapeCharacters ) { | |
| 75 | this.escapeCharacters.set( escapeCharacters ); | |
| 76 | } | |
| 77 | ||
| 78 | private String escape( final String s ) { | |
| 79 | final String escapeChars = getEscapeCharacters(); | |
| 80 | ||
| 81 | return isEmpty( escapeChars ) ? s : | |
| 82 | s.replaceAll( "([" + escapeChars.replaceAll( | |
| 83 | "(.)", | |
| 84 | "\\\\$1" ) + "])", "\\\\$1" ); | |
| 85 | } | |
| 86 | ||
| 87 | private String unescape( final String s ) { | |
| 88 | final String escapeChars = getEscapeCharacters(); | |
| 89 | ||
| 90 | return isEmpty( escapeChars ) ? s : | |
| 91 | s.replaceAll( "\\\\([" + escapeChars | |
| 92 | .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" ); | |
| 93 | } | |
| 94 | ||
| 95 | private static boolean isEmpty( final String s ) { | |
| 96 | return s == null || s.isEmpty(); | |
| 97 | } | |
| 98 | } | |
| 1 | 99 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.ui.controls; | |
| 3 | ||
| 4 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 5 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 6 | import javafx.beans.property.IntegerProperty; | |
| 7 | import javafx.beans.property.SimpleIntegerProperty; | |
| 8 | import javafx.beans.value.ChangeListener; | |
| 9 | import javafx.event.ActionEvent; | |
| 10 | import javafx.event.EventHandler; | |
| 11 | import javafx.geometry.Orientation; | |
| 12 | import javafx.geometry.Pos; | |
| 13 | import javafx.scene.Node; | |
| 14 | import javafx.scene.control.Button; | |
| 15 | import javafx.scene.control.Separator; | |
| 16 | import javafx.scene.control.TextField; | |
| 17 | import javafx.scene.control.Tooltip; | |
| 18 | import javafx.scene.layout.HBox; | |
| 19 | import javafx.scene.layout.Priority; | |
| 20 | import javafx.scene.layout.Region; | |
| 21 | import javafx.scene.layout.VBox; | |
| 22 | import javafx.scene.text.Text; | |
| 23 | import org.controlsfx.control.textfield.CustomTextField; | |
| 24 | ||
| 25 | import static com.keenwrite.Messages.get; | |
| 26 | import static java.lang.StrictMath.max; | |
| 27 | import static java.lang.String.format; | |
| 28 | ||
| 29 | /** | |
| 30 | * Responsible for presenting user interface options for searching through | |
| 31 | * the document. | |
| 32 | */ | |
| 33 | public final class SearchBar extends HBox { | |
| 34 | ||
| 35 | private static final String MESSAGE_KEY = "Main.search.%s.%s"; | |
| 36 | ||
| 37 | private final Button mButtonStop = createButtonStop(); | |
| 38 | private final Button mButtonNext = createButton( "next" ); | |
| 39 | private final Button mButtonPrev = createButton( "prev" ); | |
| 40 | private final TextField mFind = createTextField(); | |
| 41 | private final Text mMatches = new Text(); | |
| 42 | private final IntegerProperty mMatchIndex = new SimpleIntegerProperty(); | |
| 43 | private final IntegerProperty mMatchCount = new SimpleIntegerProperty(); | |
| 44 | ||
| 45 | public SearchBar() { | |
| 46 | setAlignment( Pos.CENTER ); | |
| 47 | addAll( | |
| 48 | mButtonStop, | |
| 49 | createSpacer( 10 ), | |
| 50 | mFind, | |
| 51 | createSpacer( 10 ), | |
| 52 | mButtonNext, | |
| 53 | createSpacer( 10 ), | |
| 54 | mButtonPrev, | |
| 55 | createSpacer( 10 ), | |
| 56 | mMatches, | |
| 57 | createSpacer( 10 ), | |
| 58 | createSeparatorVertical(), | |
| 59 | createSpacer( 5 ) | |
| 60 | ); | |
| 61 | ||
| 62 | mMatchIndex.addListener( ( c, o, n ) -> updateMatchText() ); | |
| 63 | mMatchCount.addListener( ( c, o, n ) -> updateMatchText() ); | |
| 64 | updateMatchText(); | |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * Gives focus to the text field. | |
| 69 | */ | |
| 70 | @Override | |
| 71 | public void requestFocus() { | |
| 72 | mFind.requestFocus(); | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Adds a listener that triggers when the input text field changes. | |
| 77 | * | |
| 78 | * @param listener The listener to notify of change events. | |
| 79 | */ | |
| 80 | public void addInputListener( final ChangeListener<String> listener ) { | |
| 81 | mFind.textProperty().addListener( listener ); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Sets the {@link EventHandler} to call when the user interface triggers | |
| 86 | * finding the next matching search string. This will wrap from the end | |
| 87 | * to the beginning. | |
| 88 | * | |
| 89 | * @param handler The handler requested to perform the find next action. | |
| 90 | */ | |
| 91 | public void setOnNextAction( final EventHandler<ActionEvent> handler ) { | |
| 92 | mButtonNext.setOnAction( handler ); | |
| 93 | mFind.setOnAction( handler ); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Sets the {@link EventHandler} to call when the user interface triggers | |
| 98 | * finding the previous matching search string. This will wrap from the | |
| 99 | * beginning to the end. | |
| 100 | * | |
| 101 | * @param handler The handler requested to perform the find next action. | |
| 102 | */ | |
| 103 | public void setOnPrevAction( final EventHandler<ActionEvent> handler ) { | |
| 104 | mButtonPrev.setOnAction( handler ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Sets the {@link EventHandler} to call when searching has been terminated. | |
| 109 | * | |
| 110 | * @param handler The {@link EventHandler} that will perform an action | |
| 111 | * when the searching has stopped (e.g., remove from this | |
| 112 | * widget from status bar). | |
| 113 | */ | |
| 114 | public void setOnCancelAction( final EventHandler<ActionEvent> handler ) { | |
| 115 | mButtonStop.setOnAction( handler ); | |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * When this property value changes, the match text is updated accordingly. | |
| 120 | * If the value is less than zero, the text will show zero. | |
| 121 | * | |
| 122 | * @return The index of the latest search string match. | |
| 123 | */ | |
| 124 | public IntegerProperty matchIndexProperty() { | |
| 125 | return mMatchIndex; | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * When this property value changes, the match text is updated accordingly. | |
| 130 | * If the value is less than zero, the text will show zero. | |
| 131 | * | |
| 132 | * @return The total number of items that match the search string. | |
| 133 | */ | |
| 134 | public IntegerProperty matchCountProperty() { | |
| 135 | return mMatchCount; | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Updates the match count. | |
| 140 | */ | |
| 141 | private void updateMatchText() { | |
| 142 | final var index = max( 0, mMatchIndex.get() ); | |
| 143 | final var count = max( 0, mMatchCount.get() ); | |
| 144 | final var suffix = count == 0 ? "none" : "some"; | |
| 145 | final var key = getMessageValue( "match", suffix ); | |
| 146 | ||
| 147 | mMatches.setText( get( key, index, count ) ); | |
| 148 | } | |
| 149 | ||
| 150 | private Button createButton( final String id ) { | |
| 151 | final var button = new Button(); | |
| 152 | final var tooltipText = getMessageValue( id, "tooltip" ); | |
| 153 | ||
| 154 | button.setMnemonicParsing( false ); | |
| 155 | button.setGraphic( getIcon( id ) ); | |
| 156 | button.setTooltip( new Tooltip( tooltipText ) ); | |
| 157 | ||
| 158 | return button; | |
| 159 | } | |
| 160 | ||
| 161 | private Button createButtonStop() { | |
| 162 | final var button = createButton( "stop" ); | |
| 163 | button.setCancelButton( true ); | |
| 164 | return button; | |
| 165 | } | |
| 166 | ||
| 167 | private TextField createTextField() { | |
| 168 | final var textField = new CustomTextField(); | |
| 169 | textField.setLeft( getIcon( "find" ) ); | |
| 170 | return textField; | |
| 171 | } | |
| 172 | ||
| 173 | /** | |
| 174 | * Creates a vertical bar, used to divide the search results from the | |
| 175 | * application status message. | |
| 176 | * | |
| 177 | * @return A vertical separator. | |
| 178 | */ | |
| 179 | private Node createSeparatorVertical() { | |
| 180 | return new Separator( Orientation.VERTICAL ); | |
| 181 | } | |
| 182 | ||
| 183 | /** | |
| 184 | * Breathing room between the search box and the application status message. | |
| 185 | * This could also be accomplished by using CSS. | |
| 186 | * | |
| 187 | * @param width The spacer's width. | |
| 188 | * @return A new {@link Node} having about 10px of space. | |
| 189 | */ | |
| 190 | private Node createSpacer( final int width ) { | |
| 191 | final var spacer = new Region(); | |
| 192 | spacer.setPrefWidth( width ); | |
| 193 | VBox.setVgrow( spacer, Priority.ALWAYS ); | |
| 194 | return spacer; | |
| 195 | } | |
| 196 | ||
| 197 | private Node getIcon( final String id ) { | |
| 198 | final var name = getMessageValue( id, "icon" ); | |
| 199 | final var glyph = FontAwesomeIcon.valueOf( name.toUpperCase() ); | |
| 200 | return FontAwesomeIconFactory.get().createIcon( glyph ); | |
| 201 | } | |
| 202 | ||
| 203 | private String getMessageValue( final String id, final String suffix ) { | |
| 204 | return get( format( MESSAGE_KEY, id, suffix ) ); | |
| 205 | } | |
| 206 | ||
| 207 | private void addAll( final Node... nodes ) { | |
| 208 | getChildren().addAll( nodes ); | |
| 209 | } | |
| 210 | } | |
| 1 | 211 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.ui.dialogs; | |
| 29 | ||
| 30 | import static com.keenwrite.Messages.get; | |
| 31 | import com.keenwrite.service.events.impl.ButtonOrderPane; | |
| 32 | import static javafx.scene.control.ButtonType.CANCEL; | |
| 33 | import static javafx.scene.control.ButtonType.OK; | |
| 34 | import javafx.scene.control.Dialog; | |
| 35 | import javafx.stage.Window; | |
| 36 | ||
| 37 | /** | |
| 38 | * Superclass that abstracts common behaviours for all dialogs. | |
| 39 | * | |
| 40 | * @param <T> The type of dialog to create (usually String). | |
| 41 | */ | |
| 42 | public abstract class AbstractDialog<T> extends Dialog<T> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Ensures that all dialogs can be closed. | |
| 46 | * | |
| 47 | * @param owner The parent window of this dialog. | |
| 48 | * @param title The messages title to display in the title bar. | |
| 49 | */ | |
| 50 | @SuppressWarnings( "OverridableMethodCallInConstructor" ) | |
| 51 | public AbstractDialog( final Window owner, final String title ) { | |
| 52 | setTitle( get( title ) ); | |
| 53 | setResizable( true ); | |
| 54 | ||
| 55 | initOwner( owner ); | |
| 56 | initCloseAction(); | |
| 57 | initDialogPane(); | |
| 58 | initDialogButtons(); | |
| 59 | initComponents(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Initialize the component layout. | |
| 64 | */ | |
| 65 | protected abstract void initComponents(); | |
| 66 | ||
| 67 | /** | |
| 68 | * Set the dialog to use a button order pane with an OK and a CANCEL button. | |
| 69 | */ | |
| 70 | protected void initDialogPane() { | |
| 71 | setDialogPane( new ButtonOrderPane() ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Set an OK and CANCEL button on the dialog. | |
| 76 | */ | |
| 77 | protected void initDialogButtons() { | |
| 78 | getDialogPane().getButtonTypes().addAll( OK, CANCEL ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Attaches a setOnCloseRequest to the dialog's [X] button so that the user | |
| 83 | * can always close the window, even if there's an error. | |
| 84 | */ | |
| 85 | protected final void initCloseAction() { | |
| 86 | final Window window = getDialogPane().getScene().getWindow(); | |
| 87 | window.setOnCloseRequest( event -> window.hide() ); | |
| 88 | } | |
| 89 | } | |
| 1 | 90 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.ui.dialogs; | |
| 28 | ||
| 29 | import static com.keenwrite.Messages.get; | |
| 30 | import com.keenwrite.ui.controls.BrowseFileButton; | |
| 31 | import com.keenwrite.ui.controls.EscapeTextField; | |
| 32 | import java.nio.file.Path; | |
| 33 | import javafx.application.Platform; | |
| 34 | import javafx.beans.binding.Bindings; | |
| 35 | import javafx.beans.property.SimpleStringProperty; | |
| 36 | import javafx.beans.property.StringProperty; | |
| 37 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 38 | import static javafx.scene.control.ButtonType.OK; | |
| 39 | import javafx.scene.control.DialogPane; | |
| 40 | import javafx.scene.control.Label; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | import javafx.stage.Window; | |
| 43 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown image. | |
| 47 | */ | |
| 48 | public class ImageDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty image = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public ImageDialog( final Window owner, final Path basePath ) { | |
| 53 | super(owner, "Dialog.image.title" ); | |
| 54 | ||
| 55 | final DialogPane dialogPane = getDialogPane(); | |
| 56 | dialogPane.setContent( pane ); | |
| 57 | ||
| 58 | linkBrowseFileButton.setBasePath( basePath ); | |
| 59 | linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) ); | |
| 60 | linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() ); | |
| 61 | ||
| 62 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 63 | urlField.escapedTextProperty().isEmpty() | |
| 64 | .or( textField.escapedTextProperty().isEmpty() ) ); | |
| 65 | ||
| 66 | image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) ); | |
| 69 | previewField.textProperty().bind( image ); | |
| 70 | ||
| 71 | setResultConverter( dialogButton -> { | |
| 72 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 73 | return (data == ButtonData.OK_DONE) ? image.get() : null; | |
| 74 | } ); | |
| 75 | ||
| 76 | Platform.runLater( () -> { | |
| 77 | urlField.requestFocus(); | |
| 78 | ||
| 79 | if( urlField.getText().startsWith( "http://" ) ) { | |
| 80 | urlField.selectRange( "http://".length(), urlField.getLength() ); | |
| 81 | } | |
| 82 | } ); | |
| 83 | } | |
| 84 | ||
| 85 | @Override | |
| 86 | protected void initComponents() { | |
| 87 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 88 | pane = new MigPane(); | |
| 89 | Label urlLabel = new Label(); | |
| 90 | urlField = new EscapeTextField(); | |
| 91 | linkBrowseFileButton = new BrowseFileButton(); | |
| 92 | Label textLabel = new Label(); | |
| 93 | textField = new EscapeTextField(); | |
| 94 | Label titleLabel = new Label(); | |
| 95 | titleField = new EscapeTextField(); | |
| 96 | Label previewLabel = new Label(); | |
| 97 | previewField = new Label(); | |
| 98 | ||
| 99 | //======== pane ======== | |
| 100 | { | |
| 101 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" ); | |
| 102 | pane.setRows( "[][][][]" ); | |
| 103 | ||
| 104 | //---- urlLabel ---- | |
| 105 | urlLabel.setText( get( "Dialog.image.urlLabel.text" ) ); | |
| 106 | pane.add( urlLabel, "cell 0 0" ); | |
| 107 | ||
| 108 | //---- urlField ---- | |
| 109 | urlField.setEscapeCharacters( "()" ); | |
| 110 | urlField.setText( "http://yourlink.com" ); | |
| 111 | urlField.setPromptText( "http://yourlink.com" ); | |
| 112 | pane.add( urlField, "cell 1 0" ); | |
| 113 | pane.add( linkBrowseFileButton, "cell 2 0" ); | |
| 114 | ||
| 115 | //---- textLabel ---- | |
| 116 | textLabel.setText( get( "Dialog.image.textLabel.text" ) ); | |
| 117 | pane.add( textLabel, "cell 0 1" ); | |
| 118 | ||
| 119 | //---- textField ---- | |
| 120 | textField.setEscapeCharacters( "[]" ); | |
| 121 | pane.add( textField, "cell 1 1 2 1" ); | |
| 122 | ||
| 123 | //---- titleLabel ---- | |
| 124 | titleLabel.setText( get( "Dialog.image.titleLabel.text" ) ); | |
| 125 | pane.add( titleLabel, "cell 0 2" ); | |
| 126 | pane.add( titleField, "cell 1 2 2 1" ); | |
| 127 | ||
| 128 | //---- previewLabel ---- | |
| 129 | previewLabel.setText( get( "Dialog.image.previewLabel.text" ) ); | |
| 130 | pane.add( previewLabel, "cell 0 3" ); | |
| 131 | pane.add( previewField, "cell 1 3 2 1" ); | |
| 132 | } | |
| 133 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 134 | } | |
| 135 | ||
| 136 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 137 | private MigPane pane; | |
| 138 | private EscapeTextField urlField; | |
| 139 | private BrowseFileButton linkBrowseFileButton; | |
| 140 | private EscapeTextField textField; | |
| 141 | private EscapeTextField titleField; | |
| 142 | private Label previewField; | |
| 143 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 144 | } | |
| 1 | 145 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "ImageDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "ImageDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 34 | name: "linkBrowseFileButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 39 | name: "textLabel" | |
| 40 | "text": new FormMessage( null, "ImageDialog.textLabel.text" ) | |
| 41 | auxiliary() { | |
| 42 | "JavaCodeGenerator.variableLocal": true | |
| 43 | } | |
| 44 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 45 | "value": "cell 0 1" | |
| 46 | } ) | |
| 47 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 48 | name: "textField" | |
| 49 | "escapeCharacters": "[]" | |
| 50 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 51 | "value": "cell 1 1 2 1" | |
| 52 | } ) | |
| 53 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 54 | name: "titleLabel" | |
| 55 | "text": new FormMessage( null, "ImageDialog.titleLabel.text" ) | |
| 56 | auxiliary() { | |
| 57 | "JavaCodeGenerator.variableLocal": true | |
| 58 | } | |
| 59 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 60 | "value": "cell 0 2" | |
| 61 | } ) | |
| 62 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 63 | name: "titleField" | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 1 2 2 1" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 68 | name: "previewLabel" | |
| 69 | "text": new FormMessage( null, "ImageDialog.previewLabel.text" ) | |
| 70 | auxiliary() { | |
| 71 | "JavaCodeGenerator.variableLocal": true | |
| 72 | } | |
| 73 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 74 | "value": "cell 0 3" | |
| 75 | } ) | |
| 76 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 77 | name: "previewField" | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 1 3 2 1" | |
| 80 | } ) | |
| 81 | }, new FormLayoutConstraints( null ) { | |
| 82 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 83 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 84 | } ) | |
| 85 | } | |
| 86 | } | |
| 1 | 87 |
| 1 | /* | |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.ui.dialogs; | |
| 29 | ||
| 30 | import com.keenwrite.ui.controls.EscapeTextField; | |
| 31 | import com.keenwrite.editors.markdown.HyperlinkModel; | |
| 32 | import javafx.application.Platform; | |
| 33 | import javafx.beans.binding.Bindings; | |
| 34 | import javafx.beans.property.SimpleStringProperty; | |
| 35 | import javafx.beans.property.StringProperty; | |
| 36 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 37 | import javafx.scene.control.DialogPane; | |
| 38 | import javafx.scene.control.Label; | |
| 39 | import javafx.stage.Window; | |
| 40 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 41 | ||
| 42 | import static com.keenwrite.Messages.get; | |
| 43 | import static javafx.scene.control.ButtonType.OK; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown link. | |
| 47 | */ | |
| 48 | public class LinkDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty link = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public LinkDialog( | |
| 53 | final Window owner, final HyperlinkModel hyperlink ) { | |
| 54 | super( owner, "Dialog.link.title" ); | |
| 55 | ||
| 56 | final DialogPane dialogPane = getDialogPane(); | |
| 57 | dialogPane.setContent( pane ); | |
| 58 | ||
| 59 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 60 | urlField.escapedTextProperty().isEmpty() ); | |
| 61 | ||
| 62 | textField.setText( hyperlink.getText() ); | |
| 63 | urlField.setText( hyperlink.getUrl() ); | |
| 64 | titleField.setText( hyperlink.getTitle() ); | |
| 65 | ||
| 66 | link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() ) | |
| 69 | .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) | |
| 70 | .otherwise( urlField.escapedTextProperty() ) ) ); | |
| 71 | ||
| 72 | setResultConverter( dialogButton -> { | |
| 73 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 74 | return (data == ButtonData.OK_DONE) ? link.get() : null; | |
| 75 | } ); | |
| 76 | ||
| 77 | Platform.runLater( () -> { | |
| 78 | urlField.requestFocus(); | |
| 79 | urlField.selectRange( 0, urlField.getLength() ); | |
| 80 | } ); | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | protected void initComponents() { | |
| 85 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 86 | pane = new MigPane(); | |
| 87 | Label urlLabel = new Label(); | |
| 88 | urlField = new EscapeTextField(); | |
| 89 | Label textLabel = new Label(); | |
| 90 | textField = new EscapeTextField(); | |
| 91 | Label titleLabel = new Label(); | |
| 92 | titleField = new EscapeTextField(); | |
| 93 | ||
| 94 | //======== pane ======== | |
| 95 | { | |
| 96 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" ); | |
| 97 | pane.setRows( "[][][][]" ); | |
| 98 | ||
| 99 | //---- urlLabel ---- | |
| 100 | urlLabel.setText( get( "Dialog.link.urlLabel.text" ) ); | |
| 101 | pane.add( urlLabel, "cell 0 0" ); | |
| 102 | ||
| 103 | //---- urlField ---- | |
| 104 | urlField.setEscapeCharacters( "()" ); | |
| 105 | pane.add( urlField, "cell 1 0" ); | |
| 106 | ||
| 107 | //---- textLabel ---- | |
| 108 | textLabel.setText( get( "Dialog.link.textLabel.text" ) ); | |
| 109 | pane.add( textLabel, "cell 0 1" ); | |
| 110 | ||
| 111 | //---- textField ---- | |
| 112 | textField.setEscapeCharacters( "[]" ); | |
| 113 | pane.add( textField, "cell 1 1 3 1" ); | |
| 114 | ||
| 115 | //---- titleLabel ---- | |
| 116 | titleLabel.setText( get( "Dialog.link.titleLabel.text" ) ); | |
| 117 | pane.add( titleLabel, "cell 0 2" ); | |
| 118 | pane.add( titleField, "cell 1 2 3 1" ); | |
| 119 | } | |
| 120 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 121 | } | |
| 122 | ||
| 123 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 124 | private MigPane pane; | |
| 125 | private EscapeTextField urlField; | |
| 126 | private EscapeTextField textField; | |
| 127 | private EscapeTextField titleField; | |
| 128 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 129 | } | |
| 1 | 130 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "LinkDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "LinkDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) { | |
| 34 | name: "linkBrowseDirectoyButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 39 | name: "linkBrowseFileButton" | |
| 40 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 41 | "value": "cell 3 0" | |
| 42 | } ) | |
| 43 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 44 | name: "textLabel" | |
| 45 | "text": new FormMessage( null, "LinkDialog.textLabel.text" ) | |
| 46 | auxiliary() { | |
| 47 | "JavaCodeGenerator.variableLocal": true | |
| 48 | } | |
| 49 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 50 | "value": "cell 0 1" | |
| 51 | } ) | |
| 52 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 53 | name: "textField" | |
| 54 | "escapeCharacters": "[]" | |
| 55 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 56 | "value": "cell 1 1 3 1" | |
| 57 | } ) | |
| 58 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 59 | name: "titleLabel" | |
| 60 | "text": new FormMessage( null, "LinkDialog.titleLabel.text" ) | |
| 61 | auxiliary() { | |
| 62 | "JavaCodeGenerator.variableLocal": true | |
| 63 | } | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 0 2" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 68 | name: "titleField" | |
| 69 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 70 | "value": "cell 1 2 3 1" | |
| 71 | } ) | |
| 72 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 73 | name: "previewLabel" | |
| 74 | "text": new FormMessage( null, "LinkDialog.previewLabel.text" ) | |
| 75 | auxiliary() { | |
| 76 | "JavaCodeGenerator.variableLocal": true | |
| 77 | } | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 0 3" | |
| 80 | } ) | |
| 81 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 82 | name: "previewField" | |
| 83 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 84 | "value": "cell 1 3 3 1" | |
| 85 | } ) | |
| 86 | }, new FormLayoutConstraints( null ) { | |
| 87 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 88 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 89 | } ) | |
| 90 | } | |
| 91 | } | |
| 1 | 92 |
| 1 | package com.keenwrite.ui.listeners; | |
| 2 | ||
| 3 | import com.keenwrite.editors.TextEditor; | |
| 4 | import com.keenwrite.processors.markdown.Caret; | |
| 5 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 6 | import javafx.beans.value.ChangeListener; | |
| 7 | import javafx.beans.value.ObservableValue; | |
| 8 | import javafx.scene.layout.VBox; | |
| 9 | import javafx.scene.text.Text; | |
| 10 | ||
| 11 | import static javafx.geometry.Pos.BASELINE_CENTER; | |
| 12 | ||
| 13 | /** | |
| 14 | * Responsible for updating the UI whenever the caret changes position. | |
| 15 | * Only one instance of {@link CaretListener} is allowed to prevent duplicate | |
| 16 | * adds to the observable property. | |
| 17 | */ | |
| 18 | public class CaretListener extends VBox implements ChangeListener<Integer> { | |
| 19 | ||
| 20 | private final Text mLineNumberText = new Text(); | |
| 21 | private volatile Caret mCaret; | |
| 22 | ||
| 23 | public CaretListener( final ReadOnlyObjectProperty<TextEditor> editor ) { | |
| 24 | assert editor != null; | |
| 25 | ||
| 26 | setAlignment( BASELINE_CENTER ); | |
| 27 | getChildren().add( mLineNumberText ); | |
| 28 | ||
| 29 | editor.addListener( ( c, o, n ) -> { | |
| 30 | if( n != null ) { | |
| 31 | updateListener( n.getCaret() ); | |
| 32 | } | |
| 33 | } ); | |
| 34 | ||
| 35 | updateListener( editor.get().getCaret() ); | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * Called whenever the caret position changes. | |
| 40 | * | |
| 41 | * @param c The caret position property. | |
| 42 | * @param o The old caret position offset. | |
| 43 | * @param n The new caret position offset. | |
| 44 | */ | |
| 45 | @Override | |
| 46 | public void changed( | |
| 47 | final ObservableValue<? extends Integer> c, | |
| 48 | final Integer o, final Integer n ) { | |
| 49 | updateLineNumber(); | |
| 50 | } | |
| 51 | ||
| 52 | private void updateListener( final Caret caret ) { | |
| 53 | assert caret != null; | |
| 54 | ||
| 55 | final var property = caret.textOffsetProperty(); | |
| 56 | ||
| 57 | property.removeListener( this ); | |
| 58 | mCaret = caret; | |
| 59 | property.addListener( this ); | |
| 60 | updateLineNumber(); | |
| 61 | } | |
| 62 | ||
| 63 | private void updateLineNumber() { | |
| 64 | mLineNumberText.setText( mCaret.toString() ); | |
| 65 | } | |
| 66 | } | |
| 1 | 67 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import com.keenwrite.Messages; | |
| 31 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 32 | import javafx.beans.value.ObservableBooleanValue; | |
| 33 | import javafx.event.ActionEvent; | |
| 34 | import javafx.event.EventHandler; | |
| 35 | import javafx.scene.Node; | |
| 36 | import javafx.scene.control.MenuItem; | |
| 37 | ||
| 38 | /** | |
| 39 | * Represents a menu action that can generate {@link MenuItem} instances and | |
| 40 | * and {@link Node} instances for a toolbar. | |
| 41 | */ | |
| 42 | public abstract class Action { | |
| 43 | /** | |
| 44 | * TODO: Reuse the {@link GenericBuilder}. | |
| 45 | * | |
| 46 | * @return The {@link Builder} for an instance of {@link Action}. | |
| 47 | */ | |
| 48 | public static Builder builder() { | |
| 49 | return new Builder(); | |
| 50 | } | |
| 51 | ||
| 52 | public abstract MenuItem createMenuItem(); | |
| 53 | ||
| 54 | public abstract Node createToolBarButton(); | |
| 55 | ||
| 56 | /** | |
| 57 | * Adds subordinate actions to the menu. This is used to establish sub-menu | |
| 58 | * relationships. The default behaviour does not wire up any registration; | |
| 59 | * subclasses are responsible for handling how actions relate to one another. | |
| 60 | * | |
| 61 | * @param action Actions that only exist with respect to this action. | |
| 62 | */ | |
| 63 | public void addSubActions( Action... action ) { | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Provides a fluent interface around constructing actions so that duplication | |
| 68 | * can be avoided. | |
| 69 | */ | |
| 70 | public static class Builder { | |
| 71 | private String mText; | |
| 72 | private String mAccelerator; | |
| 73 | private GlyphIcons mIcon; | |
| 74 | private EventHandler<ActionEvent> mAction; | |
| 75 | private ObservableBooleanValue mDisabled; | |
| 76 | ||
| 77 | /** | |
| 78 | * Sets the action text based on a resource bundle key. | |
| 79 | * | |
| 80 | * @param key The key to look up in the {@link Messages}. | |
| 81 | * @return The corresponding value, or the key name if none found. | |
| 82 | */ | |
| 83 | public Builder setText( final String key ) { | |
| 84 | mText = Messages.get( key, key ); | |
| 85 | return this; | |
| 86 | } | |
| 87 | ||
| 88 | public Builder setAccelerator( final String accelerator ) { | |
| 89 | mAccelerator = accelerator; | |
| 90 | return this; | |
| 91 | } | |
| 92 | ||
| 93 | public Builder setIcon( final GlyphIcons icon ) { | |
| 94 | mIcon = icon; | |
| 95 | return this; | |
| 96 | } | |
| 97 | ||
| 98 | public Builder setAction( final EventHandler<ActionEvent> action ) { | |
| 99 | mAction = action; | |
| 100 | return this; | |
| 101 | } | |
| 102 | ||
| 103 | public Builder setDisabled( final ObservableBooleanValue disabled ) { | |
| 104 | mDisabled = disabled; | |
| 105 | return this; | |
| 106 | } | |
| 107 | ||
| 108 | public Action build() { | |
| 109 | return new MenuAction( mText, mAccelerator, mIcon, mAction, mDisabled ); | |
| 110 | } | |
| 111 | } | |
| 112 | } | |
| 113 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import javafx.scene.Node; | |
| 30 | import javafx.scene.control.Menu; | |
| 31 | import javafx.scene.control.MenuItem; | |
| 32 | import javafx.scene.control.ToolBar; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for creating menu items and toolbar buttons. | |
| 36 | */ | |
| 37 | public class ActionUtils { | |
| 38 | ||
| 39 | public static Menu createMenu( final String text, final Action... actions ) { | |
| 40 | return new Menu( text, null, createMenuItems( actions ) ); | |
| 41 | } | |
| 42 | ||
| 43 | public static MenuItem[] createMenuItems( final Action... actions ) { | |
| 44 | final var menuItems = new MenuItem[ actions.length ]; | |
| 45 | ||
| 46 | for( int i = 0; i < actions.length; i++ ) { | |
| 47 | menuItems[ i ] = actions[ i ].createMenuItem(); | |
| 48 | } | |
| 49 | ||
| 50 | return menuItems; | |
| 51 | } | |
| 52 | ||
| 53 | public static ToolBar createToolBar( final Action... actions ) { | |
| 54 | return new ToolBar( createToolBarButtons( actions ) ); | |
| 55 | } | |
| 56 | ||
| 57 | public static Node[] createToolBarButtons( final Action... actions ) { | |
| 58 | Node[] buttons = new Node[ actions.length ]; | |
| 59 | ||
| 60 | for( int i = 0; i < actions.length; i++ ) { | |
| 61 | buttons[ i ] = actions[ i ].createToolBarButton(); | |
| 62 | } | |
| 63 | ||
| 64 | return buttons; | |
| 65 | } | |
| 66 | } | |
| 67 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.util; |
| 29 | 3 | |
| ... | ||
| 38 | 12 | * @param <V> The type of value mapped to a key. |
| 39 | 13 | */ |
| 40 | public class BoundedCache<K, V> extends LinkedHashMap<K, V> { | |
| 14 | public final class BoundedCache<K, V> extends LinkedHashMap<K, V> { | |
| 41 | 15 | private final int mCacheSize; |
| 42 | 16 | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.util; | |
| 3 | ||
| 4 | import java.util.List; | |
| 5 | import java.util.ListIterator; | |
| 6 | import java.util.NoSuchElementException; | |
| 7 | ||
| 8 | /** | |
| 9 | * Responsible for iterating over a list either forwards or backwards. When | |
| 10 | * the iterator reaches the last element in the list, the next element will | |
| 11 | * be the first. When the iterator reaches the first element in the list, | |
| 12 | * the previous element will be the last. | |
| 13 | * <p> | |
| 14 | * Due to the ability to move forwards and backwards through the list, rather | |
| 15 | * than force client classes to track the list index independently, this | |
| 16 | * iterator provides an accessor to the index. The index is zero-based. | |
| 17 | * </p> | |
| 18 | * | |
| 19 | * @param <T> The type of list to be cycled. | |
| 20 | */ | |
| 21 | public class CyclicIterator<T> implements ListIterator<T> { | |
| 22 | private final List<T> mList; | |
| 23 | ||
| 24 | /** | |
| 25 | * Initialize to an invalid index so that the first calls to either | |
| 26 | * {@link #previous()} or {@link #next()} will return the starting or ending | |
| 27 | * element. | |
| 28 | */ | |
| 29 | private int mIndex = -1; | |
| 30 | ||
| 31 | /** | |
| 32 | * Creates an iterator that cycles indefinitely through the given list. | |
| 33 | * | |
| 34 | * @param list The list to cycle through indefinitely. | |
| 35 | */ | |
| 36 | public CyclicIterator( final List<T> list ) { | |
| 37 | mList = list; | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * @return {@code true} if there is at least one element. | |
| 42 | */ | |
| 43 | @Override | |
| 44 | public boolean hasNext() { | |
| 45 | return !mList.isEmpty(); | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * @return {@code true} if there is at least one element. | |
| 50 | */ | |
| 51 | @Override | |
| 52 | public boolean hasPrevious() { | |
| 53 | return !mList.isEmpty(); | |
| 54 | } | |
| 55 | ||
| 56 | @Override | |
| 57 | public int nextIndex() { | |
| 58 | return computeIndex( +1 ); | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public int previousIndex() { | |
| 63 | return computeIndex( -1 ); | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | public void remove() { | |
| 68 | mList.remove( mIndex ); | |
| 69 | } | |
| 70 | ||
| 71 | @Override | |
| 72 | public void set( final T t ) { | |
| 73 | mList.set( mIndex, t ); | |
| 74 | } | |
| 75 | ||
| 76 | @Override | |
| 77 | public void add( final T t ) { | |
| 78 | mList.add( mIndex, t ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Returns the next item in the list, which will cycle to the first | |
| 83 | * item as necessary. | |
| 84 | * | |
| 85 | * @return The next item in the list, cycling to the start if needed. | |
| 86 | */ | |
| 87 | @Override | |
| 88 | public T next() { | |
| 89 | return cycle( +1 ); | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * Returns the previous item in the list, which will cycle to the last | |
| 94 | * item as necessary. | |
| 95 | * | |
| 96 | * @return The previous item in the list, cycling to the end if needed. | |
| 97 | */ | |
| 98 | @Override | |
| 99 | public T previous() { | |
| 100 | return cycle( -1 ); | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Cycles to the next or previous element, depending on the direction value. | |
| 105 | * | |
| 106 | * @param direction Use -1 for previous, +1 for next. | |
| 107 | * @return The next or previous item in the list. | |
| 108 | */ | |
| 109 | private T cycle( final int direction ) { | |
| 110 | try { | |
| 111 | return mList.get( mIndex = computeIndex( direction ) ); | |
| 112 | } catch( final Exception ex ) { | |
| 113 | throw new NoSuchElementException( ex ); | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Returns the index of the value retrieved from the most recent call to | |
| 119 | * either {@link #previous()} or {@link #next()}. | |
| 120 | * | |
| 121 | * @return The list item index or -1 if no calls have been made to retrieve | |
| 122 | * an item from the list. | |
| 123 | */ | |
| 124 | public int getIndex() { | |
| 125 | return mIndex; | |
| 126 | } | |
| 127 | ||
| 128 | private int computeIndex( final int direction ) { | |
| 129 | final var i = mIndex + direction; | |
| 130 | final var size = mList.size(); | |
| 131 | final var result = i < 0 | |
| 132 | ? size - 1 | |
| 133 | : size == 0 ? 0 : i % size; | |
| 134 | ||
| 135 | // Ensure the invariant holds. | |
| 136 | assert 0 <= result && result < size || size == 0 && result <= 0; | |
| 137 | ||
| 138 | return result; | |
| 139 | } | |
| 140 | } | |
| 1 | 141 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.util; | |
| 3 | ||
| 4 | import com.keenwrite.preview.HtmlPreview; | |
| 5 | ||
| 6 | import java.awt.*; | |
| 7 | import java.awt.font.TextAttribute; | |
| 8 | import java.io.FileInputStream; | |
| 9 | import java.io.IOException; | |
| 10 | import java.io.InputStream; | |
| 11 | import java.net.URI; | |
| 12 | import java.util.Map; | |
| 13 | ||
| 14 | import static com.keenwrite.Constants.FONT_DIRECTORY; | |
| 15 | import static com.keenwrite.StatusBarNotifier.clue; | |
| 16 | import static com.keenwrite.util.ProtocolScheme.valueFrom; | |
| 17 | import static com.keenwrite.util.ResourceWalker.GLOB_FONTS; | |
| 18 | import static com.keenwrite.util.ResourceWalker.walk; | |
| 19 | import static java.awt.Font.TRUETYPE_FONT; | |
| 20 | import static java.awt.Font.createFont; | |
| 21 | import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment; | |
| 22 | import static java.awt.font.TextAttribute.*; | |
| 23 | ||
| 24 | /** | |
| 25 | * Responsible for loading fonts into the application's | |
| 26 | * {@link GraphicsEnvironment} so that the {@link HtmlPreview} can display | |
| 27 | * the text using a non-system font. | |
| 28 | */ | |
| 29 | public final class FontLoader { | |
| 30 | ||
| 31 | /** | |
| 32 | * Walks the resources associated with the application to load all TrueType | |
| 33 | * font resources found. This method must run before the windowing system | |
| 34 | * kicks in, otherwise the fonts will not be found. | |
| 35 | * <p> | |
| 36 | * All fonts must be TrueType fonts. No PostScript Type 1 fonts are | |
| 37 | * supported. | |
| 38 | * </p> | |
| 39 | */ | |
| 40 | @SuppressWarnings("unchecked") | |
| 41 | public static void initFonts() { | |
| 42 | final var ge = getLocalGraphicsEnvironment(); | |
| 43 | ||
| 44 | try { | |
| 45 | walk( | |
| 46 | FONT_DIRECTORY, GLOB_FONTS, path -> { | |
| 47 | final var uri = path.toUri(); | |
| 48 | final var filename = path.toString(); | |
| 49 | ||
| 50 | try( final var is = openFont( uri, filename ) ) { | |
| 51 | final var font = createFont( TRUETYPE_FONT, is ); | |
| 52 | final var attributes = | |
| 53 | (Map<TextAttribute, Integer>) font.getAttributes(); | |
| 54 | ||
| 55 | attributes.put( LIGATURES, LIGATURES_ON ); | |
| 56 | attributes.put( KERNING, KERNING_ON ); | |
| 57 | ge.registerFont( font.deriveFont( attributes ) ); | |
| 58 | } catch( final Exception ex ) { | |
| 59 | clue( ex ); | |
| 60 | } | |
| 61 | } | |
| 62 | ); | |
| 63 | } catch( final Exception ex ) { | |
| 64 | clue( ex ); | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | /** | |
| 69 | * Attempts to open a font, regardless of whether the font is a resource in | |
| 70 | * a JAR file or somewhere on the file system. | |
| 71 | * | |
| 72 | * @param uri Directory or archive containing a font. | |
| 73 | * @param filename Name of the font file. | |
| 74 | * @return An open file handled to the font. | |
| 75 | * @throws IOException Could not open the resource as a stream. | |
| 76 | */ | |
| 77 | private static InputStream openFont( final URI uri, final String filename ) | |
| 78 | throws IOException { | |
| 79 | return valueFrom( uri ).isJar() | |
| 80 | ? FontLoader.class.getResourceAsStream( filename ) | |
| 81 | : new FileInputStream( filename ); | |
| 82 | } | |
| 83 | } | |
| 1 | 84 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | 2 | package com.keenwrite.util; |
| 2 | 3 | |
| ... | ||
| 44 | 45 | protected GenericBuilder( |
| 45 | 46 | final Supplier<MT> mutator, final Function<MT, IT> immutable ) { |
| 47 | assert mutator != null; | |
| 48 | assert immutable != null; | |
| 49 | ||
| 46 | 50 | mMutable = mutator; |
| 47 | 51 | mImmutable = immutable; |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 30 | import javafx.beans.value.ObservableBooleanValue; | |
| 31 | import javafx.event.ActionEvent; | |
| 32 | import javafx.event.EventHandler; | |
| 33 | import javafx.scene.control.Button; | |
| 34 | import javafx.scene.control.Menu; | |
| 35 | import javafx.scene.control.MenuItem; | |
| 36 | import javafx.scene.control.Tooltip; | |
| 37 | import javafx.scene.input.KeyCombination; | |
| 38 | ||
| 39 | import java.util.ArrayList; | |
| 40 | import java.util.List; | |
| 41 | ||
| 42 | import static de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory.get; | |
| 43 | import static javafx.scene.input.KeyCombination.valueOf; | |
| 44 | ||
| 45 | /** | |
| 46 | * Defines actions the user can take by interacting with the GUI. | |
| 47 | */ | |
| 48 | public class MenuAction extends Action { | |
| 49 | private final String mText; | |
| 50 | private final KeyCombination mAccelerator; | |
| 51 | private final GlyphIcons mIcon; | |
| 52 | private final EventHandler<ActionEvent> mAction; | |
| 53 | private final ObservableBooleanValue mDisabled; | |
| 54 | private final List<Action> mSubActions = new ArrayList<>(); | |
| 55 | ||
| 56 | public MenuAction( | |
| 57 | final String text, | |
| 58 | final String accelerator, | |
| 59 | final GlyphIcons icon, | |
| 60 | final EventHandler<ActionEvent> action, | |
| 61 | final ObservableBooleanValue disabled ) { | |
| 62 | ||
| 63 | mText = text; | |
| 64 | mAccelerator = accelerator == null ? null : valueOf( accelerator ); | |
| 65 | mIcon = icon; | |
| 66 | mAction = action; | |
| 67 | mDisabled = disabled; | |
| 68 | } | |
| 69 | ||
| 70 | @Override | |
| 71 | public MenuItem createMenuItem() { | |
| 72 | // This will either become a menu or a menu item, depending on whether | |
| 73 | // sub-actions are defined. | |
| 74 | final MenuItem menuItem; | |
| 75 | ||
| 76 | if( mSubActions.isEmpty() ) { | |
| 77 | // Regular menu item has no sub-menus. | |
| 78 | menuItem = new MenuItem( mText ); | |
| 79 | } | |
| 80 | else { | |
| 81 | // Sub-actions are translated into sub-menu items beneath this action. | |
| 82 | final var submenu = new Menu( mText ); | |
| 83 | ||
| 84 | for( final var action : mSubActions ) { | |
| 85 | // Recursive call that creates a sub-menu hierarchy. | |
| 86 | submenu.getItems().add( action.createMenuItem() ); | |
| 87 | } | |
| 88 | ||
| 89 | menuItem = submenu; | |
| 90 | } | |
| 91 | ||
| 92 | if( mAccelerator != null ) { | |
| 93 | menuItem.setAccelerator( mAccelerator ); | |
| 94 | } | |
| 95 | ||
| 96 | if( mIcon != null ) { | |
| 97 | menuItem.setGraphic( get().createIcon( mIcon ) ); | |
| 98 | } | |
| 99 | ||
| 100 | if( mAction != null ) { | |
| 101 | menuItem.setOnAction( mAction ); | |
| 102 | } | |
| 103 | ||
| 104 | if( mDisabled != null ) { | |
| 105 | menuItem.disableProperty().bind( mDisabled ); | |
| 106 | } | |
| 107 | ||
| 108 | menuItem.setMnemonicParsing( true ); | |
| 109 | ||
| 110 | return menuItem; | |
| 111 | } | |
| 112 | ||
| 113 | @Override | |
| 114 | public Button createToolBarButton() { | |
| 115 | final Button button = new Button(); | |
| 116 | button.setGraphic( | |
| 117 | get().createIcon( mIcon, "1.2em" ) ); | |
| 118 | ||
| 119 | String tooltip = mText; | |
| 120 | ||
| 121 | if( tooltip.endsWith( "..." ) ) { | |
| 122 | tooltip = tooltip.substring( 0, tooltip.length() - 3 ); | |
| 123 | } | |
| 124 | ||
| 125 | if( mAccelerator != null ) { | |
| 126 | tooltip += " (" + mAccelerator.getDisplayText() + ')'; | |
| 127 | } | |
| 128 | ||
| 129 | button.setTooltip( new Tooltip( tooltip ) ); | |
| 130 | button.setFocusTraversable( false ); | |
| 131 | button.setOnAction( mAction ); | |
| 132 | ||
| 133 | if( mDisabled != null ) { | |
| 134 | button.disableProperty().bind( mDisabled ); | |
| 135 | } | |
| 136 | ||
| 137 | return button; | |
| 138 | } | |
| 139 | ||
| 140 | @Override | |
| 141 | public void addSubActions( final Action... action ) { | |
| 142 | mSubActions.addAll( List.of( action ) ); | |
| 143 | } | |
| 144 | } | |
| 145 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import java.util.AbstractMap; | |
| 31 | import java.util.Map; | |
| 32 | ||
| 33 | /** | |
| 34 | * Convenience class for pairing two objects together; this is a synonym for | |
| 35 | * {@link Map.Entry}. | |
| 36 | * | |
| 37 | * @param <K> The type of key to store in this pair. | |
| 38 | * @param <V> The type of value to store in this pair. | |
| 39 | */ | |
| 40 | public class Pair<K, V> extends AbstractMap.SimpleImmutableEntry<K, V> { | |
| 41 | /** | |
| 42 | * Associates a new key-value pair. | |
| 43 | * | |
| 44 | * @param key The key for this key-value pairing. | |
| 45 | * @param value The value for this key-value pairing. | |
| 46 | */ | |
| 47 | public Pair( final K key, final V value ) { | |
| 48 | super( key, value ); | |
| 49 | } | |
| 50 | } | |
| 51 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.net.MalformedURLException; | |
| 32 | import java.net.URI; | |
| 33 | import java.net.URL; | |
| 34 | ||
| 35 | import static com.keenwrite.util.ProtocolScheme.UNKNOWN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for determining the protocol of a resource. | |
| 39 | */ | |
| 40 | public class ProtocolResolver { | |
| 41 | /** | |
| 42 | * Returns the protocol for a given URI or filename. | |
| 43 | * | |
| 44 | * @param resource Determine the protocol for this URI or filename. | |
| 45 | * @return The protocol for the given resource. | |
| 46 | */ | |
| 47 | public static ProtocolScheme getProtocol( final String resource ) { | |
| 48 | String protocol; | |
| 49 | ||
| 50 | try { | |
| 51 | final URI uri = new URI( resource ); | |
| 52 | ||
| 53 | if( uri.isAbsolute() ) { | |
| 54 | protocol = uri.getScheme(); | |
| 55 | } | |
| 56 | else { | |
| 57 | final URL url = new URL( resource ); | |
| 58 | protocol = url.getProtocol(); | |
| 59 | } | |
| 60 | } catch( final Exception e ) { | |
| 61 | // Could be HTTP, HTTPS? | |
| 62 | if( resource.startsWith( "//" ) ) { | |
| 63 | throw new IllegalArgumentException( "Relative context: " + resource ); | |
| 64 | } | |
| 65 | else { | |
| 66 | final File file = new File( resource ); | |
| 67 | protocol = getProtocol( file ); | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | return ProtocolScheme.valueFrom( protocol ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Returns the protocol for a given file. | |
| 76 | * | |
| 77 | * @param file Determine the protocol for this file. | |
| 78 | * @return The protocol for the given file, or {@link ProtocolScheme#UNKNOWN} | |
| 79 | * if the protocol cannot be determined. | |
| 80 | */ | |
| 81 | private static String getProtocol( final File file ) { | |
| 82 | try { | |
| 83 | return file.toURI().toURL().getProtocol(); | |
| 84 | } catch( final MalformedURLException ex ) { | |
| 85 | // Return a protocol guaranteed to be undefined. | |
| 86 | return UNKNOWN.toString(); | |
| 87 | } | |
| 88 | } | |
| 89 | } | |
| 90 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.util; |
| 3 | ||
| 4 | import java.io.File; | |
| 5 | import java.net.URI; | |
| 6 | import java.net.URL; | |
| 29 | 7 | |
| 30 | 8 | /** |
| 31 | 9 | * Represents the type of data encoding scheme used for a universal resource |
| 32 | * indicator. | |
| 10 | * indicator. Prefer to use the {@code is*} methods to check equality because | |
| 11 | * there are cases where the protocol represents more than one possible type | |
| 12 | * (e.g., a Java Archive is a file, so comparing {@link #FILE} directly could | |
| 13 | * lead to incorrect results). | |
| 33 | 14 | */ |
| 34 | 15 | public enum ProtocolScheme { |
| 16 | /** | |
| 17 | * Denotes a local file. | |
| 18 | */ | |
| 19 | FILE, | |
| 35 | 20 | /** |
| 36 | 21 | * Denotes either HTTP or HTTPS. |
| 37 | 22 | */ |
| 38 | 23 | HTTP, |
| 39 | 24 | /** |
| 40 | * Denotes a local file. | |
| 25 | * Denotes Java archive file. | |
| 41 | 26 | */ |
| 42 | FILE, | |
| 27 | JAR, | |
| 43 | 28 | /** |
| 44 | 29 | * Could not determine schema (or is not supported by the application). |
| 45 | 30 | */ |
| 46 | 31 | UNKNOWN; |
| 47 | ||
| 48 | /** | |
| 49 | * Answers {@code true} if the given protocol is either HTTP or HTTPS. | |
| 50 | * | |
| 51 | * @return {@code true} the protocol is either HTTP or HTTPS. | |
| 52 | */ | |
| 53 | public boolean isHttp() { | |
| 54 | return this == HTTP; | |
| 55 | } | |
| 56 | 32 | |
| 57 | 33 | /** |
| 58 | * Answers {@code true} if the given protocol is for a local file. | |
| 34 | * Returns the protocol for a given URI or file name. | |
| 59 | 35 | * |
| 60 | * @return {@code true} the protocol is for a local file reference. | |
| 36 | * @param resource Determine the protocol for this URI or file name. | |
| 37 | * @return The protocol for the given resource. | |
| 61 | 38 | */ |
| 62 | public boolean isFile() { | |
| 63 | return this == FILE; | |
| 39 | public static ProtocolScheme getProtocol( final String resource ) { | |
| 40 | try { | |
| 41 | final var uri = new URI( resource ); | |
| 42 | return uri.isAbsolute() | |
| 43 | ? valueFrom( uri ) | |
| 44 | : valueFrom( new URL( resource ) ); | |
| 45 | } catch( final Exception ex ) { | |
| 46 | // Using double-slashes is a short-hand to instruct the browser to | |
| 47 | // reference a resource using the parent URL's security model. This | |
| 48 | // is known as a protocol-relative URL. | |
| 49 | return resource.startsWith( "//" ) | |
| 50 | ? HTTP | |
| 51 | : valueFrom( new File( resource ) ); | |
| 52 | } | |
| 64 | 53 | } |
| 65 | 54 | |
| 66 | 55 | /** |
| 67 | 56 | * Determines the protocol scheme for a given string. |
| 68 | 57 | * |
| 69 | 58 | * @param protocol A string representing data encoding protocol scheme. |
| 70 | 59 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a |
| 71 | 60 | * valid value from this enumeration. |
| 72 | 61 | */ |
| 73 | public static ProtocolScheme valueFrom( String protocol ) { | |
| 74 | ProtocolScheme result = UNKNOWN; | |
| 75 | protocol = sanitize( protocol ); | |
| 62 | public static ProtocolScheme valueFrom( final String protocol ) { | |
| 63 | final var sanitized = protocol == null ? "" : protocol.toUpperCase(); | |
| 76 | 64 | |
| 77 | 65 | for( final var scheme : values() ) { |
| 78 | 66 | // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate. |
| 79 | if( protocol.startsWith( scheme.name() ) ) { | |
| 80 | result = scheme; | |
| 81 | break; | |
| 67 | if( sanitized.startsWith( scheme.name() ) ) { | |
| 68 | return scheme; | |
| 82 | 69 | } |
| 83 | 70 | } |
| 84 | 71 | |
| 85 | return result; | |
| 72 | return UNKNOWN; | |
| 86 | 73 | } |
| 87 | 74 | |
| 88 | 75 | /** |
| 89 | * Returns an empty string if the given string to sanitize is {@code null}, | |
| 90 | * otherwise the given string in uppercase. Uppercase is used to align with | |
| 91 | * the enum name. | |
| 76 | * Determines the protocol scheme for a given {@link File}. | |
| 92 | 77 | * |
| 93 | * @param s The string to sanitize, may be {@code null}. | |
| 94 | * @return A non-{@code null} string. | |
| 78 | * @param file A file having a URI that contains a protocol scheme. | |
| 79 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a | |
| 80 | * valid value from this enumeration. | |
| 95 | 81 | */ |
| 96 | private static String sanitize( final String s ) { | |
| 97 | return s == null ? "" : s.toUpperCase(); | |
| 82 | public static ProtocolScheme valueFrom( final File file ) { | |
| 83 | return valueFrom( file.toURI() ); | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * Determines the protocol scheme for a given {@link URI}. | |
| 88 | * | |
| 89 | * @param uri A URI that contains a protocol scheme. | |
| 90 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a | |
| 91 | * valid value from this enumeration. | |
| 92 | */ | |
| 93 | public static ProtocolScheme valueFrom( final URI uri ) { | |
| 94 | try { | |
| 95 | return valueFrom( uri.toURL() ); | |
| 96 | } catch( final Exception ex ) { | |
| 97 | return UNKNOWN; | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | /** | |
| 102 | * Determines the protocol scheme for a given {@link URL}. | |
| 103 | * | |
| 104 | * @param url A {@link URL} that contains a protocol scheme. | |
| 105 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a | |
| 106 | * valid value from this enumeration. | |
| 107 | */ | |
| 108 | public static ProtocolScheme valueFrom( final URL url ) { | |
| 109 | return valueFrom( url.getProtocol() ); | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Answers {@code true} if the given protocol is for a local file, which | |
| 114 | * includes a JAR file. | |
| 115 | * | |
| 116 | * @return {@code false} the protocol is not a local file reference. | |
| 117 | */ | |
| 118 | public boolean isFile() { | |
| 119 | return this == FILE || this == JAR; | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Answers {@code true} if the given protocol is either HTTP or HTTPS. | |
| 124 | * | |
| 125 | * @return {@code true} the protocol is either HTTP or HTTPS. | |
| 126 | */ | |
| 127 | public boolean isHttp() { | |
| 128 | return this == HTTP; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Answers {@code true} if the given protocol is for a Java archive file. | |
| 133 | * | |
| 134 | * @return {@code false} the protocol is not a Java archive file. | |
| 135 | */ | |
| 136 | public boolean isJar() { | |
| 137 | return this == JAR; | |
| 98 | 138 | } |
| 99 | 139 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | package com.keenwrite.util; |
| 29 | 3 | |
| 30 | 4 | import java.io.IOException; |
| 5 | import java.net.URI; | |
| 31 | 6 | import java.net.URISyntaxException; |
| 32 | import java.nio.file.*; | |
| 7 | import java.nio.file.Files; | |
| 8 | import java.nio.file.Path; | |
| 9 | import java.nio.file.Paths; | |
| 33 | 10 | import java.util.function.Consumer; |
| 34 | 11 | |
| 12 | import static java.nio.file.FileSystems.getDefault; | |
| 35 | 13 | import static java.nio.file.FileSystems.newFileSystem; |
| 36 | 14 | import static java.util.Collections.emptyMap; |
| 37 | 15 | |
| 38 | 16 | /** |
| 39 | 17 | * Responsible for finding file resources. |
| 40 | 18 | */ |
| 41 | 19 | public class ResourceWalker { |
| 42 | private static final PathMatcher PATH_MATCHER = | |
| 43 | FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" ); | |
| 20 | /** | |
| 21 | * Globbing pattern to match font names. | |
| 22 | */ | |
| 23 | public static final String GLOB_FONTS = "**.{ttf,otf}"; | |
| 44 | 24 | |
| 45 | 25 | /** |
| 46 | * @param dirName The root directory to scan for files matching the glob. | |
| 47 | * @param c The consumer function to call for each matching path found. | |
| 26 | * @param directory The root directory to scan for files matching the glob. | |
| 27 | * @param c The consumer function to call for each matching path | |
| 28 | * found. | |
| 48 | 29 | * @throws URISyntaxException Could not convert the resource to a URI. |
| 49 | 30 | * @throws IOException Could not walk the tree. |
| 50 | 31 | */ |
| 51 | public static void walk( final String dirName, final Consumer<Path> c ) | |
| 32 | public static void walk( | |
| 33 | final String directory, final String glob, final Consumer<Path> c ) | |
| 52 | 34 | throws URISyntaxException, IOException { |
| 53 | final var resource = ResourceWalker.class.getResource( dirName ); | |
| 35 | final var resource = ResourceWalker.class.getResource( directory ); | |
| 36 | final var matcher = getDefault().getPathMatcher( "glob:" + glob ); | |
| 54 | 37 | |
| 55 | 38 | if( resource != null ) { |
| 56 | 39 | final var uri = resource.toURI(); |
| 57 | final var path = uri.getScheme().equals( "jar" ) | |
| 58 | ? newFileSystem( uri, emptyMap() ).getPath( dirName ) | |
| 59 | : Paths.get( uri ); | |
| 60 | final var walk = Files.walk( path, 10 ); | |
| 40 | final var jar = uri.getScheme().equals( "jar" ); | |
| 41 | final var path = jar ? toFileSystem( uri, directory ) : Paths.get( uri ); | |
| 61 | 42 | |
| 62 | for( final var it = walk.iterator(); it.hasNext(); ) { | |
| 63 | final Path p = it.next(); | |
| 64 | if( PATH_MATCHER.matches( p ) ) { | |
| 65 | c.accept( p ); | |
| 43 | try( final var walk = Files.walk( path, 10 ) ) { | |
| 44 | for( final var it = walk.iterator(); it.hasNext(); ) { | |
| 45 | final Path p = it.next(); | |
| 46 | if( matcher.matches( p ) ) { | |
| 47 | c.accept( p ); | |
| 48 | } | |
| 66 | 49 | } |
| 67 | 50 | } |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 54 | private static Path toFileSystem( final URI uri, final String directory ) | |
| 55 | throws IOException { | |
| 56 | try( final var fs = newFileSystem( uri, emptyMap() ) ) { | |
| 57 | return fs.getPath( directory ); | |
| 68 | 58 | } |
| 69 | 59 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.MenuItem; | |
| 32 | import javafx.scene.control.Separator; | |
| 33 | import javafx.scene.control.SeparatorMenuItem; | |
| 34 | ||
| 35 | /** | |
| 36 | * Represents a menu bar action that has no operation, acting as a placeholder | |
| 37 | * for line separators. | |
| 38 | */ | |
| 39 | public class SeparatorAction extends Action { | |
| 40 | @Override | |
| 41 | public MenuItem createMenuItem() { | |
| 42 | return new SeparatorMenuItem(); | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public Node createToolBarButton() { | |
| 47 | return new Separator(); | |
| 48 | } | |
| 49 | } | |
| 50 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import java.util.prefs.Preferences; | |
| 30 | ||
| 31 | import javafx.application.Platform; | |
| 32 | import javafx.scene.shape.Rectangle; | |
| 33 | import javafx.stage.Stage; | |
| 34 | import javafx.stage.WindowEvent; | |
| 35 | ||
| 36 | /** | |
| 37 | * Saves and restores Stage state (window bounds, maximized, fullScreen). | |
| 38 | */ | |
| 39 | public class StageState { | |
| 40 | ||
| 41 | public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition"; | |
| 42 | public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor"; | |
| 43 | public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview"; | |
| 44 | ||
| 45 | private final Stage mStage; | |
| 46 | private final Preferences mState; | |
| 47 | ||
| 48 | private Rectangle normalBounds; | |
| 49 | private boolean runLaterPending; | |
| 50 | ||
| 51 | public StageState( final Stage stage, final Preferences state ) { | |
| 52 | mStage = stage; | |
| 53 | mState = state; | |
| 54 | ||
| 55 | restore(); | |
| 56 | ||
| 57 | stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() ); | |
| 58 | ||
| 59 | stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 60 | stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 61 | stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 62 | stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 63 | } | |
| 64 | ||
| 65 | private void save() { | |
| 66 | final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds; | |
| 67 | ||
| 68 | if( bounds != null ) { | |
| 69 | mState.putDouble( "windowX", bounds.getX() ); | |
| 70 | mState.putDouble( "windowY", bounds.getY() ); | |
| 71 | mState.putDouble( "windowWidth", bounds.getWidth() ); | |
| 72 | mState.putDouble( "windowHeight", bounds.getHeight() ); | |
| 73 | } | |
| 74 | ||
| 75 | mState.putBoolean( "windowMaximized", mStage.isMaximized() ); | |
| 76 | mState.putBoolean( "windowFullScreen", mStage.isFullScreen() ); | |
| 77 | } | |
| 78 | ||
| 79 | private void restore() { | |
| 80 | final double x = mState.getDouble( "windowX", Double.NaN ); | |
| 81 | final double y = mState.getDouble( "windowY", Double.NaN ); | |
| 82 | final double w = mState.getDouble( "windowWidth", Double.NaN ); | |
| 83 | final double h = mState.getDouble( "windowHeight", Double.NaN ); | |
| 84 | final boolean maximized = mState.getBoolean( "windowMaximized", false ); | |
| 85 | final boolean fullScreen = mState.getBoolean( "windowFullScreen", false ); | |
| 86 | ||
| 87 | if( !Double.isNaN( x ) && !Double.isNaN( y ) ) { | |
| 88 | mStage.setX( x ); | |
| 89 | mStage.setY( y ); | |
| 90 | } // else: default behavior is center on screen | |
| 91 | ||
| 92 | if( !Double.isNaN( w ) && !Double.isNaN( h ) ) { | |
| 93 | mStage.setWidth( w ); | |
| 94 | mStage.setHeight( h ); | |
| 95 | } // else: default behavior is use scene size | |
| 96 | ||
| 97 | if( fullScreen != mStage.isFullScreen() ) { | |
| 98 | mStage.setFullScreen( fullScreen ); | |
| 99 | } | |
| 100 | ||
| 101 | if( maximized != mStage.isMaximized() ) { | |
| 102 | mStage.setMaximized( maximized ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Remembers the window bounds when the window is not iconified, maximized or | |
| 108 | * in fullScreen. | |
| 109 | */ | |
| 110 | private void boundsChanged() { | |
| 111 | // avoid too many (and useless) runLater() invocations | |
| 112 | if( runLaterPending ) { | |
| 113 | return; | |
| 114 | } | |
| 115 | ||
| 116 | runLaterPending = true; | |
| 117 | ||
| 118 | // must use runLater() to ensure that change of all properties | |
| 119 | // (x, y, width, height, iconified, maximized and fullScreen) | |
| 120 | // has finished | |
| 121 | Platform.runLater( () -> { | |
| 122 | runLaterPending = false; | |
| 123 | ||
| 124 | if( isNormalState() ) { | |
| 125 | normalBounds = getStageBounds(); | |
| 126 | } | |
| 127 | } ); | |
| 128 | } | |
| 129 | ||
| 130 | private boolean isNormalState() { | |
| 131 | return !mStage.isIconified() && | |
| 132 | !mStage.isMaximized() && | |
| 133 | !mStage.isFullScreen(); | |
| 134 | } | |
| 135 | ||
| 136 | private Rectangle getStageBounds() { | |
| 137 | return new Rectangle( | |
| 138 | mStage.getX(), | |
| 139 | mStage.getY(), | |
| 140 | mStage.getWidth(), | |
| 141 | mStage.getHeight() | |
| 142 | ); | |
| 143 | } | |
| 144 | } | |
| 145 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import java.util.ArrayList; | |
| 30 | import java.util.prefs.Preferences; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for trimming, storing, and retrieving strings. | |
| 34 | */ | |
| 35 | public class Utils { | |
| 36 | ||
| 37 | public static String ltrim( final String s ) { | |
| 38 | int i = 0; | |
| 39 | ||
| 40 | while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 41 | i++; | |
| 42 | } | |
| 43 | ||
| 44 | return s.substring( i ); | |
| 45 | } | |
| 46 | ||
| 47 | public static String rtrim( final String s ) { | |
| 48 | int i = s.length() - 1; | |
| 49 | ||
| 50 | while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 51 | i--; | |
| 52 | } | |
| 53 | ||
| 54 | return s.substring( 0, i + 1 ); | |
| 55 | } | |
| 56 | ||
| 57 | public static String[] getPrefsStrings( final Preferences prefs, | |
| 58 | String key ) { | |
| 59 | final ArrayList<String> arr = new ArrayList<>( 256 ); | |
| 60 | ||
| 61 | for( int i = 0; i < 10000; i++ ) { | |
| 62 | final String s = prefs.get( key + (i + 1), null ); | |
| 63 | ||
| 64 | if( s == null ) { | |
| 65 | break; | |
| 66 | } | |
| 67 | ||
| 68 | arr.add( s ); | |
| 69 | } | |
| 70 | ||
| 71 | return arr.toArray( new String[ 0 ] ); | |
| 72 | } | |
| 73 | ||
| 74 | public static void putPrefsStrings( Preferences prefs, String key, | |
| 75 | String[] strings ) { | |
| 76 | for( int i = 0; i < strings.length; i++ ) { | |
| 77 | prefs.put( key + (i + 1), strings[ i ] ); | |
| 78 | } | |
| 79 | ||
| 80 | for( int i = strings.length; prefs.get( key + (i + 1), | |
| 81 | null ) != null; i++ ) { | |
| 82 | prefs.remove( key + (i + 1) ); | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 86 | 1 |
| 1 | com.keenwrite.service.impl.DefaultOptions | |
| 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 28 | 2 | |
| 29 | .markdown-editor { | |
| 3 | .markdown { | |
| 4 | -fx-font-family: 'Noto Sans'; | |
| 30 | 5 | -fx-font-size: 11pt; |
| 6 | -fx-padding: 1em; | |
| 31 | 7 | } |
| 32 | 8 | |
| 33 | 9 | /* Subtly highlight the current paragraph. */ |
| 34 | .markdown-editor .paragraph-box:has-caret { | |
| 10 | .markdown .paragraph-box:has-caret { | |
| 35 | 11 | -fx-background-color: #fcfeff; |
| 36 | 12 | } |
| 37 | 13 | |
| 38 | 14 | /* Light colour for selection highlight. */ |
| 39 | .markdown-editor .selection { | |
| 15 | .markdown .selection { | |
| 40 | 16 | -fx-fill: #a6d2ff; |
| 41 | 17 | } |
| 42 | 18 | |
| 43 | 19 | /* Decoration for words not found in the lexicon. */ |
| 44 | .markdown-editor .spelling { | |
| 20 | .markdown .spelling { | |
| 45 | 21 | -rtfx-underline-color: rgba(255, 131, 67, .7); |
| 46 | 22 | -rtfx-underline-dash-array: 4, 2; |
| 47 | 23 | -rtfx-underline-width: 2; |
| 48 | 24 | -rtfx-underline-cap: round; |
| 25 | } | |
| 26 | ||
| 27 | .markdown .search { | |
| 28 | -rtfx-background-color: #ffe959; | |
| 49 | 29 | } |
| 50 | 30 |
| 1 | 1 |
| 1 | 1 |
| 1 | 1 |
| 1 | 1 |
| 1 | 1 |
| 1 | 1 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK JP'; | |
| 3 | } | |
| 1 | 4 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK KR'; | |
| 3 | } | |
| 1 | 4 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK SC'; | |
| 3 | } | |
| 1 | 4 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK SC'; | |
| 3 | } | |
| 1 | 4 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK HK'; | |
| 3 | } | |
| 1 | 4 |
| 1 | .markdown { | |
| 2 | -fx-font-family: 'Noto Sans CJK TC'; | |
| 3 | } | |
| 1 | 4 |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no" ?> | |
| 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |
| 3 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve"> | |
| 4 | <desc>Created with Fabric.js 3.6.3</desc> | |
| 5 | <defs> | |
| 6 | </defs> | |
| 7 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo" > | |
| 8 | <rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" paint-order="stroke" x="-325" y="-260" rx="0" ry="0" width="650" height="520" /> | |
| 9 | </g> | |
| 10 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo" > | |
| 11 | <g style="" paint-order="stroke" > | |
| 12 | <g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)" > | |
| 13 | <linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)" x1="0" y1="0" x2="1" y2="0"> | |
| 14 | <stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/> | |
| 15 | <stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/> | |
| 16 | </linearGradient> | |
| 17 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" /> | |
| 18 | </g> | |
| 19 | <g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)" > | |
| 20 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" /> | |
| 21 | </g> | |
| 22 | <g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)" > | |
| 23 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" /> | |
| 24 | </g> | |
| 25 | <g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)" > | |
| 26 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" /> | |
| 27 | </g> | |
| 28 | <g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)" > | |
| 29 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" /> | |
| 30 | </g> | |
| 31 | <g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)" > | |
| 32 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" /> | |
| 33 | </g> | |
| 34 | <g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)" > | |
| 35 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" /> | |
| 36 | </g> | |
| 37 | <g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)" > | |
| 38 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" /> | |
| 39 | </g> | |
| 40 | </g> | |
| 41 | </g> | |
| 42 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path" > | |
| 43 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" /> | |
| 44 | </g> | |
| 45 | </svg> | |
| 1 |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <svg | |
| 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 4 | xmlns:cc="http://creativecommons.org/ns#" | |
| 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 6 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 7 | xmlns="http://www.w3.org/2000/svg" | |
| 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 10 | version="1.1" | |
| 11 | width="1280" | |
| 12 | height="1024" | |
| 13 | viewBox="0 0 1280 1024" | |
| 14 | xml:space="preserve" | |
| 15 | id="svg52" | |
| 16 | sodipodi:docname="logo-text.svg" | |
| 17 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata | |
| 18 | id="metadata56"><rdf:RDF><cc:Work | |
| 19 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | |
| 20 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview | |
| 21 | inkscape:document-rotation="0" | |
| 22 | pagecolor="#ffffff" | |
| 23 | bordercolor="#666666" | |
| 24 | borderopacity="1" | |
| 25 | objecttolerance="10" | |
| 26 | gridtolerance="10" | |
| 27 | guidetolerance="10" | |
| 28 | inkscape:pageopacity="0" | |
| 29 | inkscape:pageshadow="2" | |
| 30 | inkscape:window-width="640" | |
| 31 | inkscape:window-height="480" | |
| 32 | id="namedview54" | |
| 33 | showgrid="false" | |
| 34 | inkscape:zoom="0.78417969" | |
| 35 | inkscape:cx="642.50039" | |
| 36 | inkscape:cy="508.59942" | |
| 37 | inkscape:current-layer="svg52" /> | |
| 38 | <desc | |
| 39 | id="desc2">Created with Fabric.js 3.6.3</desc> | |
| 40 | <defs | |
| 41 | id="defs4"><rect | |
| 42 | x="114.92139" | |
| 43 | y="132.06312" | |
| 44 | width="470.12033" | |
| 45 | height="175.55822" | |
| 46 | id="rect933" /> | |
| 47 | ||
| 48 | ||
| 49 | ||
| 50 | ||
| 51 | ||
| 52 | ||
| 53 | ||
| 54 | ||
| 55 | ||
| 56 | ||
| 57 | ||
| 58 | ||
| 59 | <linearGradient | |
| 60 | y2="-0.049471263" | |
| 61 | x2="0.96880889" | |
| 62 | y1="-0.044911571" | |
| 63 | x1="0.15235768" | |
| 64 | gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)" | |
| 65 | gradientUnits="userSpaceOnUse" | |
| 66 | id="SVGID_1_302284"> | |
| 67 | <stop | |
| 68 | id="stop9" | |
| 69 | style="stop-color:#ec706a;stop-opacity:1" | |
| 70 | offset="0%" /> | |
| 71 | <stop | |
| 72 | id="stop11" | |
| 73 | style="stop-color:#ecd980;stop-opacity:1" | |
| 74 | offset="100%" /> | |
| 75 | </linearGradient> | |
| 76 | ||
| 77 | ||
| 78 | ||
| 79 | ||
| 80 | ||
| 81 | ||
| 82 | ||
| 83 | ||
| 84 | ||
| 85 | ||
| 86 | ||
| 87 | ||
| 88 | ||
| 89 | ||
| 90 | ||
| 91 | </defs> | |
| 92 | ||
| 93 | <g | |
| 94 | id="g853"><path | |
| 95 | style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 96 | paint-order="stroke" | |
| 97 | d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534" | |
| 98 | stroke-linecap="round" | |
| 99 | id="path14" /><path | |
| 100 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 101 | paint-order="stroke" | |
| 102 | d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z" | |
| 103 | stroke-linecap="round" | |
| 104 | id="path22" /><path | |
| 105 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 106 | paint-order="stroke" | |
| 107 | d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z" | |
| 108 | stroke-linecap="round" | |
| 109 | id="path26" /><path | |
| 110 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 111 | paint-order="stroke" | |
| 112 | d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z" | |
| 113 | stroke-linecap="round" | |
| 114 | id="path30" /><path | |
| 115 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 116 | paint-order="stroke" | |
| 117 | d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z" | |
| 118 | stroke-linecap="round" | |
| 119 | id="path34" /><path | |
| 120 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 121 | paint-order="stroke" | |
| 122 | d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z" | |
| 123 | stroke-linecap="round" | |
| 124 | id="path38" /><path | |
| 125 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 126 | paint-order="stroke" | |
| 127 | d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508" | |
| 128 | stroke-linecap="round" | |
| 129 | id="path42" /></g> | |
| 130 | ||
| 131 | <text | |
| 132 | xml:space="preserve" | |
| 133 | id="text931" | |
| 134 | style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);" /><text | |
| 135 | xml:space="preserve" | |
| 136 | style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none;" | |
| 137 | x="311.87085" | |
| 138 | y="820.2641" | |
| 139 | id="text939"><tspan | |
| 140 | sodipodi:role="line" | |
| 141 | id="tspan937" | |
| 142 | x="311.87085" | |
| 143 | y="820.2641">KeenWrite</tspan></text></svg> | |
| 144 | 1 |
| 5 | 5 | # suppress inspection "UnusedProperty" for whole file |
| 6 | 6 | |
| 7 | Main.menu.file=_File | |
| 8 | Main.menu.file.new=_New | |
| 9 | Main.menu.file.open=_Open... | |
| 10 | Main.menu.file.close=_Close | |
| 11 | Main.menu.file.close_all=Close All | |
| 12 | Main.menu.file.save=_Save | |
| 13 | Main.menu.file.save_as=Save _As | |
| 14 | Main.menu.file.save_all=Save A_ll | |
| 15 | Main.menu.file.export=_Export As | |
| 16 | Main.menu.file.export.html_svg=HTML and S_VG | |
| 17 | Main.menu.file.export.html_tex=HTML and _TeX | |
| 18 | Main.menu.file.export.markdown=Markdown | |
| 19 | Main.menu.file.exit=E_xit | |
| 7 | # ######################################################################## | |
| 8 | # Menu Bar | |
| 9 | # ######################################################################## | |
| 20 | 10 | |
| 11 | Main.menu.file=_File | |
| 21 | 12 | Main.menu.edit=_Edit |
| 22 | Main.menu.edit.undo=_Undo | |
| 23 | Main.menu.edit.redo=_Redo | |
| 24 | Main.menu.edit.cut=Cu_t | |
| 25 | Main.menu.edit.copy=_Copy | |
| 26 | Main.menu.edit.paste=_Paste | |
| 27 | Main.menu.edit.selectAll=Select _All | |
| 28 | Main.menu.edit.find=_Find | |
| 29 | Main.menu.edit.find.next=Find _Next | |
| 30 | Main.menu.edit.preferences=_Preferences | |
| 31 | ||
| 32 | 13 | Main.menu.insert=_Insert |
| 33 | Main.menu.insert.blockquote=_Blockquote | |
| 34 | Main.menu.insert.code=Inline _Code | |
| 35 | Main.menu.insert.fenced_code_block=_Fenced Code Block | |
| 36 | Main.menu.insert.fenced_code_block.prompt=Enter code here | |
| 37 | Main.menu.insert.link=_Link... | |
| 38 | Main.menu.insert.image=_Image... | |
| 39 | Main.menu.insert.heading.1=Heading _1 | |
| 40 | Main.menu.insert.heading.1.prompt=heading 1 | |
| 41 | Main.menu.insert.heading.2=Heading _2 | |
| 42 | Main.menu.insert.heading.2.prompt=heading 2 | |
| 43 | Main.menu.insert.heading.3=Heading _3 | |
| 44 | Main.menu.insert.heading.3.prompt=heading 3 | |
| 45 | Main.menu.insert.unordered_list=_Unordered List | |
| 46 | Main.menu.insert.ordered_list=_Ordered List | |
| 47 | Main.menu.insert.horizontal_rule=_Horizontal Rule | |
| 48 | ||
| 49 | 14 | Main.menu.format=Forma_t |
| 50 | Main.menu.format.bold=_Bold | |
| 51 | Main.menu.format.italic=_Italic | |
| 52 | Main.menu.format.superscript=Su_perscript | |
| 53 | Main.menu.format.subscript=Su_bscript | |
| 54 | Main.menu.format.strikethrough=Stri_kethrough | |
| 55 | ||
| 56 | 15 | Main.menu.definition=_Definition |
| 57 | Main.menu.definition.create=_Create | |
| 58 | Main.menu.definition.insert=_Insert | |
| 59 | ||
| 16 | Main.menu.view=_View | |
| 60 | 17 | Main.menu.help=_Help |
| 61 | Main.menu.help.about=About | |
| 18 | ||
| 19 | # ######################################################################## | |
| 20 | # Detachable Tabs | |
| 21 | # ######################################################################## | |
| 22 | ||
| 23 | # {0} is the application title; {1} is a unique window ID. | |
| 24 | Detach.tab.title={0} - {1} | |
| 62 | 25 | |
| 63 | 26 | # ######################################################################## |
| ... | ||
| 79 | 42 | Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}'' |
| 80 | 43 | Main.status.error.messages.syntax=Missing ''}'' in ''{0}'' |
| 44 | ||
| 45 | Main.status.error.undo=Cannot undo; beginning of undo history reached | |
| 46 | Main.status.error.redo=Cannot redo; end of redo history reached | |
| 47 | ||
| 48 | Main.status.image.request.init=Initializing HTTP request | |
| 49 | Main.status.image.request.fetch=Requesting content type from {0} | |
| 50 | Main.status.image.request.success=Detected content type ''{0}'' | |
| 81 | 51 | |
| 82 | 52 | # ######################################################################## |
| 83 | # Preferences | |
| 53 | # Search Bar | |
| 84 | 54 | # ######################################################################## |
| 85 | 55 | |
| 86 | Preferences.r=R | |
| 87 | Preferences.r.script=Startup Script | |
| 88 | Preferences.r.script.desc=Script runs prior to executing R statements within the document. | |
| 89 | Preferences.r.directory=Working Directory | |
| 90 | Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script. | |
| 91 | Preferences.r.delimiter.began=Delimiter Prefix | |
| 92 | Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions. | |
| 93 | Preferences.r.delimiter.ended=Delimiter Suffix | |
| 94 | Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions. | |
| 56 | Main.search.stop.tooltip=Close search bar | |
| 57 | Main.search.stop.icon=CLOSE | |
| 58 | Main.search.next.tooltip=Find next match | |
| 59 | Main.search.next.icon=CHEVRON_DOWN | |
| 60 | Main.search.prev.tooltip=Find previous match | |
| 61 | Main.search.prev.icon=CHEVRON_UP | |
| 62 | Main.search.find.tooltip=Search document for text | |
| 63 | Main.search.find.icon=SEARCH | |
| 64 | Main.search.match.none=No matches | |
| 65 | Main.search.match.some={0} of {1} matches | |
| 95 | 66 | |
| 96 | Preferences.images=Images | |
| 97 | Preferences.images.directory=Relative Directory | |
| 98 | Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths. | |
| 99 | Preferences.images.suffixes=Extensions | |
| 100 | Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces. | |
| 67 | # ######################################################################## | |
| 68 | # Workspace preferences | |
| 69 | # ######################################################################## | |
| 101 | 70 | |
| 102 | Preferences.definitions=Definitions | |
| 103 | Preferences.definitions.path=File name | |
| 104 | Preferences.definitions.path.desc=Absolute path to interpolated string definitions. | |
| 105 | Preferences.definitions.delimiter.began=Delimiter Prefix | |
| 106 | Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting. | |
| 107 | Preferences.definitions.delimiter.ended=Delimiter Suffix | |
| 108 | Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending. | |
| 71 | workspace.r=R | |
| 72 | workspace.r.script=Startup Script | |
| 73 | workspace.r.script.desc=Script runs prior to executing R statements within the document. | |
| 74 | workspace.r.dir=Working Directory | |
| 75 | workspace.r.dir.desc=Value assigned to {0}application.r.working.directory{1} and usable in the startup script. | |
| 76 | workspace.r.dir.title=Directory | |
| 77 | workspace.r.delimiter.began=Delimiter Prefix | |
| 78 | workspace.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions. | |
| 79 | workspace.r.delimiter.began.title=Opening | |
| 80 | workspace.r.delimiter.ended=Delimiter Suffix | |
| 81 | workspace.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions. | |
| 82 | workspace.r.delimiter.ended.title=Closing | |
| 109 | 83 | |
| 110 | Preferences.fonts=Editor | |
| 111 | Preferences.fonts.size_editor=Font Size | |
| 112 | Preferences.fonts.size_editor.desc=Font size to use for the text editor. | |
| 84 | workspace.images=Images | |
| 85 | workspace.images.dir=Relative Directory | |
| 86 | workspace.images.dir.desc=Path prepended to embedded images referenced using local file paths. | |
| 87 | workspace.images.dir.title=Directory | |
| 88 | workspace.images.order=Extensions | |
| 89 | workspace.images.order.desc=Preferred order of image file types to embed, separated by spaces. | |
| 90 | workspace.images.order.title=Extensions | |
| 91 | ||
| 92 | workspace.definition=Definition | |
| 93 | workspace.definition.path=File name | |
| 94 | workspace.definition.path.desc=Absolute path to interpolated string definition. | |
| 95 | workspace.definition.path.title=Path | |
| 96 | workspace.definition.delimiter.began=Delimiter Prefix | |
| 97 | workspace.definition.delimiter.began.desc=Indicates when a definition key is starting. | |
| 98 | workspace.definition.delimiter.began.title=Opening | |
| 99 | workspace.definition.delimiter.ended=Delimiter Suffix | |
| 100 | workspace.definition.delimiter.ended.desc=Indicates when a definition key is ending. | |
| 101 | workspace.definition.delimiter.ended.title=Closing | |
| 102 | ||
| 103 | workspace.ui.font=Fonts | |
| 104 | workspace.ui.font.editor.size=Editor Font Size | |
| 105 | workspace.ui.font.editor.size.desc=Text editor font size. | |
| 106 | workspace.ui.font.editor.size.title=Points | |
| 107 | workspace.ui.font.preview.size=Preview Font Size | |
| 108 | workspace.ui.font.preview.size.desc=Preview pane font size. | |
| 109 | workspace.ui.font.preview.size.title=Points | |
| 110 | workspace.ui.font.locale=Locale | |
| 111 | workspace.ui.font.locale.desc=Character set for editing and previewing. | |
| 112 | workspace.ui.font.locale.title=Language | |
| 113 | 113 | |
| 114 | 114 | # ######################################################################## |
| 115 | 115 | # Definition Pane and its Tree View |
| 116 | 116 | # ######################################################################## |
| 117 | 117 | |
| 118 | Definition.menu.create=Create | |
| 119 | Definition.menu.rename=Rename | |
| 120 | Definition.menu.remove=Delete | |
| 121 | 118 | Definition.menu.add.default=Undefined |
| 119 | ||
| 120 | # ######################################################################## | |
| 121 | # Definition Pane | |
| 122 | # ######################################################################## | |
| 123 | ||
| 124 | Pane.definition.node.root.title=Definitions | |
| 122 | 125 | |
| 123 | 126 | # ######################################################################## |
| 124 | 127 | # Failure messages with respect to YAML files. |
| 125 | 128 | # ######################################################################## |
| 129 | ||
| 126 | 130 | yaml.error.open=Could not open YAML file (ensure non-empty file). |
| 127 | 131 | yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''. |
| 128 | 132 | yaml.error.missing=Empty definition value for key ''{0}''. |
| 129 | 133 | yaml.error.tree.form=Unassigned definition near ''{0}''. |
| 130 | 134 | |
| 131 | 135 | # ######################################################################## |
| 132 | # File Editor | |
| 136 | # Text Resource | |
| 133 | 137 | # ######################################################################## |
| 134 | 138 | |
| 135 | FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1} | |
| 136 | FileEditor.loadFailed.title=Load | |
| 137 | FileEditor.loadFailed.reason.permissions=File must be readable and writable. | |
| 138 | FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 139 | FileEditor.saveFailed.title=Save | |
| 139 | TextResource.load.error.unsaved=The file ''{0}'' is unsaved or does not exist. | |
| 140 | TextResource.load.error.permissions=The file ''{0}'' must be readable and writable. | |
| 141 | ||
| 142 | # ######################################################################## | |
| 143 | # Text Resources | |
| 144 | # ######################################################################## | |
| 145 | ||
| 146 | TextResource.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 147 | TextResource.saveFailed.title=Save | |
| 140 | 148 | |
| 141 | 149 | # ######################################################################## |
| ... | ||
| 151 | 159 | Dialog.file.choose.filter.title.xml=XML Files |
| 152 | 160 | Dialog.file.choose.filter.title.all=All Files |
| 153 | ||
| 154 | # ######################################################################## | |
| 155 | # Alert Dialog | |
| 156 | # ######################################################################## | |
| 157 | ||
| 158 | Alert.file.close.title=Close | |
| 159 | Alert.file.close.text=Save changes to {0}? | |
| 160 | ||
| 161 | # ######################################################################## | |
| 162 | # Definition Pane | |
| 163 | # ######################################################################## | |
| 164 | ||
| 165 | Pane.definition.node.root.title=Definitions | |
| 166 | Pane.definition.button.create.label=_Create | |
| 167 | Pane.definition.button.rename.label=_Rename | |
| 168 | Pane.definition.button.delete.label=_Delete | |
| 169 | Pane.definition.button.create.tooltip=Add new item (Insert) | |
| 170 | Pane.definition.button.rename.tooltip=Rename selected item (F2) | |
| 171 | Pane.definition.button.delete.tooltip=Delete selected items (Delete) | |
| 172 | ||
| 173 | # Controls ############################################################### | |
| 174 | 161 | |
| 175 | 162 | # ######################################################################## |
| 176 | 163 | # Browse File |
| 177 | 164 | # ######################################################################## |
| 178 | 165 | |
| 179 | 166 | BrowseFileButton.chooser.title=Browse for local file |
| 180 | 167 | BrowseFileButton.chooser.allFilesFilter=All Files |
| 181 | 168 | BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} |
| 182 | 169 | |
| 183 | # Dialogs ################################################################ | |
| 170 | # ######################################################################## | |
| 171 | # Alert Dialog | |
| 172 | # ######################################################################## | |
| 173 | ||
| 174 | Alert.file.close.title=Close | |
| 175 | Alert.file.close.text=Save changes to {0}? | |
| 184 | 176 | |
| 185 | 177 | # ######################################################################## |
| 186 | # Image | |
| 178 | # Image Dialog | |
| 187 | 179 | # ######################################################################## |
| 188 | 180 | |
| ... | ||
| 195 | 187 | |
| 196 | 188 | # ######################################################################## |
| 197 | # Hyperlink | |
| 189 | # Hyperlink Dialog | |
| 198 | 190 | # ######################################################################## |
| 199 | 191 | |
| 200 | 192 | Dialog.link.title=Link |
| 201 | 193 | Dialog.link.previewLabel.text=Markdown Preview\: |
| 202 | 194 | Dialog.link.textLabel.text=Link Text\: |
| 203 | 195 | Dialog.link.titleLabel.text=Title (tooltip)\: |
| 204 | 196 | Dialog.link.urlLabel.text=Link URL\: |
| 205 | 197 | |
| 206 | 198 | # ######################################################################## |
| 207 | # About | |
| 199 | # About Dialog | |
| 208 | 200 | # ######################################################################## |
| 209 | 201 | |
| 210 | 202 | Dialog.about.title=About {0} |
| 211 | 203 | Dialog.about.header={0} |
| 212 | 204 | Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber |
| 205 | ||
| 206 | # ######################################################################## | |
| 207 | # Application Actions | |
| 208 | # ######################################################################## | |
| 209 | ||
| 210 | App.action.file.new.description=Create a new file | |
| 211 | App.action.file.new.accelerator=Shortcut+N | |
| 212 | App.action.file.new.icon=FILE_ALT | |
| 213 | App.action.file.new.text=_New | |
| 214 | ||
| 215 | App.action.file.open.description=Open a new file | |
| 216 | App.action.file.open.accelerator=Shortcut+O | |
| 217 | App.action.file.open.text=_Open... | |
| 218 | App.action.file.open.icon=FOLDER_OPEN_ALT | |
| 219 | ||
| 220 | App.action.file.close.description=Close the current document | |
| 221 | App.action.file.close.accelerator=Shortcut+W | |
| 222 | App.action.file.close.text=_Close | |
| 223 | ||
| 224 | App.action.file.close_all.description=Close all open documents | |
| 225 | App.action.file.close_all.accelerator=Ctrl+F4 | |
| 226 | App.action.file.close_all.text=Close All | |
| 227 | ||
| 228 | App.action.file.save.description=Save the document | |
| 229 | App.action.file.save.accelerator=Shortcut+S | |
| 230 | App.action.file.save.text=_Save | |
| 231 | App.action.file.save.icon=FLOPPY_ALT | |
| 232 | ||
| 233 | App.action.file.save_as.description=Rename the current document | |
| 234 | App.action.file.save_as.text=Save _As | |
| 235 | ||
| 236 | App.action.file.save_all.description=Save all open documents | |
| 237 | App.action.file.save_all.accelerator=Shortcut+Shift+S | |
| 238 | App.action.file.save_all.text=Save A_ll | |
| 239 | ||
| 240 | App.action.file.export.html_svg.description=Export the current document as HTML + SVG | |
| 241 | App.action.file.export.text=_Export As | |
| 242 | App.action.file.export.html_svg.text=HTML and S_VG | |
| 243 | ||
| 244 | App.action.file.export.html_tex.description=Export the current document as HTML + TeX | |
| 245 | App.action.file.export.html_tex.text=HTML and _TeX | |
| 246 | ||
| 247 | App.action.file.export.markdown.description=Export the current document as Markdown | |
| 248 | App.action.file.export.markdown.text=Markdown | |
| 249 | ||
| 250 | App.action.file.exit.description=Quit the application | |
| 251 | App.action.file.exit.text=E_xit | |
| 252 | ||
| 253 | ||
| 254 | App.action.edit.undo.description=Undo the previous edit | |
| 255 | App.action.edit.undo.accelerator=Shortcut+Z | |
| 256 | App.action.edit.undo.text=_Undo | |
| 257 | App.action.edit.undo.icon=UNDO | |
| 258 | ||
| 259 | App.action.edit.redo.description=Redo the previous edit | |
| 260 | App.action.edit.redo.accelerator=Shortcut+Y | |
| 261 | App.action.edit.redo.text=_Redo | |
| 262 | App.action.edit.redo.icon=REPEAT | |
| 263 | ||
| 264 | App.action.edit.cut.description=Delete the selected text or line | |
| 265 | App.action.edit.cut.accelerator=Shortcut+X | |
| 266 | App.action.edit.cut.text=Cu_t | |
| 267 | App.action.edit.cut.icon=CUT | |
| 268 | ||
| 269 | App.action.edit.copy.description=Copy the selected text | |
| 270 | App.action.edit.copy.accelerator=Shortcut+C | |
| 271 | App.action.edit.copy.text=_Copy | |
| 272 | App.action.edit.copy.icon=COPY | |
| 273 | ||
| 274 | App.action.edit.paste.description=Paste from the clipboard | |
| 275 | App.action.edit.paste.accelerator=Shortcut+V | |
| 276 | App.action.edit.paste.text=_Paste | |
| 277 | App.action.edit.paste.icon=PASTE | |
| 278 | ||
| 279 | App.action.edit.select_all.description=Highlight the current document text | |
| 280 | App.action.edit.select_all.accelerator=Shortcut+A | |
| 281 | App.action.edit.select_all.text=Select _All | |
| 282 | ||
| 283 | App.action.edit.find.description=Search for text in the document | |
| 284 | App.action.edit.find.accelerator=Shortcut+F | |
| 285 | App.action.edit.find.text=_Find | |
| 286 | App.action.edit.find.icon=SEARCH | |
| 287 | ||
| 288 | App.action.edit.find_next.description=Find next occurrence | |
| 289 | App.action.edit.find_next.accelerator=F3 | |
| 290 | App.action.edit.find_next.text=Find _Next | |
| 291 | ||
| 292 | App.action.edit.find_prev.description=Find previous occurrence | |
| 293 | App.action.edit.find_prev.accelerator=Shift+F3 | |
| 294 | App.action.edit.find_prev.text=Find _Prev | |
| 295 | ||
| 296 | App.action.edit.preferences.description=Edit user preferences | |
| 297 | App.action.edit.preferences.accelerator=Ctrl+Alt+S | |
| 298 | App.action.edit.preferences.text=_Preferences | |
| 299 | ||
| 300 | ||
| 301 | App.action.format.bold.description=Insert strong text | |
| 302 | App.action.format.bold.accelerator=Shortcut+B | |
| 303 | App.action.format.bold.text=_Bold | |
| 304 | App.action.format.bold.icon=BOLD | |
| 305 | ||
| 306 | App.action.format.italic.description=Insert text emphasis | |
| 307 | App.action.format.italic.accelerator=Shortcut+I | |
| 308 | App.action.format.italic.text=_Italic | |
| 309 | App.action.format.italic.icon=ITALIC | |
| 310 | ||
| 311 | App.action.format.superscript.description=Insert superscript text | |
| 312 | App.action.format.superscript.accelerator=Shortcut+[ | |
| 313 | App.action.format.superscript.text=Su_perscript | |
| 314 | App.action.format.superscript.icon=SUPERSCRIPT | |
| 315 | ||
| 316 | App.action.format.subscript.description=Insert subscript text | |
| 317 | App.action.format.subscript.accelerator=Shortcut+] | |
| 318 | App.action.format.subscript.text=Su_bscript | |
| 319 | App.action.format.subscript.icon=SUBSCRIPT | |
| 320 | ||
| 321 | App.action.format.strikethrough.description=Insert struck text | |
| 322 | App.action.format.strikethrough.accelerator=Shortcut+T | |
| 323 | App.action.format.strikethrough.text=Stri_kethrough | |
| 324 | App.action.format.strikethrough.icon=STRIKETHROUGH | |
| 325 | ||
| 326 | ||
| 327 | App.action.insert.blockquote.description=Insert blockquote | |
| 328 | App.action.insert.blockquote.accelerator=Ctrl+Q | |
| 329 | App.action.insert.blockquote.text=_Blockquote | |
| 330 | App.action.insert.blockquote.icon=QUOTE_LEFT | |
| 331 | ||
| 332 | App.action.insert.code.description=Insert inline code | |
| 333 | App.action.insert.code.accelerator=Shortcut+K | |
| 334 | App.action.insert.code.text=Inline _Code | |
| 335 | App.action.insert.code.icon=CODE | |
| 336 | ||
| 337 | App.action.insert.fenced_code_block.description=Insert code block | |
| 338 | App.action.insert.fenced_code_block.accelerator=Shortcut+Shift+K | |
| 339 | App.action.insert.fenced_code_block.text=_Fenced Code Block | |
| 340 | App.action.insert.fenced_code_block.prompt.text=Enter code here | |
| 341 | App.action.insert.fenced_code_block.icon=FILE_CODE_ALT | |
| 342 | ||
| 343 | App.action.insert.link.description=Insert hyperlink | |
| 344 | App.action.insert.link.accelerator=Shortcut+L | |
| 345 | App.action.insert.link.text=_Link... | |
| 346 | App.action.insert.link.icon=LINK | |
| 347 | ||
| 348 | App.action.insert.image.description=Insert image | |
| 349 | App.action.insert.image.accelerator=Shortcut+G | |
| 350 | App.action.insert.image.text=_Image... | |
| 351 | App.action.insert.image.icon=PICTURE_ALT | |
| 352 | ||
| 353 | App.action.insert.heading.description=Insert heading level | |
| 354 | App.action.insert.heading.accelerator=Shortcut+ | |
| 355 | App.action.insert.heading.icon=HEADER | |
| 356 | ||
| 357 | App.action.insert.heading_1.description=${App.action.insert.heading.description} 1 | |
| 358 | App.action.insert.heading_1.accelerator=${App.action.insert.heading.accelerator}1 | |
| 359 | App.action.insert.heading_1.text=Heading _1 | |
| 360 | App.action.insert.heading_1.icon=${App.action.insert.heading.icon} | |
| 361 | ||
| 362 | App.action.insert.heading_2.description=${App.action.insert.heading.description} 2 | |
| 363 | App.action.insert.heading_2.accelerator=${App.action.insert.heading.accelerator}2 | |
| 364 | App.action.insert.heading_2.text=Heading _2 | |
| 365 | App.action.insert.heading_2.icon=${App.action.insert.heading.icon} | |
| 366 | ||
| 367 | App.action.insert.heading_3.description=${App.action.insert.heading.description} 3 | |
| 368 | App.action.insert.heading_3.accelerator=${App.action.insert.heading.accelerator}3 | |
| 369 | App.action.insert.heading_3.text=Heading _3 | |
| 370 | App.action.insert.heading_3.icon=${App.action.insert.heading.icon} | |
| 371 | ||
| 372 | App.action.insert.unordered_list.description=Insert bulleted list | |
| 373 | App.action.insert.unordered_list.accelerator=Shortcut+U | |
| 374 | App.action.insert.unordered_list.text=_Unordered List | |
| 375 | App.action.insert.unordered_list.icon=LIST_UL | |
| 376 | ||
| 377 | App.action.insert.ordered_list.description=Insert enumerated list | |
| 378 | App.action.insert.ordered_list.accelerator=Shortcut+Shift+O | |
| 379 | App.action.insert.ordered_list.text=_Ordered List | |
| 380 | App.action.insert.ordered_list.icon=LIST_OL | |
| 381 | ||
| 382 | App.action.insert.horizontal_rule.description=Insert horizontal rule | |
| 383 | App.action.insert.horizontal_rule.accelerator=Shortcut+H | |
| 384 | App.action.insert.horizontal_rule.text=_Horizontal Rule | |
| 385 | App.action.insert.horizontal_rule.icon=LIST_OL | |
| 386 | ||
| 387 | ||
| 388 | App.action.definition.create.description=Create a new variable definition | |
| 389 | App.action.definition.create.text=_Create | |
| 390 | App.action.definition.create.icon=TREE | |
| 391 | App.action.definition.create.tooltip=Add new item (Insert) | |
| 392 | ||
| 393 | App.action.definition.rename.description=Rename the selected variable definition | |
| 394 | App.action.definition.rename.text=_Rename | |
| 395 | App.action.definition.rename.icon=EDIT | |
| 396 | App.action.definition.rename.tooltip=Rename selected item (F2) | |
| 397 | ||
| 398 | App.action.definition.delete.description=Delete the selected variable definitions | |
| 399 | App.action.definition.delete.text=_Delete | |
| 400 | App.action.definition.delete.icon=TRASH | |
| 401 | App.action.definition.delete.tooltip=Delete selected items (Delete) | |
| 402 | ||
| 403 | App.action.definition.insert.description=Insert a definition | |
| 404 | App.action.definition.insert.accelerator=Ctrl+Space | |
| 405 | App.action.definition.insert.text=_Insert | |
| 406 | App.action.definition.insert.icon=STAR | |
| 407 | ||
| 408 | ||
| 409 | App.action.view.refresh.description=Clear all caches | |
| 410 | App.action.view.refresh.accelerator=F5 | |
| 411 | App.action.view.refresh.text=Refresh | |
| 412 | ||
| 413 | App.action.view.preview.description=Open document preview | |
| 414 | App.action.view.preview.accelerator=F7 | |
| 415 | App.action.view.preview.text=Preview | |
| 416 | ||
| 417 | App.action.view.outline.description=Open document outline | |
| 418 | App.action.view.outline.accelerator=F8 | |
| 419 | App.action.view.outline.text=Outline | |
| 420 | ||
| 421 | ||
| 422 | App.action.view.files.description=Open file system browser | |
| 423 | App.action.view.files.accelerator=F9 | |
| 424 | App.action.view.files.text=File system | |
| 425 | ||
| 426 | ||
| 427 | App.action.help.about.description=Show help dialog | |
| 428 | App.action.help.about.accelerator=F1 | |
| 429 | App.action.help.about.text=About | |
| 430 | App.action.help.about.icon=INFO | |
| 213 | 431 | |
| 1 | /* RESET ***/ | |
| 2 | 1 | html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0} |
| 3 | 2 | |
| 4 | /* BODY ***/ | |
| 5 | 3 | body { |
| 6 | /* Must be bundled in JAR file. */ | |
| 7 | font-family: "Vollkorn", serif; | |
| 4 | /* Noto Serif introduces whitespace on style transitions. */ | |
| 5 | font-family: 'Source Serif Pro'; | |
| 6 | font-size: 12pt; | |
| 7 | ||
| 8 | 8 | background-color: #fff; |
| 9 | 9 | margin: 0 auto; |
| 10 | max-width: 960px; | |
| 11 | 10 | line-height: 1.6; |
| 12 | 11 | color: #454545; |
| 13 | 12 | padding: 1em; |
| 14 | font-feature-settings: "liga" 1; | |
| 13 | font-feature-settings: 'liga' 1; | |
| 15 | 14 | font-variant-ligatures: normal; |
| 16 | 15 | } |
| ... | ||
| 132 | 131 | pre, code, tt { |
| 133 | 132 | /* Must be bundled in JAR file. */ |
| 134 | font-family: "Fira Code", monospace; | |
| 133 | font-family: 'Source Code Pro'; | |
| 135 | 134 | font-size: 10pt; |
| 136 | 135 | background-color: #f8f8f8; |
| ... | ||
| 207 | 206 | img { |
| 208 | 207 | max-width: 100%; |
| 208 | ||
| 209 | /* Tell FlyingSaucer to treat images as block elements. | |
| 210 | * See SvgReplacedElementFactory. | |
| 211 | */ | |
| 212 | display: inline-block; | |
| 209 | 213 | } |
| 210 | 214 | |
| 211 | /* Required for FlyingSaucer to detect the node. | |
| 212 | * See SVGReplacedElementFactory for details. | |
| 215 | /* Tell FlyingSaucer to treat tex elements as nodes. | |
| 216 | * See SvgReplacedElementFactory. | |
| 213 | 217 | */ |
| 214 | 218 | tex { |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK JP'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK JP'; | |
| 7 | } | |
| 1 | 8 |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK KR'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK KR'; | |
| 7 | } | |
| 1 | 8 |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK SC'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK SC'; | |
| 7 | } | |
| 1 | 8 |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK SC'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK SC'; | |
| 7 | } | |
| 1 | 8 |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK SC'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK HK'; | |
| 7 | } | |
| 1 | 8 |
| 1 | body { | |
| 2 | font-family: 'Noto Serif CJK TC'; | |
| 3 | } | |
| 4 | ||
| 5 | pre, code, tt { | |
| 6 | font-family: 'Noto Sans Mono CJK TC'; | |
| 7 | } | |
| 1 | 8 |
| 26 | 26 | */ |
| 27 | 27 | |
| 28 | /*---- toolbar ----*/ | |
| 29 | ||
| 30 | 28 | .tool-bar { |
| 31 | 29 | -fx-spacing: 0; |
| ... | ||
| 43 | 41 | .tool-bar .button:armed { |
| 44 | 42 | -fx-color: -fx-pressed-base; |
| 43 | } | |
| 44 | ||
| 45 | /* Definition editor drag and drop target. | |
| 46 | */ | |
| 47 | .drop-target { | |
| 48 | -fx-border-color: #eea82f; | |
| 49 | -fx-border-width: 0 0 2 0; | |
| 50 | -fx-padding: 3 3 1 3 | |
| 45 | 51 | } |
| 46 | 52 | |
| 23 | 23 | # File and Path References |
| 24 | 24 | # ######################################################################## |
| 25 | ||
| 26 | #file.stylesheet.dock=com/panemu/tiwulfx/control/dock/tiwulfx-dock.css | |
| 25 | 27 | file.stylesheet.scene=${application.package}/scene.css |
| 26 | 28 | file.stylesheet.markdown=${application.package}/editor/markdown.css |
| 27 | file.stylesheet.preview=webview.css | |
| 29 | # {0} language code, {1} script code, {2} country code | |
| 30 | file.stylesheet.markdown.locale=${application.package}/editor/markdown_{0}-{1}-{2}.css | |
| 28 | 31 | file.stylesheet.xml=${application.package}/xml.css |
| 29 | 32 | |
| 30 | file.logo.16 =${application.package}/logo16.png | |
| 31 | file.logo.32 =${application.package}/logo32.png | |
| 33 | # Preview styles are loaded statically through a class's classloader. | |
| 34 | file.stylesheet.preview=webview.css | |
| 35 | # {0} language code, {1} script code, {2} country code | |
| 36 | file.stylesheet.preview.locale=webview_{0}-{1}-{2}.css | |
| 37 | ||
| 38 | file.logo.16=${application.package}/logo16.png | |
| 39 | file.logo.32=${application.package}/logo32.png | |
| 32 | 40 | file.logo.128=${application.package}/logo128.png |
| 33 | 41 | file.logo.256=${application.package}/logo256.png |
| 34 | 42 | file.logo.512=${application.package}/logo512.png |
| 35 | 43 | |
| 36 | 44 | # Default file name when a new file is created. |
| 37 | 45 | # This ensures that the file type can always be |
| 38 | 46 | # discerned so that the correct type of variable |
| 39 | 47 | # reference can be inserted. |
| 40 | file.default=untitled.md | |
| 41 | file.definition.default=variables.yaml | |
| 48 | file.default.document=untitled.md | |
| 49 | file.default.definition=variables.yaml | |
| 42 | 50 | |
| 43 | 51 | # ######################################################################## |
| 1 | --- | |
| 2 | c: | |
| 3 | protagonist: | |
| 4 | name: | |
| 5 | First: Chloe | |
| 6 | First_pos: $c.protagonist.name.First$'s | |
| 7 | Middle: Irene | |
| 8 | Family: Angelos | |
| 9 | nick: | |
| 10 | Father: Savant | |
| 11 | Mother: Sweetie | |
| 12 | colour: | |
| 13 | eyes: green | |
| 14 | hair: dark auburn | |
| 15 | syn_1: black | |
| 16 | syn_2: purple | |
| 17 | syn_11: teal | |
| 18 | syn_6: silver | |
| 19 | favourite: emerald green | |
| 20 | speech: | |
| 21 | tic: oh | |
| 22 | father: | |
| 23 | heritage: Greek | |
| 24 | name: | |
| 25 | Short: Bryce | |
| 26 | First: Bryson | |
| 27 | First_pos: $c.protagonist.father.name.First$'s | |
| 28 | Honourific: Mr. | |
| 29 | education: Masters | |
| 30 | vocation: | |
| 31 | name: robotics | |
| 32 | title: roboticist | |
| 33 | employer: | |
| 34 | name: | |
| 35 | Short: Rabota | |
| 36 | Full: $c.protagonist.father.employer.name.Short$ Designs | |
| 37 | hair: | |
| 38 | style: thick, curly | |
| 39 | colour: black | |
| 40 | eyes: | |
| 41 | colour: dark brown | |
| 42 | Endear: Dad | |
| 43 | vehicle: coupé | |
| 44 | mother: | |
| 45 | name: | |
| 46 | Short: Cass | |
| 47 | First: Cassandra | |
| 48 | First_pos: $c.protagonist.mother.name.First$'s | |
| 49 | Honourific: Mrs. | |
| 50 | education: PhD | |
| 51 | speech: | |
| 52 | tic: cute | |
| 53 | Honorific: Doctor | |
| 54 | vocation: | |
| 55 | article: an | |
| 56 | name: oceanography | |
| 57 | title: oceanographer | |
| 58 | employer: | |
| 59 | name: | |
| 60 | Full: Oregon State University | |
| 61 | Short: OSU | |
| 62 | eyes: | |
| 63 | colour: blue | |
| 64 | hair: | |
| 65 | style: thick, curly | |
| 66 | colour: dark brown | |
| 67 | Endear: Mom | |
| 68 | Endear_pos: Mom's | |
| 69 | uncle: | |
| 70 | name: | |
| 71 | First: Damian | |
| 72 | First_pos: $c.protagonist.uncle.name.First$'s | |
| 73 | Family: Moros | |
| 74 | hands: | |
| 75 | fingers: | |
| 76 | shape: long, bony | |
| 77 | friend: | |
| 78 | primary: | |
| 79 | name: | |
| 80 | First: Gerard | |
| 81 | First_pos: $c.protagonist.friend.primary.name.First$'s | |
| 82 | Family: Baran | |
| 83 | Family_pos: $c.protagonist.friend.primary.name.Family$'s | |
| 84 | favourite: | |
| 85 | colour: midnight blue | |
| 86 | eyes: | |
| 87 | colour: hazel | |
| 88 | mother: | |
| 89 | name: | |
| 90 | First: Isabella | |
| 91 | Short: Izzy | |
| 92 | Honourific: Mrs. | |
| 93 | father: | |
| 94 | name: | |
| 95 | Short: Mo | |
| 96 | First: Montgomery | |
| 97 | First_pos: $c.protagonist.friend.primary.father.name.First$'s | |
| 98 | Honourific: Mr. | |
| 99 | speech: | |
| 100 | tic: y'know | |
| 101 | endear: Pops | |
| 102 | military: | |
| 103 | primary: | |
| 104 | name: | |
| 105 | First: Felix | |
| 106 | Family: LeMay | |
| 107 | Family_pos: LeMay's | |
| 108 | rank: | |
| 109 | Short: General | |
| 110 | Full: Brigadier $c.military.primary.rank.Short$ | |
| 111 | colour: | |
| 112 | eyes: gray | |
| 113 | hair: dirty brown | |
| 114 | secondary: | |
| 115 | name: | |
| 116 | Family: Grell | |
| 117 | rank: Colonel | |
| 118 | colour: | |
| 119 | eyes: green | |
| 120 | hair: deep red | |
| 121 | quaternary: | |
| 122 | name: | |
| 123 | First: Gretchen | |
| 124 | Family: Steinherz | |
| 125 | minor: | |
| 126 | primary: | |
| 127 | name: | |
| 128 | First: River | |
| 129 | Family: Banks | |
| 130 | Honourific: Mx. | |
| 131 | vocation: | |
| 132 | title: salesperson | |
| 133 | employer: | |
| 134 | Name: Geophysical Prospecting Incorporated | |
| 135 | Abbr: GPI | |
| 136 | Area: Cold Spring Creek | |
| 137 | payment: twenty million | |
| 138 | secondary: | |
| 139 | name: | |
| 140 | First: Renato | |
| 141 | Middle: Carroña | |
| 142 | Family: Salvatierra | |
| 143 | Family_pos: $c.minor.secondary.name.Family$'s | |
| 144 | Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$ | |
| 145 | Honourific: Mister | |
| 146 | Honourific_sp: Señor | |
| 147 | vocation: | |
| 148 | title: detective | |
| 149 | tertiary: | |
| 150 | name: | |
| 151 | First: Robert | |
| 152 | Family: Hanssen | |
| 153 | ||
| 154 | ai: | |
| 155 | protagonist: | |
| 156 | name: | |
| 157 | first: yoky | |
| 158 | First: Yoky | |
| 159 | First_pos: $c.ai.protagonist.name.First$'s | |
| 160 | Family: Tsukuda | |
| 161 | id: 46692 | |
| 162 | persona: | |
| 163 | name: | |
| 164 | First: Hoshi | |
| 165 | First_pos: $c.ai.protagonist.persona.name.First$'s | |
| 166 | Family: Yamamoto | |
| 167 | Family_pos: $c.ai.protagonist.persona.name.Family$'s | |
| 168 | culture: Japanese-American | |
| 169 | ethnicity: Asian | |
| 170 | rank: Technical Sergeant | |
| 171 | speech: | |
| 172 | tic: okay | |
| 173 | first: | |
| 174 | Name: Prôtos | |
| 175 | Name_pos: Prôtos' | |
| 176 | age: | |
| 177 | actual: twenty-six weeks | |
| 178 | virtual: five years | |
| 179 | second: | |
| 180 | Name: Défteros | |
| 181 | third: | |
| 182 | Name: Trítos | |
| 183 | fourth: | |
| 184 | Name: Tétartos | |
| 185 | material: | |
| 186 | type: metal | |
| 187 | raw: ilmenite | |
| 188 | extract: ore | |
| 189 | name: | |
| 190 | short: titanium | |
| 191 | long: $c.ai.material.name.short$ dioxide | |
| 192 | Abbr: TiO~2~ | |
| 193 | pejorative: tin | |
| 194 | animal: | |
| 195 | protagonist: | |
| 196 | Name: Trufflers | |
| 197 | type: pig | |
| 198 | antagonist: | |
| 199 | name: coywolf | |
| 200 | Name: Coywolf | |
| 201 | plural: coywolves | |
| 202 | ||
| 203 | narrator: | |
| 204 | one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$) | |
| 205 | two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$) | |
| 206 | ||
| 207 | military: | |
| 208 | name: | |
| 209 | Short: Agency | |
| 210 | Short_pos: $military.name.Short$'s | |
| 211 | plural: agencies | |
| 212 | machine: | |
| 213 | Name: Skopós | |
| 214 | Name_pos: $military.machine.Name$' | |
| 215 | Location: Arctic | |
| 216 | predictor: quantum chips | |
| 217 | land: | |
| 218 | name: | |
| 219 | Full: $military.name.Short$ of Defence | |
| 220 | Slogan: Safety in Numbers | |
| 221 | air: | |
| 222 | name: | |
| 223 | Full: $military.name.Short$ of Air | |
| 224 | compound: | |
| 225 | type: base | |
| 226 | lights: | |
| 227 | colour: blue | |
| 228 | nick: | |
| 229 | Prefix: Catacombs | |
| 230 | prep: of | |
| 231 | Suffix: Tartarus | |
| 232 | ||
| 233 | government: | |
| 234 | Country: United States | |
| 235 | ||
| 236 | location: | |
| 237 | protagonist: | |
| 238 | City: Corvallis | |
| 239 | Region: Oregon | |
| 240 | Geography: Willamette Valley | |
| 241 | secondary: | |
| 242 | City: Willow Branch Spring | |
| 243 | Region: Oregon | |
| 244 | Geography: Wheeler County | |
| 245 | Water: Clarno Rapids | |
| 246 | Road: Shaniko-Fossil Highway | |
| 247 | tertiary: | |
| 248 | City: Leavenworth | |
| 249 | Region: Washington | |
| 250 | Type: Bavarian village | |
| 251 | school: | |
| 252 | address: 1400 Northwest Buchanan Avenue | |
| 253 | hospital: | |
| 254 | Name: Good Samaritan Regional Medical Center | |
| 255 | ai: | |
| 256 | escape: | |
| 257 | country: | |
| 258 | Name: Ecuador | |
| 259 | Name_pos: Ecuador's | |
| 260 | mountain: | |
| 261 | Name: Chimborazo | |
| 262 | ||
| 263 | language: | |
| 264 | ai: | |
| 265 | article: an | |
| 266 | singular: exanimis | |
| 267 | plural: exanimēs | |
| 268 | brain: | |
| 269 | singular: superum | |
| 270 | plural: supera | |
| 271 | title: memristor array | |
| 272 | Title: Memristor Array | |
| 273 | police: | |
| 274 | slang: | |
| 275 | singular: mippo | |
| 276 | plural: $language.police.slang.singular$s | |
| 277 | ||
| 278 | date: | |
| 279 | anchor: 2042-09-02 | |
| 280 | protagonist: | |
| 281 | born: 0 | |
| 282 | conceived: -243 | |
| 283 | attacked: | |
| 284 | first: 2192 | |
| 285 | second: 8064 | |
| 286 | father: | |
| 287 | attacked: | |
| 288 | first: -8205 | |
| 289 | date: | |
| 290 | second: -1550 | |
| 291 | family: | |
| 292 | moved: | |
| 293 | first: $date.protagonist.conceived$ + 35 | |
| 294 | game: | |
| 295 | played: | |
| 296 | first: $date.protagonist.born$ - 672 | |
| 297 | second: $date.protagonist.family.moved.first$ + 2 | |
| 298 | ai: | |
| 299 | interviewed: 6198 | |
| 300 | onboarded: $date.ai.interviewed$ + 290 | |
| 301 | diagnosed: $date.ai.onboarded$ + 2 | |
| 302 | resigned: $date.ai.diagnosed$ + 3 | |
| 303 | trapped: $date.ai.resigned$ + 26 | |
| 304 | torturer: $date.ai.trapped$ + 18 | |
| 305 | memristor: $date.ai.torturer$ + 61 | |
| 306 | ethics: $date.ai.memristor$ + 415 | |
| 307 | trained: $date.ai.ethics$ + 385 | |
| 308 | mindjacked: $date.ai.trained$ + 22 | |
| 309 | bombed: $date.ai.mindjacked$ + 458 | |
| 310 | military: | |
| 311 | machine: | |
| 312 | Construction: Six years | |
| 313 | ||
| 314 | plot: | |
| 315 | Log: $c.ai.protagonist.name.First_pos$ Chronicles | |
| 316 | Channel: Quantum Channel | |
| 317 | ||
| 318 | device: | |
| 319 | computer: | |
| 320 | Name: Tau | |
| 321 | network: | |
| 322 | Name: Internet | |
| 323 | paper: | |
| 324 | name: | |
| 325 | full: electronic sheet | |
| 326 | short: sheet | |
| 327 | typewriter: | |
| 328 | Name: Underwood | |
| 329 | year: nineteen twenties | |
| 330 | room: root cellar | |
| 331 | portable: | |
| 332 | name: nanobook | |
| 333 | vehicle: | |
| 334 | name: robocars | |
| 335 | Name: Robocars | |
| 336 | sensor: | |
| 337 | name: BMP1580 | |
| 338 | phone: | |
| 339 | name: comm | |
| 340 | name_pos: $plot.device.phone.name$'s | |
| 341 | Name: Comm | |
| 342 | plural: $plot.device.phone.name$s | |
| 343 | video: | |
| 344 | name: vidfeed | |
| 345 | plural: $plot.device.video.name$s | |
| 346 | game: | |
| 347 | Name: Psynæris | |
| 348 | thought: transed | |
| 349 | machine: telecognos | |
| 350 | location: | |
| 351 | Building: Nijō Castle | |
| 352 | District: Gion | |
| 353 | City: Kyoto | |
| 354 | Country: Japan | |
| 355 | ||
| 356 | farm: | |
| 357 | population: | |
| 358 | estimate: 350 | |
| 359 | actual: 1,000 | |
| 360 | energy: 9800kJ | |
| 361 | width: 55m | |
| 362 | length: 55m | |
| 363 | storeys: 10 | |
| 364 | ||
| 365 | lamp: | |
| 366 | height: 0.17m | |
| 367 | length: 1.22m | |
| 368 | width: 0.28m | |
| 369 | ||
| 370 | crop: | |
| 371 | name: | |
| 372 | singular: tomato | |
| 373 | plural: $crop.name.singular$es | |
| 374 | energy: 318kJ | |
| 375 | weight: 450g | |
| 376 | yield: 50 | |
| 377 | harvests: 7 | |
| 378 | diameter: 2m | |
| 379 | height: 1.5m | |
| 380 | ||
| 381 | heading: | |
| 382 | ch_01: Till | |
| 383 | ch_02: Sow | |
| 384 | ch_03: Seed | |
| 385 | ch_04: Germinate | |
| 386 | ch_05: Grow | |
| 387 | ch_06: Shoot | |
| 388 | ch_07: Bud | |
| 389 | ch_08: Bloom | |
| 390 | ch_09: Pollinate | |
| 391 | ch_10: Fruit | |
| 392 | ch_11: Harvest | |
| 393 | ch_12: Deliver | |
| 394 | ch_13: Spoil | |
| 395 | ch_14: Revolt | |
| 396 | ch_15: Compost | |
| 397 | ch_16: Burn | |
| 398 | ch_17: Release | |
| 399 | ch_18: End Notes | |
| 400 | ch_19: Characters | |
| 401 | ||
| 402 | inference: | |
| 403 | unit: per cent | |
| 404 | min: two | |
| 405 | ch_sow: eighty | |
| 406 | ch_seed: fifty-two | |
| 407 | ch_germinate: thirty-one | |
| 408 | ch_grow: fifteen | |
| 409 | ch_shoot: seven | |
| 410 | ch_bloom: four | |
| 411 | ch_pollinate: two | |
| 412 | ch_harvest: ninety-five | |
| 413 | ch_delivery: ninety-eight | |
| 414 | ||
| 415 | link: | |
| 416 | tartarus: https://en.wikipedia.org/wiki/Tartarus | |
| 417 | exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls | |
| 418 | atalanta: https://en.wikipedia.org/wiki/Atalanta | |
| 419 | detain: https://goo.gl/RCNuOQ | |
| 420 | ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics | |
| 421 | algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon | |
| 422 | holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust | |
| 423 | memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf | |
| 424 | surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487 | |
| 425 | tor: https://www.torproject.org | |
| 426 | hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra | |
| 427 | foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134 | |
| 428 | drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist | |
| 429 | fermi: https://arxiv.org/pdf/1404.0204v1.pdf | |
| 430 | face: https://www.youtube.com/watch?v=ladqJQLR2bA | |
| 431 | expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures | |
| 432 | governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531 | |
| 433 | asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics | |
| 434 | clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws | |
| 435 | jetpack: http://jetpackaviation.com/ | |
| 436 | hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ | |
| 437 | eyes_five: https://en.wikipedia.org/wiki/Five_Eyes | |
| 438 | eyes_nine: https://www.privacytools.io/ | |
| 439 | eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html | |
| 440 | tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml | |
| 441 | ||
| 442 | 1 |
| 1 | #!/usr/bin/env bash | |
| 2 | ||
| 3 | # Writes the name for all OTF files found in the current directory or lower | |
| 4 | ||
| 5 | find . -type f \( -name "*otf" -o -name "*ttf" \) -exec \ | |
| 6 | fc-scan --format "%{foundry}: %{family}\n" {} \; | uniq | sort | |
| 7 | ||
| 1 | 8 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.definition; | |
| 3 | ||
| 4 | import com.keenwrite.editors.definition.DefinitionEditor; | |
| 5 | import com.keenwrite.editors.definition.yaml.YamlTreeTransformer; | |
| 6 | import com.keenwrite.editors.markdown.MarkdownEditor; | |
| 7 | import com.keenwrite.preferences.Workspace; | |
| 8 | import com.keenwrite.preview.HtmlPreview; | |
| 9 | import com.panemu.tiwulfx.control.dock.DetachableTabPane; | |
| 10 | import javafx.application.Application; | |
| 11 | import javafx.beans.property.SimpleObjectProperty; | |
| 12 | import javafx.event.Event; | |
| 13 | import javafx.event.EventHandler; | |
| 14 | import javafx.scene.Node; | |
| 15 | import javafx.scene.Scene; | |
| 16 | import javafx.scene.control.ColorPicker; | |
| 17 | import javafx.scene.control.SplitPane; | |
| 18 | import javafx.scene.control.Tooltip; | |
| 19 | import javafx.scene.control.TreeItem; | |
| 20 | import javafx.stage.Stage; | |
| 21 | import org.testfx.framework.junit5.Start; | |
| 22 | ||
| 23 | import static com.keenwrite.util.FontLoader.initFonts; | |
| 24 | ||
| 25 | //@ExtendWith(ApplicationExtension.class) | |
| 26 | public class TreeViewTest extends Application { | |
| 27 | private final SimpleObjectProperty<Node> mTextEditor = | |
| 28 | new SimpleObjectProperty<>(); | |
| 29 | ||
| 30 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> mTreeHandler = | |
| 31 | event -> refresh( mTextEditor.get() ); | |
| 32 | ||
| 33 | private void refresh( final Node node ) { | |
| 34 | throw new RuntimeException( "Derp: " + node ); | |
| 35 | } | |
| 36 | ||
| 37 | public static void main( final String[] args ) { | |
| 38 | initFonts(); | |
| 39 | launch( args ); | |
| 40 | } | |
| 41 | ||
| 42 | public void start( final Stage stage ) { | |
| 43 | onStart( stage ); | |
| 44 | } | |
| 45 | ||
| 46 | @Start | |
| 47 | private void onStart( final Stage stage ) { | |
| 48 | final var workspace = new Workspace(); | |
| 49 | final var mainPane = new SplitPane(); | |
| 50 | ||
| 51 | final var transformer = new YamlTreeTransformer(); | |
| 52 | final var editor = new DefinitionEditor( transformer ); | |
| 53 | ||
| 54 | final var tabPane1 = new DetachableTabPane(); | |
| 55 | tabPane1.addTab( "Editor", editor ); | |
| 56 | ||
| 57 | final var tabPane2 = new DetachableTabPane(); | |
| 58 | final var tab21 = tabPane2.addTab( "Picker", new ColorPicker() ); | |
| 59 | final var tab22 = tabPane2.addTab( "Editor", | |
| 60 | new MarkdownEditor( workspace ) ); | |
| 61 | tab21.setTooltip( new Tooltip( "Colour Picker" ) ); | |
| 62 | tab22.setTooltip( new Tooltip( "Text Editor" ) ); | |
| 63 | ||
| 64 | final var tabPane3 = new DetachableTabPane(); | |
| 65 | tabPane3.addTab( "Preview", new HtmlPreview( workspace ) ); | |
| 66 | ||
| 67 | editor.addTreeChangeHandler( mTreeHandler ); | |
| 68 | ||
| 69 | mainPane.getItems().addAll( tabPane1, tabPane2, tabPane3 ); | |
| 70 | ||
| 71 | final var scene = new Scene( mainPane ); | |
| 72 | stage.setScene( scene ); | |
| 73 | ||
| 74 | stage.show(); | |
| 75 | } | |
| 76 | } | |
| 1 | 77 |
| 1 | package com.keenwrite.editors.markdown; | |
| 2 | ||
| 3 | import com.keenwrite.preferences.Workspace; | |
| 4 | import org.junit.jupiter.api.Test; | |
| 5 | import org.junit.jupiter.api.extension.ExtendWith; | |
| 6 | import org.testfx.framework.junit5.ApplicationExtension; | |
| 7 | ||
| 8 | import java.util.regex.Pattern; | |
| 9 | ||
| 10 | import static java.util.regex.Pattern.compile; | |
| 11 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 12 | import static org.junit.jupiter.api.Assertions.assertTrue; | |
| 13 | ||
| 14 | @ExtendWith( ApplicationExtension.class ) | |
| 15 | public class MarkdownEditorTest { | |
| 16 | private static final String[] WORDS = new String[]{ | |
| 17 | "Italicize", | |
| 18 | "English's", | |
| 19 | "foreign", | |
| 20 | "words", | |
| 21 | "based", | |
| 22 | "on", | |
| 23 | "popularity,", | |
| 24 | "like", | |
| 25 | "_bête_", | |
| 26 | "_noire_", | |
| 27 | "and", | |
| 28 | "_Weltanschauung_", | |
| 29 | "but", | |
| 30 | "not", | |
| 31 | "résumé.", | |
| 32 | "Don't", | |
| 33 | "omit", | |
| 34 | "accented", | |
| 35 | "characters!", | |
| 36 | "Cœlacanthe", | |
| 37 | "L'Haÿ-les-Roses", | |
| 38 | "Mühlfeldstraße", | |
| 39 | "Da̱nx̱a̱laga̱litła̱n", | |
| 40 | }; | |
| 41 | ||
| 42 | private static final String TEXT = String.join( " ", WORDS ); | |
| 43 | ||
| 44 | private static final Pattern REGEX = compile( | |
| 45 | "[^\\p{Mn}\\p{Me}\\p{L}\\p{N}'-]+" ); | |
| 46 | ||
| 47 | /** | |
| 48 | * Test that the {@link MarkdownEditor} can retrieve a word at the caret | |
| 49 | * position, regardless of whether the caret is at the beginning, middle, or | |
| 50 | * end of the word. | |
| 51 | */ | |
| 52 | @Test | |
| 53 | public void test_CaretWord_GetISO88591Word_WordSelected() { | |
| 54 | final var editor = createMarkdownEditor(); | |
| 55 | ||
| 56 | for( int i = 0; i < WORDS.length; i++ ) { | |
| 57 | final var word = WORDS[ i ]; | |
| 58 | final var len = word.length(); | |
| 59 | final var expected = REGEX.matcher( word ).replaceAll( "" ); | |
| 60 | ||
| 61 | for( int j = 0; j < len; j++ ) { | |
| 62 | editor.moveTo( offset( i ) + j ); | |
| 63 | final var actual = editor.getCaretWordText(); | |
| 64 | assertEquals( expected, actual ); | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Test that the {@link MarkdownEditor} can make a word bold. | |
| 71 | */ | |
| 72 | @Test | |
| 73 | public void test_CaretWord_SetWordBold_WordIsBold() { | |
| 74 | final var index = 20; | |
| 75 | final var editor = createMarkdownEditor(); | |
| 76 | ||
| 77 | editor.moveTo( offset( index ) ); | |
| 78 | editor.bold(); | |
| 79 | assertTrue( editor.getText().contains( "**" + WORDS[ index ] + "**" ) ); | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Returns the document offset for a string at the given index. | |
| 84 | */ | |
| 85 | private static int offset( final int index ) { | |
| 86 | assert 0 <= index && index < WORDS.length; | |
| 87 | int offset = 0; | |
| 88 | ||
| 89 | for( int i = 0; i < index; i++ ) { | |
| 90 | offset += WORDS[ i ].length(); | |
| 91 | } | |
| 92 | ||
| 93 | // Add the index to compensate for one space between words. | |
| 94 | return offset + index; | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Returns an instance of {@link MarkdownEditor} pre-populated with | |
| 99 | * {@link #TEXT}. | |
| 100 | * | |
| 101 | * @return A new {@link MarkdownEditor} instance, ready for unit tests. | |
| 102 | */ | |
| 103 | private MarkdownEditor createMarkdownEditor() { | |
| 104 | final var workspace = new Workspace(); | |
| 105 | final var editor = new MarkdownEditor( workspace ); | |
| 106 | editor.setText( TEXT ); | |
| 107 | return editor; | |
| 108 | } | |
| 109 | } | |
| 1 | 110 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.io; | |
| 3 | ||
| 4 | import org.junit.jupiter.api.Test; | |
| 5 | ||
| 6 | import java.net.URI; | |
| 7 | import java.util.Map; | |
| 8 | ||
| 9 | import static com.keenwrite.io.MediaType.*; | |
| 10 | import static org.junit.jupiter.api.Assertions.*; | |
| 11 | ||
| 12 | /** | |
| 13 | * Test that {@link MediaType} instances can be queried and return reliable | |
| 14 | * results. | |
| 15 | */ | |
| 16 | public class MediaTypeTest { | |
| 17 | /** | |
| 18 | * Test that {@link MediaType#equals(String, String)} is case insensitive. | |
| 19 | */ | |
| 20 | @Test | |
| 21 | public void test_Equality_IgnoreCase_Success() { | |
| 22 | final var mediaType = TEXT_PLAIN; | |
| 23 | assertTrue( mediaType.equals( "TeXt", "Plain" ) ); | |
| 24 | assertEquals( "text/plain", mediaType.toString() ); | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Test that {@link MediaType#valueFrom(String)} can lookup by file name. | |
| 29 | */ | |
| 30 | @Test | |
| 31 | public void test_FilenameExtensions_Supported_Success() { | |
| 32 | final var map = Map.of( | |
| 33 | "jpeg", IMAGE_JPEG, | |
| 34 | "png", IMAGE_PNG, | |
| 35 | "svg", IMAGE_SVG_XML, | |
| 36 | "md", TEXT_MARKDOWN, | |
| 37 | "Rmd", TEXT_R_MARKDOWN, | |
| 38 | "Rxml", TEXT_R_XML, | |
| 39 | "txt", TEXT_PLAIN, | |
| 40 | "yml", TEXT_YAML | |
| 41 | ); | |
| 42 | ||
| 43 | map.forEach( ( k, v ) -> assertEquals( v, valueFrom( "f." + k ) ) ); | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * Test that {@link HttpMediaType#valueFrom(URI)} will pull and identify the | |
| 48 | * type of resource based on the HTTP Content-Type header. | |
| 49 | */ | |
| 50 | @Test | |
| 51 | public void test_HttpRequest_Supported_Success() { | |
| 52 | //@formatter:off | |
| 53 | final var map = Map.of( | |
| 54 | "https://stackoverflow.com/robots.txt", TEXT_PLAIN, | |
| 55 | "https://placekitten.com/g/400/400", IMAGE_JPEG, | |
| 56 | "https://upload.wikimedia.org/wikipedia/commons/9/9f/Vimlogo.svg", IMAGE_SVG_XML, | |
| 57 | "https://kroki.io//graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", TEXT_PLAIN | |
| 58 | ); | |
| 59 | //@formatter:on | |
| 60 | ||
| 61 | map.forEach( ( k, v ) -> { | |
| 62 | try { | |
| 63 | assertEquals( v, HttpMediaType.valueFrom( new URI( k ) ) ); | |
| 64 | } catch( Exception e ) { | |
| 65 | fail(); | |
| 66 | } | |
| 67 | } ); | |
| 68 | } | |
| 69 | } | |
| 1 | 70 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.markdown; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 6 | import com.vladsch.flexmark.parser.Parser; | |
| 7 | import org.junit.jupiter.api.Test; | |
| 8 | import org.junit.jupiter.api.extension.ExtendWith; | |
| 9 | import org.testfx.framework.junit5.ApplicationExtension; | |
| 10 | ||
| 11 | import java.io.File; | |
| 12 | import java.net.URISyntaxException; | |
| 13 | import java.net.URL; | |
| 14 | import java.nio.file.Path; | |
| 15 | import java.nio.file.Paths; | |
| 16 | import java.util.HashMap; | |
| 17 | import java.util.List; | |
| 18 | import java.util.Map; | |
| 19 | ||
| 20 | import static java.lang.String.format; | |
| 21 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 22 | import static org.junit.jupiter.api.Assertions.assertNotNull; | |
| 23 | ||
| 24 | /** | |
| 25 | * Responsible for testing that linked images render into HTML according to | |
| 26 | * the {@link ImageLinkExtension} rules. | |
| 27 | */ | |
| 28 | @ExtendWith( ApplicationExtension.class ) | |
| 29 | @SuppressWarnings( "SameParameterValue" ) | |
| 30 | public class ImageLinkExtensionTest { | |
| 31 | ||
| 32 | private static final Map<String, String> IMAGES = new HashMap<>(); | |
| 33 | ||
| 34 | private static final String URI_WEB = "placekitten.com/200/200"; | |
| 35 | private static final String URI_DIRNAME = "images"; | |
| 36 | private static final String URI_FILENAME = "kitten"; | |
| 37 | ||
| 38 | /** | |
| 39 | * Path to use for testing image file name resolution. Note that resources use | |
| 40 | * forward slashes, regardless of OS. | |
| 41 | */ | |
| 42 | private static final String URI_PATH = URI_DIRNAME + '/' + URI_FILENAME; | |
| 43 | ||
| 44 | /** | |
| 45 | * Extension for the first existing image that matches the preferred image | |
| 46 | * extension order. | |
| 47 | */ | |
| 48 | private static final String URI_IMAGE_EXT = ".png"; | |
| 49 | ||
| 50 | /** | |
| 51 | * Relative path to an image that exists. | |
| 52 | */ | |
| 53 | private static final String URI_IMAGE = URI_PATH + URI_IMAGE_EXT; | |
| 54 | ||
| 55 | static { | |
| 56 | addUri( URI_PATH + ".png" ); | |
| 57 | addUri( URI_PATH + ".jpg" ); | |
| 58 | addUri( URI_PATH, URI_PATH + URI_IMAGE_EXT ); | |
| 59 | addUri( "//" + URI_WEB ); | |
| 60 | addUri( "http://" + URI_WEB ); | |
| 61 | addUri( "https://" + URI_WEB ); | |
| 62 | } | |
| 63 | ||
| 64 | private static void addUri( final String uri ) { | |
| 65 | addUri( uri, uri ); | |
| 66 | } | |
| 67 | ||
| 68 | private static void addUri( final String uriKey, final String uriValue ) { | |
| 69 | IMAGES.put( toMd( uriKey ), toHtml( uriValue ) ); | |
| 70 | } | |
| 71 | ||
| 72 | private static String toMd( final String file ) { | |
| 73 | return format( "", file ); | |
| 74 | } | |
| 75 | ||
| 76 | private static String toHtml( final String file ) { | |
| 77 | return format( | |
| 78 | "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", file ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Test that the key URIs present in the {@link #IMAGES} map are rendered | |
| 83 | * as the value URIs present in the same map. | |
| 84 | */ | |
| 85 | @Test | |
| 86 | void test_LocalImage_RelativePathWithExtension_ResolvedSuccessfully() | |
| 87 | throws URISyntaxException { | |
| 88 | final var workspace = new Workspace(); | |
| 89 | final var resource = getPathResource( URI_IMAGE ); | |
| 90 | final var imagePath = new File( URI_IMAGE ).toPath(); | |
| 91 | final var subpaths = resource.getNameCount() - imagePath.getNameCount(); | |
| 92 | final var subpath = resource.subpath( 0, subpaths ); | |
| 93 | ||
| 94 | // The root component isn't considered part of the path, so add it back. | |
| 95 | final var path = resource.getRoot().resolve( subpath ); | |
| 96 | ||
| 97 | final var extension = ImageLinkExtension.create( path, workspace ); | |
| 98 | final var extensions = List.of( extension ); | |
| 99 | final var pBuilder = Parser.builder(); | |
| 100 | final var hBuilder = HtmlRenderer.builder(); | |
| 101 | final var parser = pBuilder.extensions( extensions ).build(); | |
| 102 | final var renderer = hBuilder.extensions( extensions ).build(); | |
| 103 | ||
| 104 | assertNotNull( parser ); | |
| 105 | assertNotNull( renderer ); | |
| 106 | ||
| 107 | // Set a default (fallback) image directory search location. | |
| 108 | //getInstance().imagesDirectoryProperty().setValue( new File( "." ) ); | |
| 109 | ||
| 110 | for( final var entry : IMAGES.entrySet() ) { | |
| 111 | final var key = entry.getKey(); | |
| 112 | final var node = parser.parse( key ); | |
| 113 | final var expectedHtml = entry.getValue(); | |
| 114 | final var actualHtml = renderer.render( node ); | |
| 115 | ||
| 116 | assertEquals( expectedHtml, actualHtml ); | |
| 117 | } | |
| 118 | } | |
| 119 | ||
| 120 | private Path getPathResource( final String path ) | |
| 121 | throws URISyntaxException { | |
| 122 | final var url = getResource( path ); | |
| 123 | assert url != null; | |
| 124 | ||
| 125 | final var uri = url.toURI(); | |
| 126 | return Paths.get( uri ); | |
| 127 | } | |
| 128 | ||
| 129 | private URL getResource( final String path ) { | |
| 130 | final var packagePath = getClass().getPackageName().replace( '.', '/' ); | |
| 131 | final var resourcePath = '/' + packagePath + '/' + path; | |
| 132 | return getClass().getResource( resourcePath ); | |
| 133 | } | |
| 134 | } | |
| 1 | 135 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | 2 | package com.keenwrite.r; |
| 2 | 3 |
| 1 | /* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.util; | |
| 3 | ||
| 4 | import org.junit.jupiter.api.Test; | |
| 5 | ||
| 6 | import java.util.List; | |
| 7 | import java.util.ListIterator; | |
| 8 | import java.util.NoSuchElementException; | |
| 9 | ||
| 10 | import static org.junit.jupiter.api.Assertions.*; | |
| 11 | ||
| 12 | /** | |
| 13 | * Tests the {@link CyclicIterator} class. | |
| 14 | */ | |
| 15 | public class CyclicIteratorTest { | |
| 16 | /** | |
| 17 | * Test that the {@link CyclicIterator} can move forwards and backwards | |
| 18 | * through a {@link List}. | |
| 19 | */ | |
| 20 | @Test | |
| 21 | public void test_Directions_NextPreviousCycles_Success() { | |
| 22 | final var list = List.of( 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ); | |
| 23 | final var iterator = createCyclicIterator( list ); | |
| 24 | ||
| 25 | // Test forwards through the iterator. | |
| 26 | for( int i = 0; i < list.size(); i++ ) { | |
| 27 | assertTrue( iterator.hasNext() ); | |
| 28 | assertEquals( i, iterator.next() ); | |
| 29 | } | |
| 30 | ||
| 31 | // Loop to the first item. | |
| 32 | iterator.next(); | |
| 33 | ||
| 34 | // Test backwards through the iterator. | |
| 35 | for( int i = list.size() - 1; i >= 0; i-- ) { | |
| 36 | assertTrue( iterator.hasPrevious() ); | |
| 37 | assertEquals( i, iterator.previous() ); | |
| 38 | } | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Test that the {@link CyclicIterator} returns the last element when | |
| 43 | * the very first API call is to {@link ListIterator#previous()}. | |
| 44 | */ | |
| 45 | @Test | |
| 46 | public void test_Direction_FirstPrevious_ReturnsLastElement() { | |
| 47 | final var list = List.of( 1, 2, 3, 4, 5, 6, 7 ); | |
| 48 | final var iterator = createCyclicIterator( list ); | |
| 49 | ||
| 50 | assertEquals( iterator.previous(), list.get( list.size() - 1 ) ); | |
| 51 | } | |
| 52 | ||
| 53 | @Test | |
| 54 | public void test_Empty_Next_Exception() { | |
| 55 | final var iterator = createCyclicIterator( List.of() ); | |
| 56 | assertThrows( NoSuchElementException.class, iterator::next ); | |
| 57 | } | |
| 58 | ||
| 59 | @Test | |
| 60 | public void test_Empty_Previous_Exception() { | |
| 61 | final var iterator = createCyclicIterator( List.of() ); | |
| 62 | assertThrows( NoSuchElementException.class, iterator::previous ); | |
| 63 | } | |
| 64 | ||
| 65 | private <T> CyclicIterator<T> createCyclicIterator( final List<T> list ) { | |
| 66 | return new CyclicIterator<>( list ); | |
| 67 | } | |
| 68 | } | |
| 1 | 69 |