Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M .gitignore
77
.nb-gradle-properties
88
scrivenvar.pro
9
out
910
M .idea/misc.xml
33
  <component name="ExternalStorageConfigurationManager" enabled="true" />
44
  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="14" project-jdk-type="JavaSDK">
5
    <output url="file://$PROJECT_DIR$/out" />
5
    <output url="file://$PROJECT_DIR$/build" />
66
  </component>
77
</project>
M build.gradle
33
  id 'org.openjfx.javafxplugin' version '0.0.8'
44
  id 'com.palantir.git-version' version '0.12.3'
5
  id 'org.beryx.jlink' version '2.16.2'
56
}
67
...
2526
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
2627
  implementation 'com.miglayout:miglayout-javafx:5.2'
27
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.6.0'
28
  implementation 'de.jensd:fontawesomefx-commons:11.0'
29
  implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
28
  implementation('com.dlsc.preferencesfx:preferencesfx-core:11.6.0') {
29
    exclude group: 'org.openjfx'
30
  }
31
  implementation('de.jensd:fontawesomefx-commons:11.0') {
32
    exclude group: 'org.openjfx'
33
  }
34
  implementation('de.jensd:fontawesomefx-fontawesome:4.7.0-11') {
35
    exclude group: 'org.openjfx'
36
  }
3037
3138
  // Markdown
...
4754
  implementation 'com.ximpleware:vtd-xml:2.13.4'
4855
  implementation 'net.sf.saxon:Saxon-HE:10.1'
56
  implementation 'xalan:xalan:2.7.2'
4957
5058
  // HTML parsing and rendering
...
7078
  implementation 'org.apache.xmlgraphics:batik-util:1.13'
7179
  implementation 'org.apache.xmlgraphics:batik-xml:1.13'
80
81
  implementation 'org.apache.bsf:bsf-api:3.1'
7282
7383
  // Misc.
7484
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
7585
  implementation 'org.apache.commons:commons-configuration2:2.7'
7686
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
7787
7888
  def os = ['win', 'linux', 'mac']
7989
  def fx = ['controls', 'graphics', 'fxml', 'swing']
8090
91
  // Create cross-platform überjar.
92
  //
93
  // Including these runtime dependencies breaks creating cross-platform binaries.
8194
  fx.each { fxitem ->
8295
    os.each { ositem ->
...
91104
javafx {
92105
  version = "14"
93
  modules = ['javafx.controls', 'javafx.graphics', 'javafx.swing']
106
  modules = ['javafx.controls', 'javafx.swing']
94107
}
95108
96109
compileJava {
97110
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
98111
}
99112
100
sourceCompatibility = JavaVersion.VERSION_11
101
applicationName = 'scrivenvar'
113
application {
114
  applicationName = 'scrivenvar'
115
  mainClassName = "com.${applicationName}.Main"
116
117
  applicationDefaultJvmArgs = [
118
      "--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
119
      "--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
120
      "--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
121
  ]
122
}
123
102124
version = gitVersion()
103
mainClassName = "com.${applicationName}.Main"
125
sourceCompatibility = JavaVersion.VERSION_11
126
104127
def launcherClassName = "com.${applicationName}.Launcher"
105128
106129
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
107130
propertiesFile.write("application.version=${version}")
108
109
//sourceSets {
110
//  main {
111
//    resources {
112
//      srcDir 'resources'
113
//    }
114
//  }
115
//}
116131
117132
jar {
...
142157
      }
143158
    }
159
  }
160
}
161
162
163
jlink {
164
  options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
165
  forceMerge 'jackson'
166
167
  launcher {
168
    name = 'java-keywords'
169
  }
170
171
  addExtraDependencies('javafx')
172
  jpackage {
173
    // Can also set via environment property BADASS_JLINK_JPACKAGE_HOME
174
    jpackageHome = '/opt/jdk'
175
//    jvmArgs = ['-splash:$APPDIR/splash.png']
176
//    imageOptions = ['--icon', 'src/main/resources/java.ico']
177
//    installerOptions = [
178
//        '--file-associations', 'src/main/resources/associations.properties',
179
//        '--app-version', version,
180
//    ]
181
//    if (org.gradle.internal.os.OperatingSystem.current().windows) {
182
//      installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
183
//    }
184
//  }
144185
  }
145186
}
M src/main/java/com/scrivenvar/AbstractFileFactory.java
4242
 * Provides common behaviours for factories that instantiate classes based on
4343
 * file type.
44
 *
45
 * @author White Magic Software, Ltd.
4644
 */
4745
public class AbstractFileFactory {
M src/main/java/com/scrivenvar/AbstractPane.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * Hides dependency on {@link MigPane} from subclasses.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public abstract class AbstractPane extends MigPane {
M src/main/java/com/scrivenvar/Constants.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * Defines application-wide default values.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public class Constants {
M src/main/java/com/scrivenvar/FileEditorTab.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 *
44
 * Redistribution and use in source and binary forms, with or without
...
6262
/**
6363
 * Editor for a single file.
64
 *
65
 * @author Karl Tauber and White Magic Software, Ltd.
6664
 */
6765
public final class FileEditorTab extends Tab {
M src/main/java/com/scrivenvar/FileEditorTabPane.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
6767
/**
6868
 * Tab pane for file editors.
69
 *
70
 * @author Karl Tauber and White Magic Software, Ltd.
7169
 */
7270
public final class FileEditorTabPane extends TabPane {
M src/main/java/com/scrivenvar/FileType.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within
33
 * settings.properties.
34
 *
35
 * @author White Magic Software, Ltd.
32
 * that correspond to the list of glob patterns found within {@code
33
 * settings.properties}.
3634
 */
3735
public enum FileType {
M src/main/java/com/scrivenvar/Launcher.java
5050
   */
5151
  public static void main( final String[] args ) throws IOException {
52
    // Shhh.
53
    System.err.close();
54
5552
    showAppInfo();
5653
    Main.main( args );
5754
  }
5855
5956
  @SuppressWarnings("RedundantStringFormatCall")
6057
  private static void showAppInfo() throws IOException {
6158
    out( format( "%s version %s", getTitle(), getVersion() ) );
62
    out( format( "Copyright %s by White Magic Software, Ltd.", getYear() ) );
59
    out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
6360
    out( format( "Portions copyright 2020 Karl Tauber." ) );
6461
  }
M src/main/java/com/scrivenvar/Main.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
5757
 * Application entry point. The application allows users to edit Markdown
5858
 * files and see a real-time preview of the edits.
59
 *
60
 * @author Karl Tauber and White Magic Software, Ltd.
6159
 */
6260
public final class Main extends Application {
6361
64
  // Suppress standard output logging; the Launcher suppresses stderr output.
6562
  static {
63
    // Suppress logging to standard output.
6664
    LogManager.getLogManager().reset();
65
66
    // Suppress logging to standard error.
67
    System.err.close();
6768
  }
6869
M src/main/java/com/scrivenvar/MainWindow.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.definition.DefinitionFactory;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.DefinitionSource;
33
import com.scrivenvar.definition.MapInterpolator;
34
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
35
import com.scrivenvar.editors.EditorPane;
36
import com.scrivenvar.editors.VariableNameInjector;
37
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
38
import com.scrivenvar.preferences.UserPreferences;
39
import com.scrivenvar.preview.HTMLPreviewPane;
40
import com.scrivenvar.processors.Processor;
41
import com.scrivenvar.processors.ProcessorFactory;
42
import com.scrivenvar.service.Options;
43
import com.scrivenvar.service.Snitch;
44
import com.scrivenvar.service.events.Notifier;
45
import com.scrivenvar.util.Action;
46
import com.scrivenvar.util.ActionBuilder;
47
import com.scrivenvar.util.ActionUtils;
48
import javafx.application.Platform;
49
import javafx.beans.binding.Bindings;
50
import javafx.beans.binding.BooleanBinding;
51
import javafx.beans.property.BooleanProperty;
52
import javafx.beans.property.SimpleBooleanProperty;
53
import javafx.beans.value.ChangeListener;
54
import javafx.beans.value.ObservableBooleanValue;
55
import javafx.beans.value.ObservableValue;
56
import javafx.collections.ListChangeListener.Change;
57
import javafx.collections.ObservableList;
58
import javafx.event.Event;
59
import javafx.event.EventHandler;
60
import javafx.geometry.Pos;
61
import javafx.scene.Node;
62
import javafx.scene.Scene;
63
import javafx.scene.control.*;
64
import javafx.scene.control.Alert.AlertType;
65
import javafx.scene.image.Image;
66
import javafx.scene.image.ImageView;
67
import javafx.scene.input.KeyEvent;
68
import javafx.scene.layout.BorderPane;
69
import javafx.scene.layout.VBox;
70
import javafx.scene.text.Text;
71
import javafx.stage.Window;
72
import javafx.stage.WindowEvent;
73
import javafx.util.Duration;
74
import org.apache.commons.lang3.SystemUtils;
75
import org.controlsfx.control.StatusBar;
76
import org.fxmisc.richtext.StyleClassedTextArea;
77
import org.reactfx.value.Val;
78
import org.xhtmlrenderer.util.XRLog;
79
80
import java.nio.file.Path;
81
import java.util.HashMap;
82
import java.util.Map;
83
import java.util.Observable;
84
import java.util.Observer;
85
import java.util.function.Function;
86
import java.util.prefs.Preferences;
87
88
import static com.scrivenvar.Constants.*;
89
import static com.scrivenvar.Messages.get;
90
import static com.scrivenvar.util.StageState.*;
91
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
92
import static javafx.event.Event.fireEvent;
93
import static javafx.scene.input.KeyCode.ENTER;
94
import static javafx.scene.input.KeyCode.TAB;
95
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
96
97
/**
98
 * Main window containing a tab pane in the center for file editors.
99
 *
100
 * @author Karl Tauber and White Magic Software, Ltd.
101
 */
102
public class MainWindow implements Observer {
103
  /**
104
   * The {@code OPTIONS} variable must be declared before all other variables
105
   * to prevent subsequent initializations from failing due to missing user
106
   * preferences.
107
   */
108
  private final static Options OPTIONS = Services.load( Options.class );
109
  private final static Snitch SNITCH = Services.load( Snitch.class );
110
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
111
112
  private final Scene mScene;
113
  private final StatusBar mStatusBar;
114
  private final Text mLineNumberText;
115
  private final TextField mFindTextField;
116
117
  private final Object mMutex = new Object();
118
119
  /**
120
   * Prevents re-instantiation of processing classes.
121
   */
122
  private final Map<FileEditorTab, Processor<String>> mProcessors =
123
      new HashMap<>();
124
125
  private final Map<String, String> mResolvedMap =
126
      new HashMap<>( DEFAULT_MAP_SIZE );
127
128
  /**
129
   * Called when the definition data is changed.
130
   */
131
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
132
      mTreeHandler = event -> {
133
    exportDefinitions( getDefinitionPath() );
134
    interpolateResolvedMap();
135
    renderActiveTab();
136
  };
137
138
  /**
139
   * Called to switch to the definition pane when the user presses the TAB key.
140
   */
141
  private final EventHandler<? super KeyEvent> mTabKeyHandler =
142
      (EventHandler<KeyEvent>) event -> {
143
        if( event.getCode() == TAB ) {
144
          getDefinitionPane().requestFocus();
145
          event.consume();
146
        }
147
      };
148
149
  /**
150
   * Called to inject the selected item when the user presses ENTER in the
151
   * definition pane.
152
   */
153
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
154
      event -> {
155
        if( event.getCode() == ENTER ) {
156
          getVariableNameInjector().injectSelectedItem();
157
        }
158
      };
159
160
  private final ChangeListener<Integer> mCaretPositionListener =
161
      ( observable, oldPosition, newPosition ) -> {
162
        final FileEditorTab tab = getActiveFileEditorTab();
163
        final EditorPane pane = tab.getEditorPane();
164
        final StyleClassedTextArea editor = pane.getEditor();
165
166
        getLineNumberText().setText(
167
            get( STATUS_BAR_LINE,
168
                 editor.getCurrentParagraph() + 1,
169
                 editor.getParagraphs().size(),
170
                 editor.getCaretPosition()
171
            )
172
        );
173
      };
174
175
  private final ChangeListener<Integer> mCaretParagraphListener =
176
      ( observable, oldIndex, newIndex ) ->
177
          scrollToParagraph( newIndex, true );
178
179
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
180
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
181
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
182
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
183
      mCaretPositionListener,
184
      mCaretParagraphListener );
185
186
  /**
187
   * Listens on the definition pane for double-click events.
188
   */
189
  private final VariableNameInjector mVariableNameInjector
190
      = new VariableNameInjector( mDefinitionPane );
191
192
  public MainWindow() {
193
    mStatusBar = createStatusBar();
194
    mLineNumberText = createLineNumberText();
195
    mFindTextField = createFindTextField();
196
    mScene = createScene();
197
198
    System.getProperties()
199
          .setProperty( "xr.util-logging.loggingEnabled", "true" );
200
    XRLog.setLoggingEnabled( true );
201
202
    initLayout();
203
    initFindInput();
204
    initSnitch();
205
    initDefinitionListener();
206
    initTabAddedListener();
207
    initTabChangedListener();
208
    initPreferences();
209
    initVariableNameInjector();
210
211
    NOTIFIER.addObserver( this );
212
  }
213
214
  private void initLayout() {
215
    final Scene appScene = getScene();
216
217
    appScene.getStylesheets().add( STYLESHEET_SCENE );
218
219
    // TODO: Apply an XML syntax highlighting for XML files.
220
//    appScene.getStylesheets().add( STYLESHEET_XML );
221
    appScene.windowProperty().addListener(
222
        ( observable, oldWindow, newWindow ) ->
223
            newWindow.setOnCloseRequest(
224
                e -> {
225
                  if( !getFileEditorPane().closeAllEditors() ) {
226
                    e.consume();
227
                  }
228
                }
229
            )
230
    );
231
  }
232
233
  /**
234
   * Initialize the find input text field to listen on F3, ENTER, and
235
   * ESCAPE key presses.
236
   */
237
  private void initFindInput() {
238
    final TextField input = getFindTextField();
239
240
    input.setOnKeyPressed( ( KeyEvent event ) -> {
241
      switch( event.getCode() ) {
242
        case F3:
243
        case ENTER:
244
          editFindNext();
245
          break;
246
        case F:
247
          if( !event.isControlDown() ) {
248
            break;
249
          }
250
        case ESCAPE:
251
          getStatusBar().setGraphic( null );
252
          getActiveFileEditorTab().getEditorPane().requestFocus();
253
          break;
254
      }
255
    } );
256
257
    // Remove when the input field loses focus.
258
    input.focusedProperty().addListener(
259
        ( focused, oldFocus, newFocus ) -> {
260
          if( !newFocus ) {
261
            getStatusBar().setGraphic( null );
262
          }
263
        }
264
    );
265
  }
266
267
  /**
268
   * Watch for changes to external files. In particular, this awaits
269
   * modifications to any XSL files associated with XML files being edited.
270
   * When
271
   * an XSL file is modified (external to the application), the snitch's ears
272
   * perk up and the file is reloaded. This keeps the XSL transformation up to
273
   * date with what's on the file system.
274
   */
275
  private void initSnitch() {
276
    SNITCH.addObserver( this );
277
  }
278
279
  /**
280
   * Listen for {@link FileEditorTabPane} to receive open definition file
281
   * event.
282
   */
283
  private void initDefinitionListener() {
284
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
285
        ( final ObservableValue<? extends Path> file,
286
          final Path oldPath, final Path newPath ) -> {
287
          // Indirectly refresh the resolved map.
288
          resetProcessors();
289
290
          openDefinitions( newPath );
291
292
          // Will create new processors and therefore a new resolved map.
293
          renderActiveTab();
294
        }
295
    );
296
  }
297
298
  /**
299
   * When tabs are added, hook the various change listeners onto the new
300
   * tab sothat the preview pane refreshes as necessary.
301
   */
302
  private void initTabAddedListener() {
303
    final FileEditorTabPane editorPane = getFileEditorPane();
304
305
    // Make sure the text processor kicks off when new files are opened.
306
    final ObservableList<Tab> tabs = editorPane.getTabs();
307
308
    // Update the preview pane on tab changes.
309
    tabs.addListener(
310
        ( final Change<? extends Tab> change ) -> {
311
          while( change.next() ) {
312
            if( change.wasAdded() ) {
313
              // Multiple tabs can be added simultaneously.
314
              for( final Tab newTab : change.getAddedSubList() ) {
315
                final FileEditorTab tab = (FileEditorTab) newTab;
316
317
                initTextChangeListener( tab );
318
                initTabKeyEventListener( tab );
319
                initScrollEventListener( tab );
320
//              initSyntaxListener( tab );
321
              }
322
            }
323
          }
324
        }
325
    );
326
  }
327
328
  private void initScrollEventListener( final FileEditorTab tab ) {
329
    final var scrollPane = tab.getEditorPane().getScrollPane();
330
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
331
332
    // Before the drag handler can be attached, the scroll bar for the
333
    // text editor pane must be visible.
334
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
335
        Platform.runLater( () -> {
336
          if( newShow ) {
337
            final var handler = new ScrollEventHandler( scrollPane, scrollBar );
338
            handler.enabledProperty().bind( tab.selectedProperty() );
339
          }
340
        } );
341
342
    Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
343
       .flatMap( Window::showingProperty )
344
       .addListener( listener );
345
  }
346
347
  /**
348
   * Listen for new tab selection events.
349
   */
350
  private void initTabChangedListener() {
351
    final FileEditorTabPane editorPane = getFileEditorPane();
352
353
    // Update the preview pane changing tabs.
354
    editorPane.addTabSelectionListener(
355
        ( tabPane, oldTab, newTab ) -> {
356
          // If there was no old tab, then this is a first time load, which
357
          // can be ignored.
358
          if( oldTab != null ) {
359
            if( newTab != null ) {
360
              final FileEditorTab tab = (FileEditorTab) newTab;
361
              updateVariableNameInjector( tab );
362
              process( tab );
363
            }
364
          }
365
        }
366
    );
367
  }
368
369
  /**
370
   * Reloads the preferences from the previous session.
371
   */
372
  private void initPreferences() {
373
    initDefinitionPane();
374
    getFileEditorPane().initPreferences();
375
  }
376
377
  private void initVariableNameInjector() {
378
    updateVariableNameInjector( getActiveFileEditorTab() );
379
  }
380
381
  /**
382
   * Ensure that the keyboard events are received when a new tab is added
383
   * to the user interface.
384
   *
385
   * @param tab The tab editor that can trigger keyboard events.
386
   */
387
  private void initTabKeyEventListener( final FileEditorTab tab ) {
388
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
389
  }
390
391
  private void initTextChangeListener( final FileEditorTab tab ) {
392
    tab.addTextChangeListener(
393
        ( editor, oldValue, newValue ) -> {
394
          process( tab );
395
          scrollToParagraph( getCurrentParagraphIndex() );
396
        }
397
    );
398
  }
399
400
  private int getCurrentParagraphIndex() {
401
    return getActiveEditorPane().getCurrentParagraphIndex();
402
  }
403
404
  private void scrollToParagraph( final int id ) {
405
    scrollToParagraph( id, false );
406
  }
407
408
  /**
409
   * @param id    The paragraph to scroll to, will be approximated if it doesn't
410
   *              exist.
411
   * @param force {@code true} means to force scrolling immediately, which
412
   *              should only be attempted when it is known that the document
413
   *              has been fully rendered. Otherwise the internal map of ID
414
   *              attributes will be incomplete and scrolling will flounder.
415
   */
416
  private void scrollToParagraph( final int id, final boolean force ) {
417
    synchronized( mMutex ) {
418
      final var previewPane = getPreviewPane();
419
      final var scrollPane = previewPane.getScrollPane();
420
      final int approxId = getActiveEditorPane().approximateParagraphId( id );
421
422
      if( force ) {
423
        previewPane.scrollTo( approxId );
424
      }
425
      else {
426
        previewPane.tryScrollTo( approxId );
427
      }
428
429
      scrollPane.repaint();
430
    }
431
  }
432
433
  private void updateVariableNameInjector( final FileEditorTab tab ) {
434
    getVariableNameInjector().addListener( tab );
435
  }
436
437
  /**
438
   * Called whenever the preview pane becomes out of sync with the file editor
439
   * tab. This can be called when the text changes, the caret paragraph
440
   * changes,
441
   * or the file tab changes.
442
   *
443
   * @param tab The file editor tab that has been changed in some fashion.
444
   */
445
  private void process( final FileEditorTab tab ) {
446
    if( tab == null ) {
447
      return;
448
    }
449
450
    getPreviewPane().setPath( tab.getPath() );
451
452
    final Processor<String> processor = getProcessors().computeIfAbsent(
453
        tab, p -> createProcessor( tab )
454
    );
455
456
    try {
457
      processor.processChain( tab.getEditorText() );
458
    } catch( final Exception ex ) {
459
      error( ex );
460
    }
461
  }
462
463
  private void renderActiveTab() {
464
    process( getActiveFileEditorTab() );
465
  }
466
467
  /**
468
   * Called when a definition source is opened.
469
   *
470
   * @param path Path to the definition source that was opened.
471
   */
472
  private void openDefinitions( final Path path ) {
473
    try {
474
      final DefinitionSource ds = createDefinitionSource( path );
475
      setDefinitionSource( ds );
476
      getUserPreferences().definitionPathProperty().setValue( path.toFile() );
477
      getUserPreferences().save();
478
479
      final Tooltip tooltipPath = new Tooltip( path.toString() );
480
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
481
482
      final DefinitionPane pane = getDefinitionPane();
483
      pane.update( ds );
484
      pane.addTreeChangeHandler( mTreeHandler );
485
      pane.addKeyEventHandler( mDefinitionKeyHandler );
486
      pane.filenameProperty().setValue( path.getFileName().toString() );
487
      pane.setTooltip( tooltipPath );
488
489
      interpolateResolvedMap();
490
    } catch( final Exception e ) {
491
      error( e );
492
    }
493
  }
494
495
  private void exportDefinitions( final Path path ) {
496
    try {
497
      final DefinitionPane pane = getDefinitionPane();
498
      final TreeItem<String> root = pane.getTreeView().getRoot();
499
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
500
501
      if( problemChild == null ) {
502
        getDefinitionSource().getTreeAdapter().export( root, path );
503
        getNotifier().clear();
504
      }
505
      else {
506
        final String msg = get(
507
            "yaml.error.tree.form", problemChild.getValue() );
508
        getNotifier().notify( msg );
509
      }
510
    } catch( final Exception e ) {
511
      error( e );
512
    }
513
  }
514
515
  private void interpolateResolvedMap() {
516
    final Map<String, String> treeMap = getDefinitionPane().toMap();
517
    final Map<String, String> map = new HashMap<>( treeMap );
518
    MapInterpolator.interpolate( map );
519
520
    getResolvedMap().clear();
521
    getResolvedMap().putAll( map );
522
  }
523
524
  private void initDefinitionPane() {
525
    openDefinitions( getDefinitionPath() );
526
  }
527
528
  /**
529
   * Called when an exception occurs that warrants the user's attention.
530
   *
531
   * @param e The exception with a message that the user should know about.
532
   */
533
  private void error( final Exception e ) {
534
    getNotifier().notify( e );
535
  }
536
537
  //---- File actions -------------------------------------------------------
538
539
  /**
540
   * Called when an {@link Observable} instance has changed. This is called
541
   * by both the {@link Snitch} service and the notify service. The @link
542
   * Snitch} service can be called for different file types, including
543
   * {@link DefinitionSource} instances.
544
   *
545
   * @param observable The observed instance.
546
   * @param value      The noteworthy item.
547
   */
548
  @Override
549
  public void update( final Observable observable, final Object value ) {
550
    if( value != null ) {
551
      if( observable instanceof Snitch && value instanceof Path ) {
552
        updateSelectedTab();
553
      }
554
      else if( observable instanceof Notifier && value instanceof String ) {
555
        updateStatusBar( (String) value );
556
      }
557
    }
558
  }
559
560
  /**
561
   * Updates the status bar to show the given message.
562
   *
563
   * @param s The message to show in the status bar.
564
   */
565
  private void updateStatusBar( final String s ) {
566
    Platform.runLater(
567
        () -> {
568
          final int index = s.indexOf( '\n' );
569
          final String message = s.substring(
570
              0, index > 0 ? index : s.length() );
571
572
          getStatusBar().setText( message );
573
        }
574
    );
575
  }
576
577
  /**
578
   * Called when a file has been modified.
579
   */
580
  private void updateSelectedTab() {
581
    Platform.runLater(
582
        () -> {
583
          // Brute-force XSLT file reload by re-instantiating all processors.
584
          resetProcessors();
585
          renderActiveTab();
586
        }
587
    );
588
  }
589
590
  /**
591
   * After resetting the processors, they will refresh anew to be up-to-date
592
   * with the files (text and definition) currently loaded into the editor.
593
   */
594
  private void resetProcessors() {
595
    getProcessors().clear();
596
  }
597
598
  //---- File actions -------------------------------------------------------
599
600
  private void fileNew() {
601
    getFileEditorPane().newEditor();
602
  }
603
604
  private void fileOpen() {
605
    getFileEditorPane().openFileDialog();
606
  }
607
608
  private void fileClose() {
609
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
610
  }
611
612
  /**
613
   * TODO: Upon closing, first remove the tab change listeners. (There's no
614
   * need to re-render each tab when all are being closed.)
615
   */
616
  private void fileCloseAll() {
617
    getFileEditorPane().closeAllEditors();
618
  }
619
620
  private void fileSave() {
621
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
622
  }
623
624
  private void fileSaveAs() {
625
    final FileEditorTab editor = getActiveFileEditorTab();
626
    getFileEditorPane().saveEditorAs( editor );
627
    getProcessors().remove( editor );
628
629
    try {
630
      process( editor );
631
    } catch( final Exception ex ) {
632
      getNotifier().notify( ex );
633
    }
634
  }
635
636
  private void fileSaveAll() {
637
    getFileEditorPane().saveAllEditors();
638
  }
639
640
  private void fileExit() {
641
    final Window window = getWindow();
642
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
643
  }
644
645
  //---- Edit actions -------------------------------------------------------
646
647
  /**
648
   * Used to find text in the active file editor window.
649
   */
650
  private void editFind() {
651
    final TextField input = getFindTextField();
652
    getStatusBar().setGraphic( input );
653
    input.requestFocus();
654
  }
655
656
  public void editFindNext() {
657
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
658
  }
659
660
  public void editPreferences() {
661
    getUserPreferences().show();
662
  }
663
664
  //---- Insert actions -----------------------------------------------------
665
666
  /**
667
   * Delegates to the active editor to handle wrapping the current text
668
   * selection with leading and trailing strings.
669
   *
670
   * @param leading  The string to put before the selection.
671
   * @param trailing The string to put after the selection.
672
   */
673
  private void insertMarkdown(
674
      final String leading, final String trailing ) {
675
    getActiveEditorPane().surroundSelection( leading, trailing );
676
  }
677
678
  private void insertMarkdown(
679
      final String leading, final String trailing, final String hint ) {
680
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
681
  }
682
683
  //---- Help actions -------------------------------------------------------
684
685
  private void helpAbout() {
686
    final Alert alert = new Alert( AlertType.INFORMATION );
687
    alert.setTitle( get( "Dialog.about.title" ) );
688
    alert.setHeaderText( get( "Dialog.about.header" ) );
689
    alert.setContentText( get( "Dialog.about.content" ) );
690
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
691
    alert.initOwner( getWindow() );
692
693
    alert.showAndWait();
694
  }
695
696
  //---- Member creators ----------------------------------------------------
697
698
  /**
699
   * Factory to create processors that are suited to different file types.
700
   *
701
   * @param tab The tab that is subjected to processing.
702
   * @return A processor suited to the file type specified by the tab's path.
703
   */
704
  private Processor<String> createProcessor( final FileEditorTab tab ) {
705
    return createProcessorFactory().createProcessor( tab );
706
  }
707
708
  private ProcessorFactory createProcessorFactory() {
709
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
710
  }
711
712
  private HTMLPreviewPane createHTMLPreviewPane() {
713
    return new HTMLPreviewPane();
714
  }
715
716
  private DefinitionSource createDefaultDefinitionSource() {
717
    return new YamlDefinitionSource( getDefinitionPath() );
718
  }
719
720
  private DefinitionSource createDefinitionSource( final Path path ) {
721
    try {
722
      return createDefinitionFactory().createDefinitionSource( path );
723
    } catch( final Exception ex ) {
724
      error( ex );
725
      return createDefaultDefinitionSource();
726
    }
727
  }
728
729
  private TextField createFindTextField() {
730
    return new TextField();
731
  }
732
733
  private DefinitionFactory createDefinitionFactory() {
734
    return new DefinitionFactory();
735
  }
736
737
  private StatusBar createStatusBar() {
738
    return new StatusBar();
739
  }
740
741
  private Scene createScene() {
742
    final SplitPane splitPane = new SplitPane(
743
        getDefinitionPane().getNode(),
744
        getFileEditorPane().getNode(),
745
        getPreviewPane().getNode() );
746
747
    splitPane.setDividerPositions(
748
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
749
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
750
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
751
752
    getDefinitionPane().prefHeightProperty()
753
                       .bind( splitPane.heightProperty() );
754
755
    final BorderPane borderPane = new BorderPane();
756
    borderPane.setPrefSize( 1024, 800 );
757
    borderPane.setTop( createMenuBar() );
758
    borderPane.setBottom( getStatusBar() );
759
    borderPane.setCenter( splitPane );
760
761
    final VBox statusBar = new VBox();
762
    statusBar.setAlignment( Pos.BASELINE_CENTER );
763
    statusBar.getChildren().add( getLineNumberText() );
764
    getStatusBar().getRightItems().add( statusBar );
765
766
    // Force preview pane refresh on Windows.
767
    splitPane.getDividers().get( 1 ).positionProperty().addListener(
768
        ( l, oValue, nValue ) -> Platform.runLater(
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.definition.DefinitionFactory;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.DefinitionSource;
33
import com.scrivenvar.definition.MapInterpolator;
34
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
35
import com.scrivenvar.editors.EditorPane;
36
import com.scrivenvar.editors.VariableNameInjector;
37
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
38
import com.scrivenvar.preferences.UserPreferences;
39
import com.scrivenvar.preview.HTMLPreviewPane;
40
import com.scrivenvar.processors.Processor;
41
import com.scrivenvar.processors.ProcessorFactory;
42
import com.scrivenvar.service.Options;
43
import com.scrivenvar.service.Snitch;
44
import com.scrivenvar.service.events.Notifier;
45
import com.scrivenvar.util.Action;
46
import com.scrivenvar.util.ActionBuilder;
47
import com.scrivenvar.util.ActionUtils;
48
import javafx.beans.binding.Bindings;
49
import javafx.beans.binding.BooleanBinding;
50
import javafx.beans.property.BooleanProperty;
51
import javafx.beans.property.SimpleBooleanProperty;
52
import javafx.beans.value.ChangeListener;
53
import javafx.beans.value.ObservableBooleanValue;
54
import javafx.beans.value.ObservableValue;
55
import javafx.collections.ListChangeListener.Change;
56
import javafx.collections.ObservableList;
57
import javafx.event.Event;
58
import javafx.event.EventHandler;
59
import javafx.geometry.Pos;
60
import javafx.scene.Node;
61
import javafx.scene.Scene;
62
import javafx.scene.control.*;
63
import javafx.scene.control.Alert.AlertType;
64
import javafx.scene.image.Image;
65
import javafx.scene.image.ImageView;
66
import javafx.scene.input.KeyEvent;
67
import javafx.scene.layout.BorderPane;
68
import javafx.scene.layout.VBox;
69
import javafx.scene.text.Text;
70
import javafx.stage.Window;
71
import javafx.stage.WindowEvent;
72
import javafx.util.Duration;
73
import org.apache.commons.lang3.SystemUtils;
74
import org.controlsfx.control.StatusBar;
75
import org.fxmisc.richtext.StyleClassedTextArea;
76
import org.reactfx.value.Val;
77
import org.xhtmlrenderer.util.XRLog;
78
79
import java.nio.file.Path;
80
import java.util.*;
81
import java.util.function.Function;
82
import java.util.prefs.Preferences;
83
84
import static com.scrivenvar.Constants.*;
85
import static com.scrivenvar.Messages.get;
86
import static com.scrivenvar.util.StageState.*;
87
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
88
import static javafx.application.Platform.runLater;
89
import static javafx.event.Event.fireEvent;
90
import static javafx.scene.input.KeyCode.ENTER;
91
import static javafx.scene.input.KeyCode.TAB;
92
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
93
94
/**
95
 * Main window containing a tab pane in the center for file editors.
96
 */
97
public class MainWindow implements Observer {
98
  /**
99
   * The {@code OPTIONS} variable must be declared before all other variables
100
   * to prevent subsequent initializations from failing due to missing user
101
   * preferences.
102
   */
103
  private final static Options OPTIONS = Services.load( Options.class );
104
  private final static Snitch SNITCH = Services.load( Snitch.class );
105
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
106
107
  private final Scene mScene;
108
  private final StatusBar mStatusBar;
109
  private final Text mLineNumberText;
110
  private final TextField mFindTextField;
111
112
  private final Object mMutex = new Object();
113
114
  /**
115
   * Prevents re-instantiation of processing classes.
116
   */
117
  private final Map<FileEditorTab, Processor<String>> mProcessors =
118
      new HashMap<>();
119
120
  private final Map<String, String> mResolvedMap =
121
      new HashMap<>( DEFAULT_MAP_SIZE );
122
123
  /**
124
   * Called when the definition data is changed.
125
   */
126
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
127
      mTreeHandler = event -> {
128
    exportDefinitions( getDefinitionPath() );
129
    interpolateResolvedMap();
130
    renderActiveTab();
131
  };
132
133
  /**
134
   * Called to switch to the definition pane when the user presses the TAB key.
135
   */
136
  private final EventHandler<? super KeyEvent> mTabKeyHandler =
137
      (EventHandler<KeyEvent>) event -> {
138
        if( event.getCode() == TAB ) {
139
          getDefinitionPane().requestFocus();
140
          event.consume();
141
        }
142
      };
143
144
  /**
145
   * Called to inject the selected item when the user presses ENTER in the
146
   * definition pane.
147
   */
148
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
149
      event -> {
150
        if( event.getCode() == ENTER ) {
151
          getVariableNameInjector().injectSelectedItem();
152
        }
153
      };
154
155
  private final ChangeListener<Integer> mCaretPositionListener =
156
      ( observable, oldPosition, newPosition ) -> {
157
        final FileEditorTab tab = getActiveFileEditorTab();
158
        final EditorPane pane = tab.getEditorPane();
159
        final StyleClassedTextArea editor = pane.getEditor();
160
161
        getLineNumberText().setText(
162
            get( STATUS_BAR_LINE,
163
                 editor.getCurrentParagraph() + 1,
164
                 editor.getParagraphs().size(),
165
                 editor.getCaretPosition()
166
            )
167
        );
168
      };
169
170
  private final ChangeListener<Integer> mCaretParagraphListener =
171
      ( observable, oldIndex, newIndex ) ->
172
          scrollToParagraph( newIndex, true );
173
174
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
175
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
176
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
177
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
178
      mCaretPositionListener,
179
      mCaretParagraphListener );
180
181
  /**
182
   * Listens on the definition pane for double-click events.
183
   */
184
  private final VariableNameInjector mVariableNameInjector
185
      = new VariableNameInjector( mDefinitionPane );
186
187
  public MainWindow() {
188
    mStatusBar = createStatusBar();
189
    mLineNumberText = createLineNumberText();
190
    mFindTextField = createFindTextField();
191
    mScene = createScene();
192
193
    System.getProperties()
194
          .setProperty( "xr.util-logging.loggingEnabled", "true" );
195
    XRLog.setLoggingEnabled( true );
196
197
    initLayout();
198
    initFindInput();
199
    initSnitch();
200
    initDefinitionListener();
201
    initTabAddedListener();
202
    initTabChangedListener();
203
    initPreferences();
204
    initVariableNameInjector();
205
206
    NOTIFIER.addObserver( this );
207
  }
208
209
  private void initLayout() {
210
    final Scene appScene = getScene();
211
212
    appScene.getStylesheets().add( STYLESHEET_SCENE );
213
214
    // TODO: Apply an XML syntax highlighting for XML files.
215
//    appScene.getStylesheets().add( STYLESHEET_XML );
216
    appScene.windowProperty().addListener(
217
        ( observable, oldWindow, newWindow ) ->
218
            newWindow.setOnCloseRequest(
219
                e -> {
220
                  if( !getFileEditorPane().closeAllEditors() ) {
221
                    e.consume();
222
                  }
223
                }
224
            )
225
    );
226
  }
227
228
  /**
229
   * Initialize the find input text field to listen on F3, ENTER, and
230
   * ESCAPE key presses.
231
   */
232
  private void initFindInput() {
233
    final TextField input = getFindTextField();
234
235
    input.setOnKeyPressed( ( KeyEvent event ) -> {
236
      switch( event.getCode() ) {
237
        case F3:
238
        case ENTER:
239
          editFindNext();
240
          break;
241
        case F:
242
          if( !event.isControlDown() ) {
243
            break;
244
          }
245
        case ESCAPE:
246
          getStatusBar().setGraphic( null );
247
          getActiveFileEditorTab().getEditorPane().requestFocus();
248
          break;
249
      }
250
    } );
251
252
    // Remove when the input field loses focus.
253
    input.focusedProperty().addListener(
254
        ( focused, oldFocus, newFocus ) -> {
255
          if( !newFocus ) {
256
            getStatusBar().setGraphic( null );
257
          }
258
        }
259
    );
260
  }
261
262
  /**
263
   * Watch for changes to external files. In particular, this awaits
264
   * modifications to any XSL files associated with XML files being edited.
265
   * When
266
   * an XSL file is modified (external to the application), the snitch's ears
267
   * perk up and the file is reloaded. This keeps the XSL transformation up to
268
   * date with what's on the file system.
269
   */
270
  private void initSnitch() {
271
    SNITCH.addObserver( this );
272
  }
273
274
  /**
275
   * Listen for {@link FileEditorTabPane} to receive open definition file
276
   * event.
277
   */
278
  private void initDefinitionListener() {
279
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
280
        ( final ObservableValue<? extends Path> file,
281
          final Path oldPath, final Path newPath ) -> {
282
          // Indirectly refresh the resolved map.
283
          resetProcessors();
284
285
          openDefinitions( newPath );
286
287
          // Will create new processors and therefore a new resolved map.
288
          renderActiveTab();
289
        }
290
    );
291
  }
292
293
  /**
294
   * When tabs are added, hook the various change listeners onto the new
295
   * tab sothat the preview pane refreshes as necessary.
296
   */
297
  private void initTabAddedListener() {
298
    final FileEditorTabPane editorPane = getFileEditorPane();
299
300
    // Make sure the text processor kicks off when new files are opened.
301
    final ObservableList<Tab> tabs = editorPane.getTabs();
302
303
    // Update the preview pane on tab changes.
304
    tabs.addListener(
305
        ( final Change<? extends Tab> change ) -> {
306
          while( change.next() ) {
307
            if( change.wasAdded() ) {
308
              // Multiple tabs can be added simultaneously.
309
              for( final Tab newTab : change.getAddedSubList() ) {
310
                final FileEditorTab tab = (FileEditorTab) newTab;
311
312
                initTextChangeListener( tab );
313
                initTabKeyEventListener( tab );
314
                initScrollEventListener( tab );
315
//              initSyntaxListener( tab );
316
              }
317
            }
318
          }
319
        }
320
    );
321
  }
322
323
  private void initScrollEventListener( final FileEditorTab tab ) {
324
    final var scrollPane = tab.getEditorPane().getScrollPane();
325
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
326
327
    // Before the drag handler can be attached, the scroll bar for the
328
    // text editor pane must be visible.
329
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
330
        runLater( () -> {
331
          if( newShow ) {
332
            final var handler = new ScrollEventHandler( scrollPane, scrollBar );
333
            handler.enabledProperty().bind( tab.selectedProperty() );
334
          }
335
        } );
336
337
    Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
338
       .flatMap( Window::showingProperty )
339
       .addListener( listener );
340
  }
341
342
  /**
343
   * Listen for new tab selection events.
344
   */
345
  private void initTabChangedListener() {
346
    final FileEditorTabPane editorPane = getFileEditorPane();
347
348
    // Update the preview pane changing tabs.
349
    editorPane.addTabSelectionListener(
350
        ( tabPane, oldTab, newTab ) -> {
351
          // If there was no old tab, then this is a first time load, which
352
          // can be ignored.
353
          if( oldTab != null ) {
354
            if( newTab != null ) {
355
              final FileEditorTab tab = (FileEditorTab) newTab;
356
              updateVariableNameInjector( tab );
357
              process( tab );
358
            }
359
          }
360
        }
361
    );
362
  }
363
364
  /**
365
   * Reloads the preferences from the previous session.
366
   */
367
  private void initPreferences() {
368
    initDefinitionPane();
369
    getFileEditorPane().initPreferences();
370
  }
371
372
  private void initVariableNameInjector() {
373
    updateVariableNameInjector( getActiveFileEditorTab() );
374
  }
375
376
  /**
377
   * Ensure that the keyboard events are received when a new tab is added
378
   * to the user interface.
379
   *
380
   * @param tab The tab editor that can trigger keyboard events.
381
   */
382
  private void initTabKeyEventListener( final FileEditorTab tab ) {
383
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
384
  }
385
386
  private void initTextChangeListener( final FileEditorTab tab ) {
387
    tab.addTextChangeListener(
388
        ( editor, oldValue, newValue ) -> {
389
          process( tab );
390
          scrollToParagraph( getCurrentParagraphIndex() );
391
        }
392
    );
393
  }
394
395
  private int getCurrentParagraphIndex() {
396
    return getActiveEditorPane().getCurrentParagraphIndex();
397
  }
398
399
  private void scrollToParagraph( final int id ) {
400
    scrollToParagraph( id, false );
401
  }
402
403
  /**
404
   * @param id    The paragraph to scroll to, will be approximated if it doesn't
405
   *              exist.
406
   * @param force {@code true} means to force scrolling immediately, which
407
   *              should only be attempted when it is known that the document
408
   *              has been fully rendered. Otherwise the internal map of ID
409
   *              attributes will be incomplete and scrolling will flounder.
410
   */
411
  private void scrollToParagraph( final int id, final boolean force ) {
412
    synchronized( mMutex ) {
413
      final var previewPane = getPreviewPane();
414
      final var scrollPane = previewPane.getScrollPane();
415
      final int approxId = getActiveEditorPane().approximateParagraphId( id );
416
417
      if( force ) {
418
        previewPane.scrollTo( approxId );
419
      }
420
      else {
421
        previewPane.tryScrollTo( approxId );
422
      }
423
424
      scrollPane.repaint();
425
    }
426
  }
427
428
  private void updateVariableNameInjector( final FileEditorTab tab ) {
429
    getVariableNameInjector().addListener( tab );
430
  }
431
432
  /**
433
   * Called whenever the preview pane becomes out of sync with the file editor
434
   * tab. This can be called when the text changes, the caret paragraph
435
   * changes,
436
   * or the file tab changes.
437
   *
438
   * @param tab The file editor tab that has been changed in some fashion.
439
   */
440
  private void process( final FileEditorTab tab ) {
441
    if( tab == null ) {
442
      return;
443
    }
444
445
    getPreviewPane().setPath( tab.getPath() );
446
447
    final Processor<String> processor = getProcessors().computeIfAbsent(
448
        tab, p -> createProcessor( tab )
449
    );
450
451
    try {
452
      processor.processChain( tab.getEditorText() );
453
    } catch( final Exception ex ) {
454
      error( ex );
455
    }
456
  }
457
458
  private void renderActiveTab() {
459
    process( getActiveFileEditorTab() );
460
  }
461
462
  /**
463
   * Called when a definition source is opened.
464
   *
465
   * @param path Path to the definition source that was opened.
466
   */
467
  private void openDefinitions( final Path path ) {
468
    try {
469
      final DefinitionSource ds = createDefinitionSource( path );
470
      setDefinitionSource( ds );
471
      getUserPreferences().definitionPathProperty().setValue( path.toFile() );
472
      getUserPreferences().save();
473
474
      final Tooltip tooltipPath = new Tooltip( path.toString() );
475
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
476
477
      final DefinitionPane pane = getDefinitionPane();
478
      pane.update( ds );
479
      pane.addTreeChangeHandler( mTreeHandler );
480
      pane.addKeyEventHandler( mDefinitionKeyHandler );
481
      pane.filenameProperty().setValue( path.getFileName().toString() );
482
      pane.setTooltip( tooltipPath );
483
484
      interpolateResolvedMap();
485
    } catch( final Exception e ) {
486
      error( e );
487
    }
488
  }
489
490
  private void exportDefinitions( final Path path ) {
491
    try {
492
      final DefinitionPane pane = getDefinitionPane();
493
      final TreeItem<String> root = pane.getTreeView().getRoot();
494
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
495
496
      if( problemChild == null ) {
497
        getDefinitionSource().getTreeAdapter().export( root, path );
498
        getNotifier().clear();
499
      }
500
      else {
501
        final String msg = get(
502
            "yaml.error.tree.form", problemChild.getValue() );
503
        getNotifier().notify( msg );
504
      }
505
    } catch( final Exception e ) {
506
      error( e );
507
    }
508
  }
509
510
  private void interpolateResolvedMap() {
511
    final Map<String, String> treeMap = getDefinitionPane().toMap();
512
    final Map<String, String> map = new HashMap<>( treeMap );
513
    MapInterpolator.interpolate( map );
514
515
    getResolvedMap().clear();
516
    getResolvedMap().putAll( map );
517
  }
518
519
  private void initDefinitionPane() {
520
    openDefinitions( getDefinitionPath() );
521
  }
522
523
  /**
524
   * Called when an exception occurs that warrants the user's attention.
525
   *
526
   * @param e The exception with a message that the user should know about.
527
   */
528
  private void error( final Exception e ) {
529
    getNotifier().notify( e );
530
  }
531
532
  //---- File actions -------------------------------------------------------
533
534
  /**
535
   * Called when an {@link Observable} instance has changed. This is called
536
   * by both the {@link Snitch} service and the notify service. The @link
537
   * Snitch} service can be called for different file types, including
538
   * {@link DefinitionSource} instances.
539
   *
540
   * @param observable The observed instance.
541
   * @param value      The noteworthy item.
542
   */
543
  @Override
544
  public void update( final Observable observable, final Object value ) {
545
    if( value != null ) {
546
      if( observable instanceof Snitch && value instanceof Path ) {
547
        updateSelectedTab();
548
      }
549
      else if( observable instanceof Notifier && value instanceof String ) {
550
        updateStatusBar( (String) value );
551
      }
552
    }
553
  }
554
555
  /**
556
   * Updates the status bar to show the given message.
557
   *
558
   * @param s The message to show in the status bar.
559
   */
560
  private void updateStatusBar( final String s ) {
561
    runLater(
562
        () -> {
563
          final int index = s.indexOf( '\n' );
564
          final String message = s.substring(
565
              0, index > 0 ? index : s.length() );
566
567
          getStatusBar().setText( message );
568
        }
569
    );
570
  }
571
572
  /**
573
   * Called when a file has been modified.
574
   */
575
  private void updateSelectedTab() {
576
    runLater(
577
        () -> {
578
          // Brute-force XSLT file reload by re-instantiating all processors.
579
          resetProcessors();
580
          renderActiveTab();
581
        }
582
    );
583
  }
584
585
  /**
586
   * After resetting the processors, they will refresh anew to be up-to-date
587
   * with the files (text and definition) currently loaded into the editor.
588
   */
589
  private void resetProcessors() {
590
    getProcessors().clear();
591
  }
592
593
  //---- File actions -------------------------------------------------------
594
595
  private void fileNew() {
596
    getFileEditorPane().newEditor();
597
  }
598
599
  private void fileOpen() {
600
    getFileEditorPane().openFileDialog();
601
  }
602
603
  private void fileClose() {
604
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
605
  }
606
607
  /**
608
   * TODO: Upon closing, first remove the tab change listeners. (There's no
609
   * need to re-render each tab when all are being closed.)
610
   */
611
  private void fileCloseAll() {
612
    getFileEditorPane().closeAllEditors();
613
  }
614
615
  private void fileSave() {
616
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
617
  }
618
619
  private void fileSaveAs() {
620
    final FileEditorTab editor = getActiveFileEditorTab();
621
    getFileEditorPane().saveEditorAs( editor );
622
    getProcessors().remove( editor );
623
624
    try {
625
      process( editor );
626
    } catch( final Exception ex ) {
627
      getNotifier().notify( ex );
628
    }
629
  }
630
631
  private void fileSaveAll() {
632
    getFileEditorPane().saveAllEditors();
633
  }
634
635
  private void fileExit() {
636
    final Window window = getWindow();
637
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
638
  }
639
640
  //---- Edit actions -------------------------------------------------------
641
642
  /**
643
   * Used to find text in the active file editor window.
644
   */
645
  private void editFind() {
646
    final TextField input = getFindTextField();
647
    getStatusBar().setGraphic( input );
648
    input.requestFocus();
649
  }
650
651
  public void editFindNext() {
652
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
653
  }
654
655
  public void editPreferences() {
656
    getUserPreferences().show();
657
  }
658
659
  //---- Insert actions -----------------------------------------------------
660
661
  /**
662
   * Delegates to the active editor to handle wrapping the current text
663
   * selection with leading and trailing strings.
664
   *
665
   * @param leading  The string to put before the selection.
666
   * @param trailing The string to put after the selection.
667
   */
668
  private void insertMarkdown(
669
      final String leading, final String trailing ) {
670
    getActiveEditorPane().surroundSelection( leading, trailing );
671
  }
672
673
  private void insertMarkdown(
674
      final String leading, final String trailing, final String hint ) {
675
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
676
  }
677
678
  //---- Help actions -------------------------------------------------------
679
680
  private void helpAbout() {
681
    final Alert alert = new Alert( AlertType.INFORMATION );
682
    alert.setTitle( get( "Dialog.about.title" ) );
683
    alert.setHeaderText( get( "Dialog.about.header" ) );
684
    alert.setContentText( get( "Dialog.about.content" ) );
685
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
686
    alert.initOwner( getWindow() );
687
688
    alert.showAndWait();
689
  }
690
691
  //---- Member creators ----------------------------------------------------
692
693
  /**
694
   * Factory to create processors that are suited to different file types.
695
   *
696
   * @param tab The tab that is subjected to processing.
697
   * @return A processor suited to the file type specified by the tab's path.
698
   */
699
  private Processor<String> createProcessor( final FileEditorTab tab ) {
700
    return createProcessorFactory().createProcessor( tab );
701
  }
702
703
  private ProcessorFactory createProcessorFactory() {
704
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
705
  }
706
707
  private HTMLPreviewPane createHTMLPreviewPane() {
708
    return new HTMLPreviewPane();
709
  }
710
711
  private DefinitionSource createDefaultDefinitionSource() {
712
    return new YamlDefinitionSource( getDefinitionPath() );
713
  }
714
715
  private DefinitionSource createDefinitionSource( final Path path ) {
716
    try {
717
      return createDefinitionFactory().createDefinitionSource( path );
718
    } catch( final Exception ex ) {
719
      error( ex );
720
      return createDefaultDefinitionSource();
721
    }
722
  }
723
724
  private TextField createFindTextField() {
725
    return new TextField();
726
  }
727
728
  private DefinitionFactory createDefinitionFactory() {
729
    return new DefinitionFactory();
730
  }
731
732
  private StatusBar createStatusBar() {
733
    return new StatusBar();
734
  }
735
736
  private Scene createScene() {
737
    final SplitPane splitPane = new SplitPane(
738
        getDefinitionPane().getNode(),
739
        getFileEditorPane().getNode(),
740
        getPreviewPane().getNode() );
741
742
    splitPane.setDividerPositions(
743
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
744
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
745
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
746
747
    getDefinitionPane().prefHeightProperty()
748
                       .bind( splitPane.heightProperty() );
749
750
    final BorderPane borderPane = new BorderPane();
751
    borderPane.setPrefSize( 1024, 800 );
752
    borderPane.setTop( createMenuBar() );
753
    borderPane.setBottom( getStatusBar() );
754
    borderPane.setCenter( splitPane );
755
756
    final VBox statusBar = new VBox();
757
    statusBar.setAlignment( Pos.BASELINE_CENTER );
758
    statusBar.getChildren().add( getLineNumberText() );
759
    getStatusBar().getRightItems().add( statusBar );
760
761
    // Force preview pane refresh on Windows.
762
    splitPane.getDividers().get( 1 ).positionProperty().addListener(
763
        ( l, oValue, nValue ) -> runLater(
769764
            () -> {
770765
              if( SystemUtils.IS_OS_WINDOWS ) {
M src/main/java/com/scrivenvar/Messages.java
11
/*
2
 * Copyright (c) 2016 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3737
 * Recursively resolves message properties. Property values can refer to other
3838
 * properties using a <code>${var}</code> syntax.
39
 *
40
 * @author Karl Tauber, Dave Jarvis
4139
 */
4240
public class Messages {
M src/main/java/com/scrivenvar/ScrollEventHandler.java
177177
178178
  private boolean isEnabled() {
179
    // As a minor optimization, when this is set to false, it could remove
179
    // TODO: As a minor optimization, when this is set to false, it could remove
180180
    // the MouseHandler and ScrollHandler so that events only dispatch to one
181181
    // object (instead of one per editor tab).
M src/main/java/com/scrivenvar/Services.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3535
 * Responsible for loading services. The services are treated as singleton
3636
 * instances.
37
 *
38
 * @author White Magic Software, Ltd.
3937
 */
4038
public class Services {
M src/main/java/com/scrivenvar/controls/BrowseFileButton.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
33
 * All rights reserved.
44
 *
...
4949
 * Button that opens a file chooser to select a local file for a URL in
5050
 * markdown.
51
 *
52
 * @author Karl Tauber
5351
 */
5452
public class BrowseFileButton extends Button {
M src/main/java/com/scrivenvar/controls/EscapeTextField.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3535
/**
3636
 * TextField that can escape/unescape characters for markdown.
37
 *
38
 * @author Karl Tauber and White Magic Software, Ltd.
3937
 */
4038
public class EscapeTextField extends TextField {
M src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Brackets variable names with <code>`r#</code> and <code>`</code>.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public class RVariableDecorator implements VariableDecorator {
M src/main/java/com/scrivenvar/decorators/VariableDecorator.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
M src/main/java/com/scrivenvar/decorators/YamlVariableDecorator.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Brackets variable names with dollar symbols.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public class YamlVariableDecorator implements VariableDecorator {
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4343
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
4444
 * database.
45
 *
46
 * @author White Magic Software, Ltd.
4745
 */
4846
public class DefinitionFactory extends AbstractFileFactory {
M src/main/java/com/scrivenvar/definition/DefinitionPane.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
5656
 * allows users to interact with key/value pairs loaded from the
5757
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
58
 *
59
 * @author White Magic Software, Ltd.
6058
 */
6159
public final class DefinitionPane extends TitledPane {
M src/main/java/com/scrivenvar/definition/FindMode.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
M src/main/java/com/scrivenvar/definition/MapInterpolator.java
3636
 * in a map. The values in the map can use a delimited syntax to refer to
3737
 * keys in the map.
38
 *
39
 * @author White Magic Software, Ltd.
4038
 */
4139
public class MapInterpolator {
M src/main/java/com/scrivenvar/definition/RootTreeItem.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4343
 *
4444
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 * @author White Magic Software, Ltd.
4645
 */
4746
public class RootTreeItem<T> extends VariableTreeItem<T> {
M src/main/java/com/scrivenvar/definition/TreeAdapter.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3636
 * Responsible for converting an object hierarchy into a {@link TreeItem}
3737
 * hierarchy.
38
 *
39
 * @author White Magic Software, Ltd.
4038
 */
4139
public interface TreeAdapter {
M src/main/java/com/scrivenvar/definition/TreeItemAdapter.java
5858
 * Reloading the definition file would work, but has a number of drawbacks.
5959
 * </p>
60
 *
61
 * @author White Magic Software, Ltd.
6260
 */
6361
public class TreeItemAdapter {
M src/main/java/com/scrivenvar/definition/VariableTreeItem.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4040
 *
4141
 * @param <T> The type of TreeItem (usually String).
42
 * @author White Magic Software, Ltd.
4342
 */
4443
public class VariableTreeItem<T> extends TreeItem<T> {
M src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java
3535
/**
3636
 * Represents a definition data source for YAML files.
37
 *
38
 * @author White Magic Software, Ltd.
3937
 */
4038
public class YamlDefinitionSource implements DefinitionSource {
M src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4242
/**
4343
 * Responsible for reading a YAML document into an object hierarchy.
44
 *
45
 * @author White Magic Software, Ltd.
4644
 */
4745
public class YamlParser implements DocumentParser<JsonNode> {
M src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4444
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
4545
 * interface and vice-versa.
46
 *
47
 * @author White Magic Software, Ltd.
4846
 */
4947
public class YamlTreeAdapter implements TreeAdapter {
M src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
3838
 * Superclass that abstracts common behaviours for all dialogs.
3939
 *
40
 * @author White Magic Software, Ltd.
4140
 * @param <T> The type of dialog to create (usually String).
4241
 */
M src/main/java/com/scrivenvar/dialogs/ImageDialog.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
33
 * All rights reserved.
44
 *
...
4545
/**
4646
 * Dialog to enter a markdown image.
47
 *
48
 * @author Karl Tauber
4947
 */
5048
public class ImageDialog extends AbstractDialog<String> {
M src/main/java/com/scrivenvar/dialogs/LinkDialog.java
4545
/**
4646
 * Dialog to enter a markdown link.
47
 *
48
 * @author Karl Tauber
4947
 */
5048
public class LinkDialog extends AbstractDialog<String> {
M src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
11
/*
2
 * Copyright 2017 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4141
 * Responsible for managing the R startup script that is run when an R source
4242
 * file is loaded.
43
 *
44
 * @author White Magic Software, Ltd.
4543
 */
4644
public class RScriptDialog extends AbstractDialog<String> {
M src/main/java/com/scrivenvar/editors/EditorPane.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
2828
package com.scrivenvar.editors;
2929
30
import javafx.application.Platform;
3130
import javafx.beans.property.ObjectProperty;
3231
import javafx.beans.property.SimpleObjectProperty;
...
4241
4342
import java.nio.file.Path;
43
import java.util.Random;
4444
import java.util.function.Consumer;
4545
46
import static javafx.application.Platform.runLater;
4647
import static org.fxmisc.wellbehaved.event.InputMap.consume;
4748
4849
/**
4950
 * Represents common editing features for various types of text editors.
50
 *
51
 * @author White Magic Software, Ltd.
5251
 */
5352
public class EditorPane extends Pane {
...
6564
  @Override
6665
  public void requestFocus() {
67
    Platform.runLater( () -> getEditor().requestFocus() );
66
    runLater( () -> getEditor().requestFocus() );
6867
  }
6968
M src/main/java/com/scrivenvar/editors/VariableNameDecoratorFactory.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3737
 * Responsible for creating a variable name decorator suited to a particular
3838
 * file type.
39
 *
40
 * @author White Magic Software, Ltd.
4139
 */
4240
public class VariableNameDecoratorFactory extends AbstractFileFactory {
M src/main/java/com/scrivenvar/editors/VariableNameInjector.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4747
/**
4848
 * Provides the logic for injecting variable names within the editor.
49
 *
50
 * @author White Magic Software, Ltd.
5149
 */
5250
public final class VariableNameInjector {
M src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * Represents the model for a hyperlink: text and url text.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public class HyperlinkModel {
M src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3434
3535
/**
36
 * @author White Magic Software, Ltd.
36
 * Visits hyperlinks in a document so that the user can edit the hyperlink
37
 * within a dialog.
3738
 */
3839
public class LinkVisitor {
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
11
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
5555
/**
5656
 * Markdown editor pane.
57
 *
58
 * @author Karl Tauber and White Magic Software, Ltd.
5957
 */
6058
public class MarkdownEditorPane extends EditorPane {
M src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3737
 * Responsible for testing whether a given path (to a file) matches one of the
3838
 * filename extension patterns provided during construction.
39
 *
40
 * @author White Magic Software, Ltd.
4139
 */
4240
public class FileTypePredicate implements Predicate<File> {
M src/main/java/com/scrivenvar/predicates/strings/ContainsPredicate.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Determines if one string contains another.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public class ContainsPredicate extends StringPredicate {
M src/main/java/com/scrivenvar/predicates/strings/StartsPredicate.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Determines if a string starts with another.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public class StartsPredicate extends StringPredicate {
M src/main/java/com/scrivenvar/predicates/strings/StringPredicate.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * General predicate for different types of string comparisons.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public abstract class StringPredicate implements Predicate<String> {
M src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
11
/*
22
 * {{{ header & license
3
 * Copyright (c) 2006 Patrick Wright
4
 * Copyright (c) 2007 Wisconsin Court System
3
 * Copyright 2006 Patrick Wright
4
 * Copyright 2007 Wisconsin Court System
55
 *
66
 * This program is free software; you can redistribute it and/or
A src/main/java/com/scrivenvar/preview/CustomImageResourceLoader.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import javafx.beans.property.IntegerProperty;
31
import javafx.beans.property.SimpleIntegerProperty;
32
import org.xhtmlrenderer.extend.FSImage;
33
import org.xhtmlrenderer.resource.ImageResource;
34
import org.xhtmlrenderer.swing.ImageResourceLoader;
35
36
import java.net.URI;
37
import java.nio.file.Files;
38
import java.nio.file.Paths;
39
40
import static com.scrivenvar.preview.SVGRasterizer.PLACEHOLDER_IMAGE;
41
import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
42
43
/**
44
 * Responsible for loading images. If the image cannot be found, a placeholder
45
 * is used instead.
46
 */
47
public class CustomImageResourceLoader extends ImageResourceLoader {
48
  /**
49
   * Placeholder that's displayed when image cannot be found.
50
   */
51
  private static final FSImage FS_PLACEHOLDER_IMAGE =
52
      createImage( PLACEHOLDER_IMAGE );
53
54
  private final IntegerProperty mMaxWidthProperty = new SimpleIntegerProperty();
55
56
  public CustomImageResourceLoader() {
57
  }
58
59
  public IntegerProperty widthProperty() {
60
    return mMaxWidthProperty;
61
  }
62
63
  @Override
64
  public synchronized ImageResource get(
65
      final String uri, final int width, final int height ) {
66
    assert uri != null;
67
    assert width >= 0;
68
    assert height >= 0;
69
70
    boolean exists;
71
72
    try {
73
      exists = Files.exists( Paths.get( new URI( uri ) ) );
74
    } catch( final Exception e ) {
75
      exists = false;
76
    }
77
78
    return exists
79
        ? scale( uri, width, height )
80
        : new ImageResource( uri, FS_PLACEHOLDER_IMAGE );
81
  }
82
83
  /**
84
   * Scales the image found at the given uri.
85
   *
86
   * @param uri Path to the image file to load.
87
   * @param w   Unused (usually -1, which is useless).
88
   * @param h   Unused (ditto).
89
   * @return Resource representing the rendered image and path.
90
   */
91
  private ImageResource scale( final String uri, final int w, final int h ) {
92
    final var ir = super.get( uri, w, h );
93
    final var image = ir.getImage();
94
    final var imageWidth = image.getWidth();
95
    final var imageHeight = image.getHeight();
96
97
    int maxWidth = mMaxWidthProperty.get();
98
    int newWidth = imageWidth;
99
    int newHeight = imageHeight;
100
101
    // Maintain aspect ratio while shrinking image to view port bounds.
102
    if( imageWidth > maxWidth ) {
103
      newWidth = maxWidth;
104
      newHeight = (newWidth * imageHeight) / imageWidth;
105
    }
106
107
    image.scale( newWidth, newHeight );
108
    return ir;
109
  }
110
}
1111
M src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
4747
import javax.swing.*;
4848
import java.awt.*;
49
import java.awt.event.ComponentEvent;
50
import java.awt.event.ComponentListener;
4951
import java.nio.file.Path;
5052
5153
import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
5254
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
55
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
5356
5457
/**
5558
 * HTML preview pane is responsible for rendering an HTML document.
56
 *
57
 * @author Karl Tauber and White Magic Software, Ltd.
5859
 */
5960
public final class HTMLPreviewPane extends Pane {
...
105106
  private final static String HTML_FOOTER = "</body></html>";
106107
107
  private final StringBuilder mHtml = new StringBuilder( 65536 );
108
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
108109
  private final int mHtmlPrefixLength;
109110
110111
  private final W3CDom mW3cDom = new W3CDom();
111112
  private final XhtmlNamespaceHandler mNamespaceHandler =
112113
      new XhtmlNamespaceHandler();
113
  private final HTMLPanel mRenderer = new HTMLPanel();
114
  private final HTMLPanel mHtmlRenderer = new HTMLPanel();
114115
  private final SwingNode mSwingNode = new SwingNode();
115
  private final JScrollPane mScrollPane = new JScrollPane( mRenderer );
116
  private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
116117
  private final DocumentEventHandler mDocumentHandler =
117118
      new DocumentEventHandler();
119
  private final CustomImageResourceLoader mImageLoader =
120
      new CustomImageResourceLoader();
118121
119122
  private Path mPath;
120123
121124
  /**
122125
   * Creates a new preview pane that can scroll to the caret position within the
123126
   * document.
124127
   */
125128
  public HTMLPreviewPane() {
129
    mHtmlDocument.append( HTML_HEADER );
130
    mHtmlPrefixLength = mHtmlDocument.length();
131
126132
    final var factory = new ChainedReplacedElementFactory();
127133
    factory.addFactory( new SVGReplacedElementFactory() );
128
    factory.addFactory( new SwingReplacedElementFactory() );
134
    factory.addFactory( new SwingReplacedElementFactory(
135
        NO_OP_REPAINT_LISTENER, mImageLoader ) );
129136
130137
    final var context = getSharedContext();
131138
    context.setReplacedElementFactory( factory );
132139
    context.getTextRenderer().setSmoothingThreshold( 0 );
133140
134141
    mSwingNode.setContent( mScrollPane );
135142
136
    mHtml.append( HTML_HEADER );
137
    mHtmlPrefixLength = mHtml.length();
143
    mHtmlRenderer.addDocumentListener( mDocumentHandler );
144
    setStyle( "-fx-background-color: white;" );
138145
139
    mRenderer.addDocumentListener( mDocumentHandler );
140
    setStyle("-fx-background-color: white;");
146
    mHtmlRenderer.addComponentListener( new ComponentListener() {
147
      @Override
148
      public void componentResized( final ComponentEvent e ) {
149
        // Scaling a bit below the full width prevents the horizontal scrollbar
150
        // from appearing.
151
        final int width = (int) (e.getComponent().getWidth() * .95);
152
        mImageLoader.widthProperty().set( width );
153
      }
154
155
      @Override
156
      public void componentMoved( final ComponentEvent e ) {
157
      }
158
159
      @Override
160
      public void componentShown( final ComponentEvent e ) {
161
      }
162
163
      @Override
164
      public void componentHidden( final ComponentEvent e ) {
165
      }
166
    } );
141167
  }
142168
...
151177
    final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
152178
153
    mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
179
    mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
154180
  }
155181
...
226252
227253
  private void scrollTo( final Point point ) {
228
    mRenderer.scrollTo( point );
254
    mHtmlRenderer.scrollTo( point );
229255
  }
230256
...
242268
243269
  private void scrollToBottom() {
244
    scrollToY( mRenderer.getHeight() );
270
    scrollToY( mHtmlRenderer.getHeight() );
245271
  }
246272
247273
  private Box getBoxById( final String id ) {
248274
    return getSharedContext().getBoxById( id );
249275
  }
250276
251277
  private String decorate( final String html ) {
252278
    // Trim the HTML back to the header.
253
    mHtml.setLength( mHtmlPrefixLength );
279
    mHtmlDocument.setLength( mHtmlPrefixLength );
254280
255281
    // Write the HTML body element followed by closing tags.
256
    return mHtml.append( html )
257
                .append( HTML_FOOTER )
258
                .toString();
282
    return mHtmlDocument.append( html )
283
                        .append( HTML_FOOTER )
284
                        .toString();
259285
  }
260286
...
307333
308334
    if( !box.getStyle().isInline() ) {
309
      final var margin = box.getMargin( mRenderer.getLayoutContext() );
335
      final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
310336
      x += margin.left();
311337
      y += margin.top();
...
323349
324350
  private SharedContext getSharedContext() {
325
    return mRenderer.getSharedContext();
351
    return mHtmlRenderer.getSharedContext();
326352
  }
327353
}
M src/main/java/com/scrivenvar/preview/SVGRasterizer.java
2828
package com.scrivenvar.preview;
2929
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
3032
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
3133
import org.apache.batik.gvt.renderer.ImageRenderer;
...
5153
5254
public class SVGRasterizer {
55
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
56
5357
  private final static SAXSVGDocumentFactory mFactory =
5458
      new SAXSVGDocumentFactory( getXMLParserClassName() );
5559
56
  private final static Map<Object, Object> RENDERING_HINTS = Map.of(
57
      KEY_ALPHA_INTERPOLATION,
58
      VALUE_ALPHA_INTERPOLATION_QUALITY,
59
      KEY_INTERPOLATION,
60
      VALUE_INTERPOLATION_BICUBIC,
60
  public final static Map<Object, Object> RENDERING_HINTS = Map.of(
6161
      KEY_ANTIALIASING,
6262
      VALUE_ANTIALIAS_ON,
63
      KEY_ALPHA_INTERPOLATION,
64
      VALUE_ALPHA_INTERPOLATION_QUALITY,
6365
      KEY_COLOR_RENDERING,
6466
      VALUE_COLOR_RENDER_QUALITY,
6567
      KEY_DITHERING,
6668
      VALUE_DITHER_DISABLE,
69
      KEY_FRACTIONALMETRICS,
70
      VALUE_FRACTIONALMETRICS_ON,
71
      KEY_INTERPOLATION,
72
      VALUE_INTERPOLATION_BICUBIC,
6773
      KEY_RENDERING,
6874
      VALUE_RENDER_QUALITY,
6975
      KEY_STROKE_CONTROL,
7076
      VALUE_STROKE_PURE,
71
      KEY_FRACTIONALMETRICS,
72
      VALUE_FRACTIONALMETRICS_ON,
7377
      KEY_TEXT_ANTIALIASING,
74
      VALUE_TEXT_ANTIALIAS_OFF
78
      VALUE_TEXT_ANTIALIAS_ON
7579
  );
80
81
  public final static BufferedImage PLACEHOLDER_IMAGE;
82
83
  static {
84
    final int w = 150;
85
    final int h = 150;
86
    final var image = new BufferedImage( w, h, TYPE_INT_RGB );
87
    final var graphics = (Graphics2D) image.getGraphics();
88
    graphics.setRenderingHints( RENDERING_HINTS );
89
90
    graphics.setColor( new Color( 204, 204, 204 ) );
91
    graphics.fillRect( 0, 0, w, h );
92
    graphics.setColor( new Color( 255, 204, 204 ) );
93
    graphics.setStroke( new BasicStroke( 4 ) );
94
    graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
95
    graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
96
                       h / 4 + (int) (w / 4 / Math.PI),
97
                       w / 2 + w / 4 - (int) (w / 4 / Math.PI),
98
                       h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
99
    PLACEHOLDER_IMAGE = image;
100
  }
76101
77102
  private static class BufferedImageTranscoder extends ImageTranscoder {
...
119144
      return rasterize( new URL( url ), width );
120145
    } catch( final Exception e ) {
121
      return createPlaceholderImage( width );
146
      NOTIFIER.notify( e );
147
      return PLACEHOLDER_IMAGE;
122148
    }
123149
  }
...
151177
152178
    return transcoder.getImage();
153
  }
154
155
  @SuppressWarnings("SuspiciousNameCombination")
156
  private static Image createPlaceholderImage( final int width ) {
157
    final var image = new BufferedImage( width, width, TYPE_INT_RGB );
158
    final var graphics = (Graphics2D) image.getGraphics();
159
160
    graphics.setColor( RED );
161
    graphics.setStroke( new BasicStroke( 5 ) );
162
    graphics.drawOval( 5, 5, width / 2, width / 2 );
163
164
    return image;
165179
  }
166180
}
M src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
6464
6565
  /**
66
   * Where to put document inline evaluated R expressions.
66
   * Where to put cached image files.
6767
   */
6868
  private final Map<String, Image> mImageCache = new LinkedHashMap<>() {
6969
    @Override
7070
    protected boolean removeEldestEntry(
7171
        final Map.Entry<String, Image> eldest ) {
7272
      return size() > MAX_CACHED_IMAGES;
7373
    }
7474
  };
7575
76
  @Override
7677
  public ReplacedElement createReplacedElement(
7778
      final LayoutContext c,
M src/main/java/com/scrivenvar/processors/AbstractProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3434
 * sub-chains.
3535
 *
36
 * @author White Magic Software, Ltd.
3736
 * @param <T> The type of object to process.
3837
 */
M src/main/java/com/scrivenvar/processors/DefinitionProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3636
 * their values into the post-processed text. The default variable syntax is
3737
 * {@code $variable$}.
38
 *
39
 * @author White Magic Software, Ltd.
4038
 */
4139
public class DefinitionProcessor extends AbstractProcessor<String> {
M src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3636
 * final HTML preview is rendered. This should be the last link in the processor
3737
 * chain.
38
 *
39
 * @author White Magic Software, Ltd.
4038
 */
4139
public class HTMLPreviewProcessor extends AbstractProcessor<String> {
M src/main/java/com/scrivenvar/processors/IdentityProcessor.java
3131
 * This is the default processor used when an unknown filename extension is
3232
 * encountered.
33
 *
34
 * @author White Magic Software, Ltd.
3533
 */
3634
public class IdentityProcessor extends AbstractProcessor<String> {
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4949
/**
5050
 * Transforms a document containing R statements into Markdown.
51
 *
52
 * @author White Magic Software, Ltd.
5351
 */
5452
public final class InlineRProcessor extends DefinitionProcessor {
M src/main/java/com/scrivenvar/processors/Processor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
 *
3333
 * @param <T> The type of processor to create.
34
 * @author White Magic Software, Ltd.
3534
 */
3635
public interface Processor<T> {
M src/main/java/com/scrivenvar/processors/ProcessorFactory.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3939
 * Responsible for creating processors capable of parsing, transforming,
4040
 * interpolating, and rendering known file types.
41
 *
42
 * @author White Magic Software, Ltd.
4341
 */
4442
public class ProcessorFactory extends AbstractFileFactory {
M src/main/java/com/scrivenvar/processors/RVariableProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3535
 * performs a substitution on the text. The default R variable syntax is
3636
 * {@code v$tree$leaf}.
37
 *
38
 * @author White Magic Software, Ltd.
3937
 */
4038
public class RVariableProcessor extends DefinitionProcessor {
M src/main/java/com/scrivenvar/processors/XMLProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
5858
 * The XSL must transform the XML document into Markdown, or another format
5959
 * recognized by the next link on the chain.
60
 *
61
 * @author White Magic Software, Ltd.
6260
 */
6361
public class XMLProcessor extends AbstractProcessor<String>
M src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
5353
 * Responsible for ensuring that images can be rendered relative to a path.
5454
 * This allows images to be located virtually anywhere.
55
 *
56
 * @author White Magic Software, Ltd.
5755
 */
5856
public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension {
M src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4949
/**
5050
 * Responsible for parsing a Markdown document and rendering it as HTML.
51
 *
52
 * @author White Magic Software, Ltd.
5351
 */
5452
public class MarkdownProcessor extends AbstractProcessor<String> {
M src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
11
/*
2
 * The MIT License
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
4
 * Copyright 2016 .
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.processors.text;
2529
2630
import java.util.Map;
2731
2832
/**
2933
 * Responsible for common behaviour across all text replacer implementations.
30
 *
31
 * @author White Magic Software, Ltd.
3234
 */
3335
public abstract class AbstractTextReplacer implements TextReplacer {
M src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3535
/**
3636
 * Replaces text using an Aho-Corasick algorithm.
37
 *
38
 * @author White Magic Software, Ltd.
3937
 */
4038
public class AhoCorasickReplacer extends AbstractTextReplacer {
M src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3434
/**
3535
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 *
37
 * @author White Magic Software, Ltd.
3836
 */
3937
public class StringUtilsReplacer extends AbstractTextReplacer {
M src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3333
 * Used to generate a class capable of efficiently replacing variable
3434
 * definitions with their values.
35
 *
36
 * @author White Magic Software, Ltd.
3735
 */
3836
public final class TextReplacementFactory {
M src/main/java/com/scrivenvar/processors/text/TextReplacer.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * Defines the ability to replace text given a set of keys and values.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public interface TextReplacer {
M src/main/java/com/scrivenvar/service/Options.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3535
/**
3636
 * Responsible for persisting options.
37
 *
38
 * @author White Magic Software, Ltd.
3937
 */
4038
public interface Options extends Service {
M src/main/java/com/scrivenvar/service/Service.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * All services inherit from this one.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public interface Service {
M src/main/java/com/scrivenvar/service/Settings.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3333
/**
3434
 * Defines how settings and options can be retrieved.
35
 *
36
 * @author White Magic Software, Ltd.
3735
 */
3836
public interface Settings extends Service {
M src/main/java/com/scrivenvar/service/Snitch.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3434
/**
3535
 * Listens for changes to file system files and directories.
36
 *
37
 * @author White Magic Software, Ltd.
3836
 */
3937
public interface Snitch extends Service, Runnable {
M src/main/java/com/scrivenvar/service/events/Notification.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3030
/**
3131
 * Represents a message that contains a title and content.
32
 *
33
 * @author White Magic Software, Ltd.
3432
 */
3533
public interface Notification {
M src/main/java/com/scrivenvar/service/events/Notifier.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4040
/**
4141
 * Provides the application with a uniform way to notify the user of events.
42
 *
43
 * @author White Magic Software, Ltd.
4442
 */
4543
public interface Notifier {
M src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3838
 * Ensures a consistent button order for alert dialogs across platforms (because
3939
 * the default button order on Linux defies all logic).
40
 *
41
 * @author White Magic Software, Ltd.
4240
 */
4341
public class ButtonOrderPane extends DialogPane {
M src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3333
3434
/**
35
 * @author White Magic Software, Ltd.
35
 * Responsible for alerting the user to prominent information.
3636
 */
3737
public class DefaultNotification implements Notification {
M src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4646
/**
4747
 * Provides the ability to notify the user of problems.
48
 *
49
 * @author White Magic Software, Ltd.
5048
 */
5149
public final class DefaultNotifier extends Observable implements Notifier {
M src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3939
/**
4040
 * Persistent options user can change at runtime.
41
 *
42
 * @author Karl Tauber and White Magic Software, Ltd.
4341
 */
4442
public class DefaultOptions implements Options {
M src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4646
/**
4747
 * Responsible for loading settings that help avoid hard-coded assumptions.
48
 *
49
 * @author White Magic Software, Ltd.
5048
 */
5149
public class DefaultSettings implements Settings {
M src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
4444
 * Listens for file changes. Other classes can register paths to be monitored
4545
 * and listen for changes to those paths.
46
 *
47
 * @author White Magic Software, Ltd.
4846
 */
4947
public class DefaultSnitch extends Observable implements Snitch {
M src/main/java/com/scrivenvar/util/Action.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber and White Magic Software, Ltd.
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3535
/**
3636
 * Simple action class
37
 *
38
 * @author Karl Tauber
39
 * @author White Magic Software, Ltd.
4037
 */
4138
public class Action {
M src/main/java/com/scrivenvar/util/ActionUtils.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3838
3939
/**
40
 * Action utilities
41
 *
42
 * @author Karl Tauber
43
 * @author White Magic Software, Ltd.
40
 * Responsible for creating menu items and toolbar buttons.
4441
 */
4542
public class ActionUtils {
...
5350
5451
    for( int i = 0; i < actions.length; i++ ) {
55
      menuItems[ i ] = (actions[ i ] != null)
56
          ? createMenuItem( actions[ i ] )
57
          : new SeparatorMenuItem();
52
      menuItems[ i ] = (actions[ i ] == null)
53
          ? new SeparatorMenuItem()
54
          : createMenuItem( actions[ i ] );
5855
    }
5956
M src/main/java/com/scrivenvar/util/Lists.java
11
/*
2
 * Copyright 2016 White Magic Software, Ltd.
2
 * Copyright 2020 White Magic Software, Ltd.
33
 *
44
 * All rights reserved.
...
3232
/**
3333
 * Convenience class that provides a clearer API for obtaining list elements.
34
 *
35
 * @author White Magic Software, Ltd.
3634
 */
3735
public final class Lists {
M src/main/java/com/scrivenvar/util/StageState.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3636
/**
3737
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 *
39
 * @author Karl Tauber
4038
 */
4139
public class StageState {
M src/main/java/com/scrivenvar/util/Utils.java
11
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
33
 * All rights reserved.
44
 *
...
3131
3232
/**
33
 * @author Karl Tauber and White Magic Software, Ltd.
33
 * Responsible for trimming, storing, and retrieving strings.
3434
 */
3535
public class Utils {
M src/main/resources/com/scrivenvar/preview/webview.css
168168
}
169169
170
dl dd {
171
  *float: none;
172
  *width: auto;
173
  *margin-left: 20%;
174
}
175
176170
/* CODE
177171
=============================================================================*/
...
199193
  border: .125em solid #ccc;
200194
  line-height: 1.6;
201
  overflow: auto;
202195
  padding: .25em .5em;
203196
  border-radius: .25em;
...
211204
kbd {
212205
  background-color: #ccc;
213
  background-image: linear-gradient(#f8f8f8, #DDDDDD);
214206
  background-repeat: repeat-x;
215207
  border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
216
  border-image: none;
217208
  border-radius: 2px;
218209
  border-style: solid;