Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .idea/compiler.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="CompilerConfiguration">
4
    <bytecodeTargetLevel target="11" />
5
  </component>
6
</project>
M .idea/gradle.xml
11
<?xml version="1.0" encoding="UTF-8"?>
22
<project version="4">
3
  <component name="GradleMigrationSettings" migrationVersion="1" />
34
  <component name="GradleSettings">
45
    <option name="linkedExternalProjectsSettings">
A .idea/jarRepositories.xml
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>
M .idea/misc.xml
22
<project version="4">
33
  <component name="ExternalStorageConfigurationManager" enabled="true" />
4
  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK1.8" project-jdk-type="JavaSDK">
4
  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="JDK1.8" project-jdk-type="JavaSDK">
55
    <output url="file://$PROJECT_DIR$/out" />
66
  </component>
A .idea/rGraphicsSettings.xml
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>
A .idea/rSettings.xml
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>
A .idea/rpackages.xml
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>
A .idea/scrivenvar.iml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<module type="JAVA_MODULE" version="4" />
A .idea/uiDesigner.xml
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>
M .idea/workspace.xml
33
  <component name="ChangeListManager">
44
    <list default="true" id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="">
5
      <change afterPath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" afterDir="false" />
6
      <change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
7
      <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
8
      <change beforePath="$PROJECT_DIR$/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/build.gradle" afterDir="false" />
9
      <change beforePath="$PROJECT_DIR$/executable.txt" beforeDir="false" />
10
      <change beforePath="$PROJECT_DIR$/gradle.properties" beforeDir="false" afterPath="$PROJECT_DIR$/gradle.properties" afterDir="false" />
11
      <change beforePath="$PROJECT_DIR$/libs/renjin-script-engine-0.9.2592-jar-with-dependencies.jar" beforeDir="false" afterPath="$PROJECT_DIR$/libs/renjin-script-engine-0.9.2707-jar-with-dependencies.jar" afterDir="false" />
12
      <change beforePath="$PROJECT_DIR$/licenses/MARKDOWN-WRITER-FX" beforeDir="false" afterPath="$PROJECT_DIR$/licenses/MARKDOWN-WRITER-FX.md" afterDir="false" />
13
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractFileFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractFileFactory.java" afterDir="false" />
14
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractPane.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractPane.java" afterDir="false" />
15
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" afterDir="false" />
16
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTab.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTab.java" afterDir="false" />
17
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java" afterDir="false" />
18
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileType.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileType.java" afterDir="false" />
19
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Main.java" afterDir="false" />
20
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/IdentityProcessor.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/IdentityProcessor.java" afterDir="false" />
21
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" afterDir="false" />
22
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Options.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Options.java" afterDir="false" />
23
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Settings.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Settings.java" afterDir="false" />
24
    </list>
25
    <ignored path="$PROJECT_DIR$/.gradle/" />
26
    <ignored path="$PROJECT_DIR$/build/" />
27
    <ignored path="$PROJECT_DIR$/out/" />
28
    <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
29
    <option name="SHOW_DIALOG" value="false" />
30
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
31
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
32
    <option name="LAST_RESOLUTION" value="IGNORE" />
33
  </component>
34
  <component name="ExternalProjectsData">
35
    <projectState path="$PROJECT_DIR$">
36
      <ProjectState />
37
    </projectState>
38
  </component>
39
  <component name="ExternalProjectsManager">
40
    <system id="GRADLE">
41
      <state>
42
        <task path="$PROJECT_DIR$">
43
          <activation />
44
        </task>
45
        <projects_view />
46
      </state>
47
    </system>
48
  </component>
49
  <component name="FUSProjectUsageTrigger">
50
    <session id="332180848">
51
      <usages-collector id="statistics.lifecycle.project">
52
        <counts>
53
          <entry key="project.open.time.2" value="1" />
54
          <entry key="project.opened" value="1" />
55
        </counts>
56
      </usages-collector>
57
      <usages-collector id="statistics.file.extensions.open">
58
        <counts>
59
          <entry key="java" value="17" />
60
        </counts>
61
      </usages-collector>
62
      <usages-collector id="statistics.file.types.open">
63
        <counts>
64
          <entry key="JAVA" value="17" />
65
        </counts>
66
      </usages-collector>
67
      <usages-collector id="statistics.file.extensions.edit">
68
        <counts>
69
          <entry key="java" value="1995" />
70
          <entry key="txt" value="19" />
71
        </counts>
72
      </usages-collector>
73
      <usages-collector id="statistics.file.types.edit">
74
        <counts>
75
          <entry key="JAVA" value="1995" />
76
          <entry key="PLAIN_TEXT" value="19" />
77
        </counts>
78
      </usages-collector>
79
    </session>
80
  </component>
81
  <component name="FileEditorManager">
82
    <leaf>
83
      <file pinned="false" current-in-tab="false">
84
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/AbstractProcessor.java">
85
          <provider selected="true" editor-type-id="text-editor">
86
            <state relative-caret-position="180">
87
              <caret line="38" column="22" selection-start-line="38" selection-start-column="22" selection-end-line="38" selection-end-column="22" />
88
            </state>
89
          </provider>
90
        </entry>
91
      </file>
92
      <file pinned="false" current-in-tab="false">
93
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java">
94
          <provider selected="true" editor-type-id="text-editor">
95
            <state relative-caret-position="-135">
96
              <caret line="41" column="22" selection-start-line="41" selection-start-column="22" selection-end-line="41" selection-end-column="22" />
97
            </state>
98
          </provider>
99
        </entry>
100
      </file>
101
      <file pinned="false" current-in-tab="false">
102
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java">
103
          <provider selected="true" editor-type-id="text-editor">
104
            <state relative-caret-position="165">
105
              <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
106
            </state>
107
          </provider>
108
        </entry>
109
      </file>
110
      <file pinned="false" current-in-tab="false">
111
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/DefaultVariableProcessor.java">
112
          <provider selected="true" editor-type-id="text-editor">
113
            <state relative-caret-position="-101">
114
              <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
115
            </state>
116
          </provider>
117
        </entry>
118
      </file>
119
      <file pinned="false" current-in-tab="false">
120
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java">
121
          <provider selected="true" editor-type-id="text-editor">
122
            <state relative-caret-position="439">
123
              <caret line="71" lean-forward="true" selection-start-line="71" selection-end-line="71" />
124
            </state>
125
          </provider>
126
        </entry>
127
      </file>
128
      <file pinned="false" current-in-tab="false">
129
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java">
130
          <provider selected="true" editor-type-id="text-editor">
131
            <state relative-caret-position="150">
132
              <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
133
            </state>
134
          </provider>
135
        </entry>
136
      </file>
137
      <file pinned="false" current-in-tab="true">
138
        <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java">
139
          <provider selected="true" editor-type-id="text-editor">
140
            <state relative-caret-position="300">
141
              <caret line="104" selection-start-line="104" selection-end-line="104" />
142
            </state>
143
          </provider>
144
        </entry>
145
      </file>
146
    </leaf>
147
  </component>
148
  <component name="Git.Settings">
149
    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
150
  </component>
151
  <component name="GradleLocalSettings">
152
    <option name="myGradleHomes">
153
      <map>
154
        <entry key="$PROJECT_DIR$" value="/opt/gradle-4.10.2" />
155
      </map>
156
    </option>
157
    <option name="myGradleVersions">
158
      <map>
159
        <entry key="$PROJECT_DIR$" value="4.10.2" />
160
      </map>
161
    </option>
162
    <option name="availableProjects">
163
      <map>
164
        <entry>
165
          <key>
166
            <ExternalProjectPojo>
167
              <option name="name" value="scrivenvar" />
168
              <option name="path" value="$PROJECT_DIR$" />
169
            </ExternalProjectPojo>
170
          </key>
171
          <value>
172
            <list>
173
              <ExternalProjectPojo>
174
                <option name="name" value="scrivenvar" />
175
                <option name="path" value="$PROJECT_DIR$" />
176
              </ExternalProjectPojo>
177
            </list>
178
          </value>
179
        </entry>
180
      </map>
181
    </option>
182
    <option name="availableTasks">
183
      <map>
184
        <entry key="$PROJECT_DIR$">
185
          <value>
186
            <list>
187
              <ExternalTaskPojo>
188
                <option name="description" value="Displays the components produced by root project 'scrivenvar'. [incubating]" />
189
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
190
                <option name="name" value="components" />
191
              </ExternalTaskPojo>
192
              <ExternalTaskPojo>
193
                <option name="description" value="Assembles and tests this project and all projects that depend on it." />
194
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
195
                <option name="name" value="buildDependents" />
196
              </ExternalTaskPojo>
197
              <ExternalTaskPojo>
198
                <option name="description" value="Displays the sub-projects of root project 'scrivenvar'." />
199
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
200
                <option name="name" value="projects" />
201
              </ExternalTaskPojo>
202
              <ExternalTaskPojo>
203
                <option name="description" value="Assembles main classes." />
204
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
205
                <option name="name" value="classes" />
206
              </ExternalTaskPojo>
207
              <ExternalTaskPojo>
208
                <option name="description" value="Displays the dependent components of components in root project 'scrivenvar'. [incubating]" />
209
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
210
                <option name="name" value="dependentComponents" />
211
              </ExternalTaskPojo>
212
              <ExternalTaskPojo>
213
                <option name="description" value="Displays all buildscript dependencies declared in root project 'scrivenvar'." />
214
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
215
                <option name="name" value="buildEnvironment" />
216
              </ExternalTaskPojo>
217
              <ExternalTaskPojo>
218
                <option name="description" value="Runs this project as a JVM application" />
219
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
220
                <option name="name" value="run" />
221
              </ExternalTaskPojo>
222
              <ExternalTaskPojo>
223
                <option name="description" value="Generates Gradle wrapper files." />
224
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
225
                <option name="name" value="wrapper" />
226
              </ExternalTaskPojo>
227
              <ExternalTaskPojo>
228
                <option name="description" value="Assembles test classes." />
229
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
230
                <option name="name" value="testClasses" />
231
              </ExternalTaskPojo>
232
              <ExternalTaskPojo>
233
                <option name="description" value="Generates Javadoc API documentation for the main source code." />
234
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
235
                <option name="name" value="javadoc" />
236
              </ExternalTaskPojo>
237
              <ExternalTaskPojo>
238
                <option name="description" value="Creates OS specific scripts to run the project as a JVM application." />
239
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
240
                <option name="name" value="startScripts" />
241
              </ExternalTaskPojo>
242
              <ExternalTaskPojo>
243
                <option name="description" value="Assembles a jar archive containing the main classes." />
244
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
245
                <option name="name" value="jar" />
246
              </ExternalTaskPojo>
247
              <ExternalTaskPojo>
248
                <option name="description" value="Displays the configuration model of root project 'scrivenvar'. [incubating]" />
249
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
250
                <option name="name" value="model" />
251
              </ExternalTaskPojo>
252
              <ExternalTaskPojo>
253
                <option name="description" value="Installs the project as a distribution as-is." />
254
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
255
                <option name="name" value="installDist" />
256
              </ExternalTaskPojo>
257
              <ExternalTaskPojo>
258
                <option name="description" value="Processes main resources." />
259
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
260
                <option name="name" value="processResources" />
261
              </ExternalTaskPojo>
262
              <ExternalTaskPojo>
263
                <option name="description" value="Displays the tasks runnable from root project 'scrivenvar'." />
264
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
265
                <option name="name" value="tasks" />
266
              </ExternalTaskPojo>
267
              <ExternalTaskPojo>
268
                <option name="description" value="Assembles the main distributions" />
269
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
270
                <option name="name" value="assembleDist" />
271
              </ExternalTaskPojo>
272
              <ExternalTaskPojo>
273
                <option name="description" value="Initializes a new Gradle build." />
274
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
275
                <option name="name" value="init" />
276
              </ExternalTaskPojo>
277
              <ExternalTaskPojo>
278
                <option name="description" value="Runs the unit tests." />
279
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
280
                <option name="name" value="test" />
281
              </ExternalTaskPojo>
282
              <ExternalTaskPojo>
283
                <option name="description" value="Compiles main Java source." />
284
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
285
                <option name="name" value="compileJava" />
286
              </ExternalTaskPojo>
287
              <ExternalTaskPojo>
288
                <option name="description" value="Displays the insight into a specific dependency in root project 'scrivenvar'." />
289
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
290
                <option name="name" value="dependencyInsight" />
291
              </ExternalTaskPojo>
292
              <ExternalTaskPojo>
293
                <option name="description" value="Runs all checks." />
294
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
295
                <option name="name" value="check" />
296
              </ExternalTaskPojo>
297
              <ExternalTaskPojo>
298
                <option name="description" value="Assembles the outputs of this project." />
299
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
300
                <option name="name" value="assemble" />
301
              </ExternalTaskPojo>
302
              <ExternalTaskPojo>
303
                <option name="description" value="Deletes the build directory." />
304
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
305
                <option name="name" value="clean" />
306
              </ExternalTaskPojo>
307
              <ExternalTaskPojo>
308
                <option name="description" value="Compiles test Java source." />
309
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
310
                <option name="name" value="compileTestJava" />
311
              </ExternalTaskPojo>
312
              <ExternalTaskPojo>
313
                <option name="description" value="Displays all dependencies declared in root project 'scrivenvar'." />
314
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
315
                <option name="name" value="dependencies" />
316
              </ExternalTaskPojo>
317
              <ExternalTaskPojo>
318
                <option name="description" value="Processes test resources." />
319
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
320
                <option name="name" value="processTestResources" />
321
              </ExternalTaskPojo>
322
              <ExternalTaskPojo>
323
                <option name="description" value="Displays a help message." />
324
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
325
                <option name="name" value="help" />
326
              </ExternalTaskPojo>
327
              <ExternalTaskPojo>
328
                <option name="description" value="Assembles and tests this project." />
329
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
330
                <option name="name" value="build" />
331
              </ExternalTaskPojo>
332
              <ExternalTaskPojo>
333
                <option name="description" value="Assembles and tests this project and all projects it depends on." />
334
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
335
                <option name="name" value="buildNeeded" />
336
              </ExternalTaskPojo>
337
              <ExternalTaskPojo>
338
                <option name="description" value="Bundles the project as a distribution." />
339
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
340
                <option name="name" value="distTar" />
341
              </ExternalTaskPojo>
342
              <ExternalTaskPojo>
343
                <option name="description" value="Bundles the project as a distribution." />
344
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
345
                <option name="name" value="distZip" />
346
              </ExternalTaskPojo>
347
              <ExternalTaskPojo>
348
                <option name="description" value="Displays the properties of root project 'scrivenvar'." />
349
                <option name="linkedExternalProjectPath" value="$PROJECT_DIR$" />
350
                <option name="name" value="properties" />
351
              </ExternalTaskPojo>
352
            </list>
353
          </value>
354
        </entry>
355
      </map>
356
    </option>
357
    <option name="modificationStamps">
358
      <map>
359
        <entry key="$PROJECT_DIR$" value="6106128056000" />
360
        <entry key="$PROJECT_DIR$/.gradle" value="0" />
361
        <entry key="$PROJECT_DIR$/build.gradle" value="3956035851" />
362
        <entry key="$PROJECT_DIR$/gradle.properties" value="4254081835" />
363
        <entry key="$PROJECT_DIR$/settings.gradle" value="0" />
364
      </map>
365
    </option>
366
    <option name="projectBuildClasspath">
367
      <map>
368
        <entry key="$PROJECT_DIR$">
369
          <value>
370
            <ExternalProjectBuildClasspathPojo>
371
              <option name="modulesBuildClasspath">
372
                <map>
373
                  <entry key="$PROJECT_DIR$">
374
                    <value>
375
                      <ExternalModuleBuildClasspathPojo>
376
                        <option name="path" value="$PROJECT_DIR$" />
377
                      </ExternalModuleBuildClasspathPojo>
378
                    </value>
379
                  </entry>
380
                </map>
381
              </option>
382
              <option name="name" value="scrivenvar" />
383
              <option name="projectBuildClasspath">
384
                <list>
385
                  <option value="/opt/gradle/lib/gradle-api-metadata-4.10.2.jar" />
386
                  <option value="/opt/gradle/lib/gradle-wrapper-4.10.2.jar" />
387
                  <option value="/opt/gradle/lib/gradle-docs-4.10.2.jar" />
388
                  <option value="/opt/gradle/lib/gradle-kotlin-dsl-1.0-rc-6.jar" />
389
                  <option value="/opt/gradle/lib/gradle-kotlin-dsl-tooling-models-1.0-rc-6.jar" />
390
                  <option value="/opt/gradle/lib/gradle-core-4.10.2.jar" />
391
                  <option value="/opt/gradle/lib/ant-1.9.11.jar" />
392
                  <option value="/opt/gradle/lib/gradle-base-services-4.10.2.jar" />
393
                  <option value="/opt/gradle/lib/ant-launcher-1.9.11.jar" />
394
                  <option value="/opt/gradle/lib/groovy-all-2.4.15.jar" />
395
                  <option value="/opt/gradle/lib/gradle-core-api-4.10.2.jar" />
396
                  <option value="/opt/gradle/lib/gradle-runtime-api-info-4.10.2.jar" />
397
                  <option value="/opt/gradle/lib/gradle-build-option-4.10.2.jar" />
398
                  <option value="/opt/gradle/lib/gradle-base-services-groovy-4.10.2.jar" />
399
                  <option value="/opt/gradle/lib/gradle-persistent-cache-4.10.2.jar" />
400
                  <option value="/opt/gradle/lib/gradle-launcher-4.10.2.jar" />
401
                  <option value="/opt/gradle/lib/gradle-installation-beacon-4.10.2.jar" />
402
                  <option value="/opt/gradle/lib/gradle-cli-4.10.2.jar" />
403
                  <option value="/opt/gradle/lib/gradle-model-core-4.10.2.jar" />
404
                  <option value="/opt/gradle/lib/gradle-kotlin-dsl-tooling-builders-1.0-rc-6.jar" />
405
                  <option value="/opt/gradle/lib/gradle-native-4.10.2.jar" />
406
                  <option value="/opt/gradle/lib/gradle-tooling-api-4.10.2.jar" />
407
                  <option value="/opt/gradle/lib/gradle-build-cache-4.10.2.jar" />
408
                  <option value="/opt/gradle/lib/gradle-process-services-4.10.2.jar" />
409
                  <option value="/opt/gradle/lib/gradle-kotlin-dsl-provider-plugins-1.0-rc-6.jar" />
410
                  <option value="/opt/gradle/lib/gradle-resources-4.10.2.jar" />
411
                  <option value="/opt/gradle/lib/gradle-messaging-4.10.2.jar" />
412
                  <option value="/opt/gradle/lib/gradle-model-groovy-4.10.2.jar" />
413
                  <option value="/opt/gradle/lib/gradle-logging-4.10.2.jar" />
414
                  <option value="/opt/gradle/lib/gradle-jvm-services-4.10.2.jar" />
415
                  <option value="/opt/gradle/lib/plugins/gradle-testing-junit-platform-4.10.2.jar" />
416
                  <option value="/opt/gradle/lib/plugins/gradle-osgi-4.10.2.jar" />
417
                  <option value="/opt/gradle/lib/plugins/gradle-plugins-4.10.2.jar" />
418
                  <option value="/opt/gradle/lib/plugins/gradle-version-control-4.10.2.jar" />
419
                  <option value="/opt/gradle/lib/plugins/gradle-test-kit-4.10.2.jar" />
420
                  <option value="/opt/gradle/lib/plugins/gradle-diagnostics-4.10.2.jar" />
421
                  <option value="/opt/gradle/lib/plugins/gradle-resources-s3-4.10.2.jar" />
422
                  <option value="/opt/gradle/lib/plugins/gradle-composite-builds-4.10.2.jar" />
423
                  <option value="/opt/gradle/lib/plugins/gradle-language-scala-4.10.2.jar" />
424
                  <option value="/opt/gradle/lib/plugins/gradle-signing-4.10.2.jar" />
425
                  <option value="/opt/gradle/lib/plugins/gradle-plugin-use-4.10.2.jar" />
426
                  <option value="/opt/gradle/lib/plugins/ivy-2.2.0.jar" />
427
                  <option value="/opt/gradle/lib/plugins/gradle-code-quality-4.10.2.jar" />
428
                  <option value="/opt/gradle/lib/plugins/gradle-publish-4.10.2.jar" />
429
                  <option value="/opt/gradle/lib/plugins/gradle-platform-base-4.10.2.jar" />
430
                  <option value="/opt/gradle/lib/plugins/gradle-ivy-4.10.2.jar" />
431
                  <option value="/opt/gradle/lib/plugins/gradle-testing-native-4.10.2.jar" />
432
                  <option value="/opt/gradle/lib/plugins/gradle-language-java-4.10.2.jar" />
433
                  <option value="/opt/gradle/lib/plugins/gradle-ide-native-4.10.2.jar" />
434
                  <option value="/opt/gradle/lib/plugins/gradle-plugin-development-4.10.2.jar" />
435
                  <option value="/opt/gradle/lib/plugins/gradle-language-native-4.10.2.jar" />
436
                  <option value="/opt/gradle/lib/plugins/gradle-ear-4.10.2.jar" />
437
                  <option value="/opt/gradle/lib/plugins/gradle-resources-sftp-4.10.2.jar" />
438
                  <option value="/opt/gradle/lib/plugins/gradle-scala-4.10.2.jar" />
439
                  <option value="/opt/gradle/lib/plugins/gradle-testing-jvm-4.10.2.jar" />
440
                  <option value="/opt/gradle/lib/plugins/gradle-jacoco-4.10.2.jar" />
441
                  <option value="/opt/gradle/lib/plugins/gradle-antlr-4.10.2.jar" />
442
                  <option value="/opt/gradle/lib/plugins/gradle-dependency-management-4.10.2.jar" />
443
                  <option value="/opt/gradle/lib/plugins/gradle-tooling-native-4.10.2.jar" />
444
                  <option value="/opt/gradle/lib/plugins/gradle-tooling-api-builders-4.10.2.jar" />
445
                  <option value="/opt/gradle/lib/plugins/gradle-reporting-4.10.2.jar" />
446
                  <option value="/opt/gradle/lib/plugins/gradle-workers-4.10.2.jar" />
447
                  <option value="/opt/gradle/lib/plugins/gradle-resources-http-4.10.2.jar" />
448
                  <option value="/opt/gradle/lib/plugins/gradle-platform-native-4.10.2.jar" />
449
                  <option value="/opt/gradle/lib/plugins/gradle-resources-gcs-4.10.2.jar" />
450
                  <option value="/opt/gradle/lib/plugins/gradle-build-comparison-4.10.2.jar" />
451
                  <option value="/opt/gradle/lib/plugins/gradle-build-init-4.10.2.jar" />
452
                  <option value="/opt/gradle/lib/plugins/gradle-platform-jvm-4.10.2.jar" />
453
                  <option value="/opt/gradle/lib/plugins/gradle-testing-base-4.10.2.jar" />
454
                  <option value="/opt/gradle/lib/plugins/gradle-ide-4.10.2.jar" />
455
                  <option value="/opt/gradle/lib/plugins/gradle-ide-play-4.10.2.jar" />
456
                  <option value="/opt/gradle/lib/plugins/gradle-maven-4.10.2.jar" />
457
                  <option value="/opt/gradle/lib/plugins/gradle-build-cache-http-4.10.2.jar" />
458
                  <option value="/opt/gradle/lib/plugins/gradle-javascript-4.10.2.jar" />
459
                  <option value="/opt/gradle/lib/plugins/gradle-announce-4.10.2.jar" />
460
                  <option value="/opt/gradle/lib/plugins/gradle-language-groovy-4.10.2.jar" />
461
                  <option value="/opt/gradle/lib/plugins/gradle-platform-play-4.10.2.jar" />
462
                  <option value="/opt/gradle/lib/plugins/gradle-language-jvm-4.10.2.jar" />
463
                </list>
464
              </option>
465
            </ExternalProjectBuildClasspathPojo>
466
          </value>
467
        </entry>
468
      </map>
469
    </option>
470
    <option name="projectSyncType">
471
      <map>
472
        <entry key="$PROJECT_DIR$" value="RE_IMPORT" />
473
      </map>
474
    </option>
475
  </component>
476
  <component name="IdeDocumentHistory">
477
    <option name="CHANGED_PATHS">
478
      <list>
479
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractFileFactory.java" />
480
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractPane.java" />
481
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Options.java" />
482
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" />
483
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java" />
484
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/FileType.java" />
485
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/Main.java" />
486
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/IdentityProcessor.java" />
487
        <option value="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" />
488
      </list>
489
    </option>
490
  </component>
491
  <component name="ProjectFrameBounds">
492
    <option name="x" value="238" />
493
    <option name="y" value="312" />
494
    <option name="width" value="1685" />
495
    <option name="height" value="1141" />
496
  </component>
497
  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
498
  <component name="ProjectView">
499
    <navigator proportions="" version="1">
500
      <foldersAlwaysOnTop value="true" />
501
    </navigator>
502
    <panes>
503
      <pane id="PackagesPane" />
504
      <pane id="Scope" />
505
      <pane id="ProjectPane">
506
        <subPane>
507
          <expand>
508
            <path>
509
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
510
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
511
            </path>
512
            <path>
513
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
514
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
515
              <item name="src" type="462c0819:PsiDirectoryNode" />
516
            </path>
517
            <path>
518
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
519
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
520
              <item name="src" type="462c0819:PsiDirectoryNode" />
521
              <item name="main" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
522
            </path>
523
            <path>
524
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
525
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
526
              <item name="src" type="462c0819:PsiDirectoryNode" />
527
              <item name="main" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
528
              <item name="java" type="462c0819:PsiDirectoryNode" />
529
            </path>
530
            <path>
531
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
532
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
533
              <item name="src" type="462c0819:PsiDirectoryNode" />
534
              <item name="main" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
535
              <item name="java" type="462c0819:PsiDirectoryNode" />
536
              <item name="scrivenvar" type="462c0819:PsiDirectoryNode" />
537
            </path>
538
            <path>
539
              <item name="scrivenvar" type="b2602c69:ProjectViewProjectNode" />
540
              <item name="scrivenvar" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
541
              <item name="src" type="462c0819:PsiDirectoryNode" />
542
              <item name="main" type="8a07ba80:GradleTreeStructureProvider$GradleModuleDirectoryNode" />
543
              <item name="java" type="462c0819:PsiDirectoryNode" />
544
              <item name="scrivenvar" type="462c0819:PsiDirectoryNode" />
545
              <item name="processors" type="462c0819:PsiDirectoryNode" />
546
            </path>
547
          </expand>
548
          <select />
549
        </subPane>
550
      </pane>
551
    </panes>
552
  </component>
553
  <component name="PropertiesComponent">
554
    <property name="com.android.tools.idea.instantapp.provision.ProvisionBeforeRunTaskProvider.myTimeStamp" value="1541653415064" />
555
    <property name="settings.editor.selected.configurable" value="preferences.sourceCode.Java" />
556
  </component>
557
  <component name="RunDashboard">
558
    <option name="ruleStates">
559
      <list>
560
        <RuleState>
561
          <option name="name" value="ConfigurationTypeDashboardGroupingRule" />
562
        </RuleState>
563
        <RuleState>
564
          <option name="name" value="StatusDashboardGroupingRule" />
565
        </RuleState>
566
      </list>
567
    </option>
568
  </component>
569
  <component name="RunManager">
570
    <configuration name="Scrivenvar" type="GradleRunConfiguration" factoryName="Gradle">
571
      <ExternalSystemSettings>
572
        <option name="executionName" />
573
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
574
        <option name="externalSystemIdString" value="GRADLE" />
575
        <option name="scriptParameters" value="" />
576
        <option name="taskDescriptions">
577
          <list />
578
        </option>
579
        <option name="taskNames">
580
          <list>
581
            <option value="run" />
582
          </list>
583
        </option>
584
        <option name="vmOptions" value="" />
585
      </ExternalSystemSettings>
586
      <method v="2" />
587
    </configuration>
588
    <configuration default="true" type="GradleRunConfiguration" factoryName="Gradle">
589
      <ExternalSystemSettings>
590
        <option name="executionName" />
591
        <option name="externalProjectPath" value="" />
592
        <option name="externalSystemIdString" value="GRADLE" />
593
        <option name="scriptParameters" value="" />
594
        <option name="taskDescriptions">
595
          <list />
596
        </option>
597
        <option name="taskNames">
598
          <list />
599
        </option>
600
        <option name="vmOptions" value="" />
601
      </ExternalSystemSettings>
602
      <method v="2" />
603
    </configuration>
604
  </component>
605
  <component name="SvnConfiguration">
606
    <configuration />
607
  </component>
608
  <component name="TaskManager">
609
    <task active="true" id="Default" summary="Default task">
610
      <changelist id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="" />
611
      <created>1541651873782</created>
612
      <option name="number" value="Default" />
613
      <option name="presentableId" value="Default" />
614
      <updated>1541651873782</updated>
615
    </task>
616
    <servers />
617
  </component>
618
  <component name="ToolWindowManager">
619
    <frame x="238" y="312" width="1685" height="1141" extended-state="0" />
620
    <editor active="true" />
621
    <layout>
622
      <window_info id="Image Layers" />
623
      <window_info id="Designer" />
624
      <window_info id="UI Designer" />
625
      <window_info id="Capture Tool" />
626
      <window_info id="Favorites" side_tool="true" />
627
      <window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.20626152" />
628
      <window_info id="Structure" order="1" side_tool="true" weight="0.25" />
629
      <window_info anchor="bottom" id="Version Control" show_stripe_button="false" />
630
      <window_info anchor="bottom" id="Terminal" />
631
      <window_info anchor="bottom" id="Event Log" side_tool="true" />
632
      <window_info anchor="bottom" id="Build" weight="0.2127451" />
633
      <window_info anchor="bottom" id="Message" order="0" />
634
      <window_info anchor="bottom" id="Find" order="1" />
635
      <window_info anchor="bottom" id="Run" order="2" visible="true" weight="0.32941177" />
636
      <window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
637
      <window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
638
      <window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
639
      <window_info anchor="bottom" id="TODO" order="6" />
640
      <window_info anchor="right" id="Palette" />
641
      <window_info anchor="right" id="Gradle" />
642
      <window_info anchor="right" id="Theme Preview" />
643
      <window_info anchor="right" id="Capture Analysis" />
644
      <window_info anchor="right" id="Palette&#9;" />
645
      <window_info anchor="right" id="Maven Projects" />
646
      <window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
647
      <window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
648
      <window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
649
    </layout>
650
  </component>
651
  <component name="VcsContentAnnotationSettings">
652
    <option name="myLimit" value="2678400000" />
653
  </component>
654
  <component name="editorHistoryManager">
655
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractFileFactory.java">
656
      <provider selected="true" editor-type-id="text-editor">
657
        <state relative-caret-position="350">
658
          <caret line="101" column="6" lean-forward="true" selection-start-line="101" selection-start-column="6" selection-end-line="101" selection-end-column="6" />
659
        </state>
660
      </provider>
661
    </entry>
662
    <entry file="jar:///opt/jdk/src.zip!/java/util/prefs/Preferences.java">
663
      <provider selected="true" editor-type-id="text-editor">
664
        <state relative-caret-position="2775">
665
          <caret line="223" column="22" selection-start-line="223" selection-start-column="22" selection-end-line="223" selection-end-column="22" />
666
        </state>
667
      </provider>
668
    </entry>
669
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/AbstractPane.java">
670
      <provider selected="true" editor-type-id="text-editor">
671
        <state>
672
          <folding>
673
            <element signature="e#1865#1866#0" expanded="true" />
674
            <element signature="e#1894#1895#0" expanded="true" />
675
            <element signature="e#2106#2107#0" expanded="true" />
676
            <element signature="e#2146#2147#0" expanded="true" />
677
          </folding>
678
        </state>
679
      </provider>
680
    </entry>
681
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/service/Options.java">
682
      <provider selected="true" editor-type-id="text-editor">
683
        <state relative-caret-position="90">
684
          <caret line="33" selection-start-line="33" selection-end-line="33" />
685
        </state>
686
      </provider>
687
    </entry>
688
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/FileType.java">
689
      <provider selected="true" editor-type-id="text-editor">
690
        <state relative-caret-position="558">
691
          <caret line="95" column="33" selection-start-line="95" selection-start-column="33" selection-end-line="95" selection-end-column="33" />
692
          <folding>
693
            <element signature="e#1987#1988#0" expanded="true" />
694
            <element signature="e#2013#2014#0" expanded="true" />
695
          </folding>
696
        </state>
697
      </provider>
698
    </entry>
699
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/FileEditorTabPane.java">
700
      <provider selected="true" editor-type-id="text-editor">
701
        <state relative-caret-position="483">
702
          <caret line="637" column="3" lean-forward="true" selection-start-line="637" selection-start-column="3" selection-end-line="637" selection-end-column="3" />
703
        </state>
704
      </provider>
705
    </entry>
706
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java">
707
      <provider selected="true" editor-type-id="text-editor">
708
        <state relative-caret-position="240">
709
          <caret line="44" column="5" lean-forward="true" selection-start-line="44" selection-start-column="5" selection-end-line="44" selection-end-column="5" />
710
        </state>
711
      </provider>
712
    </entry>
713
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java">
714
      <provider selected="true" editor-type-id="text-editor">
715
        <state relative-caret-position="150">
716
          <caret line="98" column="13" selection-start-line="98" selection-start-column="13" selection-end-line="98" selection-end-column="13" />
717
        </state>
718
      </provider>
719
    </entry>
720
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/Main.java">
721
      <provider selected="true" editor-type-id="text-editor">
722
        <state relative-caret-position="195">
723
          <caret line="50" column="26" lean-forward="true" selection-start-line="50" selection-start-column="26" selection-end-line="50" selection-end-column="26" />
724
        </state>
725
      </provider>
726
    </entry>
727
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/AbstractProcessor.java">
728
      <provider selected="true" editor-type-id="text-editor">
729
        <state relative-caret-position="180">
730
          <caret line="38" column="22" selection-start-line="38" selection-start-column="22" selection-end-line="38" selection-end-column="22" />
731
        </state>
732
      </provider>
733
    </entry>
734
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java">
735
      <provider selected="true" editor-type-id="text-editor">
736
        <state relative-caret-position="-135">
737
          <caret line="41" column="22" selection-start-line="41" selection-start-column="22" selection-end-line="41" selection-end-column="22" />
738
        </state>
739
      </provider>
740
    </entry>
741
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java">
742
      <provider selected="true" editor-type-id="text-editor">
743
        <state relative-caret-position="165">
744
          <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
745
        </state>
746
      </provider>
747
    </entry>
748
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/DefaultVariableProcessor.java">
749
      <provider selected="true" editor-type-id="text-editor">
750
        <state relative-caret-position="-101">
751
          <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
752
        </state>
753
      </provider>
754
    </entry>
755
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java">
756
      <provider selected="true" editor-type-id="text-editor">
757
        <state relative-caret-position="150">
758
          <caret line="38" column="13" selection-start-line="38" selection-start-column="13" selection-end-line="38" selection-end-column="13" />
759
        </state>
760
      </provider>
761
    </entry>
762
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/IdentityProcessor.java">
763
      <provider selected="true" editor-type-id="text-editor">
764
        <state relative-caret-position="210">
765
          <caret line="40" column="76" lean-forward="true" selection-start-line="40" selection-start-column="76" selection-end-line="40" selection-end-column="76" />
766
        </state>
767
      </provider>
768
    </entry>
769
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java">
770
      <provider selected="true" editor-type-id="text-editor">
771
        <state relative-caret-position="439">
772
          <caret line="71" lean-forward="true" selection-start-line="71" selection-end-line="71" />
773
        </state>
774
      </provider>
775
    </entry>
776
    <entry file="file://$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java">
777
      <provider selected="true" editor-type-id="text-editor">
778
        <state relative-caret-position="300">
779
          <caret line="104" selection-start-line="104" selection-end-line="104" />
780
        </state>
781
      </provider>
782
    </entry>
5
      <change afterPath="$PROJECT_DIR$/.idea/rGraphicsSettings.xml" afterDir="false" />
6
      <change afterPath="$PROJECT_DIR$/.idea/rpackages.xml" afterDir="false" />
7
      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
8
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" afterDir="false" />
9
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" afterDir="false" />
10
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Messages.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Messages.java" afterDir="false" />
11
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/dialogs/RScriptDialog.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/dialogs/RScriptDialog.java" afterDir="false" />
12
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" afterDir="false" />
13
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/ProcessorFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/ProcessorFactory.java" afterDir="false" />
14
      <change beforePath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" afterDir="false" />
15
    </list>
16
    <option name="SHOW_DIALOG" value="false" />
17
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
18
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
19
    <option name="LAST_RESOLUTION" value="IGNORE" />
20
  </component>
21
  <component name="ExternalProjectsData">
22
    <projectState path="$PROJECT_DIR$">
23
      <ProjectState />
24
    </projectState>
25
  </component>
26
  <component name="ExternalProjectsManager">
27
    <system id="GRADLE">
28
      <state>
29
        <task path="$PROJECT_DIR$">
30
          <activation />
31
        </task>
32
        <projects_view>
33
          <tree_state>
34
            <expand>
35
              <path>
36
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
37
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
38
              </path>
39
              <path>
40
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
41
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
42
                <item name="Tasks" type="e4a08cd1:TasksNode" />
43
              </path>
44
              <path>
45
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
46
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
47
                <item name="Run Configurations" type="7b0102dc:RunConfigurationsNode" />
48
              </path>
49
            </expand>
50
            <select />
51
          </tree_state>
52
        </projects_view>
53
      </state>
54
    </system>
55
  </component>
56
  <component name="FileTemplateManagerImpl">
57
    <option name="RECENT_TEMPLATES">
58
      <list>
59
        <option value="Class" />
60
      </list>
61
    </option>
62
  </component>
63
  <component name="Git.Settings">
64
    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
65
  </component>
66
  <component name="ProjectId" id="1bxOWN6JNt3E3hwxZwoPKSZNdNc" />
67
  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
68
  <component name="ProjectViewState">
69
    <option name="hideEmptyMiddlePackages" value="true" />
70
    <option name="showLibraryContents" value="true" />
71
  </component>
72
  <component name="PropertiesComponent">
73
    <property name="ASKED_ADD_EXTERNAL_FILES" value="true" />
74
    <property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
75
    <property name="com.android.tools.idea.instantapp.provision.ProvisionBeforeRunTaskProvider.myTimeStamp" value="1541653415064" />
76
    <property name="last_opened_file_path" value="$PROJECT_DIR$" />
77
    <property name="settings.editor.selected.configurable" value="reference.settingsdialog.project.gradle" />
78
  </component>
79
  <component name="RunManager" selected="Application.Launcher">
80
    <configuration name="Launcher" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
81
      <option name="MAIN_CLASS_NAME" value="com.scrivenvar.Launcher" />
82
      <module name="scrivenvar_main" />
83
      <extension name="coverage">
84
        <pattern>
85
          <option name="PATTERN" value="com.scrivenvar.*" />
86
          <option name="ENABLED" value="true" />
87
        </pattern>
88
      </extension>
89
      <method v="2">
90
        <option name="Make" enabled="true" />
91
      </method>
92
    </configuration>
93
    <configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
94
      <option name="MAIN_CLASS_NAME" value="com.scrivenvar.Main" />
95
      <module name="scrivenvar_main" />
96
      <extension name="coverage">
97
        <pattern>
98
          <option name="PATTERN" value="com.scrivenvar.*" />
99
          <option name="ENABLED" value="true" />
100
        </pattern>
101
      </extension>
102
      <method v="2">
103
        <option name="Make" enabled="true" />
104
      </method>
105
    </configuration>
106
    <configuration name="Scrivenvar" type="GradleRunConfiguration" factoryName="Gradle">
107
      <ExternalSystemSettings>
108
        <option name="executionName" />
109
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
110
        <option name="externalSystemIdString" value="GRADLE" />
111
        <option name="scriptParameters" value="" />
112
        <option name="taskDescriptions">
113
          <list />
114
        </option>
115
        <option name="taskNames">
116
          <list>
117
            <option value="run" />
118
          </list>
119
        </option>
120
        <option name="vmOptions" value="" />
121
      </ExternalSystemSettings>
122
      <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
123
      <method v="2" />
124
    </configuration>
125
    <configuration default="true" type="GradleRunConfiguration" factoryName="Gradle">
126
      <ExternalSystemSettings>
127
        <option name="executionName" />
128
        <option name="externalProjectPath" value="" />
129
        <option name="externalSystemIdString" value="GRADLE" />
130
        <option name="scriptParameters" value="" />
131
        <option name="taskDescriptions">
132
          <list />
133
        </option>
134
        <option name="taskNames">
135
          <list />
136
        </option>
137
        <option name="vmOptions" value="" />
138
      </ExternalSystemSettings>
139
      <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
140
      <method v="2" />
141
    </configuration>
142
    <recent_temporary>
143
      <list>
144
        <item itemvalue="Application.Launcher" />
145
        <item itemvalue="Application.Main" />
146
      </list>
147
    </recent_temporary>
148
  </component>
149
  <component name="SvnConfiguration">
150
    <configuration />
151
  </component>
152
  <component name="TaskManager">
153
    <task active="true" id="Default" summary="Default task">
154
      <changelist id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="" />
155
      <created>1541651873782</created>
156
      <option name="number" value="Default" />
157
      <option name="presentableId" value="Default" />
158
      <updated>1541651873782</updated>
159
    </task>
160
    <servers />
161
  </component>
162
  <component name="WindowStateProjectService">
163
    <state x="521" y="258" width="605" height="787" key="#Scrivenvar" timestamp="1589659082008">
164
      <screen x="0" y="28" width="2560" height="1529" />
165
    </state>
166
    <state x="521" y="258" width="605" height="787" key="#Scrivenvar/0.28.2560.1529@0.28.2560.1529" timestamp="1589659082008" />
167
    <state x="285" y="311" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1589659128840">
168
      <screen x="0" y="28" width="2560" height="1529" />
169
    </state>
170
    <state x="285" y="311" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589659128840" />
171
    <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser" timestamp="1589658771205">
172
      <screen x="0" y="28" width="2560" height="1529" />
173
    </state>
174
    <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser/0.28.2560.1529@0.28.2560.1529" timestamp="1589658771205" />
175
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1589658744105">
176
      <screen x="0" y="28" width="2560" height="1529" />
177
    </state>
178
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589658744105" />
179
    <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl" timestamp="1589659107517">
180
      <screen x="0" y="28" width="2560" height="1529" />
181
    </state>
182
    <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl/0.28.2560.1529@0.28.2560.1529" timestamp="1589659107517" />
183
    <state width="1573" height="321" key="GridCell.Tab.0.bottom" timestamp="1589671859570">
184
      <screen x="0" y="28" width="2560" height="1529" />
185
    </state>
186
    <state width="1573" height="321" key="GridCell.Tab.0.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859570" />
187
    <state width="1573" height="321" key="GridCell.Tab.0.center" timestamp="1589671859569">
188
      <screen x="0" y="28" width="2560" height="1529" />
189
    </state>
190
    <state width="1573" height="321" key="GridCell.Tab.0.center/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859569" />
191
    <state width="1573" height="321" key="GridCell.Tab.0.left" timestamp="1589671859569">
192
      <screen x="0" y="28" width="2560" height="1529" />
193
    </state>
194
    <state width="1573" height="321" key="GridCell.Tab.0.left/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859569" />
195
    <state width="1573" height="321" key="GridCell.Tab.0.right" timestamp="1589671859570">
196
      <screen x="0" y="28" width="2560" height="1529" />
197
    </state>
198
    <state width="1573" height="321" key="GridCell.Tab.0.right/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859570" />
199
    <state width="1573" height="396" key="GridCell.Tab.1.bottom" timestamp="1589671859555">
200
      <screen x="0" y="28" width="2560" height="1529" />
201
    </state>
202
    <state width="1573" height="396" key="GridCell.Tab.1.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
203
    <state width="1573" height="396" key="GridCell.Tab.1.center" timestamp="1589671859555">
204
      <screen x="0" y="28" width="2560" height="1529" />
205
    </state>
206
    <state width="1573" height="396" key="GridCell.Tab.1.center/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
207
    <state width="1573" height="396" key="GridCell.Tab.1.left" timestamp="1589671859554">
208
      <screen x="0" y="28" width="2560" height="1529" />
209
    </state>
210
    <state width="1573" height="396" key="GridCell.Tab.1.left/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859554" />
211
    <state width="1573" height="396" key="GridCell.Tab.1.right" timestamp="1589671859555">
212
      <screen x="0" y="28" width="2560" height="1529" />
213
    </state>
214
    <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
215
    <state x="324" y="288" key="SettingsEditor" timestamp="1589576619807">
216
      <screen x="0" y="28" width="2560" height="1529" />
217
    </state>
218
    <state x="324" y="288" key="SettingsEditor/0.28.2560.1529@0.28.2560.1529" timestamp="1589576619807" />
219
    <state x="1071" y="397" width="1417" height="979" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1589661186060">
220
      <screen x="0" y="28" width="2560" height="1529" />
221
    </state>
222
    <state x="1071" y="397" width="1417" height="979" key="com.intellij.history.integration.ui.views.FileHistoryDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589661186060" />
223
    <state x="531" y="261" width="586" height="753" key="find.popup" timestamp="1589669468040">
224
      <screen x="0" y="28" width="2560" height="1529" />
225
    </state>
226
    <state x="531" y="261" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589669468040" />
227
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog" timestamp="1589663937037">
228
      <screen x="0" y="28" width="2560" height="1529" />
229
    </state>
230
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589663937037" />
231
    <state x="490" y="304" key="run.anything.popup" timestamp="1589657324666">
232
      <screen x="0" y="28" width="2560" height="1529" />
233
    </state>
234
    <state x="490" y="304" key="run.anything.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589657324666" />
235
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup" timestamp="1589669442714">
236
      <screen x="0" y="28" width="2560" height="1529" />
237
    </state>
238
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589669442714" />
239
  </component>
240
  <component name="XDebuggerManager">
241
    <breakpoint-manager>
242
      <breakpoints>
243
        <line-breakpoint enabled="true" type="java-line">
244
          <url>file://$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java</url>
245
          <line>410</line>
246
          <option name="timeStamp" value="4" />
247
        </line-breakpoint>
248
      </breakpoints>
249
    </breakpoint-manager>
783250
  </component>
784251
  <component name="masterDetails">
M BUILD.md
1
Build
2
===
1
# Build
32
4
Once the [requirements](README.md) are met, build the application as follows:
3
This document describes how to build the application.
54
6
    gradle build
5
# Requirements
6
7
Download and install the following software packages:
8
9
* [OpenJDK 14](https://openjdk.java.net)
10
* [Gradle 6.4](https://gradle.org/releases)
11
12
# Compile
13
14
Build the application as follows:
15
16
    gradle clean jar
17
18
The application is built.
19
20
# Run
21
22
After the application is compiled, run it as follows:
23
24
    java -jar build/libs/scrivenvar.jar
25
26
On Windows:
27
28
    java -jar build\libs\scrivenvar.jar
729
830
M CREDITS.md
1
Credits
2
===
1
# Credits
32
4
  * Dave Jarvis: [Scrivenvar](https://github.com/DaveJarvis/scrivenvar/)
5
  * Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
6
  * Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
7
  * Mikael Grev: [MigLayout](http://www.miglayout.com/)
8
  * Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
9
  * Vladimir Schneider: [flexmark](https://website.com)
10
  * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
11
  * Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
12
 * David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
3
* Dave Jarvis: [Scrivenvar](https://github.com/DaveJarvis/scrivenvar/)
4
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
5
* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
6
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
7
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
8
* Vladimir Schneider: [flexmark](https://website.com)
9
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
10
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
11
* David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
12
* Alex Bertram, [Renjin](https://www.renjin.org/)
13
* Michael Kay, [XSLT Processor](http://www.saxonica.com/)
14
15
M LICENSE.md
11
# License
22
3
Copyright 2018 White Magic Software, Ltd.
3
Copyright 2020 White Magic Software, Ltd.
44
All rights reserved.
55
M README.md
11
![Logo](images/logo64.png)
22
3
$application.title$
4
===
3
# $application.title$
54
6
Word processing with variables.
5
Text editing using interpolated strings.
76
8
Requirements
9
---
7
## Requirements
8
109
Download and install the following software packages:
1110
12
* [Java 8u40](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) (or newer)
13
* [Gradle 4.4.1](https://gradle.org/)
11
* [OpenJDK 14](https://openjdk.java.net)
1412
15
Quick Start
16
---
13
## Quick Start
14
1715
Complete the following steps to run the application:
1816
19
1. [Download](https://github.com/DaveJarvis/scrivenvar/releases) `scrivenvar.jar`.
17
1. [Download](https://github.com/DaveJarvis/scrivenvar/releases)
18
`scrivenvar.jar`.
2019
1. Double-click `scrivenvar.jar` to start the application.
2120
22
Features
23
---
21
## Command Line Start
22
23
If the quick start fails, run the application as follows:
24
25
1. Open a command prompt.
26
1. Change to the download directory containing the archive file.
27
1. Run: `java -jar scrivenvar.jar`
28
29
## Features
30
2431
* R integration
2532
* User-defined variables, interpolated
2633
* Real-time preview with variable substitution
2734
* Auto-complete variable names based on variable values
28
* XML document transformation using XSLT2
35
* XML document transformation using XSLT3 or older
2936
* Platform independent (Windows, Linux, MacOS)
3037
31
Future Features
32
---
38
## Future Features
39
3340
* Spell check
3441
* Search and replace using variables
3542
* Re-organize variable names
3643
37
Screenshot
38
---
44
## Screenshot
3945
4046
![Screenshot](images/screenshot.png)
4147
42
License
43
---
48
## License
49
4450
This software is licensed under the [BSD 2-Clause License](LICENSE.md).
51
4552
D Scrivenvar.jfdproj
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project>
3
  <entry key="classpath1" value="bin" />
4
  <entry key="classpath2" value="lib/fontawesomefx-8.9.jar" />
5
  <entry key="sourcefolders1" value="src/main/java" />
6
  <entry key="sourcefolders2" value="src/main/resources" />
7
  <node name="designer">
8
    <entry key="_i18nJavaSettingsEnabled" value="true" />
9
    <entry key="_i18nSettingsEnabled" value="true" />
10
    <entry key="i18n.externalizeexcludes1" value="com.scrivenvar.controls.EscapeTextField#escapeCharacters" />
11
    <entry key="i18n.externalizeexcludes2" value="com.scrivenvar.controls.WebHyperlink#uri" />
12
    <entry key="i18n.javagetstringformat" value="Messages.get(${key})" />
13
  </node>
14
  <node name="javacodegenerator">
15
    <entry key="_codeStyleSettingsEnabled" value="true" />
16
    <entry key="_generalSettingsEnabled" value="true" />
17
    <entry key="explicitimports" value="true" />
18
    <entry key="lineseparator" value="\n" />
19
  </node>
20
</project>
211
A USAGE-R.md
1
# Introduction
2
3
This document describes how to use the [R](https://www.r-project.org/)
4
programming language from within the application. The application uses an
5
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
6
7
# Hello World
8
9
Complete the following steps to see R in action:
10
11
1. Start the application.
12
1. Click **File → New** to create a new file.
13
1. Click **File → Save As**.
14
1. Set **Name** to: `addition.Rmd`
15
1. Click **Save**.
16
17
Setting the file name extension tells the application what processor to
18
use when transforming the contents for display in the preview pane. Continue
19
by typing in the following text, including the backticks:
20
21
```r
22
`r#1 + 1`
23
```
24
25
The preview pane shows the result of `1` plus `1`:
26
27
```
28
2.0
29
```
30
31
# Bootstrap Script
32
33
Being able to run R code while editing an R Markdown document is convenient.
34
Having the ability to call functions is where the power of R can be
35
leveraged.
36
37
Complete the following steps to call an R function from your own library:
38
39
1. Click **File → New** to create a new file.
40
1. Click **File → Save As**.
41
1. Browse to your home directory.
42
1. Set **Name** to: `library.R`.
43
1. Click **Save**.
44
1. Set the contents to:
45
    ``` r
46
    sum <- function( a, b ) {
47
      a + b
48
    }
49
    ```
50
1. Click the **Save** icon.
51
1. Click **R → Script**.
52
1. Set the **R Startup Script** contents to:
53
    ``` r
54
    source( 'library.R' );
55
    ```
56
1. Click **OK**.
57
1. Create a new file.
58
1. Set the contents to:
59
    ``` r
60
    `r#sum( 5, 5 )`
61
    ```
62
1. Save the file as `sum.R`.
63
64
The preview panel shows the result of calling the `sum` function:
65
66
```
67
10.0
68
```
69
70
This shows how the bootstrap script can load `library.R`, which defines
71
a `sum` function that is called by name in the Markdown document.
72
73
# Working Directory
74
75
R files may be sourced from any directory, not just the user's home
76
directory. Accomplish this as follows:
77
78
1. Click **R → Directory**.
79
1. Set **Directory** to a different directory.
80
1. Click **OK**.
81
1. Create the directory if it does not exist.
82
1. Move `library.R` into the directory.
83
1. Append a new function to `library.R` as follows:
84
``` r
85
mul <- function( a, b ) {
86
  a * b
87
}
88
```
89
1. Click **R → Script**.
90
1. Set the **R Startup Script** contents to:
91
    ``` r
92
    setwd( '$application.r.working.directory$' );
93
    source( 'library.R' );
94
    ```
95
1. Change `sum.Rmd` to:
96
    ``` r
97
    `r#mul( 5, 5 )`
98
    ```
99
1. Close the file `sum.Rmd`.
100
1. Confirm saving the file when prompted.
101
1. Re-open `sum.Rmd`.
102
103
The preview panel shows:
104
105
```
106
25.0
107
```
108
109
Calling `setwd` using `'$application.r.working.directory$'` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML Definitions
113
114
To see how variable definitions work in R, try the following:
115
116
1. Create a new file.
117
1. Change the contents to (use spaces not tabs):
118
    ``` yaml
119
    project:
120
      title: Project Title
121
      author: Author Name
122
    ```
123
1. Save the file as `definitions.yaml`.
124
1. Click **File → Open**.
125
1. Set **Source Files** to **Definition Files**.
126
1. Select `definitions.yaml`.
127
1. Click **Open**.
128
1. Open `sum.Rmd` if it is not already open.
129
1. Type: `je`
130
1. Press `Ctrl+Space`
131
132
The editor inserts the following text (matches `je` against Pro**je**ct):
133
134
``` r
135
`r#x( v$project$title )`
136
```
137
138
The preview panel shows:
139
140
```
141
r#x( 'Project Title' )
142
```
143
144
This is because the application inserts definition reference names based
145
on the type of file being edited. By default, the R engine does not have
146
a function named `x` defined.
147
148
Continue as follows:
149
150
1. Click **R → Script**.
151
1. Append the following:
152
    ``` r
153
    x <- function( s ) {
154
      tryCatch({
155
        r = eval( parse( text = s ) )
156
157
        if( is.atomic( r ) ) { r }
158
        else { s }
159
      },
160
      warning = function( w ) { s },
161
      error = function( e ) { s })
162
    }
163
    ```
164
1. Click **OK**.
165
1. Close and re-open `sum.Rmd`.
166
167
The preview panel shows:
168
169
```
170
25.0
171
172
Project Title
173
```
174
175
The `x` function attempts to evaluate the expression defined by the YAML
176
variable. This means that the YAML definitions can also include expressions
177
that R is capable of evaluating.
178
179
While the `x` function can be defined within the R Startup Script, it is
180
better practice to put it into its own library so that it can be reused
181
outside of the application.
182
1183
M build.gradle
1
apply plugin: 'java'
2
apply plugin: 'java-library-distribution'
3
apply plugin: 'application'
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.8'
4
}
45
56
repositories {
6
  jcenter()
77
  mavenCentral()
8
  
8
  jcenter()
9
910
  maven {
10
    url 'https://oss.sonatype.org/content/repositories/snapshots/' 
11
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
1112
  }
12
}
1313
14
compileJava {
15
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
14
  maven {
15
    url "https://nexus.bedatadriven.com/content/groups/public"
16
  }
1617
}
1718
1819
dependencies {
19
  compile 'org.controlsfx:controlsfx:8.40.14'
20
  compile 'org.fxmisc.wellbehaved:wellbehavedfx:0.3'
21
  compile 'org.fxmisc.richtext:richtextfx:0.8.1'
22
  compile 'com.miglayout:miglayout-javafx:5.0'
23
  compile 'org.ahocorasick:ahocorasick:0.4.0'
24
  compile 'com.vladsch.flexmark:flexmark:0.28.34'
25
  compile 'com.vladsch.flexmark:flexmark-ext-tables:0.28.34'
26
  compile 'com.vladsch.flexmark:flexmark-ext-superscript:0.28.34'
27
  compile 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.28.34'
28
  compile 'com.fasterxml.jackson.core:jackson-core:2.9.3'
29
  compile 'com.fasterxml.jackson.core:jackson-databind:2.9.3'
30
  compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.3'
31
  compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.3'
32
  compile 'org.yaml:snakeyaml:1.19'
33
  compile 'com.ximpleware:vtd-xml:2.13.4'
34
  compile 'net.sf.saxon:Saxon-HE:9.8.0-7'
35
  compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
36
  compile 'org.apache.commons:commons-configuration2:2.2'
37
  compile files('libs/fontawesomefx-commons-8.12.jar')
38
  compile files('libs/fontawesomefx-fontawesome-4.5.0.jar')
39
  compile files('libs/renjin-script-engine-0.9.2726-jar-with-dependencies.jar')
20
  implementation 'org.controlsfx:controlsfx:11.0.0'
21
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
22
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
23
  implementation 'com.miglayout:miglayout-javafx:5.2'
24
  implementation 'com.vladsch.flexmark:flexmark:0.61.28'
25
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.61.28'
26
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.61.28'
27
  implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.61.28'
28
  implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
29
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
30
  implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
31
  implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0'
32
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
33
  implementation 'org.yaml:snakeyaml:1.26'
34
  implementation 'com.ximpleware:vtd-xml:2.13.4'
35
  implementation 'net.sf.saxon:Saxon-HE:10.1'
36
  implementation 'org.apache.commons:commons-configuration2:2.7'
37
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
38
  implementation 'de.jensd:fontawesomefx-commons:11.0'
39
  implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
40
  implementation "org.renjin:renjin-script-engine:0.9.2726"
41
42
  def os = ['win', 'linux', 'mac']
43
  def fx = ['controls', 'graphics', 'web', 'fxml']
44
45
  fx.each { fxitem ->
46
    os.each { ositem ->
47
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
48
    }
49
  }
4050
}
4151
42
version = '1.3.8'
52
javafx {
53
  version = "14"
54
  modules = ['javafx.controls', 'javafx.graphics', 'javafx.web']
55
}
56
57
compileJava {
58
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
59
}
60
61
sourceCompatibility = JavaVersion.VERSION_11
62
version = '1.4.0'
4363
applicationName = 'scrivenvar'
4464
mainClassName = 'com.scrivenvar.Main'
45
sourceCompatibility = JavaVersion.VERSION_1_8
65
def launcherClassName = 'com.scrivenvar.Launcher'
4666
4767
jar {
48
  baseName = applicationName
49
  archiveName = "${applicationName}.jar"
50
  
51
  doFirst {
52
    from {
53
      configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
68
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
69
70
  manifest {
71
    attributes 'Main-Class': launcherClassName
72
  }
73
74
  from {
75
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
76
      it.isDirectory() ? it : zipTree(it)
5477
    }
5578
  }
5679
57
  // Remove digital signature files to ensure an executable JAR file.
58
  exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' 
80
  archiveFileName = 'scrivenvar.jar'
5981
60
  manifest {
61
    attributes 'Main-Class': mainClassName
62
    attributes 'Class-Path': configurations.compile.collect {
63
     'libs/' + it.getName()
64
    }.join(' ')
65
  }
82
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
6683
}
6784
6885
distributions {
6986
  main {
70
    baseName = applicationName
87
    distributionBaseName = applicationName
7188
    contents {
7289
      from { ['LICENSE.md', 'README.md'] }
73
      into( 'images' ) {
90
      into('images') {
7491
        from { 'images' }
7592
      }
D libs/fontawesomefx-commons-8.12.jar
Binary file
D libs/fontawesomefx-fontawesome-4.5.0.jar
Binary file
D libs/renjin-script-engine-0.9.2726-jar-with-dependencies.jar
Binary file
M src/main/java/com/scrivenvar/AbstractFileFactory.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
3130
import com.scrivenvar.predicates.files.FileTypePredicate;
3231
import com.scrivenvar.service.Settings;
32
3333
import java.nio.file.Path;
3434
import java.util.Iterator;
3535
import java.util.List;
36
37
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
38
import static java.lang.String.format;
3639
3740
/**
3841
 * Provides common behaviours for factories that instantiate classes based on
3942
 * file type.
4043
 *
4144
 * @author White Magic Software, Ltd.
4245
 */
4346
public class AbstractFileFactory {
4447
45
  private final Settings settings = Services.load( Settings.class );
48
  private static final String MSG_UNKNOWN_FILE_TYPE = "Unknown type '%s' for " +
49
      "file '%s'.";
50
51
  private final Settings mSettings = Services.load( Settings.class );
4652
4753
  /**
4854
   * Determines the file type from the path extension. This should only be
4955
   * called when it is known that the file type won't be a definition file
5056
   * (e.g., YAML or other definition source), but rather an editable file
5157
   * (e.g., Markdown, XML, etc.).
5258
   *
5359
   * @param path The path with a file name extension.
54
   *
5560
   * @return The FileType for the given path.
5661
   */
5762
  public FileType lookup( final Path path ) {
5863
    return lookup( path, GLOB_PREFIX_FILE );
5964
  }
6065
6166
  /**
6267
   * Creates a file type that corresponds to the given path.
6368
   *
64
   * @param path Reference to a variable definition file.
69
   * @param path   Reference to a variable definition file.
6570
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
66
   *
6771
   * @return The file type that corresponds to the given path.
6872
   */
6973
  protected FileType lookup( final Path path, final String prefix ) {
74
    assert path != null;
75
    assert prefix != null;
76
7077
    final Settings properties = getSettings();
7178
    final Iterator<String> keys = properties.getKeys( prefix );
...
8592
        fileType = FileType.from( suffix );
8693
      }
94
    }
95
96
    if( fileType == null ) {
97
      unknownFileType( fileType, path );
8798
    }
8899
...
97108
   * @param path The path to a source of definitions.
98109
   */
99
  protected void unknownFileType( final String type, final String path ) {
100
    throw new IllegalArgumentException(
101
      "Unknown type '" + type + "' for '" + path + "'."
102
    );
110
  protected void unknownFileType( final FileType type, final Path path ) {
111
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
112
    throw new IllegalArgumentException( msg );
103113
  }
104114
105115
  /**
106
   * Throws IllegalArgumentException because the extension for the given path
107
   * could not be recognized.
116
   * Throws IllegalArgumentException because the given path could not be
117
   * recognized. This exists because
108118
   *
109
   * @param path The path to a file that could not be loaded.
119
   * @param type The detected path type (protocol, file extension, etc.).
120
   * @param path The path to a source of definitions.
110121
   */
111
  protected void unknownExtension( final Path path ) {
112
    throw new IllegalArgumentException(
113
      "Unknown extension for '" + path + "'."
114
    );
122
  protected void unknownFileType( final String type, final String path ) {
123
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
124
    throw new IllegalArgumentException( msg );
115125
  }
116126
117127
  /**
118128
   * Return the singleton Settings instance.
119129
   *
120130
   * @return A non-null instance.
121131
   */
122132
  private Settings getSettings() {
123
    return this.settings;
133
    return this.mSettings;
124134
  }
125135
}
M src/main/java/com/scrivenvar/Constants.java
5151
  }
5252
53
  @SuppressWarnings("SameParameterValue")
5354
  private static int get( final String key, final int defaultValue ) {
5455
    return SETTINGS.getSetting( key, defaultValue );
5556
  }
5657
58
  @SuppressWarnings("SameParameterValue")
5759
  private static Collection<String> getStringSettingList( final String key ) {
5860
    return SETTINGS.getStringSettingList( key );
...
7173
  public static final String STYLESHEET_MARKDOWN = get( "file.stylesheet.markdown" );
7274
  public static final String STYLESHEET_PREVIEW = get( "file.stylesheet.preview" );
73
  public static final String STYLESHEET_XML = get( "file.stylesheet.xml" );
7475
7576
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
...
115116
   */
116117
  public static final String PERSIST_R_STARTUP = "rStartup";
118
119
  /**
120
   * Bootstrap directory for R startup script.
121
   */
122
  public static final String PERSIST_R_DIRECTORY = "rDirectory";
123
124
  /**
125
   * Default working directory to use for R startup script.
126
   */
127
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
117128
}
118129
M src/main/java/com/scrivenvar/FileEditorTab.java
3030
import com.scrivenvar.service.events.Notification;
3131
import com.scrivenvar.service.events.Notifier;
32
import java.io.IOException;
33
import java.nio.charset.Charset;
34
import static java.nio.charset.StandardCharsets.UTF_8;
35
import java.nio.file.Files;
36
import java.nio.file.Path;
37
import static java.util.Locale.ENGLISH;
38
import java.util.function.Consumer;
39
import javafx.application.Platform;
40
import javafx.beans.binding.Bindings;
41
import javafx.beans.property.BooleanProperty;
42
import javafx.beans.property.ReadOnlyBooleanProperty;
43
import javafx.beans.property.ReadOnlyBooleanWrapper;
44
import javafx.beans.property.SimpleBooleanProperty;
45
import javafx.beans.value.ChangeListener;
46
import javafx.beans.value.ObservableValue;
47
import javafx.event.Event;
48
import javafx.scene.Node;
49
import javafx.scene.Scene;
50
import javafx.scene.control.Tab;
51
import javafx.scene.control.Tooltip;
52
import javafx.scene.input.InputEvent;
53
import javafx.scene.text.Text;
54
import javafx.stage.Window;
55
import org.fxmisc.richtext.StyleClassedTextArea;
56
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
57
import org.fxmisc.richtext.model.TwoDimensional.Position;
58
import org.fxmisc.undo.UndoManager;
59
import org.fxmisc.wellbehaved.event.EventPattern;
60
import org.fxmisc.wellbehaved.event.InputMap;
61
import org.mozilla.universalchardet.UniversalDetector;
62
63
/**
64
 * Editor for a single file.
65
 *
66
 * @author Karl Tauber and White Magic Software, Ltd.
67
 */
68
public final class FileEditorTab extends Tab {
69
70
  /**
71
   * 
72
   */
73
  private final Notifier alertService = Services.load( Notifier.class );
74
  private EditorPane editorPane;
75
76
  /**
77
   * Character encoding used by the file (or default encoding if none found).
78
   */
79
  private Charset encoding;
80
81
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
82
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
83
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
84
85
  private Path path;
86
87
  public FileEditorTab( final Path path ) {
88
    setPath( path );
89
90
    this.modified.addListener( (observable, oldPath, newPath) -> updateTab() );
91
92
    setOnSelectionChanged( e -> {
93
      if( isSelected() ) {
94
        Platform.runLater( () -> activated() );
95
      }
96
    } );
97
  }
98
99
  private void updateTab() {
100
    setText( getTabTitle() );
101
    setGraphic( getModifiedMark() );
102
    setTooltip( getTabTooltip() );
103
  }
104
105
  /**
106
   * Returns the base filename (without the directory names).
107
   *
108
   * @return The untitled text if the path hasn't been set.
109
   */
110
  private String getTabTitle() {
111
    final Path filePath = getPath();
112
113
    return (filePath == null)
114
      ? Messages.get( "FileEditor.untitled" )
115
      : filePath.getFileName().toString();
116
  }
117
118
  /**
119
   * Returns the full filename represented by the path.
120
   *
121
   * @return The untitled text if the path hasn't been set.
122
   */
123
  private Tooltip getTabTooltip() {
124
    final Path filePath = getPath();
125
    return new Tooltip( filePath == null ? "" : filePath.toString() );
126
  }
127
128
  /**
129
   * Returns a marker to indicate whether the file has been modified.
130
   *
131
   * @return "*" when the file has changed; otherwise null.
132
   */
133
  private Text getModifiedMark() {
134
    return isModified() ? new Text( "*" ) : null;
135
  }
136
137
  /**
138
   * Called when the user switches tab.
139
   */
140
  private void activated() {
141
    // Tab is closed or no longer active.
142
    if( getTabPane() == null || !isSelected() ) {
143
      return;
144
    }
145
146
    // Switch to the tab without loading if the contents are already in memory.
147
    if( getContent() != null ) {
148
      getEditorPane().requestFocus();
149
      return;
150
    }
151
152
    // Load the text and update the preview before the undo manager.
153
    load();
154
155
    // Track undo requests -- can only be called *after* load.
156
    initUndoManager();
157
    initLayout();
158
    initFocus();
159
  }
160
161
  private void initLayout() {
162
    setContent( getScrollPane() );
163
  }
164
165
  private Node getScrollPane() {
166
    return getEditorPane().getScrollPane();
167
  }
168
169
  private void initFocus() {
170
    getEditorPane().requestFocus();
171
  }
172
173
  private void initUndoManager() {
174
    final UndoManager undoManager = getUndoManager();
175
176
    // Clear undo history after first load.
177
    undoManager.forgetHistory();
178
179
    // Bind the editor undo manager to the properties.
180
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
181
    canUndo.bind( undoManager.undoAvailableProperty() );
182
    canRedo.bind( undoManager.redoAvailableProperty() );
183
  }
184
185
  /**
186
   * Searches from the caret position forward for the given string.
187
   *
188
   * @param needle The text string to match.
189
   */
190
  public void searchNext( final String needle ) {
191
    final String haystack = getEditorText();
192
    int index = haystack.indexOf( needle, getCaretPosition() );
193
194
    // Wrap around.
195
    if( index == -1 ) {
196
      index = haystack.indexOf( needle, 0 );
197
    }
198
199
    if( index >= 0 ) {
200
      setCaretPosition( index );
201
      getEditor().selectRange( index, index + needle.length() );
202
    }
203
  }
204
205
  /**
206
   * Returns the index into the text where the caret blinks happily away.
207
   *
208
   * @return A number from 0 to the editor's document text length.
209
   */
210
  public int getCaretPosition() {
211
    return getEditor().getCaretPosition();
212
  }
213
214
  /**
215
   * Moves the caret to a given offset.
216
   *
217
   * @param offset The new caret offset.
218
   */
219
  private void setCaretPosition( final int offset ) {
220
    getEditor().moveTo( offset );
221
    getEditor().requestFollowCaret();
222
  }
223
224
  /**
225
   * Returns the caret's current row and column position.
226
   *
227
   * @return The caret's offset into the document.
228
   */
229
  public Position getCaretOffset() {
230
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
231
  }
232
233
  /**
234
   * Allows observers to synchronize caret position changes.
235
   *
236
   * @return An observable caret property value.
237
   */
238
  public final ObservableValue<Integer> caretPositionProperty() {
239
    return getEditor().caretPositionProperty();
240
  }
241
242
  /**
243
   * Returns the text area associated with this tab.
244
   *
245
   * @return A text editor.
246
   */
247
  private StyleClassedTextArea getEditor() {
248
    return getEditorPane().getEditor();
249
  }
250
251
  /**
252
   * Returns true if the given path exactly matches this tab's path.
253
   *
254
   * @param check The path to compare against.
255
   *
256
   * @return true The paths are the same.
257
   */
258
  public boolean isPath( final Path check ) {
259
    final Path filePath = getPath();
260
261
    return filePath == null ? false : filePath.equals( check );
262
  }
263
264
  /**
265
   * Reads the entire file contents from the path associated with this tab.
266
   */
267
  private void load() {
268
    final Path filePath = getPath();
269
270
    if( filePath != null ) {
271
      try {
272
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
273
        getEditorPane().scrollToTop();
274
      } catch( final IOException ex ) {
275
        getNotifyService().notify( ex );
276
      }
277
    }
278
  }
279
280
  /**
281
   * Saves the entire file contents from the path associated with this tab.
282
   *
283
   * @return true The file has been saved.
284
   */
285
  public boolean save() {
286
    try {
287
      final EditorPane editor = getEditorPane();
288
      Files.write( getPath(), asBytes( editor.getText() ) );
289
      editor.getUndoManager().mark();
290
      return true;
291
    } catch( final IOException ex ) {
292
      return alert(
293
        "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
294
      );
295
    }
296
  }
297
298
  /**
299
   * Creates an alert dialog and waits for it to close.
300
   *
301
   * @param titleKey Resource bundle key for the alert dialog title.
302
   * @param messageKey Resource bundle key for the alert dialog message.
303
   * @param e The unexpected happening.
304
   *
305
   * @return false
306
   */
307
  private boolean alert(
308
    final String titleKey, final String messageKey, final Exception e ) {
309
    final Notifier service = getNotifyService();
310
    final Path filePath = getPath();
311
312
    final Notification message = service.createNotification(
313
      Messages.get( titleKey ),
314
      Messages.get( messageKey ),
315
      filePath == null ? "" : filePath,
316
      e.getMessage()
317
    );
318
319
    try {
320
      service.createError( getWindow(), message ).showAndWait();
321
    } catch( final Exception ex ) {
322
      getNotifyService().notify( ex );
323
    }
324
    
325
    return false;
326
  }
327
328
  private Window getWindow() {
329
    final Scene scene = getEditorPane().getScene();
330
331
    if( scene == null ) {
332
      throw new UnsupportedOperationException( "" );
333
    }
334
335
    return scene.getWindow();
336
  }
337
338
  /**
339
   * Returns a best guess at the file encoding. If the encoding could not be
340
   * detected, this will return the default charset for the JVM.
341
   *
342
   * @param bytes The bytes to perform character encoding detection.
343
   *
344
   * @return The character encoding.
345
   */
346
  private Charset detectEncoding( final byte[] bytes ) {
347
    final UniversalDetector detector = new UniversalDetector( null );
348
    detector.handleData( bytes, 0, bytes.length );
349
    detector.dataEnd();
350
351
    final String charset = detector.getDetectedCharset();
352
    final Charset charEncoding = charset == null
353
      ? Charset.defaultCharset()
354
      : Charset.forName( charset.toUpperCase( ENGLISH ) );
355
356
    detector.reset();
357
358
    return charEncoding;
359
  }
360
361
  /**
362
   * Converts the given string to an array of bytes using the encoding that was
363
   * originally detected (if any) and associated with this file.
364
   *
365
   * @param text The text to convert into the original file encoding.
366
   *
367
   * @return A series of bytes ready for writing to a file.
368
   */
369
  private byte[] asBytes( final String text ) {
370
    return text.getBytes( getEncoding() );
371
  }
372
373
  /**
374
   * Converts the given bytes into a Java String. This will call setEncoding
375
   * with the encoding detected by the CharsetDetector.
376
   *
377
   * @param text The text of unknown character encoding.
378
   *
379
   * @return The text, in its auto-detected encoding, as a String.
380
   */
381
  private String asString( final byte[] text ) {
382
    setEncoding( detectEncoding( text ) );
383
    return new String( text, getEncoding() );
384
  }
385
386
  public Path getPath() {
387
    return this.path;
388
  }
389
390
  public void setPath( final Path path ) {
391
    this.path = path;
392
393
    updateTab();
394
  }
395
396
  /**
397
   * Answers whether this tab has an initialized path reference.
398
   *
399
   * @return false This tab has no path.
400
   */
401
  public boolean isFileOpen() {
402
    return this.path != null;
403
  }
404
405
  public boolean isModified() {
406
    return this.modified.get();
407
  }
408
409
  ReadOnlyBooleanProperty modifiedProperty() {
410
    return this.modified.getReadOnlyProperty();
411
  }
412
413
  BooleanProperty canUndoProperty() {
414
    return this.canUndo;
415
  }
416
417
  BooleanProperty canRedoProperty() {
418
    return this.canRedo;
419
  }
420
421
  private UndoManager getUndoManager() {
422
    return getEditorPane().getUndoManager();
423
  }
424
425
  /**
426
   * Forwards the request to the editor pane.
427
   *
428
   * @param <T> The type of event listener to add.
429
   * @param <U> The type of consumer to add.
430
   * @param event The event that should trigger updates to the listener.
431
   * @param consumer The listener to receive update events.
432
   */
433
  public <T extends Event, U extends T> void addEventListener(
434
    final EventPattern<? super T, ? extends U> event,
435
    final Consumer<? super U> consumer ) {
436
    getEditorPane().addKeyboardListener( event, consumer );
437
  }
438
439
  /**
440
   * Forwards to the editor pane's listeners for keyboard events.
441
   *
442
   * @param map The new input map to replace the existing keyboard listener.
443
   */
444
  public void addEventListener( final InputMap<InputEvent> map ) {
445
    getEditorPane().addEventListener( map );
446
  }
447
448
  /**
449
   * Forwards to the editor pane's listeners for keyboard events.
450
   *
451
   * @param map The existing input map to remove from the keyboard listeners.
452
   */
453
  public void removeEventListener( final InputMap<InputEvent> map ) {
454
    getEditorPane().removeEventListener( map );
455
  }
456
457
  /**
458
   * Forwards to the editor pane's listeners for text change events.
459
   *
460
   * @param listener The listener to notify when the text changes.
461
   */
462
  public void addTextChangeListener( final ChangeListener<String> listener ) {
463
    getEditorPane().addTextChangeListener( listener );
464
  }
465
466
  /**
467
   * Forwards to the editor pane's listeners for caret paragraph change events.
468
   *
469
   * @param listener The listener to notify when the caret changes paragraphs.
470
   */
471
  public void addCaretParagraphListener( final ChangeListener<Integer> listener ) {
472
    getEditorPane().addCaretParagraphListener( listener );
473
  }
474
475
  /**
476
   * Forwards the request to the editor pane.
477
   *
478
   * @return The text to process.
479
   */
480
  public String getEditorText() {
481
    return getEditorPane().getText();
482
  }
483
484
  /**
485
   * Returns the editor pane, or creates one if it doesn't yet exist.
486
   *
487
   * @return The editor pane, never null.
488
   */
489
  public synchronized EditorPane getEditorPane() {
490
    if( this.editorPane == null ) {
491
      this.editorPane = new MarkdownEditorPane();
492
    }
493
494
    return this.editorPane;
495
  }
496
497
  private Notifier getNotifyService() {
498
    return this.alertService;
499
  }
500
501
  /**
502
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
503
   * determined.
504
   * 
505
   * @return The file encoding or UTF-8 if unknown.
506
   */
507
  private Charset getEncoding() {
508
    if( this.encoding == null ) {
509
      this.encoding = UTF_8;
510
    }
511
    
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.BooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.SimpleBooleanProperty;
38
import javafx.beans.value.ChangeListener;
39
import javafx.beans.value.ObservableValue;
40
import javafx.event.Event;
41
import javafx.scene.Node;
42
import javafx.scene.Scene;
43
import javafx.scene.control.Tab;
44
import javafx.scene.control.Tooltip;
45
import javafx.scene.input.InputEvent;
46
import javafx.scene.text.Text;
47
import javafx.stage.Window;
48
import org.fxmisc.richtext.StyleClassedTextArea;
49
import org.fxmisc.richtext.model.TwoDimensional.Position;
50
import org.fxmisc.undo.UndoManager;
51
import org.fxmisc.wellbehaved.event.EventPattern;
52
import org.fxmisc.wellbehaved.event.InputMap;
53
import org.mozilla.universalchardet.UniversalDetector;
54
55
import java.io.IOException;
56
import java.nio.charset.Charset;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.util.function.Consumer;
60
61
import static java.nio.charset.StandardCharsets.UTF_8;
62
import static java.util.Locale.ENGLISH;
63
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
64
65
/**
66
 * Editor for a single file.
67
 *
68
 * @author Karl Tauber and White Magic Software, Ltd.
69
 */
70
public final class FileEditorTab extends Tab {
71
72
  /**
73
   *
74
   */
75
  private final Notifier alertService = Services.load( Notifier.class );
76
  private EditorPane editorPane;
77
78
  /**
79
   * Character encoding used by the file (or default encoding if none found).
80
   */
81
  private Charset encoding;
82
83
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
84
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
85
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
86
87
  private Path path;
88
89
  public FileEditorTab( final Path path ) {
90
    setPath( path );
91
92
    this.modified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
93
94
    setOnSelectionChanged( e -> {
95
      if( isSelected() ) {
96
        Platform.runLater( this::activated );
97
      }
98
    } );
99
  }
100
101
  private void updateTab() {
102
    setText( getTabTitle() );
103
    setGraphic( getModifiedMark() );
104
    setTooltip( getTabTooltip() );
105
  }
106
107
  /**
108
   * Returns the base filename (without the directory names).
109
   *
110
   * @return The untitled text if the path hasn't been set.
111
   */
112
  private String getTabTitle() {
113
    final Path filePath = getPath();
114
115
    return (filePath == null)
116
        ? Messages.get( "FileEditor.untitled" )
117
        : filePath.getFileName().toString();
118
  }
119
120
  /**
121
   * Returns the full filename represented by the path.
122
   *
123
   * @return The untitled text if the path hasn't been set.
124
   */
125
  private Tooltip getTabTooltip() {
126
    final Path filePath = getPath();
127
    return new Tooltip( filePath == null ? "" : filePath.toString() );
128
  }
129
130
  /**
131
   * Returns a marker to indicate whether the file has been modified.
132
   *
133
   * @return "*" when the file has changed; otherwise null.
134
   */
135
  private Text getModifiedMark() {
136
    return isModified() ? new Text( "*" ) : null;
137
  }
138
139
  /**
140
   * Called when the user switches tab.
141
   */
142
  private void activated() {
143
    // Tab is closed or no longer active.
144
    if( getTabPane() == null || !isSelected() ) {
145
      return;
146
    }
147
148
    // Switch to the tab without loading if the contents are already in memory.
149
    if( getContent() != null ) {
150
      getEditorPane().requestFocus();
151
      return;
152
    }
153
154
    // Load the text and update the preview before the undo manager.
155
    load();
156
157
    // Track undo requests -- can only be called *after* load.
158
    initUndoManager();
159
    initLayout();
160
    initFocus();
161
  }
162
163
  private void initLayout() {
164
    setContent( getScrollPane() );
165
  }
166
167
  private Node getScrollPane() {
168
    return getEditorPane().getScrollPane();
169
  }
170
171
  private void initFocus() {
172
    getEditorPane().requestFocus();
173
  }
174
175
  private void initUndoManager() {
176
    final UndoManager undoManager = getUndoManager();
177
178
    // Clear undo history after first load.
179
    undoManager.forgetHistory();
180
181
    // Bind the editor undo manager to the properties.
182
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
183
    canUndo.bind( undoManager.undoAvailableProperty() );
184
    canRedo.bind( undoManager.redoAvailableProperty() );
185
  }
186
187
  /**
188
   * Searches from the caret position forward for the given string.
189
   *
190
   * @param needle The text string to match.
191
   */
192
  public void searchNext( final String needle ) {
193
    final String haystack = getEditorText();
194
    int index = haystack.indexOf( needle, getCaretPosition() );
195
196
    // Wrap around.
197
    if( index == -1 ) {
198
      index = haystack.indexOf( needle, 0 );
199
    }
200
201
    if( index >= 0 ) {
202
      setCaretPosition( index );
203
      getEditor().selectRange( index, index + needle.length() );
204
    }
205
  }
206
207
  /**
208
   * Returns the index into the text where the caret blinks happily away.
209
   *
210
   * @return A number from 0 to the editor's document text length.
211
   */
212
  public int getCaretPosition() {
213
    return getEditor().getCaretPosition();
214
  }
215
216
  /**
217
   * Moves the caret to a given offset.
218
   *
219
   * @param offset The new caret offset.
220
   */
221
  private void setCaretPosition( final int offset ) {
222
    getEditor().moveTo( offset );
223
    getEditor().requestFollowCaret();
224
  }
225
226
  /**
227
   * Returns the caret's current row and column position.
228
   *
229
   * @return The caret's offset into the document.
230
   */
231
  public Position getCaretOffset() {
232
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
233
  }
234
235
  /**
236
   * Allows observers to synchronize caret position changes.
237
   *
238
   * @return An observable caret property value.
239
   */
240
  public final ObservableValue<Integer> caretPositionProperty() {
241
    return getEditor().caretPositionProperty();
242
  }
243
244
  /**
245
   * Returns the text area associated with this tab.
246
   *
247
   * @return A text editor.
248
   */
249
  private StyleClassedTextArea getEditor() {
250
    return getEditorPane().getEditor();
251
  }
252
253
  /**
254
   * Returns true if the given path exactly matches this tab's path.
255
   *
256
   * @param check The path to compare against.
257
   * @return true The paths are the same.
258
   */
259
  public boolean isPath( final Path check ) {
260
    final Path filePath = getPath();
261
262
    return filePath != null && filePath.equals( check );
263
  }
264
265
  /**
266
   * Reads the entire file contents from the path associated with this tab.
267
   */
268
  private void load() {
269
    final Path filePath = getPath();
270
271
    if( filePath != null ) {
272
      try {
273
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
274
        getEditorPane().scrollToTop();
275
      } catch( final Exception ex ) {
276
        getNotifyService().notify( ex );
277
      }
278
    }
279
  }
280
281
  /**
282
   * Saves the entire file contents from the path associated with this tab.
283
   *
284
   * @return true The file has been saved.
285
   */
286
  public boolean save() {
287
    try {
288
      final EditorPane editor = getEditorPane();
289
      Files.write( getPath(), asBytes( editor.getText() ) );
290
      editor.getUndoManager().mark();
291
      return true;
292
    } catch( final IOException ex ) {
293
      return alert(
294
          "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
295
      );
296
    }
297
  }
298
299
  /**
300
   * Creates an alert dialog and waits for it to close.
301
   *
302
   * @param titleKey   Resource bundle key for the alert dialog title.
303
   * @param messageKey Resource bundle key for the alert dialog message.
304
   * @param e          The unexpected happening.
305
   * @return false
306
   */
307
  private boolean alert(
308
      final String titleKey, final String messageKey, final Exception e ) {
309
    final Notifier service = getNotifyService();
310
    final Path filePath = getPath();
311
312
    final Notification message = service.createNotification(
313
        Messages.get( titleKey ),
314
        Messages.get( messageKey ),
315
        filePath == null ? "" : filePath,
316
        e.getMessage()
317
    );
318
319
    try {
320
      service.createError( getWindow(), message ).showAndWait();
321
    } catch( final Exception ex ) {
322
      getNotifyService().notify( ex );
323
    }
324
325
    return false;
326
  }
327
328
  private Window getWindow() {
329
    final Scene scene = getEditorPane().getScene();
330
331
    if( scene == null ) {
332
      throw new UnsupportedOperationException( "" );
333
    }
334
335
    return scene.getWindow();
336
  }
337
338
  /**
339
   * Returns a best guess at the file encoding. If the encoding could not be
340
   * detected, this will return the default charset for the JVM.
341
   *
342
   * @param bytes The bytes to perform character encoding detection.
343
   * @return The character encoding.
344
   */
345
  private Charset detectEncoding( final byte[] bytes ) {
346
    final UniversalDetector detector = new UniversalDetector( null );
347
    detector.handleData( bytes, 0, bytes.length );
348
    detector.dataEnd();
349
350
    final String charset = detector.getDetectedCharset();
351
    final Charset charEncoding = charset == null
352
        ? Charset.defaultCharset()
353
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
354
355
    detector.reset();
356
357
    return charEncoding;
358
  }
359
360
  /**
361
   * Converts the given string to an array of bytes using the encoding that was
362
   * originally detected (if any) and associated with this file.
363
   *
364
   * @param text The text to convert into the original file encoding.
365
   * @return A series of bytes ready for writing to a file.
366
   */
367
  private byte[] asBytes( final String text ) {
368
    return text.getBytes( getEncoding() );
369
  }
370
371
  /**
372
   * Converts the given bytes into a Java String. This will call setEncoding
373
   * with the encoding detected by the CharsetDetector.
374
   *
375
   * @param text The text of unknown character encoding.
376
   * @return The text, in its auto-detected encoding, as a String.
377
   */
378
  private String asString( final byte[] text ) {
379
    setEncoding( detectEncoding( text ) );
380
    return new String( text, getEncoding() );
381
  }
382
383
  public Path getPath() {
384
    return this.path;
385
  }
386
387
  public void setPath( final Path path ) {
388
    this.path = path;
389
390
    updateTab();
391
  }
392
393
  /**
394
   * Answers whether this tab has an initialized path reference.
395
   *
396
   * @return false This tab has no path.
397
   */
398
  public boolean isFileOpen() {
399
    return this.path != null;
400
  }
401
402
  public boolean isModified() {
403
    return this.modified.get();
404
  }
405
406
  ReadOnlyBooleanProperty modifiedProperty() {
407
    return this.modified.getReadOnlyProperty();
408
  }
409
410
  BooleanProperty canUndoProperty() {
411
    return this.canUndo;
412
  }
413
414
  BooleanProperty canRedoProperty() {
415
    return this.canRedo;
416
  }
417
418
  private UndoManager getUndoManager() {
419
    return getEditorPane().getUndoManager();
420
  }
421
422
  /**
423
   * Forwards the request to the editor pane.
424
   *
425
   * @param <T>      The type of event listener to add.
426
   * @param <U>      The type of consumer to add.
427
   * @param event    The event that should trigger updates to the listener.
428
   * @param consumer The listener to receive update events.
429
   */
430
  public <T extends Event, U extends T> void addEventListener(
431
      final EventPattern<? super T, ? extends U> event,
432
      final Consumer<? super U> consumer ) {
433
    getEditorPane().addKeyboardListener( event, consumer );
434
  }
435
436
  /**
437
   * Forwards to the editor pane's listeners for keyboard events.
438
   *
439
   * @param map The new input map to replace the existing keyboard listener.
440
   */
441
  public void addEventListener( final InputMap<InputEvent> map ) {
442
    getEditorPane().addEventListener( map );
443
  }
444
445
  /**
446
   * Forwards to the editor pane's listeners for keyboard events.
447
   *
448
   * @param map The existing input map to remove from the keyboard listeners.
449
   */
450
  public void removeEventListener( final InputMap<InputEvent> map ) {
451
    getEditorPane().removeEventListener( map );
452
  }
453
454
  /**
455
   * Forwards to the editor pane's listeners for text change events.
456
   *
457
   * @param listener The listener to notify when the text changes.
458
   */
459
  public void addTextChangeListener( final ChangeListener<String> listener ) {
460
    getEditorPane().addTextChangeListener( listener );
461
  }
462
463
  /**
464
   * Forwards to the editor pane's listeners for caret paragraph change events.
465
   *
466
   * @param listener The listener to notify when the caret changes paragraphs.
467
   */
468
  public void addCaretParagraphListener(
469
      final ChangeListener<Integer> listener ) {
470
    getEditorPane().addCaretParagraphListener( listener );
471
  }
472
473
  /**
474
   * Forwards the request to the editor pane.
475
   *
476
   * @return The text to process.
477
   */
478
  public String getEditorText() {
479
    return getEditorPane().getText();
480
  }
481
482
  /**
483
   * Returns the editor pane, or creates one if it doesn't yet exist.
484
   *
485
   * @return The editor pane, never null.
486
   */
487
  public synchronized EditorPane getEditorPane() {
488
    if( this.editorPane == null ) {
489
      this.editorPane = new MarkdownEditorPane();
490
    }
491
492
    return this.editorPane;
493
  }
494
495
  private Notifier getNotifyService() {
496
    return this.alertService;
497
  }
498
499
  /**
500
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
501
   * determined.
502
   *
503
   * @return The file encoding or UTF-8 if unknown.
504
   */
505
  private Charset getEncoding() {
506
    if( this.encoding == null ) {
507
      this.encoding = UTF_8;
508
    }
509
512510
    return this.encoding;
513511
  }
M src/main/java/com/scrivenvar/FileEditorTabPane.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
31
import static com.scrivenvar.FileType.*;
32
import static com.scrivenvar.Messages.get;
33
34
import com.scrivenvar.predicates.files.FileTypePredicate;
35
import com.scrivenvar.service.Options;
36
import com.scrivenvar.service.Settings;
37
import com.scrivenvar.service.events.Notification;
38
import com.scrivenvar.service.events.Notifier;
39
40
import static com.scrivenvar.service.events.Notifier.NO;
41
import static com.scrivenvar.service.events.Notifier.YES;
42
43
import com.scrivenvar.util.Utils;
44
45
import java.io.File;
46
import java.nio.file.Path;
47
import java.util.ArrayList;
48
import java.util.List;
49
import java.util.function.Consumer;
50
import java.util.prefs.Preferences;
51
import java.util.stream.Collectors;
52
53
import javafx.beans.property.ReadOnlyBooleanProperty;
54
import javafx.beans.property.ReadOnlyBooleanWrapper;
55
import javafx.beans.property.ReadOnlyObjectProperty;
56
import javafx.beans.property.ReadOnlyObjectWrapper;
57
import javafx.beans.value.ChangeListener;
58
import javafx.beans.value.ObservableValue;
59
import javafx.collections.ListChangeListener;
60
import javafx.collections.ObservableList;
61
import javafx.event.Event;
62
import javafx.scene.Node;
63
import javafx.scene.control.Alert;
64
import javafx.scene.control.ButtonType;
65
import javafx.scene.control.Tab;
66
import javafx.scene.control.TabPane;
67
import javafx.scene.control.TabPane.TabClosingPolicy;
68
import javafx.scene.input.InputEvent;
69
import javafx.stage.FileChooser;
70
import javafx.stage.FileChooser.ExtensionFilter;
71
import javafx.stage.Window;
72
import org.fxmisc.richtext.StyledTextArea;
73
import org.fxmisc.wellbehaved.event.EventPattern;
74
import org.fxmisc.wellbehaved.event.InputMap;
75
76
/**
77
 * Tab pane for file editors.
78
 *
79
 * @author Karl Tauber and White Magic Software, Ltd.
80
 */
81
public final class FileEditorTabPane extends TabPane {
82
83
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
84
85
  private final Options options = Services.load( Options.class );
86
  private final Settings settings = Services.load( Settings.class );
87
  private final Notifier notifyService = Services.load( Notifier.class );
88
89
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
90
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
91
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
92
93
  /**
94
   * Constructs a new file editor tab pane.
95
   */
96
  public FileEditorTabPane() {
97
    final ObservableList<Tab> tabs = getTabs();
98
99
    setFocusTraversable( false );
100
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
101
102
    addTabSelectionListener(
103
      ( ObservableValue<? extends Tab> tabPane,
104
        final Tab oldTab, final Tab newTab ) -> {
105
106
        if( newTab != null ) {
107
          activeFileEditor.set( (FileEditorTab) newTab );
108
        }
109
      }
110
    );
111
112
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue, newValue ) -> {
113
      for( final Tab tab : tabs ) {
114
        if( ((FileEditorTab) tab).isModified() ) {
115
          this.anyFileEditorModified.set( true );
116
          break;
117
        }
118
      }
119
    };
120
121
    tabs.addListener(
122
      (ListChangeListener<Tab>) change -> {
123
        while( change.next() ) {
124
          if( change.wasAdded() ) {
125
            change.getAddedSubList().stream().forEach( ( tab ) -> {
126
              ((FileEditorTab) tab).modifiedProperty().addListener( modifiedListener );
127
            } );
128
          } else if( change.wasRemoved() ) {
129
            change.getRemoved().stream().forEach( ( tab ) -> {
130
              ((FileEditorTab) tab).modifiedProperty().removeListener( modifiedListener );
131
            } );
132
          }
133
        }
134
135
        // Changes in the tabs may also change anyFileEditorModified property
136
        // (e.g. closed modified file)
137
        modifiedListener.changed( null, null, null );
138
      }
139
    );
140
  }
141
142
  /**
143
   * Delegates to the active file editor.
144
   *
145
   * @param <T>      Event type.
146
   * @param <U>      Consumer type.
147
   * @param event    Event to pass to the editor.
148
   * @param consumer Consumer to pass to the editor.
149
   */
150
  public <T extends Event, U extends T> void addEventListener(
151
    final EventPattern<? super T, ? extends U> event,
152
    final Consumer<? super U> consumer ) {
153
    getActiveFileEditor().addEventListener( event, consumer );
154
  }
155
156
  /**
157
   * Delegates to the active file editor pane, and, ultimately, to its text
158
   * area.
159
   *
160
   * @param map The map of methods to events.
161
   */
162
  public void addEventListener( final InputMap<InputEvent> map ) {
163
    getActiveFileEditor().addEventListener( map );
164
  }
165
166
  /**
167
   * Remove a keyboard event listener from the active file editor.
168
   *
169
   * @param map The keyboard events to remove.
170
   */
171
  public void removeEventListener( final InputMap<InputEvent> map ) {
172
    getActiveFileEditor().removeEventListener( map );
173
  }
174
175
  /**
176
   * Allows observers to be notified when the current file editor tab changes.
177
   *
178
   * @param listener The listener to notify of tab change events.
179
   */
180
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
181
    // Observe the tab so that when a new tab is opened or selected,
182
    // a notification is kicked off.
183
    getSelectionModel().selectedItemProperty().addListener( listener );
184
  }
185
186
  /**
187
   * Allows clients to manipulate the editor content directly.
188
   *
189
   * @return The text area for the active file editor.
190
   */
191
  public StyledTextArea getEditor() {
192
    return getActiveFileEditor().getEditorPane().getEditor();
193
  }
194
195
  /**
196
   * Returns the tab that has keyboard focus.
197
   *
198
   * @return A non-null instance.
199
   */
200
  public FileEditorTab getActiveFileEditor() {
201
    return this.activeFileEditor.get();
202
  }
203
204
  /**
205
   * Returns the property corresponding to the tab that has focus.
206
   *
207
   * @return A non-null instance.
208
   */
209
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
210
    return this.activeFileEditor.getReadOnlyProperty();
211
  }
212
213
  /**
214
   * Property that can answer whether the text has been modified.
215
   *
216
   * @return A non-null instance, true meaning the content has not been saved.
217
   */
218
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
219
    return this.anyFileEditorModified.getReadOnlyProperty();
220
  }
221
222
  /**
223
   * Creates a new editor instance from the given path.
224
   *
225
   * @param path The file to open.
226
   * @return A non-null instance.
227
   */
228
  private FileEditorTab createFileEditor( final Path path ) {
229
    final FileEditorTab tab = new FileEditorTab( path );
230
231
    tab.setOnCloseRequest( e -> {
232
      if( !canCloseEditor( tab ) ) {
233
        e.consume();
234
      }
235
    } );
236
237
    return tab;
238
  }
239
240
  /**
241
   * Called when the user selects New from the File menu.
242
   *
243
   * @return The newly added tab.
244
   */
245
  void newEditor() {
246
    final FileEditorTab tab = createFileEditor( null );
247
248
    getTabs().add( tab );
249
    getSelectionModel().select( tab );
250
  }
251
252
  void openFileDialog() {
253
    final String title = get( "Dialog.file.choose.open.title" );
254
    final FileChooser dialog = createFileChooser( title );
255
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
256
257
    if( files != null ) {
258
      openFiles( files );
259
    }
260
  }
261
262
  /**
263
   * Opens the files into new editors, unless one of those files was a
264
   * definition file. The definition file is loaded into the definition pane,
265
   * but only the first one selected (multiple definition files will result in a
266
   * warning).
267
   *
268
   * @param files The list of non-definition files that the were requested to
269
   *              open.
270
   * @return A list of files that can be opened in text editors.
271
   */
272
  private void openFiles( final List<File> files ) {
273
    final FileTypePredicate predicate
274
      = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
275
276
    // The user might have opened multiple definitions files. These will
277
    // be discarded from the text editable files.
278
    final List<File> definitions
279
      = files.stream().filter( predicate ).collect( Collectors.toList() );
280
281
    // Create a modifiable list to remove any definition files that were
282
    // opened.
283
    final List<File> editors = new ArrayList<>( files );
284
285
    if( editors.size() > 0 ) {
286
      saveLastDirectory( editors.get( 0 ) );
287
    }
288
289
    editors.removeAll( definitions );
290
291
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
292
    if( editors.size() > 0 ) {
293
      openEditors( editors, 0 );
294
    }
295
296
    if( definitions.size() > 0 ) {
297
      openDefinition( definitions.get( 0 ) );
298
    }
299
  }
300
301
  private void openEditors( final List<File> files, final int activeIndex ) {
302
    final int fileTally = files.size();
303
    final List<Tab> tabs = getTabs();
304
305
    // Close single unmodified "Untitled" tab.
306
    if( tabs.size() == 1 ) {
307
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
308
309
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
310
        closeEditor( fileEditor, false );
311
      }
312
    }
313
314
    for( int i = 0; i < fileTally; i++ ) {
315
      final Path path = files.get( i ).toPath();
316
317
      FileEditorTab fileEditorTab = findEditor( path );
318
319
      // Only open new files.
320
      if( fileEditorTab == null ) {
321
        fileEditorTab = createFileEditor( path );
322
        getTabs().add( fileEditorTab );
323
      }
324
325
      // Select the first file in the list.
326
      if( i == activeIndex ) {
327
        getSelectionModel().select( fileEditorTab );
328
      }
329
    }
330
  }
331
332
  /**
333
   * Returns a property that changes when a new definition file is opened.
334
   *
335
   * @return The path to a definition file that was opened.
336
   */
337
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
338
    return getOnOpenDefinitionFile().getReadOnlyProperty();
339
  }
340
341
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
342
    return this.openDefinition;
343
  }
344
345
  /**
346
   * Called when the user has opened a definition file (using the file open
347
   * dialog box). This will replace the current set of definitions for the
348
   * active tab.
349
   *
350
   * @param definition The file to open.
351
   */
352
  private void openDefinition( final File definition ) {
353
    // TODO: Prevent reading this file twice when a new text document is opened.
354
    // (might be a matter of checking the value first).
355
    getOnOpenDefinitionFile().set( definition.toPath() );
356
  }
357
358
  /**
359
   * Called when the contents of the editor are to be saved.
360
   *
361
   * @param tab The tab containing content to save.
362
   * @return true The contents were saved (or needn't be saved).
363
   */
364
  public boolean saveEditor( final FileEditorTab tab ) {
365
    if( tab == null || !tab.isModified() ) {
366
      return true;
367
    }
368
369
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
370
  }
371
372
  /**
373
   * Opens the Save As dialog for the user to save the content under a new
374
   * path.
375
   *
376
   * @param tab The tab with contents to save.
377
   * @return true The contents were saved, or the tab was null.
378
   */
379
  public boolean saveEditorAs( final FileEditorTab tab ) {
380
    if( tab == null ) {
381
      return true;
382
    }
383
384
    getSelectionModel().select( tab );
385
386
    final FileChooser fileChooser = createFileChooser( get( "Dialog.file.choose.save.title" ) );
387
    final File file = fileChooser.showSaveDialog( getWindow() );
388
    if( file == null ) {
389
      return false;
390
    }
391
392
    saveLastDirectory( file );
393
    tab.setPath( file.toPath() );
394
395
    return tab.save();
396
  }
397
398
  boolean saveAllEditors() {
399
    boolean success = true;
400
401
    for( FileEditorTab fileEditor : getAllEditors() ) {
402
      if( !saveEditor( fileEditor ) ) {
403
        success = false;
404
      }
405
    }
406
407
    return success;
408
  }
409
410
  /**
411
   * Answers whether the file has had modifications. '
412
   *
413
   * @param tab THe tab to check for modifications.
414
   * @return false The file is unmodified.
415
   */
416
  boolean canCloseEditor( final FileEditorTab tab ) {
417
    if( !tab.isModified() ) {
418
      return true;
419
    }
420
421
    final Notification message = getNotifyService().createNotification(
422
      Messages.get( "Alert.file.close.title" ),
423
      Messages.get( "Alert.file.close.text" ),
424
      tab.getText()
425
    );
426
427
    final Alert alert = getNotifyService().createConfirmation(
428
      getWindow(), message );
429
    final ButtonType response = alert.showAndWait().get();
430
431
    return response == YES ? saveEditor( tab ) : response == NO;
432
  }
433
434
  private Notifier getNotifyService() {
435
    return this.notifyService;
436
  }
437
438
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
439
    if( fileEditor == null ) {
440
      return true;
441
    }
442
443
    final Tab tab = fileEditor;
444
445
    if( save ) {
446
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
447
      Event.fireEvent( tab, event );
448
449
      if( event.isConsumed() ) {
450
        return false;
451
      }
452
    }
453
454
    getTabs().remove( tab );
455
456
    if( tab.getOnClosed() != null ) {
457
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
458
    }
459
460
    return true;
461
  }
462
463
  boolean closeAllEditors() {
464
    final FileEditorTab[] allEditors = getAllEditors();
465
    final FileEditorTab activeEditor = getActiveFileEditor();
466
467
    // try to save active tab first because in case the user decides to cancel,
468
    // then it stays active
469
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
470
      return false;
471
    }
472
473
    // This should be called any time a tab changes.
474
    persistPreferences();
475
476
    // save modified tabs
477
    for( int i = 0; i < allEditors.length; i++ ) {
478
      final FileEditorTab fileEditor = allEditors[ i ];
479
480
      if( fileEditor == activeEditor ) {
481
        continue;
482
      }
483
484
      if( fileEditor.isModified() ) {
485
        // activate the modified tab to make its modified content visible to the user
486
        getSelectionModel().select( i );
487
488
        if( !canCloseEditor( fileEditor ) ) {
489
          return false;
490
        }
491
      }
492
    }
493
494
    // Close all tabs.
495
    for( final FileEditorTab fileEditor : allEditors ) {
496
      if( !closeEditor( fileEditor, false ) ) {
497
        return false;
498
      }
499
    }
500
501
    return getTabs().isEmpty();
502
  }
503
504
  private FileEditorTab[] getAllEditors() {
505
    final ObservableList<Tab> tabs = getTabs();
506
    final int length = tabs.size();
507
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
508
509
    for( int i = 0; i < length; i++ ) {
510
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
511
    }
512
513
    return allEditors;
514
  }
515
516
  /**
517
   * Returns the file editor tab that has the given path.
518
   *
519
   * @return null No file editor tab for the given path was found.
520
   */
521
  private FileEditorTab findEditor( final Path path ) {
522
    for( final Tab tab : getTabs() ) {
523
      final FileEditorTab fileEditor = (FileEditorTab) tab;
524
525
      if( fileEditor.isPath( path ) ) {
526
        return fileEditor;
527
      }
528
    }
529
530
    return null;
531
  }
532
533
  private FileChooser createFileChooser( String title ) {
534
    final FileChooser fileChooser = new FileChooser();
535
536
    fileChooser.setTitle( title );
537
    fileChooser.getExtensionFilters().addAll(
538
      createExtensionFilters() );
539
540
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
541
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
542
543
    if( !file.isDirectory() ) {
544
      file = new File( "." );
545
    }
546
547
    fileChooser.setInitialDirectory( file );
548
    return fileChooser;
549
  }
550
551
  private List<ExtensionFilter> createExtensionFilters() {
552
    final List<ExtensionFilter> list = new ArrayList<>();
553
554
    // TODO: Return a list of all properties that match the filter prefix.
555
    // This will allow dynamic filters to be added and removed just by
556
    // updating the properties file.
557
    list.add( createExtensionFilter( SOURCE ) );
558
    list.add( createExtensionFilter( DEFINITION ) );
559
    list.add( createExtensionFilter( XML ) );
560
    list.add( createExtensionFilter( ALL ) );
561
    return list;
562
  }
563
564
  /**
565
   * Returns a filter for file name extensions recognized by the application
566
   * that can be opened by the user.
567
   *
568
   * @param filetype Used to find the globbing pattern for extensions.
569
   * @return A filename filter suitable for use by a FileDialog instance.
570
   */
571
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
572
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
573
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
574
575
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
576
  }
577
578
  private List<String> getExtensions( final String key ) {
579
    return getSettings().getStringSettingList( key );
580
  }
581
582
  private void saveLastDirectory( final File file ) {
583
    getPreferences().put( "lastDirectory", file.getParent() );
584
  }
585
586
  public void restorePreferences() {
587
    int activeIndex = 0;
588
589
    final Preferences preferences = getPreferences();
590
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
591
    final String activeFileName = preferences.get( "activeFile", null );
592
593
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
594
595
    for( final String fileName : fileNames ) {
596
      final File file = new File( fileName );
597
598
      if( file.exists() ) {
599
        files.add( file );
600
601
        if( fileName.equals( activeFileName ) ) {
602
          activeIndex = files.size() - 1;
603
        }
604
      }
605
    }
606
607
    if( files.isEmpty() ) {
608
      newEditor();
609
    } else {
610
      openEditors( files, activeIndex );
611
    }
612
  }
613
614
  public void persistPreferences() {
615
    final ObservableList<Tab> allEditors = getTabs();
616
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
617
618
    for( final Tab tab : allEditors ) {
619
      final FileEditorTab fileEditor = (FileEditorTab) tab;
620
      final Path filePath = fileEditor.getPath();
621
622
      if( filePath != null ) {
623
        fileNames.add( filePath.toString() );
624
      }
625
    }
626
627
    final Preferences preferences = getPreferences();
628
    Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
629
630
    final FileEditorTab activeEditor = getActiveFileEditor();
631
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
632
633
    if( filePath == null ) {
634
      preferences.remove( "activeFile" );
635
    } else {
30
import com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Settings;
33
import com.scrivenvar.service.events.Notification;
34
import com.scrivenvar.service.events.Notifier;
35
import com.scrivenvar.util.Utils;
36
import javafx.beans.property.ReadOnlyBooleanProperty;
37
import javafx.beans.property.ReadOnlyBooleanWrapper;
38
import javafx.beans.property.ReadOnlyObjectProperty;
39
import javafx.beans.property.ReadOnlyObjectWrapper;
40
import javafx.beans.value.ChangeListener;
41
import javafx.beans.value.ObservableValue;
42
import javafx.collections.ListChangeListener;
43
import javafx.collections.ObservableList;
44
import javafx.event.Event;
45
import javafx.scene.Node;
46
import javafx.scene.control.Alert;
47
import javafx.scene.control.ButtonType;
48
import javafx.scene.control.Tab;
49
import javafx.scene.control.TabPane;
50
import javafx.scene.input.InputEvent;
51
import javafx.stage.FileChooser;
52
import javafx.stage.FileChooser.ExtensionFilter;
53
import javafx.stage.Window;
54
import org.fxmisc.richtext.StyledTextArea;
55
import org.fxmisc.wellbehaved.event.EventPattern;
56
import org.fxmisc.wellbehaved.event.InputMap;
57
58
import java.io.File;
59
import java.nio.file.Path;
60
import java.util.ArrayList;
61
import java.util.List;
62
import java.util.function.Consumer;
63
import java.util.prefs.Preferences;
64
import java.util.stream.Collectors;
65
66
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
67
import static com.scrivenvar.FileType.*;
68
import static com.scrivenvar.Messages.get;
69
import static com.scrivenvar.service.events.Notifier.NO;
70
import static com.scrivenvar.service.events.Notifier.YES;
71
72
/**
73
 * Tab pane for file editors.
74
 *
75
 * @author Karl Tauber and White Magic Software, Ltd.
76
 */
77
public final class FileEditorTabPane extends TabPane {
78
79
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose" +
80
      ".filter";
81
82
  private final Options options = Services.load( Options.class );
83
  private final Settings settings = Services.load( Settings.class );
84
  private final Notifier notifyService = Services.load( Notifier.class );
85
86
  private final ReadOnlyObjectWrapper<Path> openDefinition =
87
      new ReadOnlyObjectWrapper<>();
88
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor =
89
      new ReadOnlyObjectWrapper<>();
90
  private final ReadOnlyBooleanWrapper anyFileEditorModified =
91
      new ReadOnlyBooleanWrapper();
92
93
  /**
94
   * Constructs a new file editor tab pane.
95
   */
96
  public FileEditorTabPane() {
97
    final ObservableList<Tab> tabs = getTabs();
98
99
    setFocusTraversable( false );
100
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
101
102
    addTabSelectionListener(
103
        ( ObservableValue<? extends Tab> tabPane,
104
          final Tab oldTab, final Tab newTab ) -> {
105
106
          if( newTab != null ) {
107
            activeFileEditor.set( (FileEditorTab) newTab );
108
          }
109
        }
110
    );
111
112
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
113
                                                       newValue ) -> {
114
      for( final Tab tab : tabs ) {
115
        if( ((FileEditorTab) tab).isModified() ) {
116
          this.anyFileEditorModified.set( true );
117
          break;
118
        }
119
      }
120
    };
121
122
    tabs.addListener(
123
        (ListChangeListener<Tab>) change -> {
124
          while( change.next() ) {
125
            if( change.wasAdded() ) {
126
              change.getAddedSubList().forEach(
127
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
128
                                                  .addListener( modifiedListener ) );
129
            }
130
            else if( change.wasRemoved() ) {
131
              change.getRemoved().forEach(
132
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
133
                                                  .removeListener(
134
                                                      modifiedListener ) );
135
            }
136
          }
137
138
          // Changes in the tabs may also change anyFileEditorModified property
139
          // (e.g. closed modified file)
140
          modifiedListener.changed( null, null, null );
141
        }
142
    );
143
  }
144
145
  /**
146
   * Delegates to the active file editor.
147
   *
148
   * @param <T>      Event type.
149
   * @param <U>      Consumer type.
150
   * @param event    Event to pass to the editor.
151
   * @param consumer Consumer to pass to the editor.
152
   */
153
  public <T extends Event, U extends T> void addEventListener(
154
      final EventPattern<? super T, ? extends U> event,
155
      final Consumer<? super U> consumer ) {
156
    getActiveFileEditor().addEventListener( event, consumer );
157
  }
158
159
  /**
160
   * Delegates to the active file editor pane, and, ultimately, to its text
161
   * area.
162
   *
163
   * @param map The map of methods to events.
164
   */
165
  public void addEventListener( final InputMap<InputEvent> map ) {
166
    getActiveFileEditor().addEventListener( map );
167
  }
168
169
  /**
170
   * Remove a keyboard event listener from the active file editor.
171
   *
172
   * @param map The keyboard events to remove.
173
   */
174
  public void removeEventListener( final InputMap<InputEvent> map ) {
175
    getActiveFileEditor().removeEventListener( map );
176
  }
177
178
  /**
179
   * Allows observers to be notified when the current file editor tab changes.
180
   *
181
   * @param listener The listener to notify of tab change events.
182
   */
183
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
184
    // Observe the tab so that when a new tab is opened or selected,
185
    // a notification is kicked off.
186
    getSelectionModel().selectedItemProperty().addListener( listener );
187
  }
188
189
  /**
190
   * Allows clients to manipulate the editor content directly.
191
   *
192
   * @return The text area for the active file editor.
193
   */
194
  public StyledTextArea getEditor() {
195
    return getActiveFileEditor().getEditorPane().getEditor();
196
  }
197
198
  /**
199
   * Returns the tab that has keyboard focus.
200
   *
201
   * @return A non-null instance.
202
   */
203
  public FileEditorTab getActiveFileEditor() {
204
    return this.activeFileEditor.get();
205
  }
206
207
  /**
208
   * Returns the property corresponding to the tab that has focus.
209
   *
210
   * @return A non-null instance.
211
   */
212
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
213
    return this.activeFileEditor.getReadOnlyProperty();
214
  }
215
216
  /**
217
   * Property that can answer whether the text has been modified.
218
   *
219
   * @return A non-null instance, true meaning the content has not been saved.
220
   */
221
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
222
    return this.anyFileEditorModified.getReadOnlyProperty();
223
  }
224
225
  /**
226
   * Creates a new editor instance from the given path.
227
   *
228
   * @param path The file to open.
229
   * @return A non-null instance.
230
   */
231
  private FileEditorTab createFileEditor( final Path path ) {
232
    final FileEditorTab tab = new FileEditorTab( path );
233
234
    tab.setOnCloseRequest( e -> {
235
      if( !canCloseEditor( tab ) ) {
236
        e.consume();
237
      }
238
    } );
239
240
    return tab;
241
  }
242
243
  /**
244
   * Called when the user selects New from the File menu.
245
   */
246
  void newEditor() {
247
    final FileEditorTab tab = createFileEditor( null );
248
249
    getTabs().add( tab );
250
    getSelectionModel().select( tab );
251
  }
252
253
  void openFileDialog() {
254
    final String title = get( "Dialog.file.choose.open.title" );
255
    final FileChooser dialog = createFileChooser( title );
256
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
257
258
    if( files != null ) {
259
      openFiles( files );
260
    }
261
  }
262
263
  /**
264
   * Opens the files into new editors, unless one of those files was a
265
   * definition file. The definition file is loaded into the definition pane,
266
   * but only the first one selected (multiple definition files will result in a
267
   * warning).
268
   *
269
   * @param files The list of non-definition files that the were requested to
270
   *              open.
271
   */
272
  private void openFiles( final List<File> files ) {
273
    final FileTypePredicate predicate
274
        =
275
        new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
276
277
    // The user might have opened multiple definitions files. These will
278
    // be discarded from the text editable files.
279
    final List<File> definitions
280
        = files.stream().filter( predicate ).collect( Collectors.toList() );
281
282
    // Create a modifiable list to remove any definition files that were
283
    // opened.
284
    final List<File> editors = new ArrayList<>( files );
285
286
    if( editors.size() > 0 ) {
287
      saveLastDirectory( editors.get( 0 ) );
288
    }
289
290
    editors.removeAll( definitions );
291
292
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
293
    if( editors.size() > 0 ) {
294
      openEditors( editors, 0 );
295
    }
296
297
    if( definitions.size() > 0 ) {
298
      openDefinition( definitions.get( 0 ) );
299
    }
300
  }
301
302
  private void openEditors( final List<File> files, final int activeIndex ) {
303
    final int fileTally = files.size();
304
    final List<Tab> tabs = getTabs();
305
306
    // Close single unmodified "Untitled" tab.
307
    if( tabs.size() == 1 ) {
308
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
309
310
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
311
        closeEditor( fileEditor, false );
312
      }
313
    }
314
315
    for( int i = 0; i < fileTally; i++ ) {
316
      final Path path = files.get( i ).toPath();
317
318
      FileEditorTab fileEditorTab = findEditor( path );
319
320
      // Only open new files.
321
      if( fileEditorTab == null ) {
322
        fileEditorTab = createFileEditor( path );
323
        getTabs().add( fileEditorTab );
324
      }
325
326
      // Select the first file in the list.
327
      if( i == activeIndex ) {
328
        getSelectionModel().select( fileEditorTab );
329
      }
330
    }
331
  }
332
333
  /**
334
   * Returns a property that changes when a new definition file is opened.
335
   *
336
   * @return The path to a definition file that was opened.
337
   */
338
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
339
    return getOnOpenDefinitionFile().getReadOnlyProperty();
340
  }
341
342
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
343
    return this.openDefinition;
344
  }
345
346
  /**
347
   * Called when the user has opened a definition file (using the file open
348
   * dialog box). This will replace the current set of definitions for the
349
   * active tab.
350
   *
351
   * @param definition The file to open.
352
   */
353
  private void openDefinition( final File definition ) {
354
    // TODO: Prevent reading this file twice when a new text document is opened.
355
    // (might be a matter of checking the value first).
356
    getOnOpenDefinitionFile().set( definition.toPath() );
357
  }
358
359
  /**
360
   * Called when the contents of the editor are to be saved.
361
   *
362
   * @param tab The tab containing content to save.
363
   * @return true The contents were saved (or needn't be saved).
364
   */
365
  public boolean saveEditor( final FileEditorTab tab ) {
366
    if( tab == null || !tab.isModified() ) {
367
      return true;
368
    }
369
370
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
371
  }
372
373
  /**
374
   * Opens the Save As dialog for the user to save the content under a new
375
   * path.
376
   *
377
   * @param tab The tab with contents to save.
378
   * @return true The contents were saved, or the tab was null.
379
   */
380
  public boolean saveEditorAs( final FileEditorTab tab ) {
381
    if( tab == null ) {
382
      return true;
383
    }
384
385
    getSelectionModel().select( tab );
386
387
    final FileChooser fileChooser = createFileChooser( get(
388
        "Dialog.file.choose.save.title" ) );
389
    final File file = fileChooser.showSaveDialog( getWindow() );
390
    if( file == null ) {
391
      return false;
392
    }
393
394
    saveLastDirectory( file );
395
    tab.setPath( file.toPath() );
396
397
    return tab.save();
398
  }
399
400
  void saveAllEditors() {
401
    for( final FileEditorTab fileEditor : getAllEditors() ) {
402
      saveEditor( fileEditor );
403
    }
404
  }
405
406
  /**
407
   * Answers whether the file has had modifications. '
408
   *
409
   * @param tab THe tab to check for modifications.
410
   * @return false The file is unmodified.
411
   */
412
  boolean canCloseEditor( final FileEditorTab tab ) {
413
    if( !tab.isModified() ) {
414
      return true;
415
    }
416
417
    final Notification message = getNotifyService().createNotification(
418
        Messages.get( "Alert.file.close.title" ),
419
        Messages.get( "Alert.file.close.text" ),
420
        tab.getText()
421
    );
422
423
    final Alert alert = getNotifyService().createConfirmation(
424
        getWindow(), message );
425
    final ButtonType response = alert.showAndWait().get();
426
427
    return response == YES ? saveEditor( tab ) : response == NO;
428
  }
429
430
  private Notifier getNotifyService() {
431
    return this.notifyService;
432
  }
433
434
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
435
    if( tab == null ) {
436
      return true;
437
    }
438
439
    if( save ) {
440
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
441
      Event.fireEvent( tab, event );
442
443
      if( event.isConsumed() ) {
444
        return false;
445
      }
446
    }
447
448
    getTabs().remove( tab );
449
450
    if( tab.getOnClosed() != null ) {
451
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
452
    }
453
454
    return true;
455
  }
456
457
  boolean closeAllEditors() {
458
    final FileEditorTab[] allEditors = getAllEditors();
459
    final FileEditorTab activeEditor = getActiveFileEditor();
460
461
    // try to save active tab first because in case the user decides to cancel,
462
    // then it stays active
463
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
464
      return false;
465
    }
466
467
    // This should be called any time a tab changes.
468
    persistPreferences();
469
470
    // save modified tabs
471
    for( int i = 0; i < allEditors.length; i++ ) {
472
      final FileEditorTab fileEditor = allEditors[ i ];
473
474
      if( fileEditor == activeEditor ) {
475
        continue;
476
      }
477
478
      if( fileEditor.isModified() ) {
479
        // activate the modified tab to make its modified content visible to
480
        // the user
481
        getSelectionModel().select( i );
482
483
        if( !canCloseEditor( fileEditor ) ) {
484
          return false;
485
        }
486
      }
487
    }
488
489
    // Close all tabs.
490
    for( final FileEditorTab fileEditor : allEditors ) {
491
      if( !closeEditor( fileEditor, false ) ) {
492
        return false;
493
      }
494
    }
495
496
    return getTabs().isEmpty();
497
  }
498
499
  private FileEditorTab[] getAllEditors() {
500
    final ObservableList<Tab> tabs = getTabs();
501
    final int length = tabs.size();
502
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
503
504
    for( int i = 0; i < length; i++ ) {
505
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
506
    }
507
508
    return allEditors;
509
  }
510
511
  /**
512
   * Returns the file editor tab that has the given path.
513
   *
514
   * @return null No file editor tab for the given path was found.
515
   */
516
  private FileEditorTab findEditor( final Path path ) {
517
    for( final Tab tab : getTabs() ) {
518
      final FileEditorTab fileEditor = (FileEditorTab) tab;
519
520
      if( fileEditor.isPath( path ) ) {
521
        return fileEditor;
522
      }
523
    }
524
525
    return null;
526
  }
527
528
  private FileChooser createFileChooser( String title ) {
529
    final FileChooser fileChooser = new FileChooser();
530
531
    fileChooser.setTitle( title );
532
    fileChooser.getExtensionFilters().addAll(
533
        createExtensionFilters() );
534
535
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
536
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
537
538
    if( !file.isDirectory() ) {
539
      file = new File( "." );
540
    }
541
542
    fileChooser.setInitialDirectory( file );
543
    return fileChooser;
544
  }
545
546
  private List<ExtensionFilter> createExtensionFilters() {
547
    final List<ExtensionFilter> list = new ArrayList<>();
548
549
    // TODO: Return a list of all properties that match the filter prefix.
550
    // This will allow dynamic filters to be added and removed just by
551
    // updating the properties file.
552
    list.add( createExtensionFilter( SOURCE ) );
553
    list.add( createExtensionFilter( DEFINITION ) );
554
    list.add( createExtensionFilter( XML ) );
555
    list.add( createExtensionFilter( ALL ) );
556
    return list;
557
  }
558
559
  /**
560
   * Returns a filter for file name extensions recognized by the application
561
   * that can be opened by the user.
562
   *
563
   * @param filetype Used to find the globbing pattern for extensions.
564
   * @return A filename filter suitable for use by a FileDialog instance.
565
   */
566
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
567
    final String tKey = String.format( "%s.title.%s",
568
                                       FILTER_EXTENSION_TITLES,
569
                                       filetype );
570
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
571
572
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
573
  }
574
575
  private List<String> getExtensions( final String key ) {
576
    return getSettings().getStringSettingList( key );
577
  }
578
579
  private void saveLastDirectory( final File file ) {
580
    getPreferences().put( "lastDirectory", file.getParent() );
581
  }
582
583
  public void restorePreferences() {
584
    int activeIndex = 0;
585
586
    final Preferences preferences = getPreferences();
587
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
588
    final String activeFileName = preferences.get( "activeFile", null );
589
590
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
591
592
    for( final String fileName : fileNames ) {
593
      final File file = new File( fileName );
594
595
      if( file.exists() ) {
596
        files.add( file );
597
598
        if( fileName.equals( activeFileName ) ) {
599
          activeIndex = files.size() - 1;
600
        }
601
      }
602
    }
603
604
    if( files.isEmpty() ) {
605
      newEditor();
606
    }
607
    else {
608
      openEditors( files, activeIndex );
609
    }
610
  }
611
612
  public void persistPreferences() {
613
    final ObservableList<Tab> allEditors = getTabs();
614
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
615
616
    for( final Tab tab : allEditors ) {
617
      final FileEditorTab fileEditor = (FileEditorTab) tab;
618
      final Path filePath = fileEditor.getPath();
619
620
      if( filePath != null ) {
621
        fileNames.add( filePath.toString() );
622
      }
623
    }
624
625
    final Preferences preferences = getPreferences();
626
    Utils.putPrefsStrings( preferences,
627
                           "file",
628
                           fileNames.toArray( new String[ 0 ] ) );
629
630
    final FileEditorTab activeEditor = getActiveFileEditor();
631
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
632
633
    if( filePath == null ) {
634
      preferences.remove( "activeFile" );
635
    }
636
    else {
636637
      preferences.put( "activeFile", filePath.toString() );
637638
    }
M src/main/java/com/scrivenvar/FileType.java
4949
  PROPERTIES( "properties" );
5050
51
  private final String type;
51
  private final String mType;
5252
5353
  /**
5454
   * Default constructor for enumerated file type.
5555
   *
5656
   * @param type Human-readable name for the file type.
5757
   */
58
  private FileType( final String type ) {
59
    this.type = type;
58
  FileType( final String type ) {
59
    mType = type;
6060
  }
6161
...
9797
   */
9898
  private String getType() {
99
    return this.type;
99
    return mType;
100100
  }
101101
A src/main/java/com/scrivenvar/Launcher.java
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.scrivenvar;
29
30
/**
31
 * Launches the application using the {@link Main} class.
32
 *
33
 * <p>
34
 * This is required until modules are implemented, which may never happen
35
 * because the application should be ported away from Java and JavaFX.
36
 * </p>
37
 */
38
public class Launcher {
39
  public static void main( final String[] args ) {
40
    Main.main( args );
41
  }
42
}
143
M src/main/java/com/scrivenvar/Main.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
3230
import com.scrivenvar.preferences.FilePreferencesFactory;
3331
import com.scrivenvar.service.Options;
3432
import com.scrivenvar.service.Snitch;
3533
import com.scrivenvar.service.events.Notifier;
3634
import com.scrivenvar.util.StageState;
37
import java.util.logging.LogManager;
3835
import javafx.application.Application;
3936
import javafx.scene.Scene;
4037
import javafx.scene.image.Image;
4138
import javafx.stage.Stage;
39
40
import java.util.logging.LogManager;
41
42
import static com.scrivenvar.Constants.*;
43
import static com.scrivenvar.Messages.get;
4244
4345
/**
44
 * Main application entry point. The application allows users to edit Markdown
46
 * Application entry point. The application allows users to edit Markdown
4547
 * files and see a real-time preview of the edits.
4648
 *
4749
 * @author Karl Tauber and White Magic Software, Ltd.
4850
 */
4951
public final class Main extends Application {
5052
51
  private Options options;
52
  private Snitch snitch;
53
  private Thread snitchThread;
53
  // Suppress logging errors to standard output.
54
  static {
55
    LogManager.getLogManager().reset();
56
  }
5457
55
  private static Application app;
56
  private final MainWindow mainWindow = new MainWindow();
58
  private static Application sApplication;
59
60
  private final Options mOptions = Services.load( Options.class );
61
  private final Notifier mNotifier = Services.load( Notifier.class );
62
  private final Snitch mSnitch = Services.load( Snitch.class );
63
  private final Thread mSnitchThread = new Thread( getSnitch() );
64
  private final MainWindow mMainWindow = new MainWindow();
65
66
  private StageState mStageState;
5767
5868
  public static void main( final String[] args ) {
59
    initLogger();
6069
    initPreferences();
6170
    launch( args );
62
  }
63
64
  /**
65
   * Prevents JavaFX from logging to standard error.
66
   *
67
   * @see http://stackoverflow.com/a/41476462/59087
68
   */
69
  private static void initLogger() {
70
    LogManager.getLogManager().reset();
7171
  }
7272
7373
  /**
7474
   * Sets the factory used for reading user preferences.
7575
   */
7676
  private static void initPreferences() {
7777
    System.setProperty(
78
      "java.util.prefs.PreferencesFactory",
79
      FilePreferencesFactory.class.getName()
78
        "java.util.prefs.PreferencesFactory",
79
        FilePreferencesFactory.class.getName()
8080
    );
8181
  }
8282
8383
  /**
8484
   * Application entry point.
8585
   *
8686
   * @param stage The primary application stage.
87
   *
88
   * @throws Exception Could not read configuration file.
8987
   */
9088
  @Override
91
  public void start( final Stage stage ) throws Exception {
89
  public void start( final Stage stage ) {
9290
    initApplication();
9391
    initNotifyService();
...
104102
105103
  private void initApplication() {
106
    Main.app = this;
104
    sApplication = this;
107105
  }
108106
109107
  /**
110108
   * Constructs the notify service and appends the main window to the list of
111109
   * notification observers.
112110
   */
113111
  private void initNotifyService() {
114
    final Notifier notifier = Services.load( Notifier.class );
115
    notifier.addObserver( getMainWindow() );
112
    mNotifier.addObserver( getMainWindow() );
116113
  }
117114
118
  private StageState initState( final Stage stage ) {
119
    return new StageState( stage, getOptions().getState() );
115
  private void initState( final Stage stage ) {
116
    mStageState = new StageState( stage, getOptions().getState() );
120117
  }
121118
122119
  private void initStage( final Stage stage ) {
123120
    stage.getIcons().addAll(
124
      createImage( FILE_LOGO_16 ),
125
      createImage( FILE_LOGO_32 ),
126
      createImage( FILE_LOGO_128 ),
127
      createImage( FILE_LOGO_256 ),
128
      createImage( FILE_LOGO_512 ) );
121
        createImage( FILE_LOGO_16 ),
122
        createImage( FILE_LOGO_32 ),
123
        createImage( FILE_LOGO_128 ),
124
        createImage( FILE_LOGO_256 ),
125
        createImage( FILE_LOGO_512 ) );
129126
    stage.setTitle( getApplicationTitle() );
130127
    stage.setScene( getScene() );
131128
  }
132129
133130
  /**
134131
   * Watch for file system changes.
135132
   */
136133
  private void initSnitch() {
137
    setSnitchThread( new Thread( getSnitch() ) );
138134
    getSnitchThread().start();
139135
  }
...
149145
150146
    final Thread thread = getSnitchThread();
151
152
    if( thread != null ) {
153
      thread.interrupt();
154
      thread.join();
155
    }
147
    thread.interrupt();
148
    thread.join();
156149
  }
157150
158151
  private synchronized Snitch getSnitch() {
159
    if( this.snitch == null ) {
160
      this.snitch = Services.load( Snitch.class );
161
    }
162
163
    return this.snitch;
152
    return mSnitch;
164153
  }
165154
166155
  private Thread getSnitchThread() {
167
    return this.snitchThread;
168
  }
169
170
  private void setSnitchThread( final Thread thread ) {
171
    this.snitchThread = thread;
156
    return mSnitchThread;
172157
  }
173158
174159
  private synchronized Options getOptions() {
175
    if( this.options == null ) {
176
      this.options = Services.load( Options.class );
177
    }
178
179
    return this.options;
160
    return mOptions;
180161
  }
181162
182163
  private Scene getScene() {
183164
    return getMainWindow().getScene();
184165
  }
185166
186167
  private MainWindow getMainWindow() {
187
    return this.mainWindow;
168
    return mMainWindow;
188169
  }
189170
190
  private String getApplicationTitle() {
191
    return get( "Main.title" );
171
  private static Application getApplication() {
172
    return sApplication;
192173
  }
193174
194
  private static Application getApplication() {
195
    return Main.app;
175
  private String getApplicationTitle() {
176
    return get( "Main.title" );
196177
  }
197178
M src/main/java/com/scrivenvar/MainWindow.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
32
import com.scrivenvar.definition.*;
33
import com.scrivenvar.dialogs.RScriptDialog;
34
import com.scrivenvar.editors.EditorPane;
35
import com.scrivenvar.editors.VariableNameInjector;
36
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
37
import com.scrivenvar.predicates.files.FileTypePredicate;
38
import com.scrivenvar.preview.HTMLPreviewPane;
39
import com.scrivenvar.processors.Processor;
40
import com.scrivenvar.processors.ProcessorFactory;
41
import com.scrivenvar.service.Options;
42
import com.scrivenvar.service.Snitch;
43
import com.scrivenvar.service.events.Notifier;
44
import com.scrivenvar.util.Action;
45
import com.scrivenvar.util.ActionUtils;
46
import static com.scrivenvar.util.StageState.*;
47
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
48
import java.io.IOException;
49
import java.nio.file.Path;
50
import java.util.HashMap;
51
import java.util.Map;
52
import java.util.Observable;
53
import java.util.Observer;
54
import java.util.Optional;
55
import java.util.function.Function;
56
import java.util.prefs.Preferences;
57
import javafx.application.Platform;
58
import javafx.beans.binding.Bindings;
59
import javafx.beans.binding.BooleanBinding;
60
import javafx.beans.property.BooleanProperty;
61
import javafx.beans.property.SimpleBooleanProperty;
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 static javafx.event.Event.fireEvent;
67
import javafx.geometry.Pos;
68
import javafx.scene.Node;
69
import javafx.scene.Scene;
70
import javafx.scene.control.Alert;
71
import javafx.scene.control.Alert.AlertType;
72
import javafx.scene.control.Menu;
73
import javafx.scene.control.MenuBar;
74
import javafx.scene.control.SplitPane;
75
import javafx.scene.control.Tab;
76
import javafx.scene.control.TextField;
77
import javafx.scene.control.ToolBar;
78
import javafx.scene.control.TreeView;
79
import javafx.scene.image.Image;
80
import javafx.scene.image.ImageView;
81
import static javafx.scene.input.KeyCode.ESCAPE;
82
import javafx.scene.input.KeyEvent;
83
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
84
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
85
import javafx.scene.layout.BorderPane;
86
import javafx.scene.layout.VBox;
87
import javafx.scene.text.Text;
88
import javafx.stage.Window;
89
import javafx.stage.WindowEvent;
90
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
91
import org.controlsfx.control.StatusBar;
92
import org.fxmisc.richtext.model.TwoDimensional.Position;
93
94
/**
95
 * Main window containing a tab pane in the center for file editors.
96
 *
97
 * @author Karl Tauber and White Magic Software, Ltd.
98
 */
99
public class MainWindow implements Observer {
100
101
  private final Options options = Services.load( Options.class );
102
  private final Snitch snitch = Services.load( Snitch.class );
103
  private final Notifier notifier = Services.load( Notifier.class );
104
105
  private Scene scene;
106
  private MenuBar menuBar;
107
  private StatusBar statusBar;
108
  private Text lineNumberText;
109
  private TextField findTextField;
110
111
  private DefinitionSource definitionSource;
112
  private DefinitionPane definitionPane;
113
  private FileEditorTabPane fileEditorPane;
114
  private HTMLPreviewPane previewPane;
115
116
  /**
117
   * Prevents re-instantiation of processing classes.
118
   */
119
  private Map<FileEditorTab, Processor<String>> processors;
120
121
  /**
122
   * Listens on the definition pane for double-click events.
123
   */
124
  private VariableNameInjector variableNameInjector;
125
126
  public MainWindow() {
127
    initLayout();
128
    initFindInput();
129
    initSnitch();
130
    initDefinitionListener();
131
    initTabAddedListener();
132
    initTabChangedListener();
133
    initPreferences();
134
  }
135
136
  /**
137
   * Watch for changes to external files. In particular, this awaits
138
   * modifications to any XSL files associated with XML files being edited. When
139
   * an XSL file is modified (external to the application), the snitch's ears
140
   * perk up and the file is reloaded. This keeps the XSL transformation up to
141
   * date with what's on the file system.
142
   */
143
  private void initSnitch() {
144
    getSnitch().addObserver( this );
145
  }
146
147
  /**
148
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
149
   * presses.
150
   */
151
  private void initFindInput() {
152
    final TextField input = getFindTextField();
153
154
    input.setOnKeyPressed( (KeyEvent event) -> {
155
      switch( event.getCode() ) {
156
        case F3:
157
        case ENTER:
158
          findNext();
159
          break;
160
        case F:
161
          if( !event.isControlDown() ) {
162
            break;
163
          }
164
        case ESCAPE:
165
          getStatusBar().setGraphic( null );
166
          getActiveFileEditor().getEditorPane().requestFocus();
167
          break;
168
      }
169
    } );
170
171
    // Remove when the input field loses focus.
172
    input.focusedProperty().addListener(
173
      (
174
        final ObservableValue<? extends Boolean> focused,
175
        final Boolean oFocus,
176
        final Boolean nFocus) -> {
177
        if( !nFocus ) {
178
          getStatusBar().setGraphic( null );
179
        }
180
      }
181
    );
182
  }
183
184
  /**
185
   * Listen for file editor tab pane to receive an open definition source event.
186
   */
187
  private void initDefinitionListener() {
188
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
189
      (ObservableValue<? extends Path> definitionFile,
190
        final Path oldPath, final Path newPath) -> {
191
        openDefinition( newPath );
192
193
        // Indirectly refresh the resolved map.
194
        setProcessors( null );
195
        updateDefinitionPane();
196
197
        try {
198
          getSnitch().ignore( oldPath );
199
          getSnitch().listen( newPath );
200
        }
201
        catch( final IOException ex ) {
202
          error( ex );
203
        }
204
205
        // Will create new processors and therefore a new resolved map.
206
        refreshSelectedTab( getActiveFileEditor() );
207
      }
208
    );
209
  }
210
211
  /**
212
   * When tabs are added, hook the various change listeners onto the new tab so
213
   * that the preview pane refreshes as necessary.
214
   */
215
  private void initTabAddedListener() {
216
    final FileEditorTabPane editorPane = getFileEditorPane();
217
218
    // Make sure the text processor kicks off when new files are opened.
219
    final ObservableList<Tab> tabs = editorPane.getTabs();
220
221
    // Update the preview pane on tab changes.
222
    tabs.addListener(
223
      (final Change<? extends Tab> change) -> {
224
        while( change.next() ) {
225
          if( change.wasAdded() ) {
226
            // Multiple tabs can be added simultaneously.
227
            for( final Tab newTab : change.getAddedSubList() ) {
228
              final FileEditorTab tab = (FileEditorTab)newTab;
229
230
              initTextChangeListener( tab );
231
              initCaretParagraphListener( tab );
232
              initKeyboardEventListeners( tab );
233
//              initSyntaxListener( tab );
234
            }
235
          }
236
        }
237
      }
238
    );
239
  }
240
241
  /**
242
   * Reloads the preferences from the previous session.
243
   */
244
  private void initPreferences() {
245
    restoreDefinitionSource();
246
    getFileEditorPane().restorePreferences();
247
    updateDefinitionPane();
248
  }
249
250
  /**
251
   * Listen for new tab selection events.
252
   */
253
  private void initTabChangedListener() {
254
    final FileEditorTabPane editorPane = getFileEditorPane();
255
256
    // Update the preview pane changing tabs.
257
    editorPane.addTabSelectionListener(
258
      (ObservableValue<? extends Tab> tabPane,
259
        final Tab oldTab, final Tab newTab) -> {
260
        updateVariableNameInjector();
261
262
        // If there was no old tab, then this is a first time load, which
263
        // can be ignored.
264
        if( oldTab != null ) {
265
          if( newTab == null ) {
266
            closeRemainingTab();
267
          }
268
          else {
269
            // Update the preview with the edited text.
270
            refreshSelectedTab( (FileEditorTab)newTab );
271
          }
272
        }
273
      }
274
    );
275
  }
276
277
  /**
278
   * Ensure that the keyboard events are received when a new tab is added
279
   * to the user interface.
280
   *
281
   * @param tab The tab that can trigger keyboard events, such as control+space.
282
   */
283
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
284
    final VariableNameInjector vin = getVariableNameInjector();
285
    vin.initKeyboardEventListeners( tab );
286
  }
287
288
  private void initTextChangeListener( final FileEditorTab tab ) {
289
    tab.addTextChangeListener(
290
      (ObservableValue<? extends String> editor,
291
        final String oldValue, final String newValue) -> {
292
        refreshSelectedTab( tab );
293
      }
294
    );
295
  }
296
297
  private void initCaretParagraphListener( final FileEditorTab tab ) {
298
    tab.addCaretParagraphListener(
299
      (ObservableValue<? extends Integer> editor,
300
        final Integer oldValue, final Integer newValue) -> {
301
        refreshSelectedTab( tab );
302
      }
303
    );
304
  }
305
306
  private void updateVariableNameInjector() {
307
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
308
  }
309
310
  private void setVariableNameInjector( final VariableNameInjector injector ) {
311
    this.variableNameInjector = injector;
312
  }
313
314
  private synchronized VariableNameInjector getVariableNameInjector() {
315
    if( this.variableNameInjector == null ) {
316
      final VariableNameInjector vin = createVariableNameInjector();
317
      setVariableNameInjector( vin );
318
    }
319
320
    return this.variableNameInjector;
321
  }
322
323
  private VariableNameInjector createVariableNameInjector() {
324
    final FileEditorTab tab = getActiveFileEditor();
325
    final DefinitionPane pane = getDefinitionPane();
326
327
    return new VariableNameInjector( tab, pane );
328
  }
329
330
  /**
331
   * Add a listener for variable name injection the given tab.
332
   *
333
   * @param tab The tab to inject variable names into upon a double-click.
334
   */
335
  private void initVariableNameInjector( final Tab tab ) {
336
    final FileEditorTab editorTab = (FileEditorTab)tab;
337
  }
338
339
  /**
340
   * Called whenever the preview pane becomes out of sync with the file editor
341
   * tab. This can be called when the text changes, the caret paragraph changes,
342
   * or the file tab changes.
343
   *
344
   * @param tab The file editor tab that has been changed in some fashion.
345
   */
346
  private void refreshSelectedTab( final FileEditorTab tab ) {
347
    if( tab.isFileOpen() ) {
348
      getPreviewPane().setPath( tab.getPath() );
349
350
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
351
      final Position p = tab.getCaretOffset();
352
      getLineNumberText().setText(
353
        get( STATUS_BAR_LINE,
354
             p.getMajor() + 1,
355
             p.getMinor() + 1,
356
             tab.getCaretPosition() + 1
357
        )
358
      );
359
360
      Processor<String> processor = getProcessors().get( tab );
361
362
      if( processor == null ) {
363
        processor = createProcessor( tab );
364
        getProcessors().put( tab, processor );
365
      }
366
367
      try {
368
        getNotifier().clear();
369
        processor.processChain( tab.getEditorText() );
370
      }
371
      catch( final Exception ex ) {
372
        error( ex );
373
      }
374
    }
375
  }
376
377
  /**
378
   * Used to find text in the active file editor window.
379
   */
380
  private void find() {
381
    final TextField input = getFindTextField();
382
    getStatusBar().setGraphic( input );
383
    input.requestFocus();
384
  }
385
386
  public void findNext() {
387
    getActiveFileEditor().searchNext( getFindTextField().getText() );
388
  }
389
390
  /**
391
   * Returns the variable map of interpolated definitions.
392
   *
393
   * @return A map to help dereference variables.
394
   */
395
  private Map<String, String> getResolvedMap() {
396
    return getDefinitionSource().getResolvedMap();
397
  }
398
399
  /**
400
   * Returns the root node for the hierarchical definition source.
401
   *
402
   * @return Data to display in the definition pane.
403
   */
404
  private TreeView<String> getTreeView() {
405
    try {
406
      return getDefinitionSource().asTreeView();
407
    }
408
    catch( Exception e ) {
409
      error( e );
410
    }
411
412
    // Slightly redundant as getDefinitionSource() might have returned an
413
    // empty definition source.
414
    return (new EmptyDefinitionSource()).asTreeView();
415
  }
416
417
  /**
418
   * Called when a definition source is opened.
419
   *
420
   * @param path Path to the definition source that was opened.
421
   */
422
  private void openDefinition( final Path path ) {
423
    try {
424
      final DefinitionSource ds = createDefinitionSource( path.toString() );
425
      setDefinitionSource( ds );
426
      storeDefinitionSource();
427
      updateDefinitionPane();
428
    }
429
    catch( final Exception e ) {
430
      error( e );
431
    }
432
  }
433
434
  private void updateDefinitionPane() {
435
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
436
  }
437
438
  private void restoreDefinitionSource() {
439
    final Preferences preferences = getPreferences();
440
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, null );
441
442
    // If there's no definition source set, don't try to load it.
443
    if( source != null ) {
444
      setDefinitionSource( createDefinitionSource( source ) );
445
    }
446
  }
447
448
  private void storeDefinitionSource() {
449
    final Preferences preferences = getPreferences();
450
    final DefinitionSource ds = getDefinitionSource();
451
452
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
453
  }
454
455
  /**
456
   * Called when the last open tab is closed to clear the preview pane.
457
   */
458
  private void closeRemainingTab() {
459
    getPreviewPane().clear();
460
  }
461
462
  /**
463
   * Called when an exception occurs that warrants the user's attention.
464
   *
465
   * @param e The exception with a message that the user should know about.
466
   */
467
  private void error( final Exception e ) {
468
    getNotifier().notify( e );
469
  }
470
471
  //---- File actions -------------------------------------------------------
472
  /**
473
   * Called when an observable instance has changed. This is called by both the
474
   * snitch service and the notify service. The snitch service can be called for
475
   * different file types, including definition sources.
476
   *
477
   * @param observable The observed instance.
478
   * @param value The noteworthy item.
479
   */
480
  @Override
481
  public void update( final Observable observable, final Object value ) {
482
    if( value != null ) {
483
      if( observable instanceof Snitch && value instanceof Path ) {
484
        final Path path = (Path)value;
485
        final FileTypePredicate predicate
486
          = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
487
488
        // Reload definitions.
489
        if( predicate.test( path.toFile() ) ) {
490
          updateDefinitionSource( path );
491
        }
492
493
        updateSelectedTab();
494
      }
495
      else if( observable instanceof Notifier && value instanceof String ) {
496
        updateStatusBar( (String)value );
497
      }
498
    }
499
  }
500
501
  /**
502
   * Updates the status bar to show the given message.
503
   *
504
   * @param s The message to show in the status bar.
505
   */
506
  private void updateStatusBar( final String s ) {
507
    Platform.runLater(
508
      () -> {
509
        final int index = s.indexOf( '\n' );
510
        final String message = s.substring( 0, index > 0 ? index : s.length() );
511
512
        getStatusBar().setText( message );
513
      }
514
    );
515
  }
516
517
  /**
518
   * Called when a file has been modified.
519
   *
520
   * @param file Path to the modified file.
521
   */
522
  private void updateSelectedTab() {
523
    Platform.runLater(
524
      () -> {
525
        // Brute-force XSLT file reload by re-instantiating all processors.
526
        resetProcessors();
527
        refreshSelectedTab( getActiveFileEditor() );
528
      }
529
    );
530
  }
531
532
  /**
533
   * Reloads the definition source from the given path.
534
   *
535
   * @param path The path containing new definition information.
536
   */
537
  private void updateDefinitionSource( final Path path ) {
538
    Platform.runLater(
539
      () -> {
540
        openDefinition( path );
541
      }
542
    );
543
  }
544
545
  /**
546
   * After resetting the processors, they will refresh anew to be up-to-date
547
   * with the files (text and definition) currently loaded into the editor.
548
   */
549
  private void resetProcessors() {
550
    getProcessors().clear();
551
  }
552
553
  //---- File actions -------------------------------------------------------
554
  private void fileNew() {
555
    getFileEditorPane().newEditor();
556
  }
557
558
  private void fileOpen() {
559
    getFileEditorPane().openFileDialog();
560
  }
561
562
  private void fileClose() {
563
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
564
  }
565
566
  private void fileCloseAll() {
567
    getFileEditorPane().closeAllEditors();
568
  }
569
570
  private void fileSave() {
571
    getFileEditorPane().saveEditor( getActiveFileEditor() );
572
  }
573
574
  private void fileSaveAs() {
575
    final FileEditorTab editor = getActiveFileEditor();
576
    getFileEditorPane().saveEditorAs( editor );
577
    getProcessors().remove( editor );
578
579
    try {
580
      refreshSelectedTab( editor );
581
    }
582
    catch( final Exception ex ) {
583
      getNotifier().notify( ex );
584
    }
585
  }
586
587
  private void fileSaveAll() {
588
    getFileEditorPane().saveAllEditors();
589
  }
590
591
  private void fileExit() {
592
    final Window window = getWindow();
593
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
594
  }
595
596
  //---- Tools actions
597
  private void toolsScript() {
598
    final String script = getStartupScript();
599
600
    final RScriptDialog dialog = new RScriptDialog(
601
      getWindow(), "Dialog.rScript.title", script );
602
    final Optional<String> result = dialog.showAndWait();
603
604
    result.ifPresent( (String s) -> {
605
      putStartupScript( s );
606
    } );
607
  }
608
609
  /**
610
   * Gets the R startup script from the user preferences.
611
   */
612
  private String getStartupScript() {
613
    return getPreferences().get( PERSIST_R_STARTUP, "" );
614
  }
615
616
  /**
617
   * Puts an R startup script into the user preferences.
618
   */
619
  private void putStartupScript( final String s ) {
620
    try {
621
      getPreferences().put( PERSIST_R_STARTUP, s );
622
    }
623
    catch( final Exception ex ) {
624
      getNotifier().notify( ex );
625
    }
626
  }
627
628
  //---- Help actions -------------------------------------------------------
629
  private void helpAbout() {
630
    Alert alert = new Alert( AlertType.INFORMATION );
631
    alert.setTitle( get( "Dialog.about.title" ) );
632
    alert.setHeaderText( get( "Dialog.about.header" ) );
633
    alert.setContentText( get( "Dialog.about.content" ) );
634
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
635
    alert.initOwner( getWindow() );
636
637
    alert.showAndWait();
638
  }
639
640
  //---- Convenience accessors ----------------------------------------------
641
  private float getFloat( final String key, final float defaultValue ) {
642
    return getPreferences().getFloat( key, defaultValue );
643
  }
644
645
  private Preferences getPreferences() {
646
    return getOptions().getState();
647
  }
648
649
  protected Scene getScene() {
650
    if( this.scene == null ) {
651
      this.scene = createScene();
652
    }
653
654
    return this.scene;
655
  }
656
657
  public Window getWindow() {
658
    return getScene().getWindow();
659
  }
660
661
  private MarkdownEditorPane getActiveEditor() {
662
    final EditorPane pane = getActiveFileEditor().getEditorPane();
663
664
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
665
  }
666
667
  private FileEditorTab getActiveFileEditor() {
668
    return getFileEditorPane().getActiveFileEditor();
669
  }
670
671
  //---- Member accessors ---------------------------------------------------
672
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
673
    this.processors = map;
674
  }
675
676
  private Map<FileEditorTab, Processor<String>> getProcessors() {
677
    if( this.processors == null ) {
678
      setProcessors( new HashMap<>() );
679
    }
680
681
    return this.processors;
682
  }
683
684
  private FileEditorTabPane getFileEditorPane() {
685
    if( this.fileEditorPane == null ) {
686
      this.fileEditorPane = createFileEditorPane();
687
    }
688
689
    return this.fileEditorPane;
690
  }
691
692
  private HTMLPreviewPane getPreviewPane() {
693
    if( this.previewPane == null ) {
694
      this.previewPane = createPreviewPane();
695
    }
696
697
    return this.previewPane;
698
  }
699
700
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
701
    this.definitionSource = definitionSource;
702
  }
703
704
  private DefinitionSource getDefinitionSource() {
705
    if( this.definitionSource == null ) {
706
      this.definitionSource = new EmptyDefinitionSource();
707
    }
708
709
    return this.definitionSource;
710
  }
711
712
  private DefinitionPane getDefinitionPane() {
713
    if( this.definitionPane == null ) {
714
      this.definitionPane = createDefinitionPane();
715
    }
716
717
    return this.definitionPane;
718
  }
719
720
  private Options getOptions() {
721
    return this.options;
722
  }
723
724
  private Snitch getSnitch() {
725
    return this.snitch;
726
  }
727
728
  private Notifier getNotifier() {
729
    return this.notifier;
730
  }
731
732
  public void setMenuBar( final MenuBar menuBar ) {
733
    this.menuBar = menuBar;
734
  }
735
736
  public MenuBar getMenuBar() {
737
    return this.menuBar;
738
  }
739
740
  private Text getLineNumberText() {
741
    if( this.lineNumberText == null ) {
742
      this.lineNumberText = createLineNumberText();
743
    }
744
745
    return this.lineNumberText;
746
  }
747
748
  private synchronized StatusBar getStatusBar() {
749
    if( this.statusBar == null ) {
750
      this.statusBar = createStatusBar();
751
    }
752
753
    return this.statusBar;
754
  }
755
756
  private TextField getFindTextField() {
757
    if( this.findTextField == null ) {
758
      this.findTextField = createFindTextField();
759
    }
760
761
    return this.findTextField;
762
  }
763
764
  //---- Member creators ----------------------------------------------------
765
  /**
766
   * Factory to create processors that are suited to different file types.
767
   *
768
   * @param tab The tab that is subjected to processing.
769
   *
770
   * @return A processor suited to the file type specified by the tab's path.
771
   */
772
  private Processor<String> createProcessor( final FileEditorTab tab ) {
773
    return createProcessorFactory().createProcessor( tab );
774
  }
775
776
  private ProcessorFactory createProcessorFactory() {
777
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
778
  }
779
780
  private DefinitionSource createDefinitionSource( final String path ) {
781
    DefinitionSource ds;
782
783
    try {
784
      ds = createDefinitionFactory().createDefinitionSource( path );
785
786
      if( ds instanceof FileDefinitionSource ) {
787
        try {
788
          getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
789
        }
790
        catch( final IOException ex ) {
791
          error( ex );
792
        }
793
      }
794
    }
795
    catch( final Exception ex ) {
796
      ds = new EmptyDefinitionSource();
797
      error( ex );
798
    }
799
800
    return ds;
801
  }
802
803
  private TextField createFindTextField() {
804
    return new TextField();
805
  }
806
807
  /**
808
   * Create an editor pane to hold file editor tabs.
809
   *
810
   * @return A new instance, never null.
811
   */
812
  private FileEditorTabPane createFileEditorPane() {
813
    return new FileEditorTabPane();
814
  }
815
816
  private HTMLPreviewPane createPreviewPane() {
817
    return new HTMLPreviewPane();
818
  }
819
820
  private DefinitionPane createDefinitionPane() {
821
    return new DefinitionPane( getTreeView() );
822
  }
823
824
  private DefinitionFactory createDefinitionFactory() {
825
    return new DefinitionFactory();
826
  }
827
828
  private StatusBar createStatusBar() {
829
    return new StatusBar();
830
  }
831
832
  private Scene createScene() {
833
    final SplitPane splitPane = new SplitPane(
834
      getDefinitionPane().getNode(),
835
      getFileEditorPane().getNode(),
836
      getPreviewPane().getNode() );
837
838
    splitPane.setDividerPositions(
839
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
840
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
841
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
842
843
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
844
    final BorderPane borderPane = new BorderPane();
845
    borderPane.setPrefSize( 1024, 800 );
846
    borderPane.setTop( createMenuBar() );
847
    borderPane.setBottom( getStatusBar() );
848
    borderPane.setCenter( splitPane );
849
850
    final VBox box = new VBox();
851
    box.setAlignment( Pos.BASELINE_CENTER );
852
    box.getChildren().add( getLineNumberText() );
853
    getStatusBar().getRightItems().add( box );
854
855
    return new Scene( borderPane );
856
  }
857
858
  private Text createLineNumberText() {
859
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
860
  }
861
862
  private Node createMenuBar() {
863
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
864
865
    // File actions
866
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
867
                                             "Shortcut+N", FILE_ALT,
868
                                             e -> fileNew() );
869
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
870
                                              "Shortcut+O", FOLDER_OPEN_ALT,
871
                                              e -> fileOpen() );
872
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
873
                                               "Shortcut+W", null,
874
                                               e -> fileClose(),
875
                                               activeFileEditorIsNull );
876
    final Action fileCloseAllAction = new Action( get(
877
      "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
878
                                                  activeFileEditorIsNull );
879
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
880
                                              "Shortcut+S", FLOPPY_ALT,
881
                                              e -> fileSave(),
882
                                              createActiveBooleanProperty(
883
                                                FileEditorTab::modifiedProperty ).not() );
884
    final Action fileSaveAsAction = new Action( Messages.get(
885
      "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
886
                                                activeFileEditorIsNull );
887
    final Action fileSaveAllAction = new Action(
888
      get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
889
      e -> fileSaveAll(),
890
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
891
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null,
892
                                              null, e -> fileExit() );
893
894
    // Edit actions
895
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
896
                                              "Shortcut+Z", UNDO,
897
                                              e -> getActiveEditor().undo(),
898
                                              createActiveBooleanProperty(
899
                                                FileEditorTab::canUndoProperty ).not() );
900
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
901
                                              "Shortcut+Y", REPEAT,
902
                                              e -> getActiveEditor().redo(),
903
                                              createActiveBooleanProperty(
904
                                                FileEditorTab::canRedoProperty ).not() );
905
    final Action editFindAction = new Action( Messages.get(
906
      "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
907
                                              e -> find(),
908
                                              activeFileEditorIsNull );
909
    final Action editReplaceAction = new Action( Messages.get(
910
      "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
911
                                                 e -> getActiveEditor().replace(),
912
                                                 activeFileEditorIsNull );
913
    final Action editFindNextAction = new Action( Messages.get(
914
      "Main.menu.edit.find.next" ), "F3", null,
915
                                                  e -> findNext(),
916
                                                  activeFileEditorIsNull );
917
    final Action editFindPreviousAction = new Action( Messages.get(
918
      "Main.menu.edit.find.previous" ), "Shift+F3", null,
919
                                                      e -> getActiveEditor().findPrevious(),
920
                                                      activeFileEditorIsNull );
921
922
    // Insert actions
923
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
924
                                                "Shortcut+B", BOLD,
925
                                                e -> getActiveEditor().surroundSelection(
926
                                                  "**", "**" ),
927
                                                activeFileEditorIsNull );
928
    final Action insertItalicAction = new Action(
929
      get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
930
      e -> getActiveEditor().surroundSelection( "*", "*" ),
931
      activeFileEditorIsNull );
932
    final Action insertSuperscriptAction = new Action( get(
933
      "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
934
                                                       e -> getActiveEditor().surroundSelection(
935
                                                         "^", "^" ),
936
                                                       activeFileEditorIsNull );
937
    final Action insertSubscriptAction = new Action( get(
938
      "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
939
                                                     e -> getActiveEditor().surroundSelection(
940
                                                       "~", "~" ),
941
                                                     activeFileEditorIsNull );
942
    final Action insertStrikethroughAction = new Action( get(
943
      "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
944
                                                         e -> getActiveEditor().surroundSelection(
945
                                                           "~~", "~~" ),
946
                                                         activeFileEditorIsNull );
947
    final Action insertBlockquoteAction = new Action( get(
948
      "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
949
                                                      e -> getActiveEditor().surroundSelection(
950
                                                        "\n\n> ", "" ),
951
                                                      activeFileEditorIsNull );
952
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
953
                                                "Shortcut+K", CODE,
954
                                                e -> getActiveEditor().surroundSelection(
955
                                                  "`", "`" ),
956
                                                activeFileEditorIsNull );
957
    final Action insertFencedCodeBlockAction = new Action( get(
958
      "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
959
                                                           e -> getActiveEditor().surroundSelection(
960
                                                             "\n\n```\n",
961
                                                             "\n```\n\n", get(
962
                                                               "Main.menu.insert.fenced_code_block.prompt" ) ),
963
                                                           activeFileEditorIsNull );
964
965
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
966
                                                "Shortcut+L", LINK,
967
                                                e -> getActiveEditor().insertLink(),
968
                                                activeFileEditorIsNull );
969
    final Action insertImageAction = new Action( get( "Main.menu.insert.image" ),
970
                                                 "Shortcut+G", PICTURE_ALT,
971
                                                 e -> getActiveEditor().insertImage(),
972
                                                 activeFileEditorIsNull );
973
974
    final Action[] headers = new Action[ 6 ];
975
976
    // Insert header actions (H1 ... H6)
977
    for( int i = 1; i <= 6; i++ ) {
978
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
979
      final String markup = String.format( "%n%n%s ", hashes );
980
      final String text = get( "Main.menu.insert.header_" + i );
981
      final String accelerator = "Shortcut+" + i;
982
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
983
984
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
985
                                     e -> getActiveEditor().surroundSelection(
986
                                       markup, "", prompt ),
987
                                     activeFileEditorIsNull );
988
    }
989
990
    final Action insertUnorderedListAction = new Action(
991
      get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
992
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
993
      activeFileEditorIsNull );
994
    final Action insertOrderedListAction = new Action(
995
      get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
996
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
997
      activeFileEditorIsNull );
998
    final Action insertHorizontalRuleAction = new Action(
999
      get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
1000
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
1001
      activeFileEditorIsNull );
1002
1003
    // Tools actions
1004
    final Action toolsScriptAction = new Action(
1005
      get( "Main.menu.tools.script" ), null, null, e -> toolsScript() );
1006
1007
    // Help actions
1008
    final Action helpAboutAction = new Action(
1009
      get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
1010
1011
    //---- MenuBar ----
1012
    final Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
1013
                                                  fileNewAction,
1014
                                                  fileOpenAction,
1015
                                                  null,
1016
                                                  fileCloseAction,
1017
                                                  fileCloseAllAction,
1018
                                                  null,
1019
                                                  fileSaveAction,
1020
                                                  fileSaveAsAction,
1021
                                                  fileSaveAllAction,
1022
                                                  null,
1023
                                                  fileExitAction );
1024
1025
    final Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
1026
                                                  editUndoAction,
1027
                                                  editRedoAction,
1028
                                                  editFindAction,
1029
                                                  editReplaceAction,
1030
                                                  editFindNextAction,
1031
                                                  editFindPreviousAction );
1032
1033
    final Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
1034
                                                    insertBoldAction,
1035
                                                    insertItalicAction,
1036
                                                    insertSuperscriptAction,
1037
                                                    insertSubscriptAction,
1038
                                                    insertStrikethroughAction,
1039
                                                    insertBlockquoteAction,
1040
                                                    insertCodeAction,
1041
                                                    insertFencedCodeBlockAction,
1042
                                                    null,
1043
                                                    insertLinkAction,
1044
                                                    insertImageAction,
1045
                                                    null,
1046
                                                    headers[ 0 ],
1047
                                                    headers[ 1 ],
1048
                                                    headers[ 2 ],
1049
                                                    headers[ 3 ],
1050
                                                    headers[ 4 ],
1051
                                                    headers[ 5 ],
1052
                                                    null,
1053
                                                    insertUnorderedListAction,
1054
                                                    insertOrderedListAction,
1055
                                                    insertHorizontalRuleAction );
1056
1057
    final Menu toolsMenu = ActionUtils.createMenu( get( "Main.menu.tools" ),
1058
                                                   toolsScriptAction );
1059
1060
    final Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
1061
                                                  helpAboutAction );
1062
1063
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
1064
1065
    //---- ToolBar ----
1066
    ToolBar toolBar = ActionUtils.createToolBar(
1067
      fileNewAction,
1068
      fileOpenAction,
1069
      fileSaveAction,
1070
      null,
1071
      editUndoAction,
1072
      editRedoAction,
1073
      null,
1074
      insertBoldAction,
1075
      insertItalicAction,
1076
      insertSuperscriptAction,
1077
      insertSubscriptAction,
1078
      insertBlockquoteAction,
1079
      insertCodeAction,
1080
      insertFencedCodeBlockAction,
1081
      null,
1082
      insertLinkAction,
1083
      insertImageAction,
1084
      null,
1085
      headers[ 0 ],
1086
      null,
1087
      insertUnorderedListAction,
1088
      insertOrderedListAction );
1089
1090
    return new VBox( menuBar, toolBar );
1091
  }
1092
1093
  /**
1094
   * Creates a boolean property that is bound to another boolean value of the
1095
   * active editor.
1096
   */
1097
  private BooleanProperty createActiveBooleanProperty(
1098
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
1099
1100
    final BooleanProperty b = new SimpleBooleanProperty();
1101
    final FileEditorTab tab = getActiveFileEditor();
1102
1103
    if( tab != null ) {
1104
      b.bind( func.apply( tab ) );
1105
    }
1106
1107
    getFileEditorPane().activeFileEditorProperty().addListener(
1108
      (observable, oldFileEditor, newFileEditor) -> {
1109
        b.unbind();
1110
1111
        if( newFileEditor != null ) {
1112
          b.bind( func.apply( newFileEditor ) );
1113
        }
1114
        else {
1115
          b.set( false );
1116
        }
1117
      }
1118
    );
1119
1120
    return b;
1121
  }
1122
1123
  private void initLayout() {
1124
    final Scene appScene = getScene();
1125
1126
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1127
1128
    // TODO: Apply an XML syntax highlighting for XML files.
1129
//    appScene.getStylesheets().add( STYLESHEET_XML );
1130
    appScene.windowProperty().addListener(
1131
      (observable, oldWindow, newWindow) -> {
1132
        newWindow.setOnCloseRequest( e -> {
1133
          if( !getFileEditorPane().closeAllEditors() ) {
1134
            e.consume();
1135
          }
1136
        } );
1137
1138
        // Workaround JavaFX bug: deselect menubar if window loses focus.
1139
        newWindow.focusedProperty().addListener(
1140
          (obs, oldFocused, newFocused) -> {
1141
            if( !newFocused ) {
1142
              // Send an ESC key event to the menubar
1143
              this.menuBar.fireEvent(
1144
                new KeyEvent(
1145
                  KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
1146
                  false, false, false, false ) );
1147
            }
1148
          }
1149
        );
1150
      }
1151
    );
30
import com.scrivenvar.definition.*;
31
import com.scrivenvar.dialogs.RScriptDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.editors.VariableNameInjector;
34
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
35
import com.scrivenvar.predicates.files.FileTypePredicate;
36
import com.scrivenvar.preview.HTMLPreviewPane;
37
import com.scrivenvar.processors.Processor;
38
import com.scrivenvar.processors.ProcessorFactory;
39
import com.scrivenvar.service.Options;
40
import com.scrivenvar.service.Snitch;
41
import com.scrivenvar.service.events.Notifier;
42
import com.scrivenvar.util.Action;
43
import com.scrivenvar.util.ActionUtils;
44
import javafx.application.Platform;
45
import javafx.beans.binding.Bindings;
46
import javafx.beans.binding.BooleanBinding;
47
import javafx.beans.property.BooleanProperty;
48
import javafx.beans.property.SimpleBooleanProperty;
49
import javafx.beans.value.ObservableBooleanValue;
50
import javafx.beans.value.ObservableValue;
51
import javafx.collections.ListChangeListener.Change;
52
import javafx.collections.ObservableList;
53
import javafx.geometry.Pos;
54
import javafx.scene.Node;
55
import javafx.scene.Scene;
56
import javafx.scene.control.*;
57
import javafx.scene.control.Alert.AlertType;
58
import javafx.scene.image.Image;
59
import javafx.scene.image.ImageView;
60
import javafx.scene.input.KeyEvent;
61
import javafx.scene.layout.BorderPane;
62
import javafx.scene.layout.VBox;
63
import javafx.scene.text.Text;
64
import javafx.stage.Window;
65
import javafx.stage.WindowEvent;
66
import org.controlsfx.control.StatusBar;
67
import org.fxmisc.richtext.model.TwoDimensional.Position;
68
69
import java.io.IOException;
70
import java.nio.file.Path;
71
import java.util.*;
72
import java.util.function.Function;
73
import java.util.prefs.Preferences;
74
75
import static com.scrivenvar.Constants.*;
76
import static com.scrivenvar.Messages.get;
77
import static com.scrivenvar.Messages.getLiteral;
78
import static com.scrivenvar.util.StageState.*;
79
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
80
import static javafx.event.Event.fireEvent;
81
import static javafx.scene.input.KeyCode.ESCAPE;
82
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
83
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
84
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
85
86
/**
87
 * Main window containing a tab pane in the center for file editors.
88
 *
89
 * @author Karl Tauber and White Magic Software, Ltd.
90
 */
91
public class MainWindow implements Observer {
92
93
  private final Options mOptions = Services.load( Options.class );
94
  private final Snitch mSnitch = Services.load( Snitch.class );
95
  private final Notifier mNotifier = Services.load( Notifier.class );
96
97
  private Scene scene;
98
  private MenuBar menuBar;
99
  private StatusBar statusBar;
100
  private Text lineNumberText;
101
  private TextField findTextField;
102
103
  private DefinitionSource definitionSource;
104
  private DefinitionPane definitionPane;
105
  private FileEditorTabPane fileEditorPane;
106
  private HTMLPreviewPane previewPane;
107
108
  /**
109
   * Prevents re-instantiation of processing classes.
110
   */
111
  private Map<FileEditorTab, Processor<String>> processors;
112
113
  /**
114
   * Listens on the definition pane for double-click events.
115
   */
116
  private VariableNameInjector variableNameInjector;
117
118
  public MainWindow() {
119
    initLayout();
120
    initFindInput();
121
    initSnitch();
122
    initDefinitionListener();
123
    initTabAddedListener();
124
    initTabChangedListener();
125
    initPreferences();
126
  }
127
128
  /**
129
   * Watch for changes to external files. In particular, this awaits
130
   * modifications to any XSL files associated with XML files being edited. When
131
   * an XSL file is modified (external to the application), the snitch's ears
132
   * perk up and the file is reloaded. This keeps the XSL transformation up to
133
   * date with what's on the file system.
134
   */
135
  private void initSnitch() {
136
    getSnitch().addObserver( this );
137
  }
138
139
  /**
140
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
141
   * presses.
142
   */
143
  private void initFindInput() {
144
    final TextField input = getFindTextField();
145
146
    input.setOnKeyPressed( ( KeyEvent event ) -> {
147
      switch( event.getCode() ) {
148
        case F3:
149
        case ENTER:
150
          findNext();
151
          break;
152
        case F:
153
          if( !event.isControlDown() ) {
154
            break;
155
          }
156
        case ESCAPE:
157
          getStatusBar().setGraphic( null );
158
          getActiveFileEditor().getEditorPane().requestFocus();
159
          break;
160
      }
161
    } );
162
163
    // Remove when the input field loses focus.
164
    input.focusedProperty().addListener(
165
        (
166
            final ObservableValue<? extends Boolean> focused,
167
            final Boolean oFocus,
168
            final Boolean nFocus ) -> {
169
          if( !nFocus ) {
170
            getStatusBar().setGraphic( null );
171
          }
172
        }
173
    );
174
  }
175
176
  /**
177
   * Listen for file editor tab pane to receive an open definition source event.
178
   */
179
  private void initDefinitionListener() {
180
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
181
        ( ObservableValue<? extends Path> definitionFile,
182
          final Path oldPath, final Path newPath ) -> {
183
          openDefinition( newPath );
184
185
          // Indirectly refresh the resolved map.
186
          setProcessors( null );
187
          updateDefinitionPane();
188
189
          try {
190
            getSnitch().ignore( oldPath );
191
            getSnitch().listen( newPath );
192
          } catch( final IOException ex ) {
193
            error( ex );
194
          }
195
196
          // Will create new processors and therefore a new resolved map.
197
          refreshSelectedTab( getActiveFileEditor() );
198
        }
199
    );
200
  }
201
202
  /**
203
   * When tabs are added, hook the various change listeners onto the new tab so
204
   * that the preview pane refreshes as necessary.
205
   */
206
  private void initTabAddedListener() {
207
    final FileEditorTabPane editorPane = getFileEditorPane();
208
209
    // Make sure the text processor kicks off when new files are opened.
210
    final ObservableList<Tab> tabs = editorPane.getTabs();
211
212
    // Update the preview pane on tab changes.
213
    tabs.addListener(
214
        ( final Change<? extends Tab> change ) -> {
215
          while( change.next() ) {
216
            if( change.wasAdded() ) {
217
              // Multiple tabs can be added simultaneously.
218
              for( final Tab newTab : change.getAddedSubList() ) {
219
                final FileEditorTab tab = (FileEditorTab) newTab;
220
221
                initTextChangeListener( tab );
222
                initCaretParagraphListener( tab );
223
                initKeyboardEventListeners( tab );
224
//              initSyntaxListener( tab );
225
              }
226
            }
227
          }
228
        }
229
    );
230
  }
231
232
  /**
233
   * Reloads the preferences from the previous session.
234
   */
235
  private void initPreferences() {
236
    restoreDefinitionSource();
237
    getFileEditorPane().restorePreferences();
238
    updateDefinitionPane();
239
  }
240
241
  /**
242
   * Listen for new tab selection events.
243
   */
244
  private void initTabChangedListener() {
245
    final FileEditorTabPane editorPane = getFileEditorPane();
246
247
    // Update the preview pane changing tabs.
248
    editorPane.addTabSelectionListener(
249
        ( ObservableValue<? extends Tab> tabPane,
250
          final Tab oldTab, final Tab newTab ) -> {
251
          updateVariableNameInjector();
252
253
          // If there was no old tab, then this is a first time load, which
254
          // can be ignored.
255
          if( oldTab != null ) {
256
            if( newTab == null ) {
257
              closeRemainingTab();
258
            }
259
            else {
260
              // Update the preview with the edited text.
261
              refreshSelectedTab( (FileEditorTab) newTab );
262
            }
263
          }
264
        }
265
    );
266
  }
267
268
  /**
269
   * Ensure that the keyboard events are received when a new tab is added
270
   * to the user interface.
271
   *
272
   * @param tab The tab that can trigger keyboard events, such as control+space.
273
   */
274
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
275
    final VariableNameInjector vin = getVariableNameInjector();
276
    vin.initKeyboardEventListeners( tab );
277
  }
278
279
  private void initTextChangeListener( final FileEditorTab tab ) {
280
    tab.addTextChangeListener(
281
        ( ObservableValue<? extends String> editor,
282
          final String oldValue, final String newValue ) ->
283
            refreshSelectedTab( tab )
284
    );
285
  }
286
287
  private void initCaretParagraphListener( final FileEditorTab tab ) {
288
    tab.addCaretParagraphListener(
289
        ( ObservableValue<? extends Integer> editor,
290
          final Integer oldValue, final Integer newValue ) ->
291
            refreshSelectedTab( tab )
292
    );
293
  }
294
295
  private void updateVariableNameInjector() {
296
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
297
  }
298
299
  private void setVariableNameInjector( final VariableNameInjector injector ) {
300
    this.variableNameInjector = injector;
301
  }
302
303
  private synchronized VariableNameInjector getVariableNameInjector() {
304
    if( this.variableNameInjector == null ) {
305
      final VariableNameInjector vin = createVariableNameInjector();
306
      setVariableNameInjector( vin );
307
    }
308
309
    return this.variableNameInjector;
310
  }
311
312
  private VariableNameInjector createVariableNameInjector() {
313
    final FileEditorTab tab = getActiveFileEditor();
314
    final DefinitionPane pane = getDefinitionPane();
315
316
    return new VariableNameInjector( tab, pane );
317
  }
318
319
  /**
320
   * Add a listener for variable name injection the given tab.
321
   *
322
   * @param tab The tab to inject variable names into upon a double-click.
323
   */
324
  private void initVariableNameInjector( final Tab tab ) {
325
    final FileEditorTab editorTab = (FileEditorTab) tab;
326
  }
327
328
  /**
329
   * Called whenever the preview pane becomes out of sync with the file editor
330
   * tab. This can be called when the text changes, the caret paragraph changes,
331
   * or the file tab changes.
332
   *
333
   * @param tab The file editor tab that has been changed in some fashion.
334
   */
335
  private void refreshSelectedTab( final FileEditorTab tab ) {
336
    if( tab.isFileOpen() ) {
337
      getPreviewPane().setPath( tab.getPath() );
338
339
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
340
      final Position p = tab.getCaretOffset();
341
      getLineNumberText().setText(
342
          get( STATUS_BAR_LINE,
343
               p.getMajor() + 1,
344
               p.getMinor() + 1,
345
               tab.getCaretPosition() + 1
346
          )
347
      );
348
349
      Processor<String> processor = getProcessors().get( tab );
350
351
      if( processor == null ) {
352
        processor = createProcessor( tab );
353
        getProcessors().put( tab, processor );
354
      }
355
356
      try {
357
        getNotifier().clear();
358
        processor.processChain( tab.getEditorText() );
359
      } catch( final Exception ex ) {
360
        error( ex );
361
      }
362
    }
363
  }
364
365
  /**
366
   * Used to find text in the active file editor window.
367
   */
368
  private void find() {
369
    final TextField input = getFindTextField();
370
    getStatusBar().setGraphic( input );
371
    input.requestFocus();
372
  }
373
374
  public void findNext() {
375
    getActiveFileEditor().searchNext( getFindTextField().getText() );
376
  }
377
378
  /**
379
   * Returns the variable map of interpolated definitions.
380
   *
381
   * @return A map to help dereference variables.
382
   */
383
  private Map<String, String> getResolvedMap() {
384
    return getDefinitionSource().getResolvedMap();
385
  }
386
387
  /**
388
   * Returns the root node for the hierarchical definition source.
389
   *
390
   * @return Data to display in the definition pane.
391
   */
392
  private TreeView<String> getTreeView() {
393
    try {
394
      return getDefinitionSource().asTreeView();
395
    } catch( Exception e ) {
396
      error( e );
397
    }
398
399
    // Slightly redundant as getDefinitionSource() might have returned an
400
    // empty definition source.
401
    return (new EmptyDefinitionSource()).asTreeView();
402
  }
403
404
  /**
405
   * Called when a definition source is opened.
406
   *
407
   * @param path Path to the definition source that was opened.
408
   */
409
  private void openDefinition( final Path path ) {
410
    try {
411
      final DefinitionSource ds = createDefinitionSource( path.toString() );
412
      setDefinitionSource( ds );
413
      storeDefinitionSource();
414
      updateDefinitionPane();
415
    } catch( final Exception e ) {
416
      error( e );
417
    }
418
  }
419
420
  private void updateDefinitionPane() {
421
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
422
  }
423
424
  private void restoreDefinitionSource() {
425
    final Preferences preferences = getPreferences();
426
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, "" );
427
428
    setDefinitionSource( createDefinitionSource( source ) );
429
  }
430
431
  private void storeDefinitionSource() {
432
    final Preferences preferences = getPreferences();
433
    final DefinitionSource ds = getDefinitionSource();
434
435
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
436
  }
437
438
  /**
439
   * Called when the last open tab is closed to clear the preview pane.
440
   */
441
  private void closeRemainingTab() {
442
    getPreviewPane().clear();
443
  }
444
445
  /**
446
   * Called when an exception occurs that warrants the user's attention.
447
   *
448
   * @param e The exception with a message that the user should know about.
449
   */
450
  private void error( final Exception e ) {
451
    getNotifier().notify( e );
452
  }
453
454
  //---- File actions -------------------------------------------------------
455
456
  /**
457
   * Called when an observable instance has changed. This is called by both the
458
   * snitch service and the notify service. The snitch service can be called for
459
   * different file types, including definition sources.
460
   *
461
   * @param observable The observed instance.
462
   * @param value      The noteworthy item.
463
   */
464
  @Override
465
  public void update( final Observable observable, final Object value ) {
466
    if( value != null ) {
467
      if( observable instanceof Snitch && value instanceof Path ) {
468
        final Path path = (Path) value;
469
        final FileTypePredicate predicate
470
            = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
471
472
        // Reload definitions.
473
        if( predicate.test( path.toFile() ) ) {
474
          updateDefinitionSource( path );
475
        }
476
477
        updateSelectedTab();
478
      }
479
      else if( observable instanceof Notifier && value instanceof String ) {
480
        updateStatusBar( (String) value );
481
      }
482
    }
483
  }
484
485
  /**
486
   * Updates the status bar to show the given message.
487
   *
488
   * @param s The message to show in the status bar.
489
   */
490
  private void updateStatusBar( final String s ) {
491
    Platform.runLater(
492
        () -> {
493
          final int index = s.indexOf( '\n' );
494
          final String message = s.substring( 0,
495
                                              index > 0 ? index : s.length() );
496
497
          getStatusBar().setText( message );
498
        }
499
    );
500
  }
501
502
  /**
503
   * Called when a file has been modified.
504
   */
505
  private void updateSelectedTab() {
506
    Platform.runLater(
507
        () -> {
508
          // Brute-force XSLT file reload by re-instantiating all processors.
509
          resetProcessors();
510
          refreshSelectedTab( getActiveFileEditor() );
511
        }
512
    );
513
  }
514
515
  /**
516
   * Reloads the definition source from the given path.
517
   *
518
   * @param path The path containing new definition information.
519
   */
520
  private void updateDefinitionSource( final Path path ) {
521
    Platform.runLater( () -> openDefinition( path ) );
522
  }
523
524
  /**
525
   * After resetting the processors, they will refresh anew to be up-to-date
526
   * with the files (text and definition) currently loaded into the editor.
527
   */
528
  private void resetProcessors() {
529
    getProcessors().clear();
530
  }
531
532
  //---- File actions -------------------------------------------------------
533
  private void fileNew() {
534
    getFileEditorPane().newEditor();
535
  }
536
537
  private void fileOpen() {
538
    getFileEditorPane().openFileDialog();
539
  }
540
541
  private void fileClose() {
542
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
543
  }
544
545
  private void fileCloseAll() {
546
    getFileEditorPane().closeAllEditors();
547
  }
548
549
  private void fileSave() {
550
    getFileEditorPane().saveEditor( getActiveFileEditor() );
551
  }
552
553
  private void fileSaveAs() {
554
    final FileEditorTab editor = getActiveFileEditor();
555
    getFileEditorPane().saveEditorAs( editor );
556
    getProcessors().remove( editor );
557
558
    try {
559
      refreshSelectedTab( editor );
560
    } catch( final Exception ex ) {
561
      getNotifier().notify( ex );
562
    }
563
  }
564
565
  private void fileSaveAll() {
566
    getFileEditorPane().saveAllEditors();
567
  }
568
569
  private void fileExit() {
570
    final Window window = getWindow();
571
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
572
  }
573
574
  //---- R menu actions
575
  private void rScript() {
576
    final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
577
    final RScriptDialog dialog = new RScriptDialog(
578
        getWindow(), "Dialog.r.script.title", script );
579
    final Optional<String> result = dialog.showAndWait();
580
581
    result.ifPresent( this::putStartupScript );
582
  }
583
584
  private void rDirectory() {
585
    final TextInputDialog dialog = new TextInputDialog(
586
        getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
587
    );
588
589
    dialog.setTitle( get( "Dialog.r.directory.title" ) );
590
    dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
591
    dialog.setContentText( "Directory" );
592
593
    final Optional<String> result = dialog.showAndWait();
594
595
    result.ifPresent( this::putStartupDirectory );
596
  }
597
598
  /**
599
   * Stores the R startup script into the user preferences.
600
   */
601
  private void putStartupScript( final String script ) {
602
    putPreference( PERSIST_R_STARTUP, script );
603
  }
604
605
  /**
606
   * Stores the R bootstrap script directory into the user preferences.
607
   */
608
  private void putStartupDirectory( final String directory ) {
609
    putPreference( PERSIST_R_DIRECTORY, directory );
610
  }
611
612
  //---- Help actions -------------------------------------------------------
613
  private void helpAbout() {
614
    Alert alert = new Alert( AlertType.INFORMATION );
615
    alert.setTitle( get( "Dialog.about.title" ) );
616
    alert.setHeaderText( get( "Dialog.about.header" ) );
617
    alert.setContentText( get( "Dialog.about.content" ) );
618
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
619
    alert.initOwner( getWindow() );
620
621
    alert.showAndWait();
622
  }
623
624
  //---- Convenience accessors ----------------------------------------------
625
  private float getFloat( final String key, final float defaultValue ) {
626
    return getPreferences().getFloat( key, defaultValue );
627
  }
628
629
  private Preferences getPreferences() {
630
    return getOptions().getState();
631
  }
632
633
  protected Scene getScene() {
634
    if( this.scene == null ) {
635
      this.scene = createScene();
636
    }
637
638
    return this.scene;
639
  }
640
641
  public Window getWindow() {
642
    return getScene().getWindow();
643
  }
644
645
  private MarkdownEditorPane getActiveEditor() {
646
    final EditorPane pane = getActiveFileEditor().getEditorPane();
647
648
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane) pane :
649
        null;
650
  }
651
652
  private FileEditorTab getActiveFileEditor() {
653
    return getFileEditorPane().getActiveFileEditor();
654
  }
655
656
  //---- Member accessors ---------------------------------------------------
657
  private void setProcessors(
658
      final Map<FileEditorTab, Processor<String>> map ) {
659
    this.processors = map;
660
  }
661
662
  private Map<FileEditorTab, Processor<String>> getProcessors() {
663
    if( this.processors == null ) {
664
      setProcessors( new HashMap<>() );
665
    }
666
667
    return this.processors;
668
  }
669
670
  private FileEditorTabPane getFileEditorPane() {
671
    if( this.fileEditorPane == null ) {
672
      this.fileEditorPane = createFileEditorPane();
673
    }
674
675
    return this.fileEditorPane;
676
  }
677
678
  private HTMLPreviewPane getPreviewPane() {
679
    if( this.previewPane == null ) {
680
      this.previewPane = createPreviewPane();
681
    }
682
683
    return this.previewPane;
684
  }
685
686
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
687
    this.definitionSource = definitionSource;
688
  }
689
690
  private DefinitionSource getDefinitionSource() {
691
    if( this.definitionSource == null ) {
692
      this.definitionSource = new EmptyDefinitionSource();
693
    }
694
695
    return this.definitionSource;
696
  }
697
698
  private DefinitionPane getDefinitionPane() {
699
    if( this.definitionPane == null ) {
700
      this.definitionPane = createDefinitionPane();
701
    }
702
703
    return this.definitionPane;
704
  }
705
706
  private Options getOptions() {
707
    return mOptions;
708
  }
709
710
  private Snitch getSnitch() {
711
    return mSnitch;
712
  }
713
714
  private Notifier getNotifier() {
715
    return mNotifier;
716
  }
717
718
  public void setMenuBar( final MenuBar menuBar ) {
719
    this.menuBar = menuBar;
720
  }
721
722
  public MenuBar getMenuBar() {
723
    return this.menuBar;
724
  }
725
726
  private Text getLineNumberText() {
727
    if( this.lineNumberText == null ) {
728
      this.lineNumberText = createLineNumberText();
729
    }
730
731
    return this.lineNumberText;
732
  }
733
734
  private synchronized StatusBar getStatusBar() {
735
    if( this.statusBar == null ) {
736
      this.statusBar = createStatusBar();
737
    }
738
739
    return this.statusBar;
740
  }
741
742
  private TextField getFindTextField() {
743
    if( this.findTextField == null ) {
744
      this.findTextField = createFindTextField();
745
    }
746
747
    return this.findTextField;
748
  }
749
750
  //---- Member creators ----------------------------------------------------
751
752
  /**
753
   * Factory to create processors that are suited to different file types.
754
   *
755
   * @param tab The tab that is subjected to processing.
756
   * @return A processor suited to the file type specified by the tab's path.
757
   */
758
  private Processor<String> createProcessor( final FileEditorTab tab ) {
759
    return createProcessorFactory().createProcessor( tab );
760
  }
761
762
  private ProcessorFactory createProcessorFactory() {
763
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
764
  }
765
766
  private DefinitionSource createDefinitionSource( final String path ) {
767
    DefinitionSource ds;
768
769
    try {
770
      ds = createDefinitionFactory().createDefinitionSource( path );
771
772
      if( ds instanceof FileDefinitionSource ) {
773
        try {
774
          getNotifier().notify( ds.getError() );
775
          getSnitch().listen( ((FileDefinitionSource) ds).getPath() );
776
        } catch( final Exception ex ) {
777
          error( ex );
778
        }
779
      }
780
    } catch( final Exception ex ) {
781
      ds = new EmptyDefinitionSource();
782
      error( ex );
783
    }
784
785
    return ds;
786
  }
787
788
  private TextField createFindTextField() {
789
    return new TextField();
790
  }
791
792
  /**
793
   * Create an editor pane to hold file editor tabs.
794
   *
795
   * @return A new instance, never null.
796
   */
797
  private FileEditorTabPane createFileEditorPane() {
798
    return new FileEditorTabPane();
799
  }
800
801
  private HTMLPreviewPane createPreviewPane() {
802
    return new HTMLPreviewPane();
803
  }
804
805
  private DefinitionPane createDefinitionPane() {
806
    return new DefinitionPane( getTreeView() );
807
  }
808
809
  private DefinitionFactory createDefinitionFactory() {
810
    return new DefinitionFactory();
811
  }
812
813
  private StatusBar createStatusBar() {
814
    return new StatusBar();
815
  }
816
817
  private Scene createScene() {
818
    final SplitPane splitPane = new SplitPane(
819
        getDefinitionPane().getNode(),
820
        getFileEditorPane().getNode(),
821
        getPreviewPane().getNode() );
822
823
    splitPane.setDividerPositions(
824
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
825
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
826
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
827
828
    // See: http://broadlyapplicable.blogspot
829
    // .ca/2015/03/javafx-capture-restorePreferences-splitpane.html
830
    final BorderPane borderPane = new BorderPane();
831
    borderPane.setPrefSize( 1024, 800 );
832
    borderPane.setTop( createMenuBar() );
833
    borderPane.setBottom( getStatusBar() );
834
    borderPane.setCenter( splitPane );
835
836
    final VBox box = new VBox();
837
    box.setAlignment( Pos.BASELINE_CENTER );
838
    box.getChildren().add( getLineNumberText() );
839
    getStatusBar().getRightItems().add( box );
840
841
    return new Scene( borderPane );
842
  }
843
844
  private Text createLineNumberText() {
845
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
846
  }
847
848
  private Node createMenuBar() {
849
    final BooleanBinding activeFileEditorIsNull =
850
        getFileEditorPane().activeFileEditorProperty()
851
                           .isNull();
852
853
    // File actions
854
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
855
                                             "Shortcut+N", FILE_ALT,
856
                                             e -> fileNew() );
857
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
858
                                              "Shortcut+O", FOLDER_OPEN_ALT,
859
                                              e -> fileOpen() );
860
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
861
                                               "Shortcut+W", null,
862
                                               e -> fileClose(),
863
                                               activeFileEditorIsNull );
864
    final Action fileCloseAllAction = new Action( get(
865
        "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
866
                                                  activeFileEditorIsNull );
867
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
868
                                              "Shortcut+S", FLOPPY_ALT,
869
                                              e -> fileSave(),
870
                                              createActiveBooleanProperty(
871
                                                  FileEditorTab::modifiedProperty )
872
                                                  .not() );
873
    final Action fileSaveAsAction = new Action( Messages.get(
874
        "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
875
                                                activeFileEditorIsNull );
876
    final Action fileSaveAllAction = new Action(
877
        get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
878
        e -> fileSaveAll(),
879
        Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
880
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ),
881
                                              null,
882
                                              null,
883
                                              e -> fileExit() );
884
885
    // Edit actions
886
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
887
                                              "Shortcut+Z", UNDO,
888
                                              e -> getActiveEditor().undo(),
889
                                              createActiveBooleanProperty(
890
                                                  FileEditorTab::canUndoProperty )
891
                                                  .not() );
892
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
893
                                              "Shortcut+Y", REPEAT,
894
                                              e -> getActiveEditor().redo(),
895
                                              createActiveBooleanProperty(
896
                                                  FileEditorTab::canRedoProperty )
897
                                                  .not() );
898
    final Action editFindAction = new Action( Messages.get(
899
        "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
900
                                              e -> find(),
901
                                              activeFileEditorIsNull );
902
    final Action editFindNextAction = new Action( Messages.get(
903
        "Main.menu.edit.find.next" ), "F3", null,
904
                                                  e -> findNext(),
905
                                                  activeFileEditorIsNull );
906
907
    // Insert actions
908
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
909
                                                "Shortcut+B", BOLD,
910
                                                e -> getActiveEditor().surroundSelection(
911
                                                    "**", "**" ),
912
                                                activeFileEditorIsNull );
913
    final Action insertItalicAction = new Action(
914
        get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
915
        e -> getActiveEditor().surroundSelection( "*", "*" ),
916
        activeFileEditorIsNull );
917
    final Action insertSuperscriptAction = new Action( get(
918
        "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
919
                                                       e -> getActiveEditor().surroundSelection(
920
                                                           "^", "^" ),
921
                                                       activeFileEditorIsNull );
922
    final Action insertSubscriptAction = new Action( get(
923
        "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
924
                                                     e -> getActiveEditor().surroundSelection(
925
                                                         "~", "~" ),
926
                                                     activeFileEditorIsNull );
927
    final Action insertStrikethroughAction = new Action( get(
928
        "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
929
                                                         e -> getActiveEditor().surroundSelection(
930
                                                             "~~", "~~" ),
931
                                                         activeFileEditorIsNull );
932
    final Action insertBlockquoteAction = new Action( get(
933
        "Main.menu.insert.blockquote" ),
934
                                                      "Ctrl+Q",
935
                                                      QUOTE_LEFT,
936
                                                      // not Shortcut+Q
937
                                                      // because of conflict
938
                                                      // on Mac
939
                                                      e -> getActiveEditor().surroundSelection(
940
                                                          "\n\n> ", "" ),
941
                                                      activeFileEditorIsNull );
942
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
943
                                                "Shortcut+K", CODE,
944
                                                e -> getActiveEditor().surroundSelection(
945
                                                    "`", "`" ),
946
                                                activeFileEditorIsNull );
947
    final Action insertFencedCodeBlockAction = new Action( get(
948
        "Main.menu.insert.fenced_code_block" ),
949
                                                           "Shortcut+Shift+K",
950
                                                           FILE_CODE_ALT,
951
                                                           e -> getActiveEditor()
952
                                                               .surroundSelection(
953
                                                                   "\n\n```\n",
954
                                                                   "\n```\n\n",
955
                                                                   get(
956
                                                                       "Main.menu.insert.fenced_code_block.prompt" ) ),
957
                                                           activeFileEditorIsNull );
958
959
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
960
                                                "Shortcut+L", LINK,
961
                                                e -> getActiveEditor().insertLink(),
962
                                                activeFileEditorIsNull );
963
    final Action insertImageAction = new Action( get( "Main.menu.insert" +
964
                                                          ".image" ),
965
                                                 "Shortcut+G", PICTURE_ALT,
966
                                                 e -> getActiveEditor().insertImage(),
967
                                                 activeFileEditorIsNull );
968
969
    final Action[] headers = new Action[ 6 ];
970
971
    // Insert header actions (H1 ... H6)
972
    for( int i = 1; i <= 6; i++ ) {
973
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
974
      final String markup = String.format( "%n%n%s ", hashes );
975
      final String text = get( "Main.menu.insert.header_" + i );
976
      final String accelerator = "Shortcut+" + i;
977
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
978
979
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
980
                                     e -> getActiveEditor().surroundSelection(
981
                                         markup, "", prompt ),
982
                                     activeFileEditorIsNull );
983
    }
984
985
    final Action insertUnorderedListAction = new Action(
986
        get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
987
        e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
988
        activeFileEditorIsNull );
989
    final Action insertOrderedListAction = new Action(
990
        get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
991
        e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
992
        activeFileEditorIsNull );
993
    final Action insertHorizontalRuleAction = new Action(
994
        get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
995
        e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
996
        activeFileEditorIsNull );
997
998
    // R actions
999
    final Action mRScriptAction = new Action(
1000
        get( "Main.menu.r.script" ), null, null, e -> rScript() );
1001
1002
    final Action mRDirectoryAction = new Action(
1003
        get( "Main.menu.r.directory" ), null, null, e -> rDirectory() );
1004
1005
    // Help actions
1006
    final Action helpAboutAction = new Action(
1007
        get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
1008
1009
    //---- MenuBar ----
1010
    final Menu fileMenu = ActionUtils.createMenu(
1011
        get( "Main.menu.file" ),
1012
        fileNewAction,
1013
        fileOpenAction,
1014
        null,
1015
        fileCloseAction,
1016
        fileCloseAllAction,
1017
        null,
1018
        fileSaveAction,
1019
        fileSaveAsAction,
1020
        fileSaveAllAction,
1021
        null,
1022
        fileExitAction );
1023
1024
    final Menu editMenu = ActionUtils.createMenu(
1025
        get( "Main.menu.edit" ),
1026
        editUndoAction,
1027
        editRedoAction,
1028
        editFindAction,
1029
        editFindNextAction );
1030
1031
    final Menu insertMenu = ActionUtils.createMenu(
1032
        get( "Main.menu.insert" ),
1033
        insertBoldAction,
1034
        insertItalicAction,
1035
        insertSuperscriptAction,
1036
        insertSubscriptAction,
1037
        insertStrikethroughAction,
1038
        insertBlockquoteAction,
1039
        insertCodeAction,
1040
        insertFencedCodeBlockAction,
1041
        null,
1042
        insertLinkAction,
1043
        insertImageAction,
1044
        null,
1045
        headers[ 0 ],
1046
        headers[ 1 ],
1047
        headers[ 2 ],
1048
        headers[ 3 ],
1049
        headers[ 4 ],
1050
        headers[ 5 ],
1051
        null,
1052
        insertUnorderedListAction,
1053
        insertOrderedListAction,
1054
        insertHorizontalRuleAction );
1055
1056
    final Menu rMenu = ActionUtils.createMenu(
1057
        get( "Main.menu.r" ),
1058
        mRScriptAction,
1059
        mRDirectoryAction );
1060
1061
    final Menu helpMenu = ActionUtils.createMenu(
1062
        get( "Main.menu.help" ),
1063
        helpAboutAction );
1064
1065
    menuBar = new MenuBar( fileMenu,
1066
                           editMenu,
1067
                           insertMenu,
1068
                           rMenu,
1069
                           helpMenu );
1070
1071
    //---- ToolBar ----
1072
    ToolBar toolBar = ActionUtils.createToolBar(
1073
        fileNewAction,
1074
        fileOpenAction,
1075
        fileSaveAction,
1076
        null,
1077
        editUndoAction,
1078
        editRedoAction,
1079
        null,
1080
        insertBoldAction,
1081
        insertItalicAction,
1082
        insertSuperscriptAction,
1083
        insertSubscriptAction,
1084
        insertBlockquoteAction,
1085
        insertCodeAction,
1086
        insertFencedCodeBlockAction,
1087
        null,
1088
        insertLinkAction,
1089
        insertImageAction,
1090
        null,
1091
        headers[ 0 ],
1092
        null,
1093
        insertUnorderedListAction,
1094
        insertOrderedListAction );
1095
1096
    return new VBox( menuBar, toolBar );
1097
  }
1098
1099
  /**
1100
   * Creates a boolean property that is bound to another boolean value of the
1101
   * active editor.
1102
   */
1103
  private BooleanProperty createActiveBooleanProperty(
1104
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1105
1106
    final BooleanProperty b = new SimpleBooleanProperty();
1107
    final FileEditorTab tab = getActiveFileEditor();
1108
1109
    if( tab != null ) {
1110
      b.bind( func.apply( tab ) );
1111
    }
1112
1113
    getFileEditorPane().activeFileEditorProperty().addListener(
1114
        ( observable, oldFileEditor, newFileEditor ) -> {
1115
          b.unbind();
1116
1117
          if( newFileEditor != null ) {
1118
            b.bind( func.apply( newFileEditor ) );
1119
          }
1120
          else {
1121
            b.set( false );
1122
          }
1123
        }
1124
    );
1125
1126
    return b;
1127
  }
1128
1129
  private void initLayout() {
1130
    final Scene appScene = getScene();
1131
1132
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1133
1134
    // TODO: Apply an XML syntax highlighting for XML files.
1135
//    appScene.getStylesheets().add( STYLESHEET_XML );
1136
    appScene.windowProperty().addListener(
1137
        ( observable, oldWindow, newWindow ) -> {
1138
          newWindow.setOnCloseRequest( e -> {
1139
            if( !getFileEditorPane().closeAllEditors() ) {
1140
              e.consume();
1141
            }
1142
          } );
1143
1144
          // Workaround JavaFX bug: deselect menubar if window loses focus.
1145
          newWindow.focusedProperty().addListener(
1146
              ( obs, oldFocused, newFocused ) -> {
1147
                if( !newFocused ) {
1148
                  // Send an ESC key event to the menubar
1149
                  this.menuBar.fireEvent(
1150
                      new KeyEvent(
1151
                          KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
1152
                          false, false, false, false ) );
1153
                }
1154
              }
1155
          );
1156
        }
1157
    );
1158
  }
1159
1160
  private void putPreference( final String key, final String value ) {
1161
    try {
1162
      getPreferences().put( key, value );
1163
    } catch( final Exception ex ) {
1164
      getNotifier().notify( ex );
1165
    }
11521166
  }
11531167
}
M src/main/java/com/scrivenvar/Messages.java
2828
2929
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
30
3031
import java.text.MessageFormat;
3132
import java.util.ResourceBundle;
...
4041
public class Messages {
4142
42
  private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(APP_BUNDLE_NAME );
43
  private static final ResourceBundle RESOURCE_BUNDLE =
44
      ResourceBundle.getBundle(
45
          APP_BUNDLE_NAME );
4346
4447
  private Messages() {
4548
  }
4649
4750
  /**
4851
   * Return the value of a resource bundle value after having resolved any
4952
   * references to other bundle variables.
5053
   *
5154
   * @param props The bundle containing resolvable properties.
52
   * @param s The value for a key to resolve.
53
   *
55
   * @param s     The value for a key to resolve.
5456
   * @return The value of the key with all references recursively dereferenced.
5557
   */
56
  private static String resolve( ResourceBundle props, String s ) {
58
  @SuppressWarnings("SameParameterValue")
59
  private static String resolve( final ResourceBundle props, final String s ) {
5760
    final int len = s.length();
5861
    final Stack<StringBuilder> stack = new Stack<>();
...
105108
   *
106109
   * @param key Retrieve the value for this key.
107
   *
108110
   * @return The value for the key.
109111
   */
...
118120
119121
    return result;
122
  }
123
124
  public static String getLiteral( final String key ) {
125
    return RESOURCE_BUNDLE.getString( key );
120126
  }
121127
122128
  /**
123129
   * Returns the value for a key from the message bundle with the arguments
124130
   * replacing <code>{#}</code> place holders.
125131
   *
126
   * @param key Retrieve the value for this key.
132
   * @param key  Retrieve the value for this key.
127133
   * @param args The values to substitute for place holders.
128
   *
129134
   * @return The value for the key.
130135
   */
M src/main/java/com/scrivenvar/Services.java
4040
public class Services {
4141
42
  @SuppressWarnings("rawtypes")
4243
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
4344
4445
  /**
4546
   * Loads a service based on its interface definition. This will return an
4647
   * existing instance if the class has already been instantiated.
4748
   *
4849
   * @param <T> The service to load.
4950
   * @param api The interface definition for the service.
50
   *
5151
   * @return A class that implements the interface.
5252
   */
5353
  public static <T> T load( final Class<T> api ) {
54
    @SuppressWarnings( "unchecked" )
55
    final T o = (T)get( api );
54
    @SuppressWarnings("unchecked") final T o = (T) get( api );
5655
5756
    return o == null ? newInstance( api ) : o;
...
7271
  }
7372
74
  private static void put( Class key, Object value ) {
73
  @SuppressWarnings("rawtypes")
74
  private static void put( final Class key, Object value ) {
7575
    SINGLETONS.put( key, value );
7676
  }
7777
78
  private static Object get( Class api ) {
78
  @SuppressWarnings("rawtypes")
79
  private static Object get( final Class api ) {
7980
    return SINGLETONS.get( api );
8081
  }
M src/main/java/com/scrivenvar/controls/BrowseDirectoryButton.java
2828
package com.scrivenvar.controls;
2929
30
import java.io.File;
31
import javafx.event.ActionEvent;
32
import javafx.scene.control.Tooltip;
33
import javafx.stage.DirectoryChooser;
3430
import com.scrivenvar.Messages;
3531
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
3632
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.event.ActionEvent;
34
import javafx.scene.control.Tooltip;
35
import javafx.stage.DirectoryChooser;
36
37
import java.io.File;
3738
3839
/**
39
 * Button that opens a directory chooser to select a local directory for a URL in markdown.
40
 * Button that opens a directory chooser to select a local directory for a
41
 * URL in markdown.
4042
 *
4143
 * @author Karl Tauber
4244
 */
4345
public class BrowseDirectoryButton
44
	extends BrowseFileButton
45
{
46
	public BrowseDirectoryButton() {
47
		setGraphic(FontAwesomeIconFactory.get().createIcon(FontAwesomeIcon.FOLDER_ALT, "1.2em"));
48
		setTooltip(new Tooltip(Messages.get("BrowseDirectoryButton.tooltip")));
49
	}
46
    extends BrowseFileButton {
47
  public BrowseDirectoryButton() {
48
    setGraphic( FontAwesomeIconFactory.get()
49
                                      .createIcon( FontAwesomeIcon.FOLDER_ALT,
50
                                                   "1.2em" ) );
51
    setTooltip( new Tooltip( Messages.get( "BrowseDirectoryButton.tooltip" ) ) );
52
  }
5053
51
	@Override
52
	protected void browse(ActionEvent e) {
53
		DirectoryChooser directoryChooser = new DirectoryChooser();
54
		directoryChooser.setTitle(Messages.get("BrowseDirectoryButton.chooser.title"));
55
		directoryChooser.setInitialDirectory(getInitialDirectory());
56
		File result = directoryChooser.showDialog(getScene().getWindow());
57
		if (result != null)
58
			updateUrl(result);
59
	}
54
  @Override
55
  protected void browse( ActionEvent e ) {
56
    DirectoryChooser directoryChooser = new DirectoryChooser();
57
    directoryChooser.setTitle( Messages.get(
58
        "BrowseDirectoryButton.chooser.title" ) );
59
    directoryChooser.setInitialDirectory( getInitialDirectory() );
60
    File result = directoryChooser.showDialog( getScene().getWindow() );
61
    if( result != null ) {
62
      updateUrl( result );
63
    }
64
  }
6065
}
6166
M src/main/java/com/scrivenvar/controls/EscapeTextField.java
3232
import javafx.scene.control.TextField;
3333
import javafx.util.StringConverter;
34
import com.scrivenvar.util.Utils;
3534
3635
/**
3736
 * TextField that can escape/unescape characters for markdown.
3837
 *
39
 * @author Karl Tauber
38
 * @author Karl Tauber and White Magic Software, Ltd.
4039
 */
41
public class EscapeTextField
42
	extends TextField
43
{
44
	public EscapeTextField() {
45
		escapedText.bindBidirectional(textProperty(), new StringConverter<String>() {
46
			@Override public String toString(String object) { return escape(object); }
47
			@Override public String fromString(String string) { return unescape(string); }
48
		});
49
		escapeCharacters.addListener(e -> escapedText.set(escape(textProperty().get())));
50
	}
40
public class EscapeTextField extends TextField {
5141
52
	// 'escapedText' property
53
	private final StringProperty escapedText = new SimpleStringProperty();
54
	public String getEscapedText() { return escapedText.get(); }
55
	public void setEscapedText(String escapedText) { this.escapedText.set(escapedText); }
56
	public StringProperty escapedTextProperty() { return escapedText; }
42
  public EscapeTextField() {
43
    escapedText.bindBidirectional(
44
        textProperty(),
45
        new StringConverter<>() {
46
          @Override
47
          public String toString( String object ) {
48
            return escape( object );
49
          }
5750
58
	// 'escapeCharacters' property
59
	private final StringProperty escapeCharacters = new SimpleStringProperty();
60
	public String getEscapeCharacters() { return escapeCharacters.get(); }
61
	public void setEscapeCharacters(String escapeCharacters) { this.escapeCharacters.set(escapeCharacters); }
62
	public StringProperty escapeCharactersProperty() { return escapeCharacters; }
51
          @Override
52
          public String fromString( String string ) {
53
            return unescape( string );
54
          }
55
        }
56
    );
57
    escapeCharacters.addListener(
58
        e -> escapedText.set( escape( textProperty().get() ) )
59
    );
60
  }
6361
64
	private String escape(String s) {
65
		String escapeChars = getEscapeCharacters();
66
		return !Utils.isNullOrEmpty(escapeChars)
67
				? s.replaceAll("([" + escapeChars.replaceAll("(.)", "\\\\$1") + "])", "\\\\$1")
68
				: s;
69
	}
62
  // 'escapedText' property
63
  private final StringProperty escapedText = new SimpleStringProperty();
7064
71
	private String unescape(String s) {
72
		String escapeChars = getEscapeCharacters();
73
		return !Utils.isNullOrEmpty(escapeChars)
74
				? s.replaceAll("\\\\([" + escapeChars.replaceAll("(.)", "\\\\$1") + "])", "$1")
75
				: s;
76
	}
65
  public StringProperty escapedTextProperty() {
66
    return escapedText;
67
  }
68
69
  // 'escapeCharacters' property
70
  private final StringProperty escapeCharacters = new SimpleStringProperty();
71
72
  public String getEscapeCharacters() {
73
    return escapeCharacters.get();
74
  }
75
76
  public void setEscapeCharacters( String escapeCharacters ) {
77
    this.escapeCharacters.set( escapeCharacters );
78
  }
79
80
  private String escape( final String s ) {
81
    final String escapeChars = getEscapeCharacters();
82
83
    return isEmpty( escapeChars ) ? s :
84
        s.replaceAll( "([" + escapeChars.replaceAll(
85
            "(.)",
86
            "\\\\$1" ) + "])", "\\\\$1" );
87
  }
88
89
  private String unescape( final String s ) {
90
    final String escapeChars = getEscapeCharacters();
91
92
    return isEmpty( escapeChars ) ? s :
93
        s.replaceAll( "\\\\([" + escapeChars
94
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
95
  }
96
97
  private static boolean isEmpty( final String s ) {
98
    return s == null || s.isEmpty();
99
  }
77100
}
78101
M src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
4747
  @Override
4848
  public String decorate( final String variableName ) {
49
    // 12 = PREFIX + x(...) + SUFFIX + 1 for good measure
50
    final StringBuilder sb = new StringBuilder( variableName.length() + 12 );
51
    
52
    sb.append( PREFIX );
53
    sb.append( "x( v$" );
54
    sb.append( variableName.replace( '.', '$' ) );
55
    sb.append( " )" );
56
    sb.append( SUFFIX );
57
    
58
    return sb.toString();
49
    return PREFIX +
50
        "x( v$" +
51
        variableName.replace( '.', '$' ) +
52
        " )" +
53
        SUFFIX;
5954
  }
6055
}
M src/main/java/com/scrivenvar/decorators/VariableDecorator.java
3939
   *
4040
   * @param variableName The text to decorate as per the filename extension
41
   * would indicate (e.g., ".md" goes to $VAR$ while ".Rmd" goes to `r#VAR`).
42
   *
41
   *                     would indicate (e.g., ".md" goes to $VAR$ while "
42
   *                     .Rmd" goes to `r#VAR`).
4343
   * @return The given variable name modified with its requisite delimiters.
4444
   */
45
  public String decorate( String variableName );
45
  String decorate( String variableName );
4646
}
4747
M src/main/java/com/scrivenvar/definition/AbstractDefinitionSource.java
3737
public abstract class AbstractDefinitionSource implements DefinitionSource {
3838
39
  private TreeView<String> treeView;
39
  private TreeView<String> mTreeView;
4040
4141
  /**
...
4848
  public TreeView<String> asTreeView() {
4949
50
    if( this.treeView == null ) {
51
      this.treeView = createTreeView();
52
      this.treeView.setEditable( true );
53
      this.treeView.setCellFactory(
54
        (TreeView<String> t) -> new TextFieldTreeCell()
50
    if( mTreeView == null ) {
51
      mTreeView = createTreeView();
52
      mTreeView.setEditable( true );
53
      mTreeView.setCellFactory(
54
          ( TreeView<String> t ) -> new TextFieldTreeCell()
5555
      );
5656
    }
5757
58
    return this.treeView;
58
    return mTreeView;
5959
  }
6060
6161
  /**
6262
   * Creates a newly instantiated tree view ready for adding to the definition
6363
   * pane.
6464
   *
6565
   * @return A new tree view instance, never null.
6666
   */
6767
  protected abstract TreeView<String> createTreeView();
68
69
  /**
70
   * Ensures that when preferences are saved that an
71
   * {@link EmptyDefinitionSource} does not get saved literally as its
72
   * memory reference (the default value returned by {@link Object#toString()}).
73
   *
74
   * @return Empty string.
75
   */
76
  @Override
77
  public String toString() {
78
    return "";
79
  }
6880
}
6981
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
2929
3030
import com.scrivenvar.AbstractFileFactory;
31
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_FILE;
32
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_UNKNOWN;
33
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
3431
import com.scrivenvar.FileType;
35
import static com.scrivenvar.FileType.YAML;
3632
import com.scrivenvar.definition.yaml.YamlFileDefinitionSource;
33
3734
import java.io.File;
38
import java.net.MalformedURLException;
3935
import java.net.URI;
40
import java.net.URISyntaxException;
4136
import java.net.URL;
4237
import java.nio.file.Path;
4338
import java.nio.file.Paths;
39
40
import static com.scrivenvar.Constants.*;
41
import static com.scrivenvar.FileType.YAML;
4442
4543
/**
...
6361
   *
6462
   * @param path Path to a resource containing definitions.
65
   *
6663
   * @return The definition source appropriate for the given path.
6764
   */
6865
  public DefinitionSource createDefinitionSource( final String path ) {
6966
    final String protocol = getProtocol( path );
7067
    DefinitionSource result = null;
71
72
    switch( protocol ) {
73
      case DEFINITION_PROTOCOL_FILE:
74
        final Path file = Paths.get( path );
75
        final FileType filetype = lookup( file, GLOB_PREFIX_DEFINITION );
76
        result = createFileDefinitionSource( filetype, file );
77
        break;
7868
79
      default:
80
        unknownFileType( protocol, path );
81
        break;
69
    if( DEFINITION_PROTOCOL_FILE.equals( protocol ) ) {
70
      final Path file = Paths.get( path );
71
      final FileType filetype = lookup( file, GLOB_PREFIX_DEFINITION );
72
      result = createFileDefinitionSource( filetype, file );
73
    }
74
    else {
75
      unknownFileType( protocol, path );
8276
    }
8377
8478
    return result;
8579
  }
8680
8781
  /**
8882
   * Creates a definition source based on the file type.
8983
   *
9084
   * @param filetype Property key name suffix from settings.properties file.
91
   * @param path Path to the file that corresponds to the extension.
92
   *
85
   * @param path     Path to the file that corresponds to the extension.
9386
   * @return A DefinitionSource capable of parsing the data stored at the path.
9487
   */
9588
  private DefinitionSource createFileDefinitionSource(
96
    final FileType filetype, final Path path ) {
89
      final FileType filetype, final Path path ) {
9790
9891
    DefinitionSource result = null;
99
100
    switch( filetype ) {
101
      case YAML:
102
        result = new YamlFileDefinitionSource( path );
103
        break;
10492
105
      default:
106
        unknownFileType( filetype.toString(), path.toString() );
107
        break;
93
    if( filetype == YAML ) {
94
      result = new YamlFileDefinitionSource( path );
95
    }
96
    else {
97
      unknownFileType( filetype, path );
10898
    }
10999
110100
    return result;
111101
  }
112102
113103
  /**
114104
   * Returns the protocol for a given URI or filename.
115105
   *
116106
   * @param source Determine the protocol for this URI or filename.
117
   *
118107
   * @return The protocol for the given source.
119108
   */
120109
  private String getProtocol( final String source ) {
121
    String protocol = null;
110
    String protocol;
122111
123112
    try {
...
131120
        protocol = url.getProtocol();
132121
      }
133
    } catch( final URISyntaxException | MalformedURLException e ) {
122
    } catch( final Exception e ) {
134123
      // Could be HTTP, HTTPS?
135124
      if( source.startsWith( "//" ) ) {
...
149138
   *
150139
   * @param file Determine the protocol for this file.
151
   *
152140
   * @return The protocol for the given file.
153141
   */
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
2929
3030
import com.scrivenvar.AbstractPane;
31
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
32
import com.scrivenvar.predicates.strings.ContainsPredicate;
33
import com.scrivenvar.predicates.strings.StartsPredicate;
34
import com.scrivenvar.predicates.strings.StringPredicate;
35
import static com.scrivenvar.util.Lists.getFirst;
36
import java.util.List;
37
import javafx.collections.ObservableList;
38
import javafx.event.EventHandler;
39
import javafx.event.EventType;
40
import javafx.scene.Node;
41
import javafx.scene.control.MultipleSelectionModel;
42
import javafx.scene.control.SelectionMode;
43
import javafx.scene.control.TreeItem;
44
import javafx.scene.control.TreeView;
45
import javafx.scene.input.MouseButton;
46
import static javafx.scene.input.MouseButton.PRIMARY;
47
import javafx.scene.input.MouseEvent;
48
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
49
50
/**
51
 * Provides a list of variables that can be referenced in the editor.
52
 *
53
 * @author White Magic Software, Ltd.
54
 */
55
public class DefinitionPane extends AbstractPane {
56
57
  /**
58
   * Trimmed off the end of a word to match a variable name.
59
   */
60
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
61
62
  private TreeView<String> treeView;
63
64
  /**
65
   * Constructs a definition pane with a given tree view root.
66
   *
67
   * @see YamlTreeAdapter.adapt
68
   * @param root The root of the variable definition tree.
69
   */
70
  public DefinitionPane( final TreeView<String> root ) {
71
    setTreeView( root );
72
    initTreeView();
73
  }
74
75
  /**
76
   * Allows observers to receive double-click events on the tree view.
77
   *
78
   * @param handler The handler that will receive double-click events.
79
   */
80
  public void addBranchSelectedListener(
81
    final EventHandler<? super MouseEvent> handler ) {
82
    getTreeView().addEventHandler(
83
      MouseEvent.ANY, event -> {
84
        final MouseButton button = event.getButton();
85
        final int clicks = event.getClickCount();
86
        final EventType<? extends MouseEvent> eventType = event.getEventType();
87
88
        if( PRIMARY.equals( button ) && clicks == 2 ) {
89
          if( MOUSE_CLICKED.equals( eventType ) ) {
90
            handler.handle( event );
91
          }
92
93
          event.consume();
94
        }
95
      } );
96
  }
97
98
  /**
99
   * Allows observers to stop receiving double-click events on the tree view.
100
   *
101
   * @param handler The handler that will no longer receive double-click events.
102
   */
103
  public void removeBranchSelectedListener(
104
    final EventHandler<? super MouseEvent> handler ) {
105
    getTreeView().removeEventHandler( MouseEvent.ANY, handler );
106
  }
107
108
  /**
109
   * Changes the root node of the tree view. Swaps the current root node for the
110
   * root node of the given
111
   *
112
   * @param treeView The tree view containing a new root node; if the parameter
113
   * is null, the tree is cleared.
114
   */
115
  public void setRoot( final TreeView<String> treeView ) {
116
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
117
  }
118
119
  /**
120
   * Clears the tree view by setting the root node to null.
121
   */
122
  public void clear() {
123
    setRoot( null );
124
  }
125
126
  /**
127
   * Finds a tree item with a value that exactly matches the given word.
128
   *
129
   * @param trunk The root item containing a list of nodes to search.
130
   * @param word The value of the item to find.
131
   * @param predicate Helps determine whether the node value matches the word.
132
   *
133
   * @return The item that matches the given word, or null if not found.
134
   */
135
  private TreeItem<String> findNode(
136
    final TreeItem<String> trunk,
137
    final StringPredicate predicate ) {
138
    TreeItem<String> result = null;
139
140
    if( trunk != null ) {
141
      final List<TreeItem<String>> branches = trunk.getChildren();
142
143
      for( final TreeItem<String> leaf : branches ) {
144
        if( predicate.test( leaf.getValue() ) ) {
145
          result = leaf;
146
          break;
147
        }
148
      }
149
    }
150
151
    return result;
152
  }
153
154
  /**
155
   * Calls findNode with the EqualsPredicate.
156
   *
157
   * @see findNode( TreeItem, String, Predicate )
158
   * @return The result from findNode.
159
   */
160
  private TreeItem<String> findStartsNode(
161
    final TreeItem<String> trunk,
162
    final String word ) {
163
    return findNode( trunk, new StartsPredicate( word ) );
164
  }
165
166
  /**
167
   * Calls findNode with the ContainsPredicate.
168
   *
169
   * @see findNode( TreeItem, String, Predicate )
170
   * @return The result from findNode.
171
   */
172
  private TreeItem<String> findSubstringNode(
173
    final TreeItem<String> trunk,
174
    final String word ) {
175
    return findNode( trunk, new ContainsPredicate( word ) );
176
  }
177
178
  /**
179
   * Finds a node that matches a prefix and suffix specified by the given path
180
   * variable. The prefix must match a valid node value. The suffix refers to
181
   * the start of a string that matches zero or more children of the node
182
   * specified by the prefix. The algorithm has the following cases:
183
   *
184
   * <ol>
185
   * <li>Path is empty, return first child.</li>
186
   * <li>Path contains a complete match, return corresponding node.</li>
187
   * <li>Path contains a partial match, return nearest node.</li>
188
   * <li>Path contains a complete and partial match, return nearest node.</li>
189
   * </ol>
190
   *
191
   * @param word The word typed by the user, which contains dot-separated node
192
   * names that represent a path within the YAML tree plus a partial variable
193
   * name match (for a node).
194
   *
195
   * @return The node value that starts with the suffix portion of the given
196
   * path, never null.
197
   */
198
  public TreeItem<String> findNode( final String word ) {
199
    String path = word;
200
201
    // Current tree item.
202
    TreeItem<String> cItem = getTreeRoot();
203
204
    // Previous tree item.
205
    TreeItem<String> pItem = cItem;
206
207
    int index = path.indexOf( SEPARATOR_CHAR );
208
209
    while( index >= 0 ) {
210
      final String node = path.substring( 0, index );
211
      path = path.substring( index + 1 );
212
213
      if( (cItem = findStartsNode( cItem, node )) == null ) {
214
        break;
215
      }
216
217
      index = path.indexOf( SEPARATOR_CHAR );
218
      pItem = cItem;
219
    }
220
221
    // Find the node that starts with whatever the user typed.
222
    cItem = findStartsNode( pItem, path );
223
224
    // If there was no matching node, then find a substring match.
225
    if( cItem == null ) {
226
      cItem = findSubstringNode( pItem, path );
227
    }
228
229
    // If neither starts with nor substring matched a node, revert to the last
230
    // known valid node.
231
    if( cItem == null ) {
232
      cItem = pItem;
233
    }
234
235
    return sanitize( cItem );
236
  }
237
238
  /**
239
   * Returns the leaf that matches the given value. If the value is terminally
240
   * punctuated, the punctuation is removed if no match was found.
241
   *
242
   * @param value The value to find, never null.
243
   *
244
   * @return The leaf that contains the given value, or null if neither the
245
   * original value nor the terminally-trimmed value was found.
246
   */
247
  public VariableTreeItem<String> findLeaf( final String value ) {
248
    return findLeaf( value, false );
249
  }
250
251
  /**
252
   * Returns the leaf that matches the given value. If the value is terminally
253
   * punctuated, the punctuation is removed if no match was found.
254
   *
255
   * @param value The value to find, never null.
256
   * @param contains Set to true to perform a substring match if starts with
257
   * fails to match.
258
   *
259
   * @return The leaf that contains the given value, or null if neither the
260
   * original value nor the terminally-trimmed value was found.
261
   */
262
  public VariableTreeItem<String> findLeaf(
263
    final String value,
264
    final boolean contains ) {
265
266
    final VariableTreeItem<String> root = getTreeRoot();
267
    final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
268
269
    return leaf == null
270
      ? root.findLeaf( rtrimTerminalPunctuation( value ) )
271
      : leaf;
272
  }
273
274
  /**
275
   * Removes punctuation from the end of a string. The character set includes:
276
   * <code>:;,.!?-/\¡¿</code>.
277
   *
278
   * @param s The string to trim, never null.
279
   *
280
   * @return The string trimmed of all terminal characters from the end
281
   */
282
  private String rtrimTerminalPunctuation( final String s ) {
283
    final StringBuilder result = new StringBuilder( s.trim() );
284
285
    while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
286
      result.setLength( result.length() - 1 );
287
    }
288
289
    return result.toString();
290
  }
291
292
  /**
293
   * Returns the tree root if either item or its first child are null.
294
   *
295
   * @param item The item to make null safe.
296
   *
297
   * @return A non-null TreeItem, possibly the root item (to avoid null).
298
   */
299
  private TreeItem<String> sanitize( final TreeItem<String> item ) {
300
    TreeItem<String> result;
301
302
    if( item == null ) {
303
      result = getTreeRoot();
304
    }
305
    else {
306
      result = item == getTreeRoot()
307
        ? getFirst( item.getChildren() )
308
        : item;
309
    }
310
311
    return result;
312
  }
313
314
  /**
315
   * Expands the node to the root, recursively.
316
   *
317
   * @param <T> The type of tree item to expand (usually String).
318
   * @param node The node to expand.
319
   */
320
  public <T> void expand( final TreeItem<T> node ) {
321
    if( node != null ) {
322
      expand( node.getParent() );
323
324
      if( !node.isLeaf() ) {
325
        node.setExpanded( true );
326
      }
327
    }
328
  }
329
330
  public void select( final TreeItem<String> item ) {
331
    clearSelection();
332
    selectItem( getTreeView().getRow( item ) );
333
  }
334
335
  private void clearSelection() {
336
    getSelectionModel().clearSelection();
337
  }
338
339
  private void selectItem( final int row ) {
340
    getSelectionModel().select( row );
341
  }
342
343
  /**
344
   * Collapses the tree, recursively.
345
   */
346
  public void collapse() {
347
    collapse( getTreeRoot().getChildren() );
348
  }
349
350
  /**
351
   * Collapses the tree, recursively.
352
   *
353
   * @param <T> The type of tree item to expand (usually String).
354
   * @param node The nodes to collapse.
355
   */
356
  private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
357
    for( final TreeItem<T> node : nodes ) {
358
      node.setExpanded( false );
359
      collapse( node.getChildren() );
360
    }
361
  }
362
363
  private void initTreeView() {
364
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
365
  }
366
367
  /**
368
   * Returns the root node to the tree view.
369
   *
370
   * @return getTreeView()
371
   */
372
  public Node getNode() {
373
    return getTreeView();
374
  }
375
376
  private MultipleSelectionModel getSelectionModel() {
377
    return getTreeView().getSelectionModel();
378
  }
379
380
  /**
381
   * Returns the tree view that contains the YAML definition hierarchy.
382
   *
383
   * @return A non-null instance.
384
   */
385
  private TreeView<String> getTreeView() {
386
    return this.treeView;
387
  }
388
389
  /**
390
   * Returns the root of the tree.
391
   *
392
   * @return The first node added to the YAML definition tree.
393
   */
394
  private VariableTreeItem<String> getTreeRoot() {
395
    final TreeItem<String> root = getTreeView().getRoot();
396
397
    return root instanceof VariableTreeItem ? (VariableTreeItem<String>)root : null;
398
  }
399
400
  public <T> boolean isRoot( final TreeItem<T> item ) {
401
    return getTreeRoot().equals( item );
402
  }
403
404
  /**
405
   * Sets the tree view (called by the constructor).
406
   *
407
   * @param treeView
408
   */
409
  private void setTreeView( final TreeView<String> treeView ) {
410
    if( treeView != null ) {
411
      this.treeView = treeView;
412
    }
31
32
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
33
34
import com.scrivenvar.predicates.strings.ContainsPredicate;
35
import com.scrivenvar.predicates.strings.StartsPredicate;
36
import com.scrivenvar.predicates.strings.StringPredicate;
37
38
import static com.scrivenvar.util.Lists.getFirst;
39
40
import java.util.List;
41
42
import javafx.collections.ObservableList;
43
import javafx.event.EventHandler;
44
import javafx.event.EventType;
45
import javafx.scene.Node;
46
import javafx.scene.control.MultipleSelectionModel;
47
import javafx.scene.control.SelectionMode;
48
import javafx.scene.control.TreeItem;
49
import javafx.scene.control.TreeView;
50
import javafx.scene.input.MouseButton;
51
52
import static javafx.scene.input.MouseButton.PRIMARY;
53
54
import javafx.scene.input.MouseEvent;
55
56
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
57
58
/**
59
 * Provides a list of variables that can be referenced in the editor.
60
 *
61
 * @author White Magic Software, Ltd.
62
 */
63
public class DefinitionPane extends AbstractPane {
64
65
  /**
66
   * Trimmed off the end of a word to match a variable name.
67
   */
68
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
69
70
  private final TreeView<String> mTreeView;
71
72
  /**
73
   * Constructs a definition pane with a given tree view root.
74
   * See {@link com.scrivenvar.definition.yaml.YamlTreeAdapter#adapt(String)}
75
   * for details.
76
   *
77
   * @param root The root of the variable definition tree.
78
   */
79
  public DefinitionPane( final TreeView<String> root ) {
80
    assert root != null;
81
82
    mTreeView = root;
83
    initTreeView();
84
  }
85
86
  /**
87
   * Allows observers to receive double-click events on the tree view.
88
   *
89
   * @param handler The handler that will receive double-click events.
90
   */
91
  public void addBranchSelectedListener(
92
      final EventHandler<? super MouseEvent> handler ) {
93
    getTreeView().addEventHandler(
94
        MouseEvent.ANY, event -> {
95
          final MouseButton button = event.getButton();
96
          final int clicks = event.getClickCount();
97
          final EventType<? extends MouseEvent> eventType =
98
              event.getEventType();
99
100
          if( PRIMARY.equals( button ) && clicks == 2 ) {
101
            if( MOUSE_CLICKED.equals( eventType ) ) {
102
              handler.handle( event );
103
            }
104
105
            event.consume();
106
          }
107
        } );
108
  }
109
110
  /**
111
   * Allows observers to stop receiving double-click events on the tree view.
112
   *
113
   * @param handler The handler that will no longer receive double-click events.
114
   */
115
  public void removeBranchSelectedListener(
116
      final EventHandler<? super MouseEvent> handler ) {
117
    getTreeView().removeEventHandler( MouseEvent.ANY, handler );
118
  }
119
120
  /**
121
   * Changes the root node of the tree view. Swaps the current root node for the
122
   * root node of the given
123
   *
124
   * @param treeView The tree view containing a new root node; if the parameter
125
   *                 is null, the tree is cleared.
126
   */
127
  public void setRoot( final TreeView<String> treeView ) {
128
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
129
  }
130
131
  /**
132
   * Clears the tree view by setting the root node to null.
133
   */
134
  public void clear() {
135
    setRoot( null );
136
  }
137
138
  /**
139
   * Finds a tree item with a value that exactly matches the given word.
140
   *
141
   * @param trunk     The root item containing a list of nodes to search.
142
   * @param predicate Helps determine whether the node value matches the word.
143
   * @return The item that matches the given word, or null if not found.
144
   */
145
  private TreeItem<String> findNode(
146
      final TreeItem<String> trunk,
147
      final StringPredicate predicate ) {
148
    TreeItem<String> result = null;
149
150
    if( trunk != null ) {
151
      final List<TreeItem<String>> branches = trunk.getChildren();
152
153
      for( final TreeItem<String> leaf : branches ) {
154
        if( predicate.test( leaf.getValue() ) ) {
155
          result = leaf;
156
          break;
157
        }
158
      }
159
    }
160
161
    return result;
162
  }
163
164
  /**
165
   * Calls findNode with the EqualsPredicate. See
166
   * {@link #findNode(TreeItem, StringPredicate)} for details.
167
   *
168
   * @return The result from findNode.
169
   */
170
  private TreeItem<String> findStartsNode(
171
      final TreeItem<String> trunk,
172
      final String word ) {
173
    return findNode( trunk, new StartsPredicate( word ) );
174
  }
175
176
  /**
177
   * Calls findNode with the ContainsPredicate. See
178
   * {@link #findNode(TreeItem, StringPredicate)} for details.
179
   *
180
   * @return The result from findNode.
181
   */
182
  private TreeItem<String> findSubstringNode(
183
      final TreeItem<String> trunk,
184
      final String word ) {
185
    return findNode( trunk, new ContainsPredicate( word ) );
186
  }
187
188
  /**
189
   * Finds a node that matches a prefix and suffix specified by the given path
190
   * variable. The prefix must match a valid node value. The suffix refers to
191
   * the start of a string that matches zero or more children of the node
192
   * specified by the prefix. The algorithm has the following cases:
193
   *
194
   * <ol>
195
   * <li>Path is empty, return first child.</li>
196
   * <li>Path contains a complete match, return corresponding node.</li>
197
   * <li>Path contains a partial match, return nearest node.</li>
198
   * <li>Path contains a complete and partial match, return nearest node.</li>
199
   * </ol>
200
   *
201
   * @param word The word typed by the user, which contains dot-separated node
202
   *             names that represent a path within the YAML tree plus a
203
   *             partial variable
204
   *             name match (for a node).
205
   * @return The node value that starts with the suffix portion of the given
206
   * path, never null.
207
   */
208
  public TreeItem<String> findNode( final String word ) {
209
    String path = word;
210
211
    // Current tree item.
212
    TreeItem<String> cItem = getTreeRoot();
213
214
    // Previous tree item.
215
    TreeItem<String> pItem = cItem;
216
217
    int index = path.indexOf( SEPARATOR_CHAR );
218
219
    while( index >= 0 ) {
220
      final String node = path.substring( 0, index );
221
      path = path.substring( index + 1 );
222
223
      if( (cItem = findStartsNode( cItem, node )) == null ) {
224
        break;
225
      }
226
227
      index = path.indexOf( SEPARATOR_CHAR );
228
      pItem = cItem;
229
    }
230
231
    // Find the node that starts with whatever the user typed.
232
    cItem = findStartsNode( pItem, path );
233
234
    // If there was no matching node, then find a substring match.
235
    if( cItem == null ) {
236
      cItem = findSubstringNode( pItem, path );
237
    }
238
239
    // If neither starts with nor substring matched a node, revert to the last
240
    // known valid node.
241
    if( cItem == null ) {
242
      cItem = pItem;
243
    }
244
245
    return sanitize( cItem );
246
  }
247
248
  /**
249
   * Returns the leaf that matches the given value. If the value is terminally
250
   * punctuated, the punctuation is removed if no match was found.
251
   *
252
   * @param value The value to find, never null.
253
   * @return The leaf that contains the given value, or null if neither the
254
   * original value nor the terminally-trimmed value was found.
255
   */
256
  public VariableTreeItem<String> findLeaf( final String value ) {
257
    return findLeaf( value, false );
258
  }
259
260
  /**
261
   * Returns the leaf that matches the given value. If the value is terminally
262
   * punctuated, the punctuation is removed if no match was found.
263
   *
264
   * @param value    The value to find, never null.
265
   * @param contains Set to true to perform a substring match if starts with
266
   *                 fails to match.
267
   * @return The leaf that contains the given value, or null if neither the
268
   * original value nor the terminally-trimmed value was found.
269
   */
270
  public VariableTreeItem<String> findLeaf(
271
      final String value,
272
      final boolean contains ) {
273
274
    final VariableTreeItem<String> root = getTreeRoot();
275
    final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
276
277
    return leaf == null
278
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
279
        : leaf;
280
  }
281
282
  /**
283
   * Removes punctuation from the end of a string. The character set includes:
284
   * <code>:;,.!?-/\¡¿</code>.
285
   *
286
   * @param s The string to trim, never null.
287
   * @return The string trimmed of all terminal characters from the end
288
   */
289
  private String rtrimTerminalPunctuation( final String s ) {
290
    final StringBuilder result = new StringBuilder( s.trim() );
291
292
    while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
293
      result.setLength( result.length() - 1 );
294
    }
295
296
    return result.toString();
297
  }
298
299
  /**
300
   * Returns the tree root if either item or its first child are null.
301
   *
302
   * @param item The item to make null safe.
303
   * @return A non-null TreeItem, possibly the root item (to avoid null).
304
   */
305
  private TreeItem<String> sanitize( final TreeItem<String> item ) {
306
    TreeItem<String> result;
307
308
    if( item == null ) {
309
      result = getTreeRoot();
310
    }
311
    else {
312
      result = item == getTreeRoot()
313
          ? getFirst( item.getChildren() )
314
          : item;
315
    }
316
317
    return result;
318
  }
319
320
  /**
321
   * Expands the node to the root, recursively.
322
   *
323
   * @param <T>  The type of tree item to expand (usually String).
324
   * @param node The node to expand.
325
   */
326
  public <T> void expand( final TreeItem<T> node ) {
327
    if( node != null ) {
328
      expand( node.getParent() );
329
330
      if( !node.isLeaf() ) {
331
        node.setExpanded( true );
332
      }
333
    }
334
  }
335
336
  public void select( final TreeItem<String> item ) {
337
    clearSelection();
338
    selectItem( getTreeView().getRow( item ) );
339
  }
340
341
  private void clearSelection() {
342
    getSelectionModel().clearSelection();
343
  }
344
345
  private void selectItem( final int row ) {
346
    getSelectionModel().select( row );
347
  }
348
349
  /**
350
   * Collapses the tree, recursively.
351
   */
352
  public void collapse() {
353
    collapse( getTreeRoot().getChildren() );
354
  }
355
356
  /**
357
   * Collapses the tree, recursively.
358
   *
359
   * @param <T>   The type of tree item to expand (usually String).
360
   * @param nodes The nodes to collapse.
361
   */
362
  private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
363
    for( final TreeItem<T> node : nodes ) {
364
      node.setExpanded( false );
365
      collapse( node.getChildren() );
366
    }
367
  }
368
369
  private void initTreeView() {
370
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
371
  }
372
373
  /**
374
   * Returns the root node to the tree view.
375
   *
376
   * @return getTreeView()
377
   */
378
  public Node getNode() {
379
    return getTreeView();
380
  }
381
382
  private MultipleSelectionModel getSelectionModel() {
383
    return getTreeView().getSelectionModel();
384
  }
385
386
  /**
387
   * Returns the tree view that contains the YAML definition hierarchy.
388
   *
389
   * @return A non-null instance.
390
   */
391
  private TreeView<String> getTreeView() {
392
    return mTreeView;
393
  }
394
395
  /**
396
   * Returns the root of the tree.
397
   *
398
   * @return The first node added to the YAML definition tree.
399
   */
400
  private VariableTreeItem<String> getTreeRoot() {
401
    final TreeItem<String> root = getTreeView().getRoot();
402
403
    return root instanceof VariableTreeItem ?
404
        (VariableTreeItem<String>) root : null;
405
  }
406
407
  public <T> boolean isRoot( final TreeItem<T> item ) {
408
    return getTreeRoot().equals( item );
413409
  }
414410
}
M src/main/java/com/scrivenvar/definition/DefinitionSource.java
2828
package com.scrivenvar.definition;
2929
30
import java.util.Map;
3130
import javafx.scene.control.TreeView;
31
32
import java.util.Map;
3233
3334
/**
...
4546
   * @return A hierarchical tree suitable for displaying in the definition pane.
4647
   */
47
  public TreeView<String> asTreeView();
48
  TreeView<String> asTreeView();
4849
4950
  /**
5051
   * Returns all the strings with their values resolved in a flat hierarchy.
5152
   * This copies all the keys and resolved values into a new map.
5253
   *
5354
   * @return The new map created with all values having been resolved,
5455
   * recursively.
5556
   */
56
  public Map<String, String> getResolvedMap();
57
  Map<String, String> getResolvedMap();
58
59
  /**
60
   * Returns the error message, if any, that occurred while loading the
61
   * definition source.
62
   *
63
   * @return The empty string if no error occurred, otherwise the error message.
64
   */
65
  default String getError() {
66
    return "";
67
  }
5768
5869
  /**
5970
   * Must return a re-loadable path to the data source. For a file, this is the
6071
   * absolute file path. For a database, this could be the JDBC connection. For
6172
   * a web site, this might be the GET URL.
6273
   *
6374
   * @return A non-null, non-empty string.
6475
   */
6576
  @Override
66
  public String toString();
77
  String toString();
6778
}
6879
M src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java
4242
  }
4343
44
4544
  @Override
4645
  public Map<String, String> getResolvedMap() {
M src/main/java/com/scrivenvar/definition/FileDefinitionSource.java
3737
public abstract class FileDefinitionSource extends AbstractDefinitionSource {
3838
39
  private Path path;
39
  private Path mPath;
4040
4141
  /**
...
5151
5252
  private void setPath( final Path path ) {
53
    this.path = path;
53
    mPath = path;
5454
  }
5555
5656
  public Path getPath() {
57
    return this.path;
57
    return mPath;
5858
  }
5959
6060
  /**
61
   * Returns the path represented by this object.
62
   *
63
   * @return The
61
   * @return The path represented by this object.
6462
   */
6563
  @Override
M src/main/java/com/scrivenvar/definition/TextFieldTreeCell.java
2828
package com.scrivenvar.definition;
2929
30
import static com.scrivenvar.Messages.get;
3130
import javafx.event.ActionEvent;
32
import javafx.scene.control.ContextMenu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.TextField;
35
import javafx.scene.control.TreeCell;
36
import javafx.scene.control.TreeItem;
37
import static javafx.scene.input.KeyCode.ENTER;
38
import static javafx.scene.input.KeyCode.ESCAPE;
31
import javafx.scene.control.*;
3932
import javafx.scene.input.KeyEvent;
33
34
import static com.scrivenvar.Messages.get;
4035
4136
/**
...
5752
    final MenuItem removeItem = createMenuItem( "Definition.menu.remove" );
5853
59
    addItem.setOnAction( (ActionEvent e) -> {
60
      final VariableTreeItem<String> treeItem = new VariableTreeItem<>( "Undefined" );
54
    addItem.setOnAction( ( ActionEvent e ) -> {
55
      final VariableTreeItem<String> treeItem = new VariableTreeItem<>(
56
          "Undefined" );
6157
      getTreeItem().getChildren().add( treeItem );
6258
    } );
6359
64
    removeItem.setOnAction( (ActionEvent e) -> {
65
      final TreeItem c = getTreeItem();
60
    removeItem.setOnAction( ( ActionEvent e ) -> {
61
      final TreeItem<?> c = getTreeItem();
6662
      c.getParent().getChildren().remove( c );
6763
    } );
...
9793
    super.cancelEdit();
9894
99
    setText( (String)getItem() );
95
    setText( getItem() );
10096
    setGraphic( getTreeItem().getGraphic() );
10197
  }
...
129125
    final TextField tf = new TextField( getItemValue() );
130126
131
    tf.setOnKeyReleased( (KeyEvent t) -> {
127
    tf.setOnKeyReleased( ( KeyEvent t ) -> {
132128
      switch( t.getCode() ) {
133129
        case ENTER:
M src/main/java/com/scrivenvar/definition/VariableTreeItem.java
119119
    }
120120
121
    return (VariableTreeItem<T>)node;
121
    return node;
122122
  }
123123
M src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
103103
    );
104104
  }
105
106
  @Override
107
  public String getError() {
108
    return getYamlParser().getError();
109
  }
105110
}
106111
M src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
2828
package com.scrivenvar.definition.yaml;
2929
30
import com.fasterxml.jackson.core.JsonGenerationException;
31
import com.fasterxml.jackson.core.ObjectCodec;
32
import com.fasterxml.jackson.core.io.IOContext;
33
import com.fasterxml.jackson.databind.JsonNode;
34
import com.fasterxml.jackson.databind.ObjectMapper;
35
import com.fasterxml.jackson.databind.node.NullNode;
36
import com.fasterxml.jackson.databind.node.ObjectNode;
37
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
38
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
39
import com.scrivenvar.decorators.VariableDecorator;
40
import com.scrivenvar.decorators.YamlVariableDecorator;
41
import java.io.IOException;
42
import java.io.InputStream;
43
import java.io.Writer;
44
import java.text.MessageFormat;
45
import java.util.HashMap;
46
import java.util.Map;
47
import java.util.Map.Entry;
48
import java.util.regex.Matcher;
49
import java.util.regex.Pattern;
50
import org.yaml.snakeyaml.DumperOptions;
51
52
/**
53
 * <p>
54
 * This program loads a YAML document into memory, scans for variable
55
 * declarations, then substitutes any self-referential values back into the
56
 * document. Its output is the given YAML document without any variables.
57
 * Variables in the YAML document are denoted using a bracketed dollar symbol
58
 * syntax. For example: $field.name$. Some nomenclature to keep from going
59
 * squirrely, consider:
60
 * </p>
61
 *
62
 * <pre>
63
 *   root:
64
 *     node:
65
 *       name: $field.name$
66
 *   field:
67
 *     name: Alan Turing
68
 * </pre>
69
 *
70
 * The various components of the given YAML are called:
71
 *
72
 * <ul>
73
 * <li><code>$field.name$</code> - delimited reference</li>
74
 * <li><code>field.name</code> - reference</li>
75
 * <li><code>name</code> - YAML field</li>
76
 * <li><code>Alan Turing</code> - (dereferenced) field value</li>
77
 * </ul>
78
 *
79
 * @author White Magic Software, Ltd.
80
 */
81
public class YamlParser {
82
83
  /**
84
   * Separates YAML variable nodes (e.g., the dots in
85
   * <code>$root.node.var$</code>).
86
   */
87
  public static final String SEPARATOR = ".";
88
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
89
90
  private final static int GROUP_DELIMITED = 1;
91
  private final static int GROUP_REFERENCE = 2;
92
93
  private final static VariableDecorator VARIABLE_DECORATOR
94
    = new YamlVariableDecorator();
95
96
  private String error;
97
98
  /**
99
   * Compiled version of DEFAULT_REGEX.
100
   */
101
  private final static Pattern REGEX_PATTERN
102
    = Pattern.compile( YamlVariableDecorator.REGEX );
103
104
  /**
105
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
106
   */
107
  private final static char SEPARATOR_YAML = '/';
108
109
  /**
110
   * Start of the Universe (the YAML document node that contains all others).
111
   */
112
  private JsonNode documentRoot;
113
114
  /**
115
   * Map of references to dereferenced field values.
116
   */
117
  private Map<String, String> references;
118
119
  public YamlParser( final InputStream in ) throws IOException {
120
    process( in );
121
  }
122
123
  /**
124
   * Returns the given string with all the delimited references swapped with
125
   * their recursively resolved values.
126
   *
127
   * @param text The text to parse with zero or more delimited references to
128
   * replace.
129
   *
130
   * @return The substituted value.
131
   */
132
  public String substitute( String text ) {
133
    final Matcher matcher = patternMatch( text );
134
    final Map<String, String> map = getReferences();
135
136
    while( matcher.find() ) {
137
      final String key = matcher.group( GROUP_DELIMITED );
138
      final String value = map.get( key );
139
140
      if( value == null ) {
141
        missing( text );
142
      }
143
      else {
144
        text = text.replace( key, value );
145
      }
146
    }
147
148
    return text;
149
  }
150
151
  /**
152
   * Returns all the strings with their values resolved in a flat hierarchy.
153
   * This copies all the keys and resolved values into a new map.
154
   *
155
   * @return The new map created with all values having been resolved,
156
   * recursively.
157
   */
158
  public Map<String, String> createResolvedMap() {
159
    final Map<String, String> map = new HashMap<>( 1024 );
160
161
    resolve( getDocumentRoot(), "", map );
162
163
    return map;
164
  }
165
166
  /**
167
   * Iterate over a given root node (at any level of the tree) and adapt each
168
   * leaf node.
169
   *
170
   * @param rootNode A JSON node (YAML node) to adapt.
171
   * @param map Container that associates definitions with values.
172
   */
173
  private void resolve(
174
    final JsonNode rootNode,
175
    final String path,
176
    final Map<String, String> map ) {
177
178
    if( rootNode != null ) {
179
      rootNode.fields().forEachRemaining(
180
        (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
181
      );
182
    }
183
  }
184
185
  /**
186
   * Recursively adapt each rootNode to a corresponding rootItem.
187
   *
188
   * @param rootNode The node to adapt.
189
   */
190
  private void resolve(
191
    final Entry<String, JsonNode> rootNode,
192
    final String path,
193
    final Map<String, String> map ) {
194
195
    final JsonNode leafNode = rootNode.getValue();
196
    final String key = rootNode.getKey();
197
198
    
199
    if( leafNode.isValueNode() ) {
200
      final String value;
201
      
202
      if( leafNode instanceof NullNode ) {
203
        value = "";
204
      }
205
      else {
206
        value = rootNode.getValue().asText();
207
      }
208
      
209
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
210
    }
211
212
    if( leafNode.isObject() ) {
213
      resolve( leafNode, path + key + SEPARATOR, map );
214
    }
215
  }
216
217
  /**
218
   * Reads the first document from the given stream of YAML data and returns a
219
   * corresponding object that represents the YAML hierarchy. The calling class
220
   * is responsible for closing the stream. Calling classes should use
221
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
222
   *
223
   * @param in The input stream containing YAML content.
224
   *
225
   * @return An object hierarchy to represent the content.
226
   *
227
   * @throws IOException Could not read the stream.
228
   */
229
  private JsonNode process( final InputStream in ) throws IOException {
230
    final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
231
    setDocumentRoot( root );
232
    process( root );
233
    return getDocumentRoot();
234
  }
235
236
  /**
237
   * Iterate over a given root node (at any level of the tree) and process each
238
   * leaf node.
239
   *
240
   * @param root A node to process.
241
   */
242
  private void process( final JsonNode root ) {
243
    root.fields().forEachRemaining( this::process );
244
  }
245
246
  /**
247
   * Process the given field, which is a named node. This is where the
248
   * application does the up-front work of mapping references to their fully
249
   * recursively dereferenced values.
250
   *
251
   * @param field The named node.
252
   */
253
  private void process( final Entry<String, JsonNode> field ) {
254
    final JsonNode node = field.getValue();
255
256
    if( node.isObject() ) {
257
      process( node );
258
    }
259
    else {
260
      final JsonNode fieldValue = field.getValue();
261
262
      // Only basic data types can be parsed into variable values. For
263
      // node structures, YAML has a built-in mechanism.
264
      if( fieldValue.isValueNode() ) {
265
        try {
266
          resolve( fieldValue.asText() );
267
        } catch( StackOverflowError e ) {
268
          setError( "Unresolvable: " + node.textValue() + " = " + fieldValue );
269
        }
270
      }
271
    }
272
  }
273
274
  /**
275
   * Inserts the delimited references and field values into the cache. This will
276
   * overwrite existing references.
277
   *
278
   * @param fieldValue YAML field containing zero or more delimited references.
279
   * If it contains a delimited reference, the parameter is modified with the
280
   * dereferenced value before it is returned.
281
   *
282
   * @return fieldValue without delimited references.
283
   */
284
  private String resolve( String fieldValue ) {
285
    final Matcher matcher = patternMatch( fieldValue );
286
287
    while( matcher.find() ) {
288
      final String delimited = matcher.group( GROUP_DELIMITED );
289
      final String reference = matcher.group( GROUP_REFERENCE );
290
      final String dereference = resolve( lookup( reference ) );
291
292
      fieldValue = fieldValue.replace( delimited, dereference );
293
294
      // This will perform some superfluous calls by overwriting existing
295
      // items in the delimited reference map.
296
      put( delimited, dereference );
297
    }
298
299
    return fieldValue;
300
  }
301
302
  /**
303
   * Inserts a key/value pair into the references map. The map retains
304
   * references and dereferenced values found in the YAML. If the reference
305
   * already exists, this will overwrite with a new value.
306
   *
307
   * @param delimited The variable name.
308
   * @param dereferenced The resolved value.
309
   */
310
  private void put( String delimited, String dereferenced ) {
311
    if( dereferenced.isEmpty() ) {
312
      missing( delimited );
313
    }
314
    else {
315
      getReferences().put( delimited, dereferenced );
316
    }
317
  }
318
319
  /**
320
   * Writes the modified YAML document to standard output.
321
   */
322
  private void writeDocument() throws IOException {
323
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
324
  }
325
326
  /**
327
   * Called when a delimited reference is dereferenced to an empty string. This
328
   * should produce a warning for the user.
329
   *
330
   * @param delimited Delimited reference with no derived value.
331
   */
332
  private void missing( final String delimited ) {
333
    setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) );
334
  }
335
336
  /**
337
   * Returns a REGEX_PATTERN matcher for the given text.
338
   *
339
   * @param text The text that contains zero or more instances of a
340
   * REGEX_PATTERN that can be found using the regular expression.
341
   */
342
  private Matcher patternMatch( String text ) {
343
    return getPattern().matcher( text );
344
  }
345
346
  /**
347
   * Finds the YAML value for a reference.
348
   *
349
   * @param reference References a value in the YAML document.
350
   *
351
   * @return The dereferenced value.
352
   */
353
  private String lookup( final String reference ) {
354
    return getDocumentRoot().at( asPath( reference ) ).asText();
355
  }
356
357
  /**
358
   * Converts a reference (not delimited) to a path that can be used to find a
359
   * value that should exist inside the YAML document.
360
   *
361
   * @param reference The reference to convert to a YAML document path.
362
   *
363
   * @return The reference with a leading slash and its separator characters
364
   * converted to slashes.
365
   */
366
  private String asPath( final String reference ) {
367
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
368
  }
369
370
  /**
371
   * Sets the parent node for the entire YAML document tree.
372
   *
373
   * @param documentRoot The parent node.
374
   */
375
  private void setDocumentRoot( final ObjectNode documentRoot ) {
376
    this.documentRoot = documentRoot;
377
  }
378
379
  /**
380
   * Returns the parent node for the entire YAML document tree.
381
   *
382
   * @return The parent node.
383
   */
384
  protected JsonNode getDocumentRoot() {
385
    return this.documentRoot;
386
  }
387
388
  /**
389
   * Returns the compiled regular expression REGEX_PATTERN used to match
390
   * delimited references.
391
   *
392
   * @return A compiled regex for use with the Matcher.
393
   */
394
  private Pattern getPattern() {
395
    return REGEX_PATTERN;
396
  }
397
398
  /**
399
   * Returns the list of references mapped to dereferenced values.
400
   *
401
   * @return
402
   */
403
  private Map<String, String> getReferences() {
404
    if( this.references == null ) {
405
      this.references = createReferences();
406
    }
407
408
    return this.references;
409
  }
410
411
  /**
412
   * Subclasses can override this method to insert their own map.
413
   *
414
   * @return An empty HashMap, never null.
415
   */
416
  protected Map<String, String> createReferences() {
417
    return new HashMap<>();
418
  }
419
420
  private final class ResolverYAMLFactory extends YAMLFactory {
421
422
    private static final long serialVersionUID = 1L;
423
424
    @Override
425
    protected YAMLGenerator _createGenerator(
426
      final Writer out, final IOContext ctxt ) throws IOException {
427
428
      return new ResolverYAMLGenerator(
429
        ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
430
        out, _version );
431
    }
432
  }
433
434
  private class ResolverYAMLGenerator extends YAMLGenerator {
435
436
    public ResolverYAMLGenerator(
437
      final IOContext ctxt,
438
      final int jsonFeatures,
439
      final int yamlFeatures,
440
      final ObjectCodec codec,
441
      final Writer out,
442
      final DumperOptions.Version version ) throws IOException {
443
444
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
445
    }
446
447
    @Override
448
    public void writeString( final String text )
449
      throws IOException, JsonGenerationException {
450
      super.writeString( substitute( text ) );
451
    }
452
  }
453
454
  private YAMLFactory getYAMLFactory() {
455
    return new ResolverYAMLFactory();
456
  }
457
458
  private ObjectMapper getObjectMapper() {
459
    return new ObjectMapper( getYAMLFactory() );
460
  }
461
462
  /**
463
   * Returns the character used to separate YAML paths within delimited
464
   * references. This will return only the first character of the command line
465
   * parameter, if the default is overridden.
466
   *
467
   * @return A period by default.
468
   */
469
  private char getDelimitedSeparator() {
470
    return SEPARATOR.charAt( 0 );
471
  }
472
473
  private void setError( final String error ) {
474
    this.error = error;
475
  }
476
477
  /**
478
   * Returns the last error message, if any, that occurred during parsing.
479
   *
480
   * @return The error message or the empty string if no error occurred.
481
   */
482
  public String getError() {
483
    return this.error == null ? "" : this.error;
30
import com.fasterxml.jackson.core.ObjectCodec;
31
import com.fasterxml.jackson.core.io.IOContext;
32
import com.fasterxml.jackson.databind.JsonNode;
33
import com.fasterxml.jackson.databind.ObjectMapper;
34
import com.fasterxml.jackson.databind.node.NullNode;
35
import com.fasterxml.jackson.databind.node.ObjectNode;
36
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
37
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
38
import com.scrivenvar.decorators.VariableDecorator;
39
import com.scrivenvar.decorators.YamlVariableDecorator;
40
import org.yaml.snakeyaml.DumperOptions;
41
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.io.Writer;
45
import java.text.MessageFormat;
46
import java.util.HashMap;
47
import java.util.Map;
48
import java.util.Map.Entry;
49
import java.util.regex.Matcher;
50
import java.util.regex.Pattern;
51
52
/**
53
 * <p>
54
 * This program loads a YAML document into memory, scans for variable
55
 * declarations, then substitutes any self-referential values back into the
56
 * document. Its output is the given YAML document without any variables.
57
 * Variables in the YAML document are denoted using a bracketed dollar symbol
58
 * syntax. For example: $field.name$. Some nomenclature to keep from going
59
 * squirrely, consider:
60
 * </p>
61
 *
62
 * <pre>
63
 *   root:
64
 *     node:
65
 *       name: $field.name$
66
 *   field:
67
 *     name: Alan Turing
68
 * </pre>
69
 * <p>
70
 * The various components of the given YAML are called:
71
 *
72
 * <ul>
73
 * <li><code>$field.name$</code> - delimited reference</li>
74
 * <li><code>field.name</code> - reference</li>
75
 * <li><code>name</code> - YAML field</li>
76
 * <li><code>Alan Turing</code> - (dereferenced) field value</li>
77
 * </ul>
78
 *
79
 * @author White Magic Software, Ltd.
80
 */
81
public class YamlParser {
82
83
  /**
84
   * Separates YAML variable nodes (e.g., the dots in
85
   * <code>$root.node.var$</code>).
86
   */
87
  public static final String SEPARATOR = ".";
88
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
89
90
  private final static int GROUP_DELIMITED = 1;
91
  private final static int GROUP_REFERENCE = 2;
92
93
  private final static VariableDecorator VARIABLE_DECORATOR
94
      = new YamlVariableDecorator();
95
96
  private String mError;
97
98
  /**
99
   * Compiled version of DEFAULT_REGEX.
100
   */
101
  private final static Pattern REGEX_PATTERN
102
      = Pattern.compile( YamlVariableDecorator.REGEX );
103
104
  /**
105
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
106
   */
107
  private final static char SEPARATOR_YAML = '/';
108
109
  /**
110
   * Start of the Universe (the YAML document node that contains all others).
111
   */
112
  private JsonNode documentRoot;
113
114
  /**
115
   * Map of references to dereferenced field values.
116
   */
117
  private Map<String, String> references;
118
119
  public YamlParser( final InputStream in ) throws IOException {
120
    process( in );
121
  }
122
123
  /**
124
   * Returns the given string with all the delimited references swapped with
125
   * their recursively resolved values.
126
   *
127
   * @param text The text to parse with zero or more delimited references to
128
   *             replace.
129
   * @return The substituted value.
130
   */
131
  public String substitute( String text ) {
132
    final Matcher matcher = patternMatch( text );
133
    final Map<String, String> map = getReferences();
134
135
    while( matcher.find() ) {
136
      final String key = matcher.group( GROUP_DELIMITED );
137
      final String value = map.get( key );
138
139
      if( value == null ) {
140
        missing( text );
141
      }
142
      else {
143
        text = text.replace( key, value );
144
      }
145
    }
146
147
    return text;
148
  }
149
150
  /**
151
   * Returns all the strings with their values resolved in a flat hierarchy.
152
   * This copies all the keys and resolved values into a new map.
153
   *
154
   * @return The new map created with all values having been resolved,
155
   * recursively.
156
   */
157
  public Map<String, String> createResolvedMap() {
158
    final Map<String, String> map = new HashMap<>( 1024 );
159
160
    resolve( getDocumentRoot(), "", map );
161
162
    return map;
163
  }
164
165
  /**
166
   * Iterate over a given root node (at any level of the tree) and adapt each
167
   * leaf node.
168
   *
169
   * @param rootNode A JSON node (YAML node) to adapt.
170
   * @param map      Container that associates definitions with values.
171
   */
172
  private void resolve(
173
      final JsonNode rootNode,
174
      final String path,
175
      final Map<String, String> map ) {
176
177
    if( rootNode != null ) {
178
      rootNode.fields().forEachRemaining(
179
          ( Entry<String, JsonNode> leaf ) -> resolve( leaf, path, map )
180
      );
181
    }
182
  }
183
184
  /**
185
   * Recursively adapt each rootNode to a corresponding rootItem.
186
   *
187
   * @param rootNode The node to adapt.
188
   */
189
  private void resolve(
190
      final Entry<String, JsonNode> rootNode,
191
      final String path,
192
      final Map<String, String> map ) {
193
194
    final JsonNode leafNode = rootNode.getValue();
195
    final String key = rootNode.getKey();
196
197
198
    if( leafNode.isValueNode() ) {
199
      final String value;
200
201
      if( leafNode instanceof NullNode ) {
202
        value = "";
203
      }
204
      else {
205
        value = rootNode.getValue().asText();
206
      }
207
208
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
209
    }
210
211
    if( leafNode.isObject() ) {
212
      resolve( leafNode, path + key + SEPARATOR, map );
213
    }
214
  }
215
216
  /**
217
   * Reads the first document from the given stream of YAML data and returns a
218
   * corresponding object that represents the YAML hierarchy. The calling class
219
   * is responsible for closing the stream. Calling classes should use
220
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
221
   *
222
   * @param in The input stream containing YAML content.
223
   * @throws IOException Could not read the stream.
224
   */
225
  private void process( final InputStream in ) throws IOException {
226
    final ObjectNode root = (ObjectNode) getObjectMapper().readTree( in );
227
    setDocumentRoot( root );
228
    process( root );
229
  }
230
231
  /**
232
   * Iterate over a given root node (at any level of the tree) and process each
233
   * leaf node.
234
   *
235
   * @param root A node to process.
236
   */
237
  private void process( final JsonNode root ) {
238
    root.fields().forEachRemaining( this::process );
239
  }
240
241
  /**
242
   * Process the given field, which is a named node. This is where the
243
   * application does the up-front work of mapping references to their fully
244
   * recursively dereferenced values.
245
   *
246
   * @param field The named node.
247
   */
248
  private void process( final Entry<String, JsonNode> field ) {
249
    final JsonNode node = field.getValue();
250
251
    if( node.isObject() ) {
252
      process( node );
253
    }
254
    else {
255
      final JsonNode fieldValue = field.getValue();
256
257
      // Only basic data types can be parsed into variable values. For
258
      // node structures, YAML has a built-in mechanism.
259
      if( fieldValue.isValueNode() ) {
260
        try {
261
          resolve( fieldValue.asText() );
262
        } catch( StackOverflowError e ) {
263
          setError( "Unresolvable: " + node.textValue() + " = " + fieldValue );
264
        }
265
      }
266
    }
267
  }
268
269
  /**
270
   * Inserts the delimited references and field values into the cache. This will
271
   * overwrite existing references.
272
   *
273
   * @param fieldValue YAML field containing zero or more delimited references.
274
   *                   If it contains a delimited reference, the parameter is
275
   *                   modified with the
276
   *                   dereferenced value before it is returned.
277
   * @return fieldValue without delimited references.
278
   */
279
  private String resolve( String fieldValue ) {
280
    final Matcher matcher = patternMatch( fieldValue );
281
282
    while( matcher.find() ) {
283
      final String delimited = matcher.group( GROUP_DELIMITED );
284
      final String reference = matcher.group( GROUP_REFERENCE );
285
      final String dereference = resolve( lookup( reference ) );
286
287
      fieldValue = fieldValue.replace( delimited, dereference );
288
289
      // This will perform some superfluous calls by overwriting existing
290
      // items in the delimited reference map.
291
      put( delimited, dereference );
292
    }
293
294
    return fieldValue;
295
  }
296
297
  /**
298
   * Inserts a key/value pair into the references map. The map retains
299
   * references and dereferenced values found in the YAML. If the reference
300
   * already exists, this will overwrite with a new value.
301
   *
302
   * @param delimited    The variable name.
303
   * @param dereferenced The resolved value.
304
   */
305
  private void put( String delimited, String dereferenced ) {
306
    if( dereferenced.isEmpty() ) {
307
      missing( delimited );
308
    }
309
    else {
310
      getReferences().put( delimited, dereferenced );
311
    }
312
  }
313
314
  /**
315
   * Writes the modified YAML document to standard output.
316
   */
317
  private void writeDocument() throws IOException {
318
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
319
  }
320
321
  /**
322
   * Called when a delimited reference is dereferenced to an empty string. This
323
   * should produce a warning for the user.
324
   *
325
   * @param delimited Delimited reference with no derived value.
326
   */
327
  private void missing( final String delimited ) {
328
    setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) );
329
  }
330
331
  /**
332
   * Returns a REGEX_PATTERN matcher for the given text.
333
   *
334
   * @param text The text that contains zero or more instances of a
335
   *             REGEX_PATTERN that can be found using the regular expression.
336
   */
337
  private Matcher patternMatch( String text ) {
338
    return getPattern().matcher( text );
339
  }
340
341
  /**
342
   * Finds the YAML value for a reference.
343
   *
344
   * @param reference References a value in the YAML document.
345
   * @return The dereferenced value.
346
   */
347
  private String lookup( final String reference ) {
348
    return getDocumentRoot().at( asPath( reference ) ).asText();
349
  }
350
351
  /**
352
   * Converts a reference (not delimited) to a path that can be used to find a
353
   * value that should exist inside the YAML document.
354
   *
355
   * @param reference The reference to convert to a YAML document path.
356
   * @return The reference with a leading slash and its separator characters
357
   * converted to slashes.
358
   */
359
  private String asPath( final String reference ) {
360
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(),
361
                                               SEPARATOR_YAML );
362
  }
363
364
  /**
365
   * Sets the parent node for the entire YAML document tree.
366
   *
367
   * @param documentRoot The parent node.
368
   */
369
  private void setDocumentRoot( final ObjectNode documentRoot ) {
370
    this.documentRoot = documentRoot;
371
  }
372
373
  /**
374
   * Returns the parent node for the entire YAML document tree.
375
   *
376
   * @return The parent node.
377
   */
378
  protected JsonNode getDocumentRoot() {
379
    return this.documentRoot;
380
  }
381
382
  /**
383
   * Returns the compiled regular expression REGEX_PATTERN used to match
384
   * delimited references.
385
   *
386
   * @return A compiled regex for use with the Matcher.
387
   */
388
  private Pattern getPattern() {
389
    return REGEX_PATTERN;
390
  }
391
392
  /**
393
   * @return The list of references mapped to dereferenced values.
394
   */
395
  private Map<String, String> getReferences() {
396
    if( this.references == null ) {
397
      this.references = createReferences();
398
    }
399
400
    return this.references;
401
  }
402
403
  /**
404
   * Subclasses can override this method to insert their own map.
405
   *
406
   * @return An empty HashMap, never null.
407
   */
408
  protected Map<String, String> createReferences() {
409
    return new HashMap<>();
410
  }
411
412
  private final class ResolverYAMLFactory extends YAMLFactory {
413
414
    private static final long serialVersionUID = 1L;
415
416
    @Override
417
    protected YAMLGenerator _createGenerator(
418
        final Writer out, final IOContext ctxt ) throws IOException {
419
420
      return new ResolverYAMLGenerator(
421
          ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
422
          out, _version );
423
    }
424
  }
425
426
  private class ResolverYAMLGenerator extends YAMLGenerator {
427
428
    public ResolverYAMLGenerator(
429
        final IOContext ctxt,
430
        final int jsonFeatures,
431
        final int yamlFeatures,
432
        final ObjectCodec codec,
433
        final Writer out,
434
        final DumperOptions.Version version ) throws IOException {
435
436
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
437
    }
438
439
    @Override
440
    public void writeString( final String text ) throws IOException {
441
      super.writeString( substitute( text ) );
442
    }
443
  }
444
445
  private YAMLFactory getYAMLFactory() {
446
    return new ResolverYAMLFactory();
447
  }
448
449
  private ObjectMapper getObjectMapper() {
450
    return new ObjectMapper( getYAMLFactory() );
451
  }
452
453
  /**
454
   * Returns the character used to separate YAML paths within delimited
455
   * references. This will return only the first character of the command line
456
   * parameter, if the default is overridden.
457
   *
458
   * @return A period by default.
459
   */
460
  private char getDelimitedSeparator() {
461
    return SEPARATOR.charAt( 0 );
462
  }
463
464
  private void setError( final String error ) {
465
    mError = error;
466
  }
467
468
  /**
469
   * Returns the last error message, if any, that occurred during parsing.
470
   *
471
   * @return The error message or the empty string if no error occurred.
472
   */
473
  public String getError() {
474
    return mError == null ? "" : mError;
484475
  }
485476
}
M src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
3030
import com.fasterxml.jackson.databind.JsonNode;
3131
import com.scrivenvar.definition.VariableTreeItem;
32
import java.util.Map.Entry;
3332
import javafx.scene.control.TreeItem;
3433
import javafx.scene.control.TreeView;
34
35
import java.util.Map.Entry;
3536
3637
/**
...
5354
   *
5455
   * @param name Root TreeItem node name.
55
   *
5656
   * @return A TreeView populated with all the keys in the YAML document.
5757
   */
58
  public TreeView<String> adapt( final String name ){
58
  public TreeView<String> adapt( final String name ) {
5959
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
6060
    final TreeItem<String> rootItem = createTreeItem( name );
...
7373
   */
7474
  private void adapt(
75
    final JsonNode rootNode, final TreeItem<String> rootItem ) {
75
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
7676
7777
    rootNode.fields().forEachRemaining(
78
      (Entry<String, JsonNode> leaf) -> adapt( leaf, rootItem )
78
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
7979
    );
8080
  }
...
8787
   */
8888
  private void adapt(
89
    final Entry<String, JsonNode> rootNode, final TreeItem<String> rootItem ) {
89
      final Entry<String, JsonNode> rootNode,
90
      final TreeItem<String> rootItem ) {
9091
9192
    final JsonNode leafNode = rootNode.getValue();
...
108109
   *
109110
   * @param value The node's value.
110
   *
111111
   * @return A new tree item node, never null.
112112
   */
M src/main/java/com/scrivenvar/dialogs/LinkDialog.java
2828
package com.scrivenvar.dialogs;
2929
30
import static com.scrivenvar.Messages.get;
3130
import com.scrivenvar.controls.EscapeTextField;
3231
import com.scrivenvar.editors.markdown.HyperlinkModel;
33
import java.nio.file.Path;
3432
import javafx.application.Platform;
3533
import javafx.beans.binding.Bindings;
3634
import javafx.beans.property.SimpleStringProperty;
3735
import javafx.beans.property.StringProperty;
3836
import javafx.scene.control.ButtonBar.ButtonData;
39
import static javafx.scene.control.ButtonType.OK;
4037
import javafx.scene.control.DialogPane;
4138
import javafx.scene.control.Label;
4239
import javafx.stage.Window;
4340
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
4444
4545
/**
...
5353
5454
  public LinkDialog(
55
    final Window owner, final HyperlinkModel hyperlink, final Path basePath ) {
55
    final Window owner, final HyperlinkModel hyperlink ) {
5656
    super( owner, "Dialog.link.title" );
5757
M src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
2828
package com.scrivenvar.dialogs;
2929
30
import static com.scrivenvar.Messages.get;
3130
import javafx.application.Platform;
3231
import javafx.geometry.Insets;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.DialogPane;
3532
import javafx.scene.control.Label;
3633
import javafx.scene.control.TextArea;
3734
import javafx.scene.layout.GridPane;
3835
import javafx.stage.Window;
36
37
import static com.scrivenvar.Messages.get;
38
import static javafx.scene.control.ButtonType.OK;
3939
4040
/**
4141
 * Responsible for managing the R startup script that is run when an R source
4242
 * file is loaded.
4343
 *
4444
 * @author White Magic Software, Ltd.
4545
 */
4646
public class RScriptDialog extends AbstractDialog<String> {
4747
48
  private TextArea scriptArea;
49
  private String originalText = "";
48
  private TextArea mScriptArea;
49
  private final String mOriginalText;
5050
5151
  public RScriptDialog(
52
    final Window parent, final String title, final String script ) {
52
      final Window parent, final String title, final String script ) {
5353
    super( parent, title );
54
    setOriginalText( script );
54
    mOriginalText = script;
5555
    getScriptArea().setText( script );
5656
  }
5757
5858
  @Override
5959
  protected void initComponents() {
60
    final DialogPane pane = getDialogPane();
61
6260
    final GridPane grid = new GridPane();
6361
    grid.setHgap( 10 );
6462
    grid.setVgap( 10 );
6563
    grid.setPadding( new Insets( 10, 10, 10, 10 ) );
6664
67
    final Label label = new Label( get( "Dialog.rScript.content" ) );
65
    final Label label = new Label( get( "Dialog.r.script.content" ) );
6866
6967
    final TextArea textArea = getScriptArea();
7068
    textArea.setEditable( true );
7169
    textArea.setWrapText( true );
7270
7371
    grid.add( label, 0, 0 );
7472
    grid.add( textArea, 0, 1 );
75
    pane.setContent( grid );
7673
77
    Platform.runLater( () -> textArea.requestFocus() );
74
    getDialogPane().setContent( grid );
7875
79
    setResultConverter( dialogButton -> {
80
      return dialogButton == OK ? textArea.getText() : getOriginalText();
81
    } );
76
    Platform.runLater( textArea::requestFocus );
77
78
    setResultConverter(
79
        dialogButton -> dialogButton == OK ?
80
            textArea.getText() :
81
            getOriginalText()
82
    );
8283
  }
8384
8485
  private TextArea getScriptArea() {
85
    if( this.scriptArea == null ) {
86
      this.scriptArea = new TextArea();
86
    if( mScriptArea == null ) {
87
      mScriptArea = new TextArea();
8788
    }
8889
89
    return this.scriptArea;
90
    return mScriptArea;
9091
  }
9192
9293
  private String getOriginalText() {
93
    return this.originalText;
94
  }
95
96
  private void setOriginalText( final String originalText ) {
97
    this.originalText = originalText;
94
    return mOriginalText;
9895
  }
9996
}
M src/main/java/com/scrivenvar/editors/EditorPane.java
2929
3030
import com.scrivenvar.AbstractPane;
31
3132
import java.nio.file.Path;
3233
import java.util.function.Consumer;
34
3335
import javafx.application.Platform;
3436
import javafx.beans.property.ObjectProperty;
...
4345
import org.fxmisc.wellbehaved.event.EventPattern;
4446
import org.fxmisc.wellbehaved.event.InputMap;
47
4548
import static org.fxmisc.wellbehaved.event.InputMap.consume;
49
4650
import org.fxmisc.wellbehaved.event.Nodes;
4751
4852
/**
4953
 * Represents common editing features for various types of text editors.
5054
 *
5155
 * @author White Magic Software, Ltd.
5256
 */
5357
public class EditorPane extends AbstractPane {
54
  
58
5559
  private StyleClassedTextArea editor;
5660
  private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
5761
  private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
5862
5963
  /**
6064
   * Set when entering variable edit mode; retrieved upon exiting.
6165
   */
6266
  private InputMap<InputEvent> nodeMap;
63
  
67
6468
  @Override
6569
  public void requestFocus() {
6670
    Platform.runLater( () -> getEditor().requestFocus() );
6771
  }
68
  
72
6973
  public void undo() {
7074
    getUndoManager().undo();
7175
  }
72
  
76
7377
  public void redo() {
7478
    getUndoManager().redo();
75
  }
76
77
  /**
78
   * TOD: Implement this.
79
   */
80
  public void replace() {
8179
  }
8280
83
  /**
84
   * TOD: Implement this.
85
   */
86
  public void findPrevious() {
87
  }
88
  
89
  public UndoManager getUndoManager() {
81
  public UndoManager<?> getUndoManager() {
9082
    return getEditor().getUndoManager();
9183
  }
92
  
84
9385
  public String getText() {
9486
    return getEditor().getText();
9587
  }
96
  
88
9789
  public void setText( final String text ) {
9890
    getEditor().deselect();
...
10799
   */
108100
  public void addTextChangeListener(
109
    final ChangeListener<? super String> listener ) {
101
      final ChangeListener<? super String> listener ) {
110102
    getEditor().textProperty().addListener( listener );
111103
  }
112104
113105
  /**
114106
   * Call to listen for when the caret moves to another paragraph.
115107
   *
116108
   * @param listener Receives paragraph change events.
117109
   */
118110
  public void addCaretParagraphListener(
119
    final ChangeListener<? super Integer> listener ) {
111
      final ChangeListener<? super Integer> listener ) {
120112
    getEditor().currentParagraphProperty().addListener( listener );
121113
  }
122114
123115
  /**
124116
   * This method adds listeners to editor events.
125117
   *
126
   * @param <T> The event type.
127
   * @param <U> The consumer type for the given event type.
128
   * @param event The event of interest.
118
   * @param <T>      The event type.
119
   * @param <U>      The consumer type for the given event type.
120
   * @param event    The event of interest.
129121
   * @param consumer The method to call when the event happens.
130122
   */
131123
  public <T extends Event, U extends T> void addKeyboardListener(
132
    final EventPattern<? super T, ? extends U> event,
133
    final Consumer<? super U> consumer ) {
124
      final EventPattern<? super T, ? extends U> event,
125
      final Consumer<? super U> consumer ) {
134126
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
135127
  }
...
142134
   * @param map The map of methods to events.
143135
   */
144
  @SuppressWarnings( "unchecked" )
136
  @SuppressWarnings("unchecked")
145137
  public void addEventListener( final InputMap<InputEvent> map ) {
146
    this.nodeMap = (InputMap<InputEvent>)getInputMap();
138
    this.nodeMap = (InputMap<InputEvent>) getInputMap();
147139
    Nodes.addInputMap( getEditor(), map );
148140
  }
...
176168
    return "org.fxmisc.wellbehaved.event.inputmap";
177169
  }
178
  
170
179171
  /**
180172
   * Repositions the cursor and scroll bar to the top of the file.
181173
   */
182174
  public void scrollToTop() {
183175
    getEditor().moveTo( 0 );
184176
    getScrollPane().scrollYToPixel( 0 );
185177
  }
186
  
178
187179
  private void setEditor( final StyleClassedTextArea textArea ) {
188180
    this.editor = textArea;
189181
  }
190
  
182
191183
  public synchronized StyleClassedTextArea getEditor() {
192184
    if( this.editor == null ) {
193185
      setEditor( createTextArea() );
194186
    }
195
    
187
196188
    return this.editor;
197189
  }
...
206198
      this.scrollPane = createScrollPane();
207199
    }
208
    
200
209201
    return this.scrollPane;
210202
  }
211
  
203
212204
  protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() {
213205
    final VirtualizedScrollPane<StyleClassedTextArea> pane
214
      = new VirtualizedScrollPane<>( getEditor() );
206
        = new VirtualizedScrollPane<>( getEditor() );
215207
    pane.setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
216
    
208
217209
    return pane;
218210
  }
219
  
211
220212
  protected StyleClassedTextArea createTextArea() {
221213
    return new StyleClassedTextArea( false );
222214
  }
223
  
215
224216
  public Path getPath() {
225217
    return this.path.get();
226218
  }
227
  
219
228220
  public void setPath( final Path path ) {
229221
    this.path.set( path );
230
  }
231
  
232
  public ObjectProperty<Path> pathProperty() {
233
    return this.path;
234222
  }
235223
}
M src/main/java/com/scrivenvar/editors/VariableNameInjector.java
3333
import com.scrivenvar.definition.DefinitionPane;
3434
import com.scrivenvar.definition.VariableTreeItem;
35
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
36
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
37
import com.scrivenvar.service.Settings;
38
import static com.scrivenvar.util.Lists.getFirst;
39
import static com.scrivenvar.util.Lists.getLast;
40
import static java.lang.Character.isWhitespace;
41
import static java.lang.Math.min;
42
import java.nio.file.Path;
43
import java.util.function.Consumer;
44
import javafx.collections.ObservableList;
45
import javafx.event.Event;
46
import javafx.event.EventHandler;
47
import javafx.scene.control.IndexRange;
48
import javafx.scene.control.TreeItem;
49
import javafx.scene.control.TreeView;
50
import javafx.scene.input.InputEvent;
51
import javafx.scene.input.KeyCode;
52
import static javafx.scene.input.KeyCode.AT;
53
import static javafx.scene.input.KeyCode.DIGIT2;
54
import static javafx.scene.input.KeyCode.ENTER;
55
import static javafx.scene.input.KeyCode.MINUS;
56
import static javafx.scene.input.KeyCode.SPACE;
57
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
58
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
59
import javafx.scene.input.KeyEvent;
60
import javafx.scene.input.MouseEvent;
61
import org.fxmisc.richtext.StyledTextArea;
62
import org.fxmisc.wellbehaved.event.EventPattern;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
64
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
65
import org.fxmisc.wellbehaved.event.InputMap;
66
import static org.fxmisc.wellbehaved.event.InputMap.consume;
67
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
68
69
/**
70
 * Provides the logic for injecting variable names within the editor.
71
 *
72
 * @author White Magic Software, Ltd.
73
 */
74
public final class VariableNameInjector {
75
76
  public static final int DEFAULT_MAX_VAR_LENGTH = 64;
77
78
  private static final int NO_DIFFERENCE = -1;
79
80
  /**
81
   * TODO: Move this into settings.
82
   */
83
  private static final String PUNCTUATION = "\"#$%&'()*+,-/:;<=>?@[]^_`{|}~";
84
85
  private final Settings settings = Services.load( Settings.class );
86
87
  /**
88
   * Used to capture keyboard events once the user presses @.
89
   */
90
  private InputMap<InputEvent> keyboardMap;
91
92
  /**
93
   * Position of the variable in the text when in variable mode (0 by default).
94
   */
95
  private int initialCaretPosition;
96
97
  /**
98
   * Recipient of name injections.
99
   */
100
  private FileEditorTab tab;
101
102
  /**
103
   * Initiates double-click events.
104
   */
105
  private DefinitionPane definitionPane;
106
107
  private EventHandler<MouseEvent> panelEventHandler;
108
109
  /**
110
   * Initializes the variable name injector against the given pane.
111
   *
112
   * @param tab The tab to inject variable names into.
113
   * @param pane The definition panel to listen to for double-click events.
114
   */
115
  public VariableNameInjector(
116
    final FileEditorTab tab, final DefinitionPane pane ) {
117
    setFileEditorTab( tab );
118
    setDefinitionPane( pane );
119
    initBranchSelectedListener();
120
    initKeyboardEventListeners();
121
  }
122
123
  /**
124
   * Traps double-click events on the definition pane.
125
   */
126
  private void initBranchSelectedListener() {
127
    final EventHandler<MouseEvent> eventHandler = getPanelEventHandler();
128
    getDefinitionPane().addBranchSelectedListener( eventHandler );
129
  }
130
131
  /**
132
   * Trap control+space and the @ key.
133
   *
134
   * @param tab The file editor that sends keyboard events for variable name
135
   * injection.
136
   */
137
  public void initKeyboardEventListeners( final FileEditorTab tab ) {
138
    setFileEditorTab( tab );
139
    initKeyboardEventListeners();
140
  }
141
142
  /**
143
   * Traps keys for performing various short-cut tasks, such as @-mode variable
144
   * insertion and control+space for variable autocomplete.
145
   *
146
   * @ key is pressed, a new keyboard map is inserted in place of the current
147
   * map -- this class goes into "variable edit mode" (a.k.a. vMode).
148
   *
149
   * @see createKeyboardMap()
150
   */
151
  private void initKeyboardEventListeners() {
152
    // Control and space are pressed.
153
    addKeyboardListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
154
155
    // @ key in Linux?
156
    addKeyboardListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
157
    // @ key in Windows.
158
    addKeyboardListener( keyPressed( AT ), this::vMode );
159
  }
160
161
  /**
162
   * The @ symbol is a short-cut to inserting a YAML variable reference.
163
   *
164
   * @param e Superfluous information about the key that was pressed.
165
   */
166
  private void vMode( KeyEvent e ) {
167
    setInitialCaretPosition();
168
    vModeStart();
169
    vModeAutocomplete();
170
  }
171
172
  /**
173
   * Receives key presses until the user completes the variable selection. This
174
   * allows the arrow keys to be used for selecting variables.
175
   *
176
   * @param e The key that was pressed.
177
   */
178
  private void vModeKeyPressed( KeyEvent e ) {
179
    final KeyCode keyCode = e.getCode();
180
181
    switch( keyCode ) {
182
      case BACK_SPACE:
183
        // Don't decorate the variable upon exiting vMode.
184
        vModeBackspace();
185
        break;
186
187
      case ESCAPE:
188
        // Don't decorate the variable upon exiting vMode.
189
        vModeStop();
190
        break;
191
192
      case ENTER:
193
      case PERIOD:
194
      case RIGHT:
195
      case END:
196
        // Stop at a leaf node, ENTER means accept.
197
        if( vModeConditionalComplete() && keyCode == ENTER ) {
198
          vModeStop();
199
200
          // Decorate the variable upon exiting vMode.
201
          decorate();
202
        }
203
        break;
204
205
      case UP:
206
        cyclePathPrev();
207
        break;
208
209
      case DOWN:
210
        cyclePathNext();
211
        break;
212
213
      default:
214
        vModeFilterKeyPressed( e );
215
        break;
216
    }
217
218
    e.consume();
219
  }
220
221
  private void vModeBackspace() {
222
    deleteSelection();
223
224
    // Break out of variable mode by back spacing to the original position.
225
    if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
226
      vModeAutocomplete();
227
    }
228
    else {
229
      vModeStop();
230
    }
231
  }
232
233
  /**
234
   * Updates the text with the path selected (or typed) by the user.
235
   */
236
  private void vModeAutocomplete() {
237
    final TreeItem<String> node = getCurrentNode();
238
239
    if( node != null && !node.isLeaf() ) {
240
      final String word = getLastPathWord();
241
      final String label = node.getValue();
242
      final int delta = difference( label, word );
243
      final String remainder = delta == NO_DIFFERENCE
244
        ? label
245
        : label.substring( delta );
246
247
      final StyledTextArea textArea = getEditor();
248
      final int posBegan = getCurrentCaretPosition();
249
      final int posEnded = posBegan + remainder.length();
250
251
      textArea.replaceSelection( remainder );
252
253
      if( posEnded - posBegan > 0 ) {
254
        textArea.selectRange( posEnded, posBegan );
255
      }
256
257
      expand( node );
258
    }
259
  }
260
261
  /**
262
   * Only variable name keys can pass through the filter. This is called when
263
   * the user presses a key.
264
   *
265
   * @param e The key that was pressed.
266
   */
267
  private void vModeFilterKeyPressed( final KeyEvent e ) {
268
    if( isVariableNameKey( e ) ) {
269
      typed( e.getText() );
270
    }
271
  }
272
273
  /**
274
   * Performs an autocomplete depending on whether the user has finished typing
275
   * in a word. If there is a selected range, then this will complete the most
276
   * recent word and jump to the next child.
277
   *
278
   * @return true The auto-completed node was a terminal node.
279
   */
280
  private boolean vModeConditionalComplete() {
281
    acceptPath();
282
283
    final TreeItem<String> node = getCurrentNode();
284
    final boolean terminal = isTerminal( node );
285
286
    if( !terminal ) {
287
      typed( SEPARATOR );
288
    }
289
290
    return terminal;
291
  }
292
293
  /**
294
   * Pressing control+space will find a node that matches the current word and
295
   * substitute the YAML variable reference. This is called when the user is not
296
   * editing in vMode.
297
   *
298
   * @param e Ignored -- it can only be Ctrl+Space.
299
   */
300
  private void autocomplete( final KeyEvent e ) {
301
    final String paragraph = getCaretParagraph();
302
    final int[] boundaries = getWordBoundaries( paragraph );
303
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
304
305
    VariableTreeItem<String> leaf = findLeaf( word );
306
307
    if( leaf == null ) {
308
      // If a leaf doesn't match using "starts with", then try using "contains".
309
      leaf = findLeaf( word, true );
310
    }
311
312
    if( leaf != null ) {
313
      replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
314
      decorate();
315
      expand( leaf );
316
    }
317
  }
318
319
  /**
320
   * Called when autocomplete finishes on a valid leaf or when the user presses
321
   * Enter to finish manual autocomplete.
322
   */
323
  private void decorate() {
324
    // A little bit of duplication...
325
    final String paragraph = getCaretParagraph();
326
    final int[] boundaries = getWordBoundaries( paragraph );
327
    final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
328
329
    final String newVariable = decorate( old );
330
331
    final int posEnded = getCurrentCaretPosition();
332
    final int posBegan = posEnded - old.length();
333
334
    getEditor().replaceText( posBegan, posEnded, newVariable );
335
  }
336
337
  /**
338
   * Called when user double-clicks on a tree view item.
339
   *
340
   * @param variable The variable to decorate.
341
   */
342
  private String decorate( final String variable ) {
343
    return getVariableDecorator().decorate( variable );
344
  }
345
346
  /**
347
   * Inserts the given string at the current caret position, or replaces
348
   * selected text (if any).
349
   *
350
   * @param s The string to inject.
351
   */
352
  private void replaceSelection( final String s ) {
353
    getEditor().replaceSelection( s );
354
  }
355
356
  /**
357
   * Updates the text at the given position within the current paragraph.
358
   *
359
   * @param posBegan The starting index in the paragraph text to replace.
360
   * @param posEnded The ending index in the paragraph text to replace.
361
   * @param text Overwrite the paragraph substring with this text.
362
   */
363
  private void replaceText(
364
    final int posBegan, final int posEnded, final String text ) {
365
    final int p = getCurrentParagraph();
366
367
    getEditor().replaceText( p, posBegan, p, posEnded, text );
368
  }
369
370
  /**
371
   * Returns the caret's current paragraph position.
372
   *
373
   * @return A number greater than or equal to 0.
374
   */
375
  private int getCurrentParagraph() {
376
    return getEditor().getCurrentParagraph();
377
  }
378
379
  /**
380
   * Returns current word boundary indexes into the current paragraph, excluding
381
   * punctuation.
382
   *
383
   * @param p The paragraph wherein to hunt word boundaries.
384
   * @param offset The offset into the paragraph to begin scanning left and
385
   * right.
386
   *
387
   * @return The starting and ending index of the word closest to the caret.
388
   */
389
  private int[] getWordBoundaries( final String p, final int offset ) {
390
    // Remove dashes, but retain hyphens. Retain same number of characters
391
    // to preserve relative indexes.
392
    final String paragraph = p.replace( "---", "   " ).replace( "--", "  " );
393
394
    return getWordAt( paragraph, offset );
395
  }
396
397
  /**
398
   * Helper method to get the word boundaries for the current paragraph.
399
   *
400
   * @param paragraph
401
   *
402
   * @return
403
   */
404
  private int[] getWordBoundaries( final String paragraph ) {
405
    return getWordBoundaries( paragraph, getCurrentCaretColumn() );
406
  }
407
408
  /**
409
   * Given an arbitrary offset into a string, this returns the word at that
410
   * index. The inputs and outputs include:
411
   *
412
   * <ul>
413
   * <li>surrounded by space: <code>hello | world!</code> ("");</li>
414
   * <li>end of word: <code>hello| world!</code> ("hello");</li>
415
   * <li>start of a word: <code>hello |world!</code> ("world");</li>
416
   * <li>within a word: <code>hello wo|rld!</code> ("world");</li>
417
   * <li>end of a paragraph: <code>hello world!|</code> ("world");</li>
418
   * <li>start of a paragraph: <code>|hello world!</code> ("hello"); or</li>
419
   * <li>after punctuation: <code>hello world!|</code> ("world").</li>
420
   * </ul>
421
   *
422
   * @param p The string to scan for a word.
423
   * @param offset The offset within s to begin searching for the nearest word
424
   * boundary, must not be out of bounds of s.
425
   *
426
   * @return The word in s at the offset.
427
   *
428
   * @see getWordBegan( String, int )
429
   * @see getWordEnded( String, int )
430
   */
431
  private int[] getWordAt( final String p, final int offset ) {
432
    return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) };
433
  }
434
435
  /**
436
   * Returns the index into s where a word begins.
437
   *
438
   * @param s Never null.
439
   * @param offset Index into s to begin searching backwards for a word
440
   * boundary.
441
   *
442
   * @return The index where a word begins.
443
   */
444
  private int getWordBegan( final String s, int offset ) {
445
    while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
446
      offset--;
447
    }
448
449
    return offset;
450
  }
451
452
  /**
453
   * Returns the index into s where a word ends.
454
   *
455
   * @param s Never null.
456
   * @param offset Index into s to begin searching forwards for a word boundary.
457
   *
458
   * @return The index where a word ends.
459
   */
460
  private int getWordEnded( final String s, int offset ) {
461
    final int length = s.length();
462
463
    while( offset < length && isBoundary( s.charAt( offset ) ) ) {
464
      offset++;
465
    }
466
467
    return offset;
468
  }
469
470
  /**
471
   * Returns true if the given character can be reasonably expected to be part
472
   * of a word, including punctuation marks.
473
   *
474
   * @param c The character to compare.
475
   *
476
   * @return false The character is a space character.
477
   */
478
  private boolean isBoundary( final char c ) {
479
    return !isWhitespace( c ) && !isPunctuation( c );
480
  }
481
482
  /**
483
   * Returns true if the given character is part of the set of Latin (English)
484
   * punctuation marks.
485
   *
486
   * @param c
487
   *
488
   * @return
489
   */
490
  private static boolean isPunctuation( final char c ) {
491
    return PUNCTUATION.indexOf( c ) != -1;
492
  }
493
494
  /**
495
   * Returns the text for the paragraph that contains the caret.
496
   *
497
   * @return A non-null string, possibly empty.
498
   */
499
  private String getCaretParagraph() {
500
    return getEditor().getText( getCurrentParagraph() );
501
  }
502
503
  /**
504
   * Returns true if the node has children that can be selected (i.e., any
505
   * non-leaves).
506
   *
507
   * @param <T> The type that the TreeItem contains.
508
   * @param node The node to test for terminality.
509
   *
510
   * @return true The node has one branch and its a leaf.
511
   */
512
  private <T> boolean isTerminal( final TreeItem<T> node ) {
513
    final ObservableList<TreeItem<T>> branches = node.getChildren();
514
515
    return branches.size() == 1 && branches.get( 0 ).isLeaf();
516
  }
517
518
  /**
519
   * Inserts text that the user typed at the current caret position, then
520
   * performs an autocomplete for the variable name.
521
   *
522
   * @param text The text to insert, never null.
523
   */
524
  private void typed( final String text ) {
525
    getEditor().replaceSelection( text );
526
    vModeAutocomplete();
527
  }
528
529
  /**
530
   * Called when the user presses either End or Enter key.
531
   */
532
  private void acceptPath() {
533
    final IndexRange range = getSelectionRange();
534
535
    if( range != null ) {
536
      final int rangeEnd = range.getEnd();
537
      final StyledTextArea textArea = getEditor();
538
      textArea.deselect();
539
      textArea.moveTo( rangeEnd );
540
    }
541
  }
542
543
  /**
544
   * Replaces the entirety of the existing path (from the initial caret
545
   * position) with the given path.
546
   *
547
   * @param oldPath The path to replace.
548
   * @param newPath The replacement path.
549
   */
550
  private void replacePath( final String oldPath, final String newPath ) {
551
    final StyledTextArea textArea = getEditor();
552
    final int posBegan = getInitialCaretPosition();
553
    final int posEnded = posBegan + oldPath.length();
554
555
    textArea.deselect();
556
    textArea.replaceText( posBegan, posEnded, newPath );
557
  }
558
559
  /**
560
   * Called when the user presses the Backspace key.
561
   */
562
  private void deleteSelection() {
563
    final StyledTextArea textArea = getEditor();
564
    textArea.replaceSelection( "" );
565
    textArea.deletePreviousChar();
566
  }
567
568
  /**
569
   * Cycles the selected text through the nodes.
570
   *
571
   * @param direction true - next; false - previous
572
   */
573
  private void cycleSelection( final boolean direction ) {
574
    final TreeItem<String> node = getCurrentNode();
575
576
    // Find the sibling for the current selection and replace the current
577
    // selection with the sibling's value
578
    TreeItem< String> cycled = direction
579
      ? node.nextSibling()
580
      : node.previousSibling();
581
582
    // When cycling at the end (or beginning) of the list, jump to the first
583
    // (or last) sibling depending on the cycle direction.
584
    if( cycled == null ) {
585
      cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
586
    }
587
588
    final String path = getCurrentPath();
589
    final String cycledWord = cycled.getValue();
590
    final String word = getLastPathWord();
591
    final int index = path.indexOf( word );
592
    final String cycledPath = path.substring( 0, index ) + cycledWord;
593
594
    expand( cycled );
595
    replacePath( path, cycledPath );
596
  }
597
598
  /**
599
   * Cycles to the next sibling of the currently selected tree node.
600
   */
601
  private void cyclePathNext() {
602
    cycleSelection( true );
603
  }
604
605
  /**
606
   * Cycles to the previous sibling of the currently selected tree node.
607
   */
608
  private void cyclePathPrev() {
609
    cycleSelection( false );
610
  }
611
612
  /**
613
   * Returns the variable name (or as much as has been typed so far). Returns
614
   * all the characters from the initial caret column to the the first
615
   * whitespace character. This will return a path that contains zero or more
616
   * separators.
617
   *
618
   * @return A non-null string, possibly empty.
619
   */
620
  private String getCurrentPath() {
621
    final String s = extractTextChunk();
622
    final int length = s.length();
623
624
    int i = 0;
625
626
    while( i < length && !isWhitespace( s.charAt( i ) ) ) {
627
      i++;
628
    }
629
630
    return s.substring( 0, i );
631
  }
632
633
  private <T> ObservableList<TreeItem<T>> getSiblings(
634
    final TreeItem<T> item ) {
635
    final TreeItem<T> parent = item.getParent();
636
    return parent == null ? item.getChildren() : parent.getChildren();
637
  }
638
639
  private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
640
    return getFirst( getSiblings( item ), item );
641
  }
642
643
  private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
644
    return getLast( getSiblings( item ), item );
645
  }
646
647
  /**
648
   * Returns the caret position as an offset into the text.
649
   *
650
   * @return A value from 0 to the length of the text (minus one).
651
   */
652
  private int getCurrentCaretPosition() {
653
    return getEditor().getCaretPosition();
654
  }
655
656
  /**
657
   * Returns the caret position within the current paragraph.
658
   *
659
   * @return A value from 0 to the length of the current paragraph.
660
   */
661
  private int getCurrentCaretColumn() {
662
    return getEditor().getCaretColumn();
663
  }
664
665
  /**
666
   * Returns the last word from the path.
667
   *
668
   * @return The last token.
669
   */
670
  private String getLastPathWord() {
671
    String path = getCurrentPath();
672
673
    int i = path.indexOf( SEPARATOR_CHAR );
674
675
    while( i > 0 ) {
676
      path = path.substring( i + 1 );
677
      i = path.indexOf( SEPARATOR_CHAR );
678
    }
679
680
    return path;
681
  }
682
683
  /**
684
   * Returns text from the initial caret position until some arbitrarily long
685
   * number of characters. The number of characters extracted will be
686
   * getMaxVarLength, or fewer, depending on how many characters remain to be
687
   * extracted. The result from this method is trimmed to the first whitespace
688
   * character.
689
   *
690
   * @return A chunk of text that includes all the words representing a path,
691
   * and then some.
692
   */
693
  private String extractTextChunk() {
694
    final StyledTextArea textArea = getEditor();
695
    final int textBegan = getInitialCaretPosition();
696
    final int remaining = textArea.getLength() - textBegan;
697
    final int textEnded = min( remaining, getMaxVarLength() );
698
699
    try {
700
      return textArea.getText( textBegan, textEnded );
701
    }
702
    catch( final Exception e ) {
703
      return textArea.getText();
704
    }
705
  }
706
707
  /**
708
   * Returns the node for the current path.
709
   */
710
  private TreeItem<String> getCurrentNode() {
711
    return findNode( getCurrentPath() );
712
  }
713
714
  /**
715
   * Finds the node that most closely matches the given path.
716
   *
717
   * @param path The path that represents a node.
718
   *
719
   * @return The node for the path, or the root node if the path could not be
720
   * found, but never null.
721
   */
722
  private TreeItem<String> findNode( final String path ) {
723
    return getDefinitionPane().findNode( path );
724
  }
725
726
  /**
727
   * Finds the first leaf having a value that starts with the given text.
728
   *
729
   * @param text The text to find in the definition tree.
730
   *
731
   * @return The leaf that starts with the given text, or null if not found.
732
   */
733
  private VariableTreeItem<String> findLeaf( final String text ) {
734
    return getDefinitionPane().findLeaf( text, false );
735
  }
736
737
  /**
738
   * Finds the first leaf having a value that starts with the given text, or
739
   * contains the text if contains is true.
740
   *
741
   * @param text The text to find in the definition tree.
742
   * @param contains Set true to perform a substring match after a starts with
743
   * match.
744
   *
745
   * @return The leaf that starts with the given text, or null if not found.
746
   */
747
  private VariableTreeItem<String> findLeaf(
748
    final String text,
749
    final boolean contains ) {
750
    return getDefinitionPane().findLeaf( text, contains );
751
  }
752
753
  /**
754
   * Used to ignore typed keys in favour of trapping pressed keys.
755
   *
756
   * @param e The key that was typed.
757
   */
758
  private void vModeKeyTyped( KeyEvent e ) {
759
    e.consume();
760
  }
761
762
  /**
763
   * Used to lazily initialize the keyboard map.
764
   *
765
   * @return Mappings for keyTyped and keyPressed.
766
   */
767
  protected InputMap<InputEvent> createKeyboardMap() {
768
    return sequence(
769
      consume( keyTyped(), this::vModeKeyTyped ),
770
      consume( keyPressed(), this::vModeKeyPressed )
771
    );
772
  }
773
774
  private InputMap<InputEvent> getKeyboardMap() {
775
    if( this.keyboardMap == null ) {
776
      this.keyboardMap = createKeyboardMap();
777
    }
778
779
    return this.keyboardMap;
780
  }
781
782
  /**
783
   * Collapses the tree then expands and selects the given node.
784
   *
785
   * @param node The node to expand.
786
   */
787
  private void expand( final TreeItem<String> node ) {
788
    final DefinitionPane pane = getDefinitionPane();
789
    pane.collapse();
790
    pane.expand( node );
791
    pane.select( node );
792
  }
793
794
  /**
795
   * Returns true iff the key code the user typed can be used as part of a YAML
796
   * variable name.
797
   *
798
   * @param keyEvent Keyboard key press event information.
799
   *
800
   * @return true The key is a value that can be inserted into the text.
801
   */
802
  private boolean isVariableNameKey( final KeyEvent keyEvent ) {
803
    final KeyCode kc = keyEvent.getCode();
804
805
    return (kc.isLetterKey()
806
      || kc.isDigitKey()
807
      || (keyEvent.isShiftDown() && kc == MINUS))
808
      && !keyEvent.isControlDown();
809
  }
810
811
  /**
812
   * Starts to capture user input events.
813
   */
814
  private void vModeStart() {
815
    addEventListener( getKeyboardMap() );
816
  }
817
818
  /**
819
   * Restores capturing of user input events to the previous event listener.
820
   * Also asks the processing chain to modify the variable text into a
821
   * machine-readable variable based on the format required by the file type.
822
   * For example, a Markdown file (.md) will substitute a $VAR$ name while an R
823
   * file (.Rmd, .Rxml) will use `r#xVAR`.
824
   */
825
  private void vModeStop() {
826
    removeEventListener( getKeyboardMap() );
827
  }
828
829
  /**
830
   * Returns a variable decorator that corresponds to the given file type.
831
   *
832
   * @return
833
   */
834
  private VariableDecorator getVariableDecorator() {
835
    return VariableNameDecoratorFactory.newInstance( getFilename() );
836
  }
837
838
  private Path getFilename() {
839
    return getFileEditorTab().getPath();
840
  }
841
842
  /**
843
   * Returns the index where the two strings diverge.
844
   *
845
   * @param s1 The string that could be a substring of s2, null allowed.
846
   * @param s2 The string that could be a substring of s1, null allowed.
847
   *
848
   * @return NO_DIFFERENCE if the strings are the same, otherwise the index
849
   * where they differ.
850
   */
851
  @SuppressWarnings( "StringEquality" )
852
  private int difference( final CharSequence s1, final CharSequence s2 ) {
853
    if( s1 == s2 ) {
854
      return NO_DIFFERENCE;
855
    }
856
857
    if( s1 == null || s2 == null ) {
858
      return 0;
859
    }
860
861
    int i = 0;
862
    final int limit = min( s1.length(), s2.length() );
863
864
    while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
865
      i++;
866
    }
867
868
    // If one string was shorter than the other, that's where they differ.
869
    return i;
870
  }
871
872
  private EditorPane getEditorPane() {
873
    return getFileEditorTab().getEditorPane();
874
  }
875
876
  /**
877
   * Delegates to the file editor pane, and, ultimately, to its text area.
878
   */
879
  private <T extends Event, U extends T> void addKeyboardListener(
880
    final EventPattern<? super T, ? extends U> event,
881
    final Consumer<? super U> consumer ) {
882
    getEditorPane().addKeyboardListener( event, consumer );
883
  }
884
885
  /**
886
   * Delegates to the file editor pane, and, ultimately, to its text area.
887
   *
888
   * @param map The map of methods to events.
889
   */
890
  private void addEventListener( final InputMap<InputEvent> map ) {
891
    getEditorPane().addEventListener( map );
892
  }
893
894
  private void removeEventListener( final InputMap<InputEvent> map ) {
895
    getEditorPane().removeEventListener( map );
896
  }
897
898
  /**
899
   * Returns the position of the caret when variable mode editing was requested.
900
   *
901
   * @return The variable mode caret position.
902
   */
903
  private int getInitialCaretPosition() {
904
    return this.initialCaretPosition;
905
  }
906
907
  /**
908
   * Sets the position of the caret when variable mode editing was requested.
909
   * Stores the current position because only the text that comes afterwards is
910
   * a suitable variable reference.
911
   *
912
   * @return The variable mode caret position.
913
   */
914
  private void setInitialCaretPosition() {
915
    this.initialCaretPosition = getEditor().getCaretPosition();
916
  }
917
918
  private StyledTextArea getEditor() {
919
    return getEditorPane().getEditor();
920
  }
921
922
  public FileEditorTab getFileEditorTab() {
923
    return this.tab;
924
  }
925
926
  public void setFileEditorTab( final FileEditorTab editorTab ) {
927
    this.tab = editorTab;
928
  }
929
930
  private DefinitionPane getDefinitionPane() {
931
    return this.definitionPane;
932
  }
933
934
  private void setDefinitionPane( final DefinitionPane definitionPane ) {
935
    this.definitionPane = definitionPane;
936
  }
937
938
  private IndexRange getSelectionRange() {
939
    return getEditor().getSelection();
940
  }
941
942
  /**
943
   * Don't look ahead too far when trying to find the end of a node.
944
   *
945
   * @return 512 by default.
946
   */
947
  private int getMaxVarLength() {
948
    return getSettings().getSetting(
949
      "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
950
  }
951
952
  private Settings getSettings() {
953
    return this.settings;
954
  }
955
956
  private void setPanelEventHandler( final EventHandler<MouseEvent> eventHandler ) {
957
    this.panelEventHandler = eventHandler;
958
  }
959
960
  private synchronized EventHandler<MouseEvent> getPanelEventHandler() {
961
    if( this.panelEventHandler == null ) {
962
      this.panelEventHandler = createPanelEventHandler();
963
    }
964
965
    return this.panelEventHandler;
966
  }
967
968
  private EventHandler<MouseEvent> createPanelEventHandler() {
969
    return new PanelEventHandler();
970
  }
971
972
  /**
973
   * Responsible for handling double-click events on the definition pane.
974
   */
975
  private class PanelEventHandler implements EventHandler<MouseEvent> {
976
977
    public PanelEventHandler() {
978
    }
979
980
    @Override
981
    public void handle( final MouseEvent event ) {
982
      final Object source = event.getSource();
983
984
      if( source instanceof TreeView ) {
985
        final TreeView tree = (TreeView)source;
986
        final TreeItem item = (TreeItem)tree.getSelectionModel().getSelectedItem();
987
988
        if( item instanceof VariableTreeItem ) {
989
          final VariableTreeItem var = (VariableTreeItem)item;
35
import com.scrivenvar.service.Settings;
36
import javafx.collections.ObservableList;
37
import javafx.event.Event;
38
import javafx.event.EventHandler;
39
import javafx.scene.control.IndexRange;
40
import javafx.scene.control.TreeItem;
41
import javafx.scene.control.TreeView;
42
import javafx.scene.input.InputEvent;
43
import javafx.scene.input.KeyCode;
44
import javafx.scene.input.KeyEvent;
45
import javafx.scene.input.MouseEvent;
46
import org.fxmisc.richtext.StyledTextArea;
47
import org.fxmisc.wellbehaved.event.EventPattern;
48
import org.fxmisc.wellbehaved.event.InputMap;
49
50
import java.nio.file.Path;
51
import java.util.function.Consumer;
52
53
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
54
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
55
import static com.scrivenvar.util.Lists.getFirst;
56
import static com.scrivenvar.util.Lists.getLast;
57
import static java.lang.Character.isWhitespace;
58
import static java.lang.Math.min;
59
import static javafx.scene.input.KeyCode.*;
60
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
61
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
64
import static org.fxmisc.wellbehaved.event.InputMap.consume;
65
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
66
67
/**
68
 * Provides the logic for injecting variable names within the editor.
69
 *
70
 * @author White Magic Software, Ltd.
71
 */
72
public final class VariableNameInjector {
73
74
  public static final int DEFAULT_MAX_VAR_LENGTH = 64;
75
76
  private static final int NO_DIFFERENCE = -1;
77
78
  /**
79
   * TODO: Move this into settings.
80
   */
81
  private static final String PUNCTUATION = "\"#$%&'()*+,-/:;<=>?@[]^_`{|}~";
82
83
  private final Settings settings = Services.load( Settings.class );
84
85
  /**
86
   * Used to capture keyboard events once the user presses @.
87
   */
88
  private InputMap<InputEvent> keyboardMap;
89
90
  /**
91
   * Position of the variable in the text when in variable mode (0 by default).
92
   */
93
  private int initialCaretPosition;
94
95
  /**
96
   * Recipient of name injections.
97
   */
98
  private FileEditorTab tab;
99
100
  /**
101
   * Initiates double-click events.
102
   */
103
  private DefinitionPane definitionPane;
104
105
  private EventHandler<MouseEvent> panelEventHandler;
106
107
  /**
108
   * Initializes the variable name injector against the given pane.
109
   *
110
   * @param tab  The tab to inject variable names into.
111
   * @param pane The definition panel to listen to for double-click events.
112
   */
113
  public VariableNameInjector(
114
      final FileEditorTab tab, final DefinitionPane pane ) {
115
    setFileEditorTab( tab );
116
    setDefinitionPane( pane );
117
    initBranchSelectedListener();
118
    initKeyboardEventListeners();
119
  }
120
121
  /**
122
   * Traps double-click events on the definition pane.
123
   */
124
  private void initBranchSelectedListener() {
125
    final EventHandler<MouseEvent> eventHandler = getPanelEventHandler();
126
    getDefinitionPane().addBranchSelectedListener( eventHandler );
127
  }
128
129
  /**
130
   * Trap control+space and the @ key.
131
   *
132
   * @param tab The file editor that sends keyboard events for variable name
133
   *            injection.
134
   */
135
  public void initKeyboardEventListeners( final FileEditorTab tab ) {
136
    setFileEditorTab( tab );
137
    initKeyboardEventListeners();
138
  }
139
140
  /**
141
   * Traps keys for performing various short-cut tasks, such as @-mode variable
142
   * insertion and control+space for variable autocomplete.
143
   *
144
   * @ key is pressed, a new keyboard map is inserted in place of the current
145
   * map -- this class goes into "variable edit mode" (a.k.a. vMode).
146
   */
147
  private void initKeyboardEventListeners() {
148
    // Control and space are pressed.
149
    addKeyboardListener( keyPressed( SPACE, CONTROL_DOWN ),
150
                         this::autocomplete );
151
152
    // @ key in Linux?
153
    addKeyboardListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
154
    // @ key in Windows.
155
    addKeyboardListener( keyPressed( AT ), this::vMode );
156
  }
157
158
  /**
159
   * The @ symbol is a short-cut to inserting a YAML variable reference.
160
   *
161
   * @param e Superfluous information about the key that was pressed.
162
   */
163
  private void vMode( KeyEvent e ) {
164
    setInitialCaretPosition();
165
    vModeStart();
166
    vModeAutocomplete();
167
  }
168
169
  /**
170
   * Receives key presses until the user completes the variable selection. This
171
   * allows the arrow keys to be used for selecting variables.
172
   *
173
   * @param e The key that was pressed.
174
   */
175
  private void vModeKeyPressed( KeyEvent e ) {
176
    final KeyCode keyCode = e.getCode();
177
178
    switch( keyCode ) {
179
      case BACK_SPACE:
180
        // Don't decorate the variable upon exiting vMode.
181
        vModeBackspace();
182
        break;
183
184
      case ESCAPE:
185
        // Don't decorate the variable upon exiting vMode.
186
        vModeStop();
187
        break;
188
189
      case ENTER:
190
      case PERIOD:
191
      case RIGHT:
192
      case END:
193
        // Stop at a leaf node, ENTER means accept.
194
        if( vModeConditionalComplete() && keyCode == ENTER ) {
195
          vModeStop();
196
197
          // Decorate the variable upon exiting vMode.
198
          decorate();
199
        }
200
        break;
201
202
      case UP:
203
        cyclePathPrev();
204
        break;
205
206
      case DOWN:
207
        cyclePathNext();
208
        break;
209
210
      default:
211
        vModeFilterKeyPressed( e );
212
        break;
213
    }
214
215
    e.consume();
216
  }
217
218
  private void vModeBackspace() {
219
    deleteSelection();
220
221
    // Break out of variable mode by back spacing to the original position.
222
    if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
223
      vModeAutocomplete();
224
    }
225
    else {
226
      vModeStop();
227
    }
228
  }
229
230
  /**
231
   * Updates the text with the path selected (or typed) by the user.
232
   */
233
  private void vModeAutocomplete() {
234
    final TreeItem<String> node = getCurrentNode();
235
236
    if( node != null && !node.isLeaf() ) {
237
      final String word = getLastPathWord();
238
      final String label = node.getValue();
239
      final int delta = difference( label, word );
240
      final String remainder = delta == NO_DIFFERENCE
241
          ? label
242
          : label.substring( delta );
243
244
      final StyledTextArea<?, ?> textArea = getEditor();
245
      final int posBegan = getCurrentCaretPosition();
246
      final int posEnded = posBegan + remainder.length();
247
248
      textArea.replaceSelection( remainder );
249
250
      if( posEnded - posBegan > 0 ) {
251
        textArea.selectRange( posEnded, posBegan );
252
      }
253
254
      expand( node );
255
    }
256
  }
257
258
  /**
259
   * Only variable name keys can pass through the filter. This is called when
260
   * the user presses a key.
261
   *
262
   * @param e The key that was pressed.
263
   */
264
  private void vModeFilterKeyPressed( final KeyEvent e ) {
265
    if( isVariableNameKey( e ) ) {
266
      typed( e.getText() );
267
    }
268
  }
269
270
  /**
271
   * Performs an autocomplete depending on whether the user has finished typing
272
   * in a word. If there is a selected range, then this will complete the most
273
   * recent word and jump to the next child.
274
   *
275
   * @return true The auto-completed node was a terminal node.
276
   */
277
  private boolean vModeConditionalComplete() {
278
    acceptPath();
279
280
    final TreeItem<String> node = getCurrentNode();
281
    final boolean terminal = isTerminal( node );
282
283
    if( !terminal ) {
284
      typed( SEPARATOR );
285
    }
286
287
    return terminal;
288
  }
289
290
  /**
291
   * Pressing control+space will find a node that matches the current word and
292
   * substitute the YAML variable reference. This is called when the user is not
293
   * editing in vMode.
294
   *
295
   * @param e Ignored -- it can only be Ctrl+Space.
296
   */
297
  private void autocomplete( final KeyEvent e ) {
298
    final String paragraph = getCaretParagraph();
299
    final int[] boundaries = getWordBoundaries( paragraph );
300
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
301
302
    VariableTreeItem<String> leaf = findLeaf( word );
303
304
    if( leaf == null ) {
305
      // If a leaf doesn't match using "starts with", then try using "contains".
306
      leaf = findLeaf( word, true );
307
    }
308
309
    if( leaf != null ) {
310
      replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
311
      decorate();
312
      expand( leaf );
313
    }
314
  }
315
316
  /**
317
   * Called when autocomplete finishes on a valid leaf or when the user presses
318
   * Enter to finish manual autocomplete.
319
   */
320
  private void decorate() {
321
    // A little bit of duplication...
322
    final String paragraph = getCaretParagraph();
323
    final int[] boundaries = getWordBoundaries( paragraph );
324
    final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
325
326
    final String newVariable = decorate( old );
327
328
    final int posEnded = getCurrentCaretPosition();
329
    final int posBegan = posEnded - old.length();
330
331
    getEditor().replaceText( posBegan, posEnded, newVariable );
332
  }
333
334
  /**
335
   * Called when user double-clicks on a tree view item.
336
   *
337
   * @param variable The variable to decorate.
338
   */
339
  private String decorate( final String variable ) {
340
    return getVariableDecorator().decorate( variable );
341
  }
342
343
  /**
344
   * Inserts the given string at the current caret position, or replaces
345
   * selected text (if any).
346
   *
347
   * @param s The string to inject.
348
   */
349
  private void replaceSelection( final String s ) {
350
    getEditor().replaceSelection( s );
351
  }
352
353
  /**
354
   * Updates the text at the given position within the current paragraph.
355
   *
356
   * @param posBegan The starting index in the paragraph text to replace.
357
   * @param posEnded The ending index in the paragraph text to replace.
358
   * @param text     Overwrite the paragraph substring with this text.
359
   */
360
  private void replaceText(
361
      final int posBegan, final int posEnded, final String text ) {
362
    final int p = getCurrentParagraph();
363
364
    getEditor().replaceText( p, posBegan, p, posEnded, text );
365
  }
366
367
  /**
368
   * Returns the caret's current paragraph position.
369
   *
370
   * @return A number greater than or equal to 0.
371
   */
372
  private int getCurrentParagraph() {
373
    return getEditor().getCurrentParagraph();
374
  }
375
376
  /**
377
   * Returns current word boundary indexes into the current paragraph, excluding
378
   * punctuation.
379
   *
380
   * @param p      The paragraph wherein to hunt word boundaries.
381
   * @param offset The offset into the paragraph to begin scanning left and
382
   *               right.
383
   * @return The starting and ending index of the word closest to the caret.
384
   */
385
  private int[] getWordBoundaries( final String p, final int offset ) {
386
    // Remove dashes, but retain hyphens. Retain same number of characters
387
    // to preserve relative indexes.
388
    final String paragraph = p.replace( "---", "   " ).replace( "--", "  " );
389
390
    return getWordAt( paragraph, offset );
391
  }
392
393
  /**
394
   * Helper method to get the word boundaries for the current paragraph.
395
   *
396
   * @param paragraph The paragraph to search for word boundaries.
397
   * @return The word boundary indexes into the paragraph.
398
   */
399
  private int[] getWordBoundaries( final String paragraph ) {
400
    return getWordBoundaries( paragraph, getCurrentCaretColumn() );
401
  }
402
403
  /**
404
   * Given an arbitrary offset into a string, this returns the word at that
405
   * index. The inputs and outputs include:
406
   *
407
   * <ul>
408
   * <li>surrounded by space: <code>hello | world!</code> ("");</li>
409
   * <li>end of word: <code>hello| world!</code> ("hello");</li>
410
   * <li>start of a word: <code>hello |world!</code> ("world");</li>
411
   * <li>within a word: <code>hello wo|rld!</code> ("world");</li>
412
   * <li>end of a paragraph: <code>hello world!|</code> ("world");</li>
413
   * <li>start of a paragraph: <code>|hello world!</code> ("hello"); or</li>
414
   * <li>after punctuation: <code>hello world!|</code> ("world").</li>
415
   * </ul>
416
   *
417
   * @param p      The string to scan for a word.
418
   * @param offset The offset within s to begin searching for the nearest word
419
   *               boundary, must not be out of bounds of s.
420
   * @return The word in s at the offset.
421
   */
422
  private int[] getWordAt( final String p, final int offset ) {
423
    return new int[]{getWordBegan( p, offset ), getWordEnded( p, offset )};
424
  }
425
426
  /**
427
   * Returns the index into s where a word begins.
428
   *
429
   * @param s      Never null.
430
   * @param offset Index into s to begin searching backwards for a word
431
   *               boundary.
432
   * @return The index where a word begins.
433
   */
434
  private int getWordBegan( final String s, int offset ) {
435
    while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
436
      offset--;
437
    }
438
439
    return offset;
440
  }
441
442
  /**
443
   * Returns the index into s where a word ends.
444
   *
445
   * @param s      Never null.
446
   * @param offset Index into s to begin searching forwards for a word boundary.
447
   * @return The index where a word ends.
448
   */
449
  private int getWordEnded( final String s, int offset ) {
450
    final int length = s.length();
451
452
    while( offset < length && isBoundary( s.charAt( offset ) ) ) {
453
      offset++;
454
    }
455
456
    return offset;
457
  }
458
459
  /**
460
   * Returns true if the given character can be reasonably expected to be part
461
   * of a word, including punctuation marks.
462
   *
463
   * @param c The character to compare.
464
   * @return false The character is a space character.
465
   */
466
  private boolean isBoundary( final char c ) {
467
    return !isWhitespace( c ) && !isPunctuation( c );
468
  }
469
470
  /**
471
   * Returns true if the given character is part of the set of Latin (English)
472
   * punctuation marks.
473
   *
474
   * @param c The character to determine whether it is punctuation.
475
   * @return {@code true} when the given character is in the set of
476
   * {@link #PUNCTUATION}.
477
   */
478
  private static boolean isPunctuation( final char c ) {
479
    return PUNCTUATION.indexOf( c ) != -1;
480
  }
481
482
  /**
483
   * Returns the text for the paragraph that contains the caret.
484
   *
485
   * @return A non-null string, possibly empty.
486
   */
487
  private String getCaretParagraph() {
488
    return getEditor().getText( getCurrentParagraph() );
489
  }
490
491
  /**
492
   * Returns true if the node has children that can be selected (i.e., any
493
   * non-leaves).
494
   *
495
   * @param <T>  The type that the TreeItem contains.
496
   * @param node The node to test for terminality.
497
   * @return true The node has one branch and its a leaf.
498
   */
499
  private <T> boolean isTerminal( final TreeItem<T> node ) {
500
    final ObservableList<TreeItem<T>> branches = node.getChildren();
501
502
    return branches.size() == 1 && branches.get( 0 ).isLeaf();
503
  }
504
505
  /**
506
   * Inserts text that the user typed at the current caret position, then
507
   * performs an autocomplete for the variable name.
508
   *
509
   * @param text The text to insert, never null.
510
   */
511
  private void typed( final String text ) {
512
    getEditor().replaceSelection( text );
513
    vModeAutocomplete();
514
  }
515
516
  /**
517
   * Called when the user presses either End or Enter key.
518
   */
519
  private void acceptPath() {
520
    final IndexRange range = getSelectionRange();
521
522
    if( range != null ) {
523
      final int rangeEnd = range.getEnd();
524
      final StyledTextArea<?, ?> textArea = getEditor();
525
      textArea.deselect();
526
      textArea.moveTo( rangeEnd );
527
    }
528
  }
529
530
  /**
531
   * Replaces the entirety of the existing path (from the initial caret
532
   * position) with the given path.
533
   *
534
   * @param oldPath The path to replace.
535
   * @param newPath The replacement path.
536
   */
537
  private void replacePath( final String oldPath, final String newPath ) {
538
    final StyledTextArea<?, ?> textArea = getEditor();
539
    final int posBegan = getInitialCaretPosition();
540
    final int posEnded = posBegan + oldPath.length();
541
542
    textArea.deselect();
543
    textArea.replaceText( posBegan, posEnded, newPath );
544
  }
545
546
  /**
547
   * Called when the user presses the Backspace key.
548
   */
549
  private void deleteSelection() {
550
    final StyledTextArea<?, ?> textArea = getEditor();
551
    textArea.replaceSelection( "" );
552
    textArea.deletePreviousChar();
553
  }
554
555
  /**
556
   * Cycles the selected text through the nodes.
557
   *
558
   * @param direction true - next; false - previous
559
   */
560
  private void cycleSelection( final boolean direction ) {
561
    final TreeItem<String> node = getCurrentNode();
562
563
    // Find the sibling for the current selection and replace the current
564
    // selection with the sibling's value
565
    TreeItem<String> cycled = direction
566
        ? node.nextSibling()
567
        : node.previousSibling();
568
569
    // When cycling at the end (or beginning) of the list, jump to the first
570
    // (or last) sibling depending on the cycle direction.
571
    if( cycled == null ) {
572
      cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
573
    }
574
575
    final String path = getCurrentPath();
576
    final String cycledWord = cycled.getValue();
577
    final String word = getLastPathWord();
578
    final int index = path.indexOf( word );
579
    final String cycledPath = path.substring( 0, index ) + cycledWord;
580
581
    expand( cycled );
582
    replacePath( path, cycledPath );
583
  }
584
585
  /**
586
   * Cycles to the next sibling of the currently selected tree node.
587
   */
588
  private void cyclePathNext() {
589
    cycleSelection( true );
590
  }
591
592
  /**
593
   * Cycles to the previous sibling of the currently selected tree node.
594
   */
595
  private void cyclePathPrev() {
596
    cycleSelection( false );
597
  }
598
599
  /**
600
   * Returns the variable name (or as much as has been typed so far). Returns
601
   * all the characters from the initial caret column to the the first
602
   * whitespace character. This will return a path that contains zero or more
603
   * separators.
604
   *
605
   * @return A non-null string, possibly empty.
606
   */
607
  private String getCurrentPath() {
608
    final String s = extractTextChunk();
609
    final int length = s.length();
610
611
    int i = 0;
612
613
    while( i < length && !isWhitespace( s.charAt( i ) ) ) {
614
      i++;
615
    }
616
617
    return s.substring( 0, i );
618
  }
619
620
  private <T> ObservableList<TreeItem<T>> getSiblings(
621
      final TreeItem<T> item ) {
622
    final TreeItem<T> parent = item.getParent();
623
    return parent == null ? item.getChildren() : parent.getChildren();
624
  }
625
626
  private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
627
    return getFirst( getSiblings( item ), item );
628
  }
629
630
  private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
631
    return getLast( getSiblings( item ), item );
632
  }
633
634
  /**
635
   * Returns the caret position as an offset into the text.
636
   *
637
   * @return A value from 0 to the length of the text (minus one).
638
   */
639
  private int getCurrentCaretPosition() {
640
    return getEditor().getCaretPosition();
641
  }
642
643
  /**
644
   * Returns the caret position within the current paragraph.
645
   *
646
   * @return A value from 0 to the length of the current paragraph.
647
   */
648
  private int getCurrentCaretColumn() {
649
    return getEditor().getCaretColumn();
650
  }
651
652
  /**
653
   * Returns the last word from the path.
654
   *
655
   * @return The last token.
656
   */
657
  private String getLastPathWord() {
658
    String path = getCurrentPath();
659
660
    int i = path.indexOf( SEPARATOR_CHAR );
661
662
    while( i > 0 ) {
663
      path = path.substring( i + 1 );
664
      i = path.indexOf( SEPARATOR_CHAR );
665
    }
666
667
    return path;
668
  }
669
670
  /**
671
   * Returns text from the initial caret position until some arbitrarily long
672
   * number of characters. The number of characters extracted will be
673
   * getMaxVarLength, or fewer, depending on how many characters remain to be
674
   * extracted. The result from this method is trimmed to the first whitespace
675
   * character.
676
   *
677
   * @return A chunk of text that includes all the words representing a path,
678
   * and then some.
679
   */
680
  private String extractTextChunk() {
681
    final StyledTextArea<?, ?> textArea = getEditor();
682
    final int textBegan = getInitialCaretPosition();
683
    final int remaining = textArea.getLength() - textBegan;
684
    final int textEnded = min( remaining, getMaxVarLength() );
685
686
    try {
687
      return textArea.getText( textBegan, textEnded );
688
    } catch( final Exception e ) {
689
      return textArea.getText();
690
    }
691
  }
692
693
  /**
694
   * Returns the node for the current path.
695
   */
696
  private TreeItem<String> getCurrentNode() {
697
    return findNode( getCurrentPath() );
698
  }
699
700
  /**
701
   * Finds the node that most closely matches the given path.
702
   *
703
   * @param path The path that represents a node.
704
   * @return The node for the path, or the root node if the path could not be
705
   * found, but never null.
706
   */
707
  private TreeItem<String> findNode( final String path ) {
708
    return getDefinitionPane().findNode( path );
709
  }
710
711
  /**
712
   * Finds the first leaf having a value that starts with the given text.
713
   *
714
   * @param text The text to find in the definition tree.
715
   * @return The leaf that starts with the given text, or null if not found.
716
   */
717
  private VariableTreeItem<String> findLeaf( final String text ) {
718
    return getDefinitionPane().findLeaf( text, false );
719
  }
720
721
  /**
722
   * Finds the first leaf having a value that starts with the given text, or
723
   * contains the text if contains is true.
724
   *
725
   * @param text     The text to find in the definition tree.
726
   * @param contains Set true to perform a substring match after a starts with
727
   *                 match.
728
   * @return The leaf that starts with the given text, or null if not found.
729
   */
730
  @SuppressWarnings("SameParameterValue")
731
  private VariableTreeItem<String> findLeaf(
732
      final String text, final boolean contains ) {
733
    return getDefinitionPane().findLeaf( text, contains );
734
  }
735
736
  /**
737
   * Used to ignore typed keys in favour of trapping pressed keys.
738
   *
739
   * @param e The key that was typed.
740
   */
741
  private void vModeKeyTyped( KeyEvent e ) {
742
    e.consume();
743
  }
744
745
  /**
746
   * Used to lazily initialize the keyboard map.
747
   *
748
   * @return Mappings for keyTyped and keyPressed.
749
   */
750
  protected InputMap<InputEvent> createKeyboardMap() {
751
    return sequence(
752
        consume( keyTyped(), this::vModeKeyTyped ),
753
        consume( keyPressed(), this::vModeKeyPressed )
754
    );
755
  }
756
757
  private InputMap<InputEvent> getKeyboardMap() {
758
    if( this.keyboardMap == null ) {
759
      this.keyboardMap = createKeyboardMap();
760
    }
761
762
    return this.keyboardMap;
763
  }
764
765
  /**
766
   * Collapses the tree then expands and selects the given node.
767
   *
768
   * @param node The node to expand.
769
   */
770
  private void expand( final TreeItem<String> node ) {
771
    final DefinitionPane pane = getDefinitionPane();
772
    pane.collapse();
773
    pane.expand( node );
774
    pane.select( node );
775
  }
776
777
  /**
778
   * Returns true iff the key code the user typed can be used as part of a YAML
779
   * variable name.
780
   *
781
   * @param keyEvent Keyboard key press event information.
782
   * @return true The key is a value that can be inserted into the text.
783
   */
784
  private boolean isVariableNameKey( final KeyEvent keyEvent ) {
785
    final KeyCode kc = keyEvent.getCode();
786
787
    return (kc.isLetterKey()
788
        || kc.isDigitKey()
789
        || (keyEvent.isShiftDown() && kc == MINUS))
790
        && !keyEvent.isControlDown();
791
  }
792
793
  /**
794
   * Starts to capture user input events.
795
   */
796
  private void vModeStart() {
797
    addEventListener( getKeyboardMap() );
798
  }
799
800
  /**
801
   * Restores capturing of user input events to the previous event listener.
802
   * Also asks the processing chain to modify the variable text into a
803
   * machine-readable variable based on the format required by the file type.
804
   * For example, a Markdown file (.md) will substitute a $VAR$ name while an R
805
   * file (.Rmd, .Rxml) will use `r#xVAR`.
806
   */
807
  private void vModeStop() {
808
    removeEventListener( getKeyboardMap() );
809
  }
810
811
  /**
812
   * @return A variable decorator that corresponds to the given file type.
813
   */
814
  private VariableDecorator getVariableDecorator() {
815
    return VariableNameDecoratorFactory.newInstance( getFilename() );
816
  }
817
818
  private Path getFilename() {
819
    return getFileEditorTab().getPath();
820
  }
821
822
  /**
823
   * Returns the index where the two strings diverge.
824
   *
825
   * @param s1 The string that could be a substring of s2, null allowed.
826
   * @param s2 The string that could be a substring of s1, null allowed.
827
   * @return NO_DIFFERENCE if the strings are the same, otherwise the index
828
   * where they differ.
829
   */
830
  private int difference( final CharSequence s1, final CharSequence s2 ) {
831
    if( s1 == s2 ) {
832
      return NO_DIFFERENCE;
833
    }
834
835
    if( s1 == null || s2 == null ) {
836
      return 0;
837
    }
838
839
    int i = 0;
840
    final int limit = min( s1.length(), s2.length() );
841
842
    while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
843
      i++;
844
    }
845
846
    // If one string was shorter than the other, that's where they differ.
847
    return i;
848
  }
849
850
  private EditorPane getEditorPane() {
851
    return getFileEditorTab().getEditorPane();
852
  }
853
854
  /**
855
   * Delegates to the file editor pane, and, ultimately, to its text area.
856
   */
857
  private <T extends Event, U extends T> void addKeyboardListener(
858
      final EventPattern<? super T, ? extends U> event,
859
      final Consumer<? super U> consumer ) {
860
    getEditorPane().addKeyboardListener( event, consumer );
861
  }
862
863
  /**
864
   * Delegates to the file editor pane, and, ultimately, to its text area.
865
   *
866
   * @param map The map of methods to events.
867
   */
868
  private void addEventListener( final InputMap<InputEvent> map ) {
869
    getEditorPane().addEventListener( map );
870
  }
871
872
  private void removeEventListener( final InputMap<InputEvent> map ) {
873
    getEditorPane().removeEventListener( map );
874
  }
875
876
  /**
877
   * Returns the position of the caret when variable mode editing was requested.
878
   *
879
   * @return The variable mode caret position.
880
   */
881
  private int getInitialCaretPosition() {
882
    return this.initialCaretPosition;
883
  }
884
885
  /**
886
   * Sets the position of the caret when variable mode editing was requested.
887
   * Stores the current position because only the text that comes afterwards is
888
   * a suitable variable reference.
889
   */
890
  private void setInitialCaretPosition() {
891
    this.initialCaretPosition = getEditor().getCaretPosition();
892
  }
893
894
  private StyledTextArea<?, ?> getEditor() {
895
    return getEditorPane().getEditor();
896
  }
897
898
  public FileEditorTab getFileEditorTab() {
899
    return this.tab;
900
  }
901
902
  public void setFileEditorTab( final FileEditorTab editorTab ) {
903
    this.tab = editorTab;
904
  }
905
906
  private DefinitionPane getDefinitionPane() {
907
    return this.definitionPane;
908
  }
909
910
  private void setDefinitionPane( final DefinitionPane definitionPane ) {
911
    this.definitionPane = definitionPane;
912
  }
913
914
  private IndexRange getSelectionRange() {
915
    return getEditor().getSelection();
916
  }
917
918
  /**
919
   * Don't look ahead too far when trying to find the end of a node.
920
   *
921
   * @return 512 by default.
922
   */
923
  private int getMaxVarLength() {
924
    return getSettings().getSetting(
925
        "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
926
  }
927
928
  private Settings getSettings() {
929
    return this.settings;
930
  }
931
932
  private synchronized EventHandler<MouseEvent> getPanelEventHandler() {
933
    if( this.panelEventHandler == null ) {
934
      this.panelEventHandler = createPanelEventHandler();
935
    }
936
937
    return this.panelEventHandler;
938
  }
939
940
  private EventHandler<MouseEvent> createPanelEventHandler() {
941
    return new PanelEventHandler();
942
  }
943
944
  /**
945
   * Responsible for handling double-click events on the definition pane.
946
   */
947
  private class PanelEventHandler implements EventHandler<MouseEvent> {
948
949
    public PanelEventHandler() {
950
    }
951
952
    @Override
953
    public void handle( final MouseEvent event ) {
954
      final Object source = event.getSource();
955
956
      if( source instanceof TreeView ) {
957
        final TreeView<?> tree = (TreeView<?>) source;
958
        final TreeItem<?> item = tree.getSelectionModel().getSelectedItem();
959
960
        if( item instanceof VariableTreeItem ) {
961
          final VariableTreeItem<?> var = (VariableTreeItem<?>) item;
990962
          final String text = decorate( var.toPath() );
991963
M src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
2929
3030
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.ast.Node;
32
import com.vladsch.flexmark.ast.NodeVisitor;
33
import com.vladsch.flexmark.ast.VisitHandler;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
3434
3535
/**
...
4747
   *
4848
   * @param index Index into the paragraph that indicates the hyperlink to
49
   * change.
49
   *              change.
5050
   */
5151
  public LinkVisitor( final int index ) {
...
5959
6060
  /**
61
   *
6261
   * @param link Not null.
6362
   */
...
8281
  protected NodeVisitor createVisitor() {
8382
    return new NodeVisitor(
84
      new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
83
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
8584
  }
8685
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
2828
package com.scrivenvar.editors.markdown;
2929
30
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
3130
import com.scrivenvar.dialogs.ImageDialog;
3231
import com.scrivenvar.dialogs.LinkDialog;
3332
import com.scrivenvar.editors.EditorPane;
3433
import com.scrivenvar.processors.MarkdownProcessor;
35
import static com.scrivenvar.util.Utils.ltrim;
36
import static com.scrivenvar.util.Utils.rtrim;
3734
import com.vladsch.flexmark.ast.Link;
38
import com.vladsch.flexmark.ast.Node;
39
import java.nio.file.Path;
40
import java.util.regex.Matcher;
41
import java.util.regex.Pattern;
42
import javafx.beans.value.ObservableValue;
35
import com.vladsch.flexmark.util.ast.Node;
4336
import javafx.scene.control.Dialog;
4437
import javafx.scene.control.IndexRange;
45
import static javafx.scene.input.KeyCode.ENTER;
4638
import javafx.scene.input.KeyEvent;
4739
import javafx.stage.Window;
4840
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.scrivenvar.Constants.STYLESHEET_MARKDOWN;
47
import static com.scrivenvar.util.Utils.ltrim;
48
import static com.scrivenvar.util.Utils.rtrim;
49
import static javafx.scene.input.KeyCode.ENTER;
4950
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
5051
...
5758
5859
  private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
59
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
60
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
6061
6162
  public MarkdownEditorPane() {
...
7172
7273
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
73
  }
74
75
  public ObservableValue<String> markdownProperty() {
76
    return getEditor().textProperty();
7774
  }
7875
7976
  private void enterPressed( final KeyEvent e ) {
8077
    final StyleClassedTextArea textArea = getEditor();
81
    final String currentLine = textArea.getText( textArea.getCurrentParagraph() );
78
    final String currentLine =
79
        textArea.getText( textArea.getCurrentParagraph() );
8280
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
8381
8482
    String newText = "\n";
8583
8684
    if( matcher.matches() ) {
8785
      if( !matcher.group( 2 ).isEmpty() ) {
88
        // indent new line with same whitespace characters and list markers as current line
86
        // indent new line with same whitespace characters and list markers
87
        // as current line
8988
        newText = newText.concat( matcher.group( 1 ) );
9089
      }
9190
      else {
9291
        // current line contains only whitespace characters and list markers
9392
        // --> empty current line
9493
        final int caretPosition = textArea.getCaretPosition();
95
        textArea.selectRange( caretPosition - currentLine.length(), caretPosition );
94
        textArea.selectRange( caretPosition - currentLine.length(),
95
                              caretPosition );
9696
      }
9797
    }
...
108108
  }
109109
110
  public void surroundSelection( String leading, String trailing, final String hint ) {
110
  public void surroundSelection( String leading, String trailing,
111
                                 final String hint ) {
111112
    final StyleClassedTextArea textArea = getEditor();
112113
...
131132
    }
132133
133
    // remove trailing whitespaces from trailing text if selection ends at text end
134
    // remove trailing whitespaces from trailing text if selection ends at
135
    // text end
134136
    if( end == textArea.getLength() ) {
135137
      trailing = rtrim( trailing );
...
184186
185187
    // replace text and update selection
186
    textArea.replaceText( start, end, leading + trimmedSelectedText + trailing );
188
    textArea.replaceText( start,
189
                          end,
190
                          leading + trimmedSelectedText + trailing );
187191
    textArea.selectRange( selStart, selEnd );
188192
  }
189193
190194
  /**
191195
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
192196
   * the markdown AST.
193197
   *
194
   * @return
198
   * @return An instance containing the link URL and display text.
195199
   */
196200
  private HyperlinkModel getHyperlink() {
...
210214
    }
211215
212
    final HyperlinkModel model = createHyperlinkModel(
213
      link, selectedText, "https://website.com"
216
    return createHyperlinkModel(
217
        link, selectedText, "https://localhost"
214218
    );
215
216
    return model;
217219
  }
218220
221
  @SuppressWarnings("SameParameterValue")
219222
  private HyperlinkModel createHyperlinkModel(
220
    final Link link, final String selection, final String url ) {
223
      final Link link, final String selection, final String url ) {
221224
222225
    return link == null
223
      ? new HyperlinkModel( selection, url )
224
      : new HyperlinkModel( link );
226
        ? new HyperlinkModel( selection, url )
227
        : new HyperlinkModel( link );
225228
  }
226229
227230
  private Path getParentPath() {
228231
    final Path parentPath = getPath();
229232
    return (parentPath != null) ? parentPath.getParent() : null;
230233
  }
231234
232235
  private Dialog<String> createLinkDialog() {
233
    return new LinkDialog( getWindow(), getHyperlink(), getParentPath() );
236
    return new LinkDialog( getWindow(), getHyperlink() );
234237
  }
235238
236239
  private Dialog<String> createImageDialog() {
237240
    return new ImageDialog( getWindow(), getParentPath() );
238241
  }
239242
240243
  private void insertObject( final Dialog<String> dialog ) {
241
    dialog.showAndWait().ifPresent( result -> {
242
      getEditor().replaceSelection( result );
243
    } );
244
    dialog.showAndWait().ifPresent(
245
        result -> getEditor().replaceSelection( result )
246
    );
244247
  }
245248
M src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
3838
 * filename extension patterns provided during construction.
3939
 *
40
 * @see http://docs.oracle.com/javase/tutorial/essential/io/find.html
41
 *
4240
 * @author White Magic Software, Ltd.
4341
 */
D src/main/java/com/scrivenvar/predicates/strings/EqualPredicate.java
1
/*
2
 * Copyright 2016 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.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if two strings are equal.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class EqualPredicate extends StringPredicate {
36
37
  /**
38
   * Calls the superclass to construct the instance.
39
   *
40
   * @param comparate Not null.
41
   */
42
  public EqualPredicate( final String comparate ) {
43
    super( comparate );
44
  }
45
46
  /**
47
   * Compares two strings.
48
   *
49
   * @param comparator A non-null string, possibly empty.
50
   *
51
   * @return true The strings are equal, ignoring case.
52
   */
53
  @Override
54
  public boolean test( final String comparator ) {
55
    return comparator.equalsIgnoreCase( getComparate() );
56
  }
57
}
581
M src/main/java/com/scrivenvar/preferences/FilePreferences.java
4848
 * problems. This class sidesteps the issue entirely by writing to the user's
4949
 * home directory, where permissions should be a bit more lax.
50
 * 
51
 * @see http://stackoverflow.com/q/208231/59087
5250
 */
5351
public class FilePreferences extends AbstractPreferences {
5452
55
  private Map<String, String> root = new TreeMap<>();
56
  private Map<String, FilePreferences> children = new TreeMap<>();
57
  private boolean isRemoved;
53
  private final Map<String, String> mRoot = new TreeMap<>();
54
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
55
  private boolean mRemoved;
5856
5957
  public FilePreferences( final AbstractPreferences parent, final String name ) {
...
6967
  @Override
7068
  protected void putSpi( final String key, final String value ) {
71
    root.put( key, value );
69
    mRoot.put( key, value );
7270
7371
    try {
...
8078
  @Override
8179
  protected String getSpi( final String key ) {
82
    return root.get( key );
80
    return mRoot.get( key );
8381
  }
8482
8583
  @Override
8684
  protected void removeSpi( final String key ) {
87
    root.remove( key );
85
    mRoot.remove( key );
8886
8987
    try {
...
9694
  @Override
9795
  protected void removeNodeSpi() throws BackingStoreException {
98
    isRemoved = true;
96
    mRemoved = true;
9997
    flush();
10098
  }
10199
102100
  @Override
103
  protected String[] keysSpi() throws BackingStoreException {
104
    return root.keySet().toArray( new String[ root.keySet().size() ] );
101
  protected String[] keysSpi() {
102
    return mRoot.keySet().toArray( new String[ 0 ] );
105103
  }
106104
107105
  @Override
108
  protected String[] childrenNamesSpi() throws BackingStoreException {
109
    return children.keySet().toArray( new String[ children.keySet().size() ] );
106
  protected String[] childrenNamesSpi() {
107
    return mChildren.keySet().toArray( new String[ 0 ] );
110108
  }
111109
112110
  @Override
113111
  protected FilePreferences childSpi( final String name ) {
114
    FilePreferences child = children.get( name );
112
    FilePreferences child = mChildren.get( name );
115113
116114
    if( child == null || child.isRemoved() ) {
117115
      child = new FilePreferences( this, name );
118
      children.put( name, child );
116
      mChildren.put( name, child );
119117
    }
120118
121119
    return child;
122120
  }
123121
124122
  @Override
125
  protected void syncSpi() throws BackingStoreException {
123
  protected void syncSpi() {
126124
    if( isRemoved() ) {
127125
      return;
...
151149
            // Only load immediate descendants
152150
            if( subKey.indexOf( '.' ) == -1 ) {
153
              root.put( subKey, p.getProperty( propKey ) );
151
              mRoot.put( subKey, p.getProperty( propKey ) );
154152
            }
155153
          }
156154
        }
157
      } catch( final IOException ex ) {
155
      } catch( final Exception ex ) {
158156
        error( new BackingStoreException( ex ) );
159157
      }
...
168166
169167
  @Override
170
  protected void flushSpi() throws BackingStoreException {
168
  protected void flushSpi() {
171169
    final File file = FilePreferencesFactory.getPreferencesFile();
172170
...
204202
205203
        // If this node hasn't been removed, add back in any values
206
        if( !isRemoved ) {
207
          for( final String s : root.keySet() ) {
208
            p.setProperty( path + s, root.get( s ) );
204
        if( !mRemoved ) {
205
          for( final String s : mRoot.keySet() ) {
206
            p.setProperty( path + s, mRoot.get( s ) );
209207
          }
210208
        }
211209
212210
        p.store( new FileOutputStream( file ), "FilePreferences" );
213
      } catch( final IOException ex ) {
211
      } catch( final Exception ex ) {
214212
        error( new BackingStoreException( ex ) );
215213
      }
M src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
2828
package com.scrivenvar.preferences;
2929
30
import static com.scrivenvar.Constants.APP_TITLE;
3130
import java.io.File;
3231
import java.nio.file.FileSystems;
3332
import java.util.prefs.Preferences;
3433
import java.util.prefs.PreferencesFactory;
34
35
import static com.scrivenvar.Constants.APP_TITLE;
3536
3637
/**
M src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
146146
  }
147147
148
  private Object execute( final String script ) {
149
    return getEngine().executeScript( script );
148
  private void execute( final String script ) {
149
    getEngine().executeScript( script );
150150
  }
151151
M src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java
4848
   *
4949
   * @param t The text that contains
50
   *
51
   * @return
50
   * @return The value of the first instance replaced.
5251
   */
5352
  @Override
...
6261
   *
6362
   * @param haystack Search this string for the needle, must not be null.
64
   * @param needle The text to find in the haystack.
65
   * @param thread Replace the needle with this text, if the needle is found.
66
   *
63
   * @param needle   The text to find in the haystack.
64
   * @param thread   Replace the needle with this text, if the needle is found.
6765
   * @return The haystack with the first instance of needle replaced with
6866
   * thread.
6967
   */
68
  @SuppressWarnings("SameParameterValue")
7069
  private static String replace(
71
    final String haystack, final String needle, final String thread ) {
72
73
    final int end = haystack.indexOf( needle, 0 );
74
75
    if( end == INDEX_NOT_FOUND ) {
76
      return haystack;
77
    }
78
79
    int start = 0;
80
    final int needleLength = needle.length();
81
82
    int len = thread.length() - needleLength;
83
    len = (len < 0 ? 0 : len);
84
    final StringBuilder buffer = new StringBuilder( haystack.length() + len );
85
86
    if( end != INDEX_NOT_FOUND ) {
87
      buffer.append( haystack.substring( start, end ) ).append( thread );
88
      start = end + needleLength;
89
    }
70
      final String haystack, final String needle, final String thread ) {
71
    final int end = haystack.indexOf( needle );
9072
91
    return buffer.append( haystack.substring( start ) ).toString();
73
    return end == INDEX_NOT_FOUND ?
74
        haystack :
75
        haystack.substring( 0, end ) + thread +
76
            haystack.substring( end + needle.length() );
9277
  }
9378
}
M src/main/java/com/scrivenvar/processors/DefaultVariableProcessor.java
4242
4343
  /**
44
   * Constructs a variable processor for dereferencing variables.
44
   * Constructs a variable processor to dereference variables.
4545
   *
4646
   * @param successor Usually the HTML Preview Processor.
M src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java
4242
4343
  // There is only one preview panel.
44
  private static HTMLPreviewPane htmlPreviewPane;
44
  private static HTMLPreviewPane sHtmlPreviewPane;
4545
4646
  /**
4747
   * Constructs the end of a processing chain.
4848
   *
4949
   * @param htmlPreviewPane The pane to update with the post-processed document.
5050
   */
5151
  public HTMLPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
52
    super( null );
53
    setHtmlPreviewPane( htmlPreviewPane );
52
    sHtmlPreviewPane = htmlPreviewPane;
5453
  }
5554
...
7271
7372
  private HTMLPreviewPane getHtmlPreviewPane() {
74
    return this.htmlPreviewPane;
75
  }
76
77
  private void setHtmlPreviewPane( final HTMLPreviewPane htmlPreviewPane ) {
78
    this.htmlPreviewPane = htmlPreviewPane;
73
    return sHtmlPreviewPane;
7974
  }
8075
}
M src/main/java/com/scrivenvar/processors/IdentityProcessor.java
3838
  /**
3939
   * Passes the link to the super constructor.
40
   * 
40
   *
4141
   * @param link The next processor in the chain to use for text processing.
4242
   */
...
4949
   *
5050
   * @param t The string to return, enclosed in "pre" tags.
51
   *
5251
   * @return The value of t wrapped in "pre" tags.
5352
   */
5453
  @Override
5554
  public String processLink( final String t ) {
56
    final StringBuilder result = new StringBuilder( t.length() + 16 );
57
    
58
    return result.append( "<pre>" ).append( t ).append( "</pre>" ).toString();
55
    return "<pre>" + t + "</pre>";
5956
  }
6057
}
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
2828
package com.scrivenvar.processors;
2929
30
import static com.scrivenvar.Constants.PERSIST_R_STARTUP;
31
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
32
import static com.scrivenvar.Messages.get;
3330
import com.scrivenvar.Services;
34
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
35
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
36
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
3731
import com.scrivenvar.service.Options;
3832
import com.scrivenvar.service.events.Notifier;
39
import java.io.IOException;
40
import static java.lang.Math.min;
41
import java.nio.file.Path;
42
import java.nio.file.Paths;
43
import java.util.Map;
33
import org.renjin.eval.EvalException;
34
4435
import javax.script.ScriptEngine;
4536
import javax.script.ScriptEngineManager;
4637
import javax.script.ScriptException;
38
import java.nio.file.Path;
39
import java.util.Map;
40
41
import static com.scrivenvar.Constants.*;
42
import static com.scrivenvar.Messages.get;
43
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
44
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
45
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
46
import static java.lang.Math.min;
4747
4848
/**
...
5858
  // Only one editor is open at a time.
5959
  private static final ScriptEngine ENGINE =
60
    (new ScriptEngineManager()).getEngineByName( "Renjin" );
60
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
6161
6262
  /**
6363
   * Constructs a processor capable of evaluating R statements.
6464
   *
6565
   * @param processor Subsequent link in the processing chain.
66
   * @param map Resolved definitions map.
67
   * @param path Path to the file being edited so that its working directory can
68
   * be extracted. Must not be null.
66
   * @param map       Resolved definitions map.
6967
   */
7068
  public InlineRProcessor(
71
    final Processor<String> processor,
72
    final Map<String, String> map,
73
    final Path path ) {
69
      final Processor<String> processor,
70
      final Map<String, String> map ) {
7471
    super( processor, map );
75
    init( path.getParent() );
72
    init();
7673
  }
7774
7875
  /**
7976
   * Initialises the R code so that R can find imported libraries.
80
   *
81
   * @param workingDirectory Location where R is to look for imports.
8277
   */
83
  private void init( final Path workingDirectory ) {
78
  private void init() {
8479
    try {
85
      final Path wd = nullSafe( workingDirectory );
80
      final Path wd = getWorkingDirectory();
8681
      final String dir = wd.toString().replace( '\\', '/' );
8782
      final Map<String, String> definitions = getDefinitions();
...
9489
        eval( rScript );
9590
      }
96
    } catch( final IOException | ScriptException e ) {
97
      throw new RuntimeException( e );
91
    } catch( final Exception e ) {
92
      // Tell the user that there was a problem.
93
      getNotifier().notify( e.getMessage() );
9894
    }
9995
  }
10096
10197
  /**
102
   * Loads the R init script from the applciation's persisted preferences.
98
   * Loads the R init script from the application's persisted preferences.
10399
   *
104100
   * @return A non-null String, possibly empty.
105
   * @throws IOException Could not load the init script.
106101
   */
107
  private String getInitScript() throws IOException {
102
  private String getInitScript() {
108103
    return getOptions().get( PERSIST_R_STARTUP, "" );
109104
  }
110105
111106
  /**
112107
   * Evaluates all R statements in the source document and inserts the
113108
   * calculated value into the generated document.
114109
   *
115110
   * @param text The document text that includes variables that should be
116
   * replaced with values when rendered as HTML.
117
   *
111
   *             replaced with values when rendered as HTML.
118112
   * @return The generated document with output from all R statements
119113
   * substituted with value returned from their execution.
...
133127
    while( currIndex >= 0 ) {
134128
      // Copy everything up to, but not including, an R statement (`r#).
135
      sb.append( text.substring( prevIndex, currIndex ) );
129
      sb.append( text, prevIndex, currIndex );
136130
137131
      // Jump to the start of the R statement.
...
159153
          // Tell the user that there was a problem.
160154
          getNotifier().notify( get( STATUS_PARSE_ERROR,
161
            e.getMessage(), currIndex )
155
                                     e.getMessage(), currIndex )
162156
          );
163157
        }
164158
165159
        // Retain the R statement's ending position in the text.
166160
        prevIndex = currIndex + 1;
167
      }
168
      else {
169
        // TODO: Implement this.
170
        // There was a starting prefix but no ending suffix. Ignore the
171
        // problem, copy to the end, and exit the loop.
172
        //sb.append()
173161
      }
174162
...
185173
   *
186174
   * @param r The expression to evaluate.
187
   *
188175
   * @return The object resulting from the evaluation.
189176
   */
190
  private Object eval( final String r ) throws ScriptException {
177
  private Object eval( final String r ) throws ScriptException, EvalException {
191178
    return getScriptEngine().eval( r );
192179
  }
...
206193
  /**
207194
   * This will return the given path if not null, otherwise it will return
208
   * Paths.get( System.getProperty( "user.dir" ) ).
209
   *
210
   * @param path The path to make null safe.
195
   * the path to the user's directory.
211196
   *
212197
   * @return A non-null path.
213198
   */
214
  private Path nullSafe( final Path path ) {
215
    return path == null ? Paths.get( System.getProperty( "user.dir" ) ) : path;
199
  private Path getWorkingDirectory() {
200
    return Path.of( getPreference( PERSIST_R_DIRECTORY, USER_DIRECTORY ) );
201
  }
202
203
  /**
204
   * Returns the user-defined preference value for the given key.
205
   *
206
   * @param key          The key to find in the user's preferences.
207
   * @param defaultValue The default value to return if no preference is set.
208
   * @return The value for the preference, or {@code defaultValue} if not found.
209
   */
210
  @SuppressWarnings("SameParameterValue")
211
  private String getPreference( final String key, final String defaultValue ) {
212
    return OPTIONS.get( key, defaultValue );
216213
  }
217214
}
M src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
3030
import static java.lang.Character.isLetter;
3131
import static java.lang.Math.min;
32
3233
import javafx.beans.value.ObservableValue;
3334
...
4344
   *
4445
   * @param processor The next processor in the chain.
45
   * @param position The caret's current position in the text.
46
   * @param position  The caret's current position in the text.
4647
   */
4748
  public MarkdownCaretInsertionProcessor(
48
    final Processor<String> processor,
49
    final ObservableValue<Integer> position ) {
49
      final Processor<String> processor,
50
      final ObservableValue<Integer> position ) {
5051
    super( processor, position );
5152
  }
5253
5354
  /**
5455
   * Changes the text to insert a "caret" at the caret position. This will
5556
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
5657
   *
5758
   * @param t The text document to process.
58
   *
5959
   * @return The text with the caret position token inserted at the caret
6060
   * position.
M src/main/java/com/scrivenvar/processors/MarkdownProcessor.java
2828
package com.scrivenvar.processors;
2929
30
import com.vladsch.flexmark.Extension;
31
import com.vladsch.flexmark.ast.Node;
3230
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
31
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
3332
import com.vladsch.flexmark.ext.tables.TablesExtension;
3433
import com.vladsch.flexmark.html.HtmlRenderer;
3534
import com.vladsch.flexmark.parser.Parser;
36
import com.vladsch.flexmark.superscript.SuperscriptExtension;
35
import com.vladsch.flexmark.util.ast.Node;
36
import com.vladsch.flexmark.util.misc.Extension;
37
3738
import java.util.ArrayList;
3839
import java.util.Collection;
...
7273
   *
7374
   * @param markdown The string to convert from Markdown to HTML.
74
   *
7575
   * @return The HTML representation of the Markdown document.
7676
   */
...
8686
   *
8787
   * @param markdown The markdown to convert into an AST.
88
   *
8988
   * @return The markdown AST for the given text (usually a paragraph).
9089
   */
...
9796
   *
9897
   * @param markdown The markdown to parse.
99
   *
10098
   * @return The root node of the markdown tree.
10199
   */
...
108106
   *
109107
   * @param markdown The markdown text to convert to HTML, must not be null.
110
   *
111108
   * @return The markdown rendered as an HTML document.
112109
   */
M src/main/java/com/scrivenvar/processors/Processor.java
3131
 * Responsible for processing documents from one known format to another.
3232
 *
33
 * @author White Magic Software, Ltd.
3433
 * @param <T> The type of processor to create.
34
 * @author White Magic Software, Ltd.
3535
 */
3636
public interface Processor<T> {
37
  
37
3838
  /**
3939
   * Provided so that the chain can be invoked from any link using a given
4040
   * value. This should be called automatically by a superclass so that
4141
   * the links in the chain need only implement the processLink method.
42
   * 
42
   *
4343
   * @param t The value to pass along to each link in the chain.
4444
   */
45
  public void processChain( T t );
45
  void processChain( T t );
4646
4747
  /**
4848
   * Processes the given content providing a transformation from one document
4949
   * format into another. For example, this could convert from XML to text using
5050
   * an XSLT processor, or from markdown to HTML.
5151
   *
5252
   * @param t The type of object to process.
53
   *
5453
   * @return The post-processed document, or null if processing should stop.
5554
   */
56
  public T processLink( T t );
55
  T processLink( T t );
5756
5857
  /**
5958
   * Adds a document processor to call after this processor finishes processing
6059
   * the document given to the process method.
6160
   *
6261
   * @return The processor that should transform the document after this
6362
   * instance has finished processing.
6463
   */
65
  public Processor<T> next();
64
  Processor<T> next();
6665
}
6766
M src/main/java/com/scrivenvar/processors/ProcessorFactory.java
3131
import com.scrivenvar.FileEditorTab;
3232
import com.scrivenvar.preview.HTMLPreviewPane;
33
import javafx.beans.value.ObservableValue;
34
3335
import java.nio.file.Path;
3436
import java.util.Map;
35
import javafx.beans.value.ObservableValue;
3637
3738
/**
...
5253
   * text and caret processing to generate a final preview.
5354
   *
54
   * @param previewPane
55
   * @param resolvedMap
55
   * @param previewPane Where the final output is rendered.
56
   * @param resolvedMap Map of definitions to replace before final render.
5657
   */
5758
  public ProcessorFactory(
58
    final HTMLPreviewPane previewPane,
59
    final Map<String, String> resolvedMap ) {
59
      final HTMLPreviewPane previewPane,
60
      final Map<String, String> resolvedMap ) {
6061
    this.previewPane = previewPane;
6162
    this.resolvedMap = resolvedMap;
6263
  }
6364
6465
  /**
6566
   * Creates a processor suitable for parsing and rendering the file opened at
6667
   * the given tab.
6768
   *
6869
   * @param tab The tab containing a text editor, path, and caret position.
69
   *
7070
   * @return A processor that can render the given tab's text.
7171
   */
...
9292
9393
      default:
94
        processor = createIdentityProcessor( tab );
94
        processor = createIdentityProcessor();
9595
        break;
9696
    }
...
121121
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
122122
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
123
    final Processor<String> mpp = new MarkdownProcessor( mcrp );
124123
125
    return mpp;
124
    return new MarkdownProcessor( mcrp );
126125
  }
127
  
128
  protected Processor<String> createIdentityProcessor( final FileEditorTab tab ) {
126
127
  protected Processor<String> createIdentityProcessor() {
129128
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
130
    final Processor<String> ip = new IdentityProcessor( hpp );
131
    
132
    return ip;
129
130
    return new IdentityProcessor( hpp );
133131
  }
134132
135
  protected Processor<String> createMarkdownProcessor( final FileEditorTab tab ) {
133
  protected Processor<String> createMarkdownProcessor(
134
      final FileEditorTab tab ) {
136135
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
137136
    final Processor<String> tpc = getCommonProcessor();
138
    final Processor<String> cip = createMarkdownInsertionProcessor( tpc, caret );
139
    final Processor<String> dvp = new DefaultVariableProcessor( cip, getResolvedMap() );
137
    final Processor<String> cip = createMarkdownInsertionProcessor(
138
        tpc, caret );
140139
141
    return dvp;
140
    return new DefaultVariableProcessor( cip, getResolvedMap() );
142141
  }
143142
144143
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
145144
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
146145
    final Processor<String> tpc = getCommonProcessor();
147146
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
148
    final Processor<String> dvp = new DefaultVariableProcessor( xmlp, getResolvedMap() );
149
    final Processor<String> xcip = createXMLInsertionProcessor( dvp, caret );
147
    final Processor<String> dvp = new DefaultVariableProcessor(
148
        xmlp, getResolvedMap() );
150149
151
    return xcip;
150
    return createXMLInsertionProcessor( dvp, caret );
152151
  }
153152
154153
  protected Processor<String> createRProcessor( final FileEditorTab tab ) {
155154
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
156155
    final Processor<String> tpc = getCommonProcessor();
157
    final Processor<String> rp = new InlineRProcessor( tpc, getResolvedMap(), tab.getPath() );
158
    final Processor<String> rvp = new RVariableProcessor( rp, getResolvedMap() );
159
    final Processor<String> cip = createRInsertionProcessor( rvp, caret );
156
    final Processor<String> rp = new InlineRProcessor( tpc, getResolvedMap() );
157
    final Processor<String> rvp = new RVariableProcessor(
158
        rp, getResolvedMap() );
160159
161
    return cip;
160
    return createRInsertionProcessor( rvp, caret );
162161
  }
163162
164163
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
165164
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
166165
    final Processor<String> tpc = getCommonProcessor();
167166
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
168
    final Processor<String> rp = new InlineRProcessor( xmlp, getResolvedMap(), tab.getPath() );
169
    final Processor<String> rvp = new RVariableProcessor( rp, getResolvedMap() );
170
    final Processor<String> xcip = createXMLInsertionProcessor( rvp, caret );
167
    final Processor<String> rp = new InlineRProcessor( xmlp, getResolvedMap() );
168
    final Processor<String> rvp = new RVariableProcessor(
169
        rp, getResolvedMap() );
171170
172
    return xcip;
171
    return createXMLInsertionProcessor( rvp, caret );
173172
  }
174173
175174
  private Processor<String> createMarkdownInsertionProcessor(
176
    final Processor<String> tpc, final ObservableValue<Integer> caret ) {
175
      final Processor<String> tpc, final ObservableValue<Integer> caret ) {
177176
    return new MarkdownCaretInsertionProcessor( tpc, caret );
178177
  }
179178
180179
  /**
181180
   * Create an insertion processor that is aware of R statements and will insert
182181
   * a caret outside of any statement the caret falls within.
183182
   *
184183
   * @param processor Another link in the processor chain.
185
   * @param caret The caret insertion point.
186
   *
184
   * @param caret     The caret insertion point.
187185
   * @return A processor that can insert a caret token without disturbing any R
188186
   * code.
189187
   */
190188
  private Processor<String> createRInsertionProcessor(
191
    final Processor<String> processor, final ObservableValue<Integer> caret ) {
189
      final Processor<String> processor,
190
      final ObservableValue<Integer> caret ) {
192191
    return new RMarkdownCaretInsertionProcessor( processor, caret );
193192
  }
194193
195194
  private Processor<String> createXMLInsertionProcessor(
196
    final Processor<String> tpc, final ObservableValue<Integer> caret ) {
195
      final Processor<String> tpc, final ObservableValue<Integer> caret ) {
197196
    return new XMLCaretInsertionProcessor( tpc, caret );
198197
  }
M src/main/java/com/scrivenvar/processors/RMarkdownCaretInsertionProcessor.java
3131
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
3232
import static java.lang.Integer.max;
33
3334
import javafx.beans.value.ObservableValue;
3435
3536
/**
3637
 * Responsible for inserting a caret position token into an R document.
3738
 *
3839
 * @author White Magic Software, Ltd.
3940
 */
4041
public class RMarkdownCaretInsertionProcessor
41
  extends MarkdownCaretInsertionProcessor {
42
    extends MarkdownCaretInsertionProcessor {
4243
4344
  /**
4445
   * Constructs a processor capable of inserting a caret marker into Markdown.
4546
   *
4647
   * @param processor The next processor in the chain.
47
   * @param position The caret's current position in the text.
48
   * @param position  The caret's current position in the text.
4849
   */
4950
  public RMarkdownCaretInsertionProcessor(
50
    final Processor<String> processor,
51
    final ObservableValue<Integer> position ) {
51
      final Processor<String> processor,
52
      final ObservableValue<Integer> position ) {
5253
    super( processor, position );
5354
  }
5455
5556
  /**
5657
   * Changes the text to insert a "caret" at the caret position. This will
5758
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
5859
   *
5960
   * @param text The text document to process.
60
   *
6161
   * @return The text with the caret position token inserted at the caret
6262
   * position.
...
102102
      // insertion point.
103103
      final boolean between = isBetween( offset, rPrefix, rSuffix );
104
      
104
105105
      // Insert the caret marker at the start of the R statement.
106106
      if( between ) {
M src/main/java/com/scrivenvar/processors/RVariableProcessor.java
4141
4242
  public RVariableProcessor(
43
    final Processor<String> rp, final Map<String, String> map ) {
43
      final Processor<String> rp, final Map<String, String> map ) {
4444
    super( rp, map );
4545
  }
...
5959
   *
6060
   * @param map Map of variable names to values.
61
   *
62
   * @return
61
   * @return Map of R variables.
6362
   */
6463
  private Map<String, String> toR( final Map<String, String> map ) {
...
7776
   *
7877
   * @param key The variable name to transform, can be empty but not null.
79
   *
8078
   * @return The transformed variable name.
8179
   */
...
105103
  /**
106104
   * TODO: Make generic method for replacing text.
107
   * 
108
   * @see CaretReplacementProcessor.replace
109105
   *
110106
   * @param haystack Search this string for the needle, must not be null.
111
   * @param needle The character to find in the haystack.
112
   * @param thread Replace the needle with this text, if the needle is found.
113
   *
107
   * @param needle   The character to find in the haystack.
108
   * @param thread   Replace the needle with this text, if the needle is found.
114109
   * @return The haystack with the all instances of needle replaced with thread.
115110
   */
111
  @SuppressWarnings("SameParameterValue")
116112
  private String escape(
117
    final String haystack, final char needle, final String thread ) {
113
      final String haystack, final char needle, final String thread ) {
118114
    int end = haystack.indexOf( needle );
119115
...
129125
130126
    while( end >= 0 ) {
131
      sb.append( haystack.substring( start, end ) ).append( thread );
127
      sb.append( haystack, start, end ).append( thread );
132128
      start = end + 1;
133129
      end = haystack.indexOf( needle, start );
M src/main/java/com/scrivenvar/processors/XMLProcessor.java
3030
import com.scrivenvar.Services;
3131
import com.scrivenvar.service.Snitch;
32
import java.io.File;
33
import java.io.IOException;
34
import java.io.Reader;
35
import java.io.StringReader;
36
import java.io.StringWriter;
37
import java.nio.file.Path;
38
import java.nio.file.Paths;
39
import java.text.ParseException;
32
import net.sf.saxon.TransformerFactoryImpl;
33
import net.sf.saxon.trans.XPathException;
34
4035
import javax.xml.stream.XMLEventReader;
4136
import javax.xml.stream.XMLInputFactory;
4237
import javax.xml.stream.XMLStreamException;
4338
import javax.xml.stream.events.ProcessingInstruction;
4439
import javax.xml.stream.events.XMLEvent;
45
import javax.xml.transform.ErrorListener;
46
import javax.xml.transform.Source;
47
import javax.xml.transform.Transformer;
48
import javax.xml.transform.TransformerConfigurationException;
49
import javax.xml.transform.TransformerException;
50
import javax.xml.transform.TransformerFactory;
40
import javax.xml.transform.*;
5141
import javax.xml.transform.stream.StreamResult;
5242
import javax.xml.transform.stream.StreamSource;
53
import net.sf.saxon.TransformerFactoryImpl;
43
import java.io.File;
44
import java.io.Reader;
45
import java.io.StringReader;
46
import java.io.StringWriter;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
5450
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
5551
5652
/**
5753
 * Transforms an XML document. The XML document must have a stylesheet specified
5854
 * as part of its processing instructions, such as:
5955
 *
6056
 * <code>xml-stylesheet type="text/xsl" href="markdown.xsl"</code>
61
 *
57
 * <p>
6258
 * The XSL must transform the XML document into Markdown, or another format
6359
 * recognized by the next link on the chain.
6460
 *
6561
 * @author White Magic Software, Ltd.
6662
 */
6763
public class XMLProcessor extends AbstractProcessor<String>
68
  implements ErrorListener {
64
    implements ErrorListener {
6965
7066
  private final Snitch snitch = Services.load( Snitch.class );
...
8379
   *
8480
   * @param processor Next link in the processing chain.
85
   * @param path The path to the XML file content to be processed.
81
   * @param path      The path to the XML file content to be processed.
8682
   */
8783
  public XMLProcessor( final Processor<String> processor, final Path path ) {
...
9490
   *
9591
   * @param text The text to transform, can be empty, cannot be null.
96
   *
9792
   * @return The transformed text, or empty if text is empty.
9893
   */
...
112107
   *
113108
   * @param text The text to transform.
114
   *
115109
   * @return The transformed text.
116110
   */
117111
  private String transform( final String text ) throws Exception {
118112
    // Extract the XML stylesheet processing instruction.
119113
    final String template = getXsltFilename( text );
120114
    final Path xsl = getXslPath( template );
121115
122116
    try(
123
      final StringWriter output = new StringWriter( text.length() );
124
      final StringReader input = new StringReader( text ) ) {
117
        final StringWriter output = new StringWriter( text.length() );
118
        final StringReader input = new StringReader( text ) ) {
125119
126120
      // Listen for external file modification events.
127121
      getSnitch().listen( xsl );
128122
129123
      getTransformer( xsl ).transform(
130
        new StreamSource( input ),
131
        new StreamResult( output )
124
          new StreamSource( input ),
125
          new StreamResult( output )
132126
      );
133127
...
142136
   *
143137
   * @param xsl The path to an XSLT file.
144
   *
145138
   * @return A transformer that will transform XML documents using the given
146139
   * XSLT file.
147
   *
148140
   * @throws TransformerConfigurationException Could not instantiate the
149
   * transformer.
141
   *                                           transformer.
150142
   */
151143
  private Transformer getTransformer( final Path xsl )
152
    throws TransformerConfigurationException, IOException {
144
      throws TransformerConfigurationException {
153145
    if( this.transformer == null ) {
154146
      this.transformer = createTransformer( xsl );
...
162154
   *
163155
   * @param xsl The stylesheet to use for transforming XML documents.
164
   *
165156
   * @return The edited XML document transformed into another format (usually
166157
   * markdown).
167
   *
168158
   * @throws TransformerConfigurationException Could not create the transformer.
169159
   */
170160
  protected Transformer createTransformer( final Path xsl )
171
    throws TransformerConfigurationException {
161
      throws TransformerConfigurationException {
172162
    final Source xslt = new StreamSource( xsl.toFile() );
173163
...
188178
   *
189179
   * @param xml The XML containing an xml-stylesheet processing instruction.
190
   *
191180
   * @return The href pseudo-attribute value.
192
   *
193181
   * @throws XMLStreamException Could not parse the XML file.
194
   * @throws ParseException Could not find a non-empty HREF attribute value.
195182
   */
196183
  private String getXsltFilename( final String xml )
197
    throws XMLStreamException, ParseException {
184
      throws XMLStreamException, XPathException {
198185
199186
    String result = "";
...
210197
211198
        if( event.isProcessingInstruction() ) {
212
          final ProcessingInstruction pi = (ProcessingInstruction)event;
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
213200
          final String target = pi.getTarget();
214201
...
225212
226213
  private XMLEventReader createXMLEventReader( final Reader reader )
227
    throws XMLStreamException {
214
      throws XMLStreamException {
228215
    return getXMLInputFactory().createXMLEventReader( reader );
229216
  }
M src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
4040
4141
  protected String[] keys( final Map<String, String> map ) {
42
    return map.keySet().toArray( new String[ map.size() ] );
42
    return map.keySet().toArray( new String[ 0 ] );
4343
  }
4444
4545
  protected String[] values( final Map<String, String> map ) {
46
    return map.values().toArray( new String[ map.size() ] );
46
    return map.values().toArray( new String[ 0 ] );
4747
  }
4848
}
M src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
6363
    // Replace all instances with dereferenced variables.
6464
    for( final Emit emit : builder.build().parseText( text ) ) {
65
      sb.append( text.substring( index, emit.getStart() ) );
65
      sb.append( text, index, emit.getStart() );
6666
      sb.append( map.get( emit.getKeyword() ) );
6767
      index = emit.getEnd() + 1;
M src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
2929
3030
import java.util.Map;
31
3132
import static org.apache.commons.lang3.StringUtils.replaceEach;
3233
M src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
5353
    // performant than the Aho-Corsick implementation.
5454
    //
55
    // Ssee http://stackoverflow.com/a/40836618/59087
55
    // See http://stackoverflow.com/a/40836618/59087
5656
    return length < 1500 ? APACHE : AHO_CORASICK;
5757
  }
M src/main/java/com/scrivenvar/processors/text/TextReplacer.java
4343
   *
4444
   * @param text The text that contains zero or more keys.
45
   * @param map The set of keys mapped to replacement values.
46
   *
45
   * @param map  The set of keys mapped to replacement values.
4746
   * @return The given text with all keys replaced with corresponding values.
4847
   */
49
  public String replace( String text, Map<String, String> map );
48
  String replace( String text, Map<String, String> map );
5049
}
5150
M src/main/java/com/scrivenvar/service/Options.java
3838
public interface Options extends Service {
3939
40
  public Preferences getState();
40
  Preferences getState();
4141
4242
  /**
4343
   * Stores the key and value into the user preferences to be loaded the next
4444
   * time the application is launched.
4545
   *
46
   * @param key Name of the key to persist along with its value.
46
   * @param key   Name of the key to persist along with its value.
4747
   * @param value Value to associate with the key.
48
   *
4948
   * @throws BackingStoreException Could not persist the change.
5049
   */
51
  public void put( String key, String value ) throws BackingStoreException;
50
  void put( String key, String value ) throws BackingStoreException;
5251
5352
  /**
5453
   * Retrieves the value for a key in the user preferences.
5554
   *
56
   * @param key Retrieve the value of this key.
55
   * @param key          Retrieve the value of this key.
5756
   * @param defaultValue The value to return in the event that the given key has
58
   * no associated value.
59
   *
57
   *                     no associated value.
6058
   * @return The value associated with the key.
6159
   */
62
  public String get( String key, String defaultValue );
63
  
60
  String get( String key, String defaultValue );
61
6462
  /**
6563
   * Retrieves the value for a key in the user preferences. This will return
6664
   * the empty string if the value cannot be found.
67
   * 
65
   *
6866
   * @param key The key to find in the preferences.
6967
   * @return A non-null, possibly empty value for the key.
7068
   */
71
  public String get( String key );
69
  String get( String key );
7270
}
7371
M src/main/java/com/scrivenvar/service/Settings.java
4141
   * Returns a setting property or its default value.
4242
   *
43
   * @param property The property key name to obtain its value.
43
   * @param property     The property key name to obtain its value.
4444
   * @param defaultValue The default value to return iff the property cannot be
45
   * found.
46
   *
45
   *                     found.
4746
   * @return The property value for the given property key.
4847
   */
49
  public String getSetting( String property, String defaultValue );
48
  String getSetting( String property, String defaultValue );
5049
5150
  /**
5251
   * Returns a setting property or its default value.
5352
   *
54
   * @param property The property key name to obtain its value.
53
   * @param property     The property key name to obtain its value.
5554
   * @param defaultValue The default value to return iff the property cannot be
56
   * found.
57
   *
55
   *                     found.
5856
   * @return The property value for the given property key.
5957
   */
60
  public int getSetting( String property, int defaultValue );
58
  int getSetting( String property, int defaultValue );
6159
6260
  /**
...
6967
   *
7068
   * @param prefix The prefix to compare against each property name.
71
   *
7269
   * @return The list of property names that have the given prefix.
7370
   */
74
  public Iterator<String> getKeys( final String prefix );
71
  Iterator<String> getKeys( final String prefix );
7572
7673
  /**
7774
   * Convert the generic list of property objects into strings.
7875
   *
7976
   * @param property The property value to coerce.
8077
   * @param defaults The defaults values to use should the property be unset.
81
   *
8278
   * @return The list of properties coerced from objects to strings.
8379
   */
84
  public List<String> getStringSettingList( String property, List<String> defaults );
80
  List<String> getStringSettingList( String property, List<String> defaults );
8581
8682
  /**
8783
   * Converts the generic list of property objects into strings.
8884
   *
8985
   * @param property The property value to coerce.
90
   *
9186
   * @return The list of properties coerced from objects to strings.
9287
   */
93
  public List<String> getStringSettingList( String property );
88
  List<String> getStringSettingList( String property );
9489
9590
  /**
9691
   * Changes key's value. This will clear the old value before setting the
9792
   * new value so that the old value is erased, not changed into a list.
9893
   *
99
   * @param key The property key name to obtain its value.
94
   * @param key   The property key name to obtain its value.
10095
   * @param value The new value to set.
10196
   */
102
  public void putSetting( String key, String value );
97
  void putSetting( String key, String value );
10398
}
10499
M src/main/java/com/scrivenvar/service/Snitch.java
4545
   *
4646
   * @param o The object to receive changed events for when monitored files
47
   * are changed.
47
   *          are changed.
4848
   */
49
  public void addObserver( Observer o );
49
  void addObserver( Observer o );
5050
5151
  /**
5252
   * Listens for changes to the path. If the path specifies a file, then only
5353
   * notifications pertaining to that file are sent. Otherwise, change events
5454
   * for the directory that contains the file are sent. This method must allow
5555
   * for multiple calls to the same file without incurring additional listeners
5656
   * or events.
5757
   *
5858
   * @param file Send notifications when this file changes, can be null.
59
   *
6059
   * @throws IOException Couldn't create a watcher for the given file.
6160
   */
62
  public void listen( Path file ) throws IOException;
61
  void listen( Path file ) throws IOException;
6362
6463
  /**
6564
   * Removes the given file from the notifications list.
6665
   *
6766
   * @param file The file to stop monitoring for any changes, can be null.
6867
   */
69
  public void ignore( Path file );
68
  void ignore( Path file );
7069
7170
  /**
7271
   * Stop listening for events.
7372
   */
74
  public void stop();
73
  void stop();
7574
}
7675
M src/main/java/com/scrivenvar/service/events/Notification.java
4040
   * @return A non-null string to use as alert message title.
4141
   */
42
  public String getTitle();
42
  String getTitle();
4343
4444
  /**
4545
   * Alert message content.
4646
   *
4747
   * @return A non-null string that contains information for the user.
4848
   */
49
  public String getContent();
49
  String getContent();
5050
}
5151
M src/main/java/com/scrivenvar/service/events/Notifier.java
2828
package com.scrivenvar.service.events;
2929
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
3034
import java.io.File;
3135
import java.io.FileWriter;
3236
import java.io.IOException;
3337
import java.io.PrintWriter;
3438
import java.util.Observer;
35
import javafx.scene.control.Alert;
36
import javafx.scene.control.ButtonType;
37
import javafx.stage.Window;
3839
3940
/**
4041
 * Provides the application with a uniform way to notify the user of events.
4142
 *
4243
 * @author White Magic Software, Ltd.
4344
 */
4445
public interface Notifier {
4546
46
  public static final ButtonType YES = ButtonType.YES;
47
  public static final ButtonType NO = ButtonType.NO;
48
  public static final ButtonType CANCEL = ButtonType.CANCEL;
47
  ButtonType YES = ButtonType.YES;
48
  ButtonType NO = ButtonType.NO;
49
  ButtonType CANCEL = ButtonType.CANCEL;
4950
5051
  /**
5152
   * Notifies the user of a problem.
5253
   *
5354
   * @param message The problem description.
5455
   */
55
  public void notify( final String message );
56
  void notify( final String message );
5657
5758
  /**
5859
   * Notifies the user about the exception.
5960
   *
6061
   * @param ex The exception containing a message to show to the user.
6162
   */
62
  default public void notify( final Exception ex ) {
63
  default void notify( final Exception ex ) {
64
    assert ex != null;
65
6366
    log( ex );
6467
    notify( ex.getMessage() );
6568
  }
66
  
69
6770
  /**
6871
   * Writes the exception to a log file. The log file should be written
6972
   * in the System's temporary directory.
70
   * 
73
   *
7174
   * @param ex The exception to show in the status bar and log to a file.
7275
   */
73
  default public void log( final Exception ex ) {
74
    try (
75
      final FileWriter fw = new FileWriter( getLogPath(), true );
76
      final PrintWriter pw = new PrintWriter( fw )
77
      ) {
78
76
  default void log( final Exception ex ) {
77
    try(
78
        final FileWriter fw = new FileWriter( getLogPath(), true );
79
        final PrintWriter pw = new PrintWriter( fw )
80
    ) {
7981
      ex.printStackTrace( pw );
80
    } catch (final IOException ioe) {
82
    } catch( final IOException ioe ) {
8183
      // The notify method will display the message on the status
8284
      // bar.
8385
    }
8486
  }
85
  
87
8688
  /**
8789
   * Returns the fully qualified path to the log file to write to when
8890
   * an exception occurs.
89
   * 
91
   *
9092
   * @return Location of the log file for writing unexpected exceptions.
9193
   */
92
  public File getLogPath();
94
  File getLogPath();
9395
9496
  /**
9597
   * Causes any displayed notifications to disappear.
9698
   */
97
  public void clear();
99
  void clear();
98100
99101
  /**
100102
   * Constructs a default alert message text for a modal alert dialog.
101103
   *
102
   * @param title The dialog box message title.
104
   * @param title   The dialog box message title.
103105
   * @param message The dialog box message content (needs formatting).
104
   * @param args The arguments to the message content that must be formatted.
105
   *
106
   * @param args    The arguments to the message content that must be formatted.
106107
   * @return The message suitable for building a modal alert dialog.
107108
   */
108
  public Notification createNotification(
109
    String title,
110
    String message,
111
    Object... args );
109
  Notification createNotification(
110
      String title,
111
      String message,
112
      Object... args );
112113
113114
  /**
114115
   * Creates an alert of alert type error with a message showing the cause of
115116
   * the error.
116117
   *
117
   * @param parent Dialog box owner (for modal purposes).
118
   * @param parent  Dialog box owner (for modal purposes).
118119
   * @param message The error message, title, and possibly more details.
119
   *
120120
   * @return A modal alert dialog box ready to display using showAndWait.
121121
   */
122
  public Alert createError( Window parent, Notification message );
122
  Alert createError( Window parent, Notification message );
123123
124124
  /**
125125
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
126126
   *
127
   * @param parent Dialog box owner (for modal purposes).
127
   * @param parent  Dialog box owner (for modal purposes).
128128
   * @param message The message, title, and possibly more details.
129
   *
130129
   * @return A modal alert dialog box ready to display using showAndWait.
131130
   */
132
  public Alert createConfirmation( Window parent, Notification message );
131
  Alert createConfirmation( Window parent, Notification message );
133132
134133
  /**
135134
   * Adds an observer to the list of objects that receive notifications about
136135
   * error messages to be presented to the user.
137136
   *
138137
   * @param observer The observer instance to notify.
139138
   */
140
  public void addObserver( Observer observer );
139
  void addObserver( Observer observer );
141140
142141
  /**
143142
   * Removes an observer from the list of objects that receive notifications
144143
   * about error messages to be presented to the user.
145144
   *
146145
   * @param observer The observer instance to no longer notify.
147146
   */
148
  public void deleteObserver( Observer observer );
147
  void deleteObserver( Observer observer );
149148
}
150149
M src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
5454
  }
5555
56
  @SuppressWarnings("SameParameterValue")
5657
  private String getSetting( final String key, final String defaultValue ) {
5758
    return getSettings().getSetting( key, defaultValue );
M src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
2929
3030
import com.scrivenvar.service.events.Notification;
31
3132
import java.text.MessageFormat;
3233
3334
/**
34
 *
3535
 * @author White Magic Software, Ltd.
3636
 */
3737
public class DefaultNotification implements Notification {
3838
3939
  private final String title;
4040
  private final String content;
4141
4242
  /**
4343
   * Constructs default message text for a notification.
44
   * 
45
   * @param title The message title.
44
   *
45
   * @param title   The message title.
4646
   * @param message The message content (needs formatting).
47
   * @param args The arguments to the message content that must be formatted.
47
   * @param args    The arguments to the message content that must be formatted.
4848
   */
4949
  public DefaultNotification(
50
    final String title,
51
    final String message,
52
    final Object... args ) {
50
      final String title,
51
      final String message,
52
      final Object... args ) {
5353
    this.title = title;
5454
    this.content = MessageFormat.format( message, args );
M src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
2828
package com.scrivenvar.service.events.impl;
2929
30
import static com.scrivenvar.Constants.APP_TITLE;
31
import static com.scrivenvar.Constants.STATUS_BAR_DEFAULT;
3230
import com.scrivenvar.service.events.Notification;
3331
import com.scrivenvar.service.events.Notifier;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import javafx.stage.Window;
35
3436
import java.io.File;
3537
import java.nio.file.Paths;
3638
import java.util.Observable;
37
import javafx.scene.control.Alert;
38
import javafx.scene.control.Alert.AlertType;
39
40
import static com.scrivenvar.Constants.APP_TITLE;
41
import static com.scrivenvar.Constants.STATUS_BAR_DEFAULT;
3942
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
4043
import static javafx.scene.control.Alert.AlertType.ERROR;
41
import javafx.stage.Window;
4244
4345
/**
...
5860
  @Override
5961
  public void notify( final String message ) {
60
    setChanged();
61
    notifyObservers( message );
62
    if( message != null && !message.isBlank() ) {
63
      setChanged();
64
      notifyObservers( message );
65
    }
6266
  }
6367
...
7074
   * Contains all the information that the user needs to know about a problem.
7175
   *
72
   * @param title The context for the message.
76
   * @param title   The context for the message.
7377
   * @param message The message content (formatted with the given args).
74
   * @param args Parameters for the message content.
75
   *
78
   * @param args    Parameters for the message content.
7679
   * @return A notification instance, never null.
7780
   */
7881
  @Override
7982
  public Notification createNotification(
80
    final String title,
81
    final String message,
82
    final Object... args ) {
83
      final String title,
84
      final String message,
85
      final Object... args ) {
8386
    return new DefaultNotification( title, message, args );
8487
  }
8588
8689
  private Alert createAlertDialog(
87
    final Window parent,
88
    final AlertType alertType,
89
    final Notification message ) {
90
      final Window parent,
91
      final AlertType alertType,
92
      final Notification message ) {
9093
9194
    final Alert alert = new Alert( alertType );
...
101104
102105
  @Override
103
  public Alert createConfirmation( final Window parent, final Notification message ) {
106
  public Alert createConfirmation( final Window parent,
107
                                   final Notification message ) {
104108
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
105109
...
117121
  public File getLogPath() {
118122
    return Paths.get(
119
      System.getProperty("java.io.tmpdir"), APP_TITLE + ".log").toFile();
123
        System.getProperty( "java.io.tmpdir" ), APP_TITLE + ".log" ).toFile();
120124
  }
121125
}
D src/main/java/com/scrivenvar/service/events/impl/FileType.java
1
/*
2
 * To change this license header, choose License Headers in Project Properties.
3
 * To change this template file, choose Tools | Templates
4
 * and open the template in the editor.
5
 */
6
package com.scrivenvar.service.events.impl;
7
8
/**
9
 * Lists known file types for creating document processors via the factory.
10
 *
11
 * @author White Magic Software, Ltd.
12
 */
13
public enum FileType {
14
  MARKDOWN("md", "markdown", "mkdown", "mdown", "mkdn", "mkd", "mdwn", "mdtxt", "mdtext", "text", "txt"),
15
  R_MARKDOWN("Rmd"),
16
  XML("xml");
17
18
  private final String[] extensions;
19
20
  private FileType(final String... extensions) {
21
    this.extensions = extensions;
22
  }
23
24
  /**
25
   * Returns true if the given file type aligns with the extension for this
26
   * enumeration.
27
   *
28
   * @param filetype The file extension to compare against the internal list.
29
   * @return true The given filetype equals (case insensitive) the internal
30
   * type.
31
   */
32
  public boolean isType(final String filetype) {
33
    boolean result = false;
34
35
    for (final String extension : this.extensions) {
36
      if (extension.equalsIgnoreCase(filetype)) {
37
        result = true;
38
        break;
39
      }
40
    }
41
42
    return result;
43
  }
44
}
451
M src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
3030
import static com.scrivenvar.Constants.PREFS_ROOT;
3131
import static com.scrivenvar.Constants.PREFS_STATE;
32
3233
import com.scrivenvar.service.Options;
34
3335
import java.util.prefs.BackingStoreException;
3436
import java.util.prefs.Preferences;
37
3538
import static java.util.prefs.Preferences.userRoot;
3639
3740
/**
3841
 * Persistent options user can change at runtime.
3942
 *
4043
 * @author Karl Tauber and White Magic Software, Ltd.
4144
 */
4245
public class DefaultOptions implements Options {
4346
44
  private Preferences preferences;
47
  private Preferences mPreferences;
4548
4649
  public DefaultOptions() {
4750
    setPreferences( getRootPreferences().node( PREFS_OPTIONS ) );
4851
  }
4952
5053
  /**
5154
   * This will throw IllegalArgumentException if the value exceeds the maximum
5255
   * preferences value length.
5356
   *
54
   * @param key The name of the key to associate with the value.
57
   * @param key   The name of the key to associate with the value.
5558
   * @param value The value to persist.
56
   *
5759
   * @throws BackingStoreException New value not persisted.
5860
   */
5961
  @Override
6062
  public void put( final String key, final String value )
61
    throws BackingStoreException {
63
      throws BackingStoreException {
6264
    getState().put( key, value );
6365
    getState().flush();
...
7577
7678
  private void setPreferences( final Preferences preferences ) {
77
    this.preferences = preferences;
79
    mPreferences = preferences;
7880
  }
7981
8082
  private Preferences getRootPreferences() {
8183
    return userRoot().node( PREFS_ROOT );
8284
  }
8385
8486
  @Override
8587
  public Preferences getState() {
8688
    return getRootPreferences().node( PREFS_STATE );
87
  }
88
89
  private Preferences getPreferences() {
90
    return this.preferences;
9189
  }
9290
}
M src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
2828
package com.scrivenvar.service.impl;
2929
30
import static com.scrivenvar.Constants.SETTINGS_NAME;
3130
import com.scrivenvar.service.Settings;
31
import org.apache.commons.configuration2.PropertiesConfiguration;
32
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
33
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
34
import org.apache.commons.configuration2.ex.ConfigurationException;
35
3236
import java.io.IOException;
3337
import java.io.InputStreamReader;
3438
import java.io.Reader;
3539
import java.net.URISyntaxException;
3640
import java.net.URL;
3741
import java.nio.charset.Charset;
3842
import java.util.Iterator;
3943
import java.util.List;
40
import org.apache.commons.configuration2.PropertiesConfiguration;
41
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
42
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
43
import org.apache.commons.configuration2.ex.ConfigurationException;
44
45
import static com.scrivenvar.Constants.SETTINGS_NAME;
4446
4547
/**
...
5557
5658
  public DefaultSettings()
57
    throws ConfigurationException, URISyntaxException, IOException {
59
      throws ConfigurationException, URISyntaxException, IOException {
5860
    setProperties( createProperties() );
5961
  }
6062
6163
  /**
6264
   * Returns the value of a string property.
6365
   *
64
   * @param property The property key.
66
   * @param property     The property key.
6567
   * @param defaultValue The value to return if no property key has been set.
66
   *
6768
   * @return The property key value, or defaultValue when no key found.
6869
   */
...
7576
   * Returns the value of a string property.
7677
   *
77
   * @param property The property key.
78
   * @param property     The property key.
7879
   * @param defaultValue The value to return if no property key has been set.
79
   *
8080
   * @return The property key value, or defaultValue when no key found.
8181
   */
...
8989
   * value so that the old value is erased, not changed into a list.
9090
   *
91
   * @param key The property key name to obtain its value.
91
   * @param key   The property key name to obtain its value.
9292
   * @param value The new value to set.
9393
   */
...
103103
   * @param property The property value to coerce.
104104
   * @param defaults The defaults values to use should the property be unset.
105
   *
106105
   * @return The list of properties coerced from objects to strings.
107106
   */
108107
  @Override
109108
  public List<String> getStringSettingList(
110
    final String property, final List<String> defaults ) {
109
      final String property, final List<String> defaults ) {
111110
    return getSettings().getList( String.class, property, defaults );
112111
  }
113112
114113
  /**
115114
   * Convert a list of property objects into strings, with no default value.
116115
   *
117116
   * @param property The property value to coerce.
118
   *
119117
   * @return The list of properties coerced from objects to strings.
120118
   */
...
128126
   *
129127
   * @param prefix The prefix to compare against each property name.
130
   *
131128
   * @return The list of property names that have the given prefix.
132129
   */
133130
  @Override
134131
  public Iterator<String> getKeys( final String prefix ) {
135132
    return getSettings().getKeys( prefix );
136133
  }
137134
138135
  private PropertiesConfiguration createProperties()
139
    throws ConfigurationException {
136
      throws ConfigurationException {
140137
141138
    final URL url = getPropertySource();
142139
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
143140
144141
    if( url != null ) {
145
      try( final Reader r = new InputStreamReader( url.openStream(), getDefaultEncoding() ) ) {
142
      try( final Reader r = new InputStreamReader( url.openStream(),
143
                                                   getDefaultEncoding() ) ) {
146144
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
147145
        configuration.read( r );
M src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
2828
package com.scrivenvar.service.impl;
2929
30
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
3130
import com.scrivenvar.service.Snitch;
31
3232
import java.io.IOException;
33
import java.nio.file.FileSystem;
34
import java.nio.file.FileSystems;
35
import java.nio.file.Files;
36
import java.nio.file.Path;
37
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
38
import java.nio.file.WatchEvent;
39
import java.nio.file.WatchKey;
40
import java.nio.file.WatchService;
33
import java.nio.file.*;
4134
import java.util.Collections;
4235
import java.util.Map;
4336
import java.util.Observable;
4437
import java.util.Set;
4538
import java.util.concurrent.ConcurrentHashMap;
39
40
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
41
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
4642
4743
/**
...
8682
   *
8783
   * @param file Path to a file to watch for changes.
88
   *
8984
   * @throws IOException The file could not be monitored.
9085
   */
...
106101
   *
107102
   * @param path The file to return as a directory, which should always be the
108
   * case.
109
   *
103
   *             case.
110104
   * @return The given path as a directory, if a file, otherwise the path
111105
   * itself.
112106
   */
113107
  private Path toDirectory( final Path path ) {
114108
    return Files.isDirectory( path )
115
      ? path
116
      : path.toFile().getParentFile().toPath();
109
        ? path
110
        : path.toFile().getParentFile().toPath();
117111
  }
118112
...
139133
   */
140134
  @Override
141
  @SuppressWarnings( "SleepWhileInLoop" )
135
  @SuppressWarnings("BusyWait")
142136
  public void run() {
143137
    setListening( true );
...
154148
155149
        for( final WatchEvent<?> event : key.pollEvents() ) {
156
          final Path changed = path.resolve( (Path)event.context() );
150
          final Path changed = path.resolve( (Path) event.context() );
157151
158152
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
...
177171
   *
178172
   * @param file Path to a system file.
179
   *
180173
   * @return true The given file is being monitored for changes.
181174
   */
...
188181
   *
189182
   * @param key The key to lookup its corresponding path.
190
   *
191183
   * @return The path for the given key.
192184
   */
...
228220
   *
229221
   * @return A valid WatchService instance, never null.
230
   *
231222
   * @throws IOException Could not create a new watch service.
232223
   */
D src/main/java/com/scrivenvar/test/TestProperties.java
1
package com.scrivenvar.test;
2
3
import java.io.IOException;
4
import java.io.StringReader;
5
import java.util.Arrays;
6
import org.apache.commons.configuration2.PropertiesConfiguration;
7
import org.apache.commons.configuration2.ex.ConfigurationException;
8
9
public class TestProperties {
10
11
  public static void main( final String args[] ) throws ConfigurationException, IOException {
12
    final String p = ""
13
      + "file.ext.definition.yaml=*.yml,*.yaml\n"
14
      + "filter.file.ext.definition=${file.ext.definition.yaml}\n";
15
16
    try( final StringReader r = new StringReader( p ) ) {
17
18
      PropertiesConfiguration config = new PropertiesConfiguration();
19
      config.read( r );
20
21
      System.out.println( config.getList( "filter.file.ext.definition" ) );
22
      System.out.println( config.getString( "filter.file.ext.definition" ) );
23
      System.out.println( Arrays.toString( config.getStringArray( "filter.file.ext.definition" ) ) );
24
    }
25
  }
26
}
271
D src/main/java/com/scrivenvar/util/Item.java
1
/*
2
 * Copyright (c) 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.scrivenvar.util;
28
29
/**
30
 * Simple item for a ChoiceBox, ComboBox or ListView. Consists of a string name
31
 * and a value object. toString() returns the name. equals() compares the value
32
 * and hashCode() returns the hash code of the value.
33
 *
34
 * @author Karl Tauber
35
 * @param <V> The type of item value.
36
 */
37
public class Item<V> {
38
39
  public final String name;
40
  public final V value;
41
42
  public Item( final String name, final V value ) {
43
    this.name = name;
44
    this.value = value;
45
  }
46
47
  @Override
48
  public boolean equals( final Object obj ) {
49
    if( this == obj ) {
50
      return true;
51
    }
52
    if( !(obj instanceof Item) ) {
53
      return false;
54
    }
55
    return Utils.safeEquals( value, ((Item<?>)obj).value );
56
  }
57
58
  @Override
59
  public int hashCode() {
60
    return (value != null) ? value.hashCode() : 0;
61
  }
62
63
  @Override
64
  public String toString() {
65
    return name;
66
  }
67
}
681
M src/main/java/com/scrivenvar/util/Utils.java
3131
3232
/**
33
 * @author Karl Tauber
33
 * @author Karl Tauber and White Magic Software, Ltd.
3434
 */
3535
public class Utils {
36
37
  public static boolean safeEquals( final Object o1, final Object o2 ) {
38
    if( o1 == o2 ) {
39
      return true;
40
    }
41
    if( o1 == null || o2 == null ) {
42
      return false;
43
    }
44
    return o1.equals( o2 );
45
  }
46
47
  public static boolean isNullOrEmpty( final String s ) {
48
    return s == null || s.isEmpty();
49
  }
5036
5137
  public static String ltrim( final String s ) {
...
6753
6854
    return s.substring( 0, i + 1 );
69
  }
70
71
  public static void putPrefs( Preferences prefs, String key, String value, String def ) {
72
    if( value != def && !value.equals( def ) ) {
73
      prefs.put( key, value );
74
    } else {
75
      prefs.remove( key );
76
    }
77
  }
78
79
  public static void putPrefsInt( Preferences prefs, String key, int value, int def ) {
80
    if( value != def ) {
81
      prefs.putInt( key, value );
82
    } else {
83
      prefs.remove( key );
84
    }
85
  }
86
87
  public static void putPrefsBoolean( Preferences prefs, String key, boolean value, boolean def ) {
88
    if( value != def ) {
89
      prefs.putBoolean( key, value );
90
    } else {
91
      prefs.remove( key );
92
    }
9355
  }
9456
95
  public static String[] getPrefsStrings( final Preferences prefs, String key ) {
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
9659
    final ArrayList<String> arr = new ArrayList<>( 256 );
9760
...
10669
    }
10770
108
    return arr.toArray( new String[ arr.size() ] );
71
    return arr.toArray( new String[ 0 ] );
10972
  }
11073
111
  public static void putPrefsStrings( Preferences prefs, String key, String[] strings ) {
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
11276
    for( int i = 0; i < strings.length; i++ ) {
11377
      prefs.put( key + (i + 1), strings[ i ] );
11478
    }
11579
116
    for( int i = strings.length; prefs.get( key + (i + 1), null ) != null; i++ ) {
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
11782
      prefs.remove( key + (i + 1) );
11883
    }
M src/main/resources/com/scrivenvar/messages.properties
8383
Main.menu.insert.ordered_list=Ordered List
8484
Main.menu.insert.horizontal_rule=Horizontal Rule
85
86
Main.menu.tools=_Tools
87
Main.menu.tools.script=_R Script
85
Main.menu.r=_R
86
Main.menu.r.script=_Script
87
Main.menu.r.directory=_Directory
8888
8989
Main.menu.help=_Help
...
209209
Dialog.about.title=About
210210
Dialog.about.header=${Main.title}
211
Dialog.about.content=Copyright 2017 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
211
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
212
213
# R ################################################################
212214
213215
# ########################################################################
214216
#
215217
# R Script
216218
#
217219
# ########################################################################
218220
219
Dialog.rScript.title=R Startup Script
220
Dialog.rScript.content=Provide R statements to run prior to interpreting R statements embedded in the document.
221
Dialog.r.script.title=R Startup Script
222
Dialog.r.script.content=Provide R statements to run prior to interpreting R statements embedded in the document.
223
224
# ########################################################################
225
#
226
# R Directory
227
#
228
# ########################################################################
229
230
Dialog.r.directory.title=Bootstrap Working Directory
231
Dialog.r.directory.header=Value for $application.r.working.directory$.
221232
222233
# Options ################################################################