Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
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/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" />
5
      <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" />
156
    </list>
167
    <option name="SHOW_DIALOG" value="false" />
...
7263
  <component name="PropertiesComponent">
7364
    <property name="ASKED_ADD_EXTERNAL_FILES" value="true" />
65
    <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
66
    <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
7467
    <property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
7568
    <property name="com.android.tools.idea.instantapp.provision.ProvisionBeforeRunTaskProvider.myTimeStamp" value="1541653415064" />
...
173166
    </state>
174167
    <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">
168
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1590855861709">
176169
      <screen x="0" y="28" width="2560" height="1529" />
177170
    </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" />
171
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1590855861709" />
179172
    <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl" timestamp="1589659107517">
180173
      <screen x="0" y="28" width="2560" height="1529" />
181174
    </state>
182175
    <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">
176
    <state width="1573" height="321" key="GridCell.Tab.0.bottom" timestamp="1590863344981">
184177
      <screen x="0" y="28" width="2560" height="1529" />
185178
    </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">
179
    <state width="1573" height="321" key="GridCell.Tab.0.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1590863344981" />
180
    <state width="1573" height="321" key="GridCell.Tab.0.center" timestamp="1590863344980">
188181
      <screen x="0" y="28" width="2560" height="1529" />
189182
    </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">
183
    <state width="1573" height="321" key="GridCell.Tab.0.center/0.28.2560.1529@0.28.2560.1529" timestamp="1590863344980" />
184
    <state width="1573" height="321" key="GridCell.Tab.0.left" timestamp="1590863344980">
192185
      <screen x="0" y="28" width="2560" height="1529" />
193186
    </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">
187
    <state width="1573" height="321" key="GridCell.Tab.0.left/0.28.2560.1529@0.28.2560.1529" timestamp="1590863344980" />
188
    <state width="1573" height="321" key="GridCell.Tab.0.right" timestamp="1590863344981">
196189
      <screen x="0" y="28" width="2560" height="1529" />
197190
    </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">
191
    <state width="1573" height="321" key="GridCell.Tab.0.right/0.28.2560.1529@0.28.2560.1529" timestamp="1590863344981" />
192
    <state width="1573" height="396" key="GridCell.Tab.1.bottom" timestamp="1590858353845">
200193
      <screen x="0" y="28" width="2560" height="1529" />
201194
    </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">
195
    <state width="1573" height="396" key="GridCell.Tab.1.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1590858353845" />
196
    <state width="1573" height="396" key="GridCell.Tab.1.center" timestamp="1590858353845">
204197
      <screen x="0" y="28" width="2560" height="1529" />
205198
    </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">
199
    <state width="1573" height="396" key="GridCell.Tab.1.center/0.28.2560.1529@0.28.2560.1529" timestamp="1590858353845" />
200
    <state width="1573" height="396" key="GridCell.Tab.1.left" timestamp="1590858353845">
208201
      <screen x="0" y="28" width="2560" height="1529" />
209202
    </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">
203
    <state width="1573" height="396" key="GridCell.Tab.1.left/0.28.2560.1529@0.28.2560.1529" timestamp="1590858353845" />
204
    <state width="1573" height="396" key="GridCell.Tab.1.right" timestamp="1590858353845">
212205
      <screen x="0" y="28" width="2560" height="1529" />
213206
    </state>
214
    <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
207
    <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1590858353845" />
215208
    <state x="324" y="288" key="SettingsEditor" timestamp="1589576619807">
216209
      <screen x="0" y="28" width="2560" height="1529" />
217210
    </state>
218211
    <state x="324" y="288" key="SettingsEditor/0.28.2560.1529@0.28.2560.1529" timestamp="1589576619807" />
219212
    <state x="1071" y="397" width="1417" height="979" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1589661186060">
220213
      <screen x="0" y="28" width="2560" height="1529" />
221214
    </state>
222215
    <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">
216
    <state x="714" y="633" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1590804130551">
224217
      <screen x="0" y="28" width="2560" height="1529" />
225218
    </state>
226
    <state x="531" y="261" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589669468040" />
219
    <state x="714" y="633" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.28.2560.1529@0.28.2560.1529" timestamp="1590804130551" />
220
    <state x="531" y="261" width="586" height="753" key="find.popup" timestamp="1590803334947">
221
      <screen x="0" y="28" width="2560" height="1529" />
222
    </state>
223
    <state x="531" y="261" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1590803334947" />
227224
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog" timestamp="1589663937037">
228225
      <screen x="0" y="28" width="2560" height="1529" />
229226
    </state>
230227
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589663937037" />
231228
    <state x="490" y="304" key="run.anything.popup" timestamp="1589657324666">
232229
      <screen x="0" y="28" width="2560" height="1529" />
233230
    </state>
234231
    <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">
232
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup" timestamp="1590858282498">
236233
      <screen x="0" y="28" width="2560" height="1529" />
237234
    </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" />
235
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1590858282498" />
239236
  </component>
240237
  <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>
238
    <watches-manager>
239
      <configuration name="Remote">
240
        <watch expression="getMatcher().matches( ( new File (&quot;./untitled.md&quot;)).toPath() )" language="JAVA" />
241
      </configuration>
242
    </watches-manager>
250243
  </component>
251244
  <component name="masterDetails">
M R/README.md
1
R Functions
2
===
1
# R Functions
32
43
Import the files in this directory into the application, which include:
54
6
* pluralise.R
5
* pluralize.R
76
* possessive.R
87
9
pluralise.R
10
===
8
# pluralize.R
119
1210
This file defines a function that implements most of Damian Conway's [An Algorithmic Approach to English Pluralization](http://blob.perl.org/tpc/1998/User_Applications/Algorithmic%20Approach%20Plurals/Algorithmic_Plurals.html).
1311
14
Usage
15
---
16
Example usages of the pluralise function include:
12
## Usage
1713
18
    `r#pluralise( 'mouse' )` - mice
19
    `r#pluralise( 'buzz' )` - buzzes
20
    `r#pluralise( 'bus' )` - busses
14
Example usages of the pluralize function include:
2115
22
possessive.R
23
===
16
    `r#pluralize( 'mouse' )` - mice
17
    `r#pluralize( 'buzz' )` - buzzes
18
    `r#pluralize( 'bus' )` - busses
19
20
# possessive.R
2421
2522
This file defines a function that applies possessives to English words.
2623
27
Usage
28
---
24
## Usage
25
2926
Example usages of the possessive function include:
3027
3128
    `r#pos( 'Ross' )` - Ross'
3229
    `r#pos( 'Ruby' )` - Ruby's
3330
    `r#pos( 'Lois' )` - Lois'
31
    `r#pos( 'my' )` - mine
32
    `r#pos( 'Your' )` - Yours
3433
3534
D R/pluralise.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
29
#   http://goo.gl/oRL4MP
30
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
31
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
32
# See also: http://www.freevectors.net/assets/files/plural.txt
33
#
34
# ######################################################################
35
36
pluralise <- function( s, n ) {
37
  result <- s
38
39
  # Partial implementation of Conway's algorithm for nouns.
40
  if( n != 1 ) {
41
    if( pl.noninflective( s ) ||
42
        pl.suffix( "es", s ) ||
43
        pl.suffix( "fish", s ) ||
44
        pl.suffix( "ois", s ) ||
45
        pl.suffix( "sheep", s ) ||
46
        pl.suffix( "deer", s ) ||
47
        pl.suffix( "pox", s ) ||
48
        pl.suffix( "[A-Z].*ese", s ) ||
49
        pl.suffix( "itis", s ) ) {
50
      # 1. Retain non-inflective user-mapped noun as is.
51
      # 2. Retain non-inflective plural as is.
52
      result <- s
53
    }
54
    else if( pl.is.irregular.pl( s ) ) {
55
      # 4. Change irregular plurals based on mapping.
56
      result <- pl.irregular.pl( s )
57
    }
58
    else if( pl.is.irregular.es( s ) ) {
59
      # x. From Shevek's
60
      result <- pl.inflect( s, "", "es" )
61
    }
62
    else if( pl.suffix( "man", s ) ) {
63
      # 5. For -man, change -an to -en
64
      result <- pl.inflect( s, "an", "en" )
65
    }
66
    else if( pl.suffix( "[lm]ouse", s ) ) {
67
      # 5. For [lm]ouse, change -ouse to -ice
68
      result <- pl.inflect( s, "ouse", "ice" )
69
    }
70
    else if( pl.suffix( "tooth", s ) ) {
71
      # 5. For -tooth, change -ooth to -eeth
72
      result <- pl.inflect( s, "ooth", "eeth" )
73
    }
74
    else if( pl.suffix( "goose", s ) ) {
75
      # 5. For -goose, change -oose to -eese
76
      result <- pl.inflect( s, "oose", "eese" )
77
    }
78
    else if( pl.suffix( "foot", s ) ) {
79
      # 5. For -foot, change -oot to -eet
80
      result <- pl.inflect( s, "oot", "eet" )
81
    }
82
    else if( pl.suffix( "zoon", s ) ) {
83
      # 5. For -zoon, change -on to -a
84
      result <- pl.inflect( s, "on", "a" )
85
    }
86
    else if( pl.suffix( "[csx]is", s ) ) {
87
      # 5. Change -cis, -sis, -xis to -es
88
      result <- pl.inflect( s, "is", "es" )
89
    }
90
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
91
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
92
      result <- pl.inflect( s, "", "es" )
93
    }
94
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
95
      # 9. Change -f to -ves
96
      result <- pl.inflect( s, "f", "ves" )
97
    }
98
    else if( pl.suffix( "[nlw]ife", s ) ) {
99
      # 10. Change -fe to -ves
100
      result <- pl.inflect( s, "fe", "ves" )
101
    }
102
    else if( pl.suffix( "[aeiou]y", s ) ) {
103
      # 11. Change -[aeiou]y to -ys
104
      result <- pl.inflect( s, "", "s" )
105
    }
106
    else if( pl.suffix( "y", s ) ) {
107
      # 12. Change -y to -ies
108
      result <- pl.inflect( s, "y", "ies" )
109
    }
110
    else if( pl.suffix( "z", s ) ) {
111
      # x. Change -z to -zzes
112
      result <- pl.inflect( s, "", "zes" )
113
    }
114
    else {
115
      # 13. Default plural: add -s
116
      result <- pl.inflect( s, "", "s" )
117
    }
118
  }
119
120
  result
121
}
122
123
# Returns the given string (s) with its suffix replaced by r.
124
pl.inflect <- function( s, suffix, r ) {
125
  gsub( paste( suffix, "$", sep="" ), r, s )
126
}
127
128
# Answers whether the given string (s) has the given ending.
129
pl.suffix <- function( ending, s ) {
130
  grepl( paste( ending, "$", sep="" ), s )
131
}
132
133
# Answers whether the given string (s) is a noninflective noun.
134
pl.noninflective <- function( s ) {
135
  v <- c(
136
    "aircraft",
137
    "Bhutanese",
138
    "bison",
139
    "bream",
140
    "Burmese",
141
    "carp",
142
    "chassis",
143
    "Chinese",
144
    "clippers",
145
    "cod",
146
    "contretemps",
147
    "corps",
148
    "debris",
149
    "djinn",
150
    "eland",
151
    "elk",
152
    "flounder",
153
    "fracas",
154
    "gallows",
155
    "graffiti",
156
    "headquarters",
157
    "high-jinks",
158
    "homework",
159
    "hovercraft",
160
    "innings",
161
    "Japanese",
162
    "Lebanese",
163
    "mackerel",
164
    "means",
165
    "mews",
166
    "mice",
167
    "mumps",
168
    "news",
169
    "pincers",
170
    "pliers",
171
    "Portuguese",
172
    "proceedings",
173
    "salmon",
174
    "scissors",
175
    "sea-bass",
176
    "Senegalese",
177
    "shears",
178
    "Siamese",
179
    "Sinhalese",
180
    "spacecraft",
181
    "swine",
182
    "trout",
183
    "tuna",
184
    "Vietnamese",
185
    "watercraft",
186
    "whiting",
187
    "wildebeest"
188
  )
189
190
  is.element( s, v )
191
}
192
193
# Answers whether the given string (s) is an irregular plural.
194
pl.is.irregular.pl <- function( s ) {
195
  # Could be refactored with pl.irregular.pl...
196
  v <- c(
197
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
198
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
199
  )
200
201
  is.element( s, v )
202
}
203
204
# Call to pluralise an irregular noun. Only call after confirming
205
# the noun is irregular via pl.is.irregular.pl.
206
pl.irregular.pl <- function( s ) {
207
  v <- list(
208
    "beef" = "beefs",
209
    "brother" = "brothers",
210
    "child" = "children",
211
    "cow" = "cows",
212
    "ephemeris" = "ephemerides",
213
    "genie" = "genies",
214
    "money" = "moneys",
215
    "mongoose" = "mongooses",
216
    "mythos" = "mythoi",
217
    "octopus" = "octopuses",
218
    "ox" = "oxen",
219
    "soliloquy" = "soliloquies",
220
    "trilby" = "trilbys"
221
  )
222
223
  # Faster version of v[[ s ]]
224
  .subset2( v, s )
225
}
226
227
# Answers whether the given string (s) pluralises with -es.
228
pl.is.irregular.es <- function( s ) {
229
  v <- c(
230
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
231
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
232
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
233
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
234
    "polis", "rhinoceros", "sassafrass", "trellis"
235
  )
236
237
  is.element( s, v )
238
}
239
2401
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
26
#   http://goo.gl/oRL4MP
27
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
28
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
29
# See also: http://www.freevectors.net/assets/files/plural.txt
30
# -----------------------------------------------------------------------------
31
pluralize <- function( s, n ) {
32
  result <- s
33
34
  # Partial implementation of Conway's algorithm for nouns.
35
  if( n != 1 ) {
36
    if( pl.noninflective( s ) ||
37
        pl.suffix( "es", s ) ||
38
        pl.suffix( "fish", s ) ||
39
        pl.suffix( "ois", s ) ||
40
        pl.suffix( "sheep", s ) ||
41
        pl.suffix( "deer", s ) ||
42
        pl.suffix( "pox", s ) ||
43
        pl.suffix( "[A-Z].*ese", s ) ||
44
        pl.suffix( "itis", s ) ) {
45
      # 1. Retain non-inflective user-mapped noun as is.
46
      # 2. Retain non-inflective plural as is.
47
      result <- s
48
    }
49
    else if( pl.is.irregular.pl( s ) ) {
50
      # 4. Change irregular plurals based on mapping.
51
      result <- pl.irregular.pl( s )
52
    }
53
    else if( pl.is.irregular.es( s ) ) {
54
      # x. From Shevek's
55
      result <- pl.inflect( s, "", "es" )
56
    }
57
    else if( pl.suffix( "man", s ) ) {
58
      # 5. For -man, change -an to -en
59
      result <- pl.inflect( s, "an", "en" )
60
    }
61
    else if( pl.suffix( "[lm]ouse", s ) ) {
62
      # 5. For [lm]ouse, change -ouse to -ice
63
      result <- pl.inflect( s, "ouse", "ice" )
64
    }
65
    else if( pl.suffix( "tooth", s ) ) {
66
      # 5. For -tooth, change -ooth to -eeth
67
      result <- pl.inflect( s, "ooth", "eeth" )
68
    }
69
    else if( pl.suffix( "goose", s ) ) {
70
      # 5. For -goose, change -oose to -eese
71
      result <- pl.inflect( s, "oose", "eese" )
72
    }
73
    else if( pl.suffix( "foot", s ) ) {
74
      # 5. For -foot, change -oot to -eet
75
      result <- pl.inflect( s, "oot", "eet" )
76
    }
77
    else if( pl.suffix( "zoon", s ) ) {
78
      # 5. For -zoon, change -on to -a
79
      result <- pl.inflect( s, "on", "a" )
80
    }
81
    else if( pl.suffix( "[csx]is", s ) ) {
82
      # 5. Change -cis, -sis, -xis to -es
83
      result <- pl.inflect( s, "is", "es" )
84
    }
85
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
86
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
87
      result <- pl.inflect( s, "", "es" )
88
    }
89
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
90
      # 9. Change -f to -ves
91
      result <- pl.inflect( s, "f", "ves" )
92
    }
93
    else if( pl.suffix( "[nlw]ife", s ) ) {
94
      # 10. Change -fe to -ves
95
      result <- pl.inflect( s, "fe", "ves" )
96
    }
97
    else if( pl.suffix( "[aeiou]y", s ) ) {
98
      # 11. Change -[aeiou]y to -ys
99
      result <- pl.inflect( s, "", "s" )
100
    }
101
    else if( pl.suffix( "y", s ) ) {
102
      # 12. Change -y to -ies
103
      result <- pl.inflect( s, "y", "ies" )
104
    }
105
    else if( pl.suffix( "z", s ) ) {
106
      # x. Change -z to -zzes
107
      result <- pl.inflect( s, "", "zes" )
108
    }
109
    else {
110
      # 13. Default plural: add -s
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# -----------------------------------------------------------------------------
119
# Returns the given string (s) with its suffix replaced by r.
120
# -----------------------------------------------------------------------------
121
pl.inflect <- function( s, suffix, r ) {
122
  gsub( paste( suffix, "$", sep="" ), r, s )
123
}
124
125
# -----------------------------------------------------------------------------
126
# Answers whether the given string (s) has the given ending.
127
# -----------------------------------------------------------------------------
128
pl.suffix <- function( ending, s ) {
129
  grepl( paste( ending, "$", sep="" ), s )
130
}
131
132
# -----------------------------------------------------------------------------
133
# Answers whether the given string (s) is a noninflective noun.
134
# -----------------------------------------------------------------------------
135
pl.noninflective <- function( s ) {
136
  v <- c(
137
    "aircraft",
138
    "Bhutanese",
139
    "bison",
140
    "bream",
141
    "Burmese",
142
    "carp",
143
    "chassis",
144
    "Chinese",
145
    "clippers",
146
    "cod",
147
    "contretemps",
148
    "corps",
149
    "debris",
150
    "djinn",
151
    "eland",
152
    "elk",
153
    "flounder",
154
    "fracas",
155
    "gallows",
156
    "graffiti",
157
    "headquarters",
158
    "high-jinks",
159
    "homework",
160
    "hovercraft",
161
    "innings",
162
    "Japanese",
163
    "Lebanese",
164
    "mackerel",
165
    "means",
166
    "mews",
167
    "mice",
168
    "mumps",
169
    "news",
170
    "pincers",
171
    "pliers",
172
    "Portuguese",
173
    "proceedings",
174
    "salmon",
175
    "scissors",
176
    "sea-bass",
177
    "Senegalese",
178
    "shears",
179
    "Siamese",
180
    "Sinhalese",
181
    "spacecraft",
182
    "swine",
183
    "trout",
184
    "tuna",
185
    "Vietnamese",
186
    "watercraft",
187
    "whiting",
188
    "wildebeest"
189
  )
190
191
  is.element( s, v )
192
}
193
194
# -----------------------------------------------------------------------------
195
# Answers whether the given string (s) is an irregular plural.
196
# -----------------------------------------------------------------------------
197
pl.is.irregular.pl <- function( s ) {
198
  # Could be refactored with pl.irregular.pl...
199
  v <- c(
200
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
201
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
202
  )
203
204
  is.element( s, v )
205
}
206
207
# -----------------------------------------------------------------------------
208
# Call to pluralize an irregular noun. Only call after confirming
209
# the noun is irregular via pl.is.irregular.pl.
210
# -----------------------------------------------------------------------------
211
pl.irregular.pl <- function( s ) {
212
  v <- list(
213
    "beef" = "beefs",
214
    "brother" = "brothers",
215
    "child" = "children",
216
    "cow" = "cows",
217
    "ephemeris" = "ephemerides",
218
    "genie" = "genies",
219
    "money" = "moneys",
220
    "mongoose" = "mongooses",
221
    "mythos" = "mythoi",
222
    "octopus" = "octopuses",
223
    "ox" = "oxen",
224
    "soliloquy" = "soliloquies",
225
    "trilby" = "trilbys"
226
  )
227
228
  # Faster version of v[[ s ]]
229
  .subset2( v, s )
230
}
231
232
# -----------------------------------------------------------------------------
233
# Answers whether the given string (s) pluralizes with -es.
234
# -----------------------------------------------------------------------------
235
pl.is.irregular.es <- function( s ) {
236
  v <- c(
237
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
238
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
239
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
240
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
241
    "polis", "rhinoceros", "sassafrass", "trellis"
242
  )
243
244
  is.element( s, v )
245
}
246
1247
M R/possessive.R
1
# ######################################################################
2
#
3
# Copyright 2017, White Magic Software, Ltd.
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
43
# 
54
# Permission is hereby granted, free of charge, to any person obtaining
...
2120
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2221
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# Changes a word into its possessive form.
29
#
30
# ######################################################################
22
# -----------------------------------------------------------------------------
3123
24
# -----------------------------------------------------------------------------
3225
# Returns leftmost n characters of s.
26
# -----------------------------------------------------------------------------
3327
lstr <- function( s, n = 1 ) {
3428
  substr( s, 0, n )
3529
}
3630
31
# -----------------------------------------------------------------------------
3732
# Returns rightmost n characters of s.
33
# -----------------------------------------------------------------------------
3834
rstr <- function( s, n = 1 ) {
39
  l = nchar( s )
35
  l <- nchar( s )
4036
  substr( s, l - n + 1, l )
4137
}
4238
43
# Returns the possessive form of the given word.
39
# -----------------------------------------------------------------------------
40
# Returns the possessive form of the given word, s.
41
# -----------------------------------------------------------------------------
4442
pos <- function( s ) {
45
  result <- s
46
47
  # Check to see if the last character is an s.
48
  ch <- rstr( s, 1 )
43
  lcs <- tolower( s )
44
  pronouns <- c( 'your', 'our', 'her', 'it', 'their' )
4945
50
  if( ch != "s" ) {
51
    result <- concat( result, "'s" )
46
  if( lcs == 'my' ) {
47
    # Change "[Mm]y" to "[Mm]ine".
48
    s <- paste0( lstr( s, 1 ), "ine" )
5249
  }
53
  else {
54
    result <- concat( result, "'" )
50
  else if( lcs %in% pronouns ) {
51
    # Append an s to most pronouns.
52
    s <- paste0( s, 's' )
53
  }
54
  else if( lcs != 'his' ) {
55
    # Possessive for all other words except 'his'.
56
    s <- paste0( s, ifelse( rstr( s, 1 ) == 's', "'" ,"'s" ) )
5557
  }
5658
57
  result
59
  s
5860
}
5961
M README.md
33
# $application.title$
44
5
Text editing using interpolated strings.
5
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
66
77
## Requirements
...
3535
* XML document transformation using XSLT3 or older
3636
* Platform independent (Windows, Linux, MacOS)
37
38
## Usage
39
40
See the following documents for more information:
41
42
* [USAGE.md](USAGE.md) - how variable definitions and string interpolation work.
43
* [USAGE-R.md](USAGE-R.md) - how to call R functions in R Markdown documents.
3744
3845
## Future Features
M USAGE-R.md
152152
    ``` r
153153
    x <- function( s ) {
154
      tryCatch({
154
      tryCatch( {
155155
        r = eval( parse( text = s ) )
156156
157
        if( is.atomic( r ) ) { r }
158
        else { s }
157
        ifelse( is.atomic( r ), r, s );
159158
      },
160159
      warning = function( w ) { s },
161
      error = function( e ) { s })
160
      error = function( e ) { s } )
162161
    }
163162
    ```
M build.gradle
3838
  implementation 'de.jensd:fontawesomefx-commons:11.0'
3939
  implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
40
  implementation "org.renjin:renjin-script-engine:0.9.2726"
40
  implementation "org.renjin:renjin-script-engine:3.5-beta76"
4141
4242
  def os = ['win', 'linux', 'mac']
...
6060
6161
sourceCompatibility = JavaVersion.VERSION_11
62
version = '1.4.0'
62
version = '1.4.1'
6363
applicationName = 'scrivenvar'
6464
mainClassName = 'com.scrivenvar.Main'
M src/main/java/com/scrivenvar/AbstractFileFactory.java
9494
    }
9595
96
    if( fileType == null ) {
97
      unknownFileType( fileType, path );
98
    }
99
10096
    return fileType;
101
  }
102
103
  /**
104
   * Throws IllegalArgumentException because the given path could not be
105
   * recognized.
106
   *
107
   * @param type The detected path type (protocol, file extension, etc.).
108
   * @param path The path to a source of definitions.
109
   */
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 );
11397
  }
11498
M src/main/java/com/scrivenvar/FileEditorTab.java
3838
import javafx.beans.value.ChangeListener;
3939
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
510
    return this.encoding;
511
  }
512
513
  private void setEncoding( final Charset encoding ) {
514
    this.encoding = encoding;
40
import javafx.scene.Node;
41
import javafx.scene.Scene;
42
import javafx.scene.control.Tab;
43
import javafx.scene.control.Tooltip;
44
import javafx.scene.text.Text;
45
import javafx.stage.Window;
46
import org.fxmisc.richtext.StyleClassedTextArea;
47
import org.fxmisc.richtext.model.TwoDimensional.Position;
48
import org.fxmisc.undo.UndoManager;
49
import org.mozilla.universalchardet.UniversalDetector;
50
51
import java.io.IOException;
52
import java.nio.charset.Charset;
53
import java.nio.file.Files;
54
import java.nio.file.Path;
55
56
import static java.nio.charset.StandardCharsets.UTF_8;
57
import static java.util.Locale.ENGLISH;
58
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
59
60
/**
61
 * Editor for a single file.
62
 *
63
 * @author Karl Tauber and White Magic Software, Ltd.
64
 */
65
public final class FileEditorTab extends Tab {
66
67
  private final Notifier mAlertService = Services.load( Notifier.class );
68
  private final EditorPane mEditorPane = new MarkdownEditorPane();
69
70
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
71
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
72
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
73
74
  /**
75
   * Character encoding used by the file (or default encoding if none found).
76
   */
77
  private Charset mEncoding = UTF_8;
78
79
  /**
80
   * File to load into the editor.
81
   */
82
  private Path mPath;
83
84
  public FileEditorTab( final Path path ) {
85
    setPath( path );
86
87
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
88
89
    setOnSelectionChanged( e -> {
90
      if( isSelected() ) {
91
        Platform.runLater( this::activated );
92
      }
93
    } );
94
  }
95
96
  private void updateTab() {
97
    setText( getTabTitle() );
98
    setGraphic( getModifiedMark() );
99
    setTooltip( getTabTooltip() );
100
  }
101
102
  /**
103
   * Returns the base filename (without the directory names).
104
   *
105
   * @return The untitled text if the path hasn't been set.
106
   */
107
  private String getTabTitle() {
108
    final Path filePath = getPath();
109
110
    return (filePath == null)
111
        ? Messages.get( "FileEditor.untitled" )
112
        : filePath.getFileName().toString();
113
  }
114
115
  /**
116
   * Returns the full filename represented by the path.
117
   *
118
   * @return The untitled text if the path hasn't been set.
119
   */
120
  private Tooltip getTabTooltip() {
121
    final Path filePath = getPath();
122
    return new Tooltip( filePath == null ? "" : filePath.toString() );
123
  }
124
125
  /**
126
   * Returns a marker to indicate whether the file has been modified.
127
   *
128
   * @return "*" when the file has changed; otherwise null.
129
   */
130
  private Text getModifiedMark() {
131
    return isModified() ? new Text( "*" ) : null;
132
  }
133
134
  /**
135
   * Called when the user switches tab.
136
   */
137
  private void activated() {
138
    // Tab is closed or no longer active.
139
    if( getTabPane() == null || !isSelected() ) {
140
      return;
141
    }
142
143
    // Switch to the tab without loading if the contents are already in memory.
144
    if( getContent() != null ) {
145
      getEditorPane().requestFocus();
146
      return;
147
    }
148
149
    // Load the text and update the preview before the undo manager.
150
    load();
151
152
    // Track undo requests -- can only be called *after* load.
153
    initUndoManager();
154
    initLayout();
155
    initFocus();
156
  }
157
158
  private void initLayout() {
159
    setContent( getScrollPane() );
160
  }
161
162
  private Node getScrollPane() {
163
    return getEditorPane().getScrollPane();
164
  }
165
166
  private void initFocus() {
167
    getEditorPane().requestFocus();
168
  }
169
170
  private void initUndoManager() {
171
    final UndoManager<?> undoManager = getUndoManager();
172
    undoManager.forgetHistory();
173
174
    // Bind the editor undo manager to the properties.
175
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
176
    canUndo.bind( undoManager.undoAvailableProperty() );
177
    canRedo.bind( undoManager.redoAvailableProperty() );
178
  }
179
180
  /**
181
   * Searches from the caret position forward for the given string.
182
   *
183
   * @param needle The text string to match.
184
   */
185
  public void searchNext( final String needle ) {
186
    final String haystack = getEditorText();
187
    int index = haystack.indexOf( needle, getCaretPosition() );
188
189
    // Wrap around.
190
    if( index == -1 ) {
191
      index = haystack.indexOf( needle );
192
    }
193
194
    if( index >= 0 ) {
195
      setCaretPosition( index );
196
      getEditor().selectRange( index, index + needle.length() );
197
    }
198
  }
199
200
  /**
201
   * Returns the index into the text where the caret blinks happily away.
202
   *
203
   * @return A number from 0 to the editor's document text length.
204
   */
205
  public int getCaretPosition() {
206
    return getEditor().getCaretPosition();
207
  }
208
209
  /**
210
   * Moves the caret to a given offset.
211
   *
212
   * @param offset The new caret offset.
213
   */
214
  private void setCaretPosition( final int offset ) {
215
    getEditor().moveTo( offset );
216
    getEditor().requestFollowCaret();
217
  }
218
219
  /**
220
   * Returns the caret's current row and column position.
221
   *
222
   * @return The caret's offset into the document.
223
   */
224
  public Position getCaretOffset() {
225
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
226
  }
227
228
  /**
229
   * Allows observers to synchronize caret position changes.
230
   *
231
   * @return An observable caret property value.
232
   */
233
  public final ObservableValue<Integer> caretPositionProperty() {
234
    return getEditor().caretPositionProperty();
235
  }
236
237
  /**
238
   * Returns the text area associated with this tab.
239
   *
240
   * @return A text editor.
241
   */
242
  private StyleClassedTextArea getEditor() {
243
    return getEditorPane().getEditor();
244
  }
245
246
  /**
247
   * Returns true if the given path exactly matches this tab's path.
248
   *
249
   * @param check The path to compare against.
250
   * @return true The paths are the same.
251
   */
252
  public boolean isPath( final Path check ) {
253
    final Path filePath = getPath();
254
255
    return filePath != null && filePath.equals( check );
256
  }
257
258
  /**
259
   * Reads the entire file contents from the path associated with this tab.
260
   */
261
  private void load() {
262
    final Path filePath = getPath();
263
264
    if( filePath != null ) {
265
      try {
266
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
267
        getEditorPane().scrollToTop();
268
      } catch( final Exception ex ) {
269
        getNotifyService().notify( ex );
270
      }
271
    }
272
  }
273
274
  /**
275
   * Saves the entire file contents from the path associated with this tab.
276
   *
277
   * @return true The file has been saved.
278
   */
279
  public boolean save() {
280
    try {
281
      final EditorPane editor = getEditorPane();
282
      Files.write( getPath(), asBytes( editor.getText() ) );
283
      editor.getUndoManager().mark();
284
      return true;
285
    } catch( final IOException ex ) {
286
      return alert(
287
          "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
288
      );
289
    }
290
  }
291
292
  /**
293
   * Creates an alert dialog and waits for it to close.
294
   *
295
   * @param titleKey   Resource bundle key for the alert dialog title.
296
   * @param messageKey Resource bundle key for the alert dialog message.
297
   * @param e          The unexpected happening.
298
   * @return false
299
   */
300
  @SuppressWarnings("SameParameterValue")
301
  private boolean alert(
302
      final String titleKey, final String messageKey, final Exception e ) {
303
    final Notifier service = getNotifyService();
304
    final Path filePath = getPath();
305
306
    final Notification message = service.createNotification(
307
        Messages.get( titleKey ),
308
        Messages.get( messageKey ),
309
        filePath == null ? "" : filePath,
310
        e.getMessage()
311
    );
312
313
    try {
314
      service.createError( getWindow(), message ).showAndWait();
315
    } catch( final Exception ex ) {
316
      getNotifyService().notify( ex );
317
    }
318
319
    return false;
320
  }
321
322
  private Window getWindow() {
323
    final Scene scene = getEditorPane().getScene();
324
325
    if( scene == null ) {
326
      throw new UnsupportedOperationException( "No scene window available" );
327
    }
328
329
    return scene.getWindow();
330
  }
331
332
  /**
333
   * Returns a best guess at the file encoding. If the encoding could not be
334
   * detected, this will return the default charset for the JVM.
335
   *
336
   * @param bytes The bytes to perform character encoding detection.
337
   * @return The character encoding.
338
   */
339
  private Charset detectEncoding( final byte[] bytes ) {
340
    final UniversalDetector detector = new UniversalDetector( null );
341
    detector.handleData( bytes, 0, bytes.length );
342
    detector.dataEnd();
343
344
    final String charset = detector.getDetectedCharset();
345
    final Charset charEncoding = charset == null
346
        ? Charset.defaultCharset()
347
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
348
349
    detector.reset();
350
351
    return charEncoding;
352
  }
353
354
  /**
355
   * Converts the given string to an array of bytes using the encoding that was
356
   * originally detected (if any) and associated with this file.
357
   *
358
   * @param text The text to convert into the original file encoding.
359
   * @return A series of bytes ready for writing to a file.
360
   */
361
  private byte[] asBytes( final String text ) {
362
    return text.getBytes( getEncoding() );
363
  }
364
365
  /**
366
   * Converts the given bytes into a Java String. This will call setEncoding
367
   * with the encoding detected by the CharsetDetector.
368
   *
369
   * @param text The text of unknown character encoding.
370
   * @return The text, in its auto-detected encoding, as a String.
371
   */
372
  private String asString( final byte[] text ) {
373
    setEncoding( detectEncoding( text ) );
374
    return new String( text, getEncoding() );
375
  }
376
377
  /**
378
   * Returns the path to the file being edited in this tab.
379
   *
380
   * @return A non-null instance.
381
   */
382
  public Path getPath() {
383
    return mPath;
384
  }
385
386
  /**
387
   * Sets the path to a file for editing and then updates the tab with the
388
   * file contents.
389
   *
390
   * @param path A non-null instance.
391
   */
392
  public void setPath( final Path path ) {
393
    assert path != null;
394
395
    mPath = path;
396
397
    updateTab();
398
  }
399
400
  public boolean isModified() {
401
    return mModified.get();
402
  }
403
404
  ReadOnlyBooleanProperty modifiedProperty() {
405
    return mModified.getReadOnlyProperty();
406
  }
407
408
  BooleanProperty canUndoProperty() {
409
    return this.canUndo;
410
  }
411
412
  BooleanProperty canRedoProperty() {
413
    return this.canRedo;
414
  }
415
416
  private UndoManager<?> getUndoManager() {
417
    return getEditorPane().getUndoManager();
418
  }
419
420
  /**
421
   * Forwards to the editor pane's listeners for text change events.
422
   *
423
   * @param listener The listener to notify when the text changes.
424
   */
425
  public void addTextChangeListener( final ChangeListener<String> listener ) {
426
    getEditorPane().addTextChangeListener( listener );
427
  }
428
429
  /**
430
   * Forwards to the editor pane's listeners for caret paragraph change events.
431
   *
432
   * @param listener The listener to notify when the caret changes paragraphs.
433
   */
434
  public void addCaretParagraphListener(
435
      final ChangeListener<Integer> listener ) {
436
    getEditorPane().addCaretParagraphListener( listener );
437
  }
438
439
  /**
440
   * Forwards the request to the editor pane.
441
   *
442
   * @return The text to process.
443
   */
444
  public String getEditorText() {
445
    return getEditorPane().getText();
446
  }
447
448
  /**
449
   * Returns the editor pane, or creates one if it doesn't yet exist.
450
   *
451
   * @return The editor pane, never null.
452
   */
453
  public EditorPane getEditorPane() {
454
    return mEditorPane;
455
  }
456
457
  private Notifier getNotifyService() {
458
    return mAlertService;
459
  }
460
461
  /**
462
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
463
   * determined.
464
   *
465
   * @return The file encoding or UTF-8 if unknown.
466
   */
467
  private Charset getEncoding() {
468
    return mEncoding;
469
  }
470
471
  private void setEncoding( final Charset encoding ) {
472
    assert encoding != null;
473
474
    mEncoding = encoding;
515475
  }
516476
M src/main/java/com/scrivenvar/FileEditorTabPane.java
4848
import javafx.scene.control.Tab;
4949
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;
50
import javafx.stage.FileChooser;
51
import javafx.stage.FileChooser.ExtensionFilter;
52
import javafx.stage.Window;
53
import org.fxmisc.richtext.StyledTextArea;
54
55
import java.io.File;
56
import java.nio.file.Path;
57
import java.util.ArrayList;
58
import java.util.List;
59
import java.util.Optional;
60
import java.util.concurrent.atomic.AtomicReference;
61
import java.util.prefs.Preferences;
62
import java.util.stream.Collectors;
63
64
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
65
import static com.scrivenvar.FileType.*;
66
import static com.scrivenvar.Messages.get;
67
import static com.scrivenvar.service.events.Notifier.YES;
68
69
/**
70
 * Tab pane for file editors.
71
 *
72
 * @author Karl Tauber and White Magic Software, Ltd.
73
 */
74
public final class FileEditorTabPane extends TabPane {
75
76
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose" +
77
      ".filter";
78
79
  private final Options options = Services.load( Options.class );
80
  private final Settings settings = Services.load( Settings.class );
81
  private final Notifier notifyService = Services.load( Notifier.class );
82
83
  private final ReadOnlyObjectWrapper<Path> openDefinition =
84
      new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
86
      new ReadOnlyObjectWrapper<>();
87
  private final ReadOnlyBooleanWrapper anyFileEditorModified =
88
      new ReadOnlyBooleanWrapper();
89
90
  /**
91
   * Constructs a new file editor tab pane.
92
   */
93
  public FileEditorTabPane() {
94
    final ObservableList<Tab> tabs = getTabs();
95
96
    setFocusTraversable( false );
97
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
98
99
    addTabSelectionListener(
100
        ( ObservableValue<? extends Tab> tabPane,
101
          final Tab oldTab, final Tab newTab ) -> {
102
103
          if( newTab != null ) {
104
            mActiveFileEditor.set( (FileEditorTab) newTab );
105
          }
106
        }
107
    );
108
109
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
110
                                                       newValue ) -> {
111
      for( final Tab tab : tabs ) {
112
        if( ((FileEditorTab) tab).isModified() ) {
113
          this.anyFileEditorModified.set( true );
114
          break;
115
        }
116
      }
117
    };
118
119
    tabs.addListener(
120
        (ListChangeListener<Tab>) change -> {
121
          while( change.next() ) {
122
            if( change.wasAdded() ) {
123
              change.getAddedSubList().forEach(
124
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
125
                                                  .addListener( modifiedListener ) );
126
            }
127
            else if( change.wasRemoved() ) {
128
              change.getRemoved().forEach(
129
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
130
                                                  .removeListener(
131
                                                      modifiedListener ) );
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
   * Allows observers to be notified when the current file editor tab changes.
144
   *
145
   * @param listener The listener to notify of tab change events.
146
   */
147
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
148
    // Observe the tab so that when a new tab is opened or selected,
149
    // a notification is kicked off.
150
    getSelectionModel().selectedItemProperty().addListener( listener );
151
  }
152
153
  /**
154
   * Allows clients to manipulate the editor content directly.
155
   *
156
   * @return The text area for the active file editor.
157
   */
158
  public StyledTextArea getEditor() {
159
    return getActiveFileEditor().getEditorPane().getEditor();
160
  }
161
162
  /**
163
   * Returns the tab that has keyboard focus.
164
   *
165
   * @return A non-null instance.
166
   */
167
  public FileEditorTab getActiveFileEditor() {
168
    return mActiveFileEditor.get();
169
  }
170
171
  /**
172
   * Returns the property corresponding to the tab that has focus.
173
   *
174
   * @return A non-null instance.
175
   */
176
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
177
    return mActiveFileEditor.getReadOnlyProperty();
178
  }
179
180
  /**
181
   * Property that can answer whether the text has been modified.
182
   *
183
   * @return A non-null instance, true meaning the content has not been saved.
184
   */
185
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
186
    return this.anyFileEditorModified.getReadOnlyProperty();
187
  }
188
189
  /**
190
   * Creates a new editor instance from the given path.
191
   *
192
   * @param path The file to open.
193
   * @return A non-null instance.
194
   */
195
  private FileEditorTab createFileEditor( final Path path ) {
196
    assert path != null;
197
198
    final FileEditorTab tab = new FileEditorTab( path );
199
200
    tab.setOnCloseRequest( e -> {
201
      if( !canCloseEditor( tab ) ) {
202
        e.consume();
203
      }
204
      else if( isActiveFileEditor( tab ) ) {
205
        // Prevent prompting the user to save when there are no file editor
206
        // tabs open.
207
        mActiveFileEditor.set( null );
208
      }
209
    } );
210
211
    return tab;
212
  }
213
214
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
215
    return getActiveFileEditor() == tab;
216
  }
217
218
  private Path getDefaultPath() {
219
    final String filename = getDefaultFilename();
220
    return (new File( filename )).toPath();
221
  }
222
223
  private String getDefaultFilename() {
224
    return getSettings().getSetting( "file.default", "untitled.md" );
225
  }
226
227
  /**
228
   * Called when the user selects New from the File menu.
229
   */
230
  void newEditor() {
231
    final Path defaultPath = getDefaultPath();
232
    final FileEditorTab tab = createFileEditor( defaultPath );
233
234
    getTabs().add( tab );
235
    getSelectionModel().select( tab );
236
  }
237
238
  void openFileDialog() {
239
    final String title = get( "Dialog.file.choose.open.title" );
240
    final FileChooser dialog = createFileChooser( title );
241
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
242
243
    if( files != null ) {
244
      openFiles( files );
245
    }
246
  }
247
248
  /**
249
   * Opens the files into new editors, unless one of those files was a
250
   * definition file. The definition file is loaded into the definition pane,
251
   * but only the first one selected (multiple definition files will result in a
252
   * warning).
253
   *
254
   * @param files The list of non-definition files that the were requested to
255
   *              open.
256
   */
257
  private void openFiles( final List<File> files ) {
258
    final List<String> extensions =
259
        createExtensionFilter( DEFINITION ).getExtensions();
260
    final FileTypePredicate predicate =
261
        new FileTypePredicate( extensions );
262
263
    // The user might have opened multiple definitions files. These will
264
    // be discarded from the text editable files.
265
    final List<File> definitions
266
        = files.stream().filter( predicate ).collect( Collectors.toList() );
267
268
    // Create a modifiable list to remove any definition files that were
269
    // opened.
270
    final List<File> editors = new ArrayList<>( files );
271
272
    if( !editors.isEmpty() ) {
273
      saveLastDirectory( editors.get( 0 ) );
274
    }
275
276
    editors.removeAll( definitions );
277
278
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
279
    if( !editors.isEmpty() ) {
280
      openEditors( editors, 0 );
281
    }
282
283
    if( !definitions.isEmpty() ) {
284
      openDefinition( definitions.get( 0 ) );
285
    }
286
  }
287
288
  private void openEditors( final List<File> files, final int activeIndex ) {
289
    final int fileTally = files.size();
290
    final List<Tab> tabs = getTabs();
291
292
    // Close single unmodified "Untitled" tab.
293
    if( tabs.size() == 1 ) {
294
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
295
296
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
297
        closeEditor( fileEditor, false );
298
      }
299
    }
300
301
    for( int i = 0; i < fileTally; i++ ) {
302
      final Path path = files.get( i ).toPath();
303
304
      FileEditorTab fileEditorTab = findEditor( path );
305
306
      // Only open new files.
307
      if( fileEditorTab == null ) {
308
        fileEditorTab = createFileEditor( path );
309
        getTabs().add( fileEditorTab );
310
      }
311
312
      // Select the first file in the list.
313
      if( i == activeIndex ) {
314
        getSelectionModel().select( fileEditorTab );
315
      }
316
    }
317
  }
318
319
  /**
320
   * Returns a property that changes when a new definition file is opened.
321
   *
322
   * @return The path to a definition file that was opened.
323
   */
324
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
325
    return getOnOpenDefinitionFile().getReadOnlyProperty();
326
  }
327
328
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
329
    return this.openDefinition;
330
  }
331
332
  /**
333
   * Called when the user has opened a definition file (using the file open
334
   * dialog box). This will replace the current set of definitions for the
335
   * active tab.
336
   *
337
   * @param definition The file to open.
338
   */
339
  private void openDefinition( final File definition ) {
340
    // TODO: Prevent reading this file twice when a new text document is opened.
341
    // (might be a matter of checking the value first).
342
    getOnOpenDefinitionFile().set( definition.toPath() );
343
  }
344
345
  /**
346
   * Called when the contents of the editor are to be saved.
347
   *
348
   * @param tab The tab containing content to save.
349
   * @return true The contents were saved (or needn't be saved).
350
   */
351
  public boolean saveEditor( final FileEditorTab tab ) {
352
    if( tab == null || !tab.isModified() ) {
353
      return true;
354
    }
355
356
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
357
  }
358
359
  /**
360
   * Opens the Save As dialog for the user to save the content under a new
361
   * path.
362
   *
363
   * @param tab The tab with contents to save.
364
   * @return true The contents were saved, or the tab was null.
365
   */
366
  public boolean saveEditorAs( final FileEditorTab tab ) {
367
    if( tab == null ) {
368
      return true;
369
    }
370
371
    getSelectionModel().select( tab );
372
373
    final FileChooser fileChooser = createFileChooser( get(
374
        "Dialog.file.choose.save.title" ) );
375
    final File file = fileChooser.showSaveDialog( getWindow() );
376
    if( file == null ) {
377
      return false;
378
    }
379
380
    saveLastDirectory( file );
381
    tab.setPath( file.toPath() );
382
383
    return tab.save();
384
  }
385
386
  void saveAllEditors() {
387
    for( final FileEditorTab fileEditor : getAllEditors() ) {
388
      saveEditor( fileEditor );
389
    }
390
  }
391
392
  /**
393
   * Answers whether the file has had modifications. '
394
   *
395
   * @param tab THe tab to check for modifications.
396
   * @return false The file is unmodified.
397
   */
398
  boolean canCloseEditor( final FileEditorTab tab ) {
399
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
400
    canClose.set( true );
401
402
    if( tab.isModified() ) {
403
      final Notification message = getNotifyService().createNotification(
404
          Messages.get( "Alert.file.close.title" ),
405
          Messages.get( "Alert.file.close.text" ),
406
          tab.getText()
407
      );
408
409
      final Alert confirmSave = getNotifyService().createConfirmation(
410
          getWindow(), message );
411
412
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
413
414
      buttonType.ifPresent(
415
          save -> canClose.set(
416
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
417
          )
418
      );
419
    }
420
421
    return canClose.get();
428422
  }
429423
M src/main/java/com/scrivenvar/MainWindow.java
7979
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
8080
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
          );
81
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
82
83
/**
84
 * Main window containing a tab pane in the center for file editors.
85
 *
86
 * @author Karl Tauber and White Magic Software, Ltd.
87
 */
88
public class MainWindow implements Observer {
89
90
  private final Options mOptions = Services.load( Options.class );
91
  private final Snitch mSnitch = Services.load( Snitch.class );
92
  private final Notifier mNotifier = Services.load( Notifier.class );
93
94
  private Scene scene;
95
  private StatusBar statusBar;
96
  private Text lineNumberText;
97
  private TextField findTextField;
98
99
  private DefinitionSource definitionSource;
100
  private DefinitionPane definitionPane;
101
  private FileEditorTabPane fileEditorPane;
102
  private HTMLPreviewPane previewPane;
103
104
  /**
105
   * Prevents re-instantiation of processing classes.
106
   */
107
  private Map<FileEditorTab, Processor<String>> processors;
108
109
  /**
110
   * Listens on the definition pane for double-click events.
111
   */
112
  private VariableNameInjector variableNameInjector;
113
114
  public MainWindow() {
115
    initLayout();
116
    initFindInput();
117
    initSnitch();
118
    initDefinitionListener();
119
    initTabAddedListener();
120
    initTabChangedListener();
121
    initPreferences();
122
  }
123
124
  /**
125
   * Watch for changes to external files. In particular, this awaits
126
   * modifications to any XSL files associated with XML files being edited. When
127
   * an XSL file is modified (external to the application), the snitch's ears
128
   * perk up and the file is reloaded. This keeps the XSL transformation up to
129
   * date with what's on the file system.
130
   */
131
  private void initSnitch() {
132
    getSnitch().addObserver( this );
133
  }
134
135
  /**
136
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
137
   * presses.
138
   */
139
  private void initFindInput() {
140
    final TextField input = getFindTextField();
141
142
    input.setOnKeyPressed( ( KeyEvent event ) -> {
143
      switch( event.getCode() ) {
144
        case F3:
145
        case ENTER:
146
          findNext();
147
          break;
148
        case F:
149
          if( !event.isControlDown() ) {
150
            break;
151
          }
152
        case ESCAPE:
153
          getStatusBar().setGraphic( null );
154
          getActiveFileEditor().getEditorPane().requestFocus();
155
          break;
156
      }
157
    } );
158
159
    // Remove when the input field loses focus.
160
    input.focusedProperty().addListener(
161
        (
162
            final ObservableValue<? extends Boolean> focused,
163
            final Boolean oFocus,
164
            final Boolean nFocus ) -> {
165
          if( !nFocus ) {
166
            getStatusBar().setGraphic( null );
167
          }
168
        }
169
    );
170
  }
171
172
  /**
173
   * Listen for file editor tab pane to receive an open definition source event.
174
   */
175
  private void initDefinitionListener() {
176
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
177
        ( ObservableValue<? extends Path> definitionFile,
178
          final Path oldPath, final Path newPath ) -> {
179
          openDefinition( newPath );
180
181
          // Indirectly refresh the resolved map.
182
          setProcessors( null );
183
          updateDefinitionPane();
184
185
          try {
186
            getSnitch().ignore( oldPath );
187
            getSnitch().listen( newPath );
188
          } catch( final IOException ex ) {
189
            error( ex );
190
          }
191
192
          // Will create new processors and therefore a new resolved map.
193
          refreshSelectedTab( getActiveFileEditor() );
194
        }
195
    );
196
  }
197
198
  /**
199
   * When tabs are added, hook the various change listeners onto the new tab so
200
   * that the preview pane refreshes as necessary.
201
   */
202
  private void initTabAddedListener() {
203
    final FileEditorTabPane editorPane = getFileEditorPane();
204
205
    // Make sure the text processor kicks off when new files are opened.
206
    final ObservableList<Tab> tabs = editorPane.getTabs();
207
208
    // Update the preview pane on tab changes.
209
    tabs.addListener(
210
        ( final Change<? extends Tab> change ) -> {
211
          while( change.next() ) {
212
            if( change.wasAdded() ) {
213
              // Multiple tabs can be added simultaneously.
214
              for( final Tab newTab : change.getAddedSubList() ) {
215
                final FileEditorTab tab = (FileEditorTab) newTab;
216
217
                initTextChangeListener( tab );
218
                initCaretParagraphListener( tab );
219
                initKeyboardEventListeners( tab );
220
//              initSyntaxListener( tab );
221
              }
222
            }
223
          }
224
        }
225
    );
226
  }
227
228
  /**
229
   * Reloads the preferences from the previous session.
230
   */
231
  private void initPreferences() {
232
    restoreDefinitionSource();
233
    getFileEditorPane().restorePreferences();
234
    updateDefinitionPane();
235
  }
236
237
  /**
238
   * Listen for new tab selection events.
239
   */
240
  private void initTabChangedListener() {
241
    final FileEditorTabPane editorPane = getFileEditorPane();
242
243
    // Update the preview pane changing tabs.
244
    editorPane.addTabSelectionListener(
245
        ( ObservableValue<? extends Tab> tabPane,
246
          final Tab oldTab, final Tab newTab ) -> {
247
          updateVariableNameInjector();
248
249
          // If there was no old tab, then this is a first time load, which
250
          // can be ignored.
251
          if( oldTab != null ) {
252
            if( newTab == null ) {
253
              closeRemainingTab();
254
            }
255
            else {
256
              // Update the preview with the edited text.
257
              refreshSelectedTab( (FileEditorTab) newTab );
258
            }
259
          }
260
        }
261
    );
262
  }
263
264
  /**
265
   * Ensure that the keyboard events are received when a new tab is added
266
   * to the user interface.
267
   *
268
   * @param tab The tab that can trigger keyboard events, such as control+space.
269
   */
270
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
271
    final VariableNameInjector vin = getVariableNameInjector();
272
    vin.initKeyboardEventListeners( tab );
273
  }
274
275
  private void initTextChangeListener( final FileEditorTab tab ) {
276
    tab.addTextChangeListener(
277
        ( ObservableValue<? extends String> editor,
278
          final String oldValue, final String newValue ) ->
279
            refreshSelectedTab( tab )
280
    );
281
  }
282
283
  private void initCaretParagraphListener( final FileEditorTab tab ) {
284
    tab.addCaretParagraphListener(
285
        ( ObservableValue<? extends Integer> editor,
286
          final Integer oldValue, final Integer newValue ) ->
287
            refreshSelectedTab( tab )
288
    );
289
  }
290
291
  private void updateVariableNameInjector() {
292
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
293
  }
294
295
  private void setVariableNameInjector( final VariableNameInjector injector ) {
296
    this.variableNameInjector = injector;
297
  }
298
299
  private synchronized VariableNameInjector getVariableNameInjector() {
300
    if( this.variableNameInjector == null ) {
301
      final VariableNameInjector vin = createVariableNameInjector();
302
      setVariableNameInjector( vin );
303
    }
304
305
    return this.variableNameInjector;
306
  }
307
308
  private VariableNameInjector createVariableNameInjector() {
309
    final FileEditorTab tab = getActiveFileEditor();
310
    final DefinitionPane pane = getDefinitionPane();
311
312
    return new VariableNameInjector( tab, pane );
313
  }
314
315
  /**
316
   * Called whenever the preview pane becomes out of sync with the file editor
317
   * tab. This can be called when the text changes, the caret paragraph changes,
318
   * or the file tab changes.
319
   *
320
   * @param tab The file editor tab that has been changed in some fashion.
321
   */
322
  private void refreshSelectedTab( final FileEditorTab tab ) {
323
    getPreviewPane().setPath( tab.getPath() );
324
325
    // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
326
    final Position p = tab.getCaretOffset();
327
    getLineNumberText().setText(
328
        get( STATUS_BAR_LINE,
329
             p.getMajor() + 1,
330
             p.getMinor() + 1,
331
             tab.getCaretPosition() + 1
332
        )
333
    );
334
335
    Processor<String> processor = getProcessors().get( tab );
336
337
    if( processor == null ) {
338
      processor = createProcessor( tab );
339
      getProcessors().put( tab, processor );
340
    }
341
342
    try {
343
      getNotifier().clear();
344
      processor.processChain( tab.getEditorText() );
345
    } catch( final Exception ex ) {
346
      error( ex );
347
    }
348
  }
349
350
  /**
351
   * Used to find text in the active file editor window.
352
   */
353
  private void find() {
354
    final TextField input = getFindTextField();
355
    getStatusBar().setGraphic( input );
356
    input.requestFocus();
357
  }
358
359
  public void findNext() {
360
    getActiveFileEditor().searchNext( getFindTextField().getText() );
361
  }
362
363
  /**
364
   * Returns the variable map of interpolated definitions.
365
   *
366
   * @return A map to help dereference variables.
367
   */
368
  private Map<String, String> getResolvedMap() {
369
    return getDefinitionSource().getResolvedMap();
370
  }
371
372
  /**
373
   * Returns the root node for the hierarchical definition source.
374
   *
375
   * @return Data to display in the definition pane.
376
   */
377
  private TreeView<String> getTreeView() {
378
    try {
379
      return getDefinitionSource().asTreeView();
380
    } catch( Exception e ) {
381
      error( e );
382
    }
383
384
    // Slightly redundant as getDefinitionSource() might have returned an
385
    // empty definition source.
386
    return (new EmptyDefinitionSource()).asTreeView();
387
  }
388
389
  /**
390
   * Called when a definition source is opened.
391
   *
392
   * @param path Path to the definition source that was opened.
393
   */
394
  private void openDefinition( final Path path ) {
395
    try {
396
      final DefinitionSource ds = createDefinitionSource( path.toString() );
397
      setDefinitionSource( ds );
398
      storeDefinitionSource();
399
      updateDefinitionPane();
400
    } catch( final Exception e ) {
401
      error( e );
402
    }
403
  }
404
405
  private void updateDefinitionPane() {
406
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
407
  }
408
409
  private void restoreDefinitionSource() {
410
    final Preferences preferences = getPreferences();
411
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, "" );
412
413
    setDefinitionSource( createDefinitionSource( source ) );
414
  }
415
416
  private void storeDefinitionSource() {
417
    final Preferences preferences = getPreferences();
418
    final DefinitionSource ds = getDefinitionSource();
419
420
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
421
  }
422
423
  /**
424
   * Called when the last open tab is closed to clear the preview pane.
425
   */
426
  private void closeRemainingTab() {
427
    getPreviewPane().clear();
428
  }
429
430
  /**
431
   * Called when an exception occurs that warrants the user's attention.
432
   *
433
   * @param e The exception with a message that the user should know about.
434
   */
435
  private void error( final Exception e ) {
436
    getNotifier().notify( e );
437
  }
438
439
  //---- File actions -------------------------------------------------------
440
441
  /**
442
   * Called when an observable instance has changed. This is called by both the
443
   * snitch service and the notify service. The snitch service can be called for
444
   * different file types, including definition sources.
445
   *
446
   * @param observable The observed instance.
447
   * @param value      The noteworthy item.
448
   */
449
  @Override
450
  public void update( final Observable observable, final Object value ) {
451
    if( value != null ) {
452
      if( observable instanceof Snitch && value instanceof Path ) {
453
        final Path path = (Path) value;
454
        final FileTypePredicate predicate
455
            = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
456
457
        // Reload definitions.
458
        if( predicate.test( path.toFile() ) ) {
459
          updateDefinitionSource( path );
460
        }
461
462
        updateSelectedTab();
463
      }
464
      else if( observable instanceof Notifier && value instanceof String ) {
465
        updateStatusBar( (String) value );
466
      }
467
    }
468
  }
469
470
  /**
471
   * Updates the status bar to show the given message.
472
   *
473
   * @param s The message to show in the status bar.
474
   */
475
  private void updateStatusBar( final String s ) {
476
    Platform.runLater(
477
        () -> {
478
          final int index = s.indexOf( '\n' );
479
          final String message = s.substring(
480
              0, index > 0 ? index : s.length() );
481
482
          getStatusBar().setText( message );
483
        }
484
    );
485
  }
486
487
  /**
488
   * Called when a file has been modified.
489
   */
490
  private void updateSelectedTab() {
491
    Platform.runLater(
492
        () -> {
493
          // Brute-force XSLT file reload by re-instantiating all processors.
494
          resetProcessors();
495
          refreshSelectedTab( getActiveFileEditor() );
496
        }
497
    );
498
  }
499
500
  /**
501
   * Reloads the definition source from the given path.
502
   *
503
   * @param path The path containing new definition information.
504
   */
505
  private void updateDefinitionSource( final Path path ) {
506
    Platform.runLater( () -> openDefinition( path ) );
507
  }
508
509
  /**
510
   * After resetting the processors, they will refresh anew to be up-to-date
511
   * with the files (text and definition) currently loaded into the editor.
512
   */
513
  private void resetProcessors() {
514
    getProcessors().clear();
515
  }
516
517
  //---- File actions -------------------------------------------------------
518
  private void fileNew() {
519
    getFileEditorPane().newEditor();
520
  }
521
522
  private void fileOpen() {
523
    getFileEditorPane().openFileDialog();
524
  }
525
526
  private void fileClose() {
527
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
528
  }
529
530
  private void fileCloseAll() {
531
    getFileEditorPane().closeAllEditors();
532
  }
533
534
  private void fileSave() {
535
    getFileEditorPane().saveEditor( getActiveFileEditor() );
536
  }
537
538
  private void fileSaveAs() {
539
    final FileEditorTab editor = getActiveFileEditor();
540
    getFileEditorPane().saveEditorAs( editor );
541
    getProcessors().remove( editor );
542
543
    try {
544
      refreshSelectedTab( editor );
545
    } catch( final Exception ex ) {
546
      getNotifier().notify( ex );
547
    }
548
  }
549
550
  private void fileSaveAll() {
551
    getFileEditorPane().saveAllEditors();
552
  }
553
554
  private void fileExit() {
555
    final Window window = getWindow();
556
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
557
  }
558
559
  //---- R menu actions
560
  private void rScript() {
561
    final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
562
    final RScriptDialog dialog = new RScriptDialog(
563
        getWindow(), "Dialog.r.script.title", script );
564
    final Optional<String> result = dialog.showAndWait();
565
566
    result.ifPresent( this::putStartupScript );
567
  }
568
569
  private void rDirectory() {
570
    final TextInputDialog dialog = new TextInputDialog(
571
        getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
572
    );
573
574
    dialog.setTitle( get( "Dialog.r.directory.title" ) );
575
    dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
576
    dialog.setContentText( "Directory" );
577
578
    final Optional<String> result = dialog.showAndWait();
579
580
    result.ifPresent( this::putStartupDirectory );
581
  }
582
583
  /**
584
   * Stores the R startup script into the user preferences.
585
   */
586
  private void putStartupScript( final String script ) {
587
    putPreference( PERSIST_R_STARTUP, script );
588
  }
589
590
  /**
591
   * Stores the R bootstrap script directory into the user preferences.
592
   */
593
  private void putStartupDirectory( final String directory ) {
594
    putPreference( PERSIST_R_DIRECTORY, directory );
595
  }
596
597
  //---- Help actions -------------------------------------------------------
598
  private void helpAbout() {
599
    Alert alert = new Alert( AlertType.INFORMATION );
600
    alert.setTitle( get( "Dialog.about.title" ) );
601
    alert.setHeaderText( get( "Dialog.about.header" ) );
602
    alert.setContentText( get( "Dialog.about.content" ) );
603
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
604
    alert.initOwner( getWindow() );
605
606
    alert.showAndWait();
607
  }
608
609
  //---- Convenience accessors ----------------------------------------------
610
  private float getFloat( final String key, final float defaultValue ) {
611
    return getPreferences().getFloat( key, defaultValue );
612
  }
613
614
  private Preferences getPreferences() {
615
    return getOptions().getState();
616
  }
617
618
  protected Scene getScene() {
619
    if( this.scene == null ) {
620
      this.scene = createScene();
621
    }
622
623
    return this.scene;
624
  }
625
626
  public Window getWindow() {
627
    return getScene().getWindow();
628
  }
629
630
  private MarkdownEditorPane getActiveEditor() {
631
    final EditorPane pane = getActiveFileEditor().getEditorPane();
632
633
    return pane instanceof MarkdownEditorPane
634
        ? (MarkdownEditorPane) pane
635
        : null;
636
  }
637
638
  private FileEditorTab getActiveFileEditor() {
639
    return getFileEditorPane().getActiveFileEditor();
640
  }
641
642
  //---- Member accessors ---------------------------------------------------
643
  private void setProcessors(
644
      final Map<FileEditorTab, Processor<String>> map ) {
645
    this.processors = map;
646
  }
647
648
  private Map<FileEditorTab, Processor<String>> getProcessors() {
649
    if( this.processors == null ) {
650
      setProcessors( new HashMap<>() );
651
    }
652
653
    return this.processors;
654
  }
655
656
  private FileEditorTabPane getFileEditorPane() {
657
    if( this.fileEditorPane == null ) {
658
      this.fileEditorPane = createFileEditorPane();
659
    }
660
661
    return this.fileEditorPane;
662
  }
663
664
  private HTMLPreviewPane getPreviewPane() {
665
    if( this.previewPane == null ) {
666
      this.previewPane = createPreviewPane();
667
    }
668
669
    return this.previewPane;
670
  }
671
672
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
673
    this.definitionSource = definitionSource;
674
  }
675
676
  private DefinitionSource getDefinitionSource() {
677
    if( this.definitionSource == null ) {
678
      this.definitionSource = new EmptyDefinitionSource();
679
    }
680
681
    return this.definitionSource;
682
  }
683
684
  private DefinitionPane getDefinitionPane() {
685
    if( this.definitionPane == null ) {
686
      this.definitionPane = createDefinitionPane();
687
    }
688
689
    return this.definitionPane;
690
  }
691
692
  private Options getOptions() {
693
    return mOptions;
694
  }
695
696
  private Snitch getSnitch() {
697
    return mSnitch;
698
  }
699
700
  private Notifier getNotifier() {
701
    return mNotifier;
702
  }
703
704
  private Text getLineNumberText() {
705
    if( this.lineNumberText == null ) {
706
      this.lineNumberText = createLineNumberText();
707
    }
708
709
    return this.lineNumberText;
710
  }
711
712
  private synchronized StatusBar getStatusBar() {
713
    if( this.statusBar == null ) {
714
      this.statusBar = createStatusBar();
715
    }
716
717
    return this.statusBar;
718
  }
719
720
  private TextField getFindTextField() {
721
    if( this.findTextField == null ) {
722
      this.findTextField = createFindTextField();
723
    }
724
725
    return this.findTextField;
726
  }
727
728
  //---- Member creators ----------------------------------------------------
729
730
  /**
731
   * Factory to create processors that are suited to different file types.
732
   *
733
   * @param tab The tab that is subjected to processing.
734
   * @return A processor suited to the file type specified by the tab's path.
735
   */
736
  private Processor<String> createProcessor( final FileEditorTab tab ) {
737
    return createProcessorFactory().createProcessor( tab );
738
  }
739
740
  private ProcessorFactory createProcessorFactory() {
741
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
742
  }
743
744
  private DefinitionSource createDefinitionSource( final String path ) {
745
    DefinitionSource ds;
746
747
    try {
748
      ds = createDefinitionFactory().createDefinitionSource( path );
749
750
      if( ds instanceof FileDefinitionSource ) {
751
        try {
752
          getNotifier().notify( ds.getError() );
753
          getSnitch().listen( ((FileDefinitionSource) ds).getPath() );
754
        } catch( final Exception ex ) {
755
          error( ex );
756
        }
757
      }
758
    } catch( final Exception ex ) {
759
      ds = new EmptyDefinitionSource();
760
      error( ex );
761
    }
762
763
    return ds;
764
  }
765
766
  private TextField createFindTextField() {
767
    return new TextField();
768
  }
769
770
  /**
771
   * Create an editor pane to hold file editor tabs.
772
   *
773
   * @return A new instance, never null.
774
   */
775
  private FileEditorTabPane createFileEditorPane() {
776
    return new FileEditorTabPane();
777
  }
778
779
  private HTMLPreviewPane createPreviewPane() {
780
    return new HTMLPreviewPane();
781
  }
782
783
  private DefinitionPane createDefinitionPane() {
784
    return new DefinitionPane( getTreeView() );
785
  }
786
787
  private DefinitionFactory createDefinitionFactory() {
788
    return new DefinitionFactory();
789
  }
790
791
  private StatusBar createStatusBar() {
792
    return new StatusBar();
793
  }
794
795
  private Scene createScene() {
796
    final SplitPane splitPane = new SplitPane(
797
        getDefinitionPane().getNode(),
798
        getFileEditorPane().getNode(),
799
        getPreviewPane().getNode() );
800
801
    splitPane.setDividerPositions(
802
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
803
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
804
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
805
806
    // See: http://broadlyapplicable.blogspot
807
    // .ca/2015/03/javafx-capture-restorePreferences-splitpane.html
808
    final BorderPane borderPane = new BorderPane();
809
    borderPane.setPrefSize( 1024, 800 );
810
    borderPane.setTop( createMenuBar() );
811
    borderPane.setBottom( getStatusBar() );
812
    borderPane.setCenter( splitPane );
813
814
    final VBox box = new VBox();
815
    box.setAlignment( Pos.BASELINE_CENTER );
816
    box.getChildren().add( getLineNumberText() );
817
    getStatusBar().getRightItems().add( box );
818
819
    return new Scene( borderPane );
820
  }
821
822
  private Text createLineNumberText() {
823
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
824
  }
825
826
  private Node createMenuBar() {
827
    final BooleanBinding activeFileEditorIsNull =
828
        getFileEditorPane().activeFileEditorProperty()
829
                           .isNull();
830
831
    // File actions
832
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
833
                                             "Shortcut+N", FILE_ALT,
834
                                             e -> fileNew() );
835
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
836
                                              "Shortcut+O", FOLDER_OPEN_ALT,
837
                                              e -> fileOpen() );
838
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
839
                                               "Shortcut+W", null,
840
                                               e -> fileClose(),
841
                                               activeFileEditorIsNull );
842
    final Action fileCloseAllAction = new Action( get(
843
        "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
844
                                                  activeFileEditorIsNull );
845
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
846
                                              "Shortcut+S", FLOPPY_ALT,
847
                                              e -> fileSave(),
848
                                              createActiveBooleanProperty(
849
                                                  FileEditorTab::modifiedProperty )
850
                                                  .not() );
851
    final Action fileSaveAsAction = new Action( Messages.get(
852
        "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
853
                                                activeFileEditorIsNull );
854
    final Action fileSaveAllAction = new Action(
855
        get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
856
        e -> fileSaveAll(),
857
        Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
858
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ),
859
                                              null,
860
                                              null,
861
                                              e -> fileExit() );
862
863
    // Edit actions
864
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
865
                                              "Shortcut+Z", UNDO,
866
                                              e -> getActiveEditor().undo(),
867
                                              createActiveBooleanProperty(
868
                                                  FileEditorTab::canUndoProperty )
869
                                                  .not() );
870
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
871
                                              "Shortcut+Y", REPEAT,
872
                                              e -> getActiveEditor().redo(),
873
                                              createActiveBooleanProperty(
874
                                                  FileEditorTab::canRedoProperty )
875
                                                  .not() );
876
    final Action editFindAction = new Action( Messages.get(
877
        "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
878
                                              e -> find(),
879
                                              activeFileEditorIsNull );
880
    final Action editFindNextAction = new Action( Messages.get(
881
        "Main.menu.edit.find.next" ), "F3", null,
882
                                                  e -> findNext(),
883
                                                  activeFileEditorIsNull );
884
885
    // Insert actions
886
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
887
                                                "Shortcut+B", BOLD,
888
                                                e -> getActiveEditor().surroundSelection(
889
                                                    "**", "**" ),
890
                                                activeFileEditorIsNull );
891
    final Action insertItalicAction = new Action(
892
        get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
893
        e -> getActiveEditor().surroundSelection( "*", "*" ),
894
        activeFileEditorIsNull );
895
    final Action insertSuperscriptAction = new Action( get(
896
        "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
897
                                                       e -> getActiveEditor().surroundSelection(
898
                                                           "^", "^" ),
899
                                                       activeFileEditorIsNull );
900
    final Action insertSubscriptAction = new Action( get(
901
        "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
902
                                                     e -> getActiveEditor().surroundSelection(
903
                                                         "~", "~" ),
904
                                                     activeFileEditorIsNull );
905
    final Action insertStrikethroughAction = new Action( get(
906
        "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
907
                                                         e -> getActiveEditor().surroundSelection(
908
                                                             "~~", "~~" ),
909
                                                         activeFileEditorIsNull );
910
    final Action insertBlockquoteAction = new Action( get(
911
        "Main.menu.insert.blockquote" ),
912
                                                      "Ctrl+Q",
913
                                                      QUOTE_LEFT,
914
                                                      // not Shortcut+Q
915
                                                      // because of conflict
916
                                                      // on Mac
917
                                                      e -> getActiveEditor().surroundSelection(
918
                                                          "\n\n> ", "" ),
919
                                                      activeFileEditorIsNull );
920
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
921
                                                "Shortcut+K", CODE,
922
                                                e -> getActiveEditor().surroundSelection(
923
                                                    "`", "`" ),
924
                                                activeFileEditorIsNull );
925
    final Action insertFencedCodeBlockAction = new Action( get(
926
        "Main.menu.insert.fenced_code_block" ),
927
                                                           "Shortcut+Shift+K",
928
                                                           FILE_CODE_ALT,
929
                                                           e -> getActiveEditor()
930
                                                               .surroundSelection(
931
                                                                   "\n\n```\n",
932
                                                                   "\n```\n\n",
933
                                                                   get(
934
                                                                       "Main.menu.insert.fenced_code_block.prompt" ) ),
935
                                                           activeFileEditorIsNull );
936
937
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
938
                                                "Shortcut+L", LINK,
939
                                                e -> getActiveEditor().insertLink(),
940
                                                activeFileEditorIsNull );
941
    final Action insertImageAction = new Action( get( "Main.menu.insert" +
942
                                                          ".image" ),
943
                                                 "Shortcut+G", PICTURE_ALT,
944
                                                 e -> getActiveEditor().insertImage(),
945
                                                 activeFileEditorIsNull );
946
947
    final Action[] headers = new Action[ 6 ];
948
949
    // Insert header actions (H1 ... H6)
950
    for( int i = 1; i <= 6; i++ ) {
951
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
952
      final String markup = String.format( "%n%n%s ", hashes );
953
      final String text = get( "Main.menu.insert.header_" + i );
954
      final String accelerator = "Shortcut+" + i;
955
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
956
957
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
958
                                     e -> getActiveEditor().surroundSelection(
959
                                         markup, "", prompt ),
960
                                     activeFileEditorIsNull );
961
    }
962
963
    final Action insertUnorderedListAction = new Action(
964
        get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
965
        e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
966
        activeFileEditorIsNull );
967
    final Action insertOrderedListAction = new Action(
968
        get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
969
        e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
970
        activeFileEditorIsNull );
971
    final Action insertHorizontalRuleAction = new Action(
972
        get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
973
        e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
974
        activeFileEditorIsNull );
975
976
    // R actions
977
    final Action mRScriptAction = new Action(
978
        get( "Main.menu.r.script" ), null, null, e -> rScript() );
979
980
    final Action mRDirectoryAction = new Action(
981
        get( "Main.menu.r.directory" ), null, null, e -> rDirectory() );
982
983
    // Help actions
984
    final Action helpAboutAction = new Action(
985
        get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
986
987
    //---- MenuBar ----
988
    final Menu fileMenu = ActionUtils.createMenu(
989
        get( "Main.menu.file" ),
990
        fileNewAction,
991
        fileOpenAction,
992
        null,
993
        fileCloseAction,
994
        fileCloseAllAction,
995
        null,
996
        fileSaveAction,
997
        fileSaveAsAction,
998
        fileSaveAllAction,
999
        null,
1000
        fileExitAction );
1001
1002
    final Menu editMenu = ActionUtils.createMenu(
1003
        get( "Main.menu.edit" ),
1004
        editUndoAction,
1005
        editRedoAction,
1006
        editFindAction,
1007
        editFindNextAction );
1008
1009
    final Menu insertMenu = ActionUtils.createMenu(
1010
        get( "Main.menu.insert" ),
1011
        insertBoldAction,
1012
        insertItalicAction,
1013
        insertSuperscriptAction,
1014
        insertSubscriptAction,
1015
        insertStrikethroughAction,
1016
        insertBlockquoteAction,
1017
        insertCodeAction,
1018
        insertFencedCodeBlockAction,
1019
        null,
1020
        insertLinkAction,
1021
        insertImageAction,
1022
        null,
1023
        headers[ 0 ],
1024
        headers[ 1 ],
1025
        headers[ 2 ],
1026
        headers[ 3 ],
1027
        headers[ 4 ],
1028
        headers[ 5 ],
1029
        null,
1030
        insertUnorderedListAction,
1031
        insertOrderedListAction,
1032
        insertHorizontalRuleAction );
1033
1034
    final Menu rMenu = ActionUtils.createMenu(
1035
        get( "Main.menu.r" ),
1036
        mRScriptAction,
1037
        mRDirectoryAction );
1038
1039
    final Menu helpMenu = ActionUtils.createMenu(
1040
        get( "Main.menu.help" ),
1041
        helpAboutAction );
1042
1043
    final MenuBar menuBar = new MenuBar(
1044
        fileMenu,
1045
        editMenu,
1046
        insertMenu,
1047
        rMenu,
1048
        helpMenu );
1049
1050
    //---- ToolBar ----
1051
    final ToolBar toolBar = ActionUtils.createToolBar(
1052
        fileNewAction,
1053
        fileOpenAction,
1054
        fileSaveAction,
1055
        null,
1056
        editUndoAction,
1057
        editRedoAction,
1058
        null,
1059
        insertBoldAction,
1060
        insertItalicAction,
1061
        insertSuperscriptAction,
1062
        insertSubscriptAction,
1063
        insertBlockquoteAction,
1064
        insertCodeAction,
1065
        insertFencedCodeBlockAction,
1066
        null,
1067
        insertLinkAction,
1068
        insertImageAction,
1069
        null,
1070
        headers[ 0 ],
1071
        null,
1072
        insertUnorderedListAction,
1073
        insertOrderedListAction );
1074
1075
    return new VBox( menuBar, toolBar );
1076
  }
1077
1078
  /**
1079
   * Creates a boolean property that is bound to another boolean value of the
1080
   * active editor.
1081
   */
1082
  private BooleanProperty createActiveBooleanProperty(
1083
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1084
1085
    final BooleanProperty b = new SimpleBooleanProperty();
1086
    final FileEditorTab tab = getActiveFileEditor();
1087
1088
    if( tab != null ) {
1089
      b.bind( func.apply( tab ) );
1090
    }
1091
1092
    getFileEditorPane().activeFileEditorProperty().addListener(
1093
        ( observable, oldFileEditor, newFileEditor ) -> {
1094
          b.unbind();
1095
1096
          if( newFileEditor != null ) {
1097
            b.bind( func.apply( newFileEditor ) );
1098
          }
1099
          else {
1100
            b.set( false );
1101
          }
1102
        }
1103
    );
1104
1105
    return b;
1106
  }
1107
1108
  private void initLayout() {
1109
    final Scene appScene = getScene();
1110
1111
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1112
1113
    // TODO: Apply an XML syntax highlighting for XML files.
1114
//    appScene.getStylesheets().add( STYLESHEET_XML );
1115
    appScene.windowProperty().addListener(
1116
        ( observable, oldWindow, newWindow ) -> {
1117
          newWindow.setOnCloseRequest( e -> {
1118
            if( !getFileEditorPane().closeAllEditors() ) {
1119
              e.consume();
1120
            }
1121
          } );
11561122
        }
11571123
    );
M src/main/java/com/scrivenvar/Messages.java
2727
package com.scrivenvar;
2828
29
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
30
3129
import java.text.MessageFormat;
3230
import java.util.ResourceBundle;
3331
import java.util.Stack;
32
33
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
34
import static java.util.ResourceBundle.getBundle;
3435
3536
/**
...
4243
4344
  private static final ResourceBundle RESOURCE_BUNDLE =
44
      ResourceBundle.getBundle(
45
          APP_BUNDLE_NAME );
45
      getBundle( APP_BUNDLE_NAME );
4646
4747
  private Messages() {
...
111111
   */
112112
  public static String get( String key ) {
113
    String result;
114
115113
    try {
116
      result = resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
114
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
117115
    } catch( final Exception ex ) {
118
      result = key;
116
      return key;
119117
    }
120
121
    return result;
122118
  }
123119
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
8989
      final FileType filetype, final Path path ) {
9090
91
    DefinitionSource result = null;
92
93
    if( filetype == YAML ) {
94
      result = new YamlFileDefinitionSource( path );
95
    }
96
    else {
97
      unknownFileType( filetype, path );
98
    }
99
100
    return result;
91
    return filetype == YAML
92
        ? new YamlFileDefinitionSource( path )
93
        : new EmptyDefinitionSource();
10194
  }
10295
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
2929
3030
import com.scrivenvar.AbstractPane;
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 );
31
import com.scrivenvar.predicates.strings.ContainsPredicate;
32
import com.scrivenvar.predicates.strings.StartsPredicate;
33
import com.scrivenvar.predicates.strings.StringPredicate;
34
import javafx.collections.ObservableList;
35
import javafx.event.EventHandler;
36
import javafx.event.EventType;
37
import javafx.scene.Node;
38
import javafx.scene.control.MultipleSelectionModel;
39
import javafx.scene.control.SelectionMode;
40
import javafx.scene.control.TreeItem;
41
import javafx.scene.control.TreeView;
42
import javafx.scene.input.MouseButton;
43
import javafx.scene.input.MouseEvent;
44
45
import java.util.List;
46
47
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
48
import static com.scrivenvar.util.Lists.getFirst;
49
import static javafx.scene.input.MouseButton.PRIMARY;
50
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
51
52
/**
53
 * Provides a list of variables that can be referenced in the editor.
54
 *
55
 * @author White Magic Software, Ltd.
56
 */
57
public class DefinitionPane extends AbstractPane {
58
59
  /**
60
   * Trimmed off the end of a word to match a variable name.
61
   */
62
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
63
64
  private final TreeView<String> mTreeView;
65
66
  /**
67
   * Constructs a definition pane with a given tree view root.
68
   * See {@link com.scrivenvar.definition.yaml.YamlTreeAdapter#adapt(String)}
69
   * for details.
70
   *
71
   * @param root The root of the variable definition tree.
72
   */
73
  public DefinitionPane( final TreeView<String> root ) {
74
    assert root != null;
75
76
    mTreeView = root;
77
    initTreeView();
78
  }
79
80
  /**
81
   * Allows observers to receive double-click events on the tree view.
82
   *
83
   * @param handler The handler that will receive double-click events.
84
   */
85
  public void addBranchSelectedListener(
86
      final EventHandler<? super MouseEvent> handler ) {
87
    getTreeView().addEventHandler(
88
        MouseEvent.ANY, event -> {
89
          final MouseButton button = event.getButton();
90
          final int clicks = event.getClickCount();
91
          final EventType<? extends MouseEvent> eventType =
92
              event.getEventType();
93
94
          if( PRIMARY.equals( button ) && clicks == 2 ) {
95
            if( MOUSE_CLICKED.equals( eventType ) ) {
96
              handler.handle( event );
97
            }
98
99
            event.consume();
100
          }
101
        } );
102
  }
103
104
  /**
105
   * Changes the root node of the tree view. Swaps the current root node for the
106
   * root node of the given
107
   *
108
   * @param treeView The tree view containing a new root node; if the parameter
109
   *                 is null, the tree is cleared.
110
   */
111
  public void setRoot( final TreeView<String> treeView ) {
112
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
113
  }
114
115
  /**
116
   * Finds a tree item with a value that exactly matches the given word.
117
   *
118
   * @param trunk     The root item containing a list of nodes to search.
119
   * @param predicate Helps determine whether the node value matches the word.
120
   * @return The item that matches the given word, or null if not found.
121
   */
122
  private TreeItem<String> findNode(
123
      final TreeItem<String> trunk,
124
      final StringPredicate predicate ) {
125
    TreeItem<String> result = null;
126
127
    if( trunk != null ) {
128
      final List<TreeItem<String>> branches = trunk.getChildren();
129
130
      for( final TreeItem<String> leaf : branches ) {
131
        if( predicate.test( leaf.getValue() ) ) {
132
          result = leaf;
133
          break;
134
        }
135
      }
136
    }
137
138
    return result;
139
  }
140
141
  /**
142
   * Calls findNode with the EqualsPredicate. See
143
   * {@link #findNode(TreeItem, StringPredicate)} for details.
144
   *
145
   * @return The result from findNode.
146
   */
147
  private TreeItem<String> findStartsNode(
148
      final TreeItem<String> trunk,
149
      final String word ) {
150
    return findNode( trunk, new StartsPredicate( word ) );
151
  }
152
153
  /**
154
   * Calls findNode with the ContainsPredicate. See
155
   * {@link #findNode(TreeItem, StringPredicate)} for details.
156
   *
157
   * @return The result from findNode.
158
   */
159
  private TreeItem<String> findSubstringNode(
160
      final TreeItem<String> trunk,
161
      final String word ) {
162
    return findNode( trunk, new ContainsPredicate( word ) );
163
  }
164
165
  /**
166
   * Finds a node that matches a prefix and suffix specified by the given path
167
   * variable. The prefix must match a valid node value. The suffix refers to
168
   * the start of a string that matches zero or more children of the node
169
   * specified by the prefix. The algorithm has the following cases:
170
   *
171
   * <ol>
172
   * <li>Path is empty, return first child.</li>
173
   * <li>Path contains a complete match, return corresponding node.</li>
174
   * <li>Path contains a partial match, return nearest node.</li>
175
   * <li>Path contains a complete and partial match, return nearest node.</li>
176
   * </ol>
177
   *
178
   * @param word The word typed by the user, which contains dot-separated node
179
   *             names that represent a path within the YAML tree plus a
180
   *             partial variable
181
   *             name match (for a node).
182
   * @return The node value that starts with the suffix portion of the given
183
   * path, never null.
184
   */
185
  public TreeItem<String> findNode( final String word ) {
186
    String path = word;
187
188
    // Current tree item.
189
    TreeItem<String> cItem = getTreeRoot();
190
191
    // Previous tree item.
192
    TreeItem<String> pItem = cItem;
193
194
    int index = path.indexOf( SEPARATOR_CHAR );
195
196
    while( index >= 0 ) {
197
      final String node = path.substring( 0, index );
198
      path = path.substring( index + 1 );
199
200
      if( (cItem = findStartsNode( cItem, node )) == null ) {
201
        break;
202
      }
203
204
      index = path.indexOf( SEPARATOR_CHAR );
205
      pItem = cItem;
206
    }
207
208
    // Find the node that starts with whatever the user typed.
209
    cItem = findStartsNode( pItem, path );
210
211
    // If there was no matching node, then find a substring match.
212
    if( cItem == null ) {
213
      cItem = findSubstringNode( pItem, path );
214
    }
215
216
    // If neither starts with nor substring matched a node, revert to the last
217
    // known valid node.
218
    if( cItem == null ) {
219
      cItem = pItem;
220
    }
221
222
    return sanitize( cItem );
223
  }
224
225
  /**
226
   * Returns the leaf that matches the given value. If the value is terminally
227
   * punctuated, the punctuation is removed if no match was found.
228
   *
229
   * @param value    The value to find, never null.
230
   * @param contains Set to true to perform a substring match if starts with
231
   *                 fails to match.
232
   * @return The leaf that contains the given value, or null if neither the
233
   * original value nor the terminally-trimmed value was found.
234
   */
235
  public VariableTreeItem<String> findLeaf(
236
      final String value,
237
      final boolean contains ) {
238
239
    final VariableTreeItem<String> root = getTreeRoot();
240
    final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
241
242
    return leaf == null
243
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
244
        : leaf;
245
  }
246
247
  /**
248
   * Removes punctuation from the end of a string. The character set includes:
249
   * <code>:;,.!?-/\¡¿</code>.
250
   *
251
   * @param s The string to trim, never null.
252
   * @return The string trimmed of all terminal characters from the end
253
   */
254
  private String rtrimTerminalPunctuation( final String s ) {
255
    final StringBuilder result = new StringBuilder( s.trim() );
256
257
    while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
258
      result.setLength( result.length() - 1 );
259
    }
260
261
    return result.toString();
262
  }
263
264
  /**
265
   * Returns the tree root if either item or its first child are null.
266
   *
267
   * @param item The item to make null safe.
268
   * @return A non-null TreeItem, possibly the root item (to avoid null).
269
   */
270
  private TreeItem<String> sanitize( final TreeItem<String> item ) {
271
    TreeItem<String> result;
272
273
    if( item == null ) {
274
      result = getTreeRoot();
275
    }
276
    else {
277
      result = item == getTreeRoot()
278
          ? getFirst( item.getChildren() )
279
          : item;
280
    }
281
282
    return result;
283
  }
284
285
  /**
286
   * Expands the node to the root, recursively.
287
   *
288
   * @param <T>  The type of tree item to expand (usually String).
289
   * @param node The node to expand.
290
   */
291
  public <T> void expand( final TreeItem<T> node ) {
292
    if( node != null ) {
293
      expand( node.getParent() );
294
295
      if( !node.isLeaf() ) {
296
        node.setExpanded( true );
297
      }
298
    }
299
  }
300
301
  public void select( final TreeItem<String> item ) {
302
    clearSelection();
303
    selectItem( getTreeView().getRow( item ) );
304
  }
305
306
  private void clearSelection() {
307
    getSelectionModel().clearSelection();
308
  }
309
310
  private void selectItem( final int row ) {
311
    getSelectionModel().select( row );
312
  }
313
314
  /**
315
   * Collapses the tree, recursively.
316
   */
317
  public void collapse() {
318
    collapse( getTreeRoot().getChildren() );
319
  }
320
321
  /**
322
   * Collapses the tree, recursively.
323
   *
324
   * @param <T>   The type of tree item to expand (usually String).
325
   * @param nodes The nodes to collapse.
326
   */
327
  private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
328
    for( final TreeItem<T> node : nodes ) {
329
      node.setExpanded( false );
330
      collapse( node.getChildren() );
331
    }
332
  }
333
334
  private void initTreeView() {
335
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
336
  }
337
338
  /**
339
   * Returns the root node to the tree view.
340
   *
341
   * @return getTreeView()
342
   */
343
  public Node getNode() {
344
    return getTreeView();
345
  }
346
347
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
348
    return getTreeView().getSelectionModel();
349
  }
350
351
  /**
352
   * Returns the tree view that contains the YAML definition hierarchy.
353
   *
354
   * @return A non-null instance.
355
   */
356
  private TreeView<String> getTreeView() {
357
    return mTreeView;
358
  }
359
360
  /**
361
   * Returns the root of the tree.
362
   *
363
   * @return The first node added to the YAML definition tree, or a new root
364
   * if no first node could be found.
365
   */
366
  private VariableTreeItem<String> getTreeRoot() {
367
    final TreeItem<String> root = getTreeView().getRoot();
368
369
    return root instanceof VariableTreeItem ?
370
        (VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
409371
  }
410372
}
M src/main/java/com/scrivenvar/definition/VariableTreeItem.java
213213
    for( final TreeItem<T> child : parent.getChildren() ) {
214214
      if( child.isLeaf() ) {
215
        @SuppressWarnings( "unchecked" )
216215
        final String key = toVariable( ((VariableTreeItem<String>)child).toPath() );
217216
        final String value = child.getValue().toString();
M src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
2828
package com.scrivenvar.definition.yaml;
2929
30
import static com.scrivenvar.Messages.get;
3130
import com.scrivenvar.definition.FileDefinitionSource;
31
import javafx.scene.control.TreeView;
32
3233
import java.io.InputStream;
3334
import java.nio.file.Files;
3435
import java.nio.file.Path;
3536
import java.util.Map;
36
import javafx.scene.control.TreeView;
37
38
import static com.scrivenvar.Messages.get;
3739
3840
/**
...
5557
    init();
5658
  }
57
  
59
5860
  private void init() {
5961
    setYamlParser( createYamlParser() );
...
100102
  protected TreeView<String> createTreeView() {
101103
    return getYamlTreeAdapter().adapt(
102
      get( "Pane.defintion.node.root.title" )
104
        get( "Pane.definition.node.root.title" )
103105
    );
104106
  }
M src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
315315
   * Writes the modified YAML document to standard output.
316316
   */
317
  @SuppressWarnings("unused")
317318
  private void writeDocument() throws IOException {
318319
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
M src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLFactory.java
3131
import java.io.Writer;
3232
33
3433
/**
34
 * Responsible for producing YAML generators.
3535
 *
3636
 * @author White Magic Software, Ltd.
M src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLGenerator.java
11
/*
2
 * The MIT License
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
4
 * Copyright 2017 White Magic Software, Ltd..
4
 * All rights reserved.
55
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
128
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
1511
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
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.
2327
 */
2428
package com.scrivenvar.definition.yaml.resolvers;
2529
26
import com.fasterxml.jackson.core.JsonGenerationException;
2730
import com.fasterxml.jackson.core.ObjectCodec;
2831
import com.fasterxml.jackson.core.io.IOContext;
2932
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
3033
import com.scrivenvar.definition.yaml.YamlParser;
34
import org.yaml.snakeyaml.DumperOptions;
35
3136
import java.io.IOException;
3237
import java.io.Writer;
33
import org.yaml.snakeyaml.DumperOptions;
3438
35
/**
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
3939
/**
4040
 * Intercepts the string writing functionality to resolve the definition
4141
 * value.
4242
 */
4343
public class ResolverYAMLGenerator extends YAMLGenerator {
4444
4545
  private YamlParser yamlParser;
4646
4747
  public ResolverYAMLGenerator(
48
    final YamlParser yamlParser,
49
    final IOContext ctxt,
50
    final int jsonFeatures,
51
    final int yamlFeatures,
52
    final ObjectCodec codec,
53
    final Writer out,
54
    final DumperOptions.Version version ) throws IOException {
48
      final YamlParser yamlParser,
49
      final IOContext ctxt,
50
      final int jsonFeatures,
51
      final int yamlFeatures,
52
      final ObjectCodec codec,
53
      final Writer out,
54
      final DumperOptions.Version version ) throws IOException {
5555
    super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
5656
    setYamlParser( yamlParser );
5757
  }
5858
5959
  @Override
60
  public void writeString( final String text )
61
    throws IOException, JsonGenerationException {
60
  public void writeString( final String text ) throws IOException {
6261
    final YamlParser parser = getYamlParser();
6362
    super.writeString( parser.substitute( text ) );
M src/main/java/com/scrivenvar/editors/EditorPane.java
5757
public class EditorPane extends AbstractPane {
5858
59
  private StyleClassedTextArea editor;
60
  private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
61
  private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
59
  private final StyleClassedTextArea mEditor =
60
      new StyleClassedTextArea( false );
61
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
62
      new VirtualizedScrollPane<>( mEditor );
63
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
6264
6365
  /**
6466
   * Set when entering variable edit mode; retrieved upon exiting.
6567
   */
66
  private InputMap<InputEvent> nodeMap;
68
  private InputMap<InputEvent> mNodeMap;
69
70
  public EditorPane() {
71
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
72
  }
6773
6874
  @Override
...
136142
  @SuppressWarnings("unchecked")
137143
  public void addEventListener( final InputMap<InputEvent> map ) {
138
    this.nodeMap = (InputMap<InputEvent>) getInputMap();
144
    mNodeMap = (InputMap<InputEvent>) getInputMap();
139145
    Nodes.addInputMap( getEditor(), map );
140146
  }
...
148154
  public void removeEventListener( final InputMap<InputEvent> map ) {
149155
    Nodes.removeInputMap( getEditor(), map );
150
    Nodes.addInputMap( getEditor(), this.nodeMap );
156
    Nodes.addInputMap( getEditor(), mNodeMap );
151157
  }
152158
...
175181
    getEditor().moveTo( 0 );
176182
    getScrollPane().scrollYToPixel( 0 );
177
  }
178
179
  private void setEditor( final StyleClassedTextArea textArea ) {
180
    this.editor = textArea;
181183
  }
182
183
  public synchronized StyleClassedTextArea getEditor() {
184
    if( this.editor == null ) {
185
      setEditor( createTextArea() );
186
    }
187184
188
    return this.editor;
185
  public StyleClassedTextArea getEditor() {
186
    return mEditor;
189187
  }
190188
191189
  /**
192190
   * Returns the scroll pane that contains the text area.
193191
   *
194192
   * @return The scroll pane that contains the content to edit.
195193
   */
196
  public synchronized VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
197
    if( this.scrollPane == null ) {
198
      this.scrollPane = createScrollPane();
199
    }
200
201
    return this.scrollPane;
202
  }
203
204
  protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() {
205
    final VirtualizedScrollPane<StyleClassedTextArea> pane
206
        = new VirtualizedScrollPane<>( getEditor() );
207
    pane.setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
208
209
    return pane;
210
  }
211
212
  protected StyleClassedTextArea createTextArea() {
213
    return new StyleClassedTextArea( false );
194
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
195
    return mScrollPane;
214196
  }
215197
216198
  public Path getPath() {
217
    return this.path.get();
199
    return mPath.get();
218200
  }
219201
220202
  public void setPath( final Path path ) {
221
    this.path.set( path );
203
    mPath.set( path );
222204
  }
223205
}
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
229229
230230
  private Path getParentPath() {
231
    final Path parentPath = getPath();
232
    return (parentPath != null) ? parentPath.getParent() : null;
231
    final Path path = getPath();
232
    return (path != null) ? path.getParent() : null;
233233
  }
234234
M src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
4242
public class FileTypePredicate implements Predicate<File> {
4343
44
  private final PathMatcher matcher;
44
  private final PathMatcher mMatcher;
4545
4646
  /**
4747
   * Constructs a new instance given a set of file extension globs.
4848
   *
4949
   * @param patterns Comma-separated list of globbed extensions including the
5050
   * Kleene star (e.g., <code>*.md,*.markdown,*.txt</code>).
5151
   */
5252
  public FileTypePredicate( final String patterns ) {
53
    this.matcher = FileSystems.getDefault().getPathMatcher(
54
      "glob:**/{" + patterns + "}"
53
    mMatcher = FileSystems.getDefault().getPathMatcher(
54
      "glob:**{" + patterns + "}"
5555
    );
5656
  }
...
7979
8080
  private PathMatcher getMatcher() {
81
    return this.matcher;
81
    return mMatcher;
8282
  }
8383
}
M src/main/java/com/scrivenvar/preferences/FilePreferences.java
3131
import java.io.FileInputStream;
3232
import java.io.FileOutputStream;
33
import java.io.IOException;
34
import java.util.ArrayList;
35
import java.util.Enumeration;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Properties;
39
import java.util.TreeMap;
33
import java.util.*;
4034
import java.util.prefs.AbstractPreferences;
4135
import java.util.prefs.BackingStoreException;
...
5549
  private boolean mRemoved;
5650
57
  public FilePreferences( final AbstractPreferences parent, final String name ) {
51
  private final Object mMutex = new Object();
52
53
  public FilePreferences( final AbstractPreferences parent,
54
                          final String name ) {
5855
    super( parent, name );
5956
...
132129
    }
133130
134
    synchronized( file ) {
131
    synchronized( mMutex ) {
135132
      final Properties p = new Properties();
136133
137134
      try {
138135
        p.load( new FileInputStream( file ) );
139136
140137
        final String path = getPath();
141138
        final Enumeration<?> pnen = p.propertyNames();
142139
143140
        while( pnen.hasMoreElements() ) {
144
          final String propKey = (String)pnen.nextElement();
141
          final String propKey = (String) pnen.nextElement();
145142
146143
          if( propKey.startsWith( path ) ) {
...
160157
161158
  private String getPath() {
162
    final FilePreferences parent = (FilePreferences)parent();
159
    final FilePreferences parent = (FilePreferences) parent();
163160
164161
    return parent == null ? "" : parent.getPath() + name() + '.';
165162
  }
166163
167164
  @Override
168165
  protected void flushSpi() {
169166
    final File file = FilePreferencesFactory.getPreferencesFile();
170167
171
    synchronized( file ) {
168
    synchronized( mMutex ) {
172169
      final Properties p = new Properties();
173170
...
184181
185182
          while( pnen.hasMoreElements() ) {
186
            String propKey = (String)pnen.nextElement();
183
            String propKey = (String) pnen.nextElement();
187184
            if( propKey.startsWith( path ) ) {
188185
              final String subKey = propKey.substring( path.length() );
M src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
3030
import static com.scrivenvar.Constants.CARET_POSITION_BASE;
3131
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
32
3233
import java.nio.file.Path;
34
3335
import javafx.beans.value.ObservableValue;
3436
import javafx.concurrent.Worker.State;
37
3538
import static javafx.concurrent.Worker.State.SUCCEEDED;
39
3640
import javafx.scene.Node;
3741
import javafx.scene.layout.Pane;
...
6771
    // Scrolls to the caret after the content has been loaded.
6872
    getEngine().getLoadWorker().stateProperty().addListener(
69
      (ObservableValue<? extends State> observable,
70
        final State oldValue, final State newValue) -> {
71
        if( newValue == SUCCEEDED ) {
72
          scrollToCaret();
73
        }
74
      } );
73
        ( ObservableValue<? extends State> observable,
74
          final State oldValue, final State newValue ) -> {
75
          if( newValue == SUCCEEDED ) {
76
            scrollToCaret();
77
          }
78
        } );
7579
  }
7680
...
8387
  private String getBase() {
8488
    final Path basePath = getPath();
89
    final Path parent = basePath == null ? null : basePath.getParent();
8590
86
    return basePath == null
87
      ? ""
88
      : ("<base href='" + basePath.getParent().toUri().toString() + "'>");
91
    return parent == null
92
        ? ""
93
        : ("<base href='" + parent.toUri().toString() + "'>");
8994
  }
9095
...
97102
  public void update( final String html ) {
98103
    getEngine().loadContent(
99
      "<!DOCTYPE html>"
100
      + "<html>"
101
      + "<head>"
102
      + "<link rel='stylesheet' href='" + getClass().getResource( STYLESHEET_PREVIEW ) + "'>"
103
      + getBase()
104
      + "</head>"
105
      + "<body>"
106
      + html
107
      + "</body>"
108
      + "</html>" );
104
        "<!DOCTYPE html>"
105
            + "<html>"
106
            + "<head>"
107
            + "<link rel='stylesheet' href='" + getClass().getResource(
108
            STYLESHEET_PREVIEW ) + "'>"
109
            + getBase()
110
            + "</head>"
111
            + "<body>"
112
            + html
113
            + "</body>"
114
            + "</html>" );
109115
  }
110116
...
122128
    execute( getScrollScript() );
123129
  }
124
  
130
125131
  /**
126132
   * Returns the JavaScript used to scroll the WebView pane.
127133
   *
128134
   * @return A script that tries to center the view port on the CARET POSITION.
129135
   */
130136
  private String getScrollScript() {
131137
    return ""
132
      + "var e = document.getElementById('" + CARET_POSITION_BASE + "');"
133
      + "if( e != null ) { "
134
      + "  Element.prototype.topOffset = function () {"
135
      + "    return this.offsetTop + (this.offsetParent ? this.offsetParent.topOffset() : 0);"
136
      + "  };"
137
      + "  window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );"
138
      + "}";
138
        + "var e = document.getElementById('" + CARET_POSITION_BASE + "');"
139
        + "if( e != null ) { "
140
        + "  Element.prototype.topOffset = function () {"
141
        + "    return this.offsetTop + (this.offsetParent ? this.offsetParent" +
142
        ".topOffset() : 0);"
143
        + "  };"
144
        + "  window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );"
145
        + "}";
139146
  }
140147
...
163170
164171
  public void setPath( final Path path ) {
172
    assert path != null;
173
165174
    this.path = path;
166175
  }
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
152152
153153
          // Tell the user that there was a problem.
154
          getNotifier().notify( get( STATUS_PARSE_ERROR,
155
                                     e.getMessage(), currIndex )
154
          getNotifier().notify(
155
              get( STATUS_PARSE_ERROR, e.getMessage(), currIndex )
156156
          );
157157
        }
M src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
3737
import java.io.InputStreamReader;
3838
import java.io.Reader;
39
import java.net.URISyntaxException;
4039
import java.net.URL;
4140
import java.nio.charset.Charset;
...
5655
  private PropertiesConfiguration properties;
5756
58
  public DefaultSettings()
59
      throws ConfigurationException, URISyntaxException, IOException {
57
  public DefaultSettings() throws ConfigurationException {
6058
    setProperties( createProperties() );
6159
  }
M src/main/resources/com/scrivenvar/messages.properties
151151
# ########################################################################
152152
153
Pane.defintion.node.root.title=Definitions
153
Pane.definition.node.root.title=Definitions
154154
155155
# Controls ###############################################################
M src/main/resources/com/scrivenvar/settings.properties
2626
# ########################################################################
2727
#
28
# File References
28
# File and Path References
2929
#
3030
# ########################################################################
...
4343
# Startup script for R
4444
file.r.startup=/${application.package}/startup.R
45
46
# Default filename when a new file is created.
47
# This ensures that the file type can always be
48
# discerned so that the correct type of variable
49
# reference can be inserted.
50
file.default=untitled.md
4551
4652
# ########################################################################